@funnelchat-hq/sandbox 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -4
- package/dist/{chunk-KH6A3I2A.js → chunk-M5E6NAXM.js} +2397 -235
- package/dist/cli.js +3 -1
- package/dist/index.d.ts +92 -2
- package/dist/index.js +1 -1
- package/funnelchat-sandbox.config.example.json +2 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -24748,7 +24775,164 @@ var openapi_default = {
|
|
|
24748
24775
|
},
|
|
24749
24776
|
summary: "Listar n\xFAmeros",
|
|
24750
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.",
|
|
24751
|
-
"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"
|
|
24752
24936
|
}
|
|
24753
24937
|
},
|
|
24754
24938
|
"/api/v1/devices/{device}/me": {
|
|
@@ -25079,6 +25263,14 @@ var openapi_default = {
|
|
|
25079
25263
|
capi_consent: {
|
|
25080
25264
|
type: "boolean",
|
|
25081
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."
|
|
25082
25274
|
}
|
|
25083
25275
|
}
|
|
25084
25276
|
}
|
|
@@ -25256,7 +25448,8 @@ var openapi_default = {
|
|
|
25256
25448
|
"x-fc-destructive": false,
|
|
25257
25449
|
summary: "Actualizar un n\xFAmero",
|
|
25258
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.",
|
|
25259
|
-
"x-fc-domain": "accounts"
|
|
25451
|
+
"x-fc-domain": "accounts",
|
|
25452
|
+
"x-fc-pending": "accounts-alta-de-numeros"
|
|
25260
25453
|
},
|
|
25261
25454
|
delete: {
|
|
25262
25455
|
operationId: "device.destroy",
|
|
@@ -25275,13 +25468,63 @@ var openapi_default = {
|
|
|
25275
25468
|
}
|
|
25276
25469
|
}
|
|
25277
25470
|
],
|
|
25471
|
+
responses: {
|
|
25472
|
+
"200": {
|
|
25473
|
+
description: "Deleted: an empty body, as today. Emits `device.deleted`."
|
|
25474
|
+
},
|
|
25475
|
+
"401": {
|
|
25476
|
+
$ref: "#/components/responses/AccountsAuthenticationException"
|
|
25477
|
+
},
|
|
25478
|
+
"403": {
|
|
25479
|
+
$ref: "#/components/responses/AccountsAuthorizationException"
|
|
25480
|
+
},
|
|
25481
|
+
"404": {
|
|
25482
|
+
$ref: "#/components/responses/AccountsModelNotFoundException"
|
|
25483
|
+
}
|
|
25484
|
+
},
|
|
25485
|
+
"x-fc-access": "write",
|
|
25486
|
+
"x-fc-permission": "accounts:operate",
|
|
25487
|
+
"x-fc-destructive": true,
|
|
25488
|
+
summary: "Eliminar un n\xFAmero",
|
|
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`.",
|
|
25490
|
+
"x-fc-domain": "accounts",
|
|
25491
|
+
"x-fc-pending-response": "accounts-alta-de-numeros"
|
|
25492
|
+
}
|
|
25493
|
+
},
|
|
25494
|
+
"/api/v1/devices/{device}/reset": {
|
|
25495
|
+
put: {
|
|
25496
|
+
operationId: "device.reset",
|
|
25497
|
+
description: "**Escritura** \xB7 requiere el permiso `accounts:operate`.\n\n> \u26A0\uFE0F **Operaci\xF3n irreversible.** No hay papelera ni deshacer: resetear o limpiar un n\xFAmero mata su sesi\xF3n de WhatsApp (hay que volver a escanear el c\xF3digo), purgar la cola descarta los mensajes encolados sin dejar registro de lo descartado, y los borrados eliminan el recurso.",
|
|
25498
|
+
tags: [
|
|
25499
|
+
"Device"
|
|
25500
|
+
],
|
|
25501
|
+
parameters: [
|
|
25502
|
+
{
|
|
25503
|
+
name: "device",
|
|
25504
|
+
in: "path",
|
|
25505
|
+
required: true,
|
|
25506
|
+
description: "The device ID",
|
|
25507
|
+
schema: {
|
|
25508
|
+
type: "integer"
|
|
25509
|
+
}
|
|
25510
|
+
}
|
|
25511
|
+
],
|
|
25278
25512
|
responses: {
|
|
25279
25513
|
"200": {
|
|
25280
25514
|
description: "",
|
|
25281
25515
|
content: {
|
|
25282
25516
|
"application/json": {
|
|
25283
25517
|
schema: {
|
|
25284
|
-
type: "object"
|
|
25518
|
+
type: "object",
|
|
25519
|
+
properties: {
|
|
25520
|
+
message: {
|
|
25521
|
+
type: "string",
|
|
25522
|
+
const: "Device reset processed successfully"
|
|
25523
|
+
}
|
|
25524
|
+
},
|
|
25525
|
+
required: [
|
|
25526
|
+
"message"
|
|
25527
|
+
]
|
|
25285
25528
|
}
|
|
25286
25529
|
}
|
|
25287
25530
|
}
|
|
@@ -25294,188 +25537,132 @@ var openapi_default = {
|
|
|
25294
25537
|
},
|
|
25295
25538
|
"404": {
|
|
25296
25539
|
$ref: "#/components/responses/AccountsModelNotFoundException"
|
|
25540
|
+
},
|
|
25541
|
+
"500": {
|
|
25542
|
+
description: "",
|
|
25543
|
+
content: {
|
|
25544
|
+
"application/json": {
|
|
25545
|
+
schema: {
|
|
25546
|
+
type: "object",
|
|
25547
|
+
properties: {
|
|
25548
|
+
message: {
|
|
25549
|
+
type: "string",
|
|
25550
|
+
const: "An error occurred while resetting the device"
|
|
25551
|
+
}
|
|
25552
|
+
},
|
|
25553
|
+
required: [
|
|
25554
|
+
"message"
|
|
25555
|
+
]
|
|
25556
|
+
}
|
|
25557
|
+
}
|
|
25558
|
+
}
|
|
25297
25559
|
}
|
|
25298
25560
|
},
|
|
25299
25561
|
"x-fc-access": "write",
|
|
25300
25562
|
"x-fc-permission": "accounts:operate",
|
|
25301
25563
|
"x-fc-destructive": true,
|
|
25302
|
-
summary: "
|
|
25303
|
-
"x-fc-proposito": "
|
|
25564
|
+
summary: "Reiniciar la sesi\xF3n de un n\xFAmero",
|
|
25565
|
+
"x-fc-proposito": "Sirve para reiniciar desde cero la sesi\xF3n de WhatsApp de un n\xFAmero; despu\xE9s hay que volver a escanear el c\xF3digo QR. Es irreversible y requiere `accounts:operate`.",
|
|
25304
25566
|
"x-fc-domain": "accounts"
|
|
25305
25567
|
}
|
|
25306
25568
|
},
|
|
25307
|
-
"/api/v1/devices/{device}/
|
|
25569
|
+
"/api/v1/devices/{device}/update-webhook-received": {
|
|
25308
25570
|
put: {
|
|
25309
|
-
operationId: "device.
|
|
25310
|
-
description: "**Escritura** \xB7 requiere el permiso `accounts:
|
|
25311
|
-
tags: [
|
|
25312
|
-
"Device"
|
|
25313
|
-
],
|
|
25314
|
-
parameters: [
|
|
25315
|
-
{
|
|
25316
|
-
name: "device",
|
|
25317
|
-
in: "path",
|
|
25318
|
-
required: true,
|
|
25319
|
-
description: "The device ID",
|
|
25320
|
-
schema: {
|
|
25321
|
-
type: "integer"
|
|
25322
|
-
}
|
|
25323
|
-
}
|
|
25324
|
-
],
|
|
25325
|
-
responses: {
|
|
25326
|
-
"200": {
|
|
25327
|
-
description: "",
|
|
25328
|
-
content: {
|
|
25329
|
-
"application/json": {
|
|
25330
|
-
schema: {
|
|
25331
|
-
type: "object"
|
|
25332
|
-
|
|
25333
|
-
|
|
25334
|
-
|
|
25335
|
-
|
|
25336
|
-
|
|
25337
|
-
|
|
25338
|
-
|
|
25339
|
-
|
|
25340
|
-
|
|
25341
|
-
|
|
25342
|
-
|
|
25343
|
-
|
|
25344
|
-
}
|
|
25345
|
-
|
|
25346
|
-
|
|
25347
|
-
|
|
25348
|
-
|
|
25349
|
-
|
|
25350
|
-
|
|
25351
|
-
|
|
25352
|
-
|
|
25353
|
-
|
|
25354
|
-
|
|
25355
|
-
|
|
25356
|
-
|
|
25357
|
-
|
|
25358
|
-
|
|
25359
|
-
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
|
|
25366
|
-
|
|
25367
|
-
|
|
25368
|
-
|
|
25369
|
-
|
|
25370
|
-
|
|
25371
|
-
|
|
25372
|
-
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
|
|
25379
|
-
|
|
25380
|
-
|
|
25381
|
-
|
|
25382
|
-
|
|
25383
|
-
|
|
25384
|
-
|
|
25385
|
-
|
|
25386
|
-
|
|
25387
|
-
|
|
25388
|
-
|
|
25389
|
-
|
|
25390
|
-
|
|
25391
|
-
|
|
25392
|
-
|
|
25393
|
-
|
|
25394
|
-
|
|
25395
|
-
|
|
25396
|
-
|
|
25397
|
-
|
|
25398
|
-
|
|
25399
|
-
|
|
25400
|
-
|
|
25401
|
-
|
|
25402
|
-
|
|
25403
|
-
|
|
25404
|
-
"application/json": {
|
|
25405
|
-
schema: {
|
|
25406
|
-
type: "object"
|
|
25407
|
-
}
|
|
25408
|
-
}
|
|
25409
|
-
}
|
|
25410
|
-
},
|
|
25411
|
-
"401": {
|
|
25412
|
-
$ref: "#/components/responses/AccountsAuthenticationException"
|
|
25413
|
-
},
|
|
25414
|
-
"403": {
|
|
25415
|
-
$ref: "#/components/responses/AccountsAuthorizationException"
|
|
25416
|
-
},
|
|
25417
|
-
"404": {
|
|
25418
|
-
$ref: "#/components/responses/AccountsModelNotFoundException"
|
|
25419
|
-
}
|
|
25420
|
-
},
|
|
25421
|
-
"x-fc-access": "write",
|
|
25422
|
-
"x-fc-permission": "accounts:write",
|
|
25423
|
-
"x-fc-destructive": false,
|
|
25424
|
-
summary: "Configurar el webhook de mensajes recibidos",
|
|
25425
|
-
"x-fc-proposito": "Sirve para configurar en un n\xFAmero el aviso de mensajes recibidos que usa Funnelchat para recibirlos.",
|
|
25426
|
-
"x-fc-domain": "accounts"
|
|
25427
|
-
}
|
|
25428
|
-
},
|
|
25429
|
-
"/api/v1/devices/{device}/update-webhook-received-and-delivery": {
|
|
25430
|
-
put: {
|
|
25431
|
-
operationId: "device.updateWebhookReceivedAndDelivery",
|
|
25432
|
-
description: "**Escritura** \xB7 requiere el permiso `accounts:write`.",
|
|
25433
|
-
tags: [
|
|
25434
|
-
"Device"
|
|
25435
|
-
],
|
|
25436
|
-
parameters: [
|
|
25437
|
-
{
|
|
25438
|
-
name: "device",
|
|
25439
|
-
in: "path",
|
|
25440
|
-
required: true,
|
|
25441
|
-
description: "The device ID",
|
|
25442
|
-
schema: {
|
|
25443
|
-
type: "integer"
|
|
25444
|
-
}
|
|
25445
|
-
}
|
|
25446
|
-
],
|
|
25447
|
-
responses: {
|
|
25448
|
-
"200": {
|
|
25449
|
-
description: "",
|
|
25450
|
-
content: {
|
|
25451
|
-
"application/json": {
|
|
25452
|
-
schema: {
|
|
25453
|
-
type: "object"
|
|
25454
|
-
}
|
|
25455
|
-
}
|
|
25456
|
-
}
|
|
25457
|
-
},
|
|
25458
|
-
"401": {
|
|
25459
|
-
$ref: "#/components/responses/AccountsAuthenticationException"
|
|
25460
|
-
},
|
|
25461
|
-
"403": {
|
|
25462
|
-
$ref: "#/components/responses/AccountsAuthorizationException"
|
|
25463
|
-
},
|
|
25464
|
-
"404": {
|
|
25465
|
-
$ref: "#/components/responses/AccountsModelNotFoundException"
|
|
25466
|
-
}
|
|
25467
|
-
},
|
|
25468
|
-
"x-fc-access": "write",
|
|
25469
|
-
"x-fc-permission": "accounts:write",
|
|
25470
|
-
"x-fc-destructive": false,
|
|
25471
|
-
summary: "Configurar el webhook de recibidos y entregas",
|
|
25472
|
-
"x-fc-proposito": "Sirve para configurar en un n\xFAmero los avisos de mensajes recibidos y de confirmaciones de entrega que usa Funnelchat.",
|
|
25473
|
-
"x-fc-domain": "accounts"
|
|
25474
|
-
}
|
|
25475
|
-
},
|
|
25476
|
-
"/api/v1/devices/{device}/clean": {
|
|
25477
|
-
post: {
|
|
25478
|
-
operationId: "device.clean",
|
|
25571
|
+
operationId: "device.updateWebhookReceived",
|
|
25572
|
+
description: "**Escritura** \xB7 requiere el permiso `accounts:write`.",
|
|
25573
|
+
tags: [
|
|
25574
|
+
"Device"
|
|
25575
|
+
],
|
|
25576
|
+
parameters: [
|
|
25577
|
+
{
|
|
25578
|
+
name: "device",
|
|
25579
|
+
in: "path",
|
|
25580
|
+
required: true,
|
|
25581
|
+
description: "The device ID",
|
|
25582
|
+
schema: {
|
|
25583
|
+
type: "integer"
|
|
25584
|
+
}
|
|
25585
|
+
}
|
|
25586
|
+
],
|
|
25587
|
+
responses: {
|
|
25588
|
+
"200": {
|
|
25589
|
+
description: "",
|
|
25590
|
+
content: {
|
|
25591
|
+
"application/json": {
|
|
25592
|
+
schema: {
|
|
25593
|
+
type: "object"
|
|
25594
|
+
}
|
|
25595
|
+
}
|
|
25596
|
+
}
|
|
25597
|
+
},
|
|
25598
|
+
"401": {
|
|
25599
|
+
$ref: "#/components/responses/AccountsAuthenticationException"
|
|
25600
|
+
},
|
|
25601
|
+
"403": {
|
|
25602
|
+
$ref: "#/components/responses/AccountsAuthorizationException"
|
|
25603
|
+
},
|
|
25604
|
+
"404": {
|
|
25605
|
+
$ref: "#/components/responses/AccountsModelNotFoundException"
|
|
25606
|
+
}
|
|
25607
|
+
},
|
|
25608
|
+
"x-fc-access": "write",
|
|
25609
|
+
"x-fc-permission": "accounts:write",
|
|
25610
|
+
"x-fc-destructive": false,
|
|
25611
|
+
summary: "Configurar el webhook de mensajes recibidos",
|
|
25612
|
+
"x-fc-proposito": "Sirve para configurar en un n\xFAmero el aviso de mensajes recibidos que usa Funnelchat para recibirlos.",
|
|
25613
|
+
"x-fc-domain": "accounts"
|
|
25614
|
+
}
|
|
25615
|
+
},
|
|
25616
|
+
"/api/v1/devices/{device}/update-webhook-received-and-delivery": {
|
|
25617
|
+
put: {
|
|
25618
|
+
operationId: "device.updateWebhookReceivedAndDelivery",
|
|
25619
|
+
description: "**Escritura** \xB7 requiere el permiso `accounts:write`.",
|
|
25620
|
+
tags: [
|
|
25621
|
+
"Device"
|
|
25622
|
+
],
|
|
25623
|
+
parameters: [
|
|
25624
|
+
{
|
|
25625
|
+
name: "device",
|
|
25626
|
+
in: "path",
|
|
25627
|
+
required: true,
|
|
25628
|
+
description: "The device ID",
|
|
25629
|
+
schema: {
|
|
25630
|
+
type: "integer"
|
|
25631
|
+
}
|
|
25632
|
+
}
|
|
25633
|
+
],
|
|
25634
|
+
responses: {
|
|
25635
|
+
"200": {
|
|
25636
|
+
description: "",
|
|
25637
|
+
content: {
|
|
25638
|
+
"application/json": {
|
|
25639
|
+
schema: {
|
|
25640
|
+
type: "object"
|
|
25641
|
+
}
|
|
25642
|
+
}
|
|
25643
|
+
}
|
|
25644
|
+
},
|
|
25645
|
+
"401": {
|
|
25646
|
+
$ref: "#/components/responses/AccountsAuthenticationException"
|
|
25647
|
+
},
|
|
25648
|
+
"403": {
|
|
25649
|
+
$ref: "#/components/responses/AccountsAuthorizationException"
|
|
25650
|
+
},
|
|
25651
|
+
"404": {
|
|
25652
|
+
$ref: "#/components/responses/AccountsModelNotFoundException"
|
|
25653
|
+
}
|
|
25654
|
+
},
|
|
25655
|
+
"x-fc-access": "write",
|
|
25656
|
+
"x-fc-permission": "accounts:write",
|
|
25657
|
+
"x-fc-destructive": false,
|
|
25658
|
+
summary: "Configurar el webhook de recibidos y entregas",
|
|
25659
|
+
"x-fc-proposito": "Sirve para configurar en un n\xFAmero los avisos de mensajes recibidos y de confirmaciones de entrega que usa Funnelchat.",
|
|
25660
|
+
"x-fc-domain": "accounts"
|
|
25661
|
+
}
|
|
25662
|
+
},
|
|
25663
|
+
"/api/v1/devices/{device}/clean": {
|
|
25664
|
+
post: {
|
|
25665
|
+
operationId: "device.clean",
|
|
25479
25666
|
description: "**Escritura** \xB7 requiere el permiso `accounts:operate`.\n\n> \u26A0\uFE0F **Operaci\xF3n irreversible.** No hay papelera ni deshacer: resetear o limpiar un n\xFAmero mata su sesi\xF3n de WhatsApp (hay que volver a escanear el c\xF3digo), purgar la cola descarta los mensajes encolados sin dejar registro de lo descartado, y los borrados eliminan el recurso.",
|
|
25480
25667
|
tags: [
|
|
25481
25668
|
"Device"
|
|
@@ -29520,6 +29707,477 @@ var openapi_default = {
|
|
|
29520
29707
|
}
|
|
29521
29708
|
}
|
|
29522
29709
|
}
|
|
29710
|
+
},
|
|
29711
|
+
"/api/v1/devices/{device}/qr": {
|
|
29712
|
+
get: {
|
|
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`.",
|
|
29716
|
+
tags: [
|
|
29717
|
+
"Device"
|
|
29718
|
+
],
|
|
29719
|
+
"x-fc-domain": "accounts",
|
|
29720
|
+
"x-fc-permission": "accounts:operate",
|
|
29721
|
+
"x-fc-pending": "accounts-alta-de-numeros",
|
|
29722
|
+
parameters: [
|
|
29723
|
+
{
|
|
29724
|
+
name: "device",
|
|
29725
|
+
in: "path",
|
|
29726
|
+
required: true,
|
|
29727
|
+
schema: {
|
|
29728
|
+
type: "integer"
|
|
29729
|
+
}
|
|
29730
|
+
}
|
|
29731
|
+
],
|
|
29732
|
+
responses: {
|
|
29733
|
+
"200": {
|
|
29734
|
+
description: "OK",
|
|
29735
|
+
content: {
|
|
29736
|
+
"application/json": {
|
|
29737
|
+
schema: {
|
|
29738
|
+
$ref: "#/components/schemas/PendingSandboxDeviceQr"
|
|
29739
|
+
}
|
|
29740
|
+
}
|
|
29741
|
+
}
|
|
29742
|
+
},
|
|
29743
|
+
"403": {
|
|
29744
|
+
description: "`number_limit_reached`, `connect_not_allowed` or `payment_required`",
|
|
29745
|
+
content: {
|
|
29746
|
+
"application/json": {
|
|
29747
|
+
schema: {
|
|
29748
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
29749
|
+
}
|
|
29750
|
+
}
|
|
29751
|
+
}
|
|
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
|
+
}
|
|
29761
|
+
}
|
|
29762
|
+
},
|
|
29763
|
+
"409": {
|
|
29764
|
+
description: "`trial_number_blocked`, `qr_requires_coexistence`, `number_suspended_by_plan`, `device_inactive` or `provisioning_quota_exceeded`",
|
|
29765
|
+
content: {
|
|
29766
|
+
"application/json": {
|
|
29767
|
+
schema: {
|
|
29768
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
29769
|
+
}
|
|
29770
|
+
}
|
|
29771
|
+
}
|
|
29772
|
+
},
|
|
29773
|
+
"422": {
|
|
29774
|
+
description: "`provider_unsupported`",
|
|
29775
|
+
content: {
|
|
29776
|
+
"application/json": {
|
|
29777
|
+
schema: {
|
|
29778
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
29779
|
+
}
|
|
29780
|
+
}
|
|
29781
|
+
}
|
|
29782
|
+
},
|
|
29783
|
+
"502": {
|
|
29784
|
+
description: "`provider_error`",
|
|
29785
|
+
content: {
|
|
29786
|
+
"application/json": {
|
|
29787
|
+
schema: {
|
|
29788
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
29789
|
+
}
|
|
29790
|
+
}
|
|
29791
|
+
}
|
|
29792
|
+
}
|
|
29793
|
+
}
|
|
29794
|
+
}
|
|
29795
|
+
},
|
|
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`.",
|
|
29801
|
+
tags: [
|
|
29802
|
+
"ConnectLink"
|
|
29803
|
+
],
|
|
29804
|
+
"x-fc-domain": "accounts",
|
|
29805
|
+
"x-fc-permission": "accounts:read",
|
|
29806
|
+
"x-fc-pending": "accounts-alta-de-numeros",
|
|
29807
|
+
parameters: [
|
|
29808
|
+
{
|
|
29809
|
+
name: "status",
|
|
29810
|
+
in: "query",
|
|
29811
|
+
required: false,
|
|
29812
|
+
schema: {
|
|
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
|
|
29849
|
+
}
|
|
29850
|
+
}
|
|
29851
|
+
],
|
|
29852
|
+
responses: {
|
|
29853
|
+
"200": {
|
|
29854
|
+
description: "OK",
|
|
29855
|
+
content: {
|
|
29856
|
+
"application/json": {
|
|
29857
|
+
schema: {
|
|
29858
|
+
type: "object",
|
|
29859
|
+
properties: {
|
|
29860
|
+
data: {
|
|
29861
|
+
type: "array",
|
|
29862
|
+
items: {
|
|
29863
|
+
$ref: "#/components/schemas/PendingSandboxConnectLink"
|
|
29864
|
+
}
|
|
29865
|
+
},
|
|
29866
|
+
links: {
|
|
29867
|
+
type: "object"
|
|
29868
|
+
},
|
|
29869
|
+
meta: {
|
|
29870
|
+
type: "object"
|
|
29871
|
+
}
|
|
29872
|
+
}
|
|
29873
|
+
}
|
|
29874
|
+
}
|
|
29875
|
+
}
|
|
29876
|
+
}
|
|
29877
|
+
},
|
|
29878
|
+
"x-fc-validation-error": "invalid_request"
|
|
29879
|
+
},
|
|
29880
|
+
post: {
|
|
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.',
|
|
29884
|
+
tags: [
|
|
29885
|
+
"ConnectLink"
|
|
29886
|
+
],
|
|
29887
|
+
"x-fc-domain": "accounts",
|
|
29888
|
+
"x-fc-permission": "accounts:operate",
|
|
29889
|
+
"x-fc-pending": "accounts-alta-de-numeros",
|
|
29890
|
+
parameters: [
|
|
29891
|
+
{
|
|
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`.",
|
|
29896
|
+
schema: {
|
|
29897
|
+
type: "string",
|
|
29898
|
+
maxLength: 255
|
|
29899
|
+
}
|
|
29900
|
+
}
|
|
29901
|
+
],
|
|
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: {
|
|
29922
|
+
type: "string",
|
|
29923
|
+
enum: [
|
|
29924
|
+
"coexistence",
|
|
29925
|
+
"dedicated"
|
|
29926
|
+
]
|
|
29927
|
+
},
|
|
29928
|
+
minItems: 1,
|
|
29929
|
+
maxItems: 2,
|
|
29930
|
+
uniqueItems: true,
|
|
29931
|
+
description: "Required with `type: meta`, forbidden with `qr`. With both, your customer chooses."
|
|
29932
|
+
},
|
|
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
|
+
]
|
|
29985
|
+
}
|
|
29986
|
+
}
|
|
29987
|
+
}
|
|
29988
|
+
},
|
|
29989
|
+
responses: {
|
|
29990
|
+
"201": {
|
|
29991
|
+
description: "Created",
|
|
29992
|
+
content: {
|
|
29993
|
+
"application/json": {
|
|
29994
|
+
schema: {
|
|
29995
|
+
type: "object",
|
|
29996
|
+
properties: {
|
|
29997
|
+
data: {
|
|
29998
|
+
$ref: "#/components/schemas/PendingSandboxConnectLinkCreated"
|
|
29999
|
+
}
|
|
30000
|
+
},
|
|
30001
|
+
required: [
|
|
30002
|
+
"data"
|
|
30003
|
+
]
|
|
30004
|
+
}
|
|
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
|
+
}
|
|
30020
|
+
}
|
|
30021
|
+
},
|
|
30022
|
+
"403": {
|
|
30023
|
+
description: "`numbers_not_included` or `number_limit_reached`",
|
|
30024
|
+
content: {
|
|
30025
|
+
"application/json": {
|
|
30026
|
+
schema: {
|
|
30027
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
30028
|
+
}
|
|
30029
|
+
}
|
|
30030
|
+
}
|
|
30031
|
+
},
|
|
30032
|
+
"409": {
|
|
30033
|
+
description: "`operation_in_progress`",
|
|
30034
|
+
content: {
|
|
30035
|
+
"application/json": {
|
|
30036
|
+
schema: {
|
|
30037
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
30038
|
+
}
|
|
30039
|
+
}
|
|
30040
|
+
}
|
|
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
|
+
}
|
|
30050
|
+
}
|
|
30051
|
+
},
|
|
30052
|
+
"429": {
|
|
30053
|
+
description: '`too_many_open_links`, or the per-minute limit (`{"message": "Too Many Attempts."}`)',
|
|
30054
|
+
content: {
|
|
30055
|
+
"application/json": {
|
|
30056
|
+
schema: {
|
|
30057
|
+
oneOf: [
|
|
30058
|
+
{
|
|
30059
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
30060
|
+
},
|
|
30061
|
+
{
|
|
30062
|
+
$ref: "#/components/schemas/PendingSandboxTooManyAttempts"
|
|
30063
|
+
}
|
|
30064
|
+
]
|
|
30065
|
+
}
|
|
30066
|
+
}
|
|
30067
|
+
}
|
|
30068
|
+
},
|
|
30069
|
+
"503": {
|
|
30070
|
+
description: "`meta_connect_disabled`",
|
|
30071
|
+
content: {
|
|
30072
|
+
"application/json": {
|
|
30073
|
+
schema: {
|
|
30074
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
30075
|
+
}
|
|
30076
|
+
}
|
|
30077
|
+
}
|
|
30078
|
+
}
|
|
30079
|
+
},
|
|
30080
|
+
"x-fc-validation-error": "invalid_request"
|
|
30081
|
+
}
|
|
30082
|
+
},
|
|
30083
|
+
"/api/v1/connect-links/{link}": {
|
|
30084
|
+
get: {
|
|
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.",
|
|
30088
|
+
tags: [
|
|
30089
|
+
"ConnectLink"
|
|
30090
|
+
],
|
|
30091
|
+
"x-fc-domain": "accounts",
|
|
30092
|
+
"x-fc-permission": "accounts:read",
|
|
30093
|
+
"x-fc-pending": "accounts-alta-de-numeros",
|
|
30094
|
+
parameters: [
|
|
30095
|
+
{
|
|
30096
|
+
name: "link",
|
|
30097
|
+
in: "path",
|
|
30098
|
+
required: true,
|
|
30099
|
+
schema: {
|
|
30100
|
+
type: "integer"
|
|
30101
|
+
}
|
|
30102
|
+
}
|
|
30103
|
+
],
|
|
30104
|
+
responses: {
|
|
30105
|
+
"200": {
|
|
30106
|
+
description: "OK",
|
|
30107
|
+
content: {
|
|
30108
|
+
"application/json": {
|
|
30109
|
+
schema: {
|
|
30110
|
+
type: "object",
|
|
30111
|
+
properties: {
|
|
30112
|
+
data: {
|
|
30113
|
+
$ref: "#/components/schemas/PendingSandboxConnectLink"
|
|
30114
|
+
}
|
|
30115
|
+
},
|
|
30116
|
+
required: [
|
|
30117
|
+
"data"
|
|
30118
|
+
]
|
|
30119
|
+
}
|
|
30120
|
+
}
|
|
30121
|
+
}
|
|
30122
|
+
},
|
|
30123
|
+
"404": {
|
|
30124
|
+
description: "`not_found`",
|
|
30125
|
+
content: {
|
|
30126
|
+
"application/json": {
|
|
30127
|
+
schema: {
|
|
30128
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
30129
|
+
}
|
|
30130
|
+
}
|
|
30131
|
+
}
|
|
30132
|
+
}
|
|
30133
|
+
}
|
|
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`.",
|
|
30139
|
+
tags: [
|
|
30140
|
+
"ConnectLink"
|
|
30141
|
+
],
|
|
30142
|
+
"x-fc-domain": "accounts",
|
|
30143
|
+
"x-fc-permission": "accounts:operate",
|
|
30144
|
+
"x-fc-pending": "accounts-alta-de-numeros",
|
|
30145
|
+
parameters: [
|
|
30146
|
+
{
|
|
30147
|
+
name: "link",
|
|
30148
|
+
in: "path",
|
|
30149
|
+
required: true,
|
|
30150
|
+
schema: {
|
|
30151
|
+
type: "integer"
|
|
30152
|
+
}
|
|
30153
|
+
}
|
|
30154
|
+
],
|
|
30155
|
+
responses: {
|
|
30156
|
+
"204": {
|
|
30157
|
+
description: "Revoked"
|
|
30158
|
+
},
|
|
30159
|
+
"404": {
|
|
30160
|
+
description: "`not_found`",
|
|
30161
|
+
content: {
|
|
30162
|
+
"application/json": {
|
|
30163
|
+
schema: {
|
|
30164
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
30165
|
+
}
|
|
30166
|
+
}
|
|
30167
|
+
}
|
|
30168
|
+
},
|
|
30169
|
+
"409": {
|
|
30170
|
+
description: "`link_not_pending`",
|
|
30171
|
+
content: {
|
|
30172
|
+
"application/json": {
|
|
30173
|
+
schema: {
|
|
30174
|
+
$ref: "#/components/schemas/PendingSandboxAccountsError"
|
|
30175
|
+
}
|
|
30176
|
+
}
|
|
30177
|
+
}
|
|
30178
|
+
}
|
|
30179
|
+
}
|
|
30180
|
+
}
|
|
29523
30181
|
}
|
|
29524
30182
|
},
|
|
29525
30183
|
webhooks: {
|
|
@@ -31717,18 +32375,43 @@ var openapi_default = {
|
|
|
31717
32375
|
"string",
|
|
31718
32376
|
"null"
|
|
31719
32377
|
],
|
|
31720
|
-
description: "
|
|
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
|
+
]
|
|
31721
32393
|
}
|
|
31722
32394
|
},
|
|
31723
32395
|
required: [
|
|
31724
32396
|
"id",
|
|
31725
32397
|
"name",
|
|
31726
|
-
"number"
|
|
31727
|
-
|
|
32398
|
+
"number",
|
|
32399
|
+
"connection",
|
|
32400
|
+
"external_reference"
|
|
32401
|
+
],
|
|
32402
|
+
"x-fc-pending": "accounts-alta-de-numeros"
|
|
31728
32403
|
},
|
|
31729
32404
|
connected_at: {
|
|
31730
32405
|
type: "string",
|
|
31731
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"
|
|
31732
32415
|
}
|
|
31733
32416
|
},
|
|
31734
32417
|
required: [
|
|
@@ -31737,6 +32420,23 @@ var openapi_default = {
|
|
|
31737
32420
|
]
|
|
31738
32421
|
}
|
|
31739
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
|
+
}
|
|
31740
32440
|
}
|
|
31741
32441
|
}
|
|
31742
32442
|
}
|
|
@@ -31776,18 +32476,56 @@ var openapi_default = {
|
|
|
31776
32476
|
"string",
|
|
31777
32477
|
"null"
|
|
31778
32478
|
],
|
|
31779
|
-
description: "
|
|
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
|
+
]
|
|
31780
32494
|
}
|
|
31781
32495
|
},
|
|
31782
32496
|
required: [
|
|
31783
32497
|
"id",
|
|
31784
32498
|
"name",
|
|
31785
|
-
"number"
|
|
31786
|
-
|
|
32499
|
+
"number",
|
|
32500
|
+
"connection",
|
|
32501
|
+
"external_reference"
|
|
32502
|
+
],
|
|
32503
|
+
"x-fc-pending": "accounts-alta-de-numeros"
|
|
31787
32504
|
},
|
|
31788
32505
|
disconnected_at: {
|
|
31789
32506
|
type: "string",
|
|
31790
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"
|
|
31791
32529
|
}
|
|
31792
32530
|
},
|
|
31793
32531
|
required: [
|
|
@@ -31796,6 +32534,24 @@ var openapi_default = {
|
|
|
31796
32534
|
]
|
|
31797
32535
|
}
|
|
31798
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
|
+
}
|
|
31799
32555
|
}
|
|
31800
32556
|
}
|
|
31801
32557
|
}
|
|
@@ -32264,6 +33020,226 @@ var openapi_default = {
|
|
|
32264
33020
|
}
|
|
32265
33021
|
}
|
|
32266
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
|
+
}
|
|
32267
33243
|
}
|
|
32268
33244
|
},
|
|
32269
33245
|
components: {
|
|
@@ -41349,6 +42325,345 @@ var openapi_default = {
|
|
|
41349
42325
|
required: [
|
|
41350
42326
|
"message"
|
|
41351
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
|
+
]
|
|
41352
42667
|
}
|
|
41353
42668
|
},
|
|
41354
42669
|
responses: {
|
|
@@ -41768,7 +43083,7 @@ var openapi_default = {
|
|
|
41768
43083
|
inputs: {
|
|
41769
43084
|
"spec/funnelchat.json": "8bbbae569b4cab520884db98f28204156c1bd6c629578bee609343c193e0dadb",
|
|
41770
43085
|
"spec/pending.json": "a14bc4dbc3025d3e958ffda8afd1fa9fdab34af264418060825f3300799aceb0",
|
|
41771
|
-
"spec/pending-sandbox.json": "
|
|
43086
|
+
"spec/pending-sandbox.json": "b2f7c2ccbab7c90f7adf22f8c3cec6700ff647700571dc306c898af8127bb22d"
|
|
41772
43087
|
},
|
|
41773
43088
|
snapshot_commit: "0ef4abb008870ed8c7cae49a08900a81e5e57ed8",
|
|
41774
43089
|
pending_commit: "88557ce7819c5a4061e84c58053a1b6c49f35ec2",
|
|
@@ -41827,15 +43142,28 @@ var openapi_default = {
|
|
|
41827
43142
|
"GET /api/v1/forms/families/{familyId}/responses [pending-sandbox.json]",
|
|
41828
43143
|
"GET /api/v1/forms/families/{familyId}/responses/{runId} [pending-sandbox.json]",
|
|
41829
43144
|
"POST /api/v1/contacts/{contact}/message/form [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]",
|
|
41830
43151
|
"POST /api/v1/contacts/{contact}/message (cuerpo: file_type, file_name) [pending-sandbox.json]",
|
|
41831
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]",
|
|
41832
43155
|
"POST /api/v1/contacts/{contact}/message (respuestas: 200) [pending-sandbox.json]",
|
|
41833
43156
|
"POST /api/v1/message/send-message (respuestas: 200) [pending-sandbox.json]",
|
|
41834
43157
|
"POST /api/v1/devices/{device}/templates/{template}/send-template (respuestas: 200) [pending-sandbox.json]",
|
|
41835
43158
|
"POST /api/v1/contacts/{contact}/message/contact-card (respuestas: 200) [pending-sandbox.json]",
|
|
41836
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]",
|
|
41837
43163
|
"webhook message.received (data: device_id) [pending-sandbox.json]",
|
|
41838
|
-
"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]"
|
|
41839
43167
|
]
|
|
41840
43168
|
}
|
|
41841
43169
|
};
|
|
@@ -41914,8 +43242,8 @@ function successResponse(op) {
|
|
|
41914
43242
|
const entries = Object.entries(op.responses ?? {}).filter(([status2]) => /^2\d\d$/.test(status2)).sort(([a], [b]) => Number(a) - Number(b));
|
|
41915
43243
|
const [status, raw] = entries[0] ?? ["200", void 0];
|
|
41916
43244
|
const response = deref(raw);
|
|
41917
|
-
const
|
|
41918
|
-
return { status: Number(status), schema:
|
|
43245
|
+
const json2 = response?.content?.["application/json"];
|
|
43246
|
+
return { status: Number(status), schema: json2?.schema, example: json2?.example };
|
|
41919
43247
|
}
|
|
41920
43248
|
|
|
41921
43249
|
// src/openapi/example.ts
|
|
@@ -42017,7 +43345,49 @@ function specData(type) {
|
|
|
42017
43345
|
}
|
|
42018
43346
|
return {};
|
|
42019
43347
|
}
|
|
42020
|
-
var
|
|
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);
|
|
42021
43391
|
function catalogEntry(type) {
|
|
42022
43392
|
return catalog.find((entry) => entry.type === type);
|
|
42023
43393
|
}
|
|
@@ -42337,6 +43707,7 @@ var edgeError = (code) => {
|
|
|
42337
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(", ") });
|
|
42338
43708
|
var validationFailed = (errors) => new ApiError(422, "validation_failed", summarize(errors), { errors });
|
|
42339
43709
|
var fieldError = (field, message) => validationFailed({ [field]: [message] });
|
|
43710
|
+
var invalidRequest = (errors) => new ApiError(422, "invalid_request", summarize(errors), { errors });
|
|
42340
43711
|
|
|
42341
43712
|
// src/forms/templates.ts
|
|
42342
43713
|
var FORM_TEMPLATES = [
|
|
@@ -42608,8 +43979,8 @@ function parseForced(mode, value, field) {
|
|
|
42608
43979
|
function takeForcedFailure(app, headers, contactId) {
|
|
42609
43980
|
const reject = headers["x-sandbox-reject"];
|
|
42610
43981
|
if (reject) return { mode: "reject", code: parseForced("reject", reject, "X-Sandbox-Reject") };
|
|
42611
|
-
const
|
|
42612
|
-
if (
|
|
43982
|
+
const fail2 = headers["x-sandbox-fail"];
|
|
43983
|
+
if (fail2) return { mode: "fail", code: parseForced("fail", fail2, "X-Sandbox-Fail") };
|
|
42613
43984
|
const armed = app.store.failures.find((f) => f.mode !== "edge" && f.remaining > 0 && (f.contactId === null || f.contactId === contactId));
|
|
42614
43985
|
if (!armed) return null;
|
|
42615
43986
|
armed.remaining--;
|
|
@@ -42785,7 +44156,7 @@ function resolveFile(body2, style) {
|
|
|
42785
44156
|
|
|
42786
44157
|
// package.json
|
|
42787
44158
|
var package_default = {
|
|
42788
|
-
version: "0.
|
|
44159
|
+
version: "0.2.0"};
|
|
42789
44160
|
|
|
42790
44161
|
// src/version.ts
|
|
42791
44162
|
var VERSION = package_default.version;
|
|
@@ -42821,6 +44192,13 @@ var Store = class {
|
|
|
42821
44192
|
forms = /* @__PURE__ */ new Map();
|
|
42822
44193
|
formFamilies = /* @__PURE__ */ new Map();
|
|
42823
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 };
|
|
42824
44202
|
/** Last inbound message per `contactId:deviceId`: the 24-hour window. */
|
|
42825
44203
|
lastInbound = /* @__PURE__ */ new Map();
|
|
42826
44204
|
/** API-key sends per contact since its last inbound message: the antiblock counters. */
|
|
@@ -42843,7 +44221,9 @@ var Store = class {
|
|
|
42843
44221
|
note: new Sequence(1),
|
|
42844
44222
|
formFamily: new Sequence(8001),
|
|
42845
44223
|
formVersion: new Sequence(8101),
|
|
42846
|
-
formRun: new Sequence(1)
|
|
44224
|
+
formRun: new Sequence(1),
|
|
44225
|
+
device: new Sequence(5),
|
|
44226
|
+
connectLink: new Sequence(1)
|
|
42847
44227
|
};
|
|
42848
44228
|
logRequest(entry) {
|
|
42849
44229
|
const full = { id: this.ids.request.next(), ...entry };
|
|
@@ -42877,8 +44257,14 @@ function seed(store, now = /* @__PURE__ */ new Date()) {
|
|
|
42877
44257
|
provider: 2,
|
|
42878
44258
|
kind: "cloud",
|
|
42879
44259
|
connected: true,
|
|
44260
|
+
coexistence: false,
|
|
44261
|
+
fromPool: false,
|
|
44262
|
+
externalReference: null,
|
|
44263
|
+
health: "available",
|
|
44264
|
+
lastConnectLinkId: null,
|
|
42880
44265
|
wabaId: "100000000000001",
|
|
42881
44266
|
phoneNumberId: "100000000000003",
|
|
44267
|
+
firstConnectedAt: now,
|
|
42882
44268
|
createdAt: now
|
|
42883
44269
|
});
|
|
42884
44270
|
store.devices.set(4, {
|
|
@@ -42888,8 +44274,14 @@ function seed(store, now = /* @__PURE__ */ new Date()) {
|
|
|
42888
44274
|
provider: 1,
|
|
42889
44275
|
kind: "qr",
|
|
42890
44276
|
connected: true,
|
|
44277
|
+
coexistence: false,
|
|
44278
|
+
fromPool: false,
|
|
44279
|
+
externalReference: null,
|
|
44280
|
+
health: null,
|
|
44281
|
+
lastConnectLinkId: null,
|
|
42891
44282
|
wabaId: null,
|
|
42892
44283
|
phoneNumberId: null,
|
|
44284
|
+
firstConnectedAt: now,
|
|
42893
44285
|
createdAt: now
|
|
42894
44286
|
});
|
|
42895
44287
|
const contactId = store.ids.contact.next();
|
|
@@ -43161,8 +44553,15 @@ var SandboxApp = class {
|
|
|
43161
44553
|
webhooks;
|
|
43162
44554
|
/** Grows on every reset, so work started before a reset does not touch the new state. */
|
|
43163
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
|
+
}
|
|
43164
44562
|
populate() {
|
|
43165
44563
|
seed(this.store);
|
|
44564
|
+
this.store.numbers = { ...this.config.numbers };
|
|
43166
44565
|
for (const preset of this.config.subscriptions) {
|
|
43167
44566
|
const unknown = preset.events.filter((e) => !isSubscribable(e));
|
|
43168
44567
|
if (unknown.length) this.logger.warn(`subscription from the configuration with types that are not in the catalog: ${unknown.join(", ")}`);
|
|
@@ -43209,6 +44608,49 @@ function listContext(ctx) {
|
|
|
43209
44608
|
function body(ctx) {
|
|
43210
44609
|
return typeof ctx.body === "object" && ctx.body !== null && !Array.isArray(ctx.body) ? ctx.body : {};
|
|
43211
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
|
+
}
|
|
43212
44654
|
|
|
43213
44655
|
// src/http/paginate.ts
|
|
43214
44656
|
function perPageOf(query, fallback, max = 100) {
|
|
@@ -43230,27 +44672,27 @@ function urlWith(path, query, extra) {
|
|
|
43230
44672
|
function paginate(items, ctx, map, fallbackPerPage = 25) {
|
|
43231
44673
|
const perPage = perPageOf(ctx.query, fallbackPerPage);
|
|
43232
44674
|
const pageRaw = Number(ctx.query.page ?? 1);
|
|
43233
|
-
const
|
|
44675
|
+
const page2 = Number.isInteger(pageRaw) && pageRaw > 0 ? pageRaw : 1;
|
|
43234
44676
|
const total = items.length;
|
|
43235
44677
|
const lastPage = Math.max(1, Math.ceil(total / perPage));
|
|
43236
|
-
const slice = items.slice((
|
|
43237
|
-
const from = slice.length ? (
|
|
44678
|
+
const slice = items.slice((page2 - 1) * perPage, page2 * perPage);
|
|
44679
|
+
const from = slice.length ? (page2 - 1) * perPage + 1 : null;
|
|
43238
44680
|
const to = slice.length ? from + slice.length - 1 : null;
|
|
43239
44681
|
const link = (n) => urlWith(ctx.path, ctx.query, { page: n });
|
|
43240
44682
|
const pageLinks = [
|
|
43241
|
-
{ url:
|
|
43242
|
-
...Array.from({ length: lastPage }, (_, i) => ({ url: link(i + 1), label: String(i + 1), active: i + 1 ===
|
|
43243
|
-
{ url:
|
|
44683
|
+
{ url: page2 > 1 ? link(page2 - 1) : null, label: "« 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 »", active: false }
|
|
43244
44686
|
];
|
|
43245
44687
|
return {
|
|
43246
44688
|
data: slice.map(map),
|
|
43247
44689
|
links: {
|
|
43248
44690
|
first: link(1),
|
|
43249
44691
|
last: link(lastPage),
|
|
43250
|
-
prev:
|
|
43251
|
-
next:
|
|
44692
|
+
prev: page2 > 1 ? link(page2 - 1) : null,
|
|
44693
|
+
next: page2 < lastPage ? link(page2 + 1) : null
|
|
43252
44694
|
},
|
|
43253
|
-
meta: { current_page:
|
|
44695
|
+
meta: { current_page: page2, from, last_page: lastPage, links: pageLinks, path: ctx.path, per_page: perPage, to, total }
|
|
43254
44696
|
};
|
|
43255
44697
|
}
|
|
43256
44698
|
function encodeCursor(position) {
|
|
@@ -43457,7 +44899,7 @@ function deviceCapabilities(device) {
|
|
|
43457
44899
|
function deviceHealth(device) {
|
|
43458
44900
|
if (device.kind !== "cloud") return null;
|
|
43459
44901
|
return {
|
|
43460
|
-
state: device.
|
|
44902
|
+
state: device.health ?? "unknown",
|
|
43461
44903
|
reason: null,
|
|
43462
44904
|
reason_truncated: false,
|
|
43463
44905
|
reason_key: null,
|
|
@@ -43469,15 +44911,19 @@ function deviceHealth(device) {
|
|
|
43469
44911
|
stale: false
|
|
43470
44912
|
};
|
|
43471
44913
|
}
|
|
44914
|
+
function deviceConnection(device) {
|
|
44915
|
+
if (device.kind === "qr") return "qr";
|
|
44916
|
+
return device.coexistence ? "coexistence" : "dedicated";
|
|
44917
|
+
}
|
|
43472
44918
|
function deviceResource(device) {
|
|
43473
44919
|
return {
|
|
43474
44920
|
id: device.id,
|
|
43475
44921
|
alias: device.alias,
|
|
43476
44922
|
image: "",
|
|
43477
44923
|
color: device.kind === "cloud" ? "#25D366" : "#128C7E",
|
|
43478
|
-
number: device.number,
|
|
44924
|
+
number: device.number ?? "",
|
|
43479
44925
|
normalized_number: device.number,
|
|
43480
|
-
first_conecction_at: laravelJson(device.
|
|
44926
|
+
first_conecction_at: device.firstConnectedAt ? laravelJson(device.firstConnectedAt) : null,
|
|
43481
44927
|
user_id: ACCOUNT_USER_ID,
|
|
43482
44928
|
provider: device.provider,
|
|
43483
44929
|
waba_id: device.wabaId ?? "",
|
|
@@ -43487,15 +44933,43 @@ function deviceResource(device) {
|
|
|
43487
44933
|
active: 1,
|
|
43488
44934
|
health: deviceHealth(device),
|
|
43489
44935
|
number_key: device.number,
|
|
43490
|
-
number_key_confirmed:
|
|
43491
|
-
is_coexistence:
|
|
44936
|
+
number_key_confirmed: device.number !== null,
|
|
44937
|
+
is_coexistence: device.coexistence,
|
|
43492
44938
|
slot_state: "held",
|
|
43493
44939
|
pool_suspended: false,
|
|
43494
44940
|
pool_suspension_reason: null,
|
|
43495
44941
|
pool_keep: false,
|
|
43496
44942
|
qr_allowed: device.kind === "qr",
|
|
43497
44943
|
can_change_to_meta: false,
|
|
43498
|
-
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
|
|
43499
44973
|
};
|
|
43500
44974
|
}
|
|
43501
44975
|
function publicMessage(message) {
|
|
@@ -43585,6 +45059,354 @@ function deliveryResource(delivery) {
|
|
|
43585
45059
|
};
|
|
43586
45060
|
}
|
|
43587
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
|
+
|
|
43588
45410
|
// src/handlers/contacts.ts
|
|
43589
45411
|
function getContact(app, id) {
|
|
43590
45412
|
const contact = app.store.contacts.get(Number(id));
|
|
@@ -43941,6 +45763,149 @@ function getCustomField(ctx) {
|
|
|
43941
45763
|
if (!field) throw objectNotFound();
|
|
43942
45764
|
return field;
|
|
43943
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
|
+
};
|
|
43944
45909
|
|
|
43945
45910
|
// src/messaging/lifecycle.ts
|
|
43946
45911
|
var ORDER = { sent: 1, delivered: 2, read: 3 };
|
|
@@ -44320,14 +46285,6 @@ var messageHandlers = {
|
|
|
44320
46285
|
if (template.deviceId !== Number(ctx.path.device)) throw notFound("La plantilla no es de este n\xFAmero.");
|
|
44321
46286
|
ctx.app.store.templates.delete(template.id);
|
|
44322
46287
|
return noContent();
|
|
44323
|
-
},
|
|
44324
|
-
// --- numbers (accounts) -----------------------------------------------------------------------------------
|
|
44325
|
-
// With an API key the list always paginates (accounts `PublicApiListings`: `perPage`, 25 by default, up to 100, by id).
|
|
44326
|
-
"GET /api/v1/devices": (ctx) => ok(paginate([...ctx.app.store.devices.values()].sort((a, b) => a.id - b.id), listContext(ctx), deviceResource)),
|
|
44327
|
-
"GET /api/v1/devices/{device}": (ctx) => {
|
|
44328
|
-
const device = ctx.app.store.devices.get(Number(ctx.path.device));
|
|
44329
|
-
if (!device) throw new RawApiError(404, { message: "Object not found" });
|
|
44330
|
-
return ok(deviceResource(device));
|
|
44331
46288
|
}
|
|
44332
46289
|
};
|
|
44333
46290
|
function setTemplateStatus(app, template, status, reason) {
|
|
@@ -45028,6 +46985,132 @@ function parseQuery(search) {
|
|
|
45028
46985
|
}
|
|
45029
46986
|
return out;
|
|
45030
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
|
+
}
|
|
45031
47114
|
|
|
45032
47115
|
// src/sandbox/panel.ts
|
|
45033
47116
|
function panelHtml() {
|
|
@@ -45133,18 +47216,34 @@ function contactOf(app, id) {
|
|
|
45133
47216
|
if (!contact) throw notFound("The contact does not exist in the sandbox.");
|
|
45134
47217
|
return contact;
|
|
45135
47218
|
}
|
|
45136
|
-
function
|
|
47219
|
+
function deviceOf2(app, id) {
|
|
45137
47220
|
const device = app.store.devices.get(Number(id ?? 3));
|
|
45138
47221
|
if (!device) throw fieldError("device_id", `There is no number ${id} in the sandbox: the example ones are 3 (official) and 4 (QR).`);
|
|
45139
47222
|
return device;
|
|
45140
47223
|
}
|
|
45141
47224
|
function emitted(result) {
|
|
45142
|
-
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
|
+
};
|
|
45143
47239
|
}
|
|
45144
47240
|
function state(app) {
|
|
45145
47241
|
const { store, config } = app;
|
|
45146
47242
|
return {
|
|
45147
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))),
|
|
45148
47247
|
counts: {
|
|
45149
47248
|
contacts: store.contacts.size,
|
|
45150
47249
|
messages: store.messages.size,
|
|
@@ -45192,7 +47291,7 @@ function limitOf2(query) {
|
|
|
45192
47291
|
}
|
|
45193
47292
|
function inboundMessage(app, req) {
|
|
45194
47293
|
const b = obj(req.body);
|
|
45195
|
-
const device =
|
|
47294
|
+
const device = deviceOf2(app, b.device_id);
|
|
45196
47295
|
let contact;
|
|
45197
47296
|
if (b.contact_id !== void 0) contact = contactOf(app, b.contact_id);
|
|
45198
47297
|
else {
|
|
@@ -45410,7 +47509,7 @@ var ROUTES = [
|
|
|
45410
47509
|
handle: (app, req) => {
|
|
45411
47510
|
const b = obj(req.body);
|
|
45412
47511
|
const contact = contactOf(app, b.contact_id);
|
|
45413
|
-
const device =
|
|
47512
|
+
const device = deviceOf2(app, b.device_id);
|
|
45414
47513
|
if (typeof b.open !== "boolean") throw fieldError("open", "open must be true or false.");
|
|
45415
47514
|
const key = `${contact.id}:${device.id}`;
|
|
45416
47515
|
if (b.open) app.store.lastInbound.set(key, /* @__PURE__ */ new Date());
|
|
@@ -45419,17 +47518,52 @@ var ROUTES = [
|
|
|
45419
47518
|
}
|
|
45420
47519
|
},
|
|
45421
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).
|
|
45422
47523
|
method: "POST",
|
|
45423
47524
|
pattern: /^\/_sandbox\/devices\/(\d+)\/(connect|disconnect)$/,
|
|
45424
|
-
handle: (app,
|
|
45425
|
-
const device =
|
|
45426
|
-
const
|
|
45427
|
-
if (
|
|
45428
|
-
|
|
45429
|
-
|
|
45430
|
-
|
|
45431
|
-
|
|
45432
|
-
|
|
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) };
|
|
45433
47567
|
}
|
|
45434
47568
|
},
|
|
45435
47569
|
{
|
|
@@ -45460,16 +47594,24 @@ async function handleSandbox(app, req) {
|
|
|
45460
47594
|
// src/server.ts
|
|
45461
47595
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
45462
47596
|
var API_KEY = /^Bearer\s+(\d+)\|fc_live_[A-Za-z0-9]+$/;
|
|
45463
|
-
var handlers = {
|
|
47597
|
+
var handlers = {
|
|
47598
|
+
...contactHandlers,
|
|
47599
|
+
...messageHandlers,
|
|
47600
|
+
...deviceHandlers,
|
|
47601
|
+
...connectLinkHandlers,
|
|
47602
|
+
...formHandlers,
|
|
47603
|
+
...gateHandlers,
|
|
47604
|
+
...webhookHandlers
|
|
47605
|
+
};
|
|
45464
47606
|
var BodyError = class extends ApiError {
|
|
45465
47607
|
};
|
|
45466
47608
|
async function readBody(req) {
|
|
45467
47609
|
const chunks = [];
|
|
45468
47610
|
let size = 0;
|
|
45469
|
-
for await (const
|
|
45470
|
-
size +=
|
|
47611
|
+
for await (const chunk2 of req) {
|
|
47612
|
+
size += chunk2.length;
|
|
45471
47613
|
if (size > MAX_BODY_BYTES) throw new BodyError(413, "payload_too_large", "El cuerpo supera 1 MB.");
|
|
45472
|
-
chunks.push(
|
|
47614
|
+
chunks.push(chunk2);
|
|
45473
47615
|
}
|
|
45474
47616
|
return Buffer.concat(chunks);
|
|
45475
47617
|
}
|
|
@@ -45547,7 +47689,7 @@ function createSandbox(options = {}) {
|
|
|
45547
47689
|
const validation = validateRequest(op.operation, match.params, parseQuery(url.searchParams), requestBody, requestBodySchema(op.operation));
|
|
45548
47690
|
if (!validation.ok) {
|
|
45549
47691
|
if (validation.kind === "not_found") throw notFound();
|
|
45550
|
-
throw validationFailed(validation.errors);
|
|
47692
|
+
throw op.operation["x-fc-validation-error"] === "invalid_request" ? invalidRequest(validation.errors) : validationFailed(validation.errors);
|
|
45551
47693
|
}
|
|
45552
47694
|
const handler = handlers[`${op.method.toUpperCase()} ${op.path}`];
|
|
45553
47695
|
const ctx = {
|
|
@@ -45595,6 +47737,25 @@ function createSandbox(options = {}) {
|
|
|
45595
47737
|
}
|
|
45596
47738
|
return;
|
|
45597
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
|
+
}
|
|
45598
47759
|
if (url.pathname === "/" && method === "GET") {
|
|
45599
47760
|
send2(res, requestId, {
|
|
45600
47761
|
status: 200,
|
|
@@ -45633,7 +47794,8 @@ function createSandbox(options = {}) {
|
|
|
45633
47794
|
server.off("error", reject);
|
|
45634
47795
|
const { port } = server.address();
|
|
45635
47796
|
const host = config.host === "0.0.0.0" || config.host === "::" ? "localhost" : config.host;
|
|
45636
|
-
|
|
47797
|
+
app.listenUrl = `http://${host}:${port}`;
|
|
47798
|
+
resolve2({ url: app.listenUrl, port });
|
|
45637
47799
|
});
|
|
45638
47800
|
}),
|
|
45639
47801
|
close: () => new Promise((resolve2) => {
|
|
@@ -45645,5 +47807,5 @@ function createSandbox(options = {}) {
|
|
|
45645
47807
|
}
|
|
45646
47808
|
|
|
45647
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 };
|
|
45648
|
-
//# sourceMappingURL=chunk-
|
|
45649
|
-
//# sourceMappingURL=chunk-
|
|
47810
|
+
//# sourceMappingURL=chunk-M5E6NAXM.js.map
|
|
47811
|
+
//# sourceMappingURL=chunk-M5E6NAXM.js.map
|