@crvouga/mockingbird-service-twilio 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -4
- package/README.md +2 -6
- package/dist/{chunk-DC2JLNZ4.js → chunk-JIDPDWLD.js} +3 -2
- package/dist/chunk-JIDPDWLD.js.map +7 -0
- package/dist/{chunk-PNGEMWUB.js → chunk-KBYCDOZD.js} +137 -14
- package/dist/chunk-KBYCDOZD.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/server.d.ts +11 -9
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-DC2JLNZ4.js.map +0 -7
- package/dist/chunk-PNGEMWUB.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-twilio
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.6.1 (2026-09-30)
|
|
4
4
|
|
|
5
5
|
### Fixes and improvements
|
|
6
6
|
|
|
7
|
-
- keep
|
|
7
|
+
- keep admin buttons pressed until the action finishes ([0aabfcb](https://github.com/crvouga/mockingbird/commit/0aabfcb5f392aa51e193f9c3f817d5877d92bc6a))
|
|
8
|
+
- size admin select fields like the text inputs beside them ([0fb3ef2](https://github.com/crvouga/mockingbird/commit/0fb3ef2ddbf43376c93a2ae6d87c6c9bf890dafc))
|
|
9
|
+
- keep admin section tabs wired when boot is renamed ([4eda25e](https://github.com/crvouga/mockingbird/commit/4eda25eacd59da048d196f7cc8369d7a02b47bfc))
|
|
10
|
+
|
|
11
|
+
## 1.6.0 (2026-09-29)
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- boot Bolt over Socket Mode ([01322df](https://github.com/crvouga/mockingbird/commit/01322df315c66b1f2f5b53094cbbeeeef6d3ae6c))
|
|
16
|
+
- model both sides of an SMS connection drop ([46e674a](https://github.com/crvouga/mockingbird/commit/46e674a4ea4c41fdbdae2c743b0ac662cab8fb83))
|
|
8
17
|
|
|
9
|
-
###
|
|
18
|
+
### Fixes and improvements
|
|
10
19
|
|
|
11
|
-
-
|
|
20
|
+
- narrow the stored message sid before comparing it ([b5955a8](https://github.com/crvouga/mockingbird/commit/b5955a81ff22b8dc878331bc56cbb4fa24201d69))
|
|
21
|
+
|
|
22
|
+
## 1.5.1 (2026-09-29)
|
|
23
|
+
|
|
24
|
+
### Fixes and improvements
|
|
25
|
+
|
|
26
|
+
- keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
|
|
12
27
|
|
|
13
28
|
## 1.5.0 (2026-09-29)
|
|
14
29
|
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ const { code, sid, status } = (await latest.json()) as { code: string; sid: stri
|
|
|
90
90
|
| `POST /verify/v2/Services/{VA}/Verifications` | `To` (valid E.164, or an email for `Channel=email`), `Channel`, optional `CustomCode`. Creates a `VE…` verification with a 6-digit code (or `fixedCode`). Sending again to the same pending verification keeps its sid and code and adds a `send_code_attempts` entry. The 6th send to one number within 10 minutes is 429 `60203`. A bad `To` is 400 `60200`. An unknown service sid (not `VA` + 32 hex) is 404 `20404`. |
|
|
91
91
|
| `POST /verify/v2/Services/{VA}/VerificationCheck` | `Code` plus `VerificationSid` or `To`. The right code returns 200 `approved`, `valid: true`. A wrong code returns 200 `pending` and counts an attempt. The 6th check is 429 `60202`. A missing, approved, canceled or expired (10 minutes on the mock clock) verification is 404 `20404`. |
|
|
92
92
|
| `GET` / `POST /verify/v2/Services/{VA}/Verifications/{VE}` | Fetches a verification, or sets `Status=canceled\|approved` on one. |
|
|
93
|
-
| `POST /api/2010-04-01/Accounts/{AC}/Messages.json` | `To`, `Body` or `MediaUrl`, and `From` or `MessagingServiceSid`. Returns 201 with an `SM…` sid (`MM…` with media)
|
|
93
|
+
| `POST /api/2010-04-01/Accounts/{AC}/Messages.json` | `To`, `Body` or `MediaUrl`, and `From` or `MessagingServiceSid`. Returns 201 with an `SM…` sid (`MM…` with media) and RFC 2822 dates. `MessagingServiceSid` alone is status `accepted`, `from: null`, `num_segments: "0"` (a sender is not chosen yet). `From` alone is status `queued`. Both are kept and the status is `queued` (Twilio queues on a specific sender). There is no idempotency key: the same body creates another sid. Errors: 21604 (no To), 21602 (no Body), 21603 (no From), 21211 (invalid To), 21617 (over 1600 characters). The message is recorded in the outbox. |
|
|
94
94
|
| `GET /api/2010-04-01/Accounts/{AC}/Messages/{SM}.json` | Reads a message back. |
|
|
95
95
|
| `GET /api/2010-04-01/Accounts/{AC}/Recordings/{RE}.wav` | The recording as a RIFF WAV. With `RequestedChannels=2` you get both channels. Otherwise a dual-channel recording is mixed down to mono, as Twilio does. |
|
|
96
96
|
| `GET` / `DELETE /api/2010-04-01/Accounts/{AC}/Recordings/{RE}.json` | Metadata, or a delete (204; a second delete is 404, which our consumer tolerates). |
|
|
@@ -132,10 +132,7 @@ filters are `sms.inbound` and `voice.{twiml,disclosure,status,recording}`.
|
|
|
132
132
|
| `GET /__admin/outbox?to=&since=&kind=sms\|verify` | Everything "sent": SMS (`{to, from, messagingServiceSid, body, sid}`) and every Verify code delivery (`{code, body: "Your verification code is: …"}`). |
|
|
133
133
|
| `GET /__admin/messages` | Message resources in the namespace. |
|
|
134
134
|
|
|
135
|
-
Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`; list them with `GET /__admin/faults/presets`):
|
|
136
|
-
`verify_5xx` (start and check answer 500 `20500`), `sms_socket_drop` (Messages.json drops the
|
|
137
|
-
connection, an unknown outcome), `sms_4xx` (400 `21211`), `lookup_5xx` (503 `20503`; the EMR
|
|
138
|
-
fails open), `webhook_duplicate`, `webhook_drop`.
|
|
135
|
+
Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n, "latencyMs"?: n}`; list them with `GET /__admin/faults/presets`, and read `hits` / `remaining` from `GET /__admin/faults`). A preset applies to the calling namespace. `verify_5xx` (start and check answer 500 `20500`), `sms_drop_before_accept` (Messages.json drops the socket before a Message exists; still registered as `sms_socket_drop`), `sms_accepted_then_socket_drop` (the Message and outbox entry are stored, then the socket closes before any response bytes), `sms_4xx` (400 `21211`), `sms_429` (429 `20429` with `Retry-After: 1`), `sms_5xx` (500 `20500`, no `Retry-After`), `sms_missing_sid` (201 whose body omits `sid`; the outbox still has the message), `lookup_5xx` (503 `20503`; the EMR fails open), `webhook_duplicate`, `webhook_drop`. A 503 on Messages.json is the same envelope with code `20503` and `Retry-After` (a standard fault, not a separate preset). None of the status faults store a message.
|
|
139
136
|
|
|
140
137
|
### Namespaces
|
|
141
138
|
|
|
@@ -153,7 +150,6 @@ can also come from the `x-mockingbird-namespace` header or a `/ns/<name>` prefix
|
|
|
153
150
|
- Verify channels other than SMS are accepted and recorded, but nothing is delivered. Verify
|
|
154
151
|
Service configuration (code length, friendly name, rate-limit buckets) is global, set with
|
|
155
152
|
`PUT /__admin/verify`.
|
|
156
|
-
- "Sent, then the socket dropped": `sms_socket_drop` drops before the message is recorded.
|
|
157
153
|
- Accounts: any `AC…` sid is its own account, and the path's `{AccountSid}` is not
|
|
158
154
|
cross-checked against the credential.
|
|
159
155
|
- libphonenumber-js drops "local only" lengths from its metadata. The mock restores them for
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRuntime
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KBYCDOZD.js";
|
|
4
4
|
|
|
5
5
|
// ../../adapters/node/dist/cli.js
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
@@ -235,6 +235,7 @@ var start = async (target, values, config) => {
|
|
|
235
235
|
onLog: formatLog(config.log)
|
|
236
236
|
});
|
|
237
237
|
const listening = await listen(runtime, { port: config.port, host: config.host });
|
|
238
|
+
target.listening?.(listening);
|
|
238
239
|
console.log(`${target.name} mock listening on ${listening.url}`);
|
|
239
240
|
console.log(`${target.name} health: GET ${listening.url}/health`);
|
|
240
241
|
console.log(`${target.name} admin: ${listening.url}/__admin (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
|
|
@@ -388,4 +389,4 @@ export {
|
|
|
388
389
|
createServer2 as createServer,
|
|
389
390
|
serveTarget
|
|
390
391
|
};
|
|
391
|
-
//# sourceMappingURL=chunk-
|
|
392
|
+
//# sourceMappingURL=chunk-JIDPDWLD.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../adapters/node/src/cli.ts", "../../../adapters/node/src/serve.ts", "../../../adapters/node/src/listen.ts", "../src/server.ts"],
|
|
4
|
+
"sourcesContent": ["import { readFile } from \"node:fs/promises\"\nimport { type ParseArgsConfig, parseArgs } from \"node:util\"\nimport type { RequestLog, ServiceInstance, ServiceRuntime } from \"@crvouga/mockingbird-service\"\nimport { type Listening, listen } from \"./listen.js\"\n\nexport type CliOption = {\n type: \"string\" | \"boolean\"\n description: string\n /** Shown in help; the value placeholder, e.g. `<port>`. */\n value?: string\n default?: string | boolean\n}\n\nexport type CliValues = Record<string, string | boolean | undefined>\n\nexport type CliCommand = {\n summary: string\n usage?: string\n options?: Record<string, CliOption>\n /** Resolves to an exit code; a server command resolves only when it stops. */\n run(values: CliValues, positionals: string[]): Promise<number>\n}\n\nexport type CliSpec = {\n bin: string\n description: string\n commands: Record<string, CliCommand>\n}\n\nconst optionHelp = (options: Record<string, CliOption>): string[] =>\n Object.entries(options).map(([name, option]) => {\n const flag = `--${name}${option.type === \"string\" ? ` ${option.value ?? \"<value>\"}` : \"\"}`\n const fallback = option.default !== undefined ? ` (default: ${String(option.default)})` : \"\"\n return ` ${flag.padEnd(30)} ${option.description}${fallback}`\n })\n\nconst help = (spec: CliSpec): string =>\n [\n `${spec.bin} \u2014 ${spec.description}`,\n \"\",\n \"Usage:\",\n ` ${spec.bin} <command> [options]`,\n \"\",\n \"Commands:\",\n ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),\n \"\",\n `Run \\`${spec.bin} <command> --help\\` for a command's options.`,\n ].join(\"\\n\")\n\nconst commandHelp = (spec: CliSpec, name: string, command: CliCommand): string =>\n [\n `${spec.bin} ${name} \u2014 ${command.summary}`,\n \"\",\n \"Usage:\",\n ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,\n ...(command.options ? [\"\", \"Options:\", ...optionHelp(command.options)] : []),\n ].join(\"\\n\")\n\n/** Parse `argv` against `spec` and run the chosen command. Resolves to an exit code. */\nexport const runCli = async (spec: CliSpec, argv: string[]): Promise<number> => {\n // Two-word commands (`corpus pull`) win over one-word ones.\n const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : undefined\n const words = pair !== undefined && spec.commands[pair] ? 2 : 1\n const name = words === 2 ? pair : argv[0]\n const rest = argv.slice(words)\n if (name === undefined || name === \"--help\" || name === \"-h\" || name === \"help\") {\n console.log(help(spec))\n return 0\n }\n const command = spec.commands[name]\n if (!command) {\n console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}\\n\\n${help(spec)}`)\n return 2\n }\n if (rest.includes(\"--help\") || rest.includes(\"-h\")) {\n console.log(commandHelp(spec, name, command))\n return 0\n }\n let parsed: { values: CliValues; positionals: string[] }\n try {\n parsed = parseArgs({\n args: rest,\n allowPositionals: true,\n strict: true,\n options: Object.fromEntries(\n Object.entries(command.options ?? {}).map(([key, option]) => [\n key,\n {\n type: option.type,\n ...(option.default !== undefined ? { default: option.default } : {}),\n },\n ]),\n ) as ParseArgsConfig[\"options\"],\n }) as { values: CliValues; positionals: string[] }\n } catch (error) {\n console.error(\n `${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}\\n\\n${commandHelp(spec, name, command)}`,\n )\n return 2\n }\n return command.run(parsed.values, parsed.positionals)\n}\n\n// \u2500\u2500 serve \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type LogFormat = \"pretty\" | \"json\" | \"off\"\n\nexport type CommonServeOptions = {\n adminKey: string | undefined\n seed: string | undefined\n onLog: ((entry: RequestLog) => void) | undefined\n}\n\n/**\n * What a service contributes to `serve`: how to build its runtime from CLI flags,\n * and what to say at startup. Every service's `./server` entry exports one as\n * `serveTarget`, which is also how `serve --config` finds services by name.\n */\nexport type ServeTarget = {\n name: string\n defaultPort: number\n /** Serve flags beyond the common ones. */\n options?: Record<string, CliOption>\n create(\n values: CliValues,\n common: CommonServeOptions,\n ): Promise<ServiceRuntime<ServiceInstance>> | ServiceRuntime<ServiceInstance>\n /** Startup lines after the listen address, e.g. the loaded corpus version. */\n banner?(runtime: ServiceRuntime<ServiceInstance>): string[]\n /** After the HTTP server is bound. A service that speaks more than HTTP attaches it here. */\n listening?(server: Listening): void\n}\n\nconst COMMON_SERVE_OPTIONS: Record<string, CliOption> = {\n port: { type: \"string\", value: \"<port>\", description: \"Port to listen on\" },\n host: { type: \"string\", value: \"<host>\", description: \"Interface to bind\", default: \"127.0.0.1\" },\n \"admin-key\": {\n type: \"string\",\n value: \"<key>\",\n description: \"Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)\",\n },\n seed: { type: \"string\", value: \"<seed>\", description: \"Seed for every random choice\" },\n log: {\n type: \"string\",\n value: \"<pretty|json|off>\",\n description: \"Request log format\",\n default: \"pretty\",\n },\n \"log-requests\": {\n type: \"boolean\",\n description:\n \"One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json\",\n },\n config: {\n type: \"string\",\n value: \"<file>\",\n description: \"Serve every service in a mockingbird.json config instead\",\n },\n}\n\nconst formatLog = (format: LogFormat) => {\n if (format === \"off\") return undefined\n if (format === \"json\") return (entry: RequestLog) => console.log(JSON.stringify(entry))\n return (entry: RequestLog) => {\n const op = entry.operationId ?? (entry.unmatched ? \"UNMATCHED\" : \"-\")\n const ns = entry.namespace === \"default\" ? \"\" : ` [${entry.namespace}]`\n const fault = entry.faultId ? ` fault=${entry.faultId}` : \"\"\n const adopted = entry.adopted ? \" adopted\" : \"\"\n // A rejection says what arrived (never the body) and why the service refused it.\n const received = entry.request\n ? ` received=${entry.request.bodyBytes === null ? \"?\" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? \"-\"}${\n entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : \"\"\n }`\n : \"\"\n const issues = entry.issues?.length\n ? ` issues=${entry.issues\n .slice(0, 3)\n .map((issue) => `${issue.path || \"$\"}: ${issue.message}`)\n .join(\"; \")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : \"\"}`\n : \"\"\n console.log(\n `${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`,\n )\n }\n}\n\nconst asString = (value: string | boolean | undefined): string | undefined =>\n typeof value === \"string\" ? value : undefined\n\n/** A service entry in `mockingbird.json`. */\nexport type ConfigService = {\n port?: number\n host?: string\n adminKey?: string\n seed?: string\n /** Service-specific serve flags, by long name: `{ \"webhook-url\": \"\u2026\" }`. */\n options?: Record<string, string | boolean>\n}\n\nexport type MockingbirdConfig = {\n /** Keyed by service name: `junction` loads `@crvouga/mockingbird-service-junction`. */\n services: Record<string, ConfigService>\n log?: LogFormat\n}\n\nconst loadTarget = async (name: string, own: ServeTarget): Promise<ServeTarget> => {\n if (name === own.name) return own\n const specifier = `@crvouga/mockingbird-service-${name}/server`\n try {\n const mod = (await import(specifier)) as { serveTarget?: ServeTarget }\n if (!mod.serveTarget) throw new Error(`${specifier} exports no serveTarget`)\n return mod.serveTarget\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new Error(\n `cannot load service \"${name}\": ${reason}. Install @crvouga/mockingbird-service-${name}.`,\n )\n }\n}\n\nconst start = async (\n target: ServeTarget,\n values: CliValues,\n config: { port: number; host: string; adminKey?: string; seed?: string; log: LogFormat },\n): Promise<Listening> => {\n const runtime = await target.create(values, {\n adminKey: config.adminKey,\n seed: config.seed,\n onLog: formatLog(config.log),\n })\n const listening = await listen(runtime, { port: config.port, host: config.host })\n target.listening?.(listening)\n console.log(`${target.name} mock listening on ${listening.url}`)\n console.log(`${target.name} health: GET ${listening.url}/health`)\n console.log(\n `${target.name} admin: ${listening.url}/__admin (${config.adminKey ? \"x-mockingbird-admin-key required\" : \"open \u2014 pass --admin-key to lock\"})`,\n )\n console.log(`${target.name} admin ui: ${listening.url}/__admin/ui`)\n for (const line of target.banner?.(runtime) ?? []) console.log(`${target.name} ${line}`)\n return listening\n}\n\nconst untilSignal = async (servers: Listening[]): Promise<number> =>\n new Promise((resolve) => {\n const stop = async () => {\n await Promise.allSettled(servers.map((s) => s.close()))\n resolve(0)\n }\n process.once(\"SIGINT\", stop)\n process.once(\"SIGTERM\", stop)\n })\n\n/** The standard `serve` command for a service, including multi-service `--config`. */\nexport const serveCommand = (target: ServeTarget): CliCommand => ({\n summary: `Serve the ${target.name} mock over HTTP`,\n options: { ...COMMON_SERVE_OPTIONS, ...target.options },\n async run(values) {\n const log = (\n values[\"log-requests\"] === true ? \"json\" : (asString(values.log) ?? \"pretty\")\n ) as LogFormat\n if (![\"pretty\", \"json\", \"off\"].includes(log)) {\n console.error(`--log must be pretty, json or off (got ${log})`)\n return 2\n }\n const configPath = asString(values.config)\n if (configPath !== undefined) {\n const config = JSON.parse(await readFile(configPath, \"utf8\")) as MockingbirdConfig\n const servers: Listening[] = []\n try {\n for (const [name, entry] of Object.entries(config.services ?? {})) {\n const each = await loadTarget(name, target)\n servers.push(\n await start(each, entry.options ?? {}, {\n port: entry.port ?? each.defaultPort,\n host: entry.host ?? \"127.0.0.1\",\n ...(entry.adminKey !== undefined ? { adminKey: entry.adminKey } : {}),\n ...(entry.seed !== undefined ? { seed: entry.seed } : {}),\n log: config.log ?? log,\n }),\n )\n }\n } catch (error) {\n await Promise.allSettled(servers.map((s) => s.close()))\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal(servers)\n }\n const port = asString(values.port)\n const adminKey = asString(values[\"admin-key\"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY\n const seed = asString(values.seed)\n let listening: Listening\n try {\n listening = await start(target, values, {\n port: port === undefined ? target.defaultPort : Number.parseInt(port, 10),\n host: asString(values.host) ?? \"127.0.0.1\",\n ...(adminKey !== undefined ? { adminKey } : {}),\n ...(seed !== undefined ? { seed } : {}),\n log,\n })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal([listening])\n },\n})\n", "import { createServer } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\n\n/** Options for {@link serve}. */\nexport type NodeServeOptions = {\n port?: number\n host?: string\n}\n\n/**\n * Serve any Mockingbird {@link FetchAPI} over `node:http`.\n * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).\n */\nexport const serve = async (api: FetchAPI, options: NodeServeOptions = {}) => {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk))\n }\n const body = Buffer.concat(chunks)\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : undefined\n const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`\n const raw = req.url ?? \"/\"\n const url = new URL(raw.replace(/^\\/+/, \"/\"), base)\n const method = req.method ?? \"GET\"\n const init: RequestInit = { method, headers: req.headers as Record<string, string> }\n if (method !== \"GET\" && method !== \"HEAD\" && body.length > 0) {\n init.body = body\n }\n const aborted = new AbortController()\n res.once(\"close\", () => {\n if (!res.writableFinished) aborted.abort()\n })\n init.signal = aborted.signal\n const request = new Request(url, init)\n let response: Response\n try {\n response = await api.fetch(request)\n } catch (error) {\n // A `drop` fault: destroy the socket so the client sees the connection die.\n if ((error as { code?: string }).code === \"MOCKINGBIRD_DROP\") {\n req.socket.destroy()\n return\n }\n res.writeHead(500, { \"content-type\": \"application/json\" })\n res.end(\n JSON.stringify({\n error: {\n type: \"mockingbird_internal\",\n message: error instanceof Error ? error.message : String(error),\n },\n }),\n )\n return\n }\n // Headers#entries() joins repeated headers; Set-Cookie must stay one header per cookie.\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers)\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers[\"set-cookie\"] = cookies\n if (!response.body) {\n res.writeHead(response.status, headers)\n res.end()\n return\n }\n // Stream the body chunk by chunk: event streams and long-polls must not be buffered.\n res.writeHead(response.status, headers)\n res.flushHeaders()\n const reader = response.body.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await new Promise<void>((resolve) => res.once(\"drain\", resolve))\n }\n res.end()\n } catch {\n res.destroy()\n } finally {\n reader.releaseLock()\n }\n })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host, resolve)\n })\n return server\n}\n", "import type { Server } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\nimport { serve } from \"./serve.js\"\n\nexport type ListenOptions = {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`: a mock should not be reachable off the machine by accident. */\n host?: string\n}\n\n/** A running server, with the address it actually bound. */\nexport type Listening = {\n url: string\n port: number\n host: string\n server: Server\n close(): Promise<void>\n}\n\nexport const listen = async (api: FetchAPI, options: ListenOptions = {}): Promise<Listening> => {\n const host = options.host ?? \"127.0.0.1\"\n const server = await serve(api, { port: options.port ?? 0, host })\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : (options.port ?? 0)\n const shown = host.includes(\":\") ? `[${host}]` : host\n return {\n url: `http://${shown}:${port}`,\n port,\n host,\n server,\n close: () =>\n new Promise<void>((resolve, reject) => {\n // Stop accepting first, then drop keep-alive sockets so close() can finish.\n server.close((error) => {\n if (error && (error as { code?: string }).code !== \"ERR_SERVER_NOT_RUNNING\") reject(error)\n else resolve()\n })\n server.closeAllConnections?.()\n }),\n }\n}\n", "/// <reference types=\"node\" />\nimport { type Listening, listen, type ServeTarget } from \"@crvouga/mockingbird-adapter-node\"\nimport { createRuntime, type TwilioRuntime, type TwilioRuntimeOptions } from \"./runtime.js\"\n\n/** Port `mockingbird-twilio serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8798\n\nexport type TwilioServerOptions = TwilioRuntimeOptions & {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`. */\n host?: string\n}\n\nexport type TwilioServer = Listening & { runtime: TwilioRuntime }\n\n/** Serve the Twilio mock over `node:http`. */\nexport const createServer = async (options: TwilioServerOptions = {}): Promise<TwilioServer> => {\n const { port, host, ...rest } = options\n const runtime = createRuntime(rest)\n const listening = await listen(runtime, {\n port: port ?? 0,\n ...(host !== undefined ? { host } : {}),\n })\n return { ...listening, runtime }\n}\n\nconst text = (value: string | boolean | undefined) =>\n typeof value === \"string\" ? value : undefined\n\n/** How `serve` (and `serve --config`) builds the Twilio mock from flags. */\nexport const serveTarget: ServeTarget = {\n name: \"twilio\",\n defaultPort: DEFAULT_PORT,\n options: {\n \"app-url\": {\n type: \"string\",\n value: \"<url>\",\n description:\n \"Post inbound SMS and voice webhooks to this app origin (e.g. http://127.0.0.1:3000)\",\n },\n \"public-base-url\": {\n type: \"string\",\n value: \"<url>\",\n description: \"Sign webhooks against this base (the app's TWILIO_VOICE_WEBHOOK_BASE_URL)\",\n },\n \"account-sid\": {\n type: \"string\",\n value: \"<AC\u2026>\",\n description: \"The app's TWILIO_ACCOUNT_SID (AccountSid in webhooks)\",\n },\n \"auth-token\": {\n type: \"string\",\n value: \"<token>\",\n description:\n \"The app's TWILIO_AUTH_TOKEN: signs webhooks, and with --account-sid is the only accepted token\",\n },\n \"caller-id\": {\n type: \"string\",\n value: \"<+1\u2026>\",\n description: \"To of inbound SMS (the app's TWILIO_VOICE_CALLER_ID)\",\n },\n \"fixed-code\": {\n type: \"string\",\n value: \"<digits>\",\n description: \"Every Verify code is this (default: random 6 digits, read via /__admin)\",\n },\n },\n create: (values, common) => {\n const appUrl = text(values[\"app-url\"])\n const accountSid = text(values[\"account-sid\"])\n const authToken = text(values[\"auth-token\"])\n const publicBaseUrl = text(values[\"public-base-url\"])\n const callerId = text(values[\"caller-id\"])\n const fixedCode = text(values[\"fixed-code\"])\n if (appUrl && !authToken) throw new Error(\"--app-url needs --auth-token (the signing key)\")\n return createRuntime({\n ...(appUrl && authToken\n ? {\n app: {\n url: appUrl,\n authToken,\n ...(publicBaseUrl ? { publicBaseUrl } : {}),\n ...(accountSid ? { accountSid } : {}),\n ...(callerId ? { callerId } : {}),\n },\n }\n : {}),\n ...(accountSid && authToken ? { accounts: { [accountSid]: authToken } } : {}),\n ...(fixedCode ? { verify: { fixedCode } } : {}),\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(common.onLog ? { onLog: common.onLog } : {}),\n })\n },\n banner: () => [\n \"routes: /lookups/v2/\u2026, /verify/v2/\u2026, /api/2010-04-01/\u2026 (the Twilio host as a path prefix)\",\n \"auth: Basic AccountSid:AuthToken; OTP: GET /__admin/verify/<e164>/latest\",\n \"namespaces: x-mockingbird-namespace, /ns/<name>/\u2026, or PUT /__admin/credentials {<AccountSid>: <ns>}\",\n ],\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["createServer"]
|
|
7
|
+
}
|
|
@@ -2192,6 +2192,50 @@ function bootAdmin(config) {
|
|
|
2192
2192
|
if (render)
|
|
2193
2193
|
await render();
|
|
2194
2194
|
}
|
|
2195
|
+
const delay = (ms) => new Promise((resolve) => {
|
|
2196
|
+
window.setTimeout(resolve, ms);
|
|
2197
|
+
});
|
|
2198
|
+
const confirmAction = async (button, pending, done, work) => {
|
|
2199
|
+
if (button.dataset.pending === "1")
|
|
2200
|
+
return;
|
|
2201
|
+
const label = button.textContent ?? "";
|
|
2202
|
+
const width = button.offsetWidth;
|
|
2203
|
+
button.dataset.pending = "1";
|
|
2204
|
+
button.classList.add("pressed");
|
|
2205
|
+
button.setAttribute("aria-busy", "true");
|
|
2206
|
+
button.style.minWidth = `${width}px`;
|
|
2207
|
+
button.textContent = pending;
|
|
2208
|
+
try {
|
|
2209
|
+
await work();
|
|
2210
|
+
button.textContent = done;
|
|
2211
|
+
await delay(1e3);
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
showError(error);
|
|
2214
|
+
} finally {
|
|
2215
|
+
button.textContent = label;
|
|
2216
|
+
button.style.minWidth = "";
|
|
2217
|
+
button.classList.remove("pressed");
|
|
2218
|
+
button.removeAttribute("aria-busy");
|
|
2219
|
+
delete button.dataset.pending;
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
document.addEventListener("click", (event) => {
|
|
2223
|
+
const target = event.target;
|
|
2224
|
+
if (!(target instanceof Element))
|
|
2225
|
+
return;
|
|
2226
|
+
const button = target.closest("button");
|
|
2227
|
+
if (!(button instanceof HTMLButtonElement) || button.dataset.pending === "1")
|
|
2228
|
+
return;
|
|
2229
|
+
button.classList.add("pressed");
|
|
2230
|
+
const token = String(Date.now());
|
|
2231
|
+
button.dataset.press = token;
|
|
2232
|
+
window.setTimeout(() => {
|
|
2233
|
+
if (button.dataset.press !== token || button.dataset.pending === "1")
|
|
2234
|
+
return;
|
|
2235
|
+
button.classList.remove("pressed");
|
|
2236
|
+
delete button.dataset.press;
|
|
2237
|
+
}, 450);
|
|
2238
|
+
});
|
|
2195
2239
|
el("namespace", HTMLSelectElement).addEventListener("change", () => {
|
|
2196
2240
|
state.namespace = el("namespace", HTMLSelectElement).value;
|
|
2197
2241
|
refresh().catch(showError);
|
|
@@ -2202,7 +2246,7 @@ function bootAdmin(config) {
|
|
|
2202
2246
|
refresh().catch(showError);
|
|
2203
2247
|
});
|
|
2204
2248
|
el("refresh", HTMLButtonElement).addEventListener("click", () => {
|
|
2205
|
-
refresh().catch(showError);
|
|
2249
|
+
confirmAction(el("refresh", HTMLButtonElement), "Refreshing...", "Updated", () => refresh()).catch(showError);
|
|
2206
2250
|
});
|
|
2207
2251
|
el("collections", HTMLElement).addEventListener("click", (event) => {
|
|
2208
2252
|
const target = event.target;
|
|
@@ -2325,10 +2369,10 @@ function bootAdmin(config) {
|
|
|
2325
2369
|
}).then(() => renderFaults()).catch(showError);
|
|
2326
2370
|
});
|
|
2327
2371
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2328
|
-
api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
|
|
2372
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2329
2373
|
});
|
|
2330
2374
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2331
|
-
api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
|
|
2375
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2332
2376
|
});
|
|
2333
2377
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2334
2378
|
const target = event.target;
|
|
@@ -2443,9 +2487,13 @@ function bootAdmin(config) {
|
|
|
2443
2487
|
var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
|
|
2444
2488
|
var adminClientSource = (config) => {
|
|
2445
2489
|
const payload = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
2490
|
+
const entry = bootAdmin.name;
|
|
2491
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(entry)) {
|
|
2492
|
+
throw new Error("admin client cannot call its boot function");
|
|
2493
|
+
}
|
|
2446
2494
|
return `<script>
|
|
2447
2495
|
${clientPrelude}
|
|
2448
|
-
|
|
2496
|
+
${entry}(${payload});
|
|
2449
2497
|
</script>`;
|
|
2450
2498
|
};
|
|
2451
2499
|
|
|
@@ -2659,9 +2707,24 @@ var renderAdminDocument = (service) => {
|
|
|
2659
2707
|
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
2660
2708
|
label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
|
|
2661
2709
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2662
|
-
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
|
|
2663
|
-
padding: 7px 8px; min-height: 36px;
|
|
2664
|
-
}
|
|
2710
|
+
background-color: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
|
|
2711
|
+
padding: 7px 8px; min-height: 36px; line-height: 1.2;
|
|
2712
|
+
}
|
|
2713
|
+
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2714
|
+
the text fields in the same form. Paint it like those fields. */
|
|
2715
|
+
label.field select {
|
|
2716
|
+
-webkit-appearance: none; appearance: none;
|
|
2717
|
+
padding-right: 28px;
|
|
2718
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2.5 4.5 6 8l3.5-3.5'/%3E%3C/svg%3E");
|
|
2719
|
+
background-repeat: no-repeat; background-position: right 10px center; background-size: 12px 12px;
|
|
2720
|
+
}
|
|
2721
|
+
.stack > .field > input,
|
|
2722
|
+
.stack > .field > select,
|
|
2723
|
+
.stack > .field > textarea,
|
|
2724
|
+
dialog .field > input,
|
|
2725
|
+
dialog .field > select,
|
|
2726
|
+
dialog .field > textarea { width: 100%; }
|
|
2727
|
+
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2665
2728
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2666
2729
|
border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
|
|
2667
2730
|
background: var(--bg-elev);
|
|
@@ -2669,6 +2732,14 @@ var renderAdminDocument = (service) => {
|
|
|
2669
2732
|
.btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
|
|
2670
2733
|
.btn-quiet { background: transparent; }
|
|
2671
2734
|
.btn-danger { color: var(--err); }
|
|
2735
|
+
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2736
|
+
so the press itself has to stay visible. */
|
|
2737
|
+
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
2738
|
+
.icon-btn.pressed, .btn.pressed, .btn-quiet.pressed, .btn-danger.pressed, nav.side button.pressed {
|
|
2739
|
+
background: var(--bg-inset); border-color: var(--line-strong); color: var(--ink);
|
|
2740
|
+
}
|
|
2741
|
+
.btn-danger:active, .btn-danger.pressed { background: var(--err-soft); color: var(--err); }
|
|
2742
|
+
.btn-primary:active, .btn-primary.pressed { filter: brightness(0.88); color: var(--accent-ink); }
|
|
2672
2743
|
/* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
|
|
2673
2744
|
record or the full nav row would widen the page past the viewport. */
|
|
2674
2745
|
.layout {
|
|
@@ -2757,8 +2828,16 @@ var renderAdminDocument = (service) => {
|
|
|
2757
2828
|
input, select, textarea { font-size: 16px; }
|
|
2758
2829
|
.brand, .vendor, .controls { width: 100%; }
|
|
2759
2830
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
2760
|
-
|
|
2831
|
+
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2832
|
+
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2833
|
+
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
2834
|
+
.layout {
|
|
2835
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2836
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
2837
|
+
align-content: start;
|
|
2838
|
+
}
|
|
2761
2839
|
nav.side {
|
|
2840
|
+
align-self: start; height: auto;
|
|
2762
2841
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2763
2842
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2764
2843
|
padding: 8px;
|
|
@@ -3552,7 +3631,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3552
3631
|
};
|
|
3553
3632
|
|
|
3554
3633
|
// ../core/dist/version.js
|
|
3555
|
-
var PACKAGE_VERSION = true ? "1.
|
|
3634
|
+
var PACKAGE_VERSION = true ? "1.6.1" : UNRELEASED_VERSION;
|
|
3556
3635
|
|
|
3557
3636
|
// ../core/dist/signing.js
|
|
3558
3637
|
var encoder = new TextEncoder();
|
|
@@ -4017,6 +4096,8 @@ var reuseSorted = (fresh, previous, compare, equal) => {
|
|
|
4017
4096
|
}
|
|
4018
4097
|
return unchanged ? previous : result;
|
|
4019
4098
|
};
|
|
4099
|
+
var faultEffects = (request) => (effects.get(request) ?? []).filter((e) => e !== void 0);
|
|
4100
|
+
var faultEffect = (request, name) => faultEffects(request).find((e) => e.name === name)?.params;
|
|
4020
4101
|
var DroppedConnectionError = class extends TypeError {
|
|
4021
4102
|
code = "MOCKINGBIRD_DROP";
|
|
4022
4103
|
constructor() {
|
|
@@ -4938,11 +5019,19 @@ var TWILIO_PRESETS = {
|
|
|
4938
5019
|
]
|
|
4939
5020
|
},
|
|
4940
5021
|
sms_socket_drop: {
|
|
4941
|
-
description: "Messages.json drops the connection
|
|
5022
|
+
description: "Messages.json drops the connection before a Message resource exists (alias of sms_drop_before_accept)",
|
|
5023
|
+
rules: [{ operationId: "CreateMessage", drop: true }]
|
|
5024
|
+
},
|
|
5025
|
+
sms_drop_before_accept: {
|
|
5026
|
+
description: "Messages.json drops the connection before a Message resource exists: an unknown outcome, safe to retry",
|
|
4942
5027
|
rules: [{ operationId: "CreateMessage", drop: true }]
|
|
4943
5028
|
},
|
|
5029
|
+
sms_accepted_then_socket_drop: {
|
|
5030
|
+
description: "Messages.json stores the Message, then drops the connection before any response bytes: an ambiguous outcome",
|
|
5031
|
+
rules: [{ operationId: "CreateMessage", effect: "accepted_then_socket_drop" }]
|
|
5032
|
+
},
|
|
4944
5033
|
sms_4xx: {
|
|
4945
|
-
description: "Messages.json answers 400 21211 (invalid To): a definite failure",
|
|
5034
|
+
description: "Messages.json answers 400 21211 (invalid To): a definite failure, nothing is stored",
|
|
4946
5035
|
rules: [
|
|
4947
5036
|
{
|
|
4948
5037
|
operationId: "CreateMessage",
|
|
@@ -4952,6 +5041,32 @@ var TWILIO_PRESETS = {
|
|
|
4952
5041
|
}
|
|
4953
5042
|
]
|
|
4954
5043
|
},
|
|
5044
|
+
sms_429: {
|
|
5045
|
+
description: "Messages.json answers 429 20429 with Retry-After: a definite failure, nothing is stored",
|
|
5046
|
+
rules: [
|
|
5047
|
+
{
|
|
5048
|
+
operationId: "CreateMessage",
|
|
5049
|
+
status: 429,
|
|
5050
|
+
body: errorBody(429, 20429, "Too many requests"),
|
|
5051
|
+
headers: { "x-twilio-error-code": "20429", "retry-after": "1" }
|
|
5052
|
+
}
|
|
5053
|
+
]
|
|
5054
|
+
},
|
|
5055
|
+
sms_5xx: {
|
|
5056
|
+
description: "Messages.json answers 500 20500: a definite failure, nothing is stored. 500 has no Retry-After",
|
|
5057
|
+
rules: [
|
|
5058
|
+
{
|
|
5059
|
+
operationId: "CreateMessage",
|
|
5060
|
+
status: 500,
|
|
5061
|
+
body: errorBody(500, 20500, "An internal server error has occurred"),
|
|
5062
|
+
headers: { "x-twilio-error-code": "20500" }
|
|
5063
|
+
}
|
|
5064
|
+
]
|
|
5065
|
+
},
|
|
5066
|
+
sms_missing_sid: {
|
|
5067
|
+
description: "Messages.json accepts and stores the Message, but the 201 body omits sid",
|
|
5068
|
+
rules: [{ operationId: "CreateMessage", effect: "missing_sid" }]
|
|
5069
|
+
},
|
|
4955
5070
|
lookup_5xx: {
|
|
4956
5071
|
description: "Lookup answers 503 (20503); the EMR phone validation fails open",
|
|
4957
5072
|
rules: [
|
|
@@ -5684,6 +5799,7 @@ var TwilioAPI = class {
|
|
|
5684
5799
|
const now = this.now();
|
|
5685
5800
|
const sid = this.state.sid(media.length > 0 ? "MM" : "SM");
|
|
5686
5801
|
const base = `/2010-04-01/Accounts/${accountSid}/Messages/${sid}`;
|
|
5802
|
+
const selectingSender = Boolean(messagingServiceSid) && !from;
|
|
5687
5803
|
const message = {
|
|
5688
5804
|
sid,
|
|
5689
5805
|
account_sid: accountSid,
|
|
@@ -5692,9 +5808,9 @@ var TwilioAPI = class {
|
|
|
5692
5808
|
to: recipient.phone_number,
|
|
5693
5809
|
from: from ?? null,
|
|
5694
5810
|
messaging_service_sid: messagingServiceSid ?? null,
|
|
5695
|
-
status:
|
|
5811
|
+
status: selectingSender ? "accepted" : "queued",
|
|
5696
5812
|
direction: "outbound-api",
|
|
5697
|
-
num_segments: String(segments(body)),
|
|
5813
|
+
num_segments: selectingSender ? "0" : String(segments(body)),
|
|
5698
5814
|
num_media: String(media.length),
|
|
5699
5815
|
price: null,
|
|
5700
5816
|
price_unit: "USD",
|
|
@@ -5719,6 +5835,13 @@ var TwilioAPI = class {
|
|
|
5719
5835
|
...media.length > 0 ? { mediaUrls: media } : {},
|
|
5720
5836
|
createdAt: new Date(now).toISOString()
|
|
5721
5837
|
});
|
|
5838
|
+
if (faultEffect(context.request, "accepted_then_socket_drop") !== void 0) {
|
|
5839
|
+
throw new DroppedConnectionError();
|
|
5840
|
+
}
|
|
5841
|
+
if (faultEffect(context.request, "missing_sid") !== void 0) {
|
|
5842
|
+
const { sid: messageSid, ...withoutSid } = message;
|
|
5843
|
+
return annotateResponse(jsonRes(201, withoutSid), { ids: { messageSid } });
|
|
5844
|
+
}
|
|
5722
5845
|
return annotateResponse(jsonRes(201, message), { ids: { messageSid: sid } });
|
|
5723
5846
|
}
|
|
5724
5847
|
fetchMessage(context) {
|
|
@@ -5862,4 +5985,4 @@ export {
|
|
|
5862
5985
|
twilioError,
|
|
5863
5986
|
TwilioAPI
|
|
5864
5987
|
};
|
|
5865
|
-
//# sourceMappingURL=chunk-
|
|
5988
|
+
//# sourceMappingURL=chunk-KBYCDOZD.js.map
|