@funnelchat-hq/sandbox 0.1.0 → 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.
@@ -4,6 +4,7 @@ import { createHmac, randomUUID, randomBytes, createHash } from 'crypto';
4
4
  import { Ajv2020 } from 'ajv/dist/2020.js';
5
5
  import addFormatsModule from 'ajv-formats';
6
6
  import { createServer } from 'http';
7
+ import { deflateSync } from 'zlib';
7
8
 
8
9
  // src/config.ts
9
10
  function isoSeconds(date) {
@@ -81,6 +82,8 @@ function defaultConfig() {
81
82
  rules: { window: true, antiblock: true, rateLimit: true },
82
83
  templateApprovalSeconds: 5,
83
84
  subscriptions: [],
85
+ numbers: { limit: 5, included: true, trial: false, metaConnect: false, linksPerMinute: 10 },
86
+ publicUrl: null,
84
87
  historyLimit: 200,
85
88
  log: true
86
89
  };
@@ -157,6 +160,24 @@ function mergeConfig(base, input) {
157
160
  return structuredClone(s);
158
161
  });
159
162
  }
163
+ if (input.numbers !== void 0) {
164
+ const n = input.numbers;
165
+ if (!isRecord(n)) throw new ConfigError("numbers must be an object {limit, included, trial, metaConnect, linksPerMinute}.");
166
+ for (const key of ["limit", "linksPerMinute"]) {
167
+ if (n[key] === void 0) continue;
168
+ if (!Number.isInteger(n[key]) || n[key] < 0) throw new ConfigError(`numbers.${key} must be an integer, 0 or more.`);
169
+ out.numbers[key] = n[key];
170
+ }
171
+ for (const key of ["included", "trial", "metaConnect"]) {
172
+ if (n[key] !== void 0) out.numbers[key] = Boolean(n[key]);
173
+ }
174
+ }
175
+ if (input.publicUrl !== void 0) {
176
+ if (input.publicUrl !== null && (typeof input.publicUrl !== "string" || !/^https?:\/\/[^/]+/.test(input.publicUrl))) {
177
+ throw new ConfigError("publicUrl must be an http(s) URL, like https://sandbox.example.com, or null.");
178
+ }
179
+ out.publicUrl = input.publicUrl === null ? null : input.publicUrl.replace(/\/+$/, "");
180
+ }
160
181
  if (input.historyLimit !== void 0) {
161
182
  if (!Number.isInteger(input.historyLimit) || input.historyLimit < 1) {
162
183
  throw new ConfigError("historyLimit must be an integer greater than 0.");
@@ -229,6 +250,9 @@ function parseArgs(argv) {
229
250
  case "--host":
230
251
  o.host = value();
231
252
  break;
253
+ case "--public-url":
254
+ o.publicUrl = value();
255
+ break;
232
256
  case "--status-delays": {
233
257
  const [sent, delivered, read] = parseSecondsList(value(), "--status-delays", true);
234
258
  if (sent === null || sent === void 0) throw new ConfigError("--status-delays: the first one (sent) must be a number.");
@@ -260,6 +284,9 @@ function parseArgs(argv) {
260
284
  o.templateApprovalSeconds = n ?? null;
261
285
  break;
262
286
  }
287
+ case "--meta-connect":
288
+ o.numbers = { ...o.numbers, metaConnect: true };
289
+ break;
263
290
  case "-q":
264
291
  case "--quiet":
265
292
  o.log = false;
@@ -17489,42 +17516,6 @@ var openapi_default = {
17489
17516
  "x-fc-proposito": "Usalo para listar los grupos de tu cuenta con filtros por tipo, estado, n\xFAmero o sin campa\xF1a asignada, por ejemplo para sincronizarlos con tu sistema.",
17490
17517
  "x-fc-domain": "communities",
17491
17518
  "x-fc-permission": "communities:read"
17492
- },
17493
- post: {
17494
- operationId: "pendingSandboxGroupActions.groups.post",
17495
- summary: "Group action by API (off)",
17496
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
17497
- tags: [
17498
- "GroupActions"
17499
- ],
17500
- "x-fc-domain": "communities",
17501
- "x-fc-permission": "communities:operate",
17502
- "x-fc-pending": "communities-group-actions",
17503
- responses: {
17504
- "403": {
17505
- description: "route_not_available: the group actions by API are off in production.",
17506
- content: {
17507
- "application/json": {
17508
- schema: {
17509
- type: "object",
17510
- properties: {
17511
- error: {
17512
- type: "string",
17513
- const: "route_not_available"
17514
- },
17515
- message: {
17516
- type: "string"
17517
- }
17518
- },
17519
- required: [
17520
- "error",
17521
- "message"
17522
- ]
17523
- }
17524
- }
17525
- }
17526
- }
17527
- }
17528
17519
  }
17529
17520
  },
17530
17521
  "/api/v1/devices/{device}/groups": {
@@ -18602,104 +18593,281 @@ var openapi_default = {
18602
18593
  "x-fc-proposito": "Sirve para ver los mensajes que se enviaron a un grupo y de qu\xE9 env\xEDo programado sali\xF3 cada uno.",
18603
18594
  "x-fc-domain": "communities",
18604
18595
  "x-fc-permission": "communities:read"
18605
- },
18606
- post: {
18607
- operationId: "pendingSandboxGroupActions.groups.group.messages.post",
18608
- summary: "Group action by API (off)",
18609
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
18596
+ }
18597
+ },
18598
+ "/api/v1/groups/{group}/scheduled-messages": {
18599
+ get: {
18600
+ operationId: "group.scheduledMessages",
18601
+ description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `id`, `name`, `created_at`, `updated_at`; direcci\xF3n `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.",
18602
+ summary: "Listar los env\xEDos programados que incluyen un grupo",
18610
18603
  tags: [
18611
- "GroupActions"
18604
+ "Grupos"
18612
18605
  ],
18613
- "x-fc-domain": "communities",
18614
- "x-fc-permission": "communities:operate",
18615
- "x-fc-pending": "communities-group-actions",
18616
18606
  parameters: [
18617
18607
  {
18618
18608
  name: "group",
18619
18609
  in: "path",
18620
18610
  required: true,
18611
+ description: "The group uid",
18612
+ schema: {
18613
+ type: "string"
18614
+ }
18615
+ },
18616
+ {
18617
+ name: "name",
18618
+ in: "query",
18619
+ schema: {
18620
+ type: "string"
18621
+ }
18622
+ },
18623
+ {
18624
+ name: "orderBy",
18625
+ in: "query",
18626
+ schema: {
18627
+ type: "string"
18628
+ }
18629
+ },
18630
+ {
18631
+ name: "orderByMethod",
18632
+ in: "query",
18621
18633
  schema: {
18622
18634
  type: "string"
18623
18635
  }
18636
+ },
18637
+ {
18638
+ name: "perPage",
18639
+ in: "query",
18640
+ schema: {
18641
+ type: "integer"
18642
+ }
18624
18643
  }
18625
18644
  ],
18626
18645
  responses: {
18627
- "403": {
18628
- description: "route_not_available: the group actions by API are off in production.",
18646
+ "200": {
18647
+ description: "Paginated set of `ScheduledMessageResource`",
18629
18648
  content: {
18630
18649
  "application/json": {
18631
18650
  schema: {
18632
18651
  type: "object",
18633
18652
  properties: {
18634
- error: {
18635
- type: "string",
18636
- const: "route_not_available"
18653
+ data: {
18654
+ type: "array",
18655
+ items: {
18656
+ $ref: "#/components/schemas/ScheduledMessageResource"
18657
+ }
18637
18658
  },
18638
- message: {
18639
- type: "string"
18659
+ links: {
18660
+ type: "object",
18661
+ properties: {
18662
+ first: {
18663
+ type: [
18664
+ "string",
18665
+ "null"
18666
+ ]
18667
+ },
18668
+ last: {
18669
+ type: [
18670
+ "string",
18671
+ "null"
18672
+ ]
18673
+ },
18674
+ prev: {
18675
+ type: [
18676
+ "string",
18677
+ "null"
18678
+ ]
18679
+ },
18680
+ next: {
18681
+ type: [
18682
+ "string",
18683
+ "null"
18684
+ ]
18685
+ }
18686
+ },
18687
+ required: [
18688
+ "first",
18689
+ "last",
18690
+ "prev",
18691
+ "next"
18692
+ ]
18693
+ },
18694
+ meta: {
18695
+ type: "object",
18696
+ properties: {
18697
+ current_page: {
18698
+ type: "integer",
18699
+ minimum: 1
18700
+ },
18701
+ from: {
18702
+ type: [
18703
+ "integer",
18704
+ "null"
18705
+ ],
18706
+ minimum: 1
18707
+ },
18708
+ last_page: {
18709
+ type: "integer",
18710
+ minimum: 1
18711
+ },
18712
+ links: {
18713
+ type: "array",
18714
+ description: "Generated paginator links.",
18715
+ items: {
18716
+ type: "object",
18717
+ properties: {
18718
+ url: {
18719
+ type: [
18720
+ "string",
18721
+ "null"
18722
+ ]
18723
+ },
18724
+ label: {
18725
+ type: "string"
18726
+ },
18727
+ active: {
18728
+ type: "boolean"
18729
+ }
18730
+ },
18731
+ required: [
18732
+ "url",
18733
+ "label",
18734
+ "active"
18735
+ ]
18736
+ }
18737
+ },
18738
+ path: {
18739
+ type: [
18740
+ "string",
18741
+ "null"
18742
+ ],
18743
+ description: "Base path for paginator generated URLs."
18744
+ },
18745
+ per_page: {
18746
+ type: "integer",
18747
+ description: "Number of items shown per page.",
18748
+ minimum: 0
18749
+ },
18750
+ to: {
18751
+ type: [
18752
+ "integer",
18753
+ "null"
18754
+ ],
18755
+ description: "Number of the last item in the slice.",
18756
+ minimum: 1
18757
+ },
18758
+ total: {
18759
+ type: "integer",
18760
+ description: "Total number of items being paginated.",
18761
+ minimum: 0
18762
+ }
18763
+ },
18764
+ required: [
18765
+ "current_page",
18766
+ "from",
18767
+ "last_page",
18768
+ "links",
18769
+ "path",
18770
+ "per_page",
18771
+ "to",
18772
+ "total"
18773
+ ]
18640
18774
  }
18641
18775
  },
18642
18776
  required: [
18643
- "error",
18644
- "message"
18777
+ "data",
18778
+ "links",
18779
+ "meta"
18645
18780
  ]
18646
18781
  }
18647
18782
  }
18648
18783
  }
18784
+ },
18785
+ "401": {
18786
+ $ref: "#/components/responses/CommunitiesAuthenticationException"
18787
+ },
18788
+ "403": {
18789
+ $ref: "#/components/responses/CommunitiesAuthorizationException"
18790
+ },
18791
+ "404": {
18792
+ $ref: "#/components/responses/CommunitiesModelNotFoundException"
18793
+ },
18794
+ "422": {
18795
+ $ref: "#/components/responses/CommunitiesValidationException"
18649
18796
  }
18650
- }
18797
+ },
18798
+ "x-fc-proposito": "Usalo para saber qu\xE9 env\xEDos programados incluyen a un grupo, por ejemplo antes de hacerle cambios.",
18799
+ "x-fc-domain": "communities",
18800
+ "x-fc-permission": "communities:read"
18651
18801
  }
18652
18802
  },
18653
- "/api/v1/groups/{group}/scheduled-messages": {
18803
+ "/api/v1/campaigns/{campaign}/participants": {
18654
18804
  get: {
18655
- operationId: "group.scheduledMessages",
18656
- description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `id`, `name`, `created_at`, `updated_at`; direcci\xF3n `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.",
18657
- summary: "Listar los env\xEDos programados que incluyen un grupo",
18805
+ operationId: "campaign.participants",
18806
+ description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `updated_at`, `id`, `created_at`; direcci\xF3n `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.",
18807
+ summary: "Listar los participantes de una campa\xF1a",
18658
18808
  tags: [
18659
- "Grupos"
18809
+ "Participantes"
18660
18810
  ],
18661
18811
  parameters: [
18662
18812
  {
18663
- name: "group",
18813
+ name: "campaign",
18664
18814
  in: "path",
18665
18815
  required: true,
18666
- description: "The group uid",
18816
+ description: "The campaign ID",
18667
18817
  schema: {
18668
- type: "string"
18818
+ type: "integer"
18669
18819
  }
18670
18820
  },
18671
18821
  {
18672
- name: "name",
18822
+ name: "phone",
18673
18823
  in: "query",
18674
18824
  schema: {
18675
- type: "string"
18825
+ type: [
18826
+ "string",
18827
+ "null"
18828
+ ]
18676
18829
  }
18677
18830
  },
18678
18831
  {
18679
18832
  name: "orderBy",
18680
18833
  in: "query",
18681
18834
  schema: {
18682
- type: "string"
18835
+ type: [
18836
+ "string",
18837
+ "null"
18838
+ ]
18683
18839
  }
18684
18840
  },
18685
18841
  {
18686
18842
  name: "orderByMethod",
18687
18843
  in: "query",
18688
18844
  schema: {
18689
- type: "string"
18845
+ type: [
18846
+ "string",
18847
+ "null"
18848
+ ],
18849
+ enum: [
18850
+ "asc",
18851
+ "desc",
18852
+ null
18853
+ ]
18690
18854
  }
18691
18855
  },
18692
18856
  {
18693
18857
  name: "perPage",
18694
18858
  in: "query",
18695
18859
  schema: {
18696
- type: "integer"
18860
+ type: [
18861
+ "integer",
18862
+ "null"
18863
+ ],
18864
+ minimum: 1
18697
18865
  }
18698
18866
  }
18699
18867
  ],
18700
18868
  responses: {
18701
18869
  "200": {
18702
- description: "Paginated set of `ScheduledMessageResource`",
18870
+ description: "Paginated set of `ParticipantResource`",
18703
18871
  content: {
18704
18872
  "application/json": {
18705
18873
  schema: {
@@ -18708,7 +18876,7 @@ var openapi_default = {
18708
18876
  data: {
18709
18877
  type: "array",
18710
18878
  items: {
18711
- $ref: "#/components/schemas/ScheduledMessageResource"
18879
+ $ref: "#/components/schemas/ParticipantResource"
18712
18880
  }
18713
18881
  },
18714
18882
  links: {
@@ -18850,16 +19018,16 @@ var openapi_default = {
18850
19018
  $ref: "#/components/responses/CommunitiesValidationException"
18851
19019
  }
18852
19020
  },
18853
- "x-fc-proposito": "Usalo para saber qu\xE9 env\xEDos programados incluyen a un grupo, por ejemplo antes de hacerle cambios.",
19021
+ "x-fc-proposito": "Sirve para listar los participantes de una campa\xF1a, con b\xFAsqueda por tel\xE9fono y si son administradores.",
18854
19022
  "x-fc-domain": "communities",
18855
19023
  "x-fc-permission": "communities:read"
18856
19024
  }
18857
19025
  },
18858
- "/api/v1/campaigns/{campaign}/participants": {
19026
+ "/api/v1/campaigns/{campaign}/campaign-participants": {
18859
19027
  get: {
18860
- operationId: "campaign.participants",
18861
- description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `updated_at`, `id`, `created_at`; direcci\xF3n `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.",
18862
- summary: "Listar los participantes de una campa\xF1a",
19028
+ operationId: "campaign.campaignParticipants",
19029
+ description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `joined_at`, `left_at`; direcci\xF3n `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.",
19030
+ summary: "Listar los participantes con su grupo actual",
18863
19031
  tags: [
18864
19032
  "Participantes"
18865
19033
  ],
@@ -18874,12 +19042,27 @@ var openapi_default = {
18874
19042
  }
18875
19043
  },
18876
19044
  {
18877
- name: "phone",
19045
+ name: "search",
19046
+ in: "query",
19047
+ schema: {
19048
+ type: [
19049
+ "string",
19050
+ "null"
19051
+ ]
19052
+ }
19053
+ },
19054
+ {
19055
+ name: "status",
18878
19056
  in: "query",
18879
19057
  schema: {
18880
19058
  type: [
18881
19059
  "string",
18882
19060
  "null"
19061
+ ],
19062
+ enum: [
19063
+ "active",
19064
+ "exited",
19065
+ null
18883
19066
  ]
18884
19067
  }
18885
19068
  },
@@ -18890,6 +19073,11 @@ var openapi_default = {
18890
19073
  type: [
18891
19074
  "string",
18892
19075
  "null"
19076
+ ],
19077
+ enum: [
19078
+ "joined_at",
19079
+ "left_at",
19080
+ null
18893
19081
  ]
18894
19082
  }
18895
19083
  },
@@ -18904,6 +19092,8 @@ var openapi_default = {
18904
19092
  enum: [
18905
19093
  "asc",
18906
19094
  "desc",
19095
+ "ASC",
19096
+ "DESC",
18907
19097
  null
18908
19098
  ]
18909
19099
  }
@@ -18918,11 +19108,84 @@ var openapi_default = {
18918
19108
  ],
18919
19109
  minimum: 1
18920
19110
  }
19111
+ },
19112
+ {
19113
+ name: "start_date",
19114
+ in: "query",
19115
+ schema: {
19116
+ type: [
19117
+ "string",
19118
+ "null"
19119
+ ],
19120
+ format: "date-time"
19121
+ }
19122
+ },
19123
+ {
19124
+ name: "end_date",
19125
+ in: "query",
19126
+ schema: {
19127
+ type: [
19128
+ "string",
19129
+ "null"
19130
+ ],
19131
+ format: "date-time"
19132
+ }
19133
+ },
19134
+ {
19135
+ name: "timezone",
19136
+ in: "query",
19137
+ schema: {
19138
+ type: [
19139
+ "string",
19140
+ "null"
19141
+ ]
19142
+ }
19143
+ },
19144
+ {
19145
+ name: "date_rule",
19146
+ in: "query",
19147
+ schema: {
19148
+ type: [
19149
+ "integer",
19150
+ "null"
19151
+ ],
19152
+ enum: [
19153
+ "1",
19154
+ "2",
19155
+ "3",
19156
+ null
19157
+ ]
19158
+ }
19159
+ },
19160
+ {
19161
+ name: "selection_type",
19162
+ in: "query",
19163
+ schema: {
19164
+ type: [
19165
+ "string",
19166
+ "null"
19167
+ ],
19168
+ enum: [
19169
+ "all",
19170
+ "none",
19171
+ null
19172
+ ]
19173
+ }
19174
+ },
19175
+ {
19176
+ name: "groups_uids[]",
19177
+ in: "query",
19178
+ schema: {
19179
+ type: "array",
19180
+ items: {
19181
+ type: "string"
19182
+ }
19183
+ }
18921
19184
  }
18922
19185
  ],
18923
19186
  responses: {
18924
19187
  "200": {
18925
- description: "Paginated set of `ParticipantResource`",
19188
+ description: "Paginated set of `CampaignParticipantResource`",
18926
19189
  content: {
18927
19190
  "application/json": {
18928
19191
  schema: {
@@ -18931,7 +19194,7 @@ var openapi_default = {
18931
19194
  data: {
18932
19195
  type: "array",
18933
19196
  items: {
18934
- $ref: "#/components/schemas/ParticipantResource"
19197
+ $ref: "#/components/schemas/CampaignParticipantResource"
18935
19198
  }
18936
19199
  },
18937
19200
  links: {
@@ -19073,16 +19336,16 @@ var openapi_default = {
19073
19336
  $ref: "#/components/responses/CommunitiesValidationException"
19074
19337
  }
19075
19338
  },
19076
- "x-fc-proposito": "Sirve para listar los participantes de una campa\xF1a, con b\xFAsqueda por tel\xE9fono y si son administradores.",
19339
+ "x-fc-proposito": "Usalo para saber en qu\xE9 grupo est\xE1 cada participante de una campa\xF1a, cu\xE1ndo entr\xF3 o sali\xF3 y si sigue activo. Ideal para sincronizar miembros con tu CRM.",
19077
19340
  "x-fc-domain": "communities",
19078
19341
  "x-fc-permission": "communities:read"
19079
19342
  }
19080
19343
  },
19081
- "/api/v1/campaigns/{campaign}/campaign-participants": {
19344
+ "/api/v1/campaigns/{campaign}/participant-events": {
19082
19345
  get: {
19083
- operationId: "campaign.campaignParticipants",
19084
- description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `joined_at`, `left_at`; direcci\xF3n `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.",
19085
- summary: "Listar los participantes con su grupo actual",
19346
+ operationId: "campaign.participantEvents",
19347
+ description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`).",
19348
+ summary: "Listar entradas y salidas de una campa\xF1a",
19086
19349
  tags: [
19087
19350
  "Participantes"
19088
19351
  ],
@@ -19099,30 +19362,16 @@ var openapi_default = {
19099
19362
  {
19100
19363
  name: "search",
19101
19364
  in: "query",
19102
- schema: {
19103
- type: [
19104
- "string",
19105
- "null"
19106
- ]
19107
- }
19108
- },
19109
- {
19110
- name: "status",
19111
- in: "query",
19112
19365
  schema: {
19113
19366
  type: [
19114
19367
  "string",
19115
19368
  "null"
19116
19369
  ],
19117
- enum: [
19118
- "active",
19119
- "exited",
19120
- null
19121
- ]
19370
+ maxLength: 50
19122
19371
  }
19123
19372
  },
19124
19373
  {
19125
- name: "orderBy",
19374
+ name: "event_type",
19126
19375
  in: "query",
19127
19376
  schema: {
19128
19377
  type: [
@@ -19130,27 +19379,22 @@ var openapi_default = {
19130
19379
  "null"
19131
19380
  ],
19132
19381
  enum: [
19133
- "joined_at",
19134
- "left_at",
19382
+ "all",
19383
+ "join",
19384
+ "leave",
19135
19385
  null
19136
19386
  ]
19137
19387
  }
19138
19388
  },
19139
19389
  {
19140
- name: "orderByMethod",
19390
+ name: "page",
19141
19391
  in: "query",
19142
19392
  schema: {
19143
19393
  type: [
19144
- "string",
19394
+ "integer",
19145
19395
  "null"
19146
19396
  ],
19147
- enum: [
19148
- "asc",
19149
- "desc",
19150
- "ASC",
19151
- "DESC",
19152
- null
19153
- ]
19397
+ minimum: 1
19154
19398
  }
19155
19399
  },
19156
19400
  {
@@ -19161,7 +19405,8 @@ var openapi_default = {
19161
19405
  "integer",
19162
19406
  "null"
19163
19407
  ],
19164
- minimum: 1
19408
+ minimum: 1,
19409
+ maximum: 100
19165
19410
  }
19166
19411
  },
19167
19412
  {
@@ -19193,21 +19438,22 @@ var openapi_default = {
19193
19438
  type: [
19194
19439
  "string",
19195
19440
  "null"
19196
- ]
19441
+ ],
19442
+ maxLength: 64
19197
19443
  }
19198
19444
  },
19199
19445
  {
19200
- name: "date_rule",
19446
+ name: "date_mode",
19201
19447
  in: "query",
19202
19448
  schema: {
19203
19449
  type: [
19204
- "integer",
19450
+ "string",
19205
19451
  "null"
19206
19452
  ],
19207
19453
  enum: [
19208
- "1",
19209
- "2",
19210
- "3",
19454
+ "both",
19455
+ "joins_only",
19456
+ "leaves_only",
19211
19457
  null
19212
19458
  ]
19213
19459
  }
@@ -19233,14 +19479,30 @@ var openapi_default = {
19233
19479
  schema: {
19234
19480
  type: "array",
19235
19481
  items: {
19236
- type: "string"
19482
+ type: "string",
19483
+ maxLength: 50
19237
19484
  }
19238
19485
  }
19486
+ },
19487
+ {
19488
+ name: "participant_filter",
19489
+ in: "query",
19490
+ schema: {
19491
+ type: [
19492
+ "string",
19493
+ "null"
19494
+ ],
19495
+ enum: [
19496
+ "all",
19497
+ "active",
19498
+ null
19499
+ ]
19500
+ }
19239
19501
  }
19240
19502
  ],
19241
19503
  responses: {
19242
19504
  "200": {
19243
- description: "Paginated set of `CampaignParticipantResource`",
19505
+ description: "Paginated set of `ParticipantEventResource`",
19244
19506
  content: {
19245
19507
  "application/json": {
19246
19508
  schema: {
@@ -19249,7 +19511,7 @@ var openapi_default = {
19249
19511
  data: {
19250
19512
  type: "array",
19251
19513
  items: {
19252
- $ref: "#/components/schemas/CampaignParticipantResource"
19514
+ $ref: "#/components/schemas/ParticipantEventResource"
19253
19515
  }
19254
19516
  },
19255
19517
  links: {
@@ -19391,173 +19653,90 @@ var openapi_default = {
19391
19653
  $ref: "#/components/responses/CommunitiesValidationException"
19392
19654
  }
19393
19655
  },
19394
- "x-fc-proposito": "Usalo para saber en qu\xE9 grupo est\xE1 cada participante de una campa\xF1a, cu\xE1ndo entr\xF3 o sali\xF3 y si sigue activo. Ideal para sincronizar miembros con tu CRM.",
19656
+ "x-fc-proposito": "Sirve para obtener el registro de entradas y salidas de toda una campa\xF1a, con fecha, grupo y pa\xEDs, filtrando por tipo de evento, fechas o grupos.",
19395
19657
  "x-fc-domain": "communities",
19396
19658
  "x-fc-permission": "communities:read"
19397
19659
  }
19398
19660
  },
19399
- "/api/v1/campaigns/{campaign}/participant-events": {
19661
+ "/api/v1/groups/participants": {
19400
19662
  get: {
19401
- operationId: "campaign.participantEvents",
19402
- description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`).",
19403
- summary: "Listar entradas y salidas de una campa\xF1a",
19663
+ operationId: "group.participants",
19664
+ description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `created_at`, `id`, `updated_at`; direcci\xF3n `orderDirection`: `asc` o `desc`. Otro valor responde `422`.",
19665
+ summary: "Listar los participantes de un grupo",
19404
19666
  tags: [
19405
19667
  "Participantes"
19406
19668
  ],
19407
19669
  parameters: [
19408
19670
  {
19409
- name: "campaign",
19410
- in: "path",
19671
+ name: "groupUid",
19672
+ in: "query",
19411
19673
  required: true,
19412
- description: "The campaign ID",
19413
19674
  schema: {
19414
- type: "integer"
19675
+ type: "string"
19415
19676
  }
19416
19677
  },
19417
19678
  {
19418
- name: "search",
19679
+ name: "phone",
19419
19680
  in: "query",
19420
19681
  schema: {
19421
- type: [
19422
- "string",
19423
- "null"
19424
- ],
19425
- maxLength: 50
19682
+ type: "string"
19426
19683
  }
19427
19684
  },
19428
19685
  {
19429
- name: "event_type",
19686
+ name: "isAdmin",
19430
19687
  in: "query",
19431
19688
  schema: {
19432
- type: [
19433
- "string",
19434
- "null"
19435
- ],
19689
+ type: "string",
19436
19690
  enum: [
19437
- "all",
19438
- "join",
19439
- "leave",
19440
- null
19691
+ "true",
19692
+ "false",
19693
+ "1",
19694
+ "0"
19441
19695
  ]
19442
19696
  }
19443
19697
  },
19444
19698
  {
19445
- name: "page",
19446
- in: "query",
19447
- schema: {
19448
- type: [
19449
- "integer",
19450
- "null"
19451
- ],
19452
- minimum: 1
19453
- }
19454
- },
19455
- {
19456
- name: "perPage",
19457
- in: "query",
19458
- schema: {
19459
- type: [
19460
- "integer",
19461
- "null"
19462
- ],
19463
- minimum: 1,
19464
- maximum: 100
19465
- }
19466
- },
19467
- {
19468
- name: "start_date",
19469
- in: "query",
19470
- schema: {
19471
- type: [
19472
- "string",
19473
- "null"
19474
- ],
19475
- format: "date-time"
19476
- }
19477
- },
19478
- {
19479
- name: "end_date",
19480
- in: "query",
19481
- schema: {
19482
- type: [
19483
- "string",
19484
- "null"
19485
- ],
19486
- format: "date-time"
19487
- }
19488
- },
19489
- {
19490
- name: "timezone",
19491
- in: "query",
19492
- schema: {
19493
- type: [
19494
- "string",
19495
- "null"
19496
- ],
19497
- maxLength: 64
19498
- }
19499
- },
19500
- {
19501
- name: "date_mode",
19699
+ name: "eligibleForAdmin",
19502
19700
  in: "query",
19503
19701
  schema: {
19504
- type: [
19505
- "string",
19506
- "null"
19507
- ],
19702
+ type: "string",
19508
19703
  enum: [
19509
- "both",
19510
- "joins_only",
19511
- "leaves_only",
19512
- null
19704
+ "true",
19705
+ "false",
19706
+ "1",
19707
+ "0"
19513
19708
  ]
19514
19709
  }
19515
19710
  },
19516
19711
  {
19517
- name: "selection_type",
19712
+ name: "orderBy",
19518
19713
  in: "query",
19519
19714
  schema: {
19520
- type: [
19521
- "string",
19522
- "null"
19523
- ],
19524
- enum: [
19525
- "all",
19526
- "none",
19527
- null
19528
- ]
19715
+ type: "string"
19529
19716
  }
19530
19717
  },
19531
19718
  {
19532
- name: "groups_uids[]",
19719
+ name: "orderDirection",
19533
19720
  in: "query",
19534
19721
  schema: {
19535
- type: "array",
19536
- items: {
19537
- type: "string",
19538
- maxLength: 50
19539
- }
19722
+ type: "string",
19723
+ enum: [
19724
+ "asc",
19725
+ "desc"
19726
+ ]
19540
19727
  }
19541
19728
  },
19542
19729
  {
19543
- name: "participant_filter",
19730
+ name: "perPage",
19544
19731
  in: "query",
19545
19732
  schema: {
19546
- type: [
19547
- "string",
19548
- "null"
19549
- ],
19550
- enum: [
19551
- "all",
19552
- "active",
19553
- null
19554
- ]
19733
+ type: "integer"
19555
19734
  }
19556
19735
  }
19557
19736
  ],
19558
19737
  responses: {
19559
19738
  "200": {
19560
- description: "Paginated set of `ParticipantEventResource`",
19739
+ description: "Paginated set of `ParticipantResource`",
19561
19740
  content: {
19562
19741
  "application/json": {
19563
19742
  schema: {
@@ -19566,241 +19745,7 @@ var openapi_default = {
19566
19745
  data: {
19567
19746
  type: "array",
19568
19747
  items: {
19569
- $ref: "#/components/schemas/ParticipantEventResource"
19570
- }
19571
- },
19572
- links: {
19573
- type: "object",
19574
- properties: {
19575
- first: {
19576
- type: [
19577
- "string",
19578
- "null"
19579
- ]
19580
- },
19581
- last: {
19582
- type: [
19583
- "string",
19584
- "null"
19585
- ]
19586
- },
19587
- prev: {
19588
- type: [
19589
- "string",
19590
- "null"
19591
- ]
19592
- },
19593
- next: {
19594
- type: [
19595
- "string",
19596
- "null"
19597
- ]
19598
- }
19599
- },
19600
- required: [
19601
- "first",
19602
- "last",
19603
- "prev",
19604
- "next"
19605
- ]
19606
- },
19607
- meta: {
19608
- type: "object",
19609
- properties: {
19610
- current_page: {
19611
- type: "integer",
19612
- minimum: 1
19613
- },
19614
- from: {
19615
- type: [
19616
- "integer",
19617
- "null"
19618
- ],
19619
- minimum: 1
19620
- },
19621
- last_page: {
19622
- type: "integer",
19623
- minimum: 1
19624
- },
19625
- links: {
19626
- type: "array",
19627
- description: "Generated paginator links.",
19628
- items: {
19629
- type: "object",
19630
- properties: {
19631
- url: {
19632
- type: [
19633
- "string",
19634
- "null"
19635
- ]
19636
- },
19637
- label: {
19638
- type: "string"
19639
- },
19640
- active: {
19641
- type: "boolean"
19642
- }
19643
- },
19644
- required: [
19645
- "url",
19646
- "label",
19647
- "active"
19648
- ]
19649
- }
19650
- },
19651
- path: {
19652
- type: [
19653
- "string",
19654
- "null"
19655
- ],
19656
- description: "Base path for paginator generated URLs."
19657
- },
19658
- per_page: {
19659
- type: "integer",
19660
- description: "Number of items shown per page.",
19661
- minimum: 0
19662
- },
19663
- to: {
19664
- type: [
19665
- "integer",
19666
- "null"
19667
- ],
19668
- description: "Number of the last item in the slice.",
19669
- minimum: 1
19670
- },
19671
- total: {
19672
- type: "integer",
19673
- description: "Total number of items being paginated.",
19674
- minimum: 0
19675
- }
19676
- },
19677
- required: [
19678
- "current_page",
19679
- "from",
19680
- "last_page",
19681
- "links",
19682
- "path",
19683
- "per_page",
19684
- "to",
19685
- "total"
19686
- ]
19687
- }
19688
- },
19689
- required: [
19690
- "data",
19691
- "links",
19692
- "meta"
19693
- ]
19694
- }
19695
- }
19696
- }
19697
- },
19698
- "401": {
19699
- $ref: "#/components/responses/CommunitiesAuthenticationException"
19700
- },
19701
- "403": {
19702
- $ref: "#/components/responses/CommunitiesAuthorizationException"
19703
- },
19704
- "404": {
19705
- $ref: "#/components/responses/CommunitiesModelNotFoundException"
19706
- },
19707
- "422": {
19708
- $ref: "#/components/responses/CommunitiesValidationException"
19709
- }
19710
- },
19711
- "x-fc-proposito": "Sirve para obtener el registro de entradas y salidas de toda una campa\xF1a, con fecha, grupo y pa\xEDs, filtrando por tipo de evento, fechas o grupos.",
19712
- "x-fc-domain": "communities",
19713
- "x-fc-permission": "communities:read"
19714
- }
19715
- },
19716
- "/api/v1/groups/participants": {
19717
- get: {
19718
- operationId: "group.participants",
19719
- description: "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tama\xF1o de p\xE1gina: `perPage` (por defecto 25, m\xE1ximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `created_at`, `id`, `updated_at`; direcci\xF3n `orderDirection`: `asc` o `desc`. Otro valor responde `422`.",
19720
- summary: "Listar los participantes de un grupo",
19721
- tags: [
19722
- "Participantes"
19723
- ],
19724
- parameters: [
19725
- {
19726
- name: "groupUid",
19727
- in: "query",
19728
- required: true,
19729
- schema: {
19730
- type: "string"
19731
- }
19732
- },
19733
- {
19734
- name: "phone",
19735
- in: "query",
19736
- schema: {
19737
- type: "string"
19738
- }
19739
- },
19740
- {
19741
- name: "isAdmin",
19742
- in: "query",
19743
- schema: {
19744
- type: "string",
19745
- enum: [
19746
- "true",
19747
- "false",
19748
- "1",
19749
- "0"
19750
- ]
19751
- }
19752
- },
19753
- {
19754
- name: "eligibleForAdmin",
19755
- in: "query",
19756
- schema: {
19757
- type: "string",
19758
- enum: [
19759
- "true",
19760
- "false",
19761
- "1",
19762
- "0"
19763
- ]
19764
- }
19765
- },
19766
- {
19767
- name: "orderBy",
19768
- in: "query",
19769
- schema: {
19770
- type: "string"
19771
- }
19772
- },
19773
- {
19774
- name: "orderDirection",
19775
- in: "query",
19776
- schema: {
19777
- type: "string",
19778
- enum: [
19779
- "asc",
19780
- "desc"
19781
- ]
19782
- }
19783
- },
19784
- {
19785
- name: "perPage",
19786
- in: "query",
19787
- schema: {
19788
- type: "integer"
19789
- }
19790
- }
19791
- ],
19792
- responses: {
19793
- "200": {
19794
- description: "Paginated set of `ParticipantResource`",
19795
- content: {
19796
- "application/json": {
19797
- schema: {
19798
- type: "object",
19799
- properties: {
19800
- data: {
19801
- type: "array",
19802
- items: {
19803
- $ref: "#/components/schemas/ParticipantResource"
19748
+ $ref: "#/components/schemas/ParticipantResource"
19804
19749
  }
19805
19750
  },
19806
19751
  links: {
@@ -24830,7 +24775,164 @@ var openapi_default = {
24830
24775
  },
24831
24776
  summary: "Listar n\xFAmeros",
24832
24777
  "x-fc-proposito": "Sirve para listar tus n\xFAmeros de WhatsApp con su proveedor, si est\xE1n conectados y activos y su estado de salud, y obtener los IDs que usan las dem\xE1s operaciones.",
24833
- "x-fc-domain": "accounts"
24778
+ "x-fc-domain": "accounts",
24779
+ parameters: [
24780
+ {
24781
+ name: "external_reference",
24782
+ in: "query",
24783
+ required: false,
24784
+ description: "Only the numbers with exactly this reference.",
24785
+ schema: {
24786
+ type: "string",
24787
+ maxLength: 255
24788
+ },
24789
+ "x-fc-pending": "accounts-alta-de-numeros"
24790
+ }
24791
+ ]
24792
+ },
24793
+ post: {
24794
+ operationId: "pendingSandboxDevices.store",
24795
+ summary: "Create a number by QR",
24796
+ description: "Creates a number of the account to connect by QR, with `connected: false`; ask for its code with `GET /devices/{device}/qr`. Same path and same rejections as the panel: the quota counts phones (QR and Meta of the same phone count once). `same_number_as` adds QR to an official number in coexistence without taking a place. An official number is created only with a sign-up link.\n\n**Pending of the spec**: from the contract of the sign-up by API (22-10, A1), while accounts writes it; its OpenAPI will be the final source.\n\nAmendment v0.1 (23-10): as accounts did it in `feat/alta-de-numeros-api` `52d49b319`. The pool keeps at most one unconnected QR number per account: another one answers `409 pending_number_exists` with its `device_id`. Without an active plan, `403` without `error`.\n\nOptional `country` (two letters, any case): the country of the phone that will scan the QR, where the WhatsApp session is opened from.",
24797
+ tags: [
24798
+ "Device"
24799
+ ],
24800
+ "x-fc-domain": "accounts",
24801
+ "x-fc-permission": "accounts:operate",
24802
+ "x-fc-pending": "accounts-alta-de-numeros",
24803
+ parameters: [
24804
+ {
24805
+ name: "Idempotency-Key",
24806
+ in: "header",
24807
+ required: false,
24808
+ description: "Up to 255 characters, kept 24 hours. The same key with the same body gives back the same answer without creating anything; with another body, `422 idempotency_key_mismatch`; while the first call runs, `409 operation_in_progress`.",
24809
+ schema: {
24810
+ type: "string",
24811
+ maxLength: 255
24812
+ }
24813
+ }
24814
+ ],
24815
+ requestBody: {
24816
+ required: true,
24817
+ content: {
24818
+ "application/json": {
24819
+ schema: {
24820
+ type: "object",
24821
+ properties: {
24822
+ connection: {
24823
+ type: "string",
24824
+ enum: [
24825
+ "qr"
24826
+ ],
24827
+ description: "For now the only value."
24828
+ },
24829
+ alias: {
24830
+ type: [
24831
+ "string",
24832
+ "null"
24833
+ ],
24834
+ maxLength: 255
24835
+ },
24836
+ external_reference: {
24837
+ type: [
24838
+ "string",
24839
+ "null"
24840
+ ],
24841
+ maxLength: 255,
24842
+ description: "Free text to find your customer: not unique. It comes back in the number, in `GET /devices?external_reference=` and in every `device.*` event."
24843
+ },
24844
+ same_number_as: {
24845
+ type: [
24846
+ "integer",
24847
+ "null"
24848
+ ],
24849
+ description: "An official number in coexistence that gets QR too."
24850
+ },
24851
+ country: {
24852
+ type: [
24853
+ "string",
24854
+ "null"
24855
+ ],
24856
+ pattern: "^[A-Za-z]{2}$",
24857
+ description: "The country of the phone that will scan the QR (ISO 3166-1 alpha-2, two letters, any case): the WhatsApp session is opened from there. Without it, the default."
24858
+ }
24859
+ },
24860
+ required: [
24861
+ "connection"
24862
+ ]
24863
+ }
24864
+ }
24865
+ }
24866
+ },
24867
+ responses: {
24868
+ "201": {
24869
+ description: "Created, not connected yet",
24870
+ content: {
24871
+ "application/json": {
24872
+ schema: {
24873
+ type: "object",
24874
+ properties: {
24875
+ data: {
24876
+ $ref: "#/components/schemas/DeviceResource"
24877
+ }
24878
+ },
24879
+ required: [
24880
+ "data"
24881
+ ]
24882
+ }
24883
+ }
24884
+ },
24885
+ headers: {
24886
+ "Idempotent-Replayed": {
24887
+ description: "`true` when the answer is a replay of the same `Idempotency-Key`.",
24888
+ schema: {
24889
+ type: "string"
24890
+ }
24891
+ }
24892
+ }
24893
+ },
24894
+ "403": {
24895
+ description: "`numbers_not_included`, `number_limit_reached` (with `limit`, `used` and `pending_device_ids`) or `connect_not_allowed`",
24896
+ content: {
24897
+ "application/json": {
24898
+ schema: {
24899
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
24900
+ }
24901
+ }
24902
+ }
24903
+ },
24904
+ "409": {
24905
+ description: "`pending_number_exists` (with `device_id`) or `operation_in_progress`",
24906
+ content: {
24907
+ "application/json": {
24908
+ schema: {
24909
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
24910
+ }
24911
+ }
24912
+ }
24913
+ },
24914
+ "422": {
24915
+ description: "`invalid_request` (with `errors`), `invalid_reference_device`, `qr_requires_coexistence`, `method_already_present` (with `device_id`), `number_suspended_by_plan`, `idempotency_key_mismatch` or `invalid_idempotency_key`",
24916
+ content: {
24917
+ "application/json": {
24918
+ schema: {
24919
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
24920
+ }
24921
+ }
24922
+ }
24923
+ },
24924
+ "500": {
24925
+ description: "`provider_error`",
24926
+ content: {
24927
+ "application/json": {
24928
+ schema: {
24929
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
24930
+ }
24931
+ }
24932
+ }
24933
+ }
24934
+ },
24935
+ "x-fc-validation-error": "invalid_request"
24834
24936
  }
24835
24937
  },
24836
24938
  "/api/v1/devices/{device}/me": {
@@ -25161,6 +25263,14 @@ var openapi_default = {
25161
25263
  capi_consent: {
25162
25264
  type: "boolean",
25163
25265
  description: "Meta Conversions API (CAPI) opt-in consent \u2014 capi-meta-connection Inc 3\n(design.md D3). Optional: only meaningful on the Meta branch, and even\nthere it is a no-op unless the `capi` feature flag is enabled for the\naccount (defense in depth, see updateMetaDevice below)."
25266
+ },
25267
+ external_reference: {
25268
+ type: [
25269
+ "string",
25270
+ "null"
25271
+ ],
25272
+ maxLength: 255,
25273
+ description: "Free text to find your customer; `null` clears it."
25164
25274
  }
25165
25275
  }
25166
25276
  }
@@ -25338,7 +25448,8 @@ var openapi_default = {
25338
25448
  "x-fc-destructive": false,
25339
25449
  summary: "Actualizar un n\xFAmero",
25340
25450
  "x-fc-proposito": "Sirve para actualizar los datos de un n\xFAmero (alias, color, imagen) o completar sus datos de WhatsApp Business, como WABA y phone number ID, y el consentimiento de Conversions API.",
25341
- "x-fc-domain": "accounts"
25451
+ "x-fc-domain": "accounts",
25452
+ "x-fc-pending": "accounts-alta-de-numeros"
25342
25453
  },
25343
25454
  delete: {
25344
25455
  operationId: "device.destroy",
@@ -25359,14 +25470,7 @@ var openapi_default = {
25359
25470
  ],
25360
25471
  responses: {
25361
25472
  "200": {
25362
- description: "",
25363
- content: {
25364
- "application/json": {
25365
- schema: {
25366
- type: "object"
25367
- }
25368
- }
25369
- }
25473
+ description: "Deleted: an empty body, as today. Emits `device.deleted`."
25370
25474
  },
25371
25475
  "401": {
25372
25476
  $ref: "#/components/responses/AccountsAuthenticationException"
@@ -25383,7 +25487,8 @@ var openapi_default = {
25383
25487
  "x-fc-destructive": true,
25384
25488
  summary: "Eliminar un n\xFAmero",
25385
25489
  "x-fc-proposito": "Usalo para dar de baja un n\xFAmero de la cuenta de forma definitiva. Es irreversible y requiere el permiso `accounts:operate`.",
25386
- "x-fc-domain": "accounts"
25490
+ "x-fc-domain": "accounts",
25491
+ "x-fc-pending-response": "accounts-alta-de-numeros"
25387
25492
  }
25388
25493
  },
25389
25494
  "/api/v1/devices/{device}/reset": {
@@ -29603,143 +29708,84 @@ var openapi_default = {
29603
29708
  }
29604
29709
  }
29605
29710
  },
29606
- "/api/v1/groups/actions/{action}": {
29711
+ "/api/v1/devices/{device}/qr": {
29607
29712
  get: {
29608
- operationId: "pendingSandboxGroupActions.groups.actions.action.get",
29609
- summary: "Group action by API (off)",
29610
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29713
+ operationId: "pendingSandboxDevices.qr",
29714
+ summary: "The QR code of a number",
29715
+ description: "Creates the instance if needed and gives its QR code. Only for QR numbers: an official one answers `422 provider_unsupported`. Scanning it gives control of a WhatsApp, so it asks for `accounts:operate`.\n\n**Pending of the spec**: from the contract of the sign-up by API (22-10, A1), while accounts writes it; its OpenAPI will be the final source.\n\nAmendment v0.1 (23-10): as accounts did it in `feat/alta-de-numeros-api` `52d49b319`. `connected` is a boolean here (`0`/`1` in the number).\n\naccounts #1335 (25-10, in production): `expires_in` is 5 with a code, the cadence of the panel. Ask for it again every 5 seconds and show the new one at once: WhatsApp changes the code every few seconds and an old one does not link. The answer carries `Cache-Control: no-store`.",
29611
29716
  tags: [
29612
- "GroupActions"
29717
+ "Device"
29613
29718
  ],
29614
- "x-fc-domain": "communities",
29615
- "x-fc-permission": "communities:operate",
29616
- "x-fc-pending": "communities-group-actions",
29719
+ "x-fc-domain": "accounts",
29720
+ "x-fc-permission": "accounts:operate",
29721
+ "x-fc-pending": "accounts-alta-de-numeros",
29617
29722
  parameters: [
29618
29723
  {
29619
- name: "action",
29724
+ name: "device",
29620
29725
  in: "path",
29621
29726
  required: true,
29622
29727
  schema: {
29623
- type: "string"
29728
+ type: "integer"
29624
29729
  }
29625
29730
  }
29626
29731
  ],
29627
29732
  responses: {
29733
+ "200": {
29734
+ description: "OK",
29735
+ content: {
29736
+ "application/json": {
29737
+ schema: {
29738
+ $ref: "#/components/schemas/PendingSandboxDeviceQr"
29739
+ }
29740
+ }
29741
+ }
29742
+ },
29628
29743
  "403": {
29629
- description: "route_not_available: the group actions by API are off in production.",
29744
+ description: "`number_limit_reached`, `connect_not_allowed` or `payment_required`",
29630
29745
  content: {
29631
29746
  "application/json": {
29632
29747
  schema: {
29633
- type: "object",
29634
- properties: {
29635
- error: {
29636
- type: "string",
29637
- const: "route_not_available"
29638
- },
29639
- message: {
29640
- type: "string"
29641
- }
29642
- },
29643
- required: [
29644
- "error",
29645
- "message"
29646
- ]
29748
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
29647
29749
  }
29648
29750
  }
29649
29751
  }
29650
- }
29651
- }
29652
- }
29653
- },
29654
- "/api/v1/groups/{group}/participants": {
29655
- post: {
29656
- operationId: "pendingSandboxGroupActions.groups.group.participants.post",
29657
- summary: "Group action by API (off)",
29658
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29659
- tags: [
29660
- "GroupActions"
29661
- ],
29662
- "x-fc-domain": "communities",
29663
- "x-fc-permission": "communities:operate",
29664
- "x-fc-pending": "communities-group-actions",
29665
- parameters: [
29666
- {
29667
- name: "group",
29668
- in: "path",
29669
- required: true,
29670
- schema: {
29671
- type: "string"
29752
+ },
29753
+ "404": {
29754
+ description: "It does not exist in the account",
29755
+ content: {
29756
+ "application/json": {
29757
+ schema: {
29758
+ $ref: "#/components/schemas/PendingSandboxModelNotFound"
29759
+ }
29760
+ }
29672
29761
  }
29673
- }
29674
- ],
29675
- responses: {
29676
- "403": {
29677
- description: "route_not_available: the group actions by API are off in production.",
29762
+ },
29763
+ "409": {
29764
+ description: "`trial_number_blocked`, `qr_requires_coexistence`, `number_suspended_by_plan`, `device_inactive` or `provisioning_quota_exceeded`",
29678
29765
  content: {
29679
29766
  "application/json": {
29680
29767
  schema: {
29681
- type: "object",
29682
- properties: {
29683
- error: {
29684
- type: "string",
29685
- const: "route_not_available"
29686
- },
29687
- message: {
29688
- type: "string"
29689
- }
29690
- },
29691
- required: [
29692
- "error",
29693
- "message"
29694
- ]
29768
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
29695
29769
  }
29696
29770
  }
29697
29771
  }
29698
- }
29699
- }
29700
- }
29701
- },
29702
- "/api/v1/groups/{group}/participants/remove": {
29703
- post: {
29704
- operationId: "pendingSandboxGroupActions.groups.group.participants.remove.post",
29705
- summary: "Group action by API (off)",
29706
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29707
- tags: [
29708
- "GroupActions"
29709
- ],
29710
- "x-fc-domain": "communities",
29711
- "x-fc-permission": "communities:operate",
29712
- "x-fc-pending": "communities-group-actions",
29713
- parameters: [
29714
- {
29715
- name: "group",
29716
- in: "path",
29717
- required: true,
29718
- schema: {
29719
- type: "string"
29772
+ },
29773
+ "422": {
29774
+ description: "`provider_unsupported`",
29775
+ content: {
29776
+ "application/json": {
29777
+ schema: {
29778
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
29779
+ }
29780
+ }
29720
29781
  }
29721
- }
29722
- ],
29723
- responses: {
29724
- "403": {
29725
- description: "route_not_available: the group actions by API are off in production.",
29782
+ },
29783
+ "502": {
29784
+ description: "`provider_error`",
29726
29785
  content: {
29727
29786
  "application/json": {
29728
29787
  schema: {
29729
- type: "object",
29730
- properties: {
29731
- error: {
29732
- type: "string",
29733
- const: "route_not_available"
29734
- },
29735
- message: {
29736
- type: "string"
29737
- }
29738
- },
29739
- required: [
29740
- "error",
29741
- "message"
29742
- ]
29788
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
29743
29789
  }
29744
29790
  }
29745
29791
  }
@@ -29747,527 +29793,385 @@ var openapi_default = {
29747
29793
  }
29748
29794
  }
29749
29795
  },
29750
- "/api/v1/groups/{group}/admins": {
29751
- post: {
29752
- operationId: "pendingSandboxGroupActions.groups.group.admins.post",
29753
- summary: "Group action by API (off)",
29754
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29796
+ "/api/v1/connect-links": {
29797
+ get: {
29798
+ operationId: "pendingSandboxConnectLinks.index",
29799
+ summary: "List the sign-up links",
29800
+ description: "Paginated like the other lists (`perPage`, 25 by default, up to 100), newest first (by id). Filters by `status` and `external_reference`.\n\nAmendment v0.1 (23-10): as accounts did it in `feat/alta-de-numeros-api` `52d49b319`.",
29755
29801
  tags: [
29756
- "GroupActions"
29802
+ "ConnectLink"
29757
29803
  ],
29758
- "x-fc-domain": "communities",
29759
- "x-fc-permission": "communities:operate",
29760
- "x-fc-pending": "communities-group-actions",
29804
+ "x-fc-domain": "accounts",
29805
+ "x-fc-permission": "accounts:read",
29806
+ "x-fc-pending": "accounts-alta-de-numeros",
29761
29807
  parameters: [
29762
29808
  {
29763
- name: "group",
29764
- in: "path",
29765
- required: true,
29809
+ name: "status",
29810
+ in: "query",
29811
+ required: false,
29766
29812
  schema: {
29767
- type: "string"
29813
+ type: "string",
29814
+ enum: [
29815
+ "pending",
29816
+ "completed",
29817
+ "failed",
29818
+ "expired",
29819
+ "revoked"
29820
+ ]
29821
+ }
29822
+ },
29823
+ {
29824
+ name: "external_reference",
29825
+ in: "query",
29826
+ required: false,
29827
+ schema: {
29828
+ type: "string",
29829
+ maxLength: 255
29830
+ }
29831
+ },
29832
+ {
29833
+ name: "page",
29834
+ in: "query",
29835
+ required: false,
29836
+ schema: {
29837
+ type: "integer",
29838
+ minimum: 1
29839
+ }
29840
+ },
29841
+ {
29842
+ name: "perPage",
29843
+ in: "query",
29844
+ required: false,
29845
+ schema: {
29846
+ type: "integer",
29847
+ minimum: 1,
29848
+ maximum: 100
29768
29849
  }
29769
29850
  }
29770
29851
  ],
29771
29852
  responses: {
29772
- "403": {
29773
- description: "route_not_available: the group actions by API are off in production.",
29853
+ "200": {
29854
+ description: "OK",
29774
29855
  content: {
29775
29856
  "application/json": {
29776
29857
  schema: {
29777
29858
  type: "object",
29778
29859
  properties: {
29779
- error: {
29780
- type: "string",
29781
- const: "route_not_available"
29860
+ data: {
29861
+ type: "array",
29862
+ items: {
29863
+ $ref: "#/components/schemas/PendingSandboxConnectLink"
29864
+ }
29782
29865
  },
29783
- message: {
29784
- type: "string"
29866
+ links: {
29867
+ type: "object"
29868
+ },
29869
+ meta: {
29870
+ type: "object"
29785
29871
  }
29786
- },
29787
- required: [
29788
- "error",
29789
- "message"
29790
- ]
29872
+ }
29791
29873
  }
29792
29874
  }
29793
29875
  }
29794
29876
  }
29795
- }
29796
- }
29797
- },
29798
- "/api/v1/groups/{group}/admins/remove": {
29877
+ },
29878
+ "x-fc-validation-error": "invalid_request"
29879
+ },
29799
29880
  post: {
29800
- operationId: "pendingSandboxGroupActions.groups.group.admins.remove.post",
29801
- summary: "Group action by API (off)",
29802
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29881
+ operationId: "pendingSandboxConnectLinks.store",
29882
+ summary: "Create a sign-up link",
29883
+ description: 'A link of the account that creates the number when your customer completes the page (`url`), or reconnects the number of `device_id` without taking a new place. The quota is checked now (except for a reconnection) and again at the end. Up to 20 open links per account. With `type: meta` while the Meta sign-up is off, `503 meta_connect_disabled`.\n\n**Pending of the spec**: from the contract of the sign-up by API (22-10, A1), while accounts writes it; its OpenAPI will be the final source.\n\nAmendment v0.1 (23-10): as accounts did it in `feat/alta-de-numeros-api` `52d49b319`. Body rules: `allowed_connection_types` required with `meta` and prohibited with `qr` (one or two distinct values); `provision_us_number` prohibited with `qr`; `same_number_as` prohibited with `meta` and excluding `device_id`; `return_url` `https`, up to 2048. Up to 10 creations per minute: one more answers Laravel\'s `429 {"message": "Too Many Attempts."}`. The answer carries `Cache-Control: no-store`.\n\nAmendment v0.2 (23-10, accounts `8852b3fad`): `provision_us_number: true` needs `dedicated` among `allowed_connection_types` and does not go with `device_id` (a pool number is always dedicated and new); otherwise `422 invalid_request`. A Meta link survives failures of Facebook or Meta: it stays `pending` so your customer retries, and ends `failed` (`facebook_auth_failed` or `meta_error`) at the fifth; `quota_exceeded`, `trial_number_blocked` and `number_not_owned` end it at the first.\n\nIn production (25-10): the Meta sign-up stays off until the domain is registered with Meta, so `type: meta` answers `503 meta_connect_disabled`; the sandbox starts with it off too. Optional `country` (two letters, `qr` only and not with `device_id`): the country of the phone, which the link returns in lower case.',
29803
29884
  tags: [
29804
- "GroupActions"
29885
+ "ConnectLink"
29805
29886
  ],
29806
- "x-fc-domain": "communities",
29807
- "x-fc-permission": "communities:operate",
29808
- "x-fc-pending": "communities-group-actions",
29887
+ "x-fc-domain": "accounts",
29888
+ "x-fc-permission": "accounts:operate",
29889
+ "x-fc-pending": "accounts-alta-de-numeros",
29809
29890
  parameters: [
29810
29891
  {
29811
- name: "group",
29812
- in: "path",
29813
- required: true,
29892
+ name: "Idempotency-Key",
29893
+ in: "header",
29894
+ required: false,
29895
+ description: "Up to 255 characters, kept 24 hours. The same key with the same body gives back the same answer without creating anything; with another body, `422 idempotency_key_mismatch`; while the first call runs, `409 operation_in_progress`.",
29814
29896
  schema: {
29815
- type: "string"
29897
+ type: "string",
29898
+ maxLength: 255
29816
29899
  }
29817
29900
  }
29818
29901
  ],
29819
- responses: {
29820
- "403": {
29821
- description: "route_not_available: the group actions by API are off in production.",
29822
- content: {
29823
- "application/json": {
29824
- schema: {
29825
- type: "object",
29826
- properties: {
29827
- error: {
29902
+ requestBody: {
29903
+ required: true,
29904
+ content: {
29905
+ "application/json": {
29906
+ schema: {
29907
+ type: "object",
29908
+ properties: {
29909
+ type: {
29910
+ type: "string",
29911
+ enum: [
29912
+ "meta",
29913
+ "qr"
29914
+ ]
29915
+ },
29916
+ allowed_connection_types: {
29917
+ type: [
29918
+ "array",
29919
+ "null"
29920
+ ],
29921
+ items: {
29828
29922
  type: "string",
29829
- const: "route_not_available"
29923
+ enum: [
29924
+ "coexistence",
29925
+ "dedicated"
29926
+ ]
29830
29927
  },
29831
- message: {
29832
- type: "string"
29833
- }
29928
+ minItems: 1,
29929
+ maxItems: 2,
29930
+ uniqueItems: true,
29931
+ description: "Required with `type: meta`, forbidden with `qr`. With both, your customer chooses."
29834
29932
  },
29835
- required: [
29836
- "error",
29837
- "message"
29838
- ]
29839
- }
29933
+ provision_us_number: {
29934
+ type: "boolean",
29935
+ description: "`meta` only (prohibited with `qr`): offer a US number of the pool (3 per account, 1 on trial accounts). Needs `dedicated` among `allowed_connection_types` and does not go with `device_id`."
29936
+ },
29937
+ device_id: {
29938
+ type: [
29939
+ "integer",
29940
+ "null"
29941
+ ],
29942
+ description: "Reconnect this number. `type` must match its method."
29943
+ },
29944
+ same_number_as: {
29945
+ type: [
29946
+ "integer",
29947
+ "null"
29948
+ ],
29949
+ description: "`qr` only (prohibited with `meta`), and not with `device_id`: add QR to a number in coexistence."
29950
+ },
29951
+ country: {
29952
+ type: [
29953
+ "string",
29954
+ "null"
29955
+ ],
29956
+ pattern: "^[A-Za-z]{2}$",
29957
+ description: "`qr` only (prohibited with `meta`), and not with `device_id`: the country of the phone (ISO 3166-1 alpha-2, two letters, any case), where the WhatsApp session is opened from. The link returns it in lower case."
29958
+ },
29959
+ external_reference: {
29960
+ type: [
29961
+ "string",
29962
+ "null"
29963
+ ],
29964
+ maxLength: 255
29965
+ },
29966
+ return_url: {
29967
+ type: [
29968
+ "string",
29969
+ "null"
29970
+ ],
29971
+ format: "uri",
29972
+ maxLength: 2048,
29973
+ description: "`https` only. Gets `connect_link_id`, `status`, `device_id` and `error_code`, nothing else."
29974
+ },
29975
+ expires_in_minutes: {
29976
+ type: "integer",
29977
+ minimum: 30,
29978
+ maximum: 10080,
29979
+ description: "1440 by default."
29980
+ }
29981
+ },
29982
+ required: [
29983
+ "type"
29984
+ ]
29840
29985
  }
29841
29986
  }
29842
29987
  }
29843
- }
29844
- }
29845
- },
29846
- "/api/v1/groups/{group}/name": {
29847
- put: {
29848
- operationId: "pendingSandboxGroupActions.groups.group.name.put",
29849
- summary: "Group action by API (off)",
29850
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29851
- tags: [
29852
- "GroupActions"
29853
- ],
29854
- "x-fc-domain": "communities",
29855
- "x-fc-permission": "communities:operate",
29856
- "x-fc-pending": "communities-group-actions",
29857
- parameters: [
29858
- {
29859
- name: "group",
29860
- in: "path",
29861
- required: true,
29862
- schema: {
29863
- type: "string"
29864
- }
29865
- }
29866
- ],
29988
+ },
29867
29989
  responses: {
29868
- "403": {
29869
- description: "route_not_available: the group actions by API are off in production.",
29990
+ "201": {
29991
+ description: "Created",
29870
29992
  content: {
29871
29993
  "application/json": {
29872
29994
  schema: {
29873
29995
  type: "object",
29874
29996
  properties: {
29875
- error: {
29876
- type: "string",
29877
- const: "route_not_available"
29878
- },
29879
- message: {
29880
- type: "string"
29997
+ data: {
29998
+ $ref: "#/components/schemas/PendingSandboxConnectLinkCreated"
29881
29999
  }
29882
30000
  },
29883
30001
  required: [
29884
- "error",
29885
- "message"
30002
+ "data"
29886
30003
  ]
29887
30004
  }
29888
30005
  }
30006
+ },
30007
+ headers: {
30008
+ "Cache-Control": {
30009
+ schema: {
30010
+ type: "string",
30011
+ const: "no-store"
30012
+ }
30013
+ },
30014
+ "Idempotent-Replayed": {
30015
+ description: "`true` when the answer is a replay of the same `Idempotency-Key`.",
30016
+ schema: {
30017
+ type: "string"
30018
+ }
30019
+ }
29889
30020
  }
29890
- }
29891
- }
29892
- }
29893
- },
29894
- "/api/v1/groups/{group}/description": {
29895
- put: {
29896
- operationId: "pendingSandboxGroupActions.groups.group.description.put",
29897
- summary: "Group action by API (off)",
29898
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29899
- tags: [
29900
- "GroupActions"
29901
- ],
29902
- "x-fc-domain": "communities",
29903
- "x-fc-permission": "communities:operate",
29904
- "x-fc-pending": "communities-group-actions",
29905
- parameters: [
29906
- {
29907
- name: "group",
29908
- in: "path",
29909
- required: true,
29910
- schema: {
29911
- type: "string"
29912
- }
29913
- }
29914
- ],
29915
- responses: {
30021
+ },
29916
30022
  "403": {
29917
- description: "route_not_available: the group actions by API are off in production.",
30023
+ description: "`numbers_not_included` or `number_limit_reached`",
29918
30024
  content: {
29919
30025
  "application/json": {
29920
30026
  schema: {
29921
- type: "object",
29922
- properties: {
29923
- error: {
29924
- type: "string",
29925
- const: "route_not_available"
29926
- },
29927
- message: {
29928
- type: "string"
29929
- }
29930
- },
29931
- required: [
29932
- "error",
29933
- "message"
29934
- ]
30027
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
29935
30028
  }
29936
30029
  }
29937
30030
  }
29938
- }
29939
- }
29940
- }
29941
- },
29942
- "/api/v1/groups/{group}/photo": {
29943
- put: {
29944
- operationId: "pendingSandboxGroupActions.groups.group.photo.put",
29945
- summary: "Group action by API (off)",
29946
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29947
- tags: [
29948
- "GroupActions"
29949
- ],
29950
- "x-fc-domain": "communities",
29951
- "x-fc-permission": "communities:operate",
29952
- "x-fc-pending": "communities-group-actions",
29953
- parameters: [
29954
- {
29955
- name: "group",
29956
- in: "path",
29957
- required: true,
29958
- schema: {
29959
- type: "string"
29960
- }
29961
- }
29962
- ],
29963
- responses: {
29964
- "403": {
29965
- description: "route_not_available: the group actions by API are off in production.",
30031
+ },
30032
+ "409": {
30033
+ description: "`operation_in_progress`",
29966
30034
  content: {
29967
30035
  "application/json": {
29968
30036
  schema: {
29969
- type: "object",
29970
- properties: {
29971
- error: {
29972
- type: "string",
29973
- const: "route_not_available"
29974
- },
29975
- message: {
29976
- type: "string"
29977
- }
29978
- },
29979
- required: [
29980
- "error",
29981
- "message"
29982
- ]
30037
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
29983
30038
  }
29984
30039
  }
29985
30040
  }
29986
- }
29987
- }
29988
- }
29989
- },
29990
- "/api/v1/groups/{group}/settings": {
29991
- put: {
29992
- operationId: "pendingSandboxGroupActions.groups.group.settings.put",
29993
- summary: "Group action by API (off)",
29994
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
29995
- tags: [
29996
- "GroupActions"
29997
- ],
29998
- "x-fc-domain": "communities",
29999
- "x-fc-permission": "communities:operate",
30000
- "x-fc-pending": "communities-group-actions",
30001
- parameters: [
30002
- {
30003
- name: "group",
30004
- in: "path",
30005
- required: true,
30006
- schema: {
30007
- type: "string"
30041
+ },
30042
+ "422": {
30043
+ description: "`invalid_request` (with `errors`), `invalid_reference_device`, `connection_type_mismatch`, `qr_requires_coexistence`, `method_already_present`, `idempotency_key_mismatch` or `invalid_idempotency_key`",
30044
+ content: {
30045
+ "application/json": {
30046
+ schema: {
30047
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
30048
+ }
30049
+ }
30008
30050
  }
30009
- }
30010
- ],
30011
- responses: {
30012
- "403": {
30013
- description: "route_not_available: the group actions by API are off in production.",
30051
+ },
30052
+ "429": {
30053
+ description: '`too_many_open_links`, or the per-minute limit (`{"message": "Too Many Attempts."}`)',
30014
30054
  content: {
30015
30055
  "application/json": {
30016
30056
  schema: {
30017
- type: "object",
30018
- properties: {
30019
- error: {
30020
- type: "string",
30021
- const: "route_not_available"
30057
+ oneOf: [
30058
+ {
30059
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
30022
30060
  },
30023
- message: {
30024
- type: "string"
30061
+ {
30062
+ $ref: "#/components/schemas/PendingSandboxTooManyAttempts"
30025
30063
  }
30026
- },
30027
- required: [
30028
- "error",
30029
- "message"
30030
30064
  ]
30031
30065
  }
30032
30066
  }
30033
30067
  }
30034
- }
30035
- }
30036
- }
30037
- },
30038
- "/api/v1/groups/{group}/invite-link/renew": {
30039
- post: {
30040
- operationId: "pendingSandboxGroupActions.groups.group.invite-link.renew.post",
30041
- summary: "Group action by API (off)",
30042
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
30043
- tags: [
30044
- "GroupActions"
30045
- ],
30046
- "x-fc-domain": "communities",
30047
- "x-fc-permission": "communities:operate",
30048
- "x-fc-pending": "communities-group-actions",
30049
- parameters: [
30050
- {
30051
- name: "group",
30052
- in: "path",
30053
- required: true,
30054
- schema: {
30055
- type: "string"
30056
- }
30057
- }
30058
- ],
30059
- responses: {
30060
- "403": {
30061
- description: "route_not_available: the group actions by API are off in production.",
30068
+ },
30069
+ "503": {
30070
+ description: "`meta_connect_disabled`",
30062
30071
  content: {
30063
30072
  "application/json": {
30064
30073
  schema: {
30065
- type: "object",
30066
- properties: {
30067
- error: {
30068
- type: "string",
30069
- const: "route_not_available"
30070
- },
30071
- message: {
30072
- type: "string"
30073
- }
30074
- },
30075
- required: [
30076
- "error",
30077
- "message"
30078
- ]
30074
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
30079
30075
  }
30080
30076
  }
30081
30077
  }
30082
30078
  }
30083
- }
30079
+ },
30080
+ "x-fc-validation-error": "invalid_request"
30084
30081
  }
30085
30082
  },
30086
- "/api/v1/groups/{group}/invite-link/live": {
30083
+ "/api/v1/connect-links/{link}": {
30087
30084
  get: {
30088
- operationId: "pendingSandboxGroupActions.groups.group.invite-link.live.get",
30089
- summary: "Group action by API (off)",
30090
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
30085
+ operationId: "pendingSandboxConnectLinks.show",
30086
+ summary: "Show a sign-up link",
30087
+ description: "The link without its token: its `status`, the `device_id` it created and its `error_code`.\n\n**Pending of the spec**: from the contract of the sign-up by API (22-10, A1), while accounts writes it; its OpenAPI will be the final source.",
30091
30088
  tags: [
30092
- "GroupActions"
30089
+ "ConnectLink"
30093
30090
  ],
30094
- "x-fc-domain": "communities",
30095
- "x-fc-permission": "communities:operate",
30096
- "x-fc-pending": "communities-group-actions",
30091
+ "x-fc-domain": "accounts",
30092
+ "x-fc-permission": "accounts:read",
30093
+ "x-fc-pending": "accounts-alta-de-numeros",
30097
30094
  parameters: [
30098
30095
  {
30099
- name: "group",
30096
+ name: "link",
30100
30097
  in: "path",
30101
30098
  required: true,
30102
30099
  schema: {
30103
- type: "string"
30100
+ type: "integer"
30104
30101
  }
30105
30102
  }
30106
30103
  ],
30107
30104
  responses: {
30108
- "403": {
30109
- description: "route_not_available: the group actions by API are off in production.",
30105
+ "200": {
30106
+ description: "OK",
30110
30107
  content: {
30111
30108
  "application/json": {
30112
30109
  schema: {
30113
30110
  type: "object",
30114
30111
  properties: {
30115
- error: {
30116
- type: "string",
30117
- const: "route_not_available"
30118
- },
30119
- message: {
30120
- type: "string"
30112
+ data: {
30113
+ $ref: "#/components/schemas/PendingSandboxConnectLink"
30121
30114
  }
30122
30115
  },
30123
30116
  required: [
30124
- "error",
30125
- "message"
30117
+ "data"
30126
30118
  ]
30127
30119
  }
30128
30120
  }
30129
30121
  }
30130
- }
30131
- }
30132
- }
30133
- },
30134
- "/api/v1/groups/{group}/join-requests": {
30135
- get: {
30136
- operationId: "pendingSandboxGroupActions.groups.group.join-requests.get",
30137
- summary: "Group action by API (off)",
30138
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
30139
- tags: [
30140
- "GroupActions"
30141
- ],
30142
- "x-fc-domain": "communities",
30143
- "x-fc-permission": "communities:operate",
30144
- "x-fc-pending": "communities-group-actions",
30145
- parameters: [
30146
- {
30147
- name: "group",
30148
- in: "path",
30149
- required: true,
30150
- schema: {
30151
- type: "string"
30152
- }
30153
- }
30154
- ],
30155
- responses: {
30156
- "403": {
30157
- description: "route_not_available: the group actions by API are off in production.",
30122
+ },
30123
+ "404": {
30124
+ description: "`not_found`",
30158
30125
  content: {
30159
30126
  "application/json": {
30160
30127
  schema: {
30161
- type: "object",
30162
- properties: {
30163
- error: {
30164
- type: "string",
30165
- const: "route_not_available"
30166
- },
30167
- message: {
30168
- type: "string"
30169
- }
30170
- },
30171
- required: [
30172
- "error",
30173
- "message"
30174
- ]
30128
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
30175
30129
  }
30176
30130
  }
30177
30131
  }
30178
30132
  }
30179
30133
  }
30180
- }
30181
- },
30182
- "/api/v1/groups/{group}/join-requests/approve": {
30183
- post: {
30184
- operationId: "pendingSandboxGroupActions.groups.group.join-requests.approve.post",
30185
- summary: "Group action by API (off)",
30186
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
30134
+ },
30135
+ delete: {
30136
+ operationId: "pendingSandboxConnectLinks.destroy",
30137
+ summary: "Revoke a sign-up link",
30138
+ description: "Revokes a `pending` link: `204` without a body. `409 link_not_pending` if it is not pending any more; `404` if it does not exist.\n\nAmendment v0.1 (23-10): as accounts did it in `feat/alta-de-numeros-api` `52d49b319`.",
30187
30139
  tags: [
30188
- "GroupActions"
30140
+ "ConnectLink"
30189
30141
  ],
30190
- "x-fc-domain": "communities",
30191
- "x-fc-permission": "communities:operate",
30192
- "x-fc-pending": "communities-group-actions",
30142
+ "x-fc-domain": "accounts",
30143
+ "x-fc-permission": "accounts:operate",
30144
+ "x-fc-pending": "accounts-alta-de-numeros",
30193
30145
  parameters: [
30194
30146
  {
30195
- name: "group",
30147
+ name: "link",
30196
30148
  in: "path",
30197
30149
  required: true,
30198
30150
  schema: {
30199
- type: "string"
30151
+ type: "integer"
30200
30152
  }
30201
30153
  }
30202
30154
  ],
30203
30155
  responses: {
30204
- "403": {
30205
- description: "route_not_available: the group actions by API are off in production.",
30156
+ "204": {
30157
+ description: "Revoked"
30158
+ },
30159
+ "404": {
30160
+ description: "`not_found`",
30206
30161
  content: {
30207
30162
  "application/json": {
30208
30163
  schema: {
30209
- type: "object",
30210
- properties: {
30211
- error: {
30212
- type: "string",
30213
- const: "route_not_available"
30214
- },
30215
- message: {
30216
- type: "string"
30217
- }
30218
- },
30219
- required: [
30220
- "error",
30221
- "message"
30222
- ]
30164
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
30223
30165
  }
30224
30166
  }
30225
30167
  }
30226
- }
30227
- }
30228
- }
30229
- },
30230
- "/api/v1/groups/{group}/join-requests/reject": {
30231
- post: {
30232
- operationId: "pendingSandboxGroupActions.groups.group.join-requests.reject.post",
30233
- summary: "Group action by API (off)",
30234
- description: "It exists in communities but is off in production: any key gets `403 route_not_available`.\n\n**Pending of the spec**: it is in production, missing from the portal's snapshot.",
30235
- tags: [
30236
- "GroupActions"
30237
- ],
30238
- "x-fc-domain": "communities",
30239
- "x-fc-permission": "communities:operate",
30240
- "x-fc-pending": "communities-group-actions",
30241
- parameters: [
30242
- {
30243
- name: "group",
30244
- in: "path",
30245
- required: true,
30246
- schema: {
30247
- type: "string"
30248
- }
30249
- }
30250
- ],
30251
- responses: {
30252
- "403": {
30253
- description: "route_not_available: the group actions by API are off in production.",
30168
+ },
30169
+ "409": {
30170
+ description: "`link_not_pending`",
30254
30171
  content: {
30255
30172
  "application/json": {
30256
30173
  schema: {
30257
- type: "object",
30258
- properties: {
30259
- error: {
30260
- type: "string",
30261
- const: "route_not_available"
30262
- },
30263
- message: {
30264
- type: "string"
30265
- }
30266
- },
30267
- required: [
30268
- "error",
30269
- "message"
30270
- ]
30174
+ $ref: "#/components/schemas/PendingSandboxAccountsError"
30271
30175
  }
30272
30176
  }
30273
30177
  }
@@ -32471,18 +32375,43 @@ var openapi_default = {
32471
32375
  "string",
32472
32376
  "null"
32473
32377
  ],
32474
- description: "Solo d\xEDgitos, sin `+`."
32378
+ description: "Digits only, without `+`."
32379
+ },
32380
+ connection: {
32381
+ type: "string",
32382
+ enum: [
32383
+ "qr",
32384
+ "coexistence",
32385
+ "dedicated"
32386
+ ]
32387
+ },
32388
+ external_reference: {
32389
+ type: [
32390
+ "string",
32391
+ "null"
32392
+ ]
32475
32393
  }
32476
32394
  },
32477
32395
  required: [
32478
32396
  "id",
32479
32397
  "name",
32480
- "number"
32481
- ]
32398
+ "number",
32399
+ "connection",
32400
+ "external_reference"
32401
+ ],
32402
+ "x-fc-pending": "accounts-alta-de-numeros"
32482
32403
  },
32483
32404
  connected_at: {
32484
32405
  type: "string",
32485
32406
  format: "date-time"
32407
+ },
32408
+ connect_link_id: {
32409
+ type: [
32410
+ "integer",
32411
+ "null"
32412
+ ],
32413
+ description: "The sign-up link of this connection, or `null`. Optional until accounts deploys it.",
32414
+ "x-fc-pending": "accounts-alta-de-numeros"
32486
32415
  }
32487
32416
  },
32488
32417
  required: [
@@ -32491,6 +32420,23 @@ var openapi_default = {
32491
32420
  ]
32492
32421
  }
32493
32422
  }
32423
+ },
32424
+ example: {
32425
+ id: "evt_0f8e4b526c1d4c7a9a3e2b5d",
32426
+ type: "device.connected",
32427
+ version: 1,
32428
+ occurred_at: "2026-10-22T03:00:00Z",
32429
+ data: {
32430
+ device: {
32431
+ id: 4521,
32432
+ name: "Ventas",
32433
+ number: "5491100000000",
32434
+ connection: "qr",
32435
+ external_reference: "cliente-8841"
32436
+ },
32437
+ connected_at: "2026-10-22T03:00:00+00:00",
32438
+ connect_link_id: 812
32439
+ }
32494
32440
  }
32495
32441
  }
32496
32442
  }
@@ -32530,18 +32476,56 @@ var openapi_default = {
32530
32476
  "string",
32531
32477
  "null"
32532
32478
  ],
32533
- description: "Solo d\xEDgitos, sin `+`."
32479
+ description: "Digits only, without `+`."
32480
+ },
32481
+ connection: {
32482
+ type: "string",
32483
+ enum: [
32484
+ "qr",
32485
+ "coexistence",
32486
+ "dedicated"
32487
+ ]
32488
+ },
32489
+ external_reference: {
32490
+ type: [
32491
+ "string",
32492
+ "null"
32493
+ ]
32534
32494
  }
32535
32495
  },
32536
32496
  required: [
32537
32497
  "id",
32538
32498
  "name",
32539
- "number"
32540
- ]
32499
+ "number",
32500
+ "connection",
32501
+ "external_reference"
32502
+ ],
32503
+ "x-fc-pending": "accounts-alta-de-numeros"
32541
32504
  },
32542
32505
  disconnected_at: {
32543
32506
  type: "string",
32544
32507
  format: "date-time"
32508
+ },
32509
+ reason: {
32510
+ type: "string",
32511
+ enum: [
32512
+ "qr_logged_out",
32513
+ "meta_account_not_registered",
32514
+ "shadow_ban_protection",
32515
+ "provider_error",
32516
+ "unknown",
32517
+ "disconnected_by_user"
32518
+ ],
32519
+ description: "`disconnected_by_user` [part 2]: \xABDesconectar\xBB or \xABBorrar datos\xBB of an official number in the panel.",
32520
+ "x-fc-pending": "accounts-alta-de-numeros"
32521
+ },
32522
+ connect_link_id: {
32523
+ type: [
32524
+ "integer",
32525
+ "null"
32526
+ ],
32527
+ description: "The sign-up link that connected this number last, or `null`. Optional until accounts deploys it.",
32528
+ "x-fc-pending": "accounts-alta-de-numeros"
32545
32529
  }
32546
32530
  },
32547
32531
  required: [
@@ -32550,6 +32534,24 @@ var openapi_default = {
32550
32534
  ]
32551
32535
  }
32552
32536
  }
32537
+ },
32538
+ example: {
32539
+ id: "evt_0f8e4b526c1d4c7a9a3e2b5d",
32540
+ type: "device.disconnected",
32541
+ version: 1,
32542
+ occurred_at: "2026-10-22T03:00:00Z",
32543
+ data: {
32544
+ device: {
32545
+ id: 4521,
32546
+ name: "Ventas",
32547
+ number: "5491100000000",
32548
+ connection: "dedicated",
32549
+ external_reference: "cliente-8841"
32550
+ },
32551
+ disconnected_at: "2026-10-22T03:00:00+00:00",
32552
+ reason: "meta_account_not_registered",
32553
+ connect_link_id: null
32554
+ }
32553
32555
  }
32554
32556
  }
32555
32557
  }
@@ -33018,6 +33020,226 @@ var openapi_default = {
33018
33020
  }
33019
33021
  }
33020
33022
  }
33023
+ },
33024
+ "device.deleted": {
33025
+ post: {
33026
+ summary: "device.deleted",
33027
+ description: "A number of the account was deleted: its place of the quota is free again. Emitted once per number, before the row goes.\n\n**Pending of the spec**: from the contract of the sign-up by API (22-10, A3), while accounts writes it; its OpenAPI will be the final source.",
33028
+ "x-fc-status": "live",
33029
+ "x-fc-domain": "accounts",
33030
+ "x-fc-pending": "accounts-alta-de-numeros",
33031
+ requestBody: {
33032
+ content: {
33033
+ "application/json": {
33034
+ schema: {
33035
+ type: "object",
33036
+ properties: {
33037
+ data: {
33038
+ type: "object",
33039
+ properties: {
33040
+ device: {
33041
+ type: "object",
33042
+ properties: {
33043
+ id: {
33044
+ type: "integer"
33045
+ },
33046
+ name: {
33047
+ type: [
33048
+ "string",
33049
+ "null"
33050
+ ]
33051
+ },
33052
+ number: {
33053
+ type: [
33054
+ "string",
33055
+ "null"
33056
+ ],
33057
+ description: "Digits only, without `+`."
33058
+ },
33059
+ connection: {
33060
+ type: "string",
33061
+ enum: [
33062
+ "qr",
33063
+ "coexistence",
33064
+ "dedicated"
33065
+ ]
33066
+ },
33067
+ external_reference: {
33068
+ type: [
33069
+ "string",
33070
+ "null"
33071
+ ]
33072
+ }
33073
+ },
33074
+ required: [
33075
+ "id",
33076
+ "name",
33077
+ "number",
33078
+ "connection",
33079
+ "external_reference"
33080
+ ]
33081
+ },
33082
+ deleted_at: {
33083
+ type: "string",
33084
+ format: "date-time"
33085
+ },
33086
+ connect_link_id: {
33087
+ type: [
33088
+ "integer",
33089
+ "null"
33090
+ ],
33091
+ description: "The sign-up link that connected this number last, or `null`. Optional until accounts deploys it."
33092
+ }
33093
+ },
33094
+ required: [
33095
+ "device",
33096
+ "deleted_at"
33097
+ ]
33098
+ }
33099
+ }
33100
+ },
33101
+ example: {
33102
+ id: "evt_0f8e4b526c1d4c7a9a3e2b5d",
33103
+ type: "device.deleted",
33104
+ version: 1,
33105
+ occurred_at: "2026-10-22T03:00:00Z",
33106
+ data: {
33107
+ device: {
33108
+ id: 4521,
33109
+ name: "Ventas",
33110
+ number: "5491100000000",
33111
+ connection: "qr",
33112
+ external_reference: "cliente-8841"
33113
+ },
33114
+ deleted_at: "2026-10-22T03:00:00+00:00",
33115
+ connect_link_id: 812
33116
+ }
33117
+ }
33118
+ }
33119
+ }
33120
+ }
33121
+ }
33122
+ },
33123
+ "device.health_changed": {
33124
+ post: {
33125
+ summary: "device.health_changed",
33126
+ description: "The aggregated health of an official number changed (not on every webhook of Meta).\n\n**Pending of the spec**: from the contract of the sign-up by API (22-10, A3), while accounts writes it; its OpenAPI will be the final source. Pending of the user: PR #1103 of accounts (open) defines `state: { previous, current }`; accounts implemented this contract. One of the two has to be chosen before merging.",
33127
+ "x-fc-status": "live",
33128
+ "x-fc-domain": "accounts",
33129
+ "x-fc-pending": "accounts-alta-de-numeros",
33130
+ requestBody: {
33131
+ content: {
33132
+ "application/json": {
33133
+ schema: {
33134
+ type: "object",
33135
+ properties: {
33136
+ data: {
33137
+ type: "object",
33138
+ properties: {
33139
+ device: {
33140
+ type: "object",
33141
+ properties: {
33142
+ id: {
33143
+ type: "integer"
33144
+ },
33145
+ name: {
33146
+ type: [
33147
+ "string",
33148
+ "null"
33149
+ ]
33150
+ },
33151
+ number: {
33152
+ type: [
33153
+ "string",
33154
+ "null"
33155
+ ],
33156
+ description: "Digits only, without `+`."
33157
+ },
33158
+ connection: {
33159
+ type: "string",
33160
+ enum: [
33161
+ "qr",
33162
+ "coexistence",
33163
+ "dedicated"
33164
+ ]
33165
+ },
33166
+ external_reference: {
33167
+ type: [
33168
+ "string",
33169
+ "null"
33170
+ ]
33171
+ }
33172
+ },
33173
+ required: [
33174
+ "id",
33175
+ "name",
33176
+ "number",
33177
+ "connection",
33178
+ "external_reference"
33179
+ ]
33180
+ },
33181
+ previous_state: {
33182
+ type: "string",
33183
+ enum: [
33184
+ "available",
33185
+ "limited",
33186
+ "blocked",
33187
+ "unknown"
33188
+ ]
33189
+ },
33190
+ state: {
33191
+ type: "string",
33192
+ enum: [
33193
+ "available",
33194
+ "limited",
33195
+ "blocked",
33196
+ "unknown"
33197
+ ]
33198
+ },
33199
+ changed_at: {
33200
+ type: "string",
33201
+ format: "date-time"
33202
+ },
33203
+ connect_link_id: {
33204
+ type: [
33205
+ "integer",
33206
+ "null"
33207
+ ],
33208
+ description: "The sign-up link that connected this number last, or `null`. Optional until accounts deploys it."
33209
+ }
33210
+ },
33211
+ required: [
33212
+ "device",
33213
+ "previous_state",
33214
+ "state",
33215
+ "changed_at"
33216
+ ]
33217
+ }
33218
+ }
33219
+ },
33220
+ example: {
33221
+ id: "evt_0f8e4b526c1d4c7a9a3e2b5d",
33222
+ type: "device.health_changed",
33223
+ version: 1,
33224
+ occurred_at: "2026-10-22T03:00:00Z",
33225
+ data: {
33226
+ device: {
33227
+ id: 4521,
33228
+ name: "Ventas",
33229
+ number: "5491100000000",
33230
+ connection: "dedicated",
33231
+ external_reference: "cliente-8841"
33232
+ },
33233
+ previous_state: "available",
33234
+ state: "limited",
33235
+ changed_at: "2026-10-22T03:00:00+00:00",
33236
+ connect_link_id: 812
33237
+ }
33238
+ }
33239
+ }
33240
+ }
33241
+ }
33242
+ }
33021
33243
  }
33022
33244
  },
33023
33245
  components: {
@@ -42103,6 +42325,345 @@ var openapi_default = {
42103
42325
  required: [
42104
42326
  "message"
42105
42327
  ]
42328
+ },
42329
+ PendingSandboxAccountsError: {
42330
+ type: "object",
42331
+ description: "An error of accounts: `error` (the code) and `message`; some codes add keys (`device_id`, `limit`).",
42332
+ properties: {
42333
+ error: {
42334
+ type: "string"
42335
+ },
42336
+ message: {
42337
+ type: "string"
42338
+ }
42339
+ },
42340
+ required: [
42341
+ "error",
42342
+ "message"
42343
+ ]
42344
+ },
42345
+ PendingSandboxModelNotFound: {
42346
+ type: "object",
42347
+ description: 'The 404 of a model in accounts: only `message`, "Object not found".',
42348
+ properties: {
42349
+ message: {
42350
+ type: "string"
42351
+ }
42352
+ },
42353
+ required: [
42354
+ "message"
42355
+ ]
42356
+ },
42357
+ PendingSandboxDeviceQr: {
42358
+ type: "object",
42359
+ properties: {
42360
+ data: {
42361
+ type: "object",
42362
+ properties: {
42363
+ connected: {
42364
+ type: "boolean"
42365
+ },
42366
+ qr_code: {
42367
+ type: [
42368
+ "string",
42369
+ "null"
42370
+ ],
42371
+ description: "A PNG image as a data URI; `null` when the number is already connected."
42372
+ },
42373
+ expires_in: {
42374
+ type: "integer",
42375
+ description: "Seconds after which to ask for it again: 5 with a QR (ask for it again and show the new one at once: an old code does not link), 3 while the session starts, 0 once connected. Never `null`."
42376
+ }
42377
+ },
42378
+ required: [
42379
+ "connected",
42380
+ "qr_code",
42381
+ "expires_in"
42382
+ ]
42383
+ }
42384
+ },
42385
+ required: [
42386
+ "data"
42387
+ ]
42388
+ },
42389
+ PendingSandboxConnectLink: {
42390
+ type: "object",
42391
+ description: "A sign-up link, without its token.",
42392
+ properties: {
42393
+ id: {
42394
+ type: "integer"
42395
+ },
42396
+ type: {
42397
+ type: "string",
42398
+ enum: [
42399
+ "meta",
42400
+ "qr"
42401
+ ]
42402
+ },
42403
+ status: {
42404
+ type: "string",
42405
+ enum: [
42406
+ "pending",
42407
+ "completed",
42408
+ "failed",
42409
+ "expired",
42410
+ "revoked"
42411
+ ]
42412
+ },
42413
+ expires_at: {
42414
+ type: "string",
42415
+ format: "date-time"
42416
+ },
42417
+ allowed_connection_types: {
42418
+ type: [
42419
+ "array",
42420
+ "null"
42421
+ ],
42422
+ items: {
42423
+ type: "string",
42424
+ enum: [
42425
+ "coexistence",
42426
+ "dedicated"
42427
+ ]
42428
+ }
42429
+ },
42430
+ provision_us_number: {
42431
+ type: "boolean"
42432
+ },
42433
+ country: {
42434
+ type: [
42435
+ "string",
42436
+ "null"
42437
+ ],
42438
+ description: "`qr` links that create a number only: the country of the phone, two letters in lower case. `null` otherwise."
42439
+ },
42440
+ device_id: {
42441
+ type: [
42442
+ "integer",
42443
+ "null"
42444
+ ],
42445
+ description: "Only for a reconnection, or once the link is `completed`."
42446
+ },
42447
+ external_reference: {
42448
+ type: [
42449
+ "string",
42450
+ "null"
42451
+ ]
42452
+ },
42453
+ return_url: {
42454
+ type: [
42455
+ "string",
42456
+ "null"
42457
+ ]
42458
+ },
42459
+ error_code: {
42460
+ type: [
42461
+ "string",
42462
+ "null"
42463
+ ],
42464
+ enum: [
42465
+ "link_expired",
42466
+ "already_used",
42467
+ "revoked",
42468
+ "quota_exceeded",
42469
+ "trial_number_blocked",
42470
+ "facebook_auth_failed",
42471
+ "meta_error",
42472
+ "number_not_owned",
42473
+ "qr_timeout",
42474
+ "provider_error",
42475
+ null
42476
+ ]
42477
+ },
42478
+ created_at: {
42479
+ type: "string",
42480
+ format: "date-time"
42481
+ },
42482
+ completed_at: {
42483
+ type: [
42484
+ "string",
42485
+ "null"
42486
+ ],
42487
+ format: "date-time"
42488
+ }
42489
+ },
42490
+ required: [
42491
+ "id",
42492
+ "type",
42493
+ "status",
42494
+ "expires_at",
42495
+ "allowed_connection_types",
42496
+ "provision_us_number",
42497
+ "country",
42498
+ "device_id",
42499
+ "external_reference",
42500
+ "return_url",
42501
+ "error_code",
42502
+ "created_at",
42503
+ "completed_at"
42504
+ ]
42505
+ },
42506
+ PendingSandboxConnectLinkCreated: {
42507
+ type: "object",
42508
+ description: "The link as created: the only answer that carries `url`, with the raw token.",
42509
+ properties: {
42510
+ id: {
42511
+ type: "integer"
42512
+ },
42513
+ type: {
42514
+ type: "string",
42515
+ enum: [
42516
+ "meta",
42517
+ "qr"
42518
+ ]
42519
+ },
42520
+ status: {
42521
+ type: "string",
42522
+ enum: [
42523
+ "pending",
42524
+ "completed",
42525
+ "failed",
42526
+ "expired",
42527
+ "revoked"
42528
+ ]
42529
+ },
42530
+ expires_at: {
42531
+ type: "string",
42532
+ format: "date-time"
42533
+ },
42534
+ allowed_connection_types: {
42535
+ type: [
42536
+ "array",
42537
+ "null"
42538
+ ],
42539
+ items: {
42540
+ type: "string",
42541
+ enum: [
42542
+ "coexistence",
42543
+ "dedicated"
42544
+ ]
42545
+ }
42546
+ },
42547
+ provision_us_number: {
42548
+ type: "boolean"
42549
+ },
42550
+ country: {
42551
+ type: [
42552
+ "string",
42553
+ "null"
42554
+ ],
42555
+ description: "`qr` links that create a number only: the country of the phone, two letters in lower case. `null` otherwise."
42556
+ },
42557
+ device_id: {
42558
+ type: [
42559
+ "integer",
42560
+ "null"
42561
+ ],
42562
+ description: "Only for a reconnection, or once the link is `completed`."
42563
+ },
42564
+ external_reference: {
42565
+ type: [
42566
+ "string",
42567
+ "null"
42568
+ ]
42569
+ },
42570
+ return_url: {
42571
+ type: [
42572
+ "string",
42573
+ "null"
42574
+ ]
42575
+ },
42576
+ error_code: {
42577
+ type: [
42578
+ "string",
42579
+ "null"
42580
+ ],
42581
+ enum: [
42582
+ "link_expired",
42583
+ "already_used",
42584
+ "revoked",
42585
+ "quota_exceeded",
42586
+ "trial_number_blocked",
42587
+ "facebook_auth_failed",
42588
+ "meta_error",
42589
+ "number_not_owned",
42590
+ "qr_timeout",
42591
+ "provider_error",
42592
+ null
42593
+ ]
42594
+ },
42595
+ created_at: {
42596
+ type: "string",
42597
+ format: "date-time"
42598
+ },
42599
+ completed_at: {
42600
+ type: [
42601
+ "string",
42602
+ "null"
42603
+ ],
42604
+ format: "date-time"
42605
+ },
42606
+ url: {
42607
+ type: "string",
42608
+ format: "uri",
42609
+ description: "`https://api.funnelchat.com/connect/<token>`, a 43-character base64url token. Shown once."
42610
+ }
42611
+ },
42612
+ required: [
42613
+ "id",
42614
+ "type",
42615
+ "status",
42616
+ "expires_at",
42617
+ "allowed_connection_types",
42618
+ "provision_us_number",
42619
+ "country",
42620
+ "device_id",
42621
+ "external_reference",
42622
+ "return_url",
42623
+ "error_code",
42624
+ "created_at",
42625
+ "completed_at",
42626
+ "url"
42627
+ ]
42628
+ },
42629
+ PendingSandboxInvalidRequest: {
42630
+ type: "object",
42631
+ description: "The `422` of these routes: `invalid_request` with the errors per field.",
42632
+ properties: {
42633
+ error: {
42634
+ type: "string",
42635
+ const: "invalid_request"
42636
+ },
42637
+ message: {
42638
+ type: "string"
42639
+ },
42640
+ errors: {
42641
+ type: "object",
42642
+ additionalProperties: {
42643
+ type: "array",
42644
+ items: {
42645
+ type: "string"
42646
+ }
42647
+ }
42648
+ }
42649
+ },
42650
+ required: [
42651
+ "error",
42652
+ "message",
42653
+ "errors"
42654
+ ]
42655
+ },
42656
+ PendingSandboxTooManyAttempts: {
42657
+ type: "object",
42658
+ description: 'Laravel\'s `429` of the per-minute limit: only `message`, "Too Many Attempts.", with `Retry-After`.',
42659
+ properties: {
42660
+ message: {
42661
+ type: "string"
42662
+ }
42663
+ },
42664
+ required: [
42665
+ "message"
42666
+ ]
42106
42667
  }
42107
42668
  },
42108
42669
  responses: {
@@ -42522,7 +43083,7 @@ var openapi_default = {
42522
43083
  inputs: {
42523
43084
  "spec/funnelchat.json": "8bbbae569b4cab520884db98f28204156c1bd6c629578bee609343c193e0dadb",
42524
43085
  "spec/pending.json": "a14bc4dbc3025d3e958ffda8afd1fa9fdab34af264418060825f3300799aceb0",
42525
- "spec/pending-sandbox.json": "d915e3b23cbc1738d999eb2b582af2b205b7989c08361cee293f23b2ef278a0e"
43086
+ "spec/pending-sandbox.json": "b2f7c2ccbab7c90f7adf22f8c3cec6700ff647700571dc306c898af8127bb22d"
42526
43087
  },
42527
43088
  snapshot_commit: "0ef4abb008870ed8c7cae49a08900a81e5e57ed8",
42528
43089
  pending_commit: "88557ce7819c5a4061e84c58053a1b6c49f35ec2",
@@ -42581,31 +43142,28 @@ var openapi_default = {
42581
43142
  "GET /api/v1/forms/families/{familyId}/responses [pending-sandbox.json]",
42582
43143
  "GET /api/v1/forms/families/{familyId}/responses/{runId} [pending-sandbox.json]",
42583
43144
  "POST /api/v1/contacts/{contact}/message/form [pending-sandbox.json]",
42584
- "GET /api/v1/groups/actions/{action} [pending-sandbox.json]",
42585
- "POST /api/v1/groups [pending-sandbox.json]",
42586
- "POST /api/v1/groups/{group}/participants [pending-sandbox.json]",
42587
- "POST /api/v1/groups/{group}/participants/remove [pending-sandbox.json]",
42588
- "POST /api/v1/groups/{group}/admins [pending-sandbox.json]",
42589
- "POST /api/v1/groups/{group}/admins/remove [pending-sandbox.json]",
42590
- "PUT /api/v1/groups/{group}/name [pending-sandbox.json]",
42591
- "PUT /api/v1/groups/{group}/description [pending-sandbox.json]",
42592
- "PUT /api/v1/groups/{group}/photo [pending-sandbox.json]",
42593
- "PUT /api/v1/groups/{group}/settings [pending-sandbox.json]",
42594
- "POST /api/v1/groups/{group}/invite-link/renew [pending-sandbox.json]",
42595
- "POST /api/v1/groups/{group}/messages [pending-sandbox.json]",
42596
- "GET /api/v1/groups/{group}/invite-link/live [pending-sandbox.json]",
42597
- "GET /api/v1/groups/{group}/join-requests [pending-sandbox.json]",
42598
- "POST /api/v1/groups/{group}/join-requests/approve [pending-sandbox.json]",
42599
- "POST /api/v1/groups/{group}/join-requests/reject [pending-sandbox.json]",
43145
+ "POST /api/v1/devices [pending-sandbox.json]",
43146
+ "GET /api/v1/devices/{device}/qr [pending-sandbox.json]",
43147
+ "GET /api/v1/connect-links [pending-sandbox.json]",
43148
+ "POST /api/v1/connect-links [pending-sandbox.json]",
43149
+ "GET /api/v1/connect-links/{link} [pending-sandbox.json]",
43150
+ "DELETE /api/v1/connect-links/{link} [pending-sandbox.json]",
42600
43151
  "POST /api/v1/contacts/{contact}/message (cuerpo: file_type, file_name) [pending-sandbox.json]",
42601
43152
  "POST /api/v1/message/send-message (cuerpo: file_type, file_name) [pending-sandbox.json]",
43153
+ "PUT /api/v1/devices/{device} (cuerpo: external_reference) [pending-sandbox.json]",
43154
+ "GET /api/v1/devices (par\xE1metros: external_reference) [pending-sandbox.json]",
42602
43155
  "POST /api/v1/contacts/{contact}/message (respuestas: 200) [pending-sandbox.json]",
42603
43156
  "POST /api/v1/message/send-message (respuestas: 200) [pending-sandbox.json]",
42604
43157
  "POST /api/v1/devices/{device}/templates/{template}/send-template (respuestas: 200) [pending-sandbox.json]",
42605
43158
  "POST /api/v1/contacts/{contact}/message/contact-card (respuestas: 200) [pending-sandbox.json]",
42606
43159
  "POST /api/v1/contacts/{contact}/messages/{message}/forward (respuestas: 200) [pending-sandbox.json]",
43160
+ "DELETE /api/v1/devices/{device} (respuestas: 200) [pending-sandbox.json]",
43161
+ "webhook device.deleted [pending-sandbox.json]",
43162
+ "webhook device.health_changed [pending-sandbox.json]",
42607
43163
  "webhook message.received (data: device_id) [pending-sandbox.json]",
42608
- "webhook message.status_changed (data: error) [pending-sandbox.json]"
43164
+ "webhook message.status_changed (data: error) [pending-sandbox.json]",
43165
+ "webhook device.connected (data: device, connect_link_id) [pending-sandbox.json]",
43166
+ "webhook device.disconnected (data: device, reason, connect_link_id) [pending-sandbox.json]"
42609
43167
  ]
42610
43168
  }
42611
43169
  };
@@ -42684,8 +43242,8 @@ function successResponse(op) {
42684
43242
  const entries = Object.entries(op.responses ?? {}).filter(([status2]) => /^2\d\d$/.test(status2)).sort(([a], [b]) => Number(a) - Number(b));
42685
43243
  const [status, raw] = entries[0] ?? ["200", void 0];
42686
43244
  const response = deref(raw);
42687
- const json = response?.content?.["application/json"];
42688
- return { status: Number(status), schema: json?.schema, example: json?.example };
43245
+ const json2 = response?.content?.["application/json"];
43246
+ return { status: Number(status), schema: json2?.schema, example: json2?.example };
42689
43247
  }
42690
43248
 
42691
43249
  // src/openapi/example.ts
@@ -42787,7 +43345,49 @@ function specData(type) {
42787
43345
  }
42788
43346
  return {};
42789
43347
  }
42790
- var catalog = catalog_default.entries;
43348
+ var PENDING_DEVICE_ENTRIES = [
43349
+ {
43350
+ type: "device.deleted",
43351
+ area: "accounts",
43352
+ status: "active",
43353
+ is_critical: false,
43354
+ label: "N\xFAmero eliminado",
43355
+ description: "Uno de los n\xFAmeros de WhatsApp de la cuenta se elimin\xF3 y liber\xF3 su lugar del plan.",
43356
+ subscribable: true
43357
+ },
43358
+ {
43359
+ type: "device.health_changed",
43360
+ area: "accounts",
43361
+ status: "active",
43362
+ is_critical: false,
43363
+ label: "Cambi\xF3 la salud de un n\xFAmero",
43364
+ description: "Cambi\xF3 el estado de salud de uno de los n\xFAmeros oficiales de la cuenta: disponible, limitado, bloqueado o desconocido.",
43365
+ subscribable: true
43366
+ }
43367
+ ];
43368
+ function envelopeFromSpec(type) {
43369
+ for (const [key, item] of Object.entries(spec.webhooks ?? {})) {
43370
+ if (eventTypeOfKey(key) !== type) continue;
43371
+ const example = item.post?.requestBody?.content?.["application/json"]?.example;
43372
+ if (isRecord(example) && "data" in example) return structuredClone(example);
43373
+ }
43374
+ return null;
43375
+ }
43376
+ function withPendingDeviceEvents(entries) {
43377
+ const out = entries.map((entry) => {
43378
+ if (entry.type !== "device.connected" && entry.type !== "device.disconnected") return entry;
43379
+ const data = entry.example?.data;
43380
+ if (isRecord(data) && isRecord(data.device) && "connection" in data.device) return entry;
43381
+ const example = envelopeFromSpec(entry.type);
43382
+ return example ? { ...entry, example } : entry;
43383
+ });
43384
+ const missing = PENDING_DEVICE_ENTRIES.filter((pending) => !out.some((entry) => entry.type === pending.type));
43385
+ if (missing.length === 0) return out;
43386
+ const after = out.map((entry) => entry.type.startsWith("device.")).lastIndexOf(true) + 1;
43387
+ out.splice(after, 0, ...missing.map((pending) => ({ ...pending, example: envelopeFromSpec(pending.type) })));
43388
+ return out;
43389
+ }
43390
+ var catalog = withPendingDeviceEvents(catalog_default.entries);
42791
43391
  function catalogEntry(type) {
42792
43392
  return catalog.find((entry) => entry.type === type);
42793
43393
  }
@@ -43107,6 +43707,7 @@ var edgeError = (code) => {
43107
43707
  var methodNotAllowed = (method, allowed) => new ApiError(405, "method_not_allowed", `El m\xE9todo ${method} no est\xE1 disponible para esta ruta. Usa ${allowed.join(", ")}.`, {}, { Allow: allowed.join(", ") });
43108
43708
  var validationFailed = (errors) => new ApiError(422, "validation_failed", summarize(errors), { errors });
43109
43709
  var fieldError = (field, message) => validationFailed({ [field]: [message] });
43710
+ var invalidRequest = (errors) => new ApiError(422, "invalid_request", summarize(errors), { errors });
43110
43711
 
43111
43712
  // src/forms/templates.ts
43112
43713
  var FORM_TEMPLATES = [
@@ -43378,8 +43979,8 @@ function parseForced(mode, value, field) {
43378
43979
  function takeForcedFailure(app, headers, contactId) {
43379
43980
  const reject = headers["x-sandbox-reject"];
43380
43981
  if (reject) return { mode: "reject", code: parseForced("reject", reject, "X-Sandbox-Reject") };
43381
- const fail = headers["x-sandbox-fail"];
43382
- if (fail) return { mode: "fail", code: parseForced("fail", fail, "X-Sandbox-Fail") };
43982
+ const fail2 = headers["x-sandbox-fail"];
43983
+ if (fail2) return { mode: "fail", code: parseForced("fail", fail2, "X-Sandbox-Fail") };
43383
43984
  const armed = app.store.failures.find((f) => f.mode !== "edge" && f.remaining > 0 && (f.contactId === null || f.contactId === contactId));
43384
43985
  if (!armed) return null;
43385
43986
  armed.remaining--;
@@ -43555,7 +44156,7 @@ function resolveFile(body2, style) {
43555
44156
 
43556
44157
  // package.json
43557
44158
  var package_default = {
43558
- version: "0.1.0"};
44159
+ version: "0.2.0"};
43559
44160
 
43560
44161
  // src/version.ts
43561
44162
  var VERSION = package_default.version;
@@ -43591,6 +44192,13 @@ var Store = class {
43591
44192
  forms = /* @__PURE__ */ new Map();
43592
44193
  formFamilies = /* @__PURE__ */ new Map();
43593
44194
  formRuns = /* @__PURE__ */ new Map();
44195
+ connectLinks = /* @__PURE__ */ new Map();
44196
+ /** When each `POST /connect-links` arrived in the last minute: the per-minute limit. */
44197
+ linkAttempts = [];
44198
+ /** Answers by `Idempotency-Key`, per resource (`devices`, `connect-links`): one account. */
44199
+ idempotency = /* @__PURE__ */ new Map();
44200
+ /** The number quota of the account and the switch of the Meta sign-up (`POST /_sandbox/numbers`). */
44201
+ numbers = { limit: 5, included: true, trial: false, metaConnect: false, linksPerMinute: 10 };
43594
44202
  /** Last inbound message per `contactId:deviceId`: the 24-hour window. */
43595
44203
  lastInbound = /* @__PURE__ */ new Map();
43596
44204
  /** API-key sends per contact since its last inbound message: the antiblock counters. */
@@ -43613,7 +44221,9 @@ var Store = class {
43613
44221
  note: new Sequence(1),
43614
44222
  formFamily: new Sequence(8001),
43615
44223
  formVersion: new Sequence(8101),
43616
- formRun: new Sequence(1)
44224
+ formRun: new Sequence(1),
44225
+ device: new Sequence(5),
44226
+ connectLink: new Sequence(1)
43617
44227
  };
43618
44228
  logRequest(entry) {
43619
44229
  const full = { id: this.ids.request.next(), ...entry };
@@ -43647,8 +44257,14 @@ function seed(store, now = /* @__PURE__ */ new Date()) {
43647
44257
  provider: 2,
43648
44258
  kind: "cloud",
43649
44259
  connected: true,
44260
+ coexistence: false,
44261
+ fromPool: false,
44262
+ externalReference: null,
44263
+ health: "available",
44264
+ lastConnectLinkId: null,
43650
44265
  wabaId: "100000000000001",
43651
44266
  phoneNumberId: "100000000000003",
44267
+ firstConnectedAt: now,
43652
44268
  createdAt: now
43653
44269
  });
43654
44270
  store.devices.set(4, {
@@ -43658,8 +44274,14 @@ function seed(store, now = /* @__PURE__ */ new Date()) {
43658
44274
  provider: 1,
43659
44275
  kind: "qr",
43660
44276
  connected: true,
44277
+ coexistence: false,
44278
+ fromPool: false,
44279
+ externalReference: null,
44280
+ health: null,
44281
+ lastConnectLinkId: null,
43661
44282
  wabaId: null,
43662
44283
  phoneNumberId: null,
44284
+ firstConnectedAt: now,
43663
44285
  createdAt: now
43664
44286
  });
43665
44287
  const contactId = store.ids.contact.next();
@@ -43931,8 +44553,15 @@ var SandboxApp = class {
43931
44553
  webhooks;
43932
44554
  /** Grows on every reset, so work started before a reset does not touch the new state. */
43933
44555
  generation = 0;
44556
+ /** The address the sandbox listens on, once it does: the host of the sign-up links when `publicUrl` is not set. */
44557
+ listenUrl = null;
44558
+ /** `http://host:port` of the sign-up page: `publicUrl`, or where the sandbox listens. */
44559
+ get publicUrl() {
44560
+ return this.config.publicUrl ?? this.listenUrl ?? `http://localhost:${this.config.port}`;
44561
+ }
43934
44562
  populate() {
43935
44563
  seed(this.store);
44564
+ this.store.numbers = { ...this.config.numbers };
43936
44565
  for (const preset of this.config.subscriptions) {
43937
44566
  const unknown = preset.events.filter((e) => !isSubscribable(e));
43938
44567
  if (unknown.length) this.logger.warn(`subscription from the configuration with types that are not in the catalog: ${unknown.join(", ")}`);
@@ -43979,6 +44608,49 @@ function listContext(ctx) {
43979
44608
  function body(ctx) {
43980
44609
  return typeof ctx.body === "object" && ctx.body !== null && !Array.isArray(ctx.body) ? ctx.body : {};
43981
44610
  }
44611
+ var IDEMPOTENCY_HEADER = "Idempotency-Key";
44612
+ var REPLAYED_HEADER = "Idempotent-Replayed";
44613
+ var TTL_MS = 24 * 60 * 60 * 1e3;
44614
+ var running = /* @__PURE__ */ new WeakMap();
44615
+ function canonical(value) {
44616
+ if (Array.isArray(value)) return value.map(canonical);
44617
+ if (typeof value !== "object" || value === null) return value;
44618
+ return Object.fromEntries(
44619
+ Object.keys(value).sort().map((key) => [key, canonical(value[key])])
44620
+ );
44621
+ }
44622
+ async function idempotent(ctx, resource, run) {
44623
+ const key = ctx.headers["idempotency-key"];
44624
+ if (key === void 0) return run();
44625
+ if (key.length < 1 || key.length > 255 || !/^[\x21-\x7e]+$/.test(key)) {
44626
+ throw new ApiError(422, "invalid_idempotency_key", `El header ${IDEMPOTENCY_HEADER} debe tener entre 1 y 255 caracteres visibles, sin espacios.`);
44627
+ }
44628
+ const { store } = ctx.app;
44629
+ const now = Date.now();
44630
+ for (const [k, answer] of store.idempotency) if (now - answer.storedAt.getTime() >= TTL_MS) store.idempotency.delete(k);
44631
+ const slot = `${resource}|${key}`;
44632
+ const fingerprint = createHash("sha256").update(JSON.stringify([ctx.op.method, ctx.pathname, canonical(ctx.body ?? null)])).digest("hex");
44633
+ const stored = store.idempotency.get(slot);
44634
+ if (stored) {
44635
+ if (stored.fingerprint !== fingerprint) {
44636
+ throw new ApiError(422, "idempotency_key_mismatch", `Ya usaste este ${IDEMPOTENCY_HEADER} con otro cuerpo. Usa una clave nueva para otra operaci\xF3n.`);
44637
+ }
44638
+ return { status: stored.status, body: structuredClone(stored.body), headers: { ...stored.headers, [REPLAYED_HEADER]: "true" } };
44639
+ }
44640
+ const inFlight = running.get(ctx.app) ?? /* @__PURE__ */ new Set();
44641
+ running.set(ctx.app, inFlight);
44642
+ if (inFlight.has(slot)) throw new ApiError(409, "operation_in_progress", `Una operaci\xF3n con este ${IDEMPOTENCY_HEADER} todav\xEDa est\xE1 en curso.`);
44643
+ inFlight.add(slot);
44644
+ try {
44645
+ const result = await run();
44646
+ if (result.status >= 200 && result.status < 300) {
44647
+ store.idempotency.set(slot, { fingerprint, status: result.status, body: structuredClone(result.body), headers: { ...result.headers }, storedAt: /* @__PURE__ */ new Date() });
44648
+ }
44649
+ return result;
44650
+ } finally {
44651
+ inFlight.delete(slot);
44652
+ }
44653
+ }
43982
44654
 
43983
44655
  // src/http/paginate.ts
43984
44656
  function perPageOf(query, fallback, max = 100) {
@@ -44000,27 +44672,27 @@ function urlWith(path, query, extra) {
44000
44672
  function paginate(items, ctx, map, fallbackPerPage = 25) {
44001
44673
  const perPage = perPageOf(ctx.query, fallbackPerPage);
44002
44674
  const pageRaw = Number(ctx.query.page ?? 1);
44003
- const page = Number.isInteger(pageRaw) && pageRaw > 0 ? pageRaw : 1;
44675
+ const page2 = Number.isInteger(pageRaw) && pageRaw > 0 ? pageRaw : 1;
44004
44676
  const total = items.length;
44005
44677
  const lastPage = Math.max(1, Math.ceil(total / perPage));
44006
- const slice = items.slice((page - 1) * perPage, page * perPage);
44007
- const from = slice.length ? (page - 1) * perPage + 1 : null;
44678
+ const slice = items.slice((page2 - 1) * perPage, page2 * perPage);
44679
+ const from = slice.length ? (page2 - 1) * perPage + 1 : null;
44008
44680
  const to = slice.length ? from + slice.length - 1 : null;
44009
44681
  const link = (n) => urlWith(ctx.path, ctx.query, { page: n });
44010
44682
  const pageLinks = [
44011
- { url: page > 1 ? link(page - 1) : null, label: "&laquo; Anterior", active: false },
44012
- ...Array.from({ length: lastPage }, (_, i) => ({ url: link(i + 1), label: String(i + 1), active: i + 1 === page })),
44013
- { url: page < lastPage ? link(page + 1) : null, label: "Siguiente &raquo;", active: false }
44683
+ { url: page2 > 1 ? link(page2 - 1) : null, label: "&laquo; Anterior", active: false },
44684
+ ...Array.from({ length: lastPage }, (_, i) => ({ url: link(i + 1), label: String(i + 1), active: i + 1 === page2 })),
44685
+ { url: page2 < lastPage ? link(page2 + 1) : null, label: "Siguiente &raquo;", active: false }
44014
44686
  ];
44015
44687
  return {
44016
44688
  data: slice.map(map),
44017
44689
  links: {
44018
44690
  first: link(1),
44019
44691
  last: link(lastPage),
44020
- prev: page > 1 ? link(page - 1) : null,
44021
- next: page < lastPage ? link(page + 1) : null
44692
+ prev: page2 > 1 ? link(page2 - 1) : null,
44693
+ next: page2 < lastPage ? link(page2 + 1) : null
44022
44694
  },
44023
- meta: { current_page: page, from, last_page: lastPage, links: pageLinks, path: ctx.path, per_page: perPage, to, total }
44695
+ meta: { current_page: page2, from, last_page: lastPage, links: pageLinks, path: ctx.path, per_page: perPage, to, total }
44024
44696
  };
44025
44697
  }
44026
44698
  function encodeCursor(position) {
@@ -44227,7 +44899,7 @@ function deviceCapabilities(device) {
44227
44899
  function deviceHealth(device) {
44228
44900
  if (device.kind !== "cloud") return null;
44229
44901
  return {
44230
- state: device.connected ? "available" : "unknown",
44902
+ state: device.health ?? "unknown",
44231
44903
  reason: null,
44232
44904
  reason_truncated: false,
44233
44905
  reason_key: null,
@@ -44239,15 +44911,19 @@ function deviceHealth(device) {
44239
44911
  stale: false
44240
44912
  };
44241
44913
  }
44914
+ function deviceConnection(device) {
44915
+ if (device.kind === "qr") return "qr";
44916
+ return device.coexistence ? "coexistence" : "dedicated";
44917
+ }
44242
44918
  function deviceResource(device) {
44243
44919
  return {
44244
44920
  id: device.id,
44245
44921
  alias: device.alias,
44246
44922
  image: "",
44247
44923
  color: device.kind === "cloud" ? "#25D366" : "#128C7E",
44248
- number: device.number,
44924
+ number: device.number ?? "",
44249
44925
  normalized_number: device.number,
44250
- first_conecction_at: laravelJson(device.createdAt),
44926
+ first_conecction_at: device.firstConnectedAt ? laravelJson(device.firstConnectedAt) : null,
44251
44927
  user_id: ACCOUNT_USER_ID,
44252
44928
  provider: device.provider,
44253
44929
  waba_id: device.wabaId ?? "",
@@ -44257,15 +44933,43 @@ function deviceResource(device) {
44257
44933
  active: 1,
44258
44934
  health: deviceHealth(device),
44259
44935
  number_key: device.number,
44260
- number_key_confirmed: true,
44261
- is_coexistence: false,
44936
+ number_key_confirmed: device.number !== null,
44937
+ is_coexistence: device.coexistence,
44262
44938
  slot_state: "held",
44263
44939
  pool_suspended: false,
44264
44940
  pool_suspension_reason: null,
44265
44941
  pool_keep: false,
44266
44942
  qr_allowed: device.kind === "qr",
44267
44943
  can_change_to_meta: false,
44268
- capabilities: deviceCapabilities(device)
44944
+ capabilities: deviceCapabilities(device),
44945
+ external_reference: device.externalReference,
44946
+ connection: deviceConnection(device)
44947
+ };
44948
+ }
44949
+ function eventDevice(device) {
44950
+ return {
44951
+ id: device.id,
44952
+ name: device.alias,
44953
+ number: device.number,
44954
+ connection: deviceConnection(device),
44955
+ external_reference: device.externalReference
44956
+ };
44957
+ }
44958
+ function connectLinkResource(link) {
44959
+ return {
44960
+ id: link.id,
44961
+ type: link.type,
44962
+ status: link.status,
44963
+ expires_at: iso8601(link.expiresAt),
44964
+ allowed_connection_types: link.allowedConnectionTypes,
44965
+ provision_us_number: link.provisionUsNumber,
44966
+ country: link.country,
44967
+ device_id: link.deviceId,
44968
+ external_reference: link.externalReference,
44969
+ return_url: link.returnUrl,
44970
+ error_code: link.errorCode,
44971
+ created_at: iso8601(link.createdAt),
44972
+ completed_at: link.completedAt ? iso8601(link.completedAt) : null
44269
44973
  };
44270
44974
  }
44271
44975
  function publicMessage(message) {
@@ -44355,6 +45059,354 @@ function deliveryResource(delivery) {
44355
45059
  };
44356
45060
  }
44357
45061
 
45062
+ // src/numbers/numbers.ts
45063
+ var DISCONNECT_REASONS = ["qr_logged_out", "meta_account_not_registered", "shadow_ban_protection", "provider_error", "unknown", "disconnected_by_user"];
45064
+ var HEALTH_STATES = ["available", "limited", "blocked", "unknown"];
45065
+ var LINK_ERROR_CODES = [
45066
+ "link_expired",
45067
+ "already_used",
45068
+ "revoked",
45069
+ "quota_exceeded",
45070
+ "trial_number_blocked",
45071
+ "facebook_auth_failed",
45072
+ "meta_error",
45073
+ "number_not_owned",
45074
+ "qr_timeout",
45075
+ "provider_error"
45076
+ ];
45077
+ var OPEN_LINKS_LIMIT = 20;
45078
+ var poolLimit = (app) => app.store.numbers.trial ? 1 : 3;
45079
+ var placeOf = (device) => device.number ?? `device:${device.id}`;
45080
+ function placesInUse(app) {
45081
+ return new Set([...app.store.devices.values()].map(placeOf)).size;
45082
+ }
45083
+ var isPendingQr = (device) => device.kind === "qr" && !device.connected && device.firstConnectedAt === null;
45084
+ function pendingQrIds(app) {
45085
+ return [...app.store.devices.values()].filter(isPendingQr).map((d) => d.id);
45086
+ }
45087
+ function checkQuota(app) {
45088
+ const { numbers: numbers2 } = app.store;
45089
+ if (!numbers2.included) {
45090
+ throw new ApiError(403, "numbers_not_included", "Tu plan no incluye n\xFAmeros de WhatsApp.");
45091
+ }
45092
+ const used = placesInUse(app);
45093
+ if (used >= numbers2.limit) {
45094
+ throw new ApiError(403, "number_limit_reached", `Alcanzaste el l\xEDmite de ${numbers2.limit} n\xFAmeros de tu plan.`, {
45095
+ limit: numbers2.limit,
45096
+ used,
45097
+ pending_device_ids: pendingQrIds(app)
45098
+ });
45099
+ }
45100
+ }
45101
+ function checkPendingNumber(app) {
45102
+ const [pending] = pendingQrIds(app);
45103
+ if (pending !== void 0) {
45104
+ throw new ApiError(409, "pending_number_exists", "Ya tienes un n\xFAmero por QR sin conectar: con\xE9ctalo o b\xF3rralo antes de crear otro.", { device_id: pending });
45105
+ }
45106
+ }
45107
+ var invalidReferenceDevice = () => new ApiError(422, "invalid_reference_device", "El n\xFAmero indicado no existe en la cuenta.");
45108
+ function checkSameNumber(app, id) {
45109
+ const target = app.store.devices.get(Number(id));
45110
+ if (!target) throw invalidReferenceDevice();
45111
+ if (target.kind === "qr") {
45112
+ throw new ApiError(422, "method_already_present", "Ese n\xFAmero ya tiene QR.", { device_id: target.id });
45113
+ }
45114
+ const sibling = [...app.store.devices.values()].find((d) => d.kind === "qr" && d.number !== null && d.number === target.number);
45115
+ if (sibling) throw new ApiError(422, "method_already_present", "Ese n\xFAmero ya tiene QR.", { device_id: sibling.id });
45116
+ if (!target.coexistence) {
45117
+ throw new ApiError(422, "qr_requires_coexistence", "Solo puedes sumar QR a un n\xFAmero oficial que est\xE9 en coexistencia con la app de WhatsApp Business.");
45118
+ }
45119
+ return target;
45120
+ }
45121
+ function createDevice(app, input) {
45122
+ const id = app.store.ids.device.next();
45123
+ const now = /* @__PURE__ */ new Date();
45124
+ const cloud = input.connection !== "qr";
45125
+ const connected = input.connected ?? false;
45126
+ const device = {
45127
+ id,
45128
+ alias: input.alias ?? `N\xFAmero ${id}`,
45129
+ number: input.number ?? null,
45130
+ provider: cloud ? 2 : 1,
45131
+ kind: cloud ? "cloud" : "qr",
45132
+ connected,
45133
+ coexistence: input.connection === "coexistence",
45134
+ fromPool: input.fromPool ?? false,
45135
+ externalReference: input.externalReference ?? null,
45136
+ health: cloud ? connected ? "available" : "unknown" : null,
45137
+ lastConnectLinkId: null,
45138
+ wabaId: cloud ? `1000000000${String(id).padStart(5, "0")}` : null,
45139
+ phoneNumberId: cloud ? `2000000000${String(id).padStart(5, "0")}` : null,
45140
+ firstConnectedAt: connected ? now : null,
45141
+ createdAt: now
45142
+ };
45143
+ app.store.devices.set(id, device);
45144
+ return device;
45145
+ }
45146
+ function fakeNumber(device) {
45147
+ const tail = String(device.id).padStart(4, "0").slice(-4);
45148
+ return device.fromPool ? `1555010${tail}` : `54911555${tail}`;
45149
+ }
45150
+ var eventData = (device, rest, connectLinkId = device.lastConnectLinkId) => ({
45151
+ device: eventDevice(device),
45152
+ ...rest,
45153
+ connect_link_id: connectLinkId
45154
+ });
45155
+ function connectDevice(app, device, options = {}) {
45156
+ if (device.connected) return null;
45157
+ const at = /* @__PURE__ */ new Date();
45158
+ const connectLinkId = options.connectLinkId ?? null;
45159
+ if (connectLinkId !== null) device.lastConnectLinkId = connectLinkId;
45160
+ if (options.number) device.number = options.number;
45161
+ device.number ??= fakeNumber(device);
45162
+ device.connected = true;
45163
+ device.firstConnectedAt ??= at;
45164
+ if (device.kind === "cloud" && (device.health === null || device.health === "unknown")) device.health = "available";
45165
+ return app.webhooks.emit("device.connected", eventData(device, { connected_at: isoSeconds(at) }, connectLinkId), { occurredAt: at });
45166
+ }
45167
+ function disconnectDevice(app, device, reason) {
45168
+ if (!device.connected) return null;
45169
+ const at = /* @__PURE__ */ new Date();
45170
+ device.connected = false;
45171
+ return app.webhooks.emit("device.disconnected", eventData(device, { disconnected_at: isoSeconds(at), reason }), { occurredAt: at });
45172
+ }
45173
+ function setHealth(app, device, state2) {
45174
+ const previous = device.health ?? "unknown";
45175
+ if (previous === state2) return null;
45176
+ device.health = state2;
45177
+ const at = /* @__PURE__ */ new Date();
45178
+ return app.webhooks.emit("device.health_changed", eventData(device, { previous_state: previous, state: state2, changed_at: isoSeconds(at) }), { occurredAt: at });
45179
+ }
45180
+ function deleteDevice(app, device) {
45181
+ const at = /* @__PURE__ */ new Date();
45182
+ const event = app.webhooks.emit("device.deleted", eventData(device, { deleted_at: isoSeconds(at) }), { occurredAt: at });
45183
+ app.store.devices.delete(device.id);
45184
+ for (const link of app.store.connectLinks.values()) {
45185
+ if (link.reconnect && link.deviceId === device.id && link.status === "pending") {
45186
+ link.status = "revoked";
45187
+ link.errorCode = "revoked";
45188
+ }
45189
+ }
45190
+ return event;
45191
+ }
45192
+ var sha256 = (text2) => createHash("sha256").update(text2).digest("hex");
45193
+ var newToken = () => randomBytes(32).toString("base64url");
45194
+ function refreshLink(link, now = /* @__PURE__ */ new Date()) {
45195
+ if (link.status === "pending" && link.expiresAt.getTime() <= now.getTime()) {
45196
+ link.status = "expired";
45197
+ link.errorCode = "link_expired";
45198
+ }
45199
+ return link;
45200
+ }
45201
+ function findLinkByToken(app, token) {
45202
+ const hash = sha256(token);
45203
+ for (const link of app.store.connectLinks.values()) if (link.tokenHash === hash) return refreshLink(link);
45204
+ return void 0;
45205
+ }
45206
+ function openLinks(app) {
45207
+ let n = 0;
45208
+ for (const link of app.store.connectLinks.values()) if (refreshLink(link).status === "pending") n++;
45209
+ return n;
45210
+ }
45211
+ var META_ATTEMPTS = 5;
45212
+ var RETRYABLE = ["facebook_auth_failed", "meta_error"];
45213
+ function fail(link, code) {
45214
+ link.status = "failed";
45215
+ link.errorCode = code;
45216
+ link.completedAt = /* @__PURE__ */ new Date();
45217
+ return "failed";
45218
+ }
45219
+ function connectionAllowed(link, connection) {
45220
+ return link.type !== "meta" || !connection || (link.allowedConnectionTypes ?? []).includes(connection);
45221
+ }
45222
+ function completeLink(app, link, input) {
45223
+ if (input.outcome === "failed") {
45224
+ const code = input.errorCode ?? (link.type === "qr" ? "qr_timeout" : "meta_error");
45225
+ if (link.type === "meta" && RETRYABLE.includes(code)) {
45226
+ link.failedAttempts++;
45227
+ if (link.failedAttempts < META_ATTEMPTS) {
45228
+ link.errorCode = null;
45229
+ return "retry";
45230
+ }
45231
+ }
45232
+ return fail(link, code);
45233
+ }
45234
+ link.status = "completed";
45235
+ if (link.reconnect) {
45236
+ const device2 = link.deviceId === null ? void 0 : app.store.devices.get(link.deviceId);
45237
+ if (!device2) return fail(link, "provider_error");
45238
+ link.completedAt = /* @__PURE__ */ new Date();
45239
+ connectDevice(app, device2, { number: input.number, connectLinkId: link.id });
45240
+ return "completed";
45241
+ }
45242
+ let connection = "qr";
45243
+ let number = input.number ?? null;
45244
+ let fromPool = false;
45245
+ if (link.sameNumberAs !== null) {
45246
+ const target = app.store.devices.get(link.sameNumberAs);
45247
+ if (!target || !target.coexistence) return fail(link, "provider_error");
45248
+ number = target.number;
45249
+ } else {
45250
+ try {
45251
+ checkQuota(app);
45252
+ } catch {
45253
+ return fail(link, "quota_exceeded");
45254
+ }
45255
+ }
45256
+ if (link.type === "meta") {
45257
+ const allowed = link.allowedConnectionTypes ?? ["dedicated"];
45258
+ connection = input.connection && allowed.includes(input.connection) ? input.connection : allowed[0];
45259
+ if (connection === "dedicated" && link.provisionUsNumber && !number) {
45260
+ const inPool = [...app.store.devices.values()].filter((d) => d.fromPool).length;
45261
+ if (inPool >= poolLimit(app)) return fail(link, app.store.numbers.trial ? "trial_number_blocked" : "quota_exceeded");
45262
+ fromPool = true;
45263
+ }
45264
+ }
45265
+ const device = createDevice(app, { connection, externalReference: link.externalReference, fromPool, number });
45266
+ link.deviceId = device.id;
45267
+ link.completedAt = /* @__PURE__ */ new Date();
45268
+ connectDevice(app, device, { connectLinkId: link.id });
45269
+ return "completed";
45270
+ }
45271
+ function returnUrlFor(link) {
45272
+ if (!link.returnUrl) return null;
45273
+ const url = new URL(link.returnUrl);
45274
+ url.searchParams.set("connect_link_id", String(link.id));
45275
+ url.searchParams.set("status", link.status === "completed" ? "completed" : "failed");
45276
+ if (link.deviceId !== null && link.status === "completed") url.searchParams.set("device_id", String(link.deviceId));
45277
+ if (link.errorCode && link.status !== "completed") url.searchParams.set("error_code", link.errorCode);
45278
+ return url.toString();
45279
+ }
45280
+
45281
+ // src/handlers/connect-links.ts
45282
+ var DEFAULT_EXPIRES_IN_MINUTES = 1440;
45283
+ function linkOf(app, id) {
45284
+ const link = app.store.connectLinks.get(Number(id));
45285
+ if (!link) throw notFound("No encontramos ese link de alta en la cuenta.");
45286
+ return refreshLink(link);
45287
+ }
45288
+ var present = (value) => value !== void 0 && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0);
45289
+ function checkLinkBody(b) {
45290
+ const errors = {};
45291
+ const meta = b.type === "meta";
45292
+ if (meta && !present(b.allowed_connection_types)) {
45293
+ errors.allowed_connection_types = ["El campo allowed_connection_types es obligatorio cuando type es meta."];
45294
+ }
45295
+ if (!meta && present(b.allowed_connection_types)) {
45296
+ errors.allowed_connection_types = ["El campo allowed_connection_types est\xE1 prohibido cuando type es qr."];
45297
+ }
45298
+ if (!meta && present(b.provision_us_number)) errors.provision_us_number = ["El campo provision_us_number est\xE1 prohibido cuando type es qr."];
45299
+ else if (meta && b.provision_us_number === true) {
45300
+ const allowed = Array.isArray(b.allowed_connection_types) ? b.allowed_connection_types : [];
45301
+ if (present(b.device_id)) errors.provision_us_number = ["El campo provision_us_number no se acepta junto con device_id."];
45302
+ else if (present(b.allowed_connection_types) && !allowed.includes("dedicated")) {
45303
+ errors.provision_us_number = ["El campo provision_us_number exige dedicated en allowed_connection_types."];
45304
+ }
45305
+ }
45306
+ if (meta && present(b.country)) errors.country = ["El campo country est\xE1 prohibido cuando type es meta."];
45307
+ else if (present(b.country) && present(b.device_id)) errors.country = ["El campo country proh\xEDbe que device_id est\xE9 presente."];
45308
+ if (meta && present(b.same_number_as)) errors.same_number_as = ["El campo same_number_as est\xE1 prohibido cuando type es meta."];
45309
+ else if (present(b.same_number_as) && present(b.device_id)) errors.same_number_as = ["El campo same_number_as proh\xEDbe que device_id est\xE9 presente."];
45310
+ if (present(b.return_url)) {
45311
+ let url = null;
45312
+ try {
45313
+ url = new URL(String(b.return_url));
45314
+ } catch {
45315
+ url = null;
45316
+ }
45317
+ if (!url || url.protocol !== "https:") errors.return_url = ["El campo return_url debe ser una URL https."];
45318
+ }
45319
+ if (Object.keys(errors).length) throw invalidRequest(errors);
45320
+ }
45321
+ function throttle(app) {
45322
+ const now = Date.now();
45323
+ const { store } = app;
45324
+ store.linkAttempts = store.linkAttempts.filter((at) => now - at < 6e4);
45325
+ if (store.linkAttempts.length >= store.numbers.linksPerMinute) {
45326
+ const retryAfter = Math.max(1, Math.ceil((6e4 - (now - store.linkAttempts[0])) / 1e3));
45327
+ throw new RawApiError(429, { message: "Too Many Attempts." }, { "Retry-After": String(retryAfter) });
45328
+ }
45329
+ store.linkAttempts.push(now);
45330
+ }
45331
+ var connectLinkHandlers = {
45332
+ "POST /api/v1/connect-links": (ctx) => {
45333
+ throttle(ctx.app);
45334
+ return idempotent(ctx, "connect-links", () => {
45335
+ const { app } = ctx;
45336
+ const b = body(ctx);
45337
+ checkLinkBody(b);
45338
+ if (b.type === "meta" && !app.store.numbers.metaConnect) {
45339
+ throw new ApiError(503, "meta_connect_disabled", "El alta de n\xFAmeros oficiales por link no est\xE1 disponible todav\xEDa.");
45340
+ }
45341
+ const reconnect = present(b.device_id);
45342
+ if (reconnect) {
45343
+ const device = app.store.devices.get(Number(b.device_id));
45344
+ if (!device) throw invalidReferenceDevice();
45345
+ if (device.kind === "qr" !== (b.type === "qr")) {
45346
+ throw new ApiError(422, "connection_type_mismatch", "El type del link tiene que coincidir con el m\xE9todo del n\xFAmero a reconectar.");
45347
+ }
45348
+ }
45349
+ const sameNumberAs = present(b.same_number_as) ? checkSameNumber(app, b.same_number_as).id : null;
45350
+ if (!reconnect && sameNumberAs === null) checkQuota(app);
45351
+ if (openLinks(app) >= OPEN_LINKS_LIMIT) {
45352
+ throw new ApiError(429, "too_many_open_links", `Tienes ${OPEN_LINKS_LIMIT} links de alta abiertos. Revoca alguno o espera a que venzan.`, { limit: OPEN_LINKS_LIMIT });
45353
+ }
45354
+ const now = /* @__PURE__ */ new Date();
45355
+ const deviceId = reconnect ? Number(b.device_id) : null;
45356
+ if (reconnect) {
45357
+ for (const other of app.store.connectLinks.values()) {
45358
+ if (other.reconnect && other.deviceId === deviceId && refreshLink(other).status === "pending") {
45359
+ other.status = "revoked";
45360
+ other.errorCode = "revoked";
45361
+ }
45362
+ }
45363
+ }
45364
+ const token = newToken();
45365
+ const minutes = present(b.expires_in_minutes) ? Number(b.expires_in_minutes) : DEFAULT_EXPIRES_IN_MINUTES;
45366
+ const link = {
45367
+ id: app.store.ids.connectLink.next(),
45368
+ type: b.type === "meta" ? "meta" : "qr",
45369
+ status: "pending",
45370
+ tokenHash: sha256(token),
45371
+ allowedConnectionTypes: b.type === "meta" ? [...new Set(b.allowed_connection_types)] : null,
45372
+ provisionUsNumber: b.type === "meta" && b.provision_us_number === true,
45373
+ // accounts keeps it in lower case, and only on a QR link that creates a number.
45374
+ country: b.type === "qr" && !reconnect && present(b.country) ? String(b.country).toLowerCase() : null,
45375
+ deviceId,
45376
+ reconnect,
45377
+ sameNumberAs,
45378
+ externalReference: present(b.external_reference) ? String(b.external_reference) : null,
45379
+ returnUrl: present(b.return_url) ? String(b.return_url) : null,
45380
+ errorCode: null,
45381
+ failedAttempts: 0,
45382
+ expiresAt: new Date(now.getTime() + minutes * 6e4),
45383
+ createdAt: now,
45384
+ completedAt: null
45385
+ };
45386
+ app.store.connectLinks.set(link.id, link);
45387
+ app.logger.info(`sign-up link ${link.id} (${link.type}) \u2192 ${app.publicUrl}/connect/\u2026`);
45388
+ const { id, type, status, ...rest } = connectLinkResource(link);
45389
+ return { status: 201, body: { data: { id, type, status, url: `${app.publicUrl}/connect/${token}`, ...rest } }, headers: { "Cache-Control": "no-store" } };
45390
+ });
45391
+ },
45392
+ // Paginated like the other lists, newest first (by id); filters by `status` and `external_reference`.
45393
+ "GET /api/v1/connect-links": (ctx) => {
45394
+ const { status, external_reference: reference } = ctx.query;
45395
+ const list = [...ctx.app.store.connectLinks.values()].map((link) => refreshLink(link)).filter((link) => !status || link.status === status).filter((link) => reference === void 0 || reference === "" || link.externalReference === String(reference)).sort((a, b) => b.id - a.id);
45396
+ return ok(paginate(list, listContext(ctx), connectLinkResource));
45397
+ },
45398
+ "GET /api/v1/connect-links/{link}": (ctx) => ok({ data: connectLinkResource(linkOf(ctx.app, ctx.path.link)) }),
45399
+ "DELETE /api/v1/connect-links/{link}": (ctx) => {
45400
+ const link = linkOf(ctx.app, ctx.path.link);
45401
+ if (link.status !== "pending") {
45402
+ throw new ApiError(409, "link_not_pending", `El link ya no est\xE1 pendiente: est\xE1 ${link.status}.`, { status: link.status });
45403
+ }
45404
+ link.status = "revoked";
45405
+ link.errorCode = "revoked";
45406
+ return noContent();
45407
+ }
45408
+ };
45409
+
44358
45410
  // src/handlers/contacts.ts
44359
45411
  function getContact(app, id) {
44360
45412
  const contact = app.store.contacts.get(Number(id));
@@ -44711,6 +45763,149 @@ function getCustomField(ctx) {
44711
45763
  if (!field) throw objectNotFound();
44712
45764
  return field;
44713
45765
  }
45766
+ var MODULES = 25;
45767
+ var SCALE = 6;
45768
+ var QUIET = 4;
45769
+ var crcTable2 = null;
45770
+ function crc32(bytes) {
45771
+ if (!crcTable2) {
45772
+ crcTable2 = new Int32Array(256);
45773
+ for (let n = 0; n < 256; n++) {
45774
+ let c = n;
45775
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
45776
+ crcTable2[n] = c;
45777
+ }
45778
+ }
45779
+ let crc = -1;
45780
+ for (const byte of bytes) crc = crcTable2[(crc ^ byte) & 255] ^ crc >>> 8;
45781
+ return (crc ^ -1) >>> 0;
45782
+ }
45783
+ function chunk(type, data) {
45784
+ const length = Buffer.alloc(4);
45785
+ length.writeUInt32BE(data.length);
45786
+ const body2 = Buffer.concat([Buffer.from(type, "ascii"), data]);
45787
+ const crc = Buffer.alloc(4);
45788
+ crc.writeUInt32BE(crc32(body2));
45789
+ return Buffer.concat([length, body2, crc]);
45790
+ }
45791
+ function modules(seed2) {
45792
+ const bits = createHash("sha512").update(seed2).digest();
45793
+ const grid = Array.from(
45794
+ { length: MODULES },
45795
+ (_, y) => Array.from({ length: MODULES }, (_2, x) => {
45796
+ const i = y * MODULES + x;
45797
+ return (bits[i % bits.length] >> i % 8 & 1) === 1;
45798
+ })
45799
+ );
45800
+ const finder = (ox, oy) => {
45801
+ for (let y = -1; y <= 7; y++) {
45802
+ for (let x = -1; x <= 7; x++) {
45803
+ const gx = ox + x;
45804
+ const gy = oy + y;
45805
+ if (gx < 0 || gy < 0 || gx >= MODULES || gy >= MODULES) continue;
45806
+ const ring = Math.max(Math.abs(x - 3), Math.abs(y - 3));
45807
+ grid[gy][gx] = ring !== 2 && ring <= 3;
45808
+ }
45809
+ }
45810
+ };
45811
+ finder(0, 0);
45812
+ finder(MODULES - 7, 0);
45813
+ finder(0, MODULES - 7);
45814
+ for (let i = 8; i < MODULES - 8; i++) {
45815
+ grid[6][i] = i % 2 === 0;
45816
+ grid[i][6] = i % 2 === 0;
45817
+ }
45818
+ return grid;
45819
+ }
45820
+ function fakeQrDataUri(seed2) {
45821
+ const grid = modules(seed2);
45822
+ const size = (MODULES + QUIET * 2) * SCALE;
45823
+ const raw = Buffer.alloc((size + 1) * size, 255);
45824
+ for (let y = 0; y < size; y++) {
45825
+ raw[y * (size + 1)] = 0;
45826
+ const my = Math.floor(y / SCALE) - QUIET;
45827
+ for (let x = 0; x < size; x++) {
45828
+ const mx = Math.floor(x / SCALE) - QUIET;
45829
+ const dark = my >= 0 && mx >= 0 && my < MODULES && mx < MODULES && grid[my][mx];
45830
+ if (dark) raw[y * (size + 1) + 1 + x] = 0;
45831
+ }
45832
+ }
45833
+ const header = Buffer.alloc(13);
45834
+ header.writeUInt32BE(size, 0);
45835
+ header.writeUInt32BE(size, 4);
45836
+ header[8] = 8;
45837
+ header[9] = 0;
45838
+ const png = Buffer.concat([
45839
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
45840
+ chunk("IHDR", header),
45841
+ chunk("IDAT", deflateSync(raw)),
45842
+ chunk("IEND", Buffer.alloc(0))
45843
+ ]);
45844
+ return `data:image/png;base64,${png.toString("base64")}`;
45845
+ }
45846
+
45847
+ // src/handlers/devices.ts
45848
+ var QR_EXPIRES_IN = 5;
45849
+ function deviceOf(app, id) {
45850
+ const device = app.store.devices.get(Number(id));
45851
+ if (!device) throw new RawApiError(404, { message: "Object not found" });
45852
+ return device;
45853
+ }
45854
+ var optionalText = (value) => value === void 0 || value === null ? null : String(value);
45855
+ var deviceHandlers = {
45856
+ // With an API key the list always paginates (accounts `PublicApiListings`: `perPage`, 25 by default, up to 100, by
45857
+ // id). `external_reference` filters by exact equality.
45858
+ "GET /api/v1/devices": (ctx) => {
45859
+ const reference = ctx.query.external_reference;
45860
+ const list = [...ctx.app.store.devices.values()].filter((d) => reference === void 0 || reference === "" || d.externalReference === String(reference)).sort((a, b) => a.id - b.id);
45861
+ return ok(paginate(list, listContext(ctx), deviceResource));
45862
+ },
45863
+ "GET /api/v1/devices/{device}": (ctx) => ok(deviceResource(deviceOf(ctx.app, ctx.path.device))),
45864
+ // What the sandbox keeps: the alias and the external reference. The rest of the body is accepted and ignored.
45865
+ "PUT /api/v1/devices/{device}": (ctx) => {
45866
+ const device = deviceOf(ctx.app, ctx.path.device);
45867
+ const b = body(ctx);
45868
+ if (typeof b.alias === "string" && b.alias.trim() !== "") device.alias = b.alias;
45869
+ if ("external_reference" in b) device.externalReference = optionalText(b.external_reference);
45870
+ return ok(deviceResource(device));
45871
+ },
45872
+ // `200` with an empty body, as today.
45873
+ "DELETE /api/v1/devices/{device}": (ctx) => {
45874
+ deleteDevice(ctx.app, deviceOf(ctx.app, ctx.path.device));
45875
+ return { status: 200 };
45876
+ },
45877
+ "POST /api/v1/devices": (ctx) => idempotent(ctx, "devices", () => {
45878
+ const b = body(ctx);
45879
+ const target = b.same_number_as === void 0 || b.same_number_as === null ? null : checkSameNumber(ctx.app, b.same_number_as);
45880
+ if (!target) {
45881
+ checkQuota(ctx.app);
45882
+ checkPendingNumber(ctx.app);
45883
+ }
45884
+ const device = createDevice(ctx.app, {
45885
+ connection: "qr",
45886
+ alias: optionalText(b.alias) ?? target?.alias ?? null,
45887
+ number: target?.number ?? null,
45888
+ externalReference: optionalText(b.external_reference) ?? target?.externalReference ?? null
45889
+ });
45890
+ return ok({ data: deviceResource(device) }, 201);
45891
+ }),
45892
+ // `Cache-Control: no-store`, as accounts: a QR read a few seconds ago may no longer link, so nothing may keep it.
45893
+ "GET /api/v1/devices/{device}/qr": (ctx) => {
45894
+ const device = deviceOf(ctx.app, ctx.path.device);
45895
+ if (device.kind !== "qr") throw new ApiError(422, "provider_unsupported", "El c\xF3digo QR solo existe para n\xFAmeros por QR.");
45896
+ return {
45897
+ status: 200,
45898
+ body: {
45899
+ data: {
45900
+ connected: device.connected,
45901
+ qr_code: device.connected ? null : fakeQrDataUri(`${device.id}:${Math.floor(Date.now() / (QR_EXPIRES_IN * 1e3))}`),
45902
+ expires_in: device.connected ? 0 : QR_EXPIRES_IN
45903
+ }
45904
+ },
45905
+ headers: { "Cache-Control": "no-store" }
45906
+ };
45907
+ }
45908
+ };
44714
45909
 
44715
45910
  // src/messaging/lifecycle.ts
44716
45911
  var ORDER = { sent: 1, delivered: 2, read: 3 };
@@ -45090,14 +46285,6 @@ var messageHandlers = {
45090
46285
  if (template.deviceId !== Number(ctx.path.device)) throw notFound("La plantilla no es de este n\xFAmero.");
45091
46286
  ctx.app.store.templates.delete(template.id);
45092
46287
  return noContent();
45093
- },
45094
- // --- numbers (accounts) -----------------------------------------------------------------------------------
45095
- // With an API key the list always paginates (accounts `PublicApiListings`: `perPage`, 25 by default, up to 100, by id).
45096
- "GET /api/v1/devices": (ctx) => ok(paginate([...ctx.app.store.devices.values()].sort((a, b) => a.id - b.id), listContext(ctx), deviceResource)),
45097
- "GET /api/v1/devices/{device}": (ctx) => {
45098
- const device = ctx.app.store.devices.get(Number(ctx.path.device));
45099
- if (!device) throw new RawApiError(404, { message: "Object not found" });
45100
- return ok(deviceResource(device));
45101
46288
  }
45102
46289
  };
45103
46290
  function setTemplateStatus(app, template, status, reason) {
@@ -45620,27 +46807,9 @@ var formHandlers = {
45620
46807
  "POST /api/v1/contacts/{contact}/message/form": sendForm
45621
46808
  };
45622
46809
 
45623
- // src/handlers/groups.ts
45624
- var GROUP_ACTION_ROUTES = [
45625
- "GET /api/v1/groups/actions/{action}",
45626
- "POST /api/v1/groups",
45627
- "POST /api/v1/groups/{group}/participants",
45628
- "POST /api/v1/groups/{group}/participants/remove",
45629
- "POST /api/v1/groups/{group}/admins",
45630
- "POST /api/v1/groups/{group}/admins/remove",
45631
- "PUT /api/v1/groups/{group}/name",
45632
- "PUT /api/v1/groups/{group}/description",
45633
- "PUT /api/v1/groups/{group}/photo",
45634
- "PUT /api/v1/groups/{group}/settings",
45635
- "POST /api/v1/groups/{group}/invite-link/renew",
45636
- "POST /api/v1/groups/{group}/messages",
45637
- "GET /api/v1/groups/{group}/invite-link/live",
45638
- "GET /api/v1/groups/{group}/join-requests",
45639
- "POST /api/v1/groups/{group}/join-requests/approve",
45640
- "POST /api/v1/groups/{group}/join-requests/reject"
45641
- ];
45642
- var GATE_CLOSED = /* @__PURE__ */ new Set([...GROUP_ACTION_ROUTES, "POST /api/v1/contacts/{contact}/message/poll", "POST /api/v1/contacts/{contact}/message/event"]);
45643
- var groupHandlers = Object.fromEntries(
46810
+ // src/handlers/gate.ts
46811
+ var GATE_CLOSED = /* @__PURE__ */ new Set(["POST /api/v1/contacts/{contact}/message/poll", "POST /api/v1/contacts/{contact}/message/event"]);
46812
+ var gateHandlers = Object.fromEntries(
45644
46813
  [...GATE_CLOSED].map((route) => [
45645
46814
  route,
45646
46815
  () => {
@@ -45816,6 +46985,132 @@ function parseQuery(search) {
45816
46985
  }
45817
46986
  return out;
45818
46987
  }
46988
+ function securityHeaders(nonce) {
46989
+ return {
46990
+ "Cache-Control": "no-store, private",
46991
+ "Referrer-Policy": "no-referrer",
46992
+ "X-Frame-Options": "DENY",
46993
+ "X-Robots-Tag": "noindex",
46994
+ "Strict-Transport-Security": "max-age=31536000",
46995
+ "Cross-Origin-Opener-Policy": "same-origin-allow-popups",
46996
+ "X-Content-Type-Options": "nosniff",
46997
+ "Content-Security-Policy": `default-src 'none'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}'; form-action 'self' https:; frame-ancestors 'none'; base-uri 'none'`
46998
+ };
46999
+ }
47000
+ var PATH = /^\/connect\/([A-Za-z0-9_-]{1,200})(?:\/(start|complete|qr))?\/?$/;
47001
+ var isConnectPath = (pathname) => pathname === "/connect" || pathname.startsWith("/connect/");
47002
+ var escape = (text2) => text2.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
47003
+ function page(status, title, text2, extra = "") {
47004
+ const nonce = randomBytes(16).toString("base64");
47005
+ const html = `<!doctype html>
47006
+ <html lang="en">
47007
+ <head>
47008
+ <meta charset="utf-8">
47009
+ <meta name="viewport" content="width=device-width, initial-scale=1">
47010
+ <meta name="robots" content="noindex">
47011
+ <title>${escape(title)} \xB7 Funnelchat</title>
47012
+ <style nonce="${nonce}">
47013
+ :root { --bg: #f6f7f9; --card: #fff; --text: #1d2433; --muted: #5b6475; --line: #dfe3ea; --accent: #0b6e4f; }
47014
+ @media (prefers-color-scheme: dark) { :root { --bg: #11151c; --card: #1a202a; --text: #e6e9ef; --muted: #9aa4b5; --line: #2b3340; --accent: #3fb68b; } }
47015
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 16px; box-sizing: border-box; font: 15px/1.5 system-ui, -apple-system, Segoe UI, sans-serif; background: var(--bg); color: var(--text); }
47016
+ main { max-width: 420px; background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 24px; }
47017
+ h1 { font-size: 20px; margin: 0 0 8px; }
47018
+ p { margin: 0 0 8px; }
47019
+ label { display: block; margin: 6px 0; }
47020
+ button { margin: 8px 0; padding: 10px 16px; border: 0; border-radius: 6px; background: var(--accent); color: #fff; font: inherit; cursor: pointer; }
47021
+ small { color: var(--muted); }
47022
+ </style>
47023
+ </head>
47024
+ <body>
47025
+ <main>
47026
+ <h1>${escape(title)}</h1>
47027
+ <p>${escape(text2)}</p>
47028
+ ${extra}<small>Funnelchat sandbox: a test page. Nothing is connected to WhatsApp.</small>
47029
+ </main>
47030
+ </body>
47031
+ </html>
47032
+ `;
47033
+ return { status, html, headers: securityHeaders(nonce) };
47034
+ }
47035
+ function json(status, body2) {
47036
+ return { status, body: body2, headers: securityHeaders(randomBytes(16).toString("base64")) };
47037
+ }
47038
+ function linkPage(token, link, again) {
47039
+ const choice = link.type === "meta" && (link.allowedConnectionTypes?.length ?? 0) > 1 ? (link.allowedConnectionTypes ?? []).map(
47040
+ (c, i) => `<label><input type="radio" name="connection" value="${c}"${i === 0 ? " checked" : ""}> ${c === "coexistence" ? "Keep using the WhatsApp Business app (coexistence)" : "A number only for the API"}</label>`
47041
+ ).join("\n") : "";
47042
+ const answer = page(
47043
+ 200,
47044
+ again?.title ?? (link.type === "qr" ? "Connect your WhatsApp by QR" : "Connect your WhatsApp Business number"),
47045
+ again?.text ?? "In the sandbox the button connects at once: there is no QR to scan and no Meta sign-up.",
47046
+ `<form method="post" action="/connect/${token}/start">
47047
+ ${choice}
47048
+ <button type="submit">Connect</button>
47049
+ </form>
47050
+ `
47051
+ );
47052
+ const nonce = /'nonce-([^']+)'/.exec(answer.headers["Content-Security-Policy"])?.[1] ?? "";
47053
+ answer.html = answer.html.replace("</body>", `<script nonce="${nonce}">history.replaceState(null, '', '/connect/reopen');</script>
47054
+ </body>`);
47055
+ return answer;
47056
+ }
47057
+ function outcomeOf(input) {
47058
+ const code = typeof input.error_code === "string" && LINK_ERROR_CODES.includes(input.error_code) ? input.error_code : null;
47059
+ const connection = input.connection === "coexistence" || input.connection === "dedicated" ? input.connection : null;
47060
+ const number = input.number === void 0 ? null : digits(input.number) || null;
47061
+ return { outcome: input.outcome === "failed" ? "failed" : "completed", errorCode: code, connection, number };
47062
+ }
47063
+ function closed(link) {
47064
+ if (link.status === "expired") return page(410, "This link expired", "Ask whoever sent it for a new one.");
47065
+ if (link.status === "revoked") return page(410, "This link is no longer valid", "Ask whoever sent it for a new one.");
47066
+ return page(410, "This link was already used", "A sign-up link works only once. Ask whoever sent it for a new one.");
47067
+ }
47068
+ function handleConnectPage(app, method, pathname, headers, search, body2) {
47069
+ if (headers.authorization !== void 0 || headers["x-api-key"] !== void 0) {
47070
+ return json(403, { error: "credentials_not_allowed", message: "Esta p\xE1gina no acepta credenciales. Abre el link en un navegador, sin API key." });
47071
+ }
47072
+ if (pathname.replace(/\/+$/, "") === "/connect/reopen" && method === "GET") {
47073
+ return page(200, "Open the link again", "For your security the link left the address bar. Open it again from the message you received.");
47074
+ }
47075
+ const match = PATH.exec(pathname);
47076
+ if (!match || match[2] === "qr") return page(404, "This link does not exist", "Check that you copied the whole link.");
47077
+ const token = match[1];
47078
+ const action = match[2];
47079
+ const expected = action ? "POST" : "GET";
47080
+ if (method !== expected) {
47081
+ const answer = page(405, "Not allowed", "Open the link in a browser.");
47082
+ return { ...answer, headers: { ...answer.headers, Allow: expected } };
47083
+ }
47084
+ const link = findLinkByToken(app, token);
47085
+ if (!link) return page(404, "This link does not exist", "Check that you copied the whole link.");
47086
+ if (link.status !== "pending") return closed(link);
47087
+ if (link.type === "meta" && !app.store.numbers.metaConnect) {
47088
+ return page(503, "Not available yet", "Connecting official numbers by link is not available yet.");
47089
+ }
47090
+ if (!action) return linkPage(token, link);
47091
+ const input = { ...parseQuery(search), ...isRecord(body2) ? body2 : {} };
47092
+ const outcome = outcomeOf(input);
47093
+ if (!connectionAllowed(link, outcome.connection)) {
47094
+ return json(422, { error: "connection_type_not_allowed", message: "Este link no permite ese tipo de conexi\xF3n.", allowed_connection_types: link.allowedConnectionTypes });
47095
+ }
47096
+ if (completeLink(app, link, outcome) === "retry") {
47097
+ const left = META_ATTEMPTS - link.failedAttempts;
47098
+ const code = outcome.errorCode ?? "meta_error";
47099
+ app.logger.info(`sign-up link ${link.id}: ${code}, still pending (${left} attempts left)`);
47100
+ if ((headers["content-type"] ?? "").includes("json") || (headers.accept ?? "").includes("application/json")) {
47101
+ return json(200, { state: "retry", error_code: code, attempts_left: left });
47102
+ }
47103
+ return linkPage(token, link, { title: "That did not work", text: `Facebook or Meta answered ${code}. Try again: ${left} attempts left.` });
47104
+ }
47105
+ app.logger.info(`sign-up link ${link.id}: ${link.status}${link.errorCode ? ` (${link.errorCode})` : ""}${link.deviceId ? ` \u2192 number ${link.deviceId}` : ""}`);
47106
+ const back = returnUrlFor(link);
47107
+ if (back) {
47108
+ const { headers: pageHeaders } = page(303, "Done", "Going back.");
47109
+ return { status: 303, headers: { ...pageHeaders, Location: back } };
47110
+ }
47111
+ const status = link.status;
47112
+ return status === "completed" ? page(200, "Done: your number is connected", "You can close this page.") : page(200, "The number could not be connected", `Reason: ${link.errorCode ?? "unknown"}. Ask whoever sent the link for a new one.`);
47113
+ }
45819
47114
 
45820
47115
  // src/sandbox/panel.ts
45821
47116
  function panelHtml() {
@@ -45921,18 +47216,34 @@ function contactOf(app, id) {
45921
47216
  if (!contact) throw notFound("The contact does not exist in the sandbox.");
45922
47217
  return contact;
45923
47218
  }
45924
- function deviceOf(app, id) {
47219
+ function deviceOf2(app, id) {
45925
47220
  const device = app.store.devices.get(Number(id ?? 3));
45926
47221
  if (!device) throw fieldError("device_id", `There is no number ${id} in the sandbox: the example ones are 3 (official) and 4 (QR).`);
45927
47222
  return device;
45928
47223
  }
45929
47224
  function emitted(result) {
45930
- return { id: result.envelope.id, type: result.envelope.type, subscriptions: result.subscriptions };
47225
+ return result ? { id: result.envelope.id, type: result.envelope.type, subscriptions: result.subscriptions } : null;
47226
+ }
47227
+ function numbersState(app) {
47228
+ const { numbers: numbers2 } = app.store;
47229
+ return {
47230
+ limit: numbers2.limit,
47231
+ in_use: placesInUse(app),
47232
+ pending_device_ids: pendingQrIds(app),
47233
+ included: numbers2.included,
47234
+ trial: numbers2.trial,
47235
+ pool_limit: poolLimit(app),
47236
+ meta_connect: numbers2.metaConnect,
47237
+ links_per_minute: numbers2.linksPerMinute
47238
+ };
45931
47239
  }
45932
47240
  function state(app) {
45933
47241
  const { store, config } = app;
45934
47242
  return {
45935
47243
  devices: [...store.devices.values()].map(deviceResource),
47244
+ numbers: numbersState(app),
47245
+ // Never the tokens: they are only in the `url` of the create answer.
47246
+ connect_links: [...store.connectLinks.values()].map((link) => connectLinkResource(refreshLink(link))),
45936
47247
  counts: {
45937
47248
  contacts: store.contacts.size,
45938
47249
  messages: store.messages.size,
@@ -45980,7 +47291,7 @@ function limitOf2(query) {
45980
47291
  }
45981
47292
  function inboundMessage(app, req) {
45982
47293
  const b = obj(req.body);
45983
- const device = deviceOf(app, b.device_id);
47294
+ const device = deviceOf2(app, b.device_id);
45984
47295
  let contact;
45985
47296
  if (b.contact_id !== void 0) contact = contactOf(app, b.contact_id);
45986
47297
  else {
@@ -46198,7 +47509,7 @@ var ROUTES = [
46198
47509
  handle: (app, req) => {
46199
47510
  const b = obj(req.body);
46200
47511
  const contact = contactOf(app, b.contact_id);
46201
- const device = deviceOf(app, b.device_id);
47512
+ const device = deviceOf2(app, b.device_id);
46202
47513
  if (typeof b.open !== "boolean") throw fieldError("open", "open must be true or false.");
46203
47514
  const key = `${contact.id}:${device.id}`;
46204
47515
  if (b.open) app.store.lastInbound.set(key, /* @__PURE__ */ new Date());
@@ -46207,17 +47518,52 @@ var ROUTES = [
46207
47518
  }
46208
47519
  },
46209
47520
  {
47521
+ // `connect` is the scan of the QR (or the end of the Meta sign-up): it takes `number`, a fake one otherwise.
47522
+ // `disconnect` takes `reason` (qr_logged_out for QR and unknown for official numbers by default).
46210
47523
  method: "POST",
46211
47524
  pattern: /^\/_sandbox\/devices\/(\d+)\/(connect|disconnect)$/,
46212
- handle: (app, _req, [id, action]) => {
46213
- const device = deviceOf(app, id);
46214
- const connected = action === "connect";
46215
- if (device.connected === connected) return { status: 200, body: { device: deviceResource(device), event: null } };
46216
- device.connected = connected;
46217
- const at = /* @__PURE__ */ new Date();
46218
- const data = connected ? { device: { id: device.id, name: device.alias, number: device.number }, connected_at: isoSeconds(at) } : { device: { id: device.id, name: device.alias, number: device.number }, disconnected_at: isoSeconds(at) };
46219
- const event = app.webhooks.emit(connected ? "device.connected" : "device.disconnected", data, { occurredAt: at });
46220
- return { status: 200, body: { device: deviceResource(device), event: emitted(event) } };
47525
+ handle: (app, req, [id, action]) => {
47526
+ const device = deviceOf2(app, id);
47527
+ const b = obj(req.body);
47528
+ if (action === "connect") {
47529
+ const number = b.number === void 0 || b.number === null ? null : digits(b.number);
47530
+ if (number !== null && number.length < 8) throw fieldError("number", "number must be a phone, digits only.");
47531
+ return { status: 200, body: { device: deviceResource(device), event: emitted(connectDevice(app, device, { number })) } };
47532
+ }
47533
+ const reason = b.reason ?? (device.kind === "qr" ? "qr_logged_out" : "unknown");
47534
+ if (!DISCONNECT_REASONS.includes(reason)) throw fieldError("reason", `reason must be one of: ${DISCONNECT_REASONS.join(", ")}.`);
47535
+ return { status: 200, body: { device: deviceResource(device), event: emitted(disconnectDevice(app, device, reason)) } };
47536
+ }
47537
+ },
47538
+ {
47539
+ method: "POST",
47540
+ pattern: /^\/_sandbox\/devices\/(\d+)\/health$/,
47541
+ handle: (app, req, [id]) => {
47542
+ const device = deviceOf2(app, id);
47543
+ if (device.kind !== "cloud") throw fieldError("device_id", "Only official numbers have a health state.");
47544
+ const state2 = obj(req.body).state;
47545
+ if (!HEALTH_STATES.includes(state2)) throw fieldError("state", `state must be one of: ${HEALTH_STATES.join(", ")}.`);
47546
+ return { status: 200, body: { device: deviceResource(device), event: emitted(setHealth(app, device, state2)) } };
47547
+ }
47548
+ },
47549
+ { method: "GET", pattern: /^\/_sandbox\/numbers$/, handle: (app) => ({ status: 200, body: numbersState(app) }) },
47550
+ {
47551
+ method: "POST",
47552
+ pattern: /^\/_sandbox\/numbers$/,
47553
+ handle: (app, req) => {
47554
+ const b = obj(req.body);
47555
+ const { numbers: numbers2 } = app.store;
47556
+ for (const [field, key] of [["limit", "limit"], ["links_per_minute", "linksPerMinute"]]) {
47557
+ if (b[field] === void 0) continue;
47558
+ if (!Number.isInteger(b[field]) || b[field] < 0) throw fieldError(field, `${field} must be an integer, 0 or more.`);
47559
+ numbers2[key] = b[field];
47560
+ }
47561
+ for (const [field, key] of [["included", "included"], ["trial", "trial"], ["meta_connect", "metaConnect"]]) {
47562
+ if (b[field] === void 0) continue;
47563
+ if (typeof b[field] !== "boolean") throw fieldError(field, `${field} must be true or false.`);
47564
+ numbers2[key] = b[field];
47565
+ }
47566
+ return { status: 200, body: numbersState(app) };
46221
47567
  }
46222
47568
  },
46223
47569
  {
@@ -46248,16 +47594,24 @@ async function handleSandbox(app, req) {
46248
47594
  // src/server.ts
46249
47595
  var MAX_BODY_BYTES = 1024 * 1024;
46250
47596
  var API_KEY = /^Bearer\s+(\d+)\|fc_live_[A-Za-z0-9]+$/;
46251
- var handlers = { ...contactHandlers, ...messageHandlers, ...formHandlers, ...groupHandlers, ...webhookHandlers };
47597
+ var handlers = {
47598
+ ...contactHandlers,
47599
+ ...messageHandlers,
47600
+ ...deviceHandlers,
47601
+ ...connectLinkHandlers,
47602
+ ...formHandlers,
47603
+ ...gateHandlers,
47604
+ ...webhookHandlers
47605
+ };
46252
47606
  var BodyError = class extends ApiError {
46253
47607
  };
46254
47608
  async function readBody(req) {
46255
47609
  const chunks = [];
46256
47610
  let size = 0;
46257
- for await (const chunk of req) {
46258
- size += chunk.length;
47611
+ for await (const chunk2 of req) {
47612
+ size += chunk2.length;
46259
47613
  if (size > MAX_BODY_BYTES) throw new BodyError(413, "payload_too_large", "El cuerpo supera 1 MB.");
46260
- chunks.push(chunk);
47614
+ chunks.push(chunk2);
46261
47615
  }
46262
47616
  return Buffer.concat(chunks);
46263
47617
  }
@@ -46335,7 +47689,7 @@ function createSandbox(options = {}) {
46335
47689
  const validation = validateRequest(op.operation, match.params, parseQuery(url.searchParams), requestBody, requestBodySchema(op.operation));
46336
47690
  if (!validation.ok) {
46337
47691
  if (validation.kind === "not_found") throw notFound();
46338
- throw validationFailed(validation.errors);
47692
+ throw op.operation["x-fc-validation-error"] === "invalid_request" ? invalidRequest(validation.errors) : validationFailed(validation.errors);
46339
47693
  }
46340
47694
  const handler = handlers[`${op.method.toUpperCase()} ${op.path}`];
46341
47695
  const ctx = {
@@ -46383,6 +47737,25 @@ function createSandbox(options = {}) {
46383
47737
  }
46384
47738
  return;
46385
47739
  }
47740
+ if (isConnectPath(url.pathname)) {
47741
+ try {
47742
+ const body2 = method === "POST" ? parseBody(await readBody(req), headers["content-type"]) : void 0;
47743
+ const answer = handleConnectPage(app, method, url.pathname, headers, url.searchParams, body2);
47744
+ if (answer.body !== void 0) {
47745
+ send2(res, requestId, answer);
47746
+ } else {
47747
+ const pageHeaders = { "X-Funnelchat-Sandbox": VERSION, ...answer.headers, [REQUEST_ID_HEADER]: requestId };
47748
+ if (answer.html !== void 0) pageHeaders["Content-Type"] = "text/html; charset=utf-8";
47749
+ res.writeHead(answer.status, pageHeaders);
47750
+ res.end(answer.html);
47751
+ }
47752
+ } catch (error) {
47753
+ const result2 = errorResult(error);
47754
+ send2(res, requestId, { ...result2, headers: { ...result2.headers, "Referrer-Policy": "no-referrer" } });
47755
+ }
47756
+ app.logger.info(`${method} /connect/*** \u2192 ${res.statusCode} (${Date.now() - started} ms)`);
47757
+ return;
47758
+ }
46386
47759
  if (url.pathname === "/" && method === "GET") {
46387
47760
  send2(res, requestId, {
46388
47761
  status: 200,
@@ -46421,7 +47794,8 @@ function createSandbox(options = {}) {
46421
47794
  server.off("error", reject);
46422
47795
  const { port } = server.address();
46423
47796
  const host = config.host === "0.0.0.0" || config.host === "::" ? "localhost" : config.host;
46424
- resolve2({ url: `http://${host}:${port}`, port });
47797
+ app.listenUrl = `http://${host}:${port}`;
47798
+ resolve2({ url: app.listenUrl, port });
46425
47799
  });
46426
47800
  }),
46427
47801
  close: () => new Promise((resolve2) => {
@@ -46433,5 +47807,5 @@ function createSandbox(options = {}) {
46433
47807
  }
46434
47808
 
46435
47809
  export { ConfigError, DEFAULT_CONFIG_FILE, DEFAULT_PORT, 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, parseArgs, resolveConfig, signature };
46436
- //# sourceMappingURL=chunk-PF4CGK6J.js.map
46437
- //# sourceMappingURL=chunk-PF4CGK6J.js.map
47810
+ //# sourceMappingURL=chunk-M5E6NAXM.js.map
47811
+ //# sourceMappingURL=chunk-M5E6NAXM.js.map