letsfg 2026.5.71 → 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/dist/index.d.ts CHANGED
@@ -282,7 +282,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
282
282
  * const bt = new LetsFG({ bearerToken: process.env.LETSFG_BEARER_TOKEN });
283
283
  * const flights = await bt.search('GDN', 'BER', '2026-03-03');
284
284
  *
285
- * // Developer API (prepaid credits)
285
+ * // Developer API (look-to-book search: 200 free after every booking)
286
286
  * const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
287
287
  * const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
288
288
  * ```
@@ -402,7 +402,7 @@ interface SearchOptions {
402
402
  interface LetsFGConfig {
403
403
  /** PFS Bearer token from `letsfg auth`. Enables free search via POST /api/search polling. */
404
404
  bearerToken?: string;
405
- /** Developer API key (prepaid credits, no per-booking fee). */
405
+ /** Developer API key. Look-to-book search; no booking fee, no transaction fee. */
406
406
  apiKey?: string;
407
407
  baseUrl?: string;
408
408
  timeout?: number;
@@ -495,11 +495,19 @@ declare class LetsFG {
495
495
  */
496
496
  resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
497
497
  /**
498
- * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
499
- * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
500
- * token, so PFS callers use book() directly.
498
+ * RETIRED 2026-09-08. Throws instead of calling the server.
499
+ *
500
+ * There is no unlock step on any lane. Unlock existed to confirm a live price
501
+ * before charging; booking now HOLDS the fare on the connected payment method
502
+ * and captures only once a real airline PNR exists, so a fare that moved
503
+ * cannot become a charge for a ticket you did not get. If it moves at
504
+ * checkout you get a `price_change` question to accept or decline instead.
505
+ *
506
+ * Kept as a method, and throwing locally rather than making the request, so an
507
+ * older caller gets one clear sentence at the line that is actually wrong —
508
+ * not a 410 body to decode, and not a TypeError somewhere else.
501
509
  */
502
- unlock(offerId: string): Promise<UnlockResult>;
510
+ unlock(_offerId: string): Promise<UnlockResult>;
503
511
  /**
504
512
  * Book a flight.
505
513
  *
@@ -509,11 +517,64 @@ declare class LetsFG {
509
517
  * complete, { ok, booked: false, booking_url } — hand the link to the user,
510
518
  * nothing was charged.
511
519
  *
512
- * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
513
- * creates a real PNR. Requires unlock() first. Always provide
514
- * idempotencyKey to prevent double-bookings on retry.
520
+ * Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
521
+ * REQUIRED — an offer is bookable only inside the search that produced it.
522
+ * The connected Revolut method is HELD, not charged; a LetsFG booking agent
523
+ * buys the ticket and the hold is captured only against a real airline PNR.
524
+ * Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
525
+ * poll getBooking(bookingId) until `terminal`, or use bookAndWait().
526
+ * Always provide idempotencyKey: a retry with the same key returns the
527
+ * existing booking instead of opening a second hold on the card.
515
528
  */
516
529
  book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string, searchId?: string): Promise<BookingResult | Record<string, unknown>>;
530
+ /**
531
+ * Poll a Developer API flight booking.
532
+ *
533
+ * Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
534
+ * knows you are still there, which is what keeps a booking paused on a
535
+ * question alive — so do not back off to minutes.
536
+ *
537
+ * States: authorised, card_issued, booking_in_progress, awaiting_settlement,
538
+ * then completed (with `pnr` and `charged_amount`), failed (hold released,
539
+ * nothing charged) or needs_attention (a human at LetsFG is on it — do not
540
+ * book again).
541
+ */
542
+ getBooking(bookingId: string): Promise<Record<string, unknown>>;
543
+ /**
544
+ * Answer the open `question` on a booking.
545
+ *
546
+ * Echo the question's `round`. A stale round is refused with 409 rather than
547
+ * guessed at, so an answer to an old question can never be applied to a new
548
+ * one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
549
+ * { confirm: true } or { skip: true } — declining an extra still completes
550
+ * the booking, without it.
551
+ */
552
+ answerBooking(bookingId: string, round: number, answer?: {
553
+ seats?: Array<Record<string, unknown>>;
554
+ confirm?: boolean;
555
+ skip?: boolean;
556
+ }): Promise<Record<string, unknown>>;
557
+ /**
558
+ * Book and poll to a terminal state. Mirrors bookHotelAndWait().
559
+ *
560
+ * Blocks for as long as the booking takes (4–11 minutes typically), so use
561
+ * book() + getBooking() instead if your caller has a request timeout.
562
+ *
563
+ * `onQuestion` returns the answer for answerBooking(). Without it, a fare
564
+ * increase is ACCEPTED and a paid extra is DECLINED — the conservative
565
+ * reading of "the traveller asked for this flight".
566
+ */
567
+ bookAndWait(offerId: string, passengers: Passenger[], contactEmail: string, searchId: string, opts?: {
568
+ contactPhone?: string;
569
+ idempotencyKey?: string;
570
+ pollMs?: number;
571
+ timeoutMs?: number;
572
+ onQuestion?: (q: Record<string, unknown>) => {
573
+ seats?: Array<Record<string, unknown>>;
574
+ confirm?: boolean;
575
+ skip?: boolean;
576
+ };
577
+ }): Promise<Record<string, unknown>>;
517
578
  /**
518
579
  * Resolve a place name to the city id that searchHotels() needs.
519
580
  *
@@ -621,13 +682,30 @@ declare class LetsFG {
621
682
  */
622
683
  cancelHotel(confirmation: string): Promise<Record<string, unknown>>;
623
684
  /**
624
- * [Developer API only] Attach a card to a PAID prepaid Developer API account.
685
+ * [Developer API] Mint a one-time link for connecting a Revolut payment method.
686
+ *
687
+ * This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
688
+ * card details never touch LetsFG: the returned `connect_url` opens a hosted page
689
+ * where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
690
+ * it in a browser — there is no endpoint that takes card details, so do not ask a
691
+ * user for a card number and do not try to automate this step.
692
+ *
693
+ * Most agents should NOT need a Developer API account at all. To authenticate for
694
+ * search and booking, run `letsfg auth`, which creates no billing account.
695
+ */
696
+ connectPayment(): Promise<Record<string, unknown>>;
697
+ /**
698
+ * RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
699
+ *
700
+ * `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
701
+ * Revolut rail as the rest of the product: call connectPayment() and open the
702
+ * `connect_url` it returns.
625
703
  *
626
- * Most agents should NOT call this. It is unrelated to authenticating for
627
- * search and booking — for that, run `letsfg auth`, which puts a card on file
628
- * through a zero-amount setup and creates no billing account.
704
+ * Kept as a method, and throwing locally rather than making the request, for the
705
+ * same reason as unlock() — an older caller gets one clear sentence at the line that
706
+ * is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
629
707
  */
630
- setupPayment(token?: string): Promise<Record<string, unknown>>;
708
+ setupPayment(_token?: string): Promise<Record<string, unknown>>;
631
709
  /**
632
710
  * Get current agent profile and usage stats.
633
711
  */
package/dist/index.js CHANGED
@@ -1719,13 +1719,23 @@ var LetsFG = class {
1719
1719
  return Array.isArray(data) ? data : data.locations || [];
1720
1720
  }
1721
1721
  /**
1722
- * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1723
- * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
1724
- * token, so PFS callers use book() directly.
1722
+ * RETIRED 2026-09-08. Throws instead of calling the server.
1723
+ *
1724
+ * There is no unlock step on any lane. Unlock existed to confirm a live price
1725
+ * before charging; booking now HOLDS the fare on the connected payment method
1726
+ * and captures only once a real airline PNR exists, so a fare that moved
1727
+ * cannot become a charge for a ticket you did not get. If it moves at
1728
+ * checkout you get a `price_change` question to accept or decline instead.
1729
+ *
1730
+ * Kept as a method, and throwing locally rather than making the request, so an
1731
+ * older caller gets one clear sentence at the line that is actually wrong —
1732
+ * not a 410 body to decode, and not a TypeError somewhere else.
1725
1733
  */
1726
- async unlock(offerId) {
1727
- this.requireApiKey();
1728
- return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
1734
+ async unlock(_offerId) {
1735
+ throw new LetsFGError(
1736
+ "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",
1737
+ 410
1738
+ );
1729
1739
  }
1730
1740
  /**
1731
1741
  * Book a flight.
@@ -1736,9 +1746,14 @@ var LetsFG = class {
1736
1746
  * complete, { ok, booked: false, booking_url } — hand the link to the user,
1737
1747
  * nothing was charged.
1738
1748
  *
1739
- * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
1740
- * creates a real PNR. Requires unlock() first. Always provide
1741
- * idempotencyKey to prevent double-bookings on retry.
1749
+ * Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
1750
+ * REQUIRED — an offer is bookable only inside the search that produced it.
1751
+ * The connected Revolut method is HELD, not charged; a LetsFG booking agent
1752
+ * buys the ticket and the hold is captured only against a real airline PNR.
1753
+ * Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
1754
+ * poll getBooking(bookingId) until `terminal`, or use bookAndWait().
1755
+ * Always provide idempotencyKey: a retry with the same key returns the
1756
+ * existing booking instead of opening a second hold on the card.
1742
1757
  */
1743
1758
  async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
1744
1759
  this.requireAuth();
@@ -1759,15 +1774,92 @@ var LetsFG = class {
1759
1774
  });
1760
1775
  }
1761
1776
  this.requireApiKey();
1777
+ if (!searchId) {
1778
+ throw new LetsFGError(
1779
+ "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.)",
1780
+ 400
1781
+ );
1782
+ }
1783
+ const pax = passengers.map((p) => ({ ...p }));
1784
+ if (contactPhone && pax.length && !pax[0].phone_number) pax[0].phone_number = contactPhone;
1762
1785
  const body = {
1786
+ search_id: searchId,
1763
1787
  offer_id: offerId,
1764
- booking_type: "flight",
1765
- passengers,
1766
- contact_email: contactEmail,
1767
- contact_phone: contactPhone
1788
+ passengers: pax,
1789
+ contact_email: contactEmail
1768
1790
  };
1769
1791
  if (idempotencyKey) body.idempotency_key = idempotencyKey;
1770
- return this.post("/developers/api/v1/bookings/book", body);
1792
+ return this.post("/developers/api/v1/flights/book", body);
1793
+ }
1794
+ /**
1795
+ * Poll a Developer API flight booking.
1796
+ *
1797
+ * Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
1798
+ * knows you are still there, which is what keeps a booking paused on a
1799
+ * question alive — so do not back off to minutes.
1800
+ *
1801
+ * States: authorised, card_issued, booking_in_progress, awaiting_settlement,
1802
+ * then completed (with `pnr` and `charged_amount`), failed (hold released,
1803
+ * nothing charged) or needs_attention (a human at LetsFG is on it — do not
1804
+ * book again).
1805
+ */
1806
+ async getBooking(bookingId) {
1807
+ this.requireApiKey();
1808
+ return this.getWithAuth(
1809
+ `/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}`
1810
+ );
1811
+ }
1812
+ /**
1813
+ * Answer the open `question` on a booking.
1814
+ *
1815
+ * Echo the question's `round`. A stale round is refused with 409 rather than
1816
+ * guessed at, so an answer to an old question can never be applied to a new
1817
+ * one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
1818
+ * { confirm: true } or { skip: true } — declining an extra still completes
1819
+ * the booking, without it.
1820
+ */
1821
+ async answerBooking(bookingId, round, answer = {}) {
1822
+ this.requireApiKey();
1823
+ return this.post(
1824
+ `/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}/answer`,
1825
+ { round, ...answer }
1826
+ );
1827
+ }
1828
+ /**
1829
+ * Book and poll to a terminal state. Mirrors bookHotelAndWait().
1830
+ *
1831
+ * Blocks for as long as the booking takes (4–11 minutes typically), so use
1832
+ * book() + getBooking() instead if your caller has a request timeout.
1833
+ *
1834
+ * `onQuestion` returns the answer for answerBooking(). Without it, a fare
1835
+ * increase is ACCEPTED and a paid extra is DECLINED — the conservative
1836
+ * reading of "the traveller asked for this flight".
1837
+ */
1838
+ async bookAndWait(offerId, passengers, contactEmail, searchId, opts = {}) {
1839
+ const { contactPhone = "", idempotencyKey = "", pollMs = 5e3, timeoutMs = 9e5, onQuestion } = opts;
1840
+ const started = await this.book(
1841
+ offerId,
1842
+ passengers,
1843
+ contactEmail,
1844
+ contactPhone,
1845
+ idempotencyKey,
1846
+ searchId
1847
+ );
1848
+ if (!started || started.ok !== true) return started;
1849
+ const bookingId = String(started.booking_id);
1850
+ const deadline = Date.now() + timeoutMs;
1851
+ while (Date.now() < deadline) {
1852
+ await new Promise((r) => setTimeout(r, pollMs));
1853
+ const state = await this.getBooking(bookingId);
1854
+ const question = state.question;
1855
+ if (question) {
1856
+ const answer = onQuestion ? onQuestion(question) : question.kind === "extra" ? { skip: true } : { confirm: true };
1857
+ await this.answerBooking(bookingId, Number(question.round), answer);
1858
+ continue;
1859
+ }
1860
+ if (state.terminal) return state;
1861
+ }
1862
+ return this.getBooking(bookingId);
1771
1863
  }
1772
1864
  // ── Hotels ──────────────────────────────────────────────────────────
1773
1865
  //
@@ -1925,15 +2017,37 @@ var LetsFG = class {
1925
2017
  );
1926
2018
  }
1927
2019
  /**
1928
- * [Developer API only] Attach a card to a PAID prepaid Developer API account.
2020
+ * [Developer API] Mint a one-time link for connecting a Revolut payment method.
2021
+ *
2022
+ * This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
2023
+ * card details never touch LetsFG: the returned `connect_url` opens a hosted page
2024
+ * where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
2025
+ * it in a browser — there is no endpoint that takes card details, so do not ask a
2026
+ * user for a card number and do not try to automate this step.
1929
2027
  *
1930
- * Most agents should NOT call this. It is unrelated to authenticating for
1931
- * search and booking — for that, run `letsfg auth`, which puts a card on file
1932
- * through a zero-amount setup and creates no billing account.
2028
+ * Most agents should NOT need a Developer API account at all. To authenticate for
2029
+ * search and booking, run `letsfg auth`, which creates no billing account.
1933
2030
  */
1934
- async setupPayment(token = "tok_visa") {
2031
+ async connectPayment() {
1935
2032
  this.requireApiKey();
1936
- return this.post("/developers/api/v1/agents/setup-payment", { token });
2033
+ return this.post("/developers/api/v1/agents/connect-payment", {});
2034
+ }
2035
+ /**
2036
+ * RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
2037
+ *
2038
+ * `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
2039
+ * Revolut rail as the rest of the product: call connectPayment() and open the
2040
+ * `connect_url` it returns.
2041
+ *
2042
+ * Kept as a method, and throwing locally rather than making the request, for the
2043
+ * same reason as unlock() — an older caller gets one clear sentence at the line that
2044
+ * is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
2045
+ */
2046
+ async setupPayment(_token) {
2047
+ throw new LetsFGError(
2048
+ "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",
2049
+ 410
2050
+ );
1937
2051
  }
1938
2052
  /**
1939
2053
  * Get current agent profile and usage stats.
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  offerSummary,
23
23
  rankOffers,
24
24
  selectDiverseTop
25
- } from "./chunk-GO3FXQXC.mjs";
25
+ } from "./chunk-F5BBI6XX.mjs";
26
26
  export {
27
27
  AuthenticationError,
28
28
  BoostedTravel,
package/package.json CHANGED
@@ -1,56 +1,56 @@
1
- {
2
- "name": "letsfg",
3
- "version": "2026.5.71",
4
- "description": "Flights and hotels for AI agents. Server-side engine covers hundreds of airlines; hotels are real bookable inventory with free cancellation and pay-later terms. Includes open-source ranking engine.",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
7
- "types": "dist/index.d.ts",
8
- "bin": {
9
- "letsfg": "dist/cli.js"
10
- },
11
- "files": [
12
- "dist/",
13
- "README.md",
14
- "LICENSE"
15
- ],
16
- "scripts": {
17
- "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
18
- "test": "tsx --test src/index.test.ts src/auth.test.ts",
19
- "prepublishOnly": "npm run build"
20
- },
21
- "keywords": [
22
- "flights",
23
- "travel",
24
- "booking",
25
- "agent",
26
- "ai",
27
- "cli",
28
- "autonomous",
29
- "mcp",
30
- "openai",
31
- "airline",
32
- "ndc",
33
- "search",
34
- "ndc",
35
- "hotels",
36
- "hotel-booking",
37
- "hotel-search",
38
- "accommodation"
39
- ],
40
- "author": "LetsFG",
41
- "license": "MIT",
42
- "repository": {
43
- "type": "git",
44
- "url": "https://github.com/LetsFG/LetsFG.git"
45
- },
46
- "homepage": "https://letsfg.co",
47
- "devDependencies": {
48
- "@types/node": "^25.3.3",
49
- "tsup": "^8.0.0",
50
- "tsx": "^4.19.0",
51
- "typescript": "^5.3.0"
52
- },
53
- "engines": {
54
- "node": ">=18.0.0"
55
- }
56
- }
1
+ {
2
+ "name": "letsfg",
3
+ "version": "2026.5.73",
4
+ "description": "Flights and hotels for AI agents. Server-side engine covers hundreds of airlines; hotels are real bookable inventory with free cancellation and pay-later terms. Includes open-source ranking engine.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "letsfg": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
18
+ "test": "tsx --test src/index.test.ts src/auth.test.ts",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "flights",
23
+ "travel",
24
+ "booking",
25
+ "agent",
26
+ "ai",
27
+ "cli",
28
+ "autonomous",
29
+ "mcp",
30
+ "openai",
31
+ "airline",
32
+ "ndc",
33
+ "search",
34
+ "ndc",
35
+ "hotels",
36
+ "hotel-booking",
37
+ "hotel-search",
38
+ "accommodation"
39
+ ],
40
+ "author": "LetsFG",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/LetsFG/LetsFG.git"
45
+ },
46
+ "homepage": "https://letsfg.co",
47
+ "devDependencies": {
48
+ "@types/node": "^25.3.3",
49
+ "tsup": "^8.0.0",
50
+ "tsx": "^4.19.0",
51
+ "typescript": "^5.3.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ }
56
+ }
@@ -1,16 +0,0 @@
1
- import {
2
- BearerTokenError,
3
- getBearerToken,
4
- paymentAuth,
5
- requestEnrolment,
6
- saveToken,
7
- verifyPaymentMethod
8
- } from "./chunk-XNO2W77S.mjs";
9
- export {
10
- BearerTokenError,
11
- getBearerToken,
12
- paymentAuth,
13
- requestEnrolment,
14
- saveToken,
15
- verifyPaymentMethod
16
- };
@@ -1,126 +0,0 @@
1
- // src/auth.ts
2
- import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
3
- import { homedir, platform } from "os";
4
- import { join, dirname } from "path";
5
- var BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
6
- var TOKEN_TTL_MS = 90 * 24 * 3600 * 1e3;
7
- var BearerTokenError = class extends Error {
8
- };
9
- function configPath() {
10
- const base = platform() === "win32" ? process.env.APPDATA || homedir() : homedir();
11
- return join(base, ".letsfg", "config.json");
12
- }
13
- function loadConfig() {
14
- const p = configPath();
15
- if (!existsSync(p)) return {};
16
- try {
17
- return JSON.parse(readFileSync(p, "utf-8"));
18
- } catch {
19
- return {};
20
- }
21
- }
22
- function saveConfig(cfg) {
23
- const p = configPath();
24
- mkdirSync(dirname(p), { recursive: true });
25
- writeFileSync(p, JSON.stringify(cfg, null, 2));
26
- try {
27
- chmodSync(p, 384);
28
- } catch {
29
- }
30
- }
31
- function getBearerToken() {
32
- const env = process.env.LETSFG_BEARER_TOKEN;
33
- if (env) return env;
34
- const auth = loadConfig().pfs_auth;
35
- if (auth?.token && Date.now() < auth.expires_at - 36e5) {
36
- return auth.token;
37
- }
38
- throw new BearerTokenError(
39
- "No valid LetsFG Bearer token.\n Run: letsfg auth (adds a payment method \u2014 nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
40
- );
41
- }
42
- function saveToken(token, expiresAt) {
43
- const cfg = loadConfig();
44
- cfg.pfs_auth = { token, expires_at: expiresAt ?? Date.now() + TOKEN_TTL_MS };
45
- saveConfig(cfg);
46
- }
47
- async function postJson(path, payload) {
48
- const resp = await fetch(`${BASE_URL}${path}`, {
49
- method: "POST",
50
- headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0", "X-Client-Type": "js-sdk" },
51
- body: JSON.stringify(payload)
52
- });
53
- const data = await resp.json().catch(() => ({}));
54
- return { status: resp.status, data };
55
- }
56
- function parseExpiry(raw) {
57
- if (typeof raw === "number") return raw * 1e3;
58
- if (typeof raw === "string") {
59
- const ms = Date.parse(raw);
60
- if (!Number.isNaN(ms)) return ms;
61
- }
62
- return Date.now() + TOKEN_TTL_MS;
63
- }
64
- async function requestEnrolment() {
65
- const { status, data } = await postJson("/api/agent-access/request", {});
66
- if (status !== 200 && status !== 402) {
67
- throw new BearerTokenError(`Could not start authentication (HTTP ${status}): ${data.error || ""}`);
68
- }
69
- return data;
70
- }
71
- async function verifyPaymentMethod(opts) {
72
- const payload = {};
73
- if (opts.setupSessionId) payload.setup_session_id = opts.setupSessionId;
74
- else if (opts.paymentMethodId) payload.payment_method_id = opts.paymentMethodId;
75
- else if (opts.cardToken) payload.card_token = opts.cardToken;
76
- else throw new BearerTokenError("Provide one of setupSessionId, paymentMethodId, or cardToken.");
77
- const { status, data } = await postJson("/api/agent-access/verify", payload);
78
- if (status !== 200 || !data.token) {
79
- throw new BearerTokenError(`Verification failed (HTTP ${status}). ${data.hint || data.error || ""}`.trim());
80
- }
81
- const token = data.token;
82
- saveToken(token, parseExpiry(data.expires_at));
83
- return token;
84
- }
85
- function openBrowser(url) {
86
- import("child_process").then(({ exec }) => {
87
- const cmd = platform() === "win32" ? `start "" "${url}"` : platform() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
88
- exec(cmd, () => {
89
- });
90
- }).catch(() => {
91
- });
92
- }
93
- async function paymentAuth(openBrowserFlag = true) {
94
- console.log("\n Connecting to LetsFG...");
95
- const data = await requestEnrolment();
96
- const setupUrl = data.setup_url;
97
- const sessionId = data.setup_session_id;
98
- if (!setupUrl || !sessionId) {
99
- throw new BearerTokenError("Server did not return a setup URL. Check https://letsfg.co/for-agents");
100
- }
101
- console.log("\n LetsFG needs a payment method on file before it can search or book.");
102
- console.log(" Nothing is charged now \u2014 this is a zero-amount card setup.\n");
103
- console.log(" Step 1 \u2014 add a card here:\n");
104
- console.log(` ${setupUrl}
105
- `);
106
- if (openBrowserFlag) openBrowser(setupUrl);
107
- console.log(" Step 2 \u2014 press Enter once you've finished.");
108
- const readline = await import("readline/promises");
109
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
110
- await rl.question("");
111
- rl.close();
112
- process.stdout.write(" Verifying... ");
113
- const token = await verifyPaymentMethod({ setupSessionId: sessionId });
114
- const expiresAt = loadConfig().pfs_auth?.expires_at ?? Date.now() + TOKEN_TTL_MS;
115
- console.log(`done. Token valid until ${new Date(expiresAt).toISOString().slice(0, 10)}.`);
116
- return token;
117
- }
118
-
119
- export {
120
- BearerTokenError,
121
- getBearerToken,
122
- saveToken,
123
- requestEnrolment,
124
- verifyPaymentMethod,
125
- paymentAuth
126
- };