@drawbridge/drawbridge-utils 0.0.128 → 0.0.130

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.
@@ -29,6 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // lib/oauth/server.js
30
30
  var server_exports = {};
31
31
  __export(server_exports, {
32
+ ROTATION_GRACE_MS: () => ROTATION_GRACE_MS,
32
33
  createClient: () => createClient,
33
34
  createOAuthModel: () => createOAuthModel,
34
35
  createOAuthServer: () => createOAuthServer
@@ -147,6 +148,7 @@ var generateToken = () => generate(32, "base64url");
147
148
 
148
149
  // lib/oauth/server.js
149
150
  var SUPPORTED_GRANTS = ["authorization_code", "client_credentials", "refresh_token"];
151
+ var ROTATION_GRACE_MS = 60 * 1e3;
150
152
  var createOAuthModel = ({ controller }) => ({
151
153
  getClient: async (clientId, clientSecret) => {
152
154
  var _a;
@@ -328,12 +330,16 @@ var createOAuthModel = ({ controller }) => ({
328
330
  const record = await controller.get({
329
331
  collection: "token",
330
332
  query: {
331
- revoked: false,
332
333
  tokenHash,
333
334
  type: tokenTypes.refresh
334
335
  }
335
336
  });
336
337
  if (!record) return false;
338
+ if (record.revoked) {
339
+ const rotatedAt = record.rotatedAt ? new Date(record.rotatedAt).getTime() : null;
340
+ if (!rotatedAt || Date.now() - rotatedAt > ROTATION_GRACE_MS) return false;
341
+ }
342
+ ;
337
343
  return {
338
344
  client: { id: record.client },
339
345
  refreshToken,
@@ -345,6 +351,15 @@ var createOAuthModel = ({ controller }) => ({
345
351
  revokeToken: async (token) => {
346
352
  const rawToken = token.refreshToken ?? token.accessToken;
347
353
  const tokenHash = hashToken(rawToken);
354
+ await controller.update({
355
+ collection: "token",
356
+ data: {
357
+ $set: {
358
+ rotatedAt: /* @__PURE__ */ new Date()
359
+ }
360
+ },
361
+ query: { revoked: { $ne: true }, tokenHash }
362
+ });
348
363
  await controller.update({
349
364
  collection: "token",
350
365
  data: {
@@ -443,6 +458,7 @@ var createClient = async ({
443
458
  };
444
459
  // Annotate the CommonJS export names for ESM import in node:
445
460
  0 && (module.exports = {
461
+ ROTATION_GRACE_MS,
446
462
  createClient,
447
463
  createOAuthModel,
448
464
  createOAuthServer
@@ -34,6 +34,11 @@ import 'crypto';
34
34
  // ever need to, store `grants` on the oauth doc and read it here.
35
35
  const SUPPORTED_GRANTS = [ 'authorization_code', 'client_credentials', 'refresh_token' ];
36
36
 
37
+ // How long a just-rotated refresh token keeps answering (see getRefreshToken).
38
+ // Long enough for the parallel requests of one navigation to all land; short
39
+ // enough that a stolen token replayed later is refused like any other.
40
+ const ROTATION_GRACE_MS = 60 * 1000;
41
+
37
42
  const createOAuthModel = ({ controller }) => ({
38
43
 
39
44
  getClient : async ( clientId, clientSecret ) => {
@@ -258,7 +263,6 @@ const createOAuthModel = ({ controller }) => ({
258
263
  const record = await controller.get({
259
264
  collection : 'token',
260
265
  query : {
261
- revoked : false,
262
266
  tokenHash,
263
267
  type : tokenTypes.refresh
264
268
  }
@@ -266,6 +270,25 @@ const createOAuthModel = ({ controller }) => ({
266
270
 
267
271
  if( ! record ) return false;
268
272
 
273
+ // ROTATION GRACE. Refresh tokens are single-use, and the dashboard's
274
+ // middleware refreshes on whichever request arrives first — Next fires
275
+ // several in parallel, so the losers present a token the winner just
276
+ // consumed. For a short window a JUST-ROTATED token still answers,
277
+ // so a race mints an extra pair instead of ending the session.
278
+ //
279
+ // `rotatedAt` is written ONCE, on the transition into revoked (see
280
+ // revokeToken) — a replay inside the window cannot refresh it, so the
281
+ // window closes sixty seconds after the first rotation no matter how
282
+ // often the old token is replayed. A token revoked WITHOUT rotatedAt
283
+ // was killed on purpose — signout, an admin, a deleted client — and
284
+ // gets no grace: those revocations must be absolute.
285
+ if( record.revoked ){
286
+
287
+ const rotatedAt = record.rotatedAt ? new Date( record.rotatedAt ).getTime() : null;
288
+
289
+ if( ! rotatedAt || ( Date.now() - rotatedAt ) > ROTATION_GRACE_MS ) return false;
290
+
291
+ }
269
292
  return {
270
293
  client : { id : record.client },
271
294
  refreshToken,
@@ -281,6 +304,20 @@ const createOAuthModel = ({ controller }) => ({
281
304
  const rawToken = token.refreshToken ?? token.accessToken;
282
305
  const tokenHash = hashToken( rawToken );
283
306
 
307
+ // rotatedAt marks that this revocation IS a rotation — the only kind
308
+ // getRefreshToken graces. Guarded on the transition (revoked $ne true)
309
+ // so a grace-window replay re-revoking the same row cannot move the
310
+ // stamp and hold the window open.
311
+ await controller.update({
312
+ collection : 'token',
313
+ data : {
314
+ $set : {
315
+ rotatedAt : new Date()
316
+ }
317
+ },
318
+ query : { revoked : { $ne : true }, tokenHash }
319
+ });
320
+
284
321
  await controller.update({
285
322
  collection : 'token',
286
323
  data : {
@@ -411,4 +448,4 @@ const createClient = async ({
411
448
 
412
449
  };
413
450
 
414
- export { createClient, createOAuthModel, createOAuthServer };
451
+ export { ROTATION_GRACE_MS, createClient, createOAuthModel, createOAuthServer };
@@ -34,6 +34,11 @@ import 'crypto';
34
34
  // ever need to, store `grants` on the oauth doc and read it here.
35
35
  const SUPPORTED_GRANTS = [ 'authorization_code', 'client_credentials', 'refresh_token' ];
36
36
 
37
+ // How long a just-rotated refresh token keeps answering (see getRefreshToken).
38
+ // Long enough for the parallel requests of one navigation to all land; short
39
+ // enough that a stolen token replayed later is refused like any other.
40
+ const ROTATION_GRACE_MS = 60 * 1000;
41
+
37
42
  const createOAuthModel = ({ controller }) => ({
38
43
 
39
44
  getClient : async ( clientId, clientSecret ) => {
@@ -258,7 +263,6 @@ const createOAuthModel = ({ controller }) => ({
258
263
  const record = await controller.get({
259
264
  collection : 'token',
260
265
  query : {
261
- revoked : false,
262
266
  tokenHash,
263
267
  type : tokenTypes.refresh
264
268
  }
@@ -266,6 +270,25 @@ const createOAuthModel = ({ controller }) => ({
266
270
 
267
271
  if( ! record ) return false;
268
272
 
273
+ // ROTATION GRACE. Refresh tokens are single-use, and the dashboard's
274
+ // middleware refreshes on whichever request arrives first — Next fires
275
+ // several in parallel, so the losers present a token the winner just
276
+ // consumed. For a short window a JUST-ROTATED token still answers,
277
+ // so a race mints an extra pair instead of ending the session.
278
+ //
279
+ // `rotatedAt` is written ONCE, on the transition into revoked (see
280
+ // revokeToken) — a replay inside the window cannot refresh it, so the
281
+ // window closes sixty seconds after the first rotation no matter how
282
+ // often the old token is replayed. A token revoked WITHOUT rotatedAt
283
+ // was killed on purpose — signout, an admin, a deleted client — and
284
+ // gets no grace: those revocations must be absolute.
285
+ if( record.revoked ){
286
+
287
+ const rotatedAt = record.rotatedAt ? new Date( record.rotatedAt ).getTime() : null;
288
+
289
+ if( ! rotatedAt || ( Date.now() - rotatedAt ) > ROTATION_GRACE_MS ) return false;
290
+
291
+ }
269
292
  return {
270
293
  client : { id : record.client },
271
294
  refreshToken,
@@ -281,6 +304,20 @@ const createOAuthModel = ({ controller }) => ({
281
304
  const rawToken = token.refreshToken ?? token.accessToken;
282
305
  const tokenHash = hashToken( rawToken );
283
306
 
307
+ // rotatedAt marks that this revocation IS a rotation — the only kind
308
+ // getRefreshToken graces. Guarded on the transition (revoked $ne true)
309
+ // so a grace-window replay re-revoking the same row cannot move the
310
+ // stamp and hold the window open.
311
+ await controller.update({
312
+ collection : 'token',
313
+ data : {
314
+ $set : {
315
+ rotatedAt : new Date()
316
+ }
317
+ },
318
+ query : { revoked : { $ne : true }, tokenHash }
319
+ });
320
+
284
321
  await controller.update({
285
322
  collection : 'token',
286
323
  data : {
@@ -411,4 +448,4 @@ const createClient = async ({
411
448
 
412
449
  };
413
450
 
414
- export { createClient, createOAuthModel, createOAuthServer };
451
+ export { ROTATION_GRACE_MS, createClient, createOAuthModel, createOAuthServer };
@@ -112,6 +112,7 @@ var generateToken = () => generate(32, "base64url");
112
112
 
113
113
  // lib/oauth/server.js
114
114
  var SUPPORTED_GRANTS = ["authorization_code", "client_credentials", "refresh_token"];
115
+ var ROTATION_GRACE_MS = 60 * 1e3;
115
116
  var createOAuthModel = ({ controller }) => ({
116
117
  getClient: async (clientId, clientSecret) => {
117
118
  var _a;
@@ -293,12 +294,16 @@ var createOAuthModel = ({ controller }) => ({
293
294
  const record = await controller.get({
294
295
  collection: "token",
295
296
  query: {
296
- revoked: false,
297
297
  tokenHash,
298
298
  type: tokenTypes.refresh
299
299
  }
300
300
  });
301
301
  if (!record) return false;
302
+ if (record.revoked) {
303
+ const rotatedAt = record.rotatedAt ? new Date(record.rotatedAt).getTime() : null;
304
+ if (!rotatedAt || Date.now() - rotatedAt > ROTATION_GRACE_MS) return false;
305
+ }
306
+ ;
302
307
  return {
303
308
  client: { id: record.client },
304
309
  refreshToken,
@@ -310,6 +315,15 @@ var createOAuthModel = ({ controller }) => ({
310
315
  revokeToken: async (token) => {
311
316
  const rawToken = token.refreshToken ?? token.accessToken;
312
317
  const tokenHash = hashToken(rawToken);
318
+ await controller.update({
319
+ collection: "token",
320
+ data: {
321
+ $set: {
322
+ rotatedAt: /* @__PURE__ */ new Date()
323
+ }
324
+ },
325
+ query: { revoked: { $ne: true }, tokenHash }
326
+ });
313
327
  await controller.update({
314
328
  collection: "token",
315
329
  data: {
@@ -407,6 +421,7 @@ var createClient = async ({
407
421
  };
408
422
  };
409
423
  export {
424
+ ROTATION_GRACE_MS,
410
425
  createClient,
411
426
  createOAuthModel,
412
427
  createOAuthServer
package/dist/pricing.cjs CHANGED
@@ -40,6 +40,7 @@ __export(pricing_exports, {
40
40
  channels: () => channels,
41
41
  conversionRate: () => conversionRate,
42
42
  free: () => free,
43
+ numbers: () => numbers,
43
44
  plans: () => plans,
44
45
  resolvePlan: () => resolvePlan,
45
46
  scrape: () => scrape,
@@ -883,6 +884,11 @@ var channels = {
883
884
  includedInAllowance: false
884
885
  }
885
886
  };
887
+ var numbers = {
888
+ sms: {
889
+ monthlyCents: 1500
890
+ }
891
+ };
886
892
  // Annotate the CommonJS export names for ESM import in node:
887
893
  0 && (module.exports = {
888
894
  LOCAL_FLAT_CENTS,
@@ -896,6 +902,7 @@ var channels = {
896
902
  channels,
897
903
  conversionRate,
898
904
  free,
905
+ numbers,
899
906
  plans,
900
907
  resolvePlan,
901
908
  scrape,
@@ -189,4 +189,20 @@ const channels = {
189
189
  }
190
190
  };
191
191
 
192
- export { channels, sending };
192
+ // The dedicated SMS number — a RECURRING SUBSCRIPTION ITEM, not an action:
193
+ // the carrier bills for holding the number whether or not it sends, so it
194
+ // joins the organization's subscription as its own line and leaves when the
195
+ // number is released. CENTS per month, like every stored money value.
196
+ //
197
+ // $15 was DECIDED in the platform-sender pricing session (2026-08-30): it
198
+ // covers the worst case of number rental (~$1-2) + carrier campaign fees
199
+ // (~$2-15) + amortized A2P registration, in line with what SMS platforms
200
+ // charge for a dedicated number. Changing it is this one line plus the
201
+ // matching Stripe Price.
202
+ const numbers = {
203
+ sms : {
204
+ monthlyCents : 1500
205
+ }
206
+ };
207
+
208
+ export { channels, numbers, sending };
package/dist/pricing.d.ts CHANGED
@@ -189,4 +189,20 @@ const channels = {
189
189
  }
190
190
  };
191
191
 
192
- export { channels, sending };
192
+ // The dedicated SMS number — a RECURRING SUBSCRIPTION ITEM, not an action:
193
+ // the carrier bills for holding the number whether or not it sends, so it
194
+ // joins the organization's subscription as its own line and leaves when the
195
+ // number is released. CENTS per month, like every stored money value.
196
+ //
197
+ // $15 was DECIDED in the platform-sender pricing session (2026-08-30): it
198
+ // covers the worst case of number rental (~$1-2) + carrier campaign fees
199
+ // (~$2-15) + amortized A2P registration, in line with what SMS platforms
200
+ // charge for a dedicated number. Changing it is this one line plus the
201
+ // matching Stripe Price.
202
+ const numbers = {
203
+ sms : {
204
+ monthlyCents : 1500
205
+ }
206
+ };
207
+
208
+ export { channels, numbers, sending };
package/dist/pricing.js CHANGED
@@ -833,6 +833,11 @@ var channels = {
833
833
  includedInAllowance: false
834
834
  }
835
835
  };
836
+ var numbers = {
837
+ sms: {
838
+ monthlyCents: 1500
839
+ }
840
+ };
836
841
  export {
837
842
  LOCAL_FLAT_CENTS,
838
843
  MARKUP,
@@ -845,6 +850,7 @@ export {
845
850
  channels,
846
851
  conversionRate,
847
852
  free,
853
+ numbers,
848
854
  plans,
849
855
  resolvePlan,
850
856
  scrape,
package/dist/twilio.cjs CHANGED
@@ -113,8 +113,9 @@ var twilio = {
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
- searchNumbers: async ({ accountSid, areaCode, authToken, country = "US", limit = 10, request: request2 = request }) => {
116
+ searchNumbers: async ({ accountSid, areaCode, authToken, contains, country = "US", cursor, limit = 10, request: request2 = request }) => {
117
117
  if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
118
+ const [page, token] = String(cursor || "").split("~");
118
119
  const result = await request2({
119
120
  method: "GET",
120
121
  url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/AvailablePhoneNumbers/" + country + "/Local.json",
@@ -124,14 +125,24 @@ var twilio = {
124
125
  query: {
125
126
  SmsEnabled: true,
126
127
  PageSize: limit,
127
- ...areaCode && { AreaCode: areaCode }
128
+ ...areaCode && { AreaCode: areaCode },
129
+ // `Contains` is the vendor's matching-pattern filter — digits
130
+ // match anywhere in the number, so one search box serves "my
131
+ // area code" and "ends in 7777" alike.
132
+ ...contains && { Contains: contains },
133
+ ...token && { Page: page, PageToken: token }
128
134
  }
129
135
  });
130
- return ((result == null ? void 0 : result.available_phone_numbers) || []).map((entry) => ({
131
- friendly: entry.friendly_name,
132
- locality: entry.locality || entry.region || null,
133
- number: entry.phone_number
134
- }));
136
+ const nextQuery = (result == null ? void 0 : result.next_page_uri) ? new URLSearchParams(String(result.next_page_uri).split("?")[1] || "") : null;
137
+ const nextToken = nextQuery == null ? void 0 : nextQuery.get("PageToken");
138
+ return {
139
+ cursor: nextToken ? (nextQuery.get("Page") || "") + "~" + nextToken : null,
140
+ items: ((result == null ? void 0 : result.available_phone_numbers) || []).map((entry) => ({
141
+ friendly: entry.friendly_name,
142
+ locality: entry.locality || entry.region || null,
143
+ number: entry.phone_number
144
+ }))
145
+ };
135
146
  },
136
147
  purchaseNumber: async ({ accountSid, authToken, number, request: request2 = request, smsUrl }) => {
137
148
  if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
package/dist/twilio.d.cts CHANGED
@@ -77,10 +77,16 @@ const twilio = {
77
77
  // DELETE /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers/{Sid}.json
78
78
  // Basic auth throughout, like Messages.
79
79
 
80
- searchNumbers : async ({ accountSid, areaCode, authToken, country = 'US', limit = 10, request: request$1 = request }) => {
80
+ searchNumbers : async ({ accountSid, areaCode, authToken, contains, country = 'US', cursor, limit = 10, request: request$1 = request }) => {
81
81
 
82
82
  if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
83
83
 
84
+ // The vendor pages with Page + PageToken and answers next_page_uri
85
+ // (AvailablePhoneNumbers resource docs). Ours is one opaque cursor —
86
+ // `<Page>~<PageToken>` — so the caller never learns the vendor's two
87
+ // halves and cannot recombine them wrong.
88
+ const [ page, token ] = String( cursor || '' ).split( '~' );
89
+
84
90
  const result = await request$1({
85
91
  method : 'GET',
86
92
  url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/AvailablePhoneNumbers/' + country + '/Local.json',
@@ -90,16 +96,30 @@ const twilio = {
90
96
  query : {
91
97
  SmsEnabled : true,
92
98
  PageSize : limit,
93
- ...( areaCode && { AreaCode : areaCode } )
99
+ ...( areaCode && { AreaCode : areaCode } ),
100
+ // `Contains` is the vendor's matching-pattern filter — digits
101
+ // match anywhere in the number, so one search box serves "my
102
+ // area code" and "ends in 7777" alike.
103
+ ...( contains && { Contains : contains } ),
104
+ ...( token && { Page : page, PageToken : token } )
94
105
  }
95
106
  });
96
107
 
108
+ const nextQuery = result?.next_page_uri
109
+ ? new URLSearchParams( String( result.next_page_uri ).split( '?' )[ 1 ] || '' )
110
+ : null;
111
+
112
+ const nextToken = nextQuery?.get( 'PageToken' );
113
+
97
114
  // The caller-facing shape is ours; the vendor's field names stay here.
98
- return ( result?.available_phone_numbers || [] ).map( ( entry ) => ({
99
- friendly : entry.friendly_name,
100
- locality : entry.locality || entry.region || null,
101
- number : entry.phone_number
102
- }) );
115
+ return {
116
+ cursor : nextToken ? ( nextQuery.get( 'Page' ) || '' ) + '~' + nextToken : null,
117
+ items : ( result?.available_phone_numbers || [] ).map( ( entry ) => ({
118
+ friendly : entry.friendly_name,
119
+ locality : entry.locality || entry.region || null,
120
+ number : entry.phone_number
121
+ }) )
122
+ };
103
123
 
104
124
  },
105
125
 
package/dist/twilio.d.ts CHANGED
@@ -77,10 +77,16 @@ const twilio = {
77
77
  // DELETE /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers/{Sid}.json
78
78
  // Basic auth throughout, like Messages.
79
79
 
80
- searchNumbers : async ({ accountSid, areaCode, authToken, country = 'US', limit = 10, request: request$1 = request }) => {
80
+ searchNumbers : async ({ accountSid, areaCode, authToken, contains, country = 'US', cursor, limit = 10, request: request$1 = request }) => {
81
81
 
82
82
  if( ! accountSid || ! authToken ) throw new Error( 'Twilio credentials missing — accountSid, authToken' );
83
83
 
84
+ // The vendor pages with Page + PageToken and answers next_page_uri
85
+ // (AvailablePhoneNumbers resource docs). Ours is one opaque cursor —
86
+ // `<Page>~<PageToken>` — so the caller never learns the vendor's two
87
+ // halves and cannot recombine them wrong.
88
+ const [ page, token ] = String( cursor || '' ).split( '~' );
89
+
84
90
  const result = await request$1({
85
91
  method : 'GET',
86
92
  url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/AvailablePhoneNumbers/' + country + '/Local.json',
@@ -90,16 +96,30 @@ const twilio = {
90
96
  query : {
91
97
  SmsEnabled : true,
92
98
  PageSize : limit,
93
- ...( areaCode && { AreaCode : areaCode } )
99
+ ...( areaCode && { AreaCode : areaCode } ),
100
+ // `Contains` is the vendor's matching-pattern filter — digits
101
+ // match anywhere in the number, so one search box serves "my
102
+ // area code" and "ends in 7777" alike.
103
+ ...( contains && { Contains : contains } ),
104
+ ...( token && { Page : page, PageToken : token } )
94
105
  }
95
106
  });
96
107
 
108
+ const nextQuery = result?.next_page_uri
109
+ ? new URLSearchParams( String( result.next_page_uri ).split( '?' )[ 1 ] || '' )
110
+ : null;
111
+
112
+ const nextToken = nextQuery?.get( 'PageToken' );
113
+
97
114
  // The caller-facing shape is ours; the vendor's field names stay here.
98
- return ( result?.available_phone_numbers || [] ).map( ( entry ) => ({
99
- friendly : entry.friendly_name,
100
- locality : entry.locality || entry.region || null,
101
- number : entry.phone_number
102
- }) );
115
+ return {
116
+ cursor : nextToken ? ( nextQuery.get( 'Page' ) || '' ) + '~' + nextToken : null,
117
+ items : ( result?.available_phone_numbers || [] ).map( ( entry ) => ({
118
+ friendly : entry.friendly_name,
119
+ locality : entry.locality || entry.region || null,
120
+ number : entry.phone_number
121
+ }) )
122
+ };
103
123
 
104
124
  },
105
125
 
package/dist/twilio.js CHANGED
@@ -88,8 +88,9 @@ var twilio = {
88
88
  // body: PhoneNumber (E.164), SmsUrl, SmsMethod
89
89
  // DELETE /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers/{Sid}.json
90
90
  // Basic auth throughout, like Messages.
91
- searchNumbers: async ({ accountSid, areaCode, authToken, country = "US", limit = 10, request: request2 = request }) => {
91
+ searchNumbers: async ({ accountSid, areaCode, authToken, contains, country = "US", cursor, limit = 10, request: request2 = request }) => {
92
92
  if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
93
+ const [page, token] = String(cursor || "").split("~");
93
94
  const result = await request2({
94
95
  method: "GET",
95
96
  url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/AvailablePhoneNumbers/" + country + "/Local.json",
@@ -99,14 +100,24 @@ var twilio = {
99
100
  query: {
100
101
  SmsEnabled: true,
101
102
  PageSize: limit,
102
- ...areaCode && { AreaCode: areaCode }
103
+ ...areaCode && { AreaCode: areaCode },
104
+ // `Contains` is the vendor's matching-pattern filter — digits
105
+ // match anywhere in the number, so one search box serves "my
106
+ // area code" and "ends in 7777" alike.
107
+ ...contains && { Contains: contains },
108
+ ...token && { Page: page, PageToken: token }
103
109
  }
104
110
  });
105
- return ((result == null ? void 0 : result.available_phone_numbers) || []).map((entry) => ({
106
- friendly: entry.friendly_name,
107
- locality: entry.locality || entry.region || null,
108
- number: entry.phone_number
109
- }));
111
+ const nextQuery = (result == null ? void 0 : result.next_page_uri) ? new URLSearchParams(String(result.next_page_uri).split("?")[1] || "") : null;
112
+ const nextToken = nextQuery == null ? void 0 : nextQuery.get("PageToken");
113
+ return {
114
+ cursor: nextToken ? (nextQuery.get("Page") || "") + "~" + nextToken : null,
115
+ items: ((result == null ? void 0 : result.available_phone_numbers) || []).map((entry) => ({
116
+ friendly: entry.friendly_name,
117
+ locality: entry.locality || entry.region || null,
118
+ number: entry.phone_number
119
+ }))
120
+ };
110
121
  },
111
122
  purchaseNumber: async ({ accountSid, authToken, number, request: request2 = request, smsUrl }) => {
112
123
  if (!accountSid || !authToken) throw new Error("Twilio credentials missing \u2014 accountSid, authToken");
package/package.json CHANGED
@@ -215,5 +215,5 @@
215
215
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
216
216
  },
217
217
  "types": "dist/index.d.ts",
218
- "version": "0.0.128"
218
+ "version": "0.0.130"
219
219
  }