@drawbridge/drawbridge-utils 0.0.133 → 0.0.135
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/dist/connections/index.cjs +81 -5
- package/dist/connections/index.d.cts +100 -4
- package/dist/connections/index.d.ts +100 -4
- package/dist/connections/index.js +81 -5
- package/dist/connections/oauth.cjs +8 -2
- package/dist/connections/oauth.d.cts +14 -3
- package/dist/connections/oauth.d.ts +14 -3
- package/dist/connections/oauth.js +8 -2
- package/dist/providers.cjs +81 -5
- package/dist/providers.js +81 -5
- package/dist/twilio.cjs +145 -7
- package/dist/twilio.d.cts +188 -7
- package/dist/twilio.d.ts +188 -7
- package/dist/twilio.js +145 -7
- package/package.json +1 -1
package/dist/providers.cjs
CHANGED
|
@@ -370,8 +370,14 @@ var authToken = async ({
|
|
|
370
370
|
});
|
|
371
371
|
const body = await response.json().catch(() => ({}));
|
|
372
372
|
if (!response.ok) {
|
|
373
|
-
throw
|
|
374
|
-
|
|
373
|
+
throw Object.assign(
|
|
374
|
+
new Error(
|
|
375
|
+
renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
|
|
376
|
+
),
|
|
377
|
+
{
|
|
378
|
+
status: response.status,
|
|
379
|
+
...renewing && { code: "grant_refused" }
|
|
380
|
+
}
|
|
375
381
|
);
|
|
376
382
|
}
|
|
377
383
|
if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");
|
|
@@ -427,7 +433,10 @@ var accessToken = async ({
|
|
|
427
433
|
}
|
|
428
434
|
if (!force && !isStale(settings, now)) return settings.accessToken;
|
|
429
435
|
if (!settings.refreshToken) {
|
|
430
|
-
throw
|
|
436
|
+
throw Object.assign(
|
|
437
|
+
new Error("This connection has expired and cannot be renewed automatically. Reconnect it."),
|
|
438
|
+
{ code: "grant_refused" }
|
|
439
|
+
);
|
|
431
440
|
}
|
|
432
441
|
const minted = await manifest.hooks.auth.token({
|
|
433
442
|
clientId,
|
|
@@ -2641,8 +2650,62 @@ var klaviyo_default2 = {
|
|
|
2641
2650
|
sms: false,
|
|
2642
2651
|
inbound: false,
|
|
2643
2652
|
// Nothing to set up or tear down at the vendor: the grant is the whole
|
|
2644
|
-
// integration
|
|
2645
|
-
lifecycle
|
|
2653
|
+
// integration. What CAN rot is the grant itself, so health is the one
|
|
2654
|
+
// lifecycle hook Klaviyo carries.
|
|
2655
|
+
lifecycle: {
|
|
2656
|
+
// Nothing to set up, tear down, or re-register at the vendor — the
|
|
2657
|
+
// grant is the whole integration.
|
|
2658
|
+
cleanup: false,
|
|
2659
|
+
register: false,
|
|
2660
|
+
rehydrate: false,
|
|
2661
|
+
// KEEP THE GRANT PROVEN AND WARM. The shell mints a fresh access
|
|
2662
|
+
// token before every hook run — for Klaviyo that spends the refresh
|
|
2663
|
+
// token, which is the only question that reaches Klaviyo (see
|
|
2664
|
+
// auth.probe) — so this body running at all proves the grant still
|
|
2665
|
+
// rotates, and the daily run keeps it inside Klaviyo's 90-day idle
|
|
2666
|
+
// window. A dead grant never gets here: the shell's mint throws
|
|
2667
|
+
// `grant_refused` and the step runner errors the connection itself.
|
|
2668
|
+
//
|
|
2669
|
+
// The one call below proves the minted token is HONOURED — mint and
|
|
2670
|
+
// acceptance are different facts, and /accounts is already the call
|
|
2671
|
+
// the connect flow makes (auth.connect), so it needs no new scope.
|
|
2672
|
+
health: async ({ connection: connection2, token }, { fetcher, read } = {}) => {
|
|
2673
|
+
const request2 = { connectionId: connection2.id };
|
|
2674
|
+
try {
|
|
2675
|
+
await api2("/accounts", { fetcher, token });
|
|
2676
|
+
return {
|
|
2677
|
+
message: "Health check passed \u2014 token minted and accepted.",
|
|
2678
|
+
request: request2,
|
|
2679
|
+
response: { pingedAt: /* @__PURE__ */ new Date() }
|
|
2680
|
+
};
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
if ([401, 403].includes(error.status) && read) {
|
|
2683
|
+
const current = await read.get({ collection: "connection", query: { id: connection2.id } });
|
|
2684
|
+
if (current) {
|
|
2685
|
+
const others = (current.errors || []).filter((entry) => entry.source !== "oauth");
|
|
2686
|
+
error.writes = [{
|
|
2687
|
+
collection: "connection",
|
|
2688
|
+
data: {
|
|
2689
|
+
$set: {
|
|
2690
|
+
errors: [
|
|
2691
|
+
...others,
|
|
2692
|
+
{
|
|
2693
|
+
message: "Klaviyo no longer accepts this connection. Reconnect Klaviyo to resume syncing.",
|
|
2694
|
+
source: "oauth"
|
|
2695
|
+
}
|
|
2696
|
+
],
|
|
2697
|
+
status: "error"
|
|
2698
|
+
}
|
|
2699
|
+
},
|
|
2700
|
+
operation: "update",
|
|
2701
|
+
query: { id: connection2.id }
|
|
2702
|
+
}];
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
throw error;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
},
|
|
2646
2709
|
resources: {
|
|
2647
2710
|
// The lists a merchant can sync into, for the picker on their
|
|
2648
2711
|
// connection.
|
|
@@ -2718,6 +2781,19 @@ var klaviyo_default2 = {
|
|
|
2718
2781
|
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? data2.status : "pending";
|
|
2719
2782
|
},
|
|
2720
2783
|
steps: {
|
|
2784
|
+
connection: {
|
|
2785
|
+
// The daily grant check — same step type and workflow shape as
|
|
2786
|
+
// Shopify's, provisioned as a system workflow per connection.
|
|
2787
|
+
health: {
|
|
2788
|
+
check: () => ({
|
|
2789
|
+
description: "Keeps the Klaviyo grant working \u2014 spends the refresh token daily so a revoked or idle grant is reported instead of discovered by a failing sync.",
|
|
2790
|
+
hook: "lifecycle.health",
|
|
2791
|
+
key: "Klaviyo Connection Health",
|
|
2792
|
+
queue: "connection",
|
|
2793
|
+
system: true
|
|
2794
|
+
})
|
|
2795
|
+
}
|
|
2796
|
+
},
|
|
2721
2797
|
contacts: {
|
|
2722
2798
|
// A DECLARATION, not the work. It names the hook that does the work, and
|
|
2723
2799
|
// says where that hook's values belong. Nested like the hooks, and the
|
package/dist/providers.js
CHANGED
|
@@ -326,8 +326,14 @@ var authToken = async ({
|
|
|
326
326
|
});
|
|
327
327
|
const body = await response.json().catch(() => ({}));
|
|
328
328
|
if (!response.ok) {
|
|
329
|
-
throw
|
|
330
|
-
|
|
329
|
+
throw Object.assign(
|
|
330
|
+
new Error(
|
|
331
|
+
renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
|
|
332
|
+
),
|
|
333
|
+
{
|
|
334
|
+
status: response.status,
|
|
335
|
+
...renewing && { code: "grant_refused" }
|
|
336
|
+
}
|
|
331
337
|
);
|
|
332
338
|
}
|
|
333
339
|
if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");
|
|
@@ -383,7 +389,10 @@ var accessToken = async ({
|
|
|
383
389
|
}
|
|
384
390
|
if (!force && !isStale(settings, now)) return settings.accessToken;
|
|
385
391
|
if (!settings.refreshToken) {
|
|
386
|
-
throw
|
|
392
|
+
throw Object.assign(
|
|
393
|
+
new Error("This connection has expired and cannot be renewed automatically. Reconnect it."),
|
|
394
|
+
{ code: "grant_refused" }
|
|
395
|
+
);
|
|
387
396
|
}
|
|
388
397
|
const minted = await manifest.hooks.auth.token({
|
|
389
398
|
clientId,
|
|
@@ -2597,8 +2606,62 @@ var klaviyo_default2 = {
|
|
|
2597
2606
|
sms: false,
|
|
2598
2607
|
inbound: false,
|
|
2599
2608
|
// Nothing to set up or tear down at the vendor: the grant is the whole
|
|
2600
|
-
// integration
|
|
2601
|
-
lifecycle
|
|
2609
|
+
// integration. What CAN rot is the grant itself, so health is the one
|
|
2610
|
+
// lifecycle hook Klaviyo carries.
|
|
2611
|
+
lifecycle: {
|
|
2612
|
+
// Nothing to set up, tear down, or re-register at the vendor — the
|
|
2613
|
+
// grant is the whole integration.
|
|
2614
|
+
cleanup: false,
|
|
2615
|
+
register: false,
|
|
2616
|
+
rehydrate: false,
|
|
2617
|
+
// KEEP THE GRANT PROVEN AND WARM. The shell mints a fresh access
|
|
2618
|
+
// token before every hook run — for Klaviyo that spends the refresh
|
|
2619
|
+
// token, which is the only question that reaches Klaviyo (see
|
|
2620
|
+
// auth.probe) — so this body running at all proves the grant still
|
|
2621
|
+
// rotates, and the daily run keeps it inside Klaviyo's 90-day idle
|
|
2622
|
+
// window. A dead grant never gets here: the shell's mint throws
|
|
2623
|
+
// `grant_refused` and the step runner errors the connection itself.
|
|
2624
|
+
//
|
|
2625
|
+
// The one call below proves the minted token is HONOURED — mint and
|
|
2626
|
+
// acceptance are different facts, and /accounts is already the call
|
|
2627
|
+
// the connect flow makes (auth.connect), so it needs no new scope.
|
|
2628
|
+
health: async ({ connection: connection2, token }, { fetcher, read } = {}) => {
|
|
2629
|
+
const request2 = { connectionId: connection2.id };
|
|
2630
|
+
try {
|
|
2631
|
+
await api2("/accounts", { fetcher, token });
|
|
2632
|
+
return {
|
|
2633
|
+
message: "Health check passed \u2014 token minted and accepted.",
|
|
2634
|
+
request: request2,
|
|
2635
|
+
response: { pingedAt: /* @__PURE__ */ new Date() }
|
|
2636
|
+
};
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
if ([401, 403].includes(error.status) && read) {
|
|
2639
|
+
const current = await read.get({ collection: "connection", query: { id: connection2.id } });
|
|
2640
|
+
if (current) {
|
|
2641
|
+
const others = (current.errors || []).filter((entry) => entry.source !== "oauth");
|
|
2642
|
+
error.writes = [{
|
|
2643
|
+
collection: "connection",
|
|
2644
|
+
data: {
|
|
2645
|
+
$set: {
|
|
2646
|
+
errors: [
|
|
2647
|
+
...others,
|
|
2648
|
+
{
|
|
2649
|
+
message: "Klaviyo no longer accepts this connection. Reconnect Klaviyo to resume syncing.",
|
|
2650
|
+
source: "oauth"
|
|
2651
|
+
}
|
|
2652
|
+
],
|
|
2653
|
+
status: "error"
|
|
2654
|
+
}
|
|
2655
|
+
},
|
|
2656
|
+
operation: "update",
|
|
2657
|
+
query: { id: connection2.id }
|
|
2658
|
+
}];
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
throw error;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
},
|
|
2602
2665
|
resources: {
|
|
2603
2666
|
// The lists a merchant can sync into, for the picker on their
|
|
2604
2667
|
// connection.
|
|
@@ -2674,6 +2737,19 @@ var klaviyo_default2 = {
|
|
|
2674
2737
|
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? data2.status : "pending";
|
|
2675
2738
|
},
|
|
2676
2739
|
steps: {
|
|
2740
|
+
connection: {
|
|
2741
|
+
// The daily grant check — same step type and workflow shape as
|
|
2742
|
+
// Shopify's, provisioned as a system workflow per connection.
|
|
2743
|
+
health: {
|
|
2744
|
+
check: () => ({
|
|
2745
|
+
description: "Keeps the Klaviyo grant working \u2014 spends the refresh token daily so a revoked or idle grant is reported instead of discovered by a failing sync.",
|
|
2746
|
+
hook: "lifecycle.health",
|
|
2747
|
+
key: "Klaviyo Connection Health",
|
|
2748
|
+
queue: "connection",
|
|
2749
|
+
system: true
|
|
2750
|
+
})
|
|
2751
|
+
}
|
|
2752
|
+
},
|
|
2677
2753
|
contacts: {
|
|
2678
2754
|
// A DECLARATION, not the work. It names the hook that does the work, and
|
|
2679
2755
|
// says where that hook's values belong. Nested like the hooks, and the
|
package/dist/twilio.cjs
CHANGED
|
@@ -107,28 +107,37 @@ var twilio = {
|
|
|
107
107
|
// NUMBER LIFECYCLE, same transport discipline as sms above. Endpoints and
|
|
108
108
|
// parameter names are Twilio's own, from the AvailablePhoneNumbers and
|
|
109
109
|
// IncomingPhoneNumbers resource docs:
|
|
110
|
-
// GET /2010-04-01/Accounts/{AccountSid}/AvailablePhoneNumbers/{CountryCode}/
|
|
111
|
-
// query: SmsEnabled,
|
|
110
|
+
// GET /2010-04-01/Accounts/{AccountSid}/AvailablePhoneNumbers/{CountryCode}/TollFree.json
|
|
111
|
+
// query: SmsEnabled, Contains, PageSize (1-1000)
|
|
112
112
|
// POST /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers.json
|
|
113
113
|
// body: PhoneNumber (E.164), SmsUrl, SmsMethod
|
|
114
114
|
// DELETE /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers/{Sid}.json
|
|
115
115
|
// Basic auth throughout, like Messages.
|
|
116
|
-
|
|
116
|
+
//
|
|
117
|
+
// TOLL-FREE, not Local, and that is a carrier-registration decision rather
|
|
118
|
+
// than an inventory one. A US local number may not send A2P SMS without full
|
|
119
|
+
// 10DLC brand-and-campaign registration; a toll-free number needs only
|
|
120
|
+
// Toll-Free Verification — one API submission per number (see
|
|
121
|
+
// submitVerification below), no TCR brand, no campaign fees. Until a
|
|
122
|
+
// verification is approved, Twilio blocks the number's US-bound messages
|
|
123
|
+
// outright — which is why a purchased number starts `pending` and the send
|
|
124
|
+
// path refuses anything but `verified`.
|
|
125
|
+
searchNumbers: async ({ accountSid, authToken, contains, country = "US", cursor, limit = 10, request: request2 = request }) => {
|
|
117
126
|
if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
|
|
118
127
|
const page = Number(cursor) || 0;
|
|
119
128
|
const result = await request2({
|
|
120
129
|
method: "GET",
|
|
121
|
-
url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/AvailablePhoneNumbers/" + country + "/
|
|
130
|
+
url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/AvailablePhoneNumbers/" + country + "/TollFree.json",
|
|
122
131
|
headers: {
|
|
123
132
|
"Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
|
|
124
133
|
},
|
|
125
134
|
query: {
|
|
126
135
|
SmsEnabled: true,
|
|
127
136
|
PageSize: limit,
|
|
128
|
-
...areaCode && { AreaCode: areaCode },
|
|
129
137
|
// `Contains` is the vendor's matching-pattern filter — digits
|
|
130
|
-
// match anywhere in the number, so one search box serves "
|
|
131
|
-
//
|
|
138
|
+
// match anywhere in the number, so one search box serves "an 888
|
|
139
|
+
// prefix" and "ends in 7777" alike. (AreaCode is a Local-only
|
|
140
|
+
// filter and went with Local.json.)
|
|
132
141
|
...contains && { Contains: contains },
|
|
133
142
|
...page && { Page: page }
|
|
134
143
|
}
|
|
@@ -165,6 +174,57 @@ var twilio = {
|
|
|
165
174
|
sid: result == null ? void 0 : result.sid
|
|
166
175
|
};
|
|
167
176
|
},
|
|
177
|
+
// THE NUMBER AS TWILIO HOLDS IT NOW. Same IncomingPhoneNumber resource the
|
|
178
|
+
// purchase and release above use (api.twilio.com 2010-04-01, GET
|
|
179
|
+
// /IncomingPhoneNumbers/{sid}.json) — a 404 means the number is no longer
|
|
180
|
+
// on the account, which the health sweep must distinguish from a transient,
|
|
181
|
+
// so it is answered as { gone : true } rather than thrown.
|
|
182
|
+
numberConfig: async ({ accountSid, authToken, request: request2 = request, sid }) => {
|
|
183
|
+
if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
|
|
184
|
+
if (!sid) throw new Error("Twilio config read needs the IncomingPhoneNumber sid");
|
|
185
|
+
let result;
|
|
186
|
+
try {
|
|
187
|
+
result = await request2({
|
|
188
|
+
method: "GET",
|
|
189
|
+
url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/IncomingPhoneNumbers/" + sid + ".json",
|
|
190
|
+
headers: {
|
|
191
|
+
"Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if ((error == null ? void 0 : error.status) === 404) return { gone: true };
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
gone: false,
|
|
200
|
+
number: result == null ? void 0 : result.phone_number,
|
|
201
|
+
smsMethod: result == null ? void 0 : result.sms_method,
|
|
202
|
+
smsUrl: result == null ? void 0 : result.sms_url,
|
|
203
|
+
status: result == null ? void 0 : result.status
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
// RE-POINT AN OWNED NUMBER'S INBOUND WEBHOOK — POST on the same resource,
|
|
207
|
+
// the exact fields purchase sets. The health sweep's repair arm.
|
|
208
|
+
updateNumber: async ({ accountSid, authToken, request: request2 = request, sid, smsUrl }) => {
|
|
209
|
+
if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
|
|
210
|
+
if (!sid) throw new Error("Twilio update needs the IncomingPhoneNumber sid");
|
|
211
|
+
if (!smsUrl) throw new Error("Twilio update needs the smsUrl to point the number at");
|
|
212
|
+
const result = await request2({
|
|
213
|
+
method: "POST",
|
|
214
|
+
type: "form",
|
|
215
|
+
url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/IncomingPhoneNumbers/" + sid + ".json",
|
|
216
|
+
headers: {
|
|
217
|
+
"Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
|
|
218
|
+
},
|
|
219
|
+
body: {
|
|
220
|
+
SmsUrl: smsUrl,
|
|
221
|
+
SmsMethod: "POST"
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
smsUrl: result == null ? void 0 : result.sms_url
|
|
226
|
+
};
|
|
227
|
+
},
|
|
168
228
|
releaseNumber: async ({ accountSid, authToken, request: request2 = request, sid }) => {
|
|
169
229
|
if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
|
|
170
230
|
if (!sid) throw new Error("Twilio release needs the IncomingPhoneNumber sid");
|
|
@@ -175,6 +235,84 @@ var twilio = {
|
|
|
175
235
|
"Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
|
|
176
236
|
}
|
|
177
237
|
});
|
|
238
|
+
},
|
|
239
|
+
// TOLL-FREE VERIFICATION — the carrier registration that lets a purchased
|
|
240
|
+
// number actually deliver. From the Tollfree Verification resource docs
|
|
241
|
+
// (messaging.twilio.com, docs/messaging/api/tollfree-verification-resource):
|
|
242
|
+
// POST /v1/Tollfree/Verifications
|
|
243
|
+
// required: BusinessName, BusinessWebsite, NotificationEmail,
|
|
244
|
+
// UseCaseCategories (array), UseCaseSummary,
|
|
245
|
+
// ProductionMessageSample, OptInImageUrls (array), OptInType,
|
|
246
|
+
// MessageVolume, TollfreePhoneNumberSid (PN…)
|
|
247
|
+
// GET /v1/Tollfree/Verifications/{Sid}
|
|
248
|
+
// status: PENDING_REVIEW | IN_REVIEW | TWILIO_APPROVED |
|
|
249
|
+
// TWILIO_REJECTED, with rejection_reason beside a rejection.
|
|
250
|
+
// Basic auth, form-encoded, like everything above. There is no status
|
|
251
|
+
// callback on this resource — the caller polls the fetch.
|
|
252
|
+
//
|
|
253
|
+
// THE END BUSINESS'S DETAILS, never the platform's. Twilio rejects ISV
|
|
254
|
+
// submissions carrying the ISV's own identity (Toll-Free Verification for
|
|
255
|
+
// ISVs, support.twilio.com) — so businessName, website and email here are
|
|
256
|
+
// the merchant's, collected at purchase.
|
|
257
|
+
//
|
|
258
|
+
// The use-case fields are PLATFORM facts and live here beside the message
|
|
259
|
+
// composition for the same reason it does: how Drawbridge collects opt-in
|
|
260
|
+
// (its own hosted entry forms) and what its messages look like are
|
|
261
|
+
// properties of sending through Drawbridge, not of any one merchant.
|
|
262
|
+
submitVerification: async ({
|
|
263
|
+
accountSid,
|
|
264
|
+
authToken,
|
|
265
|
+
businessName,
|
|
266
|
+
email,
|
|
267
|
+
numberSid,
|
|
268
|
+
optInImage,
|
|
269
|
+
request: request2 = request,
|
|
270
|
+
volume,
|
|
271
|
+
website
|
|
272
|
+
}) => {
|
|
273
|
+
const missing = Object.entries({ accountSid, authToken, businessName, email, numberSid, optInImage, website }).filter(([, value]) => !value).map(([name]) => name);
|
|
274
|
+
if (missing.length) throw new Error("Toll-free verification submission missing \u2014 " + missing.join(", "));
|
|
275
|
+
const body = new URLSearchParams();
|
|
276
|
+
body.append("BusinessName", businessName);
|
|
277
|
+
body.append("BusinessWebsite", website);
|
|
278
|
+
body.append("NotificationEmail", email);
|
|
279
|
+
body.append("UseCaseCategories", "MARKETING");
|
|
280
|
+
body.append("UseCaseSummary", "Prize-draw entry confirmations, winner notifications and campaign updates to consumers who opted in on this business's Drawbridge-hosted entry form.");
|
|
281
|
+
body.append("ProductionMessageSample", businessName + "\nThanks for entering! We'll text you here about your entry.\nReply STOP to opt out");
|
|
282
|
+
body.append("OptInImageUrls", optInImage);
|
|
283
|
+
body.append("OptInType", "WEB_FORM");
|
|
284
|
+
body.append("MessageVolume", volume || "1,000");
|
|
285
|
+
body.append("TollfreePhoneNumberSid", numberSid);
|
|
286
|
+
const result = await request2({
|
|
287
|
+
method: "POST",
|
|
288
|
+
type: "form",
|
|
289
|
+
url: "https://messaging.twilio.com/v1/Tollfree/Verifications",
|
|
290
|
+
headers: {
|
|
291
|
+
"Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
|
|
292
|
+
},
|
|
293
|
+
body
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
sid: result == null ? void 0 : result.sid,
|
|
297
|
+
status: result == null ? void 0 : result.status
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
fetchVerification: async ({ accountSid, authToken, request: request2 = request, sid }) => {
|
|
301
|
+
if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
|
|
302
|
+
if (!sid) throw new Error("Twilio verification fetch needs the verification sid");
|
|
303
|
+
const result = await request2({
|
|
304
|
+
method: "GET",
|
|
305
|
+
url: "https://messaging.twilio.com/v1/Tollfree/Verifications/" + sid,
|
|
306
|
+
headers: {
|
|
307
|
+
"Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
// TWILIO_APPROVED → verified, TWILIO_REJECTED → rejected, both
|
|
312
|
+
// review states → pending.
|
|
313
|
+
outcome: (result == null ? void 0 : result.status) === "TWILIO_APPROVED" ? "approved" : (result == null ? void 0 : result.status) === "TWILIO_REJECTED" ? "rejected" : "pending",
|
|
314
|
+
reason: (result == null ? void 0 : result.rejection_reason) || null
|
|
315
|
+
};
|
|
178
316
|
}
|
|
179
317
|
};
|
|
180
318
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/twilio.d.cts
CHANGED
|
@@ -70,14 +70,23 @@ const twilio = {
|
|
|
70
70
|
// NUMBER LIFECYCLE, same transport discipline as sms above. Endpoints and
|
|
71
71
|
// parameter names are Twilio's own, from the AvailablePhoneNumbers and
|
|
72
72
|
// IncomingPhoneNumbers resource docs:
|
|
73
|
-
// GET /2010-04-01/Accounts/{AccountSid}/AvailablePhoneNumbers/{CountryCode}/
|
|
74
|
-
// query: SmsEnabled,
|
|
73
|
+
// GET /2010-04-01/Accounts/{AccountSid}/AvailablePhoneNumbers/{CountryCode}/TollFree.json
|
|
74
|
+
// query: SmsEnabled, Contains, PageSize (1-1000)
|
|
75
75
|
// POST /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers.json
|
|
76
76
|
// body: PhoneNumber (E.164), SmsUrl, SmsMethod
|
|
77
77
|
// DELETE /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers/{Sid}.json
|
|
78
78
|
// Basic auth throughout, like Messages.
|
|
79
|
+
//
|
|
80
|
+
// TOLL-FREE, not Local, and that is a carrier-registration decision rather
|
|
81
|
+
// than an inventory one. A US local number may not send A2P SMS without full
|
|
82
|
+
// 10DLC brand-and-campaign registration; a toll-free number needs only
|
|
83
|
+
// Toll-Free Verification — one API submission per number (see
|
|
84
|
+
// submitVerification below), no TCR brand, no campaign fees. Until a
|
|
85
|
+
// verification is approved, Twilio blocks the number's US-bound messages
|
|
86
|
+
// outright — which is why a purchased number starts `pending` and the send
|
|
87
|
+
// path refuses anything but `verified`.
|
|
79
88
|
|
|
80
|
-
searchNumbers : async ({ accountSid,
|
|
89
|
+
searchNumbers : async ({ accountSid, authToken, contains, country = 'US', cursor, limit = 10, request: request$1 = request }) => {
|
|
81
90
|
|
|
82
91
|
if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
|
|
83
92
|
|
|
@@ -93,17 +102,17 @@ const twilio = {
|
|
|
93
102
|
|
|
94
103
|
const result = await request$1({
|
|
95
104
|
method : 'GET',
|
|
96
|
-
url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/AvailablePhoneNumbers/' + country + '/
|
|
105
|
+
url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/AvailablePhoneNumbers/' + country + '/TollFree.json',
|
|
97
106
|
headers : {
|
|
98
107
|
'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
|
|
99
108
|
},
|
|
100
109
|
query : {
|
|
101
110
|
SmsEnabled : true,
|
|
102
111
|
PageSize : limit,
|
|
103
|
-
...( areaCode && { AreaCode : areaCode } ),
|
|
104
112
|
// `Contains` is the vendor's matching-pattern filter — digits
|
|
105
|
-
// match anywhere in the number, so one search box serves "
|
|
106
|
-
//
|
|
113
|
+
// match anywhere in the number, so one search box serves "an 888
|
|
114
|
+
// prefix" and "ends in 7777" alike. (AreaCode is a Local-only
|
|
115
|
+
// filter and went with Local.json.)
|
|
107
116
|
...( contains && { Contains : contains } ),
|
|
108
117
|
...( page && { Page : page } )
|
|
109
118
|
}
|
|
@@ -150,6 +159,73 @@ const twilio = {
|
|
|
150
159
|
|
|
151
160
|
},
|
|
152
161
|
|
|
162
|
+
// THE NUMBER AS TWILIO HOLDS IT NOW. Same IncomingPhoneNumber resource the
|
|
163
|
+
// purchase and release above use (api.twilio.com 2010-04-01, GET
|
|
164
|
+
// /IncomingPhoneNumbers/{sid}.json) — a 404 means the number is no longer
|
|
165
|
+
// on the account, which the health sweep must distinguish from a transient,
|
|
166
|
+
// so it is answered as { gone : true } rather than thrown.
|
|
167
|
+
numberConfig : async ({ accountSid, authToken, request: request$1 = request, sid }) => {
|
|
168
|
+
|
|
169
|
+
if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
|
|
170
|
+
if( ! sid ) throw new Error( 'Twilio config read needs the IncomingPhoneNumber sid' );
|
|
171
|
+
|
|
172
|
+
let result;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
|
|
176
|
+
result = await request$1({
|
|
177
|
+
method : 'GET',
|
|
178
|
+
url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/IncomingPhoneNumbers/' + sid + '.json',
|
|
179
|
+
headers : {
|
|
180
|
+
'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
} catch ( error ) {
|
|
185
|
+
|
|
186
|
+
if( error?.status === 404 ) return { gone : true };
|
|
187
|
+
|
|
188
|
+
throw error;
|
|
189
|
+
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
gone : false,
|
|
194
|
+
number : result?.phone_number,
|
|
195
|
+
smsMethod : result?.sms_method,
|
|
196
|
+
smsUrl : result?.sms_url,
|
|
197
|
+
status : result?.status
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// RE-POINT AN OWNED NUMBER'S INBOUND WEBHOOK — POST on the same resource,
|
|
203
|
+
// the exact fields purchase sets. The health sweep's repair arm.
|
|
204
|
+
updateNumber : async ({ accountSid, authToken, request: request$1 = request, sid, smsUrl }) => {
|
|
205
|
+
|
|
206
|
+
if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
|
|
207
|
+
if( ! sid ) throw new Error( 'Twilio update needs the IncomingPhoneNumber sid' );
|
|
208
|
+
if( ! smsUrl ) throw new Error( 'Twilio update needs the smsUrl to point the number at' );
|
|
209
|
+
|
|
210
|
+
const result = await request$1({
|
|
211
|
+
method : 'POST',
|
|
212
|
+
type : 'form',
|
|
213
|
+
url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/IncomingPhoneNumbers/' + sid + '.json',
|
|
214
|
+
headers : {
|
|
215
|
+
'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
|
|
216
|
+
},
|
|
217
|
+
body : {
|
|
218
|
+
SmsUrl : smsUrl,
|
|
219
|
+
SmsMethod : 'POST'
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
smsUrl : result?.sms_url
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
},
|
|
228
|
+
|
|
153
229
|
releaseNumber : async ({ accountSid, authToken, request: request$1 = request, sid }) => {
|
|
154
230
|
|
|
155
231
|
if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
|
|
@@ -163,6 +239,111 @@ const twilio = {
|
|
|
163
239
|
}
|
|
164
240
|
});
|
|
165
241
|
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// TOLL-FREE VERIFICATION — the carrier registration that lets a purchased
|
|
245
|
+
// number actually deliver. From the Tollfree Verification resource docs
|
|
246
|
+
// (messaging.twilio.com, docs/messaging/api/tollfree-verification-resource):
|
|
247
|
+
// POST /v1/Tollfree/Verifications
|
|
248
|
+
// required: BusinessName, BusinessWebsite, NotificationEmail,
|
|
249
|
+
// UseCaseCategories (array), UseCaseSummary,
|
|
250
|
+
// ProductionMessageSample, OptInImageUrls (array), OptInType,
|
|
251
|
+
// MessageVolume, TollfreePhoneNumberSid (PN…)
|
|
252
|
+
// GET /v1/Tollfree/Verifications/{Sid}
|
|
253
|
+
// status: PENDING_REVIEW | IN_REVIEW | TWILIO_APPROVED |
|
|
254
|
+
// TWILIO_REJECTED, with rejection_reason beside a rejection.
|
|
255
|
+
// Basic auth, form-encoded, like everything above. There is no status
|
|
256
|
+
// callback on this resource — the caller polls the fetch.
|
|
257
|
+
//
|
|
258
|
+
// THE END BUSINESS'S DETAILS, never the platform's. Twilio rejects ISV
|
|
259
|
+
// submissions carrying the ISV's own identity (Toll-Free Verification for
|
|
260
|
+
// ISVs, support.twilio.com) — so businessName, website and email here are
|
|
261
|
+
// the merchant's, collected at purchase.
|
|
262
|
+
//
|
|
263
|
+
// The use-case fields are PLATFORM facts and live here beside the message
|
|
264
|
+
// composition for the same reason it does: how Drawbridge collects opt-in
|
|
265
|
+
// (its own hosted entry forms) and what its messages look like are
|
|
266
|
+
// properties of sending through Drawbridge, not of any one merchant.
|
|
267
|
+
submitVerification : async ({
|
|
268
|
+
accountSid,
|
|
269
|
+
authToken,
|
|
270
|
+
businessName,
|
|
271
|
+
email,
|
|
272
|
+
numberSid,
|
|
273
|
+
optInImage,
|
|
274
|
+
request: request$1 = request,
|
|
275
|
+
volume,
|
|
276
|
+
website
|
|
277
|
+
}) => {
|
|
278
|
+
|
|
279
|
+
const missing = Object.entries({ accountSid, authToken, businessName, email, numberSid, optInImage, website })
|
|
280
|
+
.filter( ( [ , value ] ) => ! value )
|
|
281
|
+
.map( ( [ name ] ) => name );
|
|
282
|
+
|
|
283
|
+
if( missing.length ) throw new Error( 'Toll-free verification submission missing — ' + missing.join( ', ' ) );
|
|
284
|
+
|
|
285
|
+
// URLSearchParams rather than an object: UseCaseCategories and
|
|
286
|
+
// OptInImageUrls are array parameters, which Twilio takes as REPEATED
|
|
287
|
+
// form keys — an object body would comma-join them into one value.
|
|
288
|
+
const body = new URLSearchParams();
|
|
289
|
+
|
|
290
|
+
body.append( 'BusinessName', businessName );
|
|
291
|
+
body.append( 'BusinessWebsite', website );
|
|
292
|
+
body.append( 'NotificationEmail', email );
|
|
293
|
+
body.append( 'UseCaseCategories', 'MARKETING' );
|
|
294
|
+
body.append( 'UseCaseSummary', 'Prize-draw entry confirmations, winner notifications and campaign updates to consumers who opted in on this business\'s Drawbridge-hosted entry form.' );
|
|
295
|
+
// Mirrors the sms() composition above — newline-separated, brand first,
|
|
296
|
+
// opt-out line appended — so the reviewed sample is the sent shape.
|
|
297
|
+
body.append( 'ProductionMessageSample', businessName + '\nThanks for entering! We\'ll text you here about your entry.\nReply STOP to opt out' );
|
|
298
|
+
body.append( 'OptInImageUrls', optInImage );
|
|
299
|
+
// The entry form is a web form; the phone field is beside the consent
|
|
300
|
+
// language on the merchant's campaign page.
|
|
301
|
+
body.append( 'OptInType', 'WEB_FORM' );
|
|
302
|
+
body.append( 'MessageVolume', volume || '1,000' );
|
|
303
|
+
body.append( 'TollfreePhoneNumberSid', numberSid );
|
|
304
|
+
|
|
305
|
+
const result = await request$1({
|
|
306
|
+
method : 'POST',
|
|
307
|
+
type : 'form',
|
|
308
|
+
url : 'https://messaging.twilio.com/v1/Tollfree/Verifications',
|
|
309
|
+
headers : {
|
|
310
|
+
'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
|
|
311
|
+
},
|
|
312
|
+
body
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
sid : result?.sid,
|
|
317
|
+
status : result?.status
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
fetchVerification : async ({ accountSid, authToken, request: request$1 = request, sid }) => {
|
|
323
|
+
|
|
324
|
+
if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
|
|
325
|
+
if( ! sid ) throw new Error( 'Twilio verification fetch needs the verification sid' );
|
|
326
|
+
|
|
327
|
+
const result = await request$1({
|
|
328
|
+
method : 'GET',
|
|
329
|
+
url : 'https://messaging.twilio.com/v1/Tollfree/Verifications/' + sid,
|
|
330
|
+
headers : {
|
|
331
|
+
'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// The caller-facing shape is ours; the vendor's vocabulary stays here.
|
|
336
|
+
return {
|
|
337
|
+
// TWILIO_APPROVED → verified, TWILIO_REJECTED → rejected, both
|
|
338
|
+
// review states → pending.
|
|
339
|
+
outcome : result?.status === 'TWILIO_APPROVED'
|
|
340
|
+
? 'approved'
|
|
341
|
+
: result?.status === 'TWILIO_REJECTED'
|
|
342
|
+
? 'rejected'
|
|
343
|
+
: 'pending',
|
|
344
|
+
reason : result?.rejection_reason || null
|
|
345
|
+
};
|
|
346
|
+
|
|
166
347
|
}
|
|
167
348
|
|
|
168
349
|
};
|