letsfg 2026.5.72 → 2026.5.73
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/README.md +283 -281
- package/dist/{chunk-GO3FXQXC.mjs → chunk-F5BBI6XX.mjs} +134 -20
- package/dist/cli.js +158 -59
- package/dist/cli.mjs +25 -40
- package/dist/index.d.mts +92 -14
- package/dist/index.d.ts +92 -14
- package/dist/index.js +134 -20
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -1671,13 +1671,23 @@ var LetsFG = class {
|
|
|
1671
1671
|
return Array.isArray(data) ? data : data.locations || [];
|
|
1672
1672
|
}
|
|
1673
1673
|
/**
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1674
|
+
* RETIRED 2026-09-08. Throws instead of calling the server.
|
|
1675
|
+
*
|
|
1676
|
+
* There is no unlock step on any lane. Unlock existed to confirm a live price
|
|
1677
|
+
* before charging; booking now HOLDS the fare on the connected payment method
|
|
1678
|
+
* and captures only once a real airline PNR exists, so a fare that moved
|
|
1679
|
+
* cannot become a charge for a ticket you did not get. If it moves at
|
|
1680
|
+
* checkout you get a `price_change` question to accept or decline instead.
|
|
1681
|
+
*
|
|
1682
|
+
* Kept as a method, and throwing locally rather than making the request, so an
|
|
1683
|
+
* older caller gets one clear sentence at the line that is actually wrong —
|
|
1684
|
+
* not a 410 body to decode, and not a TypeError somewhere else.
|
|
1677
1685
|
*/
|
|
1678
|
-
async unlock(
|
|
1679
|
-
|
|
1680
|
-
|
|
1686
|
+
async unlock(_offerId) {
|
|
1687
|
+
throw new LetsFGError(
|
|
1688
|
+
"unlock() was retired on 2026-09-08 and the endpoint answers 410 Gone. There is no unlock step: call book() directly. The fare is held on the connected payment method and captured only against a real airline PNR. See https://letsfg.co/developers/api/docs",
|
|
1689
|
+
410
|
|
1690
|
+
);
|
|
1681
1691
|
}
|
|
1682
1692
|
/**
|
|
1683
1693
|
* Book a flight.
|
|
@@ -1688,9 +1698,14 @@ var LetsFG = class {
|
|
|
1688
1698
|
* complete, { ok, booked: false, booking_url } — hand the link to the user,
|
|
1689
1699
|
* nothing was charged.
|
|
1690
1700
|
*
|
|
1691
|
-
* Developer API (X-API-Key):
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1701
|
+
* Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
|
|
1702
|
+
* REQUIRED — an offer is bookable only inside the search that produced it.
|
|
1703
|
+
* The connected Revolut method is HELD, not charged; a LetsFG booking agent
|
|
1704
|
+
* buys the ticket and the hold is captured only against a real airline PNR.
|
|
1705
|
+
* Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
|
|
1706
|
+
* poll getBooking(bookingId) until `terminal`, or use bookAndWait().
|
|
1707
|
+
* Always provide idempotencyKey: a retry with the same key returns the
|
|
1708
|
+
* existing booking instead of opening a second hold on the card.
|
|
1694
1709
|
*/
|
|
1695
1710
|
async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
|
|
1696
1711
|
this.requireAuth();
|
|
@@ -1711,15 +1726,92 @@ var LetsFG = class {
|
|
|
1711
1726
|
});
|
|
1712
1727
|
}
|
|
1713
1728
|
this.requireApiKey();
|
|
1729
|
+
if (!searchId) {
|
|
1730
|
+
throw new LetsFGError(
|
|
1731
|
+
"searchId is required to book on the Developer API \u2014 pass the search_id from search()'s result. An offer can only be booked inside the search that produced it. (Before 2026-09-08 this argument was ignored on this path.)",
|
|
1732
|
+
400
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
const pax = passengers.map((p) => ({ ...p }));
|
|
1736
|
+
if (contactPhone && pax.length && !pax[0].phone_number) pax[0].phone_number = contactPhone;
|
|
1714
1737
|
const body = {
|
|
1738
|
+
search_id: searchId,
|
|
1715
1739
|
offer_id: offerId,
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
contact_email: contactEmail,
|
|
1719
|
-
contact_phone: contactPhone
|
|
1740
|
+
passengers: pax,
|
|
1741
|
+
contact_email: contactEmail
|
|
1720
1742
|
};
|
|
1721
1743
|
if (idempotencyKey) body.idempotency_key = idempotencyKey;
|
|
1722
|
-
return this.post("/developers/api/v1/
|
|
1744
|
+
return this.post("/developers/api/v1/flights/book", body);
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Poll a Developer API flight booking.
|
|
1748
|
+
*
|
|
1749
|
+
* Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
|
|
1750
|
+
* knows you are still there, which is what keeps a booking paused on a
|
|
1751
|
+
* question alive — so do not back off to minutes.
|
|
1752
|
+
*
|
|
1753
|
+
* States: authorised, card_issued, booking_in_progress, awaiting_settlement,
|
|
1754
|
+
* then completed (with `pnr` and `charged_amount`), failed (hold released,
|
|
1755
|
+
* nothing charged) or needs_attention (a human at LetsFG is on it — do not
|
|
1756
|
+
* book again).
|
|
1757
|
+
*/
|
|
1758
|
+
async getBooking(bookingId) {
|
|
1759
|
+
this.requireApiKey();
|
|
1760
|
+
return this.getWithAuth(
|
|
1761
|
+
`/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Answer the open `question` on a booking.
|
|
1766
|
+
*
|
|
1767
|
+
* Echo the question's `round`. A stale round is refused with 409 rather than
|
|
1768
|
+
* guessed at, so an answer to an old question can never be applied to a new
|
|
1769
|
+
* one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
|
|
1770
|
+
* { confirm: true } or { skip: true } — declining an extra still completes
|
|
1771
|
+
* the booking, without it.
|
|
1772
|
+
*/
|
|
1773
|
+
async answerBooking(bookingId, round, answer = {}) {
|
|
1774
|
+
this.requireApiKey();
|
|
1775
|
+
return this.post(
|
|
1776
|
+
`/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}/answer`,
|
|
1777
|
+
{ round, ...answer }
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Book and poll to a terminal state. Mirrors bookHotelAndWait().
|
|
1782
|
+
*
|
|
1783
|
+
* Blocks for as long as the booking takes (4–11 minutes typically), so use
|
|
1784
|
+
* book() + getBooking() instead if your caller has a request timeout.
|
|
1785
|
+
*
|
|
1786
|
+
* `onQuestion` returns the answer for answerBooking(). Without it, a fare
|
|
1787
|
+
* increase is ACCEPTED and a paid extra is DECLINED — the conservative
|
|
1788
|
+
* reading of "the traveller asked for this flight".
|
|
1789
|
+
*/
|
|
1790
|
+
async bookAndWait(offerId, passengers, contactEmail, searchId, opts = {}) {
|
|
1791
|
+
const { contactPhone = "", idempotencyKey = "", pollMs = 5e3, timeoutMs = 9e5, onQuestion } = opts;
|
|
1792
|
+
const started = await this.book(
|
|
1793
|
+
offerId,
|
|
1794
|
+
passengers,
|
|
1795
|
+
contactEmail,
|
|
1796
|
+
contactPhone,
|
|
1797
|
+
idempotencyKey,
|
|
1798
|
+
searchId
|
|
1799
|
+
);
|
|
1800
|
+
if (!started || started.ok !== true) return started;
|
|
1801
|
+
const bookingId = String(started.booking_id);
|
|
1802
|
+
const deadline = Date.now() + timeoutMs;
|
|
1803
|
+
while (Date.now() < deadline) {
|
|
1804
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
1805
|
+
const state = await this.getBooking(bookingId);
|
|
1806
|
+
const question = state.question;
|
|
1807
|
+
if (question) {
|
|
1808
|
+
const answer = onQuestion ? onQuestion(question) : question.kind === "extra" ? { skip: true } : { confirm: true };
|
|
1809
|
+
await this.answerBooking(bookingId, Number(question.round), answer);
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
if (state.terminal) return state;
|
|
1813
|
+
}
|
|
1814
|
+
return this.getBooking(bookingId);
|
|
1723
1815
|
}
|
|
1724
1816
|
// ── Hotels ──────────────────────────────────────────────────────────
|
|
1725
1817
|
//
|
|
@@ -1877,15 +1969,37 @@ var LetsFG = class {
|
|
|
1877
1969
|
);
|
|
1878
1970
|
}
|
|
1879
1971
|
/**
|
|
1880
|
-
* [Developer API
|
|
1972
|
+
* [Developer API] Mint a one-time link for connecting a Revolut payment method.
|
|
1973
|
+
*
|
|
1974
|
+
* This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
|
|
1975
|
+
* card details never touch LetsFG: the returned `connect_url` opens a hosted page
|
|
1976
|
+
* where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
|
|
1977
|
+
* it in a browser — there is no endpoint that takes card details, so do not ask a
|
|
1978
|
+
* user for a card number and do not try to automate this step.
|
|
1881
1979
|
*
|
|
1882
|
-
* Most agents should NOT
|
|
1883
|
-
* search and booking
|
|
1884
|
-
* through a zero-amount setup and creates no billing account.
|
|
1980
|
+
* Most agents should NOT need a Developer API account at all. To authenticate for
|
|
1981
|
+
* search and booking, run `letsfg auth`, which creates no billing account.
|
|
1885
1982
|
*/
|
|
1886
|
-
async
|
|
1983
|
+
async connectPayment() {
|
|
1887
1984
|
this.requireApiKey();
|
|
1888
|
-
return this.post("/developers/api/v1/agents/
|
|
1985
|
+
return this.post("/developers/api/v1/agents/connect-payment", {});
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
|
|
1989
|
+
*
|
|
1990
|
+
* `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
|
|
1991
|
+
* Revolut rail as the rest of the product: call connectPayment() and open the
|
|
1992
|
+
* `connect_url` it returns.
|
|
1993
|
+
*
|
|
1994
|
+
* Kept as a method, and throwing locally rather than making the request, for the
|
|
1995
|
+
* same reason as unlock() — an older caller gets one clear sentence at the line that
|
|
1996
|
+
* is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
|
|
1997
|
+
*/
|
|
1998
|
+
async setupPayment(_token) {
|
|
1999
|
+
throw new LetsFGError(
|
|
2000
|
+
"setupPayment() was retired on 2026-09-08 with Stripe and the endpoint answers 410 Gone. Call connectPayment() instead and open the connect_url it returns; nothing is charged to connect. See https://letsfg.co/developers/api/docs",
|
|
2001
|
+
410
|
|
2002
|
+
);
|
|
1889
2003
|
}
|
|
1890
2004
|
/**
|
|
1891
2005
|
* Get current agent profile and usage stats.
|
package/dist/cli.js
CHANGED
|
@@ -427,13 +427,23 @@ var LetsFG = class {
|
|
|
427
427
|
return Array.isArray(data) ? data : data.locations || [];
|
|
428
428
|
}
|
|
429
429
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
430
|
+
* RETIRED 2026-09-08. Throws instead of calling the server.
|
|
431
|
+
*
|
|
432
|
+
* There is no unlock step on any lane. Unlock existed to confirm a live price
|
|
433
|
+
* before charging; booking now HOLDS the fare on the connected payment method
|
|
434
|
+
* and captures only once a real airline PNR exists, so a fare that moved
|
|
435
|
+
* cannot become a charge for a ticket you did not get. If it moves at
|
|
436
|
+
* checkout you get a `price_change` question to accept or decline instead.
|
|
437
|
+
*
|
|
438
|
+
* Kept as a method, and throwing locally rather than making the request, so an
|
|
439
|
+
* older caller gets one clear sentence at the line that is actually wrong —
|
|
440
|
+
* not a 410 body to decode, and not a TypeError somewhere else.
|
|
433
441
|
*/
|
|
434
|
-
async unlock(
|
|
435
|
-
|
|
436
|
-
|
|
442
|
+
async unlock(_offerId) {
|
|
443
|
+
throw new LetsFGError(
|
|
444
|
+
"unlock() was retired on 2026-09-08 and the endpoint answers 410 Gone. There is no unlock step: call book() directly. The fare is held on the connected payment method and captured only against a real airline PNR. See https://letsfg.co/developers/api/docs",
|
|
445
|
+
410
|
|
446
|
+
);
|
|
437
447
|
}
|
|
438
448
|
/**
|
|
439
449
|
* Book a flight.
|
|
@@ -444,9 +454,14 @@ var LetsFG = class {
|
|
|
444
454
|
* complete, { ok, booked: false, booking_url } — hand the link to the user,
|
|
445
455
|
* nothing was charged.
|
|
446
456
|
*
|
|
447
|
-
* Developer API (X-API-Key):
|
|
448
|
-
*
|
|
449
|
-
*
|
|
457
|
+
* Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
|
|
458
|
+
* REQUIRED — an offer is bookable only inside the search that produced it.
|
|
459
|
+
* The connected Revolut method is HELD, not charged; a LetsFG booking agent
|
|
460
|
+
* buys the ticket and the hold is captured only against a real airline PNR.
|
|
461
|
+
* Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
|
|
462
|
+
* poll getBooking(bookingId) until `terminal`, or use bookAndWait().
|
|
463
|
+
* Always provide idempotencyKey: a retry with the same key returns the
|
|
464
|
+
* existing booking instead of opening a second hold on the card.
|
|
450
465
|
*/
|
|
451
466
|
async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
|
|
452
467
|
this.requireAuth();
|
|
@@ -467,15 +482,92 @@ var LetsFG = class {
|
|
|
467
482
|
});
|
|
468
483
|
}
|
|
469
484
|
this.requireApiKey();
|
|
485
|
+
if (!searchId) {
|
|
486
|
+
throw new LetsFGError(
|
|
487
|
+
"searchId is required to book on the Developer API \u2014 pass the search_id from search()'s result. An offer can only be booked inside the search that produced it. (Before 2026-09-08 this argument was ignored on this path.)",
|
|
488
|
+
400
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const pax = passengers.map((p) => ({ ...p }));
|
|
492
|
+
if (contactPhone && pax.length && !pax[0].phone_number) pax[0].phone_number = contactPhone;
|
|
470
493
|
const body = {
|
|
494
|
+
search_id: searchId,
|
|
471
495
|
offer_id: offerId,
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
contact_email: contactEmail,
|
|
475
|
-
contact_phone: contactPhone
|
|
496
|
+
passengers: pax,
|
|
497
|
+
contact_email: contactEmail
|
|
476
498
|
};
|
|
477
499
|
if (idempotencyKey) body.idempotency_key = idempotencyKey;
|
|
478
|
-
return this.post("/developers/api/v1/
|
|
500
|
+
return this.post("/developers/api/v1/flights/book", body);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Poll a Developer API flight booking.
|
|
504
|
+
*
|
|
505
|
+
* Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
|
|
506
|
+
* knows you are still there, which is what keeps a booking paused on a
|
|
507
|
+
* question alive — so do not back off to minutes.
|
|
508
|
+
*
|
|
509
|
+
* States: authorised, card_issued, booking_in_progress, awaiting_settlement,
|
|
510
|
+
* then completed (with `pnr` and `charged_amount`), failed (hold released,
|
|
511
|
+
* nothing charged) or needs_attention (a human at LetsFG is on it — do not
|
|
512
|
+
* book again).
|
|
513
|
+
*/
|
|
514
|
+
async getBooking(bookingId) {
|
|
515
|
+
this.requireApiKey();
|
|
516
|
+
return this.getWithAuth(
|
|
517
|
+
`/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Answer the open `question` on a booking.
|
|
522
|
+
*
|
|
523
|
+
* Echo the question's `round`. A stale round is refused with 409 rather than
|
|
524
|
+
* guessed at, so an answer to an old question can never be applied to a new
|
|
525
|
+
* one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
|
|
526
|
+
* { confirm: true } or { skip: true } — declining an extra still completes
|
|
527
|
+
* the booking, without it.
|
|
528
|
+
*/
|
|
529
|
+
async answerBooking(bookingId, round, answer = {}) {
|
|
530
|
+
this.requireApiKey();
|
|
531
|
+
return this.post(
|
|
532
|
+
`/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}/answer`,
|
|
533
|
+
{ round, ...answer }
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Book and poll to a terminal state. Mirrors bookHotelAndWait().
|
|
538
|
+
*
|
|
539
|
+
* Blocks for as long as the booking takes (4–11 minutes typically), so use
|
|
540
|
+
* book() + getBooking() instead if your caller has a request timeout.
|
|
541
|
+
*
|
|
542
|
+
* `onQuestion` returns the answer for answerBooking(). Without it, a fare
|
|
543
|
+
* increase is ACCEPTED and a paid extra is DECLINED — the conservative
|
|
544
|
+
* reading of "the traveller asked for this flight".
|
|
545
|
+
*/
|
|
546
|
+
async bookAndWait(offerId, passengers, contactEmail, searchId, opts = {}) {
|
|
547
|
+
const { contactPhone = "", idempotencyKey = "", pollMs = 5e3, timeoutMs = 9e5, onQuestion } = opts;
|
|
548
|
+
const started = await this.book(
|
|
549
|
+
offerId,
|
|
550
|
+
passengers,
|
|
551
|
+
contactEmail,
|
|
552
|
+
contactPhone,
|
|
553
|
+
idempotencyKey,
|
|
554
|
+
searchId
|
|
555
|
+
);
|
|
556
|
+
if (!started || started.ok !== true) return started;
|
|
557
|
+
const bookingId = String(started.booking_id);
|
|
558
|
+
const deadline = Date.now() + timeoutMs;
|
|
559
|
+
while (Date.now() < deadline) {
|
|
560
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
561
|
+
const state = await this.getBooking(bookingId);
|
|
562
|
+
const question = state.question;
|
|
563
|
+
if (question) {
|
|
564
|
+
const answer = onQuestion ? onQuestion(question) : question.kind === "extra" ? { skip: true } : { confirm: true };
|
|
565
|
+
await this.answerBooking(bookingId, Number(question.round), answer);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (state.terminal) return state;
|
|
569
|
+
}
|
|
570
|
+
return this.getBooking(bookingId);
|
|
479
571
|
}
|
|
480
572
|
// ── Hotels ──────────────────────────────────────────────────────────
|
|
481
573
|
//
|
|
@@ -633,15 +725,37 @@ var LetsFG = class {
|
|
|
633
725
|
);
|
|
634
726
|
}
|
|
635
727
|
/**
|
|
636
|
-
* [Developer API
|
|
728
|
+
* [Developer API] Mint a one-time link for connecting a Revolut payment method.
|
|
729
|
+
*
|
|
730
|
+
* This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
|
|
731
|
+
* card details never touch LetsFG: the returned `connect_url` opens a hosted page
|
|
732
|
+
* where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
|
|
733
|
+
* it in a browser — there is no endpoint that takes card details, so do not ask a
|
|
734
|
+
* user for a card number and do not try to automate this step.
|
|
637
735
|
*
|
|
638
|
-
* Most agents should NOT
|
|
639
|
-
* search and booking
|
|
640
|
-
* through a zero-amount setup and creates no billing account.
|
|
736
|
+
* Most agents should NOT need a Developer API account at all. To authenticate for
|
|
737
|
+
* search and booking, run `letsfg auth`, which creates no billing account.
|
|
641
738
|
*/
|
|
642
|
-
async
|
|
739
|
+
async connectPayment() {
|
|
643
740
|
this.requireApiKey();
|
|
644
|
-
return this.post("/developers/api/v1/agents/
|
|
741
|
+
return this.post("/developers/api/v1/agents/connect-payment", {});
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
|
|
745
|
+
*
|
|
746
|
+
* `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
|
|
747
|
+
* Revolut rail as the rest of the product: call connectPayment() and open the
|
|
748
|
+
* `connect_url` it returns.
|
|
749
|
+
*
|
|
750
|
+
* Kept as a method, and throwing locally rather than making the request, for the
|
|
751
|
+
* same reason as unlock() — an older caller gets one clear sentence at the line that
|
|
752
|
+
* is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
|
|
753
|
+
*/
|
|
754
|
+
async setupPayment(_token) {
|
|
755
|
+
throw new LetsFGError(
|
|
756
|
+
"setupPayment() was retired on 2026-09-08 with Stripe and the endpoint answers 410 Gone. Call connectPayment() instead and open the connect_url it returns; nothing is charged to connect. See https://letsfg.co/developers/api/docs",
|
|
757
|
+
410
|
|
758
|
+
);
|
|
645
759
|
}
|
|
646
760
|
/**
|
|
647
761
|
* Get current agent profile and usage stats.
|
|
@@ -1051,38 +1165,15 @@ async function cmdSearch(args) {
|
|
|
1051
1165
|
`);
|
|
1052
1166
|
} else {
|
|
1053
1167
|
console.log(`
|
|
1054
|
-
To
|
|
1055
|
-
console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
|
|
1168
|
+
To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
|
|
1056
1169
|
`);
|
|
1057
1170
|
}
|
|
1058
1171
|
}
|
|
1059
|
-
async function cmdUnlock(
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
if (!offerId) {
|
|
1065
|
-
console.error("Usage: letsfg unlock <offer_id>");
|
|
1066
|
-
process.exit(1);
|
|
1067
|
-
}
|
|
1068
|
-
const bt = new LetsFG({ apiKey, baseUrl });
|
|
1069
|
-
const result = await bt.unlock(offerId);
|
|
1070
|
-
if (jsonOut) {
|
|
1071
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
if (result.unlock_status === "unlocked") {
|
|
1075
|
-
console.log(`
|
|
1076
|
-
\u2713 Offer unlocked!`);
|
|
1077
|
-
console.log(` Confirmed price: ${result.confirmed_currency} ${result.confirmed_price?.toFixed(2)}`);
|
|
1078
|
-
console.log(` Expires at: ${result.offer_expires_at}`);
|
|
1079
|
-
console.log(`
|
|
1080
|
-
Next: letsfg book ${offerId} --passenger '{...}' --email you@example.com
|
|
1081
|
-
`);
|
|
1082
|
-
} else {
|
|
1083
|
-
console.error(` \u2717 Unlock failed: ${result.message}`);
|
|
1084
|
-
process.exit(1);
|
|
1085
|
-
}
|
|
1172
|
+
async function cmdUnlock(_args) {
|
|
1173
|
+
console.error(
|
|
1174
|
+
"\n letsfg unlock was retired on 2026-09-08 and the endpoint answers 410 Gone.\n\n There is no unlock step any more. Book directly:\n letsfg book <offer_id> --search-id <search_id> --passenger '{...}' --email you@example.com\n\n The fare is HELD on your connected payment method and captured only once a real\n airline PNR exists, which is what unlock existed to protect against. If the fare moves\n at checkout you are asked to accept or decline it.\n"
|
|
1175
|
+
);
|
|
1176
|
+
process.exit(1);
|
|
1086
1177
|
}
|
|
1087
1178
|
async function cmdBook(args) {
|
|
1088
1179
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
@@ -1214,19 +1305,26 @@ async function cmdSetupPayment(args) {
|
|
|
1214
1305
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
1215
1306
|
const apiKey = getFlag(args, "--api-key", "-k");
|
|
1216
1307
|
const baseUrl = getFlag(args, "--base-url");
|
|
1217
|
-
|
|
1308
|
+
if (getFlag(args, "--token", "-t")) {
|
|
1309
|
+
console.log("\n Note: --token was part of the Stripe enrolment, retired 2026-09-08. Ignoring it.");
|
|
1310
|
+
}
|
|
1218
1311
|
const bt = new LetsFG({ apiKey, baseUrl });
|
|
1219
|
-
const result = await bt.
|
|
1312
|
+
const result = await bt.connectPayment();
|
|
1220
1313
|
if (jsonOut) {
|
|
1221
1314
|
console.log(JSON.stringify(result, null, 2));
|
|
1222
1315
|
return;
|
|
1223
1316
|
}
|
|
1224
|
-
|
|
1317
|
+
const url = result.connect_url;
|
|
1318
|
+
if (url) {
|
|
1225
1319
|
console.log(`
|
|
1226
|
-
|
|
1320
|
+
Open this in a browser to connect a payment method:
|
|
1321
|
+
`);
|
|
1322
|
+
console.log(` ${url}
|
|
1323
|
+
`);
|
|
1324
|
+
console.log(` Nothing is charged. Run \`letsfg me\` afterwards to confirm it landed.
|
|
1227
1325
|
`);
|
|
1228
1326
|
} else {
|
|
1229
|
-
console.error(` \u2717
|
|
1327
|
+
console.error(` \u2717 Could not mint a connect link: ${result.message || result.status}`);
|
|
1230
1328
|
process.exit(1);
|
|
1231
1329
|
}
|
|
1232
1330
|
}
|
|
@@ -1247,10 +1345,9 @@ async function cmdMe(args) {
|
|
|
1247
1345
|
console.log(` Email: ${p.email}`);
|
|
1248
1346
|
console.log(` Tier: ${p.tier}`);
|
|
1249
1347
|
const access = p.access_granted || false;
|
|
1250
|
-
console.log(` Access: ${access ? "\u2713 Granted (search,
|
|
1348
|
+
console.log(` Access: ${access ? "\u2713 Granted (search, book)" : "\u2717 Not granted"}`);
|
|
1251
1349
|
console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2014"}`);
|
|
1252
1350
|
console.log(` Searches: ${u.total_searches || 0}`);
|
|
1253
|
-
console.log(` Unlocks: ${u.total_unlocks || 0}`);
|
|
1254
1351
|
console.log(` Bookings: ${u.total_bookings || 0}`);
|
|
1255
1352
|
console.log(` Total spent: $${((u.total_spent_cents || 0) / 100).toFixed(2)}
|
|
1256
1353
|
`);
|
|
@@ -1267,14 +1364,15 @@ Commands:
|
|
|
1267
1364
|
auth Connect a card at letsfg.co/connect. Nothing charged
|
|
1268
1365
|
search <origin> <dest> <date> Search for flights (free), prints search_id
|
|
1269
1366
|
locations <query> Resolve city name to IATA codes
|
|
1270
|
-
book <offer_id> --search-id ... Book a flight. No
|
|
1367
|
+
book <offer_id> --search-id ... Book a flight. No booking or transaction fee, no unlock step
|
|
1271
1368
|
me Show agent profile
|
|
1272
1369
|
|
|
1273
1370
|
Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
|
|
1274
1371
|
they create a billing account. Use auth above instead):
|
|
1275
1372
|
register --name ... --email ... Create a paid Developer API account
|
|
1276
|
-
|
|
1277
|
-
|
|
1373
|
+
connect-payment Print a link to connect a card to that paid account
|
|
1374
|
+
setup-payment Alias of connect-payment (the Stripe lane retired 2026-09-08)
|
|
1375
|
+
unlock <offer_id> RETIRED 2026-09-08 \u2014 no unlock step, book directly
|
|
1278
1376
|
|
|
1279
1377
|
Options:
|
|
1280
1378
|
--json, -j Output raw JSON
|
|
@@ -1310,6 +1408,7 @@ async function main() {
|
|
|
1310
1408
|
case "register":
|
|
1311
1409
|
await cmdRegister(args);
|
|
1312
1410
|
break;
|
|
1411
|
+
case "connect-payment":
|
|
1313
1412
|
case "setup-payment":
|
|
1314
1413
|
await cmdSetupPayment(args);
|
|
1315
1414
|
break;
|
package/dist/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
LetsFG,
|
|
4
4
|
LetsFGError,
|
|
5
5
|
offerSummary
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-F5BBI6XX.mjs";
|
|
7
7
|
|
|
8
8
|
// src/auth.ts
|
|
9
9
|
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
|
|
@@ -329,38 +329,15 @@ async function cmdSearch(args) {
|
|
|
329
329
|
`);
|
|
330
330
|
} else {
|
|
331
331
|
console.log(`
|
|
332
|
-
To
|
|
333
|
-
console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
|
|
332
|
+
To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
|
|
334
333
|
`);
|
|
335
334
|
}
|
|
336
335
|
}
|
|
337
|
-
async function cmdUnlock(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (!offerId) {
|
|
343
|
-
console.error("Usage: letsfg unlock <offer_id>");
|
|
344
|
-
process.exit(1);
|
|
345
|
-
}
|
|
346
|
-
const bt = new LetsFG({ apiKey, baseUrl });
|
|
347
|
-
const result = await bt.unlock(offerId);
|
|
348
|
-
if (jsonOut) {
|
|
349
|
-
console.log(JSON.stringify(result, null, 2));
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (result.unlock_status === "unlocked") {
|
|
353
|
-
console.log(`
|
|
354
|
-
\u2713 Offer unlocked!`);
|
|
355
|
-
console.log(` Confirmed price: ${result.confirmed_currency} ${result.confirmed_price?.toFixed(2)}`);
|
|
356
|
-
console.log(` Expires at: ${result.offer_expires_at}`);
|
|
357
|
-
console.log(`
|
|
358
|
-
Next: letsfg book ${offerId} --passenger '{...}' --email you@example.com
|
|
359
|
-
`);
|
|
360
|
-
} else {
|
|
361
|
-
console.error(` \u2717 Unlock failed: ${result.message}`);
|
|
362
|
-
process.exit(1);
|
|
363
|
-
}
|
|
336
|
+
async function cmdUnlock(_args) {
|
|
337
|
+
console.error(
|
|
338
|
+
"\n letsfg unlock was retired on 2026-09-08 and the endpoint answers 410 Gone.\n\n There is no unlock step any more. Book directly:\n letsfg book <offer_id> --search-id <search_id> --passenger '{...}' --email you@example.com\n\n The fare is HELD on your connected payment method and captured only once a real\n airline PNR exists, which is what unlock existed to protect against. If the fare moves\n at checkout you are asked to accept or decline it.\n"
|
|
339
|
+
);
|
|
340
|
+
process.exit(1);
|
|
364
341
|
}
|
|
365
342
|
async function cmdBook(args) {
|
|
366
343
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
@@ -492,19 +469,26 @@ async function cmdSetupPayment(args) {
|
|
|
492
469
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
493
470
|
const apiKey = getFlag(args, "--api-key", "-k");
|
|
494
471
|
const baseUrl = getFlag(args, "--base-url");
|
|
495
|
-
|
|
472
|
+
if (getFlag(args, "--token", "-t")) {
|
|
473
|
+
console.log("\n Note: --token was part of the Stripe enrolment, retired 2026-09-08. Ignoring it.");
|
|
474
|
+
}
|
|
496
475
|
const bt = new LetsFG({ apiKey, baseUrl });
|
|
497
|
-
const result = await bt.
|
|
476
|
+
const result = await bt.connectPayment();
|
|
498
477
|
if (jsonOut) {
|
|
499
478
|
console.log(JSON.stringify(result, null, 2));
|
|
500
479
|
return;
|
|
501
480
|
}
|
|
502
|
-
|
|
481
|
+
const url = result.connect_url;
|
|
482
|
+
if (url) {
|
|
503
483
|
console.log(`
|
|
504
|
-
|
|
484
|
+
Open this in a browser to connect a payment method:
|
|
485
|
+
`);
|
|
486
|
+
console.log(` ${url}
|
|
487
|
+
`);
|
|
488
|
+
console.log(` Nothing is charged. Run \`letsfg me\` afterwards to confirm it landed.
|
|
505
489
|
`);
|
|
506
490
|
} else {
|
|
507
|
-
console.error(` \u2717
|
|
491
|
+
console.error(` \u2717 Could not mint a connect link: ${result.message || result.status}`);
|
|
508
492
|
process.exit(1);
|
|
509
493
|
}
|
|
510
494
|
}
|
|
@@ -525,10 +509,9 @@ async function cmdMe(args) {
|
|
|
525
509
|
console.log(` Email: ${p.email}`);
|
|
526
510
|
console.log(` Tier: ${p.tier}`);
|
|
527
511
|
const access = p.access_granted || false;
|
|
528
|
-
console.log(` Access: ${access ? "\u2713 Granted (search,
|
|
512
|
+
console.log(` Access: ${access ? "\u2713 Granted (search, book)" : "\u2717 Not granted"}`);
|
|
529
513
|
console.log(` Payment: ${p.payment_ready ? "\u2713 Ready" : "\u2014"}`);
|
|
530
514
|
console.log(` Searches: ${u.total_searches || 0}`);
|
|
531
|
-
console.log(` Unlocks: ${u.total_unlocks || 0}`);
|
|
532
515
|
console.log(` Bookings: ${u.total_bookings || 0}`);
|
|
533
516
|
console.log(` Total spent: $${((u.total_spent_cents || 0) / 100).toFixed(2)}
|
|
534
517
|
`);
|
|
@@ -545,14 +528,15 @@ Commands:
|
|
|
545
528
|
auth Connect a card at letsfg.co/connect. Nothing charged
|
|
546
529
|
search <origin> <dest> <date> Search for flights (free), prints search_id
|
|
547
530
|
locations <query> Resolve city name to IATA codes
|
|
548
|
-
book <offer_id> --search-id ... Book a flight. No
|
|
531
|
+
book <offer_id> --search-id ... Book a flight. No booking or transaction fee, no unlock step
|
|
549
532
|
me Show agent profile
|
|
550
533
|
|
|
551
534
|
Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
|
|
552
535
|
they create a billing account. Use auth above instead):
|
|
553
536
|
register --name ... --email ... Create a paid Developer API account
|
|
554
|
-
|
|
555
|
-
|
|
537
|
+
connect-payment Print a link to connect a card to that paid account
|
|
538
|
+
setup-payment Alias of connect-payment (the Stripe lane retired 2026-09-08)
|
|
539
|
+
unlock <offer_id> RETIRED 2026-09-08 \u2014 no unlock step, book directly
|
|
556
540
|
|
|
557
541
|
Options:
|
|
558
542
|
--json, -j Output raw JSON
|
|
@@ -588,6 +572,7 @@ async function main() {
|
|
|
588
572
|
case "register":
|
|
589
573
|
await cmdRegister(args);
|
|
590
574
|
break;
|
|
575
|
+
case "connect-payment":
|
|
591
576
|
case "setup-payment":
|
|
592
577
|
await cmdSetupPayment(args);
|
|
593
578
|
break;
|