@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,436 @@
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, type CapabilityReport, type CapabilitySpec, verifyBoundary, isInfrastructureError, harnessError } from '@volter/world-tooling';
31
+ import { handleTwilioTwinRequest, type TwilioResponse } from './twilio-twin.ts';
32
+ import { checkTwilioConformance } from './twilio-conformance.ts';
33
+ import { syncTwilioFromReal, type TwilioLikeClient } from './twilio-connector.ts';
34
+ import {
35
+ verifyTwilioSignature,
36
+ buildSignedStatusCallback,
37
+ buildSignedInboundMessage,
38
+ TwilioSignatureVerificationError,
39
+ } from './twilio-signature.ts';
40
+ import { twilioAuthToken } from './twilio-credentials.ts';
41
+
42
+ // (Twilio is an API-first vendor for this pack's surface — docs/contributing/architecture.md C1b — so this pack
43
+ // ships no mirror; the Console is a config/analytics dashboard, not an agent navigation target —
44
+ // see README `## Coverage` and ui-scope.json needsUi:false.)
45
+
46
+ const OCCURRED_AT = '2026-01-01T00:00:00.000Z';
47
+ const ACCOUNT_SID = 'ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
48
+ // The pair the three PURE-CRYPTO webhook verifies sign and verify with. They assert the ALGORITHM
49
+ // — that a built delivery verifies and a tampered one does not — so they take an explicit,
50
+ // vendor-shaped pair rather than reaching for a world's: twilio-signature.ts holds no credential
51
+ // and reads no state, which is exactly why it is legitimately mutation-test ALLOW-listed. A
52
+ // world's own pair (`twilioAuthToken(root)`) is what `twilio.accounts.fetch` below asserts.
53
+ const SIGNING_PAIR = { accountSid: ACCOUNT_SID, authToken: 'b'.repeat(32) };
54
+ const SERVICE_SID = 'VAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
55
+
56
+ /** Pure re-derivation of the twin's expected Verify code, SAME formula as twilio-twin.ts's
57
+ * (private, unexported) `expectedVerificationCode` — build spec §6: "Test recomputes
58
+ * sha256(serviceSid+':'+to)-derived 6-digit SAME as handler." Recomputed independently here
59
+ * (not imported) so this capability's teeth run through `handleTwilioTwinRequest` for the actual
60
+ * approve/pending assertion — only the EXPECTED VALUE is precomputed locally. */
61
+ function expectedCodeFor(serviceSid: string, to: string): string {
62
+ const hex = createHash('sha256').update(`${serviceSid}:${to}`).digest('hex').slice(0, 6);
63
+ return String(parseInt(hex, 16) % 1_000_000).padStart(6, '0');
64
+ }
65
+
66
+ // ── API verify: drive REAL requests against a fresh temp root, then assert status/shape ──
67
+ type Step = { m: string; p: string; b?: Record<string, string>; host?: string };
68
+ type Body = Record<string, any>;
69
+
70
+ function encodeForm(b?: Record<string, string>): string | undefined {
71
+ if (b === undefined) return undefined;
72
+ return new URLSearchParams(b).toString();
73
+ }
74
+
75
+ /** Run a sequence of real Twilio-twin requests against an isolated root; return all responses.
76
+ * The callback also receives `root` itself (some verifies build signed artifacts against the
77
+ * SAME root the handler used — the signing-key-match discipline fal-capabilities.ts's `withRoot`
78
+ * established). Threads `host` per step (api.twilio.com/verify.twilio.com/lookups.twilio.com). */
79
+ async function withRoot(steps: (h: (s: Step) => Promise<TwilioResponse>, root: string) => Promise<boolean>): Promise<boolean> {
80
+ const root = mkdtempSync(join(tmpdir(), 'twilio-cap-'));
81
+ const h = (s: Step) =>
82
+ handleTwilioTwinRequest({
83
+ method: s.m,
84
+ path: s.p,
85
+ body: encodeForm(s.b),
86
+ root,
87
+ occurredAt: OCCURRED_AT,
88
+ ...(s.host ? { host: s.host } : {}),
89
+ });
90
+ try {
91
+ return await verifyBoundary('twilio.withRoot', () => steps(h, root));
92
+ } finally {
93
+ rmSync(root, { recursive: true, force: true });
94
+ }
95
+ }
96
+
97
+ const done = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], verify: CapabilitySpec['verify']): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'done', verify });
98
+ const todo = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier']): CapabilitySpec => ({ id, area, title, dimension, tier, expected: 'todo' });
99
+
100
+ export const TWILIO_CAPABILITIES: CapabilitySpec[] = [
101
+ // ── MESSAGES (send -> status progression -> get/list) ────────────────────────────────────────
102
+ 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', () =>
103
+ withRoot(async (h) => {
104
+ 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' });
105
+ if (r.status !== 201) return false;
106
+ const b = r.body as Body;
107
+ return typeof b.sid === 'string' && /^SM[0-9a-f]{32}$/.test(b.sid) && b.status === 'queued'
108
+ && b.to === '+15551234567' && b.from === '+15557654321' && b.body === 'hi';
109
+ })),
110
+ 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', () =>
111
+ withRoot(async (h) => {
112
+ const r = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { From: '+15557654321', Body: 'hi' }, host: 'api.twilio.com' });
113
+ const b = r.body as Body;
114
+ return r.status === 400 && b.code === 21604;
115
+ })),
116
+ 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', () =>
117
+ withRoot(async (h) => {
118
+ 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' });
119
+ const b = r.body as Body;
120
+ return r.status === 400 && b.code === 21211;
121
+ })),
122
+ done('twilio.messages.send_rejects_non_urlencoded', 'messages', 'a JSON request body (not form-urlencoded) -> 400 vendor-shaped, never silently misparsed', 'api', 'common', async () => {
123
+ const root = mkdtempSync(join(tmpdir(), 'twilio-cap-'));
124
+ try {
125
+ 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 });
126
+ const b = r.body as Body;
127
+ return r.status === 400 && typeof b.code === 'number' && typeof b.message === 'string';
128
+ } finally {
129
+ rmSync(root, { recursive: true, force: true });
130
+ }
131
+ }),
132
+ done('twilio.messages.get', 'messages', 'GET Messages/{Sid}.json round-trips the created message (sid/to/from/body match)', 'api', 'core', () =>
133
+ withRoot(async (h) => {
134
+ 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' });
135
+ const sid = (c.body as Body).sid as string;
136
+ const g = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sid}.json`, host: 'api.twilio.com' });
137
+ const b = g.body as Body;
138
+ return g.status === 200 && b.sid === sid && b.to === '+15551234567' && b.body === 'roundtrip';
139
+ })),
140
+ 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', () =>
141
+ withRoot(async (h) => {
142
+ const r = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/SMdoesnotexist00000000000000000.json`, host: 'api.twilio.com' });
143
+ const b = r.body as Body;
144
+ return r.status === 404 && b.code === 20404;
145
+ })),
146
+ 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', () =>
147
+ withRoot(async (h) => {
148
+ await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234567', From: '+15557654321', Body: 'a' }, host: 'api.twilio.com' });
149
+ await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { To: '+15551234568', From: '+15557654321', Body: 'b' }, host: 'api.twilio.com' });
150
+ const l = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, host: 'api.twilio.com' });
151
+ const b = l.body as Body;
152
+ return l.status === 200 && Array.isArray(b.messages) && b.messages.length === 2
153
+ && typeof b.page === 'number' && typeof b.page_size === 'number' && 'first_page_uri' in b && 'next_page_uri' in b && 'previous_page_uri' in b;
154
+ })),
155
+ 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', () =>
156
+ withRoot(async (h) => {
157
+ 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' });
158
+ const sid = (c.body as Body).sid as string;
159
+ const get = () => h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sid}.json`, host: 'api.twilio.com' });
160
+ const p1 = await get(); if ((p1.body as Body).status !== 'queued') return false;
161
+ const p2 = await get(); if ((p2.body as Body).status !== 'sending') return false;
162
+ const p3 = await get(); if ((p3.body as Body).status !== 'sent') return false;
163
+ const p4 = await get(); if ((p4.body as Body).status !== 'delivered') return false;
164
+ return true;
165
+ })),
166
+ done('twilio.messages.status_terminal_delivered', 'messages', 'delivered is terminal — repeated polls stay delivered, idempotent', 'api', 'common', () =>
167
+ withRoot(async (h) => {
168
+ 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' });
169
+ const sid = (c.body as Body).sid as string;
170
+ const get = () => h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sid}.json`, host: 'api.twilio.com' });
171
+ await get(); await get(); await get(); await get(); // -> delivered
172
+ const again1 = await get();
173
+ const again2 = await get();
174
+ return (again1.body as Body).status === 'delivered' && (again2.body as Body).status === 'delivered';
175
+ })),
176
+ done('twilio.messages.sid_format', 'messages', 'message sid matches the real Twilio SM + 32-hex-char shape exactly', 'api', 'common', () =>
177
+ withRoot(async (h) => {
178
+ 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' });
179
+ const sid = (c.body as Body).sid as string;
180
+ return typeof sid === 'string' && sid.length === 34 && /^SM[0-9a-f]{32}$/.test(sid);
181
+ })),
182
+ done('twilio.messages.isolation', 'messages', 'two independent messages: progressing one to delivered leaves the other at queued', 'api', 'common', () =>
183
+ withRoot(async (h) => {
184
+ 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' });
185
+ 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' });
186
+ const sidA = (a.body as Body).sid as string;
187
+ const sidB = (b.body as Body).sid as string;
188
+ const getA = () => h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sidA}.json`, host: 'api.twilio.com' });
189
+ await getA(); await getA(); await getA(); const finalA = await getA();
190
+ const getB = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sidB}.json`, host: 'api.twilio.com' });
191
+ return (finalA.body as Body).status === 'delivered' && (getB.body as Body).status === 'queued';
192
+ })),
193
+ 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', () =>
194
+ withRoot(async (h) => {
195
+ 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' });
196
+ return (c.body as Body).direction === 'outbound-api';
197
+ })),
198
+ 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', () =>
199
+ withRoot(async (h) => {
200
+ 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' });
201
+ const longBody = 'x'.repeat(200);
202
+ 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' });
203
+ const shortSeg = (short.body as Body).num_segments;
204
+ const longSeg = (long.body as Body).num_segments;
205
+ return shortSeg === '1' && typeof longSeg === 'string' && Number(longSeg) > 1;
206
+ })),
207
+
208
+ // ── ACCOUNTS (pure per-root derivation — build spec's kernel-gotcha finding, see twilio-twin.ts header) ──
209
+ 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', () =>
210
+ withRoot(async (h, root) => {
211
+ const r = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}.json`, host: 'api.twilio.com' });
212
+ const b = r.body as Body;
213
+ return r.status === 200 && b.sid === ACCOUNT_SID && b.status === 'active' && typeof b.friendly_name === 'string'
214
+ && b.type === 'Full' && b.auth_token === await twilioAuthToken(root);
215
+ })),
216
+
217
+ // ── VERIFY (start -> check, twin-internal deterministic expected code) ───────────────────────
218
+ 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', () =>
219
+ withRoot(async (h) => {
220
+ const r = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/Verifications`, b: { To: '+15551234567', Channel: 'sms' }, host: 'verify.twilio.com' });
221
+ const b = r.body as Body;
222
+ return r.status === 201 && typeof b.sid === 'string' && /^VE[0-9a-f]{32}$/.test(b.sid) && b.status === 'pending' && b.valid === false;
223
+ })),
224
+ 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', () =>
225
+ withRoot(async (h) => {
226
+ const r = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/Verifications`, b: { Channel: 'sms' }, host: 'verify.twilio.com' });
227
+ const b = r.body as Body;
228
+ return r.status === 400 && b.code === 60200;
229
+ })),
230
+ 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', () =>
231
+ withRoot(async (h) => {
232
+ const to = '+15551234567';
233
+ await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/Verifications`, b: { To: to, Channel: 'sms' }, host: 'verify.twilio.com' });
234
+ const code = expectedCodeFor(SERVICE_SID, to);
235
+ const check = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/VerificationCheck`, b: { To: to, Code: code }, host: 'verify.twilio.com' });
236
+ const b = check.body as Body;
237
+ return check.status === 200 && b.status === 'approved' && b.valid === true;
238
+ })),
239
+ 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', () =>
240
+ withRoot(async (h) => {
241
+ const to = '+15551234568';
242
+ await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/Verifications`, b: { To: to, Channel: 'sms' }, host: 'verify.twilio.com' });
243
+ const check = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/VerificationCheck`, b: { To: to, Code: '000000' }, host: 'verify.twilio.com' });
244
+ const b = check.body as Body;
245
+ return check.status === 200 && b.status === 'pending' && b.valid === false;
246
+ })),
247
+ done('twilio.verify.check_unknown_404', 'verify', "VerificationCheck for a To with no pending verification -> 404 {code:20404}", 'api', 'common', () =>
248
+ withRoot(async (h) => {
249
+ const r = await h({ m: 'POST', p: `/v2/Services/${SERVICE_SID}/VerificationCheck`, b: { To: '+19999999999', Code: '123456' }, host: 'verify.twilio.com' });
250
+ const b = r.body as Body;
251
+ return r.status === 404 && b.code === 20404;
252
+ })),
253
+
254
+ // ── LOOKUP (pure deterministic derivation, no kernel row) ─────────────────────────────────────
255
+ done('twilio.lookup.phone_number', 'lookup', 'GET PhoneNumbers/{E164} -> {phone_number,national_format,country_code,valid:true}', 'api', 'core', () =>
256
+ withRoot(async (h) => {
257
+ const r = await h({ m: 'GET', p: '/v2/PhoneNumbers/+15551234567', host: 'lookups.twilio.com' });
258
+ const b = r.body as Body;
259
+ return r.status === 200 && b.phone_number === '+15551234567' && typeof b.national_format === 'string' && b.country_code === 'US' && b.valid === true;
260
+ })),
261
+ 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', () =>
262
+ withRoot(async (h) => {
263
+ const r = await h({ m: 'GET', p: '/v2/PhoneNumbers/+15551234567?Fields=line_type_intelligence', host: 'lookups.twilio.com' });
264
+ const b = r.body as Body;
265
+ const lti = b.line_type_intelligence as Body;
266
+ return r.status === 200 && lti && typeof lti.type === 'string' && typeof lti.carrier_name === 'string';
267
+ })),
268
+ 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', () =>
269
+ withRoot(async (h) => {
270
+ const r = await h({ m: 'GET', p: '/v2/PhoneNumbers/not-a-real-number', host: 'lookups.twilio.com' });
271
+ const b = r.body as Body;
272
+ return r.status === 404 && b.code === 20404;
273
+ })),
274
+
275
+ // ── INCOMING PHONE NUMBERS (lazily-seeded, kernel-persisted) ──────────────────────────────────
276
+ 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', () =>
277
+ withRoot(async (h) => {
278
+ const r = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/IncomingPhoneNumbers.json`, host: 'api.twilio.com' });
279
+ const b = r.body as Body;
280
+ if (r.status !== 200 || !Array.isArray(b.incoming_phone_numbers) || b.incoming_phone_numbers.length < 2) return false;
281
+ const first = b.incoming_phone_numbers[0] as Body;
282
+ return typeof first.phone_number === 'string' && typeof first.type === 'string' && first.number_type === undefined;
283
+ })),
284
+
285
+ // ── WEBHOOKS (X-Twilio-Signature: FRESH HMAC-SHA1, sign/verify/tamper + inbound builder) ─────
286
+ 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', () => {
287
+ const url = 'https://example.com/status-callback';
288
+ const built = buildSignedStatusCallback({ url, messageSid: 'SMabc', messageStatus: 'delivered', to: '+15551234567', from: '+15557654321', ...SIGNING_PAIR });
289
+ try {
290
+ verifyTwilioSignature(url, built.params, built.headers['x-twilio-signature']!, SIGNING_PAIR.authToken);
291
+ return true;
292
+ } catch (err) {
293
+ if (isInfrastructureError(err)) throw harnessError('twilio.webhooks.status_callback_signature', err);
294
+ return false;
295
+ }
296
+ }),
297
+ 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', () => {
298
+ const url = 'https://example.com/status-callback';
299
+ const built = buildSignedStatusCallback({ url, messageSid: 'SMabc', messageStatus: 'delivered', to: '+15551234567', from: '+15557654321', ...SIGNING_PAIR });
300
+ const tamperedParams = { ...built.params, To: '+19999999999' };
301
+ try {
302
+ verifyTwilioSignature(url, tamperedParams, built.headers['x-twilio-signature']!, SIGNING_PAIR.authToken);
303
+ return false; // must have thrown
304
+ } catch (err) {
305
+ return err instanceof TwilioSignatureVerificationError;
306
+ }
307
+ }),
308
+ 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', () => {
309
+ const url = 'https://example.com/sms-inbound';
310
+ const built = buildSignedInboundMessage({ url, from: '+15551234567', to: '+15557654321', body: 'inbound hi', ...SIGNING_PAIR });
311
+ if (built.params.From !== '+15551234567' || built.params.To !== '+15557654321' || built.params.Body !== 'inbound hi' || typeof built.params.MessageSid !== 'string') return false;
312
+ try {
313
+ verifyTwilioSignature(url, built.params, built.headers['x-twilio-signature']!, SIGNING_PAIR.authToken);
314
+ return true;
315
+ } catch (err) {
316
+ if (isInfrastructureError(err)) throw harnessError('twilio.webhooks.inbound_message_shape', err);
317
+ return false;
318
+ }
319
+ }),
320
+
321
+ // ── ERRORS ────────────────────────────────────────────────────────────────────────────────
322
+ 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', () =>
323
+ withRoot(async (h) => {
324
+ const r = await h({ m: 'POST', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`, b: { From: '+15557654321', Body: 'x' }, host: 'api.twilio.com' });
325
+ const b = r.body as Body;
326
+ const keys = Object.keys(b).sort();
327
+ return JSON.stringify(keys) === JSON.stringify(['code', 'message', 'more_info', 'status']) && b.status === 400;
328
+ })),
329
+
330
+ // ── SAFETY / HONESTY ─────────────────────────────────────────────────────────────────────
331
+ done('twilio.read_only.rejects_writes', 'safety', 'readOnly mode rejects writes with 405 but allows GET', 'api', 'common', async () => {
332
+ const root = mkdtempSync(join(tmpdir(), 'twilio-ro-'));
333
+ try {
334
+ 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 });
335
+ const g = await handleTwilioTwinRequest({ method: 'GET', path: `/2010-04-01/Accounts/${ACCOUNT_SID}.json`, root, readOnly: true, host: 'api.twilio.com', occurredAt: OCCURRED_AT });
336
+ return w.status === 405 && g.status === 200;
337
+ } finally {
338
+ rmSync(root, { recursive: true, force: true });
339
+ }
340
+ }),
341
+ 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', () =>
342
+ withRoot(async (h) => {
343
+ const r = await h({ m: 'GET', p: '/some/totally/unknown/route', host: 'api.twilio.com' });
344
+ const b = r.body as Body;
345
+ return r.status === 404 && typeof b.code === 'number' && typeof b.message === 'string';
346
+ })),
347
+ 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', () =>
348
+ withRoot(async (h) => {
349
+ 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' });
350
+ const sid = (c.body as Body).sid;
351
+ if (typeof sid !== 'string' || sid.length === 0) return false;
352
+ const g = await h({ m: 'GET', p: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages/${sid}.json`, host: 'api.twilio.com' });
353
+ return g.status === 200 && (g.body as Body).sid === sid;
354
+ })),
355
+
356
+ // ── CONFORMANCE / CONNECTOR ──────────────────────────────────────────────────────────────
357
+ 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', () => {
358
+ const report = checkTwilioConformance();
359
+ return report.ok && report.endpointsChecked >= 10 && report.resourceTypesChecked === 4;
360
+ }),
361
+ 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 () => {
362
+ const root = mkdtempSync(join(tmpdir(), 'twilio-conn-'));
363
+ try {
364
+ const client: TwilioLikeClient = {
365
+ messages: {
366
+ list: async () => [{ sid: 'SM' + '1'.repeat(32), to: '+15551234567', from: '+15557654321', body: 'real pulled message', status: 'delivered', direction: 'outbound-api', numSegments: '1', numMedia: '0' }],
367
+ },
368
+ };
369
+ const first = await syncTwilioFromReal(client, { root, occurredAt: OCCURRED_AT, budgetOptions: { root } });
370
+ if (first.observed !== 1 || first.deltasAppended < 1) return false;
371
+ const second = await syncTwilioFromReal(client, { root, occurredAt: OCCURRED_AT, budgetOptions: { root } });
372
+ if (second.deltasAppended !== 0) return false;
373
+ 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 });
374
+ return r.status === 200 && (r.body as Body).body === 'real pulled message';
375
+ } finally {
376
+ rmSync(root, { recursive: true, force: true });
377
+ }
378
+ }),
379
+ done('twilio.connector.empty_client', 'connector', 'syncTwilioFromReal with no client.messages observes nothing', 'connector', 'common', async () => {
380
+ const root = mkdtempSync(join(tmpdir(), 'twilio-conn-empty-'));
381
+ try {
382
+ const result = await syncTwilioFromReal({}, { root, occurredAt: OCCURRED_AT, budgetOptions: { root } });
383
+ return result.observed === 0 && result.deltasAppended === 0;
384
+ } finally {
385
+ rmSync(root, { recursive: true, force: true });
386
+ }
387
+ }),
388
+
389
+ // ── TODO: the rest of the Twilio v1 surface (honest denominator) ────────────────────────────
390
+ todo('twilio.messages.mms_media', 'messages', 'MMS MediaUrl attachments (send + Media subresource fetch)', 'api', 'common'),
391
+ todo('twilio.messages.messaging_service_sid', 'messages', 'sending via a MessagingServiceSid (sender-pool routing) instead of an explicit From', 'api', 'common'),
392
+ todo('twilio.messages.scheduled_send', 'messages', 'SendAt / schedule_type:fixed scheduled sends + the scheduled status', 'api', 'niche'),
393
+ todo('twilio.messages.delete', 'messages', 'DELETE Messages/{Sid}.json', 'api', 'common'),
394
+ todo('twilio.messages.redact', 'messages', 'POST Messages/{Sid}.json Body="" redaction', 'api', 'niche'),
395
+ todo('twilio.messages.pagination_cursor', 'pagination', 'real cursor-based pagination (PageToken / next_page_uri actually fetchable) beyond a single-page list', 'api', 'common'),
396
+ 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'),
397
+ todo('twilio.messaging.services', 'messaging', 'Messaging Services (sender pools, sticky sender) CRUD', 'api', 'common'),
398
+ todo('twilio.messaging.copilot', 'messaging', 'Messaging Copilot / Advanced Opt-Out features', 'api', 'niche'),
399
+ todo('twilio.messaging.short_codes', 'messaging', 'short code provisioning/management', 'api', 'niche'),
400
+ todo('twilio.messaging.senders', 'messaging', 'sender (phone number / short code / alphanumeric) pool management', 'api', 'niche'),
401
+ todo('twilio.voice.calls', 'voice', 'Calls resource (voice call create/status)', 'api', 'common'),
402
+ todo('twilio.voice.twiml', 'voice', 'TwiML document generation/execution', 'api', 'common'),
403
+ todo('twilio.conversations.create', 'messaging', 'Conversations API (multi-channel conversation threads)', 'api', 'niche'),
404
+ todo('twilio.verify.channel_email', 'verify', "Verify email channel (Channel:'email')", 'api', 'common'),
405
+ todo('twilio.verify.channel_whatsapp', 'verify', "Verify WhatsApp channel (Channel:'whatsapp')", 'api', 'common'),
406
+ todo('twilio.verify.channel_call', 'verify', "Verify voice-call channel (Channel:'call')", 'api', 'niche'),
407
+ 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'),
408
+ todo('twilio.verify.rate_limit', 'verify', 'Verify RateLimits resource / 429 rate-limiting', 'api', 'niche'),
409
+ todo('twilio.verify.fraud_guard', 'verify', 'Verify Fraud Guard / RiskCheck', 'api', 'niche'),
410
+ todo('twilio.lookup.carrier_live', 'lookup', 'a LIVE (non-deterministic-derivation) carrier lookup against a real carrier database', 'api', 'niche'),
411
+ todo('twilio.lookup.caller_name', 'lookup', 'CNAM caller-name lookup (?Fields=caller_name)', 'api', 'common'),
412
+ todo('twilio.lookup.sim_swap', 'lookup', 'SIM swap detection (?Fields=sim_swap)', 'api', 'niche'),
413
+ todo('twilio.lookup.reassigned_number', 'lookup', 'reassigned number detection (?Fields=reassigned_number)', 'api', 'niche'),
414
+ todo('twilio.incoming_phone_numbers.provision', 'phone_numbers', 'POST IncomingPhoneNumbers.json (real provisioning of a new number)', 'api', 'common'),
415
+ todo('twilio.incoming_phone_numbers.update', 'phone_numbers', 'POST IncomingPhoneNumbers/{Sid}.json (update SmsUrl/VoiceUrl/etc.)', 'api', 'common'),
416
+ todo('twilio.accounts.subaccounts', 'auth', 'sub-account creation/management', 'api', 'niche'),
417
+ todo('twilio.usage.records', 'auth', 'Usage Records (billing/usage-tracking resource)', 'api', 'niche'),
418
+ todo('twilio.regulatory.bundles', 'phone_numbers', 'Regulatory Compliance Bundles (number provisioning compliance)', 'api', 'niche'),
419
+ todo('twilio.errors.code_catalog', 'errors', 'the full Twilio error-code catalog (this pack grounds 6 codes live; ~thousands exist)', 'api', 'niche'),
420
+ todo('twilio.webhooks.status_callback_retry', 'webhooks', 'status-callback delivery retry policy on a non-2xx response', 'api', 'niche'),
421
+ todo('twilio.webhooks.fallback_url', 'webhooks', 'SmsFallbackUrl / StatusCallback fallback-on-error routing', 'api', 'niche'),
422
+ 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'),
423
+ todo('twilio.auth.api_key_auth', 'auth', 'API Key/Secret (SK.../secret) auth as an alternative to AccountSid/AuthToken', 'api', 'niche'),
424
+ todo('twilio.connector.push', 'connector', 'push a locally-created message/verification against a real Twilio account', 'connector', 'common'),
425
+ 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'),
426
+ todo('twilio.fixtures.seed', 'connector', 'seed a small library of realistic message/verification fixtures beyond the 2 lazily-seeded phone numbers', 'connector', 'niche'),
427
+ ];
428
+
429
+ export const TWILIO_AREAS = [
430
+ 'messages', 'verify', 'lookup', 'phone_numbers', 'messaging', 'voice', 'webhooks', 'auth',
431
+ 'pagination', 'errors', 'connector', 'conformance', 'safety', 'state',
432
+ ] as const;
433
+
434
+ export async function twilioCapabilities(): Promise<CapabilityReport> {
435
+ return checkCapabilities('twilio', TWILIO_CAPABILITIES);
436
+ }
@@ -0,0 +1,45 @@
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.ts';
7
+
8
+ export type TwilioConformanceReport = {
9
+ ok: boolean;
10
+ endpointsChecked: number;
11
+ resourceTypesChecked: number;
12
+ violations: string[];
13
+ };
14
+
15
+ export function checkTwilioConformance(): TwilioConformanceReport {
16
+ const snapshot = twilioTwinSnapshot();
17
+ const violations: string[] = [];
18
+ // Every declared resource type must have at least one implemented endpoint touching it.
19
+ // `signing_key` has no REST path of its own — it's served (purely derived, per-root) at the
20
+ // Accounts fetch endpoint, so it maps to the 'Accounts' stem instead of its own name (build
21
+ // spec §4).
22
+ const stemFor: Record<string, string> = {
23
+ message: '/Messages',
24
+ verification: '/Verifications',
25
+ phone_number: '/IncomingPhoneNumbers',
26
+ signing_key: '/Accounts',
27
+ };
28
+ for (const type of TWILIO_RESOURCE_TYPES) {
29
+ const stem = stemFor[type];
30
+ if (!stem || !snapshot.implementedEndpoints.some((e) => e.includes(stem))) {
31
+ violations.push(`resource type '${type}' has no implemented endpoint`);
32
+ }
33
+ }
34
+ // The core messaging/verify/lookup endpoints must be present in the protocol surface.
35
+ if (!snapshot.implementedEndpoints.some((e) => e.startsWith('POST api.twilio.com') && e.includes('Messages.json'))) violations.push('missing message send endpoint');
36
+ if (!snapshot.implementedEndpoints.some((e) => e.startsWith('POST verify.twilio.com') && e.includes('Verifications'))) violations.push('missing verify start endpoint');
37
+ if (!snapshot.implementedEndpoints.some((e) => e.includes('VerificationCheck'))) violations.push('missing verify check endpoint');
38
+ if (!snapshot.implementedEndpoints.some((e) => e.startsWith('GET lookups.twilio.com'))) violations.push('missing lookup endpoint');
39
+ return {
40
+ ok: violations.length === 0,
41
+ endpointsChecked: snapshot.implementedEndpoints.length,
42
+ resourceTypesChecked: snapshot.resourceTypes.length,
43
+ violations,
44
+ };
45
+ }