letsfg 2026.5.65 → 2026.5.67
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 +43 -32
- package/dist/auth-SYZ5AYZ6.mjs +16 -0
- package/dist/{chunk-DEFFV47W.mjs → chunk-2SNO3AGS.mjs} +4 -4
- package/dist/chunk-XNO2W77S.mjs +126 -0
- package/dist/cli.js +251 -27
- package/dist/cli.mjs +90 -24
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
| | **CLI / SDK** (this package) | **Developer API** |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **Search cost** | Free (
|
|
13
|
-
| **Booking** | `POST /api/agent-book` | Direct airline URL |
|
|
12
|
+
| **Search cost** | Free (Bearer token via `letsfg auth` — zero-amount card setup) | Prepaid credits |
|
|
13
|
+
| **Booking** | `POST /api/agent-book` — confirmed order or a booking link, no LetsFG fee | Direct airline URL (unlock required first) |
|
|
14
14
|
| **Speed** | 60–90 s | 2–5 s (discover) · 60–90 s (full) |
|
|
15
15
|
| **Setup** | `npm install letsfg` then `letsfg auth` | [letsfg.co/developers](https://letsfg.co/developers) |
|
|
16
16
|
|
|
@@ -27,60 +27,60 @@ npm install letsfg
|
|
|
27
27
|
```typescript
|
|
28
28
|
import { LetsFG, cheapestOffer, offerSummary } from 'letsfg';
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Use
|
|
35
|
-
const bt = new LetsFG({ apiKey: 'trav_...' });
|
|
30
|
+
// PFS — free. Get a Bearer token once with `letsfg auth` (zero-amount card
|
|
31
|
+
// setup, nothing charged), then pass it here.
|
|
32
|
+
const bt = new LetsFG({ bearerToken: 'eyJ...' });
|
|
36
33
|
|
|
37
34
|
// Search — FREE
|
|
38
35
|
const flights = await bt.search('GDN', 'BER', '2026-03-03');
|
|
39
36
|
const best = cheapestOffer(flights);
|
|
40
37
|
console.log(offerSummary(best));
|
|
41
38
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
// Book
|
|
46
|
-
const booking = await bt.book(
|
|
39
|
+
// Book — free, ticket price only, no LetsFG fee. No unlock step.
|
|
40
|
+
const result = await bt.book(
|
|
47
41
|
best.id,
|
|
48
|
-
[{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
gender: 'm',
|
|
54
|
-
title: 'mr',
|
|
55
|
-
email: 'john@example.com',
|
|
56
|
-
}],
|
|
57
|
-
'john@example.com'
|
|
42
|
+
[{ given_name: 'John', family_name: 'Doe', born_on: '1990-01-15', gender: 'm' }],
|
|
43
|
+
'john@example.com',
|
|
44
|
+
'',
|
|
45
|
+
'',
|
|
46
|
+
flights.search_id,
|
|
58
47
|
);
|
|
59
|
-
|
|
48
|
+
if (result.booked) {
|
|
49
|
+
console.log(`Order: ${result.order_id}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`Booking link (nothing charged): ${result.booking_url}`);
|
|
52
|
+
}
|
|
60
53
|
```
|
|
61
54
|
|
|
55
|
+
Prefer the paid Developer API instead? Pass `apiKey` instead of `bearerToken` —
|
|
56
|
+
`search()`/`book()` dispatch automatically. That path requires `unlock()`
|
|
57
|
+
(1% fee, min $3) before `book()`.
|
|
58
|
+
|
|
62
59
|
## Quick Start (CLI)
|
|
63
60
|
|
|
64
61
|
```bash
|
|
65
|
-
export LETSFG_BEARER_TOKEN=<your-bearer-token>
|
|
62
|
+
export LETSFG_BEARER_TOKEN=<your-bearer-token> # from `letsfg auth`
|
|
66
63
|
|
|
67
64
|
letsfg search GDN BER 2026-03-03 --sort price
|
|
68
65
|
letsfg search LON BCN 2026-04-01 --json # Machine-readable
|
|
69
|
-
letsfg
|
|
70
|
-
letsfg book off_xxx -p '{"id":"pas_xxx","given_name":"John",...}' -e john@example.com
|
|
66
|
+
letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"John","family_name":"Doe","born_on":"1990-01-15","gender":"m"}' -e john@example.com
|
|
71
67
|
```
|
|
72
68
|
|
|
73
69
|
## API
|
|
74
70
|
|
|
75
|
-
### `new LetsFG({ apiKey
|
|
71
|
+
### `new LetsFG({ bearerToken?, apiKey?, baseUrl?, timeout? })`
|
|
76
72
|
|
|
77
73
|
### `bt.search(origin, destination, dateFrom, options?)`
|
|
78
74
|
### `bt.resolveLocation(query)`
|
|
79
|
-
### `bt.unlock(offerId)`
|
|
80
|
-
### `bt.book(offerId, passengers, contactEmail, contactPhone?)`
|
|
81
|
-
|
|
75
|
+
### `bt.unlock(offerId)` — Developer API only
|
|
76
|
+
### `bt.book(offerId, passengers, contactEmail, contactPhone?, idempotencyKey?, searchId?)`
|
|
77
|
+
Dispatches on which credential is set: `bearerToken` → free PFS booking via
|
|
78
|
+
`POST /api/agent-book` (pass `searchId`, one passenger). `apiKey` → paid
|
|
79
|
+
Developer API `book` (requires `unlock()` first, supports multiple passengers
|
|
80
|
+
and `idempotencyKey`).
|
|
81
|
+
### `bt.setupPayment(token?)` — Developer API only
|
|
82
82
|
### `bt.me()`
|
|
83
|
-
### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)`
|
|
83
|
+
### `LetsFG.register(agentName, email, baseUrl?, ownerName?, description?)` — Developer API only, most agents don't need this
|
|
84
84
|
|
|
85
85
|
### Helpers
|
|
86
86
|
- `offerSummary(offer)` — One-line string summary
|
|
@@ -150,6 +150,17 @@ else; after it, the hotel's own cancellation ladder applies and can reach 100%.
|
|
|
150
150
|
That ladder ships in the booking's `terms`, so you can always see the cost before
|
|
151
151
|
you cancel.
|
|
152
152
|
|
|
153
|
+
### What search costs
|
|
154
|
+
|
|
155
|
+
Search is metered separately from booking, on **either** auth path (free PFS
|
|
156
|
+
Bearer token or Developer API key — both count against the same agent):
|
|
157
|
+
**the first 1,000 `search_hotels` calls since your last hotel booking are
|
|
158
|
+
free.** Past that, searches are billed in blocks of 1,000 for **$5**
|
|
159
|
+
(~$0.005/search) from your prepaid balance — refused with a 402 if the
|
|
160
|
+
balance can't cover the next block, never silently allowed. Book a hotel
|
|
161
|
+
and the count resets to zero. Resolving a city name (`hotel_destinations`)
|
|
162
|
+
is not metered, only the search call itself.
|
|
163
|
+
|
|
153
164
|
### Things worth knowing before you build
|
|
154
165
|
|
|
155
166
|
- **A card on file is required for every hotel call, including search.** That is
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
};
|
|
@@ -1629,12 +1629,12 @@ var LetsFG = class {
|
|
|
1629
1629
|
}
|
|
1630
1630
|
/**
|
|
1631
1631
|
* Unlock a flight offer — confirms live price, reveals direct airline booking URL.
|
|
1632
|
-
* Cost: 1% of ticket price, min $3.
|
|
1632
|
+
* Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
|
|
1633
|
+
* endpoint on a PFS Bearer token, so PFS callers use book() directly.
|
|
1633
1634
|
*/
|
|
1634
1635
|
async unlock(offerId) {
|
|
1635
|
-
this.
|
|
1636
|
-
|
|
1637
|
-
return this.postWithAuth(path, { offer_id: offerId });
|
|
1636
|
+
this.requireApiKey();
|
|
1637
|
+
return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
|
|
1638
1638
|
}
|
|
1639
1639
|
/**
|
|
1640
1640
|
* Book a flight.
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,167 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// src/auth.ts
|
|
34
|
+
var auth_exports = {};
|
|
35
|
+
__export(auth_exports, {
|
|
36
|
+
BearerTokenError: () => BearerTokenError,
|
|
37
|
+
getBearerToken: () => getBearerToken,
|
|
38
|
+
paymentAuth: () => paymentAuth,
|
|
39
|
+
requestEnrolment: () => requestEnrolment,
|
|
40
|
+
saveToken: () => saveToken,
|
|
41
|
+
verifyPaymentMethod: () => verifyPaymentMethod
|
|
42
|
+
});
|
|
43
|
+
function configPath() {
|
|
44
|
+
const base = (0, import_node_os.platform)() === "win32" ? process.env.APPDATA || (0, import_node_os.homedir)() : (0, import_node_os.homedir)();
|
|
45
|
+
return (0, import_node_path.join)(base, ".letsfg", "config.json");
|
|
46
|
+
}
|
|
47
|
+
function loadConfig() {
|
|
48
|
+
const p = configPath();
|
|
49
|
+
if (!(0, import_node_fs.existsSync)(p)) return {};
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function saveConfig(cfg) {
|
|
57
|
+
const p = configPath();
|
|
58
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(p), { recursive: true });
|
|
59
|
+
(0, import_node_fs.writeFileSync)(p, JSON.stringify(cfg, null, 2));
|
|
60
|
+
try {
|
|
61
|
+
(0, import_node_fs.chmodSync)(p, 384);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function getBearerToken() {
|
|
66
|
+
const env = process.env.LETSFG_BEARER_TOKEN;
|
|
67
|
+
if (env) return env;
|
|
68
|
+
const auth = loadConfig().pfs_auth;
|
|
69
|
+
if (auth?.token && Date.now() < auth.expires_at - 36e5) {
|
|
70
|
+
return auth.token;
|
|
71
|
+
}
|
|
72
|
+
throw new BearerTokenError(
|
|
73
|
+
"No valid LetsFG Bearer token.\n Run: letsfg auth (adds a payment method \u2014 nothing is charged)\n Or: export LETSFG_BEARER_TOKEN=<token>"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function saveToken(token, expiresAt) {
|
|
77
|
+
const cfg = loadConfig();
|
|
78
|
+
cfg.pfs_auth = { token, expires_at: expiresAt ?? Date.now() + TOKEN_TTL_MS };
|
|
79
|
+
saveConfig(cfg);
|
|
80
|
+
}
|
|
81
|
+
async function postJson(path, payload) {
|
|
82
|
+
const resp = await fetch(`${BASE_URL}${path}`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json", "User-Agent": "LetsFG-js/0.1.0", "X-Client-Type": "js-sdk" },
|
|
85
|
+
body: JSON.stringify(payload)
|
|
86
|
+
});
|
|
87
|
+
const data = await resp.json().catch(() => ({}));
|
|
88
|
+
return { status: resp.status, data };
|
|
89
|
+
}
|
|
90
|
+
function parseExpiry(raw) {
|
|
91
|
+
if (typeof raw === "number") return raw * 1e3;
|
|
92
|
+
if (typeof raw === "string") {
|
|
93
|
+
const ms = Date.parse(raw);
|
|
94
|
+
if (!Number.isNaN(ms)) return ms;
|
|
95
|
+
}
|
|
96
|
+
return Date.now() + TOKEN_TTL_MS;
|
|
97
|
+
}
|
|
98
|
+
async function requestEnrolment() {
|
|
99
|
+
const { status, data } = await postJson("/api/agent-access/request", {});
|
|
100
|
+
if (status !== 200 && status !== 402) {
|
|
101
|
+
throw new BearerTokenError(`Could not start authentication (HTTP ${status}): ${data.error || ""}`);
|
|
102
|
+
}
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
async function verifyPaymentMethod(opts) {
|
|
106
|
+
const payload = {};
|
|
107
|
+
if (opts.setupSessionId) payload.setup_session_id = opts.setupSessionId;
|
|
108
|
+
else if (opts.paymentMethodId) payload.payment_method_id = opts.paymentMethodId;
|
|
109
|
+
else if (opts.cardToken) payload.card_token = opts.cardToken;
|
|
110
|
+
else throw new BearerTokenError("Provide one of setupSessionId, paymentMethodId, or cardToken.");
|
|
111
|
+
const { status, data } = await postJson("/api/agent-access/verify", payload);
|
|
112
|
+
if (status !== 200 || !data.token) {
|
|
113
|
+
throw new BearerTokenError(`Verification failed (HTTP ${status}). ${data.hint || data.error || ""}`.trim());
|
|
114
|
+
}
|
|
115
|
+
const token = data.token;
|
|
116
|
+
saveToken(token, parseExpiry(data.expires_at));
|
|
117
|
+
return token;
|
|
118
|
+
}
|
|
119
|
+
function openBrowser(url) {
|
|
120
|
+
import("child_process").then(({ exec }) => {
|
|
121
|
+
const cmd = (0, import_node_os.platform)() === "win32" ? `start "" "${url}"` : (0, import_node_os.platform)() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
122
|
+
exec(cmd, () => {
|
|
123
|
+
});
|
|
124
|
+
}).catch(() => {
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async function paymentAuth(openBrowserFlag = true) {
|
|
128
|
+
console.log("\n Connecting to LetsFG...");
|
|
129
|
+
const data = await requestEnrolment();
|
|
130
|
+
const setupUrl = data.setup_url;
|
|
131
|
+
const sessionId = data.setup_session_id;
|
|
132
|
+
if (!setupUrl || !sessionId) {
|
|
133
|
+
throw new BearerTokenError("Server did not return a setup URL. Check https://letsfg.co/for-agents");
|
|
134
|
+
}
|
|
135
|
+
console.log("\n LetsFG needs a payment method on file before it can search or book.");
|
|
136
|
+
console.log(" Nothing is charged now \u2014 this is a zero-amount card setup.\n");
|
|
137
|
+
console.log(" Step 1 \u2014 add a card here:\n");
|
|
138
|
+
console.log(` ${setupUrl}
|
|
139
|
+
`);
|
|
140
|
+
if (openBrowserFlag) openBrowser(setupUrl);
|
|
141
|
+
console.log(" Step 2 \u2014 press Enter once you've finished.");
|
|
142
|
+
const readline = await import("readline/promises");
|
|
143
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
144
|
+
await rl.question("");
|
|
145
|
+
rl.close();
|
|
146
|
+
process.stdout.write(" Verifying... ");
|
|
147
|
+
const token = await verifyPaymentMethod({ setupSessionId: sessionId });
|
|
148
|
+
const expiresAt = loadConfig().pfs_auth?.expires_at ?? Date.now() + TOKEN_TTL_MS;
|
|
149
|
+
console.log(`done. Token valid until ${new Date(expiresAt).toISOString().slice(0, 10)}.`);
|
|
150
|
+
return token;
|
|
151
|
+
}
|
|
152
|
+
var import_node_fs, import_node_os, import_node_path, BASE_URL, TOKEN_TTL_MS, BearerTokenError;
|
|
153
|
+
var init_auth = __esm({
|
|
154
|
+
"src/auth.ts"() {
|
|
155
|
+
"use strict";
|
|
156
|
+
import_node_fs = require("fs");
|
|
157
|
+
import_node_os = require("os");
|
|
158
|
+
import_node_path = require("path");
|
|
159
|
+
BASE_URL = process.env.LETSFG_BASE_URL || "https://letsfg.co";
|
|
160
|
+
TOKEN_TTL_MS = 90 * 24 * 3600 * 1e3;
|
|
161
|
+
BearerTokenError = class extends Error {
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
});
|
|
3
165
|
|
|
4
166
|
// src/ranking.ts
|
|
5
167
|
var W = {
|
|
@@ -384,12 +546,12 @@ var LetsFG = class {
|
|
|
384
546
|
}
|
|
385
547
|
/**
|
|
386
548
|
* Unlock a flight offer — confirms live price, reveals direct airline booking URL.
|
|
387
|
-
* Cost: 1% of ticket price, min $3.
|
|
549
|
+
* Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
|
|
550
|
+
* endpoint on a PFS Bearer token, so PFS callers use book() directly.
|
|
388
551
|
*/
|
|
389
552
|
async unlock(offerId) {
|
|
390
|
-
this.
|
|
391
|
-
|
|
392
|
-
return this.postWithAuth(path, { offer_id: offerId });
|
|
553
|
+
this.requireApiKey();
|
|
554
|
+
return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
|
|
393
555
|
}
|
|
394
556
|
/**
|
|
395
557
|
* Book a flight.
|
|
@@ -684,6 +846,16 @@ var LetsFG = class {
|
|
|
684
846
|
};
|
|
685
847
|
|
|
686
848
|
// src/cli.ts
|
|
849
|
+
init_auth();
|
|
850
|
+
function resolveCredentials(apiKeyFlag) {
|
|
851
|
+
const apiKey = apiKeyFlag || process.env.LETSFG_API_KEY;
|
|
852
|
+
if (apiKey) return { apiKey };
|
|
853
|
+
try {
|
|
854
|
+
return { bearerToken: getBearerToken() };
|
|
855
|
+
} catch {
|
|
856
|
+
return {};
|
|
857
|
+
}
|
|
858
|
+
}
|
|
687
859
|
function getFlag(args, flag, alias) {
|
|
688
860
|
for (let i = 0; i < args.length; i++) {
|
|
689
861
|
if (args[i] === flag || alias && args[i] === alias) {
|
|
@@ -738,7 +910,8 @@ async function cmdSearch(args) {
|
|
|
738
910
|
console.error("Usage: letsfg search <origin> <destination> <date> [options]");
|
|
739
911
|
process.exit(1);
|
|
740
912
|
}
|
|
741
|
-
const
|
|
913
|
+
const creds = resolveCredentials(apiKey);
|
|
914
|
+
const bt = new LetsFG({ ...creds, baseUrl });
|
|
742
915
|
const result = await bt.search(origin, destination, date, {
|
|
743
916
|
returnDate,
|
|
744
917
|
adults,
|
|
@@ -752,6 +925,7 @@ async function cmdSearch(args) {
|
|
|
752
925
|
});
|
|
753
926
|
if (jsonOut) {
|
|
754
927
|
console.log(JSON.stringify({
|
|
928
|
+
search_id: result.search_id,
|
|
755
929
|
passenger_ids: result.passenger_ids,
|
|
756
930
|
total_results: result.total_results,
|
|
757
931
|
offers: result.offers.map((o) => ({
|
|
@@ -775,16 +949,25 @@ async function cmdSearch(args) {
|
|
|
775
949
|
}
|
|
776
950
|
console.log(`
|
|
777
951
|
${result.total_results} offers | ${origin} \u2192 ${destination} | ${date}`);
|
|
952
|
+
if (result.search_id) {
|
|
953
|
+
console.log(` search_id: ${result.search_id} (needed for \`letsfg book\`, offers expire ~15 min after search)`);
|
|
954
|
+
}
|
|
778
955
|
console.log(` Passenger IDs: ${JSON.stringify(result.passenger_ids)}
|
|
779
956
|
`);
|
|
780
957
|
result.offers.forEach((o, i) => {
|
|
781
958
|
console.log(` ${(i + 1).toString().padStart(3)}. ${offerSummary(o)}`);
|
|
782
959
|
console.log(` ID: ${o.id}`);
|
|
783
960
|
});
|
|
784
|
-
|
|
961
|
+
if (creds.bearerToken) {
|
|
962
|
+
console.log(`
|
|
963
|
+
To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
|
|
964
|
+
`);
|
|
965
|
+
} else {
|
|
966
|
+
console.log(`
|
|
785
967
|
To unlock: letsfg unlock <offer_id>`);
|
|
786
|
-
|
|
968
|
+
console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
|
|
787
969
|
`);
|
|
970
|
+
}
|
|
788
971
|
}
|
|
789
972
|
async function cmdUnlock(args) {
|
|
790
973
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
@@ -818,33 +1001,55 @@ async function cmdBook(args) {
|
|
|
818
1001
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
819
1002
|
const apiKey = getFlag(args, "--api-key", "-k");
|
|
820
1003
|
const baseUrl = getFlag(args, "--base-url");
|
|
1004
|
+
const searchId = getFlag(args, "--search-id");
|
|
821
1005
|
const email = getFlag(args, "--email", "-e") || "";
|
|
822
1006
|
const phone = getFlag(args, "--phone") || "";
|
|
823
1007
|
const passengerStrs = getAllFlags(args, "--passenger", "-p");
|
|
824
1008
|
const offerId = args[0];
|
|
825
1009
|
if (!offerId || !passengerStrs.length || !email) {
|
|
826
|
-
console.error(`Usage: letsfg book <offer_id> --passenger '{"
|
|
1010
|
+
console.error(`Usage: letsfg book <offer_id> --search-id <id> --passenger '{"given_name":"John",...}' --email you@example.com`);
|
|
827
1011
|
process.exit(1);
|
|
828
1012
|
}
|
|
829
1013
|
const passengers = passengerStrs.map((s) => JSON.parse(s));
|
|
830
|
-
const
|
|
831
|
-
|
|
1014
|
+
const creds = resolveCredentials(apiKey);
|
|
1015
|
+
if (creds.bearerToken && !searchId) {
|
|
1016
|
+
console.error("Error: --search-id is required (from your `letsfg search` results) to book via the free PFS path.");
|
|
1017
|
+
process.exit(1);
|
|
1018
|
+
}
|
|
1019
|
+
const bt = new LetsFG({ ...creds, baseUrl });
|
|
1020
|
+
const result = await bt.book(offerId, passengers, email, phone, "", searchId);
|
|
832
1021
|
if (jsonOut) {
|
|
833
1022
|
console.log(JSON.stringify(result, null, 2));
|
|
834
1023
|
return;
|
|
835
1024
|
}
|
|
836
|
-
if (
|
|
1025
|
+
if ("booked" in result) {
|
|
1026
|
+
if (result.booked) {
|
|
1027
|
+
console.log(`
|
|
1028
|
+
\u2713 Booking confirmed!`);
|
|
1029
|
+
console.log(` Order ID: ${result.order_id}`);
|
|
1030
|
+
console.log(` Charged: ${result.charged ?? 0} ${result.currency ?? ""}
|
|
1031
|
+
`);
|
|
1032
|
+
} else {
|
|
1033
|
+
console.log(`
|
|
1034
|
+
Could not complete a confirmed booking. Nothing was charged.`);
|
|
1035
|
+
console.log(` Booking link: ${result.booking_url ?? "(none)"}
|
|
1036
|
+
`);
|
|
1037
|
+
}
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const br = result;
|
|
1041
|
+
if (br.status === "confirmed") {
|
|
837
1042
|
console.log(`
|
|
838
1043
|
\u2713 Booking confirmed!`);
|
|
839
|
-
console.log(` PNR: ${
|
|
840
|
-
console.log(` Flight: ${
|
|
841
|
-
console.log(` Fee: ${
|
|
842
|
-
console.log(` Total: ${
|
|
843
|
-
console.log(` Order: ${
|
|
1044
|
+
console.log(` PNR: ${br.booking_reference}`);
|
|
1045
|
+
console.log(` Flight: ${br.currency} ${br.flight_price.toFixed(2)}`);
|
|
1046
|
+
console.log(` Fee: ${br.currency} ${br.service_fee.toFixed(2)} (${br.service_fee_percentage}%)`);
|
|
1047
|
+
console.log(` Total: ${br.currency} ${br.total_charged.toFixed(2)}`);
|
|
1048
|
+
console.log(` Order: ${br.order_id}
|
|
844
1049
|
`);
|
|
845
1050
|
} else {
|
|
846
1051
|
console.error(` \u2717 Booking failed`);
|
|
847
|
-
console.error(JSON.stringify(
|
|
1052
|
+
console.error(JSON.stringify(br.details, null, 2));
|
|
848
1053
|
process.exit(1);
|
|
849
1054
|
}
|
|
850
1055
|
}
|
|
@@ -876,6 +1081,19 @@ async function cmdLocations(args) {
|
|
|
876
1081
|
console.log(` ${iata} ${name} (${type}) \u2014 ${city}, ${country}`);
|
|
877
1082
|
}
|
|
878
1083
|
}
|
|
1084
|
+
async function cmdAuth(args) {
|
|
1085
|
+
const cardToken = getFlag(args, "--card-token");
|
|
1086
|
+
const paymentMethodId = getFlag(args, "--payment-method");
|
|
1087
|
+
const noBrowser = hasFlag(args, "--no-browser");
|
|
1088
|
+
if (cardToken || paymentMethodId) {
|
|
1089
|
+
const { verifyPaymentMethod: verifyPaymentMethod2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
1090
|
+
await verifyPaymentMethod2({ cardToken: cardToken || void 0, paymentMethodId: paymentMethodId || void 0 });
|
|
1091
|
+
console.log("\n \u2713 Authenticated. Nothing was charged.");
|
|
1092
|
+
} else {
|
|
1093
|
+
await paymentAuth(!noBrowser);
|
|
1094
|
+
}
|
|
1095
|
+
console.log("\n You're all set. Run: letsfg search WAW BCN 2026-07-15\n");
|
|
1096
|
+
}
|
|
879
1097
|
async function cmdRegister(args) {
|
|
880
1098
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
881
1099
|
const baseUrl = getFlag(args, "--base-url");
|
|
@@ -956,11 +1174,11 @@ Authenticate once with letsfg auth \u2014 a zero-amount card setup, nothing is
|
|
|
956
1174
|
charged \u2014 then search and book.
|
|
957
1175
|
|
|
958
1176
|
Commands:
|
|
959
|
-
auth
|
|
960
|
-
search <origin> <dest> <date>
|
|
961
|
-
locations <query>
|
|
962
|
-
book <offer_id> --
|
|
963
|
-
me
|
|
1177
|
+
auth Put a card on file -> 90-day token. Nothing charged
|
|
1178
|
+
search <origin> <dest> <date> Search for flights (free), prints search_id
|
|
1179
|
+
locations <query> Resolve city name to IATA codes
|
|
1180
|
+
book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
|
|
1181
|
+
me Show agent profile
|
|
964
1182
|
|
|
965
1183
|
Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
|
|
966
1184
|
they create a billing account. Use auth above instead):
|
|
@@ -969,20 +1187,26 @@ they create a billing account. Use auth above instead):
|
|
|
969
1187
|
unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
|
|
970
1188
|
|
|
971
1189
|
Options:
|
|
972
|
-
--json, -j
|
|
973
|
-
--api-key, -k
|
|
974
|
-
--base-url
|
|
1190
|
+
--json, -j Output raw JSON
|
|
1191
|
+
--api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
|
|
1192
|
+
--base-url API URL (default: https://letsfg.co)
|
|
1193
|
+
--card-token (auth only) Stripe tok_... you already hold, for a headless auth
|
|
1194
|
+
--payment-method (auth only) Stripe pm_... you already hold, for a headless auth
|
|
1195
|
+
--no-browser (auth only) Don't try to auto-open the card setup page
|
|
975
1196
|
|
|
976
1197
|
Examples:
|
|
977
1198
|
letsfg auth
|
|
978
1199
|
letsfg search GDN BER 2026-03-03 --sort price
|
|
979
|
-
letsfg book off_xxx -p '{"given_name":"Ada"
|
|
1200
|
+
letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"Ada","family_name":"Lovelace","born_on":"1990-04-01","gender":"f"}' -e ada@example.com
|
|
980
1201
|
`;
|
|
981
1202
|
async function main() {
|
|
982
1203
|
const args = process.argv.slice(2);
|
|
983
1204
|
const command = args.shift();
|
|
984
1205
|
try {
|
|
985
1206
|
switch (command) {
|
|
1207
|
+
case "auth":
|
|
1208
|
+
await cmdAuth(args);
|
|
1209
|
+
break;
|
|
986
1210
|
case "search":
|
|
987
1211
|
await cmdSearch(args);
|
|
988
1212
|
break;
|
|
@@ -1016,7 +1240,7 @@ async function main() {
|
|
|
1016
1240
|
process.exit(1);
|
|
1017
1241
|
}
|
|
1018
1242
|
} catch (e) {
|
|
1019
|
-
if (e instanceof LetsFGError) {
|
|
1243
|
+
if (e instanceof LetsFGError || e instanceof BearerTokenError) {
|
|
1020
1244
|
console.error(`Error: ${e.message}`);
|
|
1021
1245
|
process.exit(1);
|
|
1022
1246
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -3,9 +3,23 @@ import {
|
|
|
3
3
|
LetsFG,
|
|
4
4
|
LetsFGError,
|
|
5
5
|
offerSummary
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-2SNO3AGS.mjs";
|
|
7
|
+
import {
|
|
8
|
+
BearerTokenError,
|
|
9
|
+
getBearerToken,
|
|
10
|
+
paymentAuth
|
|
11
|
+
} from "./chunk-XNO2W77S.mjs";
|
|
7
12
|
|
|
8
13
|
// src/cli.ts
|
|
14
|
+
function resolveCredentials(apiKeyFlag) {
|
|
15
|
+
const apiKey = apiKeyFlag || process.env.LETSFG_API_KEY;
|
|
16
|
+
if (apiKey) return { apiKey };
|
|
17
|
+
try {
|
|
18
|
+
return { bearerToken: getBearerToken() };
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
9
23
|
function getFlag(args, flag, alias) {
|
|
10
24
|
for (let i = 0; i < args.length; i++) {
|
|
11
25
|
if (args[i] === flag || alias && args[i] === alias) {
|
|
@@ -60,7 +74,8 @@ async function cmdSearch(args) {
|
|
|
60
74
|
console.error("Usage: letsfg search <origin> <destination> <date> [options]");
|
|
61
75
|
process.exit(1);
|
|
62
76
|
}
|
|
63
|
-
const
|
|
77
|
+
const creds = resolveCredentials(apiKey);
|
|
78
|
+
const bt = new LetsFG({ ...creds, baseUrl });
|
|
64
79
|
const result = await bt.search(origin, destination, date, {
|
|
65
80
|
returnDate,
|
|
66
81
|
adults,
|
|
@@ -74,6 +89,7 @@ async function cmdSearch(args) {
|
|
|
74
89
|
});
|
|
75
90
|
if (jsonOut) {
|
|
76
91
|
console.log(JSON.stringify({
|
|
92
|
+
search_id: result.search_id,
|
|
77
93
|
passenger_ids: result.passenger_ids,
|
|
78
94
|
total_results: result.total_results,
|
|
79
95
|
offers: result.offers.map((o) => ({
|
|
@@ -97,16 +113,25 @@ async function cmdSearch(args) {
|
|
|
97
113
|
}
|
|
98
114
|
console.log(`
|
|
99
115
|
${result.total_results} offers | ${origin} \u2192 ${destination} | ${date}`);
|
|
116
|
+
if (result.search_id) {
|
|
117
|
+
console.log(` search_id: ${result.search_id} (needed for \`letsfg book\`, offers expire ~15 min after search)`);
|
|
118
|
+
}
|
|
100
119
|
console.log(` Passenger IDs: ${JSON.stringify(result.passenger_ids)}
|
|
101
120
|
`);
|
|
102
121
|
result.offers.forEach((o, i) => {
|
|
103
122
|
console.log(` ${(i + 1).toString().padStart(3)}. ${offerSummary(o)}`);
|
|
104
123
|
console.log(` ID: ${o.id}`);
|
|
105
124
|
});
|
|
106
|
-
|
|
125
|
+
if (creds.bearerToken) {
|
|
126
|
+
console.log(`
|
|
127
|
+
To book: letsfg book <offer_id> --search-id ${result.search_id} --passenger '{...}' --email you@example.com
|
|
128
|
+
`);
|
|
129
|
+
} else {
|
|
130
|
+
console.log(`
|
|
107
131
|
To unlock: letsfg unlock <offer_id>`);
|
|
108
|
-
|
|
132
|
+
console.log(` Passenger IDs needed for booking: ${JSON.stringify(result.passenger_ids)}
|
|
109
133
|
`);
|
|
134
|
+
}
|
|
110
135
|
}
|
|
111
136
|
async function cmdUnlock(args) {
|
|
112
137
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
@@ -140,33 +165,55 @@ async function cmdBook(args) {
|
|
|
140
165
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
141
166
|
const apiKey = getFlag(args, "--api-key", "-k");
|
|
142
167
|
const baseUrl = getFlag(args, "--base-url");
|
|
168
|
+
const searchId = getFlag(args, "--search-id");
|
|
143
169
|
const email = getFlag(args, "--email", "-e") || "";
|
|
144
170
|
const phone = getFlag(args, "--phone") || "";
|
|
145
171
|
const passengerStrs = getAllFlags(args, "--passenger", "-p");
|
|
146
172
|
const offerId = args[0];
|
|
147
173
|
if (!offerId || !passengerStrs.length || !email) {
|
|
148
|
-
console.error(`Usage: letsfg book <offer_id> --passenger '{"
|
|
174
|
+
console.error(`Usage: letsfg book <offer_id> --search-id <id> --passenger '{"given_name":"John",...}' --email you@example.com`);
|
|
149
175
|
process.exit(1);
|
|
150
176
|
}
|
|
151
177
|
const passengers = passengerStrs.map((s) => JSON.parse(s));
|
|
152
|
-
const
|
|
153
|
-
|
|
178
|
+
const creds = resolveCredentials(apiKey);
|
|
179
|
+
if (creds.bearerToken && !searchId) {
|
|
180
|
+
console.error("Error: --search-id is required (from your `letsfg search` results) to book via the free PFS path.");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const bt = new LetsFG({ ...creds, baseUrl });
|
|
184
|
+
const result = await bt.book(offerId, passengers, email, phone, "", searchId);
|
|
154
185
|
if (jsonOut) {
|
|
155
186
|
console.log(JSON.stringify(result, null, 2));
|
|
156
187
|
return;
|
|
157
188
|
}
|
|
158
|
-
if (
|
|
189
|
+
if ("booked" in result) {
|
|
190
|
+
if (result.booked) {
|
|
191
|
+
console.log(`
|
|
192
|
+
\u2713 Booking confirmed!`);
|
|
193
|
+
console.log(` Order ID: ${result.order_id}`);
|
|
194
|
+
console.log(` Charged: ${result.charged ?? 0} ${result.currency ?? ""}
|
|
195
|
+
`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(`
|
|
198
|
+
Could not complete a confirmed booking. Nothing was charged.`);
|
|
199
|
+
console.log(` Booking link: ${result.booking_url ?? "(none)"}
|
|
200
|
+
`);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const br = result;
|
|
205
|
+
if (br.status === "confirmed") {
|
|
159
206
|
console.log(`
|
|
160
207
|
\u2713 Booking confirmed!`);
|
|
161
|
-
console.log(` PNR: ${
|
|
162
|
-
console.log(` Flight: ${
|
|
163
|
-
console.log(` Fee: ${
|
|
164
|
-
console.log(` Total: ${
|
|
165
|
-
console.log(` Order: ${
|
|
208
|
+
console.log(` PNR: ${br.booking_reference}`);
|
|
209
|
+
console.log(` Flight: ${br.currency} ${br.flight_price.toFixed(2)}`);
|
|
210
|
+
console.log(` Fee: ${br.currency} ${br.service_fee.toFixed(2)} (${br.service_fee_percentage}%)`);
|
|
211
|
+
console.log(` Total: ${br.currency} ${br.total_charged.toFixed(2)}`);
|
|
212
|
+
console.log(` Order: ${br.order_id}
|
|
166
213
|
`);
|
|
167
214
|
} else {
|
|
168
215
|
console.error(` \u2717 Booking failed`);
|
|
169
|
-
console.error(JSON.stringify(
|
|
216
|
+
console.error(JSON.stringify(br.details, null, 2));
|
|
170
217
|
process.exit(1);
|
|
171
218
|
}
|
|
172
219
|
}
|
|
@@ -198,6 +245,19 @@ async function cmdLocations(args) {
|
|
|
198
245
|
console.log(` ${iata} ${name} (${type}) \u2014 ${city}, ${country}`);
|
|
199
246
|
}
|
|
200
247
|
}
|
|
248
|
+
async function cmdAuth(args) {
|
|
249
|
+
const cardToken = getFlag(args, "--card-token");
|
|
250
|
+
const paymentMethodId = getFlag(args, "--payment-method");
|
|
251
|
+
const noBrowser = hasFlag(args, "--no-browser");
|
|
252
|
+
if (cardToken || paymentMethodId) {
|
|
253
|
+
const { verifyPaymentMethod } = await import("./auth-SYZ5AYZ6.mjs");
|
|
254
|
+
await verifyPaymentMethod({ cardToken: cardToken || void 0, paymentMethodId: paymentMethodId || void 0 });
|
|
255
|
+
console.log("\n \u2713 Authenticated. Nothing was charged.");
|
|
256
|
+
} else {
|
|
257
|
+
await paymentAuth(!noBrowser);
|
|
258
|
+
}
|
|
259
|
+
console.log("\n You're all set. Run: letsfg search WAW BCN 2026-07-15\n");
|
|
260
|
+
}
|
|
201
261
|
async function cmdRegister(args) {
|
|
202
262
|
const jsonOut = hasFlag(args, "--json") || hasFlag(args, "-j");
|
|
203
263
|
const baseUrl = getFlag(args, "--base-url");
|
|
@@ -278,11 +338,11 @@ Authenticate once with letsfg auth \u2014 a zero-amount card setup, nothing is
|
|
|
278
338
|
charged \u2014 then search and book.
|
|
279
339
|
|
|
280
340
|
Commands:
|
|
281
|
-
auth
|
|
282
|
-
search <origin> <dest> <date>
|
|
283
|
-
locations <query>
|
|
284
|
-
book <offer_id> --
|
|
285
|
-
me
|
|
341
|
+
auth Put a card on file -> 90-day token. Nothing charged
|
|
342
|
+
search <origin> <dest> <date> Search for flights (free), prints search_id
|
|
343
|
+
locations <query> Resolve city name to IATA codes
|
|
344
|
+
book <offer_id> --search-id ... Book a flight. No LetsFG fee, no unlock step
|
|
345
|
+
me Show agent profile
|
|
286
346
|
|
|
287
347
|
Developer API only (a SEPARATE paid product \u2014 most agents should not use these;
|
|
288
348
|
they create a billing account. Use auth above instead):
|
|
@@ -291,20 +351,26 @@ they create a billing account. Use auth above instead):
|
|
|
291
351
|
unlock <offer_id> [Developer API only] Unlock offer \u2014 1% of ticket (min $3)
|
|
292
352
|
|
|
293
353
|
Options:
|
|
294
|
-
--json, -j
|
|
295
|
-
--api-key, -k
|
|
296
|
-
--base-url
|
|
354
|
+
--json, -j Output raw JSON
|
|
355
|
+
--api-key, -k Developer API key (or set LETSFG_API_KEY) \u2014 switches book/search to the paid path
|
|
356
|
+
--base-url API URL (default: https://letsfg.co)
|
|
357
|
+
--card-token (auth only) Stripe tok_... you already hold, for a headless auth
|
|
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
|
|
297
360
|
|
|
298
361
|
Examples:
|
|
299
362
|
letsfg auth
|
|
300
363
|
letsfg search GDN BER 2026-03-03 --sort price
|
|
301
|
-
letsfg book off_xxx -p '{"given_name":"Ada"
|
|
364
|
+
letsfg book off_xxx --search-id srch_xxx -p '{"given_name":"Ada","family_name":"Lovelace","born_on":"1990-04-01","gender":"f"}' -e ada@example.com
|
|
302
365
|
`;
|
|
303
366
|
async function main() {
|
|
304
367
|
const args = process.argv.slice(2);
|
|
305
368
|
const command = args.shift();
|
|
306
369
|
try {
|
|
307
370
|
switch (command) {
|
|
371
|
+
case "auth":
|
|
372
|
+
await cmdAuth(args);
|
|
373
|
+
break;
|
|
308
374
|
case "search":
|
|
309
375
|
await cmdSearch(args);
|
|
310
376
|
break;
|
|
@@ -338,7 +404,7 @@ async function main() {
|
|
|
338
404
|
process.exit(1);
|
|
339
405
|
}
|
|
340
406
|
} catch (e) {
|
|
341
|
-
if (e instanceof LetsFGError) {
|
|
407
|
+
if (e instanceof LetsFGError || e instanceof BearerTokenError) {
|
|
342
408
|
console.error(`Error: ${e.message}`);
|
|
343
409
|
process.exit(1);
|
|
344
410
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -472,7 +472,8 @@ declare class LetsFG {
|
|
|
472
472
|
resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
|
|
473
473
|
/**
|
|
474
474
|
* Unlock a flight offer — confirms live price, reveals direct airline booking URL.
|
|
475
|
-
* Cost: 1% of ticket price, min $3.
|
|
475
|
+
* Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
|
|
476
|
+
* endpoint on a PFS Bearer token, so PFS callers use book() directly.
|
|
476
477
|
*/
|
|
477
478
|
unlock(offerId: string): Promise<UnlockResult>;
|
|
478
479
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -472,7 +472,8 @@ declare class LetsFG {
|
|
|
472
472
|
resolveLocation(query: string): Promise<Array<Record<string, unknown>>>;
|
|
473
473
|
/**
|
|
474
474
|
* Unlock a flight offer — confirms live price, reveals direct airline booking URL.
|
|
475
|
-
* Cost: 1% of ticket price, min $3.
|
|
475
|
+
* Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
|
|
476
|
+
* endpoint on a PFS Bearer token, so PFS callers use book() directly.
|
|
476
477
|
*/
|
|
477
478
|
unlock(offerId: string): Promise<UnlockResult>;
|
|
478
479
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1677,12 +1677,12 @@ var LetsFG = class {
|
|
|
1677
1677
|
}
|
|
1678
1678
|
/**
|
|
1679
1679
|
* Unlock a flight offer — confirms live price, reveals direct airline booking URL.
|
|
1680
|
-
* Cost: 1% of ticket price, min $3.
|
|
1680
|
+
* Cost: 1% of ticket price, min $3. Developer API only — there is no unlock
|
|
1681
|
+
* endpoint on a PFS Bearer token, so PFS callers use book() directly.
|
|
1681
1682
|
*/
|
|
1682
1683
|
async unlock(offerId) {
|
|
1683
|
-
this.
|
|
1684
|
-
|
|
1685
|
-
return this.postWithAuth(path, { offer_id: offerId });
|
|
1684
|
+
this.requireApiKey();
|
|
1685
|
+
return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
|
|
1686
1686
|
}
|
|
1687
1687
|
/**
|
|
1688
1688
|
* Book a flight.
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letsfg",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.67",
|
|
4
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
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean",
|
|
18
|
-
"test": "tsx --test src/index.test.ts",
|
|
18
|
+
"test": "tsx --test src/index.test.ts src/auth.test.ts",
|
|
19
19
|
"prepublishOnly": "npm run build"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|