@volter/twin-twilio 0.1.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.
@@ -0,0 +1,414 @@
1
+ // Twilio capability manifest — the EXPECTED REAL-PRODUCT SURFACE (the target), authored top-down
2
+ // from what the Twilio API actually does — GROUNDED read-only (2026-07-09) against the published
3
+ // twilio-oai OpenAPI documents (github.com/twilio/twilio-oai: twilio_api_v2010.json,
4
+ // twilio_verify_v2.json, twilio_lookups_v2.json), the actually-installed `twilio@6.0.2` npm SDK
5
+ // source (fetched via `npm pack`), and the public Twilio error-code reference pages
6
+ // (twilio.com/docs/api/errors/<code>, fetched read-only) — see spec-sources.json — NOT from what
7
+ // this twin has built. This is the honest denominator: many entries start as `todo` and coverage
8
+ // reads LOW until the twin truly reaches 100% of the API. `verify()` (required to count as done)
9
+ // is ground truth and drives the KERNEL-BACKED handler on a FRESH temp root — never a spawned
10
+ // mock. `expected:'done'` only on capabilities we genuinely claim, so a broken one shows as a
11
+ // regression.
12
+ //
13
+ // v1 SLICE (honesty, per the build spec): Messages send + async delivery-status lifecycle
14
+ // (queued->sending->sent->delivered, kernel-folded poll-progression, no wall clock) + Verify
15
+ // start/check (twin-internal deterministic expected code — no real OTP is ever sent) + Lookup
16
+ // (pure deterministic derivation) + IncomingPhoneNumbers list (lazily-seeded) + Accounts fetch
17
+ // (pure per-root derivation) + X-Twilio-Signature-signed status-callback/inbound webhooks are
18
+ // modeled `done`. MMS media, Messaging Services/Copilot, TwiML/voice, Conversations,
19
+ // and sub-accounts are left `todo` — a deliberate v1 scope cut, not an oversight; see README
20
+ // `## Coverage`.
21
+ //
22
+ // THE DETERMINISTIC VERIFY CODE: Verify never sends a real OTP (no SMS/voice/email provider is
23
+ // contacted) — the "expected code" is a pure, deterministic function of (serviceSid, to) both
24
+ // this twin and a caller can independently re-derive. The lifecycle SEMANTICS (right code
25
+ // approves, wrong code stays pending) are faithful.
26
+ import { mkdtempSync, rmSync } from 'node:fs';
27
+ import { tmpdir } from 'node:os';
28
+ import { join } from 'node:path';
29
+ import { createHash } from 'node:crypto';
30
+ import { checkCapabilities, verifyBoundary, isInfrastructureError, harnessError } from '@volter/world-tooling';
31
+ import { handleTwilioTwinRequest } from "./twilio-twin.js";
32
+ import { checkTwilioConformance } from "./twilio-conformance.js";
33
+ import { syncTwilioFromReal } from "./twilio-connector.js";
34
+ import { verifyTwilioSignature, buildSignedStatusCallback, buildSignedInboundMessage, TwilioSignatureVerificationError, } from "./twilio-signature.js";
35
+ import { twilioAuthToken } from "./twilio-credentials.js";
36
+ // (Twilio is an API-first vendor for this pack's surface — docs/contributing/architecture.md C1b — so this pack
37
+ // ships no mirror; the Console is a config/analytics dashboard, not an agent navigation target —
38
+ // see README `## Coverage` and ui-scope.json needsUi:false.)
39
+ const OCCURRED_AT = '2026-01-01T00:00:00.000Z';
40
+ const ACCOUNT_SID = 'ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
41
+ // The pair the three PURE-CRYPTO webhook verifies sign and verify with. They assert the ALGORITHM
42
+ // — that a built delivery verifies and a tampered one does not — so they take an explicit,
43
+ // vendor-shaped pair rather than reaching for a world's: twilio-signature.ts holds no credential
44
+ // and reads no state, which is exactly why it is legitimately mutation-test ALLOW-listed. A
45
+ // world's own pair (`twilioAuthToken(root)`) is what `twilio.accounts.fetch` below asserts.
46
+ const SIGNING_PAIR = { accountSid: ACCOUNT_SID, authToken: 'b'.repeat(32) };
47
+ const SERVICE_SID = 'VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
48
+ /** Pure re-derivation of the twin's expected Verify code, SAME formula as twilio-twin.ts's
49
+ * (private, unexported) `expectedVerificationCode` — build spec §6: "Test recomputes
50
+ * sha256(serviceSid+':'+to)-derived 6-digit SAME as handler." Recomputed independently here
51
+ * (not imported) so this capability's teeth run through `handleTwilioTwinRequest` for the actual
52
+ * approve/pending assertion — only the EXPECTED VALUE is precomputed locally. */
53
+ function expectedCodeFor(serviceSid, to) {
54
+ const hex = createHash('sha256').update(`${serviceSid}:${to}`).digest('hex').slice(0, 6);
55
+ return String(parseInt(hex, 16) % 1_000_000).padStart(6, '0');
56
+ }
57
+ function encodeForm(b) {
58
+ if (b === undefined)
59
+ return undefined;
60
+ return new URLSearchParams(b).toString();
61
+ }
62
+ /** Run a sequence of real Twilio-twin requests against an isolated root; return all responses.
63
+ * The callback also receives `root` itself (some verifies build signed artifacts against the
64
+ * SAME root the handler used — the signing-key-match discipline fal-capabilities.ts's `withRoot`
65
+ * established). Threads `host` per step (api.twilio.com/verify.twilio.com/lookups.twilio.com). */
66
+ async function withRoot(steps) {
67
+ const root = mkdtempSync(join(tmpdir(), 'twilio-cap-'));
68
+ const h = (s) => handleTwilioTwinRequest({
69
+ method: s.m,
70
+ path: s.p,
71
+ body: encodeForm(s.b),
72
+ root,
73
+ occurredAt: OCCURRED_AT,
74
+ ...(s.host ? { host: s.host } : {}),
75
+ });
76
+ try {
77
+ return await verifyBoundary('twilio.withRoot', () => steps(h, root));
78
+ }
79
+ finally {
80
+ rmSync(root, { recursive: true, force: true });
81
+ }
82
+ }
83
+ const done = (id, area, title, dimension, tier, verify) => ({ id, area, title, dimension, tier, expected: 'done', verify });
84
+ const todo = (id, area, title, dimension, tier) => ({ id, area, title, dimension, tier, expected: 'todo' });
85
+ export const TWILIO_CAPABILITIES = [
86
+ // ── MESSAGES (send -> status progression -> get/list) ────────────────────────────────────────
87
+ done('twilio.messages.send', 'messages', "POST Messages.json (form-urlencoded) -> 201 (GROUNDED — twilio_api_v2010.json's own responses map documents ONLY 201 for message create) with sid/status/to/from echoed", 'api', 'core', () => withRoot(async (h) => {
88
+ const r = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'hi' }, host: 'api.twilio.com' });
89
+ if (r.status !== 201)
90
+ return false;
91
+ const b = r.body;
92
+ return typeof b.sid === 'string' && /^SM[0-9a-f]{32}$/.test(b.sid) && b.status === 'queued'
93
+ && b.to === '+15551234567' && b.from === '+15557654321' && b.body === 'hi';
94
+ })),
95
+ done('twilio.messages.send_requires_to', 'messages', "POST Messages.json without To -> 400 {code:21604} (GROUNDED live from twilio.com/docs/api/errors/21604)", 'api', 'core', () => withRoot(async (h) => {
96
+ const r = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { From: '+15557654321', Body: 'hi' }, host: 'api.twilio.com' });
97
+ const b = r.body;
98
+ return r.status === 400 && b.code === 21604;
99
+ })),
100
+ done('twilio.messages.send_invalid_to_e164', 'messages', "POST Messages.json with a non-E.164 To -> 400 {code:21211} (GROUNDED live from twilio.com/docs/api/errors/21211)", 'api', 'core', () => withRoot(async (h) => {
101
+ const r = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: 'not-a-number', From: '+15557654321', Body: 'hi' }, host: 'api.twilio.com' });
102
+ const b = r.body;
103
+ return r.status === 400 && b.code === 21211;
104
+ })),
105
+ done('twilio.messages.send_rejects_non_urlencoded', 'messages', 'a JSON request body (not form-urlencoded) -> 400 vendor-shaped, never silently misparsed', 'api', 'common', async () => {
106
+ const root = mkdtempSync(join(tmpdir(), 'twilio-cap-'));
107
+ try {
108
+ const r = await handleTwilioTwinRequest({ method: 'POST', path: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, body: JSON.stringify({ To: '+15551234567', From: '+15557654321', Body: 'hi' }), root, host: 'api.twilio.com', occurredAt: OCCURRED_AT });
109
+ const b = r.body;
110
+ return r.status === 400 && typeof b.code === 'number' && typeof b.message === 'string';
111
+ }
112
+ finally {
113
+ rmSync(root, { recursive: true, force: true });
114
+ }
115
+ }),
116
+ done('twilio.messages.get', 'messages', 'GET Messages/{Sid}.json round-trips the created message (sid/to/from/body match)', 'api', 'core', () => withRoot(async (h) => {
117
+ const c = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'roundtrip' }, host: 'api.twilio.com' });
118
+ const sid = c.body.sid;
119
+ const g = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sid}.json`, host: 'api.twilio.com' });
120
+ const b = g.body;
121
+ return g.status === 200 && b.sid === sid && b.to === '+15551234567' && b.body === 'roundtrip';
122
+ })),
123
+ done('twilio.messages.get_unknown_404', 'messages', "GET Messages/{unknown}.json -> 404 {code:20404} (GROUNDED live from twilio.com/docs/api/errors/20404)", 'api', 'common', () => withRoot(async (h) => {
124
+ const r = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/SMdoesnotexist00000000000000000.json`, host: 'api.twilio.com' });
125
+ const b = r.body;
126
+ return r.status === 404 && b.code === 20404;
127
+ })),
128
+ done('twilio.messages.list', 'messages', "GET Messages.json list envelope is EXACTLY {end,first_page_uri,next_page_uri,page,page_size,previous_page_uri,messages} (GROUNDED byte-for-byte from twilio_api_v2010.json's own list example, not the build spec's approximate guess)", 'api', 'core', () => withRoot(async (h) => {
129
+ await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'a' }, host: 'api.twilio.com' });
130
+ await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234568', From: '+15557654321', Body: 'b' }, host: 'api.twilio.com' });
131
+ const l = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, host: 'api.twilio.com' });
132
+ const b = l.body;
133
+ return l.status === 200 && Array.isArray(b.messages) && b.messages.length === 2
134
+ && typeof b.page === 'number' && typeof b.page_size === 'number' && 'first_page_uri' in b && 'next_page_uri' in b && 'previous_page_uri' in b;
135
+ })),
136
+ done('twilio.messages.status_lifecycle', 'messages', 'status polls progress EXACTLY queued(echo)->sending->sent->delivered — kernel-folded poll-count, no wall clock (dies under httpEmpty: no exact sequence to assert)', 'api', 'core', () => withRoot(async (h) => {
137
+ const c = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'x' }, host: 'api.twilio.com' });
138
+ const sid = c.body.sid;
139
+ const get = () => h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sid}.json`, host: 'api.twilio.com' });
140
+ const p1 = await get();
141
+ if (p1.body.status !== 'queued')
142
+ return false;
143
+ const p2 = await get();
144
+ if (p2.body.status !== 'sending')
145
+ return false;
146
+ const p3 = await get();
147
+ if (p3.body.status !== 'sent')
148
+ return false;
149
+ const p4 = await get();
150
+ if (p4.body.status !== 'delivered')
151
+ return false;
152
+ return true;
153
+ })),
154
+ done('twilio.messages.status_terminal_delivered', 'messages', 'delivered is terminal — repeated polls stay delivered, idempotent', 'api', 'common', () => withRoot(async (h) => {
155
+ const c = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'x' }, host: 'api.twilio.com' });
156
+ const sid = c.body.sid;
157
+ const get = () => h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sid}.json`, host: 'api.twilio.com' });
158
+ await get();
159
+ await get();
160
+ await get();
161
+ await get(); // -> delivered
162
+ const again1 = await get();
163
+ const again2 = await get();
164
+ return again1.body.status === 'delivered' && again2.body.status === 'delivered';
165
+ })),
166
+ done('twilio.messages.sid_format', 'messages', 'message sid matches the real Twilio SM + 32-hex-char shape exactly', 'api', 'common', () => withRoot(async (h) => {
167
+ const c = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'x' }, host: 'api.twilio.com' });
168
+ const sid = c.body.sid;
169
+ return typeof sid === 'string' && sid.length === 34 && /^SM[0-9a-f]{32}$/.test(sid);
170
+ })),
171
+ done('twilio.messages.isolation', 'messages', 'two independent messages: progressing one to delivered leaves the other at queued', 'api', 'common', () => withRoot(async (h) => {
172
+ const a = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'a' }, host: 'api.twilio.com' });
173
+ const b = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'b' }, host: 'api.twilio.com' });
174
+ const sidA = a.body.sid;
175
+ const sidB = b.body.sid;
176
+ const getA = () => h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sidA}.json`, host: 'api.twilio.com' });
177
+ await getA();
178
+ await getA();
179
+ await getA();
180
+ const finalA = await getA();
181
+ const getB = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sidB}.json`, host: 'api.twilio.com' });
182
+ return finalA.body.status === 'delivered' && getB.body.status === 'queued';
183
+ })),
184
+ done('twilio.messages.direction_outbound', 'messages', "every twin-created message has direction:'outbound-api' (the real enum member a twin-originated send uses — twilio_api_v2010.json message_enum_direction)", 'api', 'common', () => withRoot(async (h) => {
185
+ const c = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'x' }, host: 'api.twilio.com' });
186
+ return c.body.direction === 'outbound-api';
187
+ })),
188
+ done('twilio.messages.num_segments', 'messages', 'num_segments is a STRING (GROUNDED from the live OpenAPI list example) — "1" for a short body, >1 for a body over 160 chars', 'api', 'common', () => withRoot(async (h) => {
189
+ const short = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'short' }, host: 'api.twilio.com' });
190
+ const longBody = 'x'.repeat(200);
191
+ const long = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: longBody }, host: 'api.twilio.com' });
192
+ const shortSeg = short.body.num_segments;
193
+ const longSeg = long.body.num_segments;
194
+ return shortSeg === '1' && typeof longSeg === 'string' && Number(longSeg) > 1;
195
+ })),
196
+ // ── ACCOUNTS (pure per-root derivation — build spec's kernel-gotcha finding, see twilio-twin.ts header) ──
197
+ done('twilio.accounts.fetch', 'auth', "GET Accounts/{Sid}.json -> {sid,status:'active',auth_token,friendly_name,type} — auth_token matches the SAME deterministic per-root derivation twilio-signature.ts's signing uses (⚠ REST-home-for-signing_key modeling choice, not independently vendor-confirmed beyond the OpenAPI schema shape)", 'api', 'common', () => withRoot(async (h, root) => {
198
+ const r = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}.json`, host: 'api.twilio.com' });
199
+ const b = r.body;
200
+ return r.status === 200 && b.sid === ACCOUNT_SID && b.status === 'active' && typeof b.friendly_name === 'string'
201
+ && b.type === 'Full' && b.auth_token === await twilioAuthToken(root);
202
+ })),
203
+ // ── VERIFY (start -> check, twin-internal deterministic expected code) ───────────────────────
204
+ done('twilio.verify.start', 'verify', "POST Verifications (form-urlencoded) -> 201 (GROUNDED — twilio_verify_v2.json documents 201) with sid VE+32hex, status:'pending', valid:false", 'api', 'core', () => withRoot(async (h) => {
205
+ const r = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/Verifications`, b: { To: '+15551234567', Channel: 'sms' }, host: 'verify.twilio.com' });
206
+ const b = r.body;
207
+ return r.status === 201 && typeof b.sid === 'string' && /^VE[0-9a-f]{32}$/.test(b.sid) && b.status === 'pending' && b.valid === false;
208
+ })),
209
+ done('twilio.verify.start_requires_to', 'verify', "POST Verifications without To -> 400 {code:60200} (GROUNDED live from twilio.com/docs/api/errors/60200)", 'api', 'core', () => withRoot(async (h) => {
210
+ const r = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/Verifications`, b: { Channel: 'sms' }, host: 'verify.twilio.com' });
211
+ const b = r.body;
212
+ return r.status === 400 && b.code === 60200;
213
+ })),
214
+ done('twilio.verify.check_correct_approves', 'verify', 'VerificationCheck with the RIGHT (independently re-derived) code -> approved,valid:true — dies under httpEmpty (no approval to assert)', 'api', 'core', () => withRoot(async (h) => {
215
+ const to = '+15551234567';
216
+ await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/Verifications`, b: { To: to, Channel: 'sms' }, host: 'verify.twilio.com' });
217
+ const code = expectedCodeFor(SERVICE_SID, to);
218
+ const check = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/VerificationCheck`, b: { To: to, Code: code }, host: 'verify.twilio.com' });
219
+ const b = check.body;
220
+ return check.status === 200 && b.status === 'approved' && b.valid === true;
221
+ })),
222
+ done('twilio.verify.check_wrong_pending', 'verify', "VerificationCheck with a WRONG code -> {status:'pending',valid:false} (⚠ exact HTTP status modeled as 200, doc lists both 200/201 generically for this route — build spec §13.3)", 'api', 'core', () => withRoot(async (h) => {
223
+ const to = '+15551234568';
224
+ await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/Verifications`, b: { To: to, Channel: 'sms' }, host: 'verify.twilio.com' });
225
+ const check = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/VerificationCheck`, b: { To: to, Code: '000000' }, host: 'verify.twilio.com' });
226
+ const b = check.body;
227
+ return check.status === 200 && b.status === 'pending' && b.valid === false;
228
+ })),
229
+ done('twilio.verify.check_unknown_404', 'verify', "VerificationCheck for a To with no pending verification -> 404 {code:20404}", 'api', 'common', () => withRoot(async (h) => {
230
+ const r = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/VerificationCheck`, b: { To: '+19999999999', Code: '123456' }, host: 'verify.twilio.com' });
231
+ const b = r.body;
232
+ return r.status === 404 && b.code === 20404;
233
+ })),
234
+ // ── LOOKUP (pure deterministic derivation, no kernel row) ─────────────────────────────────────
235
+ done('twilio.lookup.phone_number', 'lookup', 'GET PhoneNumbers/{E164} -> {phone_number,national_format,country_code,valid:true}', 'api', 'core', () => withRoot(async (h) => {
236
+ const r = await h({ m: 'GET', p: '/v2/PhoneNumbers/+15551234567', host: 'lookups.twilio.com' });
237
+ const b = r.body;
238
+ return r.status === 200 && b.phone_number === '+15551234567' && typeof b.national_format === 'string' && b.country_code === 'US' && b.valid === true;
239
+ })),
240
+ done('twilio.lookup.line_type_intelligence', 'lookup', '?Fields=line_type_intelligence -> {line_type_intelligence:{type,carrier_name}} — type is NESTED (kernel type/id/updatedAt gotcha does not bite; Lookup has no kernel row at all)', 'api', 'common', () => withRoot(async (h) => {
241
+ const r = await h({ m: 'GET', p: '/v2/PhoneNumbers/+15551234567?Fields=line_type_intelligence', host: 'lookups.twilio.com' });
242
+ const b = r.body;
243
+ const lti = b.line_type_intelligence;
244
+ return r.status === 200 && lti && typeof lti.type === 'string' && typeof lti.carrier_name === 'string';
245
+ })),
246
+ done('twilio.lookup.invalid_404', 'lookup', "GET PhoneNumbers/{malformed} -> 404 {code:20404} (⚠ this pack's own generic not-found envelope; the real vendor's malformed-number-specific error taxonomy was not independently confirmed beyond the generic REST 404 shape)", 'api', 'common', () => withRoot(async (h) => {
247
+ const r = await h({ m: 'GET', p: '/v2/PhoneNumbers/not-a-real-number', host: 'lookups.twilio.com' });
248
+ const b = r.body;
249
+ return r.status === 404 && b.code === 20404;
250
+ })),
251
+ // ── INCOMING PHONE NUMBERS (lazily-seeded, kernel-persisted) ──────────────────────────────────
252
+ done('twilio.incoming_phone_numbers.list', 'phone_numbers', 'GET IncomingPhoneNumbers.json returns the lazily-seeded numbers, each with a `type` field (kernel gotcha: renamed number_type internally, remapped to type on read — see twilio-twin.ts header)', 'api', 'common', () => withRoot(async (h) => {
253
+ const r = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/IncomingPhoneNumbers.json`, host: 'api.twilio.com' });
254
+ const b = r.body;
255
+ if (r.status !== 200 || !Array.isArray(b.incoming_phone_numbers) || b.incoming_phone_numbers.length < 2)
256
+ return false;
257
+ const first = b.incoming_phone_numbers[0];
258
+ return typeof first.phone_number === 'string' && typeof first.type === 'string' && first.number_type === undefined;
259
+ })),
260
+ // ── WEBHOOKS (X-Twilio-Signature: FRESH HMAC-SHA1, sign/verify/tamper + inbound builder) ─────
261
+ done('twilio.webhooks.status_callback_signature', 'webhooks', 'a built status-callback delivery verifies against the SAME AuthToken (pure local HMAC-SHA1 in twilio-signature.ts — ALLOW crypto, never calls the handler and never reads state)', 'api', 'core', () => {
262
+ const url = 'https://example.com/status-callback';
263
+ const built = buildSignedStatusCallback({ url, messageSid: 'SMabc', messageStatus: 'delivered', to: '+15551234567', from: '+15557654321', ...SIGNING_PAIR });
264
+ try {
265
+ verifyTwilioSignature(url, built.params, built.headers['x-twilio-signature'], SIGNING_PAIR.authToken);
266
+ return true;
267
+ }
268
+ catch (err) {
269
+ if (isInfrastructureError(err))
270
+ throw harnessError('twilio.webhooks.status_callback_signature', err);
271
+ return false;
272
+ }
273
+ }),
274
+ done('twilio.webhooks.signature_tamper', 'webhooks', 'flipping one signed param (or the signature itself) is REJECTED by verifyTwilioSignature (pure crypto tamper-reject — ALLOW, never calls the handler)', 'api', 'core', () => {
275
+ const url = 'https://example.com/status-callback';
276
+ const built = buildSignedStatusCallback({ url, messageSid: 'SMabc', messageStatus: 'delivered', to: '+15551234567', from: '+15557654321', ...SIGNING_PAIR });
277
+ const tamperedParams = { ...built.params, To: '+19999999999' };
278
+ try {
279
+ verifyTwilioSignature(url, tamperedParams, built.headers['x-twilio-signature'], SIGNING_PAIR.authToken);
280
+ return false; // must have thrown
281
+ }
282
+ catch (err) {
283
+ return err instanceof TwilioSignatureVerificationError;
284
+ }
285
+ }),
286
+ done('twilio.webhooks.inbound_message_shape', 'webhooks', 'buildSignedInboundMessage produces a signed {MessageSid,From,To,Body} delivery whose signature verifies (pure builder — ALLOW, never calls the handler)', 'api', 'common', () => {
287
+ const url = 'https://example.com/sms-inbound';
288
+ const built = buildSignedInboundMessage({ url, from: '+15551234567', to: '+15557654321', body: 'inbound hi', ...SIGNING_PAIR });
289
+ if (built.params.From !== '+15551234567' || built.params.To !== '+15557654321' || built.params.Body !== 'inbound hi' || typeof built.params.MessageSid !== 'string')
290
+ return false;
291
+ try {
292
+ verifyTwilioSignature(url, built.params, built.headers['x-twilio-signature'], SIGNING_PAIR.authToken);
293
+ return true;
294
+ }
295
+ catch (err) {
296
+ if (isInfrastructureError(err))
297
+ throw harnessError('twilio.webhooks.inbound_message_shape', err);
298
+ return false;
299
+ }
300
+ }),
301
+ // ── ERRORS ────────────────────────────────────────────────────────────────────────────────
302
+ done('twilio.errors.envelope_shape', 'errors', "every error response is EXACTLY {code,message,more_info,status} (Twilio's own documented envelope — NOT fal's {detail})", 'api', 'core', () => withRoot(async (h) => {
303
+ const r = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { From: '+15557654321', Body: 'x' }, host: 'api.twilio.com' });
304
+ const b = r.body;
305
+ const keys = Object.keys(b).sort();
306
+ return JSON.stringify(keys) === JSON.stringify(['code', 'message', 'more_info', 'status']) && b.status === 400;
307
+ })),
308
+ // ── SAFETY / HONESTY ─────────────────────────────────────────────────────────────────────
309
+ done('twilio.read_only.rejects_writes', 'safety', 'readOnly mode rejects writes with 405 but allows GET', 'api', 'common', async () => {
310
+ const root = mkdtempSync(join(tmpdir(), 'twilio-ro-'));
311
+ try {
312
+ const w = await handleTwilioTwinRequest({ method: 'POST', path: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, body: 'To=%2B15551234567&From=%2B15557654321&Body=x', root, readOnly: true, host: 'api.twilio.com', occurredAt: OCCURRED_AT });
313
+ const g = await handleTwilioTwinRequest({ method: 'GET', path: `/2010-04-01/Accounts/${ACCOUNT_SID}.json`, root, readOnly: true, host: 'api.twilio.com', occurredAt: OCCURRED_AT });
314
+ return w.status === 405 && g.status === 200;
315
+ }
316
+ finally {
317
+ rmSync(root, { recursive: true, force: true });
318
+ }
319
+ }),
320
+ done('twilio.unmodeled_route.404', 'safety', "an unmodeled route returns Twilio's own {code,message,more_info,status} 404 envelope (never a fabricated success)", 'api', 'common', () => withRoot(async (h) => {
321
+ const r = await h({ m: 'GET', p: '/some/totally/unknown/route', host: 'api.twilio.com' });
322
+ const b = r.body;
323
+ return r.status === 404 && typeof b.code === 'number' && typeof b.message === 'string';
324
+ })),
325
+ done('twilio.state.kernel_persisted', 'state', 'state is kernel-folded — a message created in one call is visible (by sid) in the next', 'api', 'core', () => withRoot(async (h) => {
326
+ const c = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'x' }, host: 'api.twilio.com' });
327
+ const sid = c.body.sid;
328
+ if (typeof sid !== 'string' || sid.length === 0)
329
+ return false;
330
+ const g = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sid}.json`, host: 'api.twilio.com' });
331
+ return g.status === 200 && g.body.sid === sid;
332
+ })),
333
+ // ── CONFORMANCE / CONNECTOR ──────────────────────────────────────────────────────────────
334
+ done('twilio.conformance.snapshot', 'conformance', 'conformance snapshot covers all 4 resource types + the core messages/verify/lookup contracts (ALLOW static — reads twilioTwinSnapshot(), never calls the handler)', 'api', 'common', () => {
335
+ const report = checkTwilioConformance();
336
+ return report.ok && report.endpointsChecked >= 10 && report.resourceTypesChecked === 4;
337
+ }),
338
+ done('twilio.connector.sync.entrypoint', 'connector', 'syncTwilioFromReal LIST-pulls real Messages (client.messages.list() — a genuine vendor list endpoint, unlike fal); idempotent re-pull; read-through-handler', 'connector', 'core', async () => {
339
+ const root = mkdtempSync(join(tmpdir(), 'twilio-conn-'));
340
+ try {
341
+ const client = {
342
+ messages: {
343
+ list: async () => [{ sid: 'SM' + '1'.repeat(32), to: '+15551234567', from: '+15557654321', body: 'real pulled message', status: 'delivered', direction: 'outbound-api', numSegments: '1', numMedia: '0' }],
344
+ },
345
+ };
346
+ const first = await syncTwilioFromReal(client, { root, occurredAt: OCCURRED_AT, budgetOptions: { root } });
347
+ if (first.observed !== 1 || first.deltasAppended < 1)
348
+ return false;
349
+ const second = await syncTwilioFromReal(client, { root, occurredAt: OCCURRED_AT, budgetOptions: { root } });
350
+ if (second.deltasAppended !== 0)
351
+ return false;
352
+ const r = await handleTwilioTwinRequest({ method: 'GET', path: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/SM${'1'.repeat(32)}.json`, root, host: 'api.twilio.com', occurredAt: OCCURRED_AT });
353
+ return r.status === 200 && r.body.body === 'real pulled message';
354
+ }
355
+ finally {
356
+ rmSync(root, { recursive: true, force: true });
357
+ }
358
+ }),
359
+ done('twilio.connector.empty_client', 'connector', 'syncTwilioFromReal with no client.messages observes nothing', 'connector', 'common', async () => {
360
+ const root = mkdtempSync(join(tmpdir(), 'twilio-conn-empty-'));
361
+ try {
362
+ const result = await syncTwilioFromReal({}, { root, occurredAt: OCCURRED_AT, budgetOptions: { root } });
363
+ return result.observed === 0 && result.deltasAppended === 0;
364
+ }
365
+ finally {
366
+ rmSync(root, { recursive: true, force: true });
367
+ }
368
+ }),
369
+ // ── TODO: the rest of the Twilio v1 surface (honest denominator) ────────────────────────────
370
+ todo('twilio.messages.mms_media', 'messages', 'MMS MediaUrl attachments (send + Media subresource fetch)', 'api', 'common'),
371
+ todo('twilio.messages.messaging_service_sid', 'messages', 'sending via a MessagingServiceSid (sender-pool routing) instead of an explicit From', 'api', 'common'),
372
+ todo('twilio.messages.scheduled_send', 'messages', 'SendAt / schedule_type:fixed scheduled sends + the scheduled status', 'api', 'niche'),
373
+ todo('twilio.messages.delete', 'messages', 'DELETE Messages/{Sid}.json', 'api', 'common'),
374
+ todo('twilio.messages.redact', 'messages', 'POST Messages/{Sid}.json Body="" redaction', 'api', 'niche'),
375
+ todo('twilio.messages.pagination_cursor', 'pagination', 'real cursor-based pagination (PageToken / next_page_uri actually fetchable) beyond a single-page list', 'api', 'common'),
376
+ todo('twilio.messages.status_callback_delivery', 'messages', 'an injected offline deliverer actually POSTs the signed status-callback to the registered StatusCallback URL on each status transition (⚠ delivery timing/retry policy unconfirmed)', 'api', 'common'),
377
+ todo('twilio.messaging.services', 'messaging', 'Messaging Services (sender pools, sticky sender) CRUD', 'api', 'common'),
378
+ todo('twilio.messaging.copilot', 'messaging', 'Messaging Copilot / Advanced Opt-Out features', 'api', 'niche'),
379
+ todo('twilio.messaging.short_codes', 'messaging', 'short code provisioning/management', 'api', 'niche'),
380
+ todo('twilio.messaging.senders', 'messaging', 'sender (phone number / short code / alphanumeric) pool management', 'api', 'niche'),
381
+ todo('twilio.voice.calls', 'voice', 'Calls resource (voice call create/status)', 'api', 'common'),
382
+ todo('twilio.voice.twiml', 'voice', 'TwiML document generation/execution', 'api', 'common'),
383
+ todo('twilio.conversations.create', 'messaging', 'Conversations API (multi-channel conversation threads)', 'api', 'niche'),
384
+ todo('twilio.verify.channel_email', 'verify', "Verify email channel (Channel:'email')", 'api', 'common'),
385
+ todo('twilio.verify.channel_whatsapp', 'verify', "Verify WhatsApp channel (Channel:'whatsapp')", 'api', 'common'),
386
+ todo('twilio.verify.channel_call', 'verify', "Verify voice-call channel (Channel:'call')", 'api', 'niche'),
387
+ todo('twilio.verify.max_attempts', 'verify', "the real max-check-attempts 404 {code:60202} path (⚠ GROUNDED message text 'Max check attempts reached' from twilio.com/docs/api/errors/60202, but the attempt-count trigger itself is unmodeled)", 'api', 'common'),
388
+ todo('twilio.verify.rate_limit', 'verify', 'Verify RateLimits resource / 429 rate-limiting', 'api', 'niche'),
389
+ todo('twilio.verify.fraud_guard', 'verify', 'Verify Fraud Guard / RiskCheck', 'api', 'niche'),
390
+ todo('twilio.lookup.carrier_live', 'lookup', 'a LIVE (non-deterministic-derivation) carrier lookup against a real carrier database', 'api', 'niche'),
391
+ todo('twilio.lookup.caller_name', 'lookup', 'CNAM caller-name lookup (?Fields=caller_name)', 'api', 'common'),
392
+ todo('twilio.lookup.sim_swap', 'lookup', 'SIM swap detection (?Fields=sim_swap)', 'api', 'niche'),
393
+ todo('twilio.lookup.reassigned_number', 'lookup', 'reassigned number detection (?Fields=reassigned_number)', 'api', 'niche'),
394
+ todo('twilio.incoming_phone_numbers.provision', 'phone_numbers', 'POST IncomingPhoneNumbers.json (real provisioning of a new number)', 'api', 'common'),
395
+ todo('twilio.incoming_phone_numbers.update', 'phone_numbers', 'POST IncomingPhoneNumbers/{Sid}.json (update SmsUrl/VoiceUrl/etc.)', 'api', 'common'),
396
+ todo('twilio.accounts.subaccounts', 'auth', 'sub-account creation/management', 'api', 'niche'),
397
+ todo('twilio.usage.records', 'auth', 'Usage Records (billing/usage-tracking resource)', 'api', 'niche'),
398
+ todo('twilio.regulatory.bundles', 'phone_numbers', 'Regulatory Compliance Bundles (number provisioning compliance)', 'api', 'niche'),
399
+ todo('twilio.errors.code_catalog', 'errors', 'the full Twilio error-code catalog (this pack grounds 6 codes live; ~thousands exist)', 'api', 'niche'),
400
+ todo('twilio.webhooks.status_callback_retry', 'webhooks', 'status-callback delivery retry policy on a non-2xx response', 'api', 'niche'),
401
+ todo('twilio.webhooks.fallback_url', 'webhooks', 'SmsFallbackUrl / StatusCallback fallback-on-error routing', 'api', 'niche'),
402
+ todo('twilio.auth.basic_401_parity', 'auth', 'missing/invalid HTTP Basic (AccountSid:AuthToken) -> 401 parity (⚠ GROUNDED message text "Permission Denied", code 20003, from twilio.com/docs/api/errors/20003 — not yet enforced by the handler)', 'api', 'common'),
403
+ todo('twilio.auth.api_key_auth', 'auth', 'API Key/Secret (SK.../secret) auth as an alternative to AccountSid/AuthToken', 'api', 'niche'),
404
+ todo('twilio.connector.push', 'connector', 'push a locally-created message/verification against a real Twilio account', 'connector', 'common'),
405
+ todo('twilio.connector.pull_verifications', 'connector', 'pull verifications from a real account (⚠ Verify v2 has NO bulk list-verifications endpoint per the live-fetched OpenAPI document — would need caller-supplied handles, mirroring fal\'s handle-driven pull; mapVerification exists and is ready)', 'connector', 'common'),
406
+ todo('twilio.fixtures.seed', 'connector', 'seed a small library of realistic message/verification fixtures beyond the 2 lazily-seeded phone numbers', 'connector', 'niche'),
407
+ ];
408
+ export const TWILIO_AREAS = [
409
+ 'messages', 'verify', 'lookup', 'phone_numbers', 'messaging', 'voice', 'webhooks', 'auth',
410
+ 'pagination', 'errors', 'connector', 'conformance', 'safety', 'state',
411
+ ];
412
+ export async function twilioCapabilities() {
413
+ return checkCapabilities('twilio', TWILIO_CAPABILITIES);
414
+ }
@@ -0,0 +1,7 @@
1
+ export type TwilioConformanceReport = {
2
+ ok: boolean;
3
+ endpointsChecked: number;
4
+ resourceTypesChecked: number;
5
+ violations: string[];
6
+ };
7
+ export declare function checkTwilioConformance(): TwilioConformanceReport;
@@ -0,0 +1,41 @@
1
+ // Twilio conformance (dev-only; lazy-imported by the CLI, NEVER from index.ts/runtime — E2).
2
+ // Checks the twin's endpoint/resource inventory against itself — SELF-REFERENTIAL, the same
3
+ // pattern fal/replicate/elevenlabs/polar use (docs/contributing/conformance.md's "2 spec" discussion): it does not
4
+ // diff against a captured vendor document, it asserts the pack's own declared snapshot is
5
+ // internally consistent (every resource type has a route, the core surface routes exist).
6
+ import { twilioTwinSnapshot, TWILIO_RESOURCE_TYPES } from "./twilio-twin.js";
7
+ export function checkTwilioConformance() {
8
+ const snapshot = twilioTwinSnapshot();
9
+ const violations = [];
10
+ // Every declared resource type must have at least one implemented endpoint touching it.
11
+ // `signing_key` has no REST path of its own — it's served (purely derived, per-root) at the
12
+ // Accounts fetch endpoint, so it maps to the 'Accounts' stem instead of its own name (build
13
+ // spec §4).
14
+ const stemFor = {
15
+ message: '/Messages',
16
+ verification: '/Verifications',
17
+ phone_number: '/IncomingPhoneNumbers',
18
+ signing_key: '/Accounts',
19
+ };
20
+ for (const type of TWILIO_RESOURCE_TYPES) {
21
+ const stem = stemFor[type];
22
+ if (!stem || !snapshot.implementedEndpoints.some((e) => e.includes(stem))) {
23
+ violations.push(`resource type '${type}' has no implemented endpoint`);
24
+ }
25
+ }
26
+ // The core messaging/verify/lookup endpoints must be present in the protocol surface.
27
+ if (!snapshot.implementedEndpoints.some((e) => e.startsWith('POST api.twilio.com') && e.includes('Messages.json')))
28
+ violations.push('missing message send endpoint');
29
+ if (!snapshot.implementedEndpoints.some((e) => e.startsWith('POST verify.twilio.com') && e.includes('Verifications')))
30
+ violations.push('missing verify start endpoint');
31
+ if (!snapshot.implementedEndpoints.some((e) => e.includes('VerificationCheck')))
32
+ violations.push('missing verify check endpoint');
33
+ if (!snapshot.implementedEndpoints.some((e) => e.startsWith('GET lookups.twilio.com')))
34
+ violations.push('missing lookup endpoint');
35
+ return {
36
+ ok: violations.length === 0,
37
+ endpointsChecked: snapshot.implementedEndpoints.length,
38
+ resourceTypesChecked: snapshot.resourceTypes.length,
39
+ violations,
40
+ };
41
+ }
@@ -0,0 +1,60 @@
1
+ import type { SyncResource } from '@volter/world-core';
2
+ import { type TwilioBudgetedOptions } from './twilio-budget.js';
3
+ export type { TwilioBudgetedOptions };
4
+ export type TwilioRealMessage = {
5
+ sid: string;
6
+ to: string;
7
+ from: string;
8
+ body: string;
9
+ status: string;
10
+ direction?: string;
11
+ numSegments?: string;
12
+ numMedia?: string;
13
+ dateCreated?: string | null;
14
+ dateSent?: string | null;
15
+ errorCode?: number | null;
16
+ errorMessage?: string | null;
17
+ accountSid?: string | null;
18
+ };
19
+ export type TwilioRealVerification = {
20
+ sid: string;
21
+ serviceSid: string;
22
+ to: string;
23
+ channel: string;
24
+ status: string;
25
+ valid: boolean;
26
+ dateCreated?: string | null;
27
+ dateUpdated?: string | null;
28
+ };
29
+ export interface TwilioLikeClient {
30
+ messages?: {
31
+ list: (opts?: {
32
+ limit?: number;
33
+ }) => Promise<TwilioRealMessage[]>;
34
+ };
35
+ }
36
+ /** Pure mapper (real Message -> SyncResource) — never touches a client, so the mutation-test
37
+ * connector-seam sweep (which sabotages every export matching the sync-or-push-or-pull-or-
38
+ * fullSync naming convention) leaves this real, per the pack convention (replicate-connector.ts's
39
+ * `mapModel` / fal-connector.ts's `mapQueueRequest`). */
40
+ export declare function mapMessage(m: TwilioRealMessage): SyncResource;
41
+ /** Pure mapper (real Verification -> SyncResource). Not wired into `syncTwilioFromReal` today —
42
+ * see header (`twilio.connector.pull_verifications` is `todo`; Verify v2 has no list-all
43
+ * endpoint to pull from without caller-supplied handles). */
44
+ export declare function mapVerification(v: TwilioRealVerification): SyncResource;
45
+ /** Pull real Messages via the injected client's `messages.list()` (list-driven — a genuine
46
+ * Twilio REST list endpoint, unlike fal's handle-only queue). An absent `client.messages`
47
+ * observes nothing. */
48
+ export declare function pullTwilioMessages(rawClient: TwilioLikeClient, opts?: TwilioBudgetedOptions): Promise<SyncResource[]>;
49
+ /**
50
+ * D7 entry point: pull the CURRENT real Messages list and fold it into the twin via ONE
51
+ * `syncPull` (shadow-diff dedup). Returns `{observed, deltasAppended}` — a re-pull of identical
52
+ * state appends ZERO deltas.
53
+ */
54
+ export declare function syncTwilioFromReal(rawClient: TwilioLikeClient, opts?: {
55
+ root?: string;
56
+ occurredAt?: string;
57
+ } & TwilioBudgetedOptions): Promise<{
58
+ observed: number;
59
+ deltasAppended: number;
60
+ }>;