letsfg 2026.5.70 → 2026.5.72
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 +78 -18
- package/dist/{chunk-SYNHNDEL.mjs → chunk-GO3FXQXC.mjs} +26 -11
- package/dist/cli.js +250 -169
- package/dist/cli.mjs +225 -23
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +26 -11
- package/dist/index.mjs +1 -1
- package/package.json +56 -56
- package/dist/auth-SYZ5AYZ6.mjs +0 -16
- package/dist/chunk-XNO2W77S.mjs +0 -126
package/dist/cli.mjs
CHANGED
|
@@ -3,12 +3,213 @@ import {
|
|
|
3
3
|
LetsFG,
|
|
4
4
|
LetsFGError,
|
|
5
5
|
offerSummary
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "
|
|
6
|
+
} from "./chunk-GO3FXQXC.mjs";
|
|
7
|
+
|
|
8
|
+
// src/auth.ts
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
|
|
10
|
+
import { homedir, platform } from "os";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
import { createHash, randomBytes } from "crypto";
|
|
13
|
+
import { createServer } from "http";
|
|
14
|
+
var BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
|
|
15
|
+
var DEV_ROOT = `${BASE_URL}/developers/api`;
|
|
16
|
+
var FALLBACK_TTL_MS = 55 * 60 * 1e3;
|
|
17
|
+
var REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
18
|
+
var BearerTokenError = class extends Error {
|
|
19
|
+
};
|
|
20
|
+
function configPath() {
|
|
21
|
+
const base = platform() === "win32" ? process.env.APPDATA || homedir() : homedir();
|
|
22
|
+
return join(base, ".letsfg", "config.json");
|
|
23
|
+
}
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
const p = configPath();
|
|
26
|
+
if (!existsSync(p)) return {};
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function saveConfig(cfg) {
|
|
34
|
+
const p = configPath();
|
|
35
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
36
|
+
writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
37
|
+
try {
|
|
38
|
+
chmodSync(p, 384);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function saveToken(token, expiresAt, extra) {
|
|
43
|
+
const cfg = loadConfig();
|
|
44
|
+
cfg.pfs_auth = {
|
|
45
|
+
...cfg.pfs_auth ?? {},
|
|
46
|
+
...extra ?? {},
|
|
47
|
+
token,
|
|
48
|
+
expires_at: expiresAt ?? Date.now() + FALLBACK_TTL_MS
|
|
49
|
+
};
|
|
50
|
+
saveConfig(cfg);
|
|
51
|
+
}
|
|
52
|
+
function getBearerToken() {
|
|
53
|
+
const env = process.env.LETSFG_BEARER_TOKEN;
|
|
54
|
+
if (env) return env;
|
|
55
|
+
const auth = loadConfig().pfs_auth;
|
|
56
|
+
if (auth?.token && Date.now() < auth.expires_at - REFRESH_SKEW_MS) {
|
|
57
|
+
return auth.token;
|
|
58
|
+
}
|
|
59
|
+
if (auth?.refresh_token) {
|
|
60
|
+
throw new BearerTokenError(
|
|
61
|
+
"LetsFG token expired. Call ensureBearerToken() to refresh it, or run: letsfg auth"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
throw new BearerTokenError(
|
|
65
|
+
"No valid LetsFG Bearer token.\n Run: letsfg auth (connects a card at letsfg.co/connect - nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
async function discover() {
|
|
69
|
+
const fallback = {
|
|
70
|
+
authorization_endpoint: `${BASE_URL}/connect`,
|
|
71
|
+
token_endpoint: `${DEV_ROOT}/oauth/token`,
|
|
72
|
+
registration_endpoint: `${DEV_ROOT}/oauth/register`
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
const resp = await fetch(`${DEV_ROOT}/.well-known/oauth-authorization-server`);
|
|
76
|
+
if (!resp.ok) return fallback;
|
|
77
|
+
const d = await resp.json();
|
|
78
|
+
return {
|
|
79
|
+
authorization_endpoint: d.authorization_endpoint || fallback.authorization_endpoint,
|
|
80
|
+
token_endpoint: d.token_endpoint || fallback.token_endpoint,
|
|
81
|
+
registration_endpoint: d.registration_endpoint || fallback.registration_endpoint
|
|
82
|
+
};
|
|
83
|
+
} catch {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function pkce() {
|
|
88
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
89
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
90
|
+
return { verifier, challenge };
|
|
91
|
+
}
|
|
92
|
+
async function postForm(url, form) {
|
|
93
|
+
const resp = await fetch(url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
// RFC 6749 4.1.3 - the token endpoint takes form encoding. The server also
|
|
96
|
+
// accepts JSON, but sending the spec encoding keeps this honest.
|
|
97
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Client-Type": "js-sdk" },
|
|
98
|
+
body: new URLSearchParams(form).toString()
|
|
99
|
+
});
|
|
100
|
+
const data = await resp.json().catch(() => ({}));
|
|
101
|
+
return { status: resp.status, data };
|
|
102
|
+
}
|
|
103
|
+
function openBrowser(url) {
|
|
104
|
+
import("child_process").then(({ exec }) => {
|
|
105
|
+
const cmd = platform() === "win32" ? `start "" "${url}"` : platform() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
106
|
+
exec(cmd, () => {
|
|
107
|
+
});
|
|
108
|
+
}).catch(() => {
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function startCallbackServer(expectedState) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
let settle = null;
|
|
114
|
+
let fail = null;
|
|
115
|
+
const codePromise = new Promise((res, rej) => {
|
|
116
|
+
settle = res;
|
|
117
|
+
fail = rej;
|
|
118
|
+
});
|
|
119
|
+
const server = createServer((req, res) => {
|
|
120
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
121
|
+
if (!url.pathname.startsWith("/callback")) {
|
|
122
|
+
res.writeHead(404).end();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const err = url.searchParams.get("error");
|
|
126
|
+
const code = url.searchParams.get("code");
|
|
127
|
+
const state = url.searchParams.get("state");
|
|
128
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
129
|
+
if (err || !code) {
|
|
130
|
+
res.end("<h1>LetsFG</h1><p>That did not complete. Nothing was charged - run <code>letsfg auth</code> again.</p>");
|
|
131
|
+
fail?.(new BearerTokenError(`Authorisation failed: ${err || "no code returned"}`));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (state !== expectedState) {
|
|
135
|
+
res.end("<h1>LetsFG</h1><p>State mismatch - nothing was charged. Run <code>letsfg auth</code> again.</p>");
|
|
136
|
+
fail?.(new BearerTokenError("State mismatch on the OAuth callback - aborted."));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
res.end("<h1>LetsFG</h1><p>Connected. You can close this tab and go back to your terminal.</p>");
|
|
140
|
+
settle?.(code);
|
|
141
|
+
});
|
|
142
|
+
server.on("error", reject);
|
|
143
|
+
server.listen(0, "127.0.0.1", () => {
|
|
144
|
+
const addr = server.address();
|
|
145
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
146
|
+
if (!port) {
|
|
147
|
+
reject(new BearerTokenError("Could not open a local callback port."));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
resolve({ port, waitForCode: () => codePromise, close: () => server.close() });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function connectAuth(openBrowserFlag = true) {
|
|
155
|
+
const meta = await discover();
|
|
156
|
+
const { verifier, challenge } = pkce();
|
|
157
|
+
const state = randomBytes(16).toString("base64url");
|
|
158
|
+
const { port, waitForCode, close } = await startCallbackServer(state);
|
|
159
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
160
|
+
try {
|
|
161
|
+
const reg = await fetch(meta.registration_endpoint, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "Content-Type": "application/json" },
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
client_name: "letsfg-cli",
|
|
166
|
+
redirect_uris: [redirectUri],
|
|
167
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
168
|
+
response_types: ["code"],
|
|
169
|
+
token_endpoint_auth_method: "none"
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
const regData = await reg.json().catch(() => ({}));
|
|
173
|
+
const clientId = typeof regData.client_id === "string" ? regData.client_id : "";
|
|
174
|
+
if (!reg.ok || !clientId) {
|
|
175
|
+
throw new BearerTokenError(
|
|
176
|
+
`Could not register with LetsFG (HTTP ${reg.status}). See ${BASE_URL}/for-agents`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const authUrl = `${meta.authorization_endpoint}?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256&state=${encodeURIComponent(state)}&scope=flights`;
|
|
180
|
+
console.log("\n LetsFG needs a card connected before it can search or book.");
|
|
181
|
+
console.log(" Nothing is charged now - you pay the fare only when you book,");
|
|
182
|
+
console.log(" and it is held, not taken, until the airline confirms.\n");
|
|
183
|
+
console.log(" Open this and add a card (or pay 0.00 with Revolut Pay):\n");
|
|
184
|
+
console.log(` ${authUrl}
|
|
185
|
+
`);
|
|
186
|
+
if (openBrowserFlag) openBrowser(authUrl);
|
|
187
|
+
process.stdout.write(" Waiting for you to finish... ");
|
|
188
|
+
const code = await waitForCode();
|
|
189
|
+
const { status, data } = await postForm(meta.token_endpoint, {
|
|
190
|
+
grant_type: "authorization_code",
|
|
191
|
+
code,
|
|
192
|
+
client_id: clientId,
|
|
193
|
+
redirect_uri: redirectUri,
|
|
194
|
+
code_verifier: verifier
|
|
195
|
+
});
|
|
196
|
+
if (status !== 200 || !data.access_token) {
|
|
197
|
+
throw new BearerTokenError(
|
|
198
|
+
`Could not complete authentication (HTTP ${status}). ${String(data.error_description || data.error || "")}`.trim()
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const expiresIn = typeof data.expires_in === "number" ? data.expires_in * 1e3 : FALLBACK_TTL_MS;
|
|
202
|
+
saveToken(String(data.access_token), Date.now() + expiresIn, {
|
|
203
|
+
refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : void 0,
|
|
204
|
+
client_id: clientId
|
|
205
|
+
});
|
|
206
|
+
console.log("done. Card connected - the token refreshes itself from now on.");
|
|
207
|
+
return String(data.access_token);
|
|
208
|
+
} finally {
|
|
209
|
+
close();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
var paymentAuth = connectAuth;
|
|
12
213
|
|
|
13
214
|
// src/cli.ts
|
|
14
215
|
function resolveCredentials(apiKeyFlag) {
|
|
@@ -246,17 +447,19 @@ async function cmdLocations(args) {
|
|
|
246
447
|
}
|
|
247
448
|
}
|
|
248
449
|
async function cmdAuth(args) {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
450
|
+
for (const dead of ["--card-token", "--payment-method"]) {
|
|
451
|
+
if (getFlag(args, dead)) {
|
|
452
|
+
console.error(
|
|
453
|
+
`
|
|
454
|
+
${dead} was part of the Stripe enrolment, retired 2026-09-02.
|
|
455
|
+
Run plain \`letsfg auth\` instead - it opens letsfg.co/connect to add a card.
|
|
456
|
+
`
|
|
457
|
+
);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
258
460
|
}
|
|
259
|
-
|
|
461
|
+
await paymentAuth(!hasFlag(args, "--no-browser"));
|
|
462
|
+
console.log("\n You are all set. Run: letsfg search WAW BCN 2026-07-15\n");
|
|
260
463
|
}
|
|
261
464
|
async function cmdRegister(args) {
|
|
262
465
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
@@ -334,11 +537,12 @@ var HELP = `
|
|
|
334
537
|
LetsFG \u2014 Agent-native flight search & booking.
|
|
335
538
|
|
|
336
539
|
Search hundreds of airlines via the LetsFG cloud engine.
|
|
337
|
-
Authenticate once with letsfg auth \u2014
|
|
338
|
-
charged
|
|
540
|
+
Authenticate once with letsfg auth \u2014 it opens letsfg.co/connect so you can add
|
|
541
|
+
a card. Nothing is charged; you pay the fare only when you book, and it is held,
|
|
542
|
+
not taken, until the airline confirms.
|
|
339
543
|
|
|
340
544
|
Commands:
|
|
341
|
-
auth
|
|
545
|
+
auth Connect a card at letsfg.co/connect. Nothing charged
|
|
342
546
|
search <origin> <dest> <date> Search for flights (free), prints search_id
|
|
343
547
|
locations <query> Resolve city name to IATA codes
|
|
344
548
|
book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
|
|
@@ -348,15 +552,13 @@ Developer API only (a SEPARATE paid product \u2014 most agents should not use th
|
|
|
348
552
|
they create a billing account. Use auth above instead):
|
|
349
553
|
register --name ... --email ... Create a paid Developer API account
|
|
350
554
|
setup-payment Attach a card to that paid account
|
|
351
|
-
unlock <offer_id> [Developer API only] Unlock offer
|
|
555
|
+
unlock <offer_id> [Developer API only] Unlock offer (legacy)
|
|
352
556
|
|
|
353
557
|
Options:
|
|
354
558
|
--json, -j Output raw JSON
|
|
355
559
|
--api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
|
|
356
560
|
--base-url API URL (default: https://letsfg.co)
|
|
357
|
-
--
|
|
358
|
-
--payment-method (auth only) Stripe pm_... you already hold, for a headless auth
|
|
359
|
-
--no-browser (auth only) Don't try to auto-open the card setup page
|
|
561
|
+
--no-browser (auth only) Print the connect URL instead of opening a browser
|
|
360
562
|
|
|
361
563
|
Examples:
|
|
362
564
|
letsfg auth
|
package/dist/index.d.mts
CHANGED
|
@@ -283,7 +283,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
|
|
|
283
283
|
* const flights = await bt.search('GDN', 'BER', '2026-03-03');
|
|
284
284
|
*
|
|
285
285
|
* // Developer API (prepaid credits)
|
|
286
|
-
* const bt2 = new LetsFG({ apiKey: '
|
|
286
|
+
* const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
|
|
287
287
|
* const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
|
|
288
288
|
* ```
|
|
289
289
|
*/
|
|
@@ -473,7 +473,7 @@ declare class LetsFG {
|
|
|
473
473
|
*
|
|
474
474
|
* Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
|
|
475
475
|
* PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
|
|
476
|
-
* Developer API: synchronous
|
|
476
|
+
* Developer API: synchronous call.
|
|
477
477
|
*
|
|
478
478
|
* @param origin - IATA code (e.g., "GDN", "LON")
|
|
479
479
|
* @param destination - IATA code (e.g., "BER", "BCN")
|
|
@@ -496,8 +496,8 @@ declare class LetsFG {
|
|
|
496
496
|
resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
|
|
497
497
|
/**
|
|
498
498
|
* Unlock a flight offer — confirms live price, reveals direct airline booking URL.
|
|
499
|
-
*
|
|
500
|
-
*
|
|
499
|
+
* Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
|
|
500
|
+
* token, so PFS callers use book() directly.
|
|
501
501
|
*/
|
|
502
502
|
unlock(offerId: string): Promise<UnlockResult>;
|
|
503
503
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -283,7 +283,7 @@ declare function getOfferDetailPromptNotes(offer: OfferDetailLike): string[];
|
|
|
283
283
|
* const flights = await bt.search('GDN', 'BER', '2026-03-03');
|
|
284
284
|
*
|
|
285
285
|
* // Developer API (prepaid credits)
|
|
286
|
-
* const bt2 = new LetsFG({ apiKey: '
|
|
286
|
+
* const bt2 = new LetsFG({ apiKey: 'letsfg_...' });
|
|
287
287
|
* const flights2 = await bt2.search('LHR', 'JFK', '2026-04-15');
|
|
288
288
|
* ```
|
|
289
289
|
*/
|
|
@@ -473,7 +473,7 @@ declare class LetsFG {
|
|
|
473
473
|
*
|
|
474
474
|
* Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
|
|
475
475
|
* PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
|
|
476
|
-
* Developer API: synchronous
|
|
476
|
+
* Developer API: synchronous call.
|
|
477
477
|
*
|
|
478
478
|
* @param origin - IATA code (e.g., "GDN", "LON")
|
|
479
479
|
* @param destination - IATA code (e.g., "BER", "BCN")
|
|
@@ -496,8 +496,8 @@ declare class LetsFG {
|
|
|
496
496
|
resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
|
|
497
497
|
/**
|
|
498
498
|
* Unlock a flight offer — confirms live price, reveals direct airline booking URL.
|
|
499
|
-
*
|
|
500
|
-
*
|
|
499
|
+
* Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
|
|
500
|
+
* token, so PFS callers use book() directly.
|
|
501
501
|
*/
|
|
502
502
|
unlock(offerId: string): Promise<UnlockResult>;
|
|
503
503
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1606,8 +1606,12 @@ function cheapestOffer(result) {
|
|
|
1606
1606
|
return result.offers.reduce((min, o) => o.price < min.price ? o : min, result.offers[0]);
|
|
1607
1607
|
}
|
|
1608
1608
|
var DEFAULT_BASE_URL = "https://letsfg.co";
|
|
1609
|
-
var PFS_POLL_INTERVAL_MS =
|
|
1609
|
+
var PFS_POLL_INTERVAL_MS = 2e3;
|
|
1610
1610
|
var PFS_POLL_TIMEOUT_MS = 12e4;
|
|
1611
|
+
var LATE_MERGE_POLL_MS = 3e3;
|
|
1612
|
+
var LATE_MERGE_GRACE_MS = 9e4;
|
|
1613
|
+
var WAIT_FOR_SPLIT = (process.env.LETSFG_WAIT_FOR_SPLIT || "").trim() !== "0";
|
|
1614
|
+
var NON_TERMINAL = ["pending", "searching"];
|
|
1611
1615
|
var LetsFG = class {
|
|
1612
1616
|
bearerToken;
|
|
1613
1617
|
apiKey;
|
|
@@ -1643,7 +1647,7 @@ var LetsFG = class {
|
|
|
1643
1647
|
*
|
|
1644
1648
|
* Uses PFS (Bearer token) or Developer API (X-API-Key) depending on config.
|
|
1645
1649
|
* PFS: async polling (POST /api/search -> poll /api/results/<id> every 10s).
|
|
1646
|
-
* Developer API: synchronous
|
|
1650
|
+
* Developer API: synchronous call.
|
|
1647
1651
|
*
|
|
1648
1652
|
* @param origin - IATA code (e.g., "GDN", "LON")
|
|
1649
1653
|
* @param destination - IATA code (e.g., "BER", "BCN")
|
|
@@ -1675,17 +1679,28 @@ var LetsFG = class {
|
|
|
1675
1679
|
/** PFS path: POST /api/search -> poll /api/results/<id> */
|
|
1676
1680
|
async searchPFS(body) {
|
|
1677
1681
|
const { search_id } = await this.postWithBearer("/api/search", body);
|
|
1682
|
+
const poll = () => this.getNoAuth(`/api/results/${search_id}`);
|
|
1683
|
+
const inbound = (r) => Boolean(r.split_ticket_pending || r.gf_enrich_pending);
|
|
1678
1684
|
const deadline = Date.now() + PFS_POLL_TIMEOUT_MS;
|
|
1685
|
+
let terminal = null;
|
|
1679
1686
|
while (Date.now() < deadline) {
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
if (!["pending", "searching"].includes(result.status)) {
|
|
1685
|
-
return result;
|
|
1687
|
+
const result = await poll();
|
|
1688
|
+
if (!NON_TERMINAL.includes(result.status)) {
|
|
1689
|
+
terminal = result;
|
|
1690
|
+
break;
|
|
1686
1691
|
}
|
|
1692
|
+
await new Promise((r) => setTimeout(r, PFS_POLL_INTERVAL_MS));
|
|
1693
|
+
}
|
|
1694
|
+
if (!terminal) {
|
|
1695
|
+
throw new LetsFGError("Search timed out after 120s. Try polling /api/results/<id> directly.", 504);
|
|
1696
|
+
}
|
|
1697
|
+
const lateDeadline = Date.now() + LATE_MERGE_GRACE_MS;
|
|
1698
|
+
while (WAIT_FOR_SPLIT && inbound(terminal) && Date.now() < lateDeadline) {
|
|
1699
|
+
await new Promise((r) => setTimeout(r, LATE_MERGE_POLL_MS));
|
|
1700
|
+
const merged = await poll();
|
|
1701
|
+
if (!NON_TERMINAL.includes(merged.status)) terminal = merged;
|
|
1687
1702
|
}
|
|
1688
|
-
|
|
1703
|
+
return terminal;
|
|
1689
1704
|
}
|
|
1690
1705
|
/**
|
|
1691
1706
|
* Resolve a city/airport name to IATA codes.
|
|
@@ -1705,8 +1720,8 @@ var LetsFG = class {
|
|
|
1705
1720
|
}
|
|
1706
1721
|
/**
|
|
1707
1722
|
* Unlock a flight offer — confirms live price, reveals direct airline booking URL.
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1723
|
+
* Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
|
|
1724
|
+
* token, so PFS callers use book() directly.
|
|
1710
1725
|
*/
|
|
1711
1726
|
async unlock(offerId) {
|
|
1712
1727
|
this.requireApiKey();
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "letsfg",
|
|
3
|
-
"version": "2026.5.
|
|
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.72",
|
|
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
|
+
}
|
package/dist/auth-SYZ5AYZ6.mjs
DELETED
|
@@ -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
|
-
};
|