@elizaos/plugin-elizacloud 2.0.0-beta.1 → 2.0.11-beta.7
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 +20 -44
- package/auto-enable.ts +10 -5
- package/dist/browser/index.browser.js +2 -2
- package/dist/browser/index.browser.js.map +4 -4
- package/dist/cjs/index.node.cjs +2874 -5915
- package/dist/cjs/index.node.js.map +47 -116
- package/dist/cloud/auth-service-types.d.ts +8 -0
- package/dist/cloud/auth-service-types.d.ts.map +1 -0
- package/dist/cloud/auth-service-types.js +36 -0
- package/dist/cloud/auth-service-types.js.map +10 -0
- package/dist/cloud/auth.js +4 -51
- package/dist/cloud/auth.js.map +4 -4
- package/dist/cloud/base-url.d.ts +6 -2
- package/dist/cloud/base-url.d.ts.map +1 -1
- package/dist/cloud/base-url.js +3 -51
- package/dist/cloud/base-url.js.map +3 -3
- package/dist/cloud/bridge-client.d.ts +3 -3
- package/dist/cloud/bridge-client.d.ts.map +1 -1
- package/dist/cloud/bridge-client.js +3 -51
- package/dist/cloud/bridge-client.js.map +3 -3
- package/dist/cloud/clack-observer.d.ts +35 -0
- package/dist/cloud/clack-observer.d.ts.map +1 -0
- package/dist/cloud/clack-observer.js +143 -0
- package/dist/cloud/clack-observer.js.map +10 -0
- package/dist/cloud/cloud-manager.js +45 -92
- package/dist/cloud/cloud-manager.js.map +6 -6
- package/dist/cloud/cloud-wallet.js +2 -4835
- package/dist/cloud/cloud-wallet.js.map +3 -82
- package/dist/cloud/duffel-client.d.ts +181 -0
- package/dist/cloud/duffel-client.d.ts.map +1 -0
- package/dist/cloud/duffel-client.js +506 -0
- package/dist/cloud/duffel-client.js.map +11 -0
- package/dist/cloud/index.d.ts +6 -0
- package/dist/cloud/index.d.ts.map +1 -1
- package/dist/cloud/index.js +1782 -1
- package/dist/cloud/index.js.map +18 -3
- package/dist/cloud/lifeops-schedule-sync-client.d.ts +43 -0
- package/dist/cloud/lifeops-schedule-sync-client.d.ts.map +1 -0
- package/dist/cloud/lifeops-schedule-sync-client.js +180 -0
- package/dist/cloud/lifeops-schedule-sync-client.js.map +11 -0
- package/dist/cloud/lifeops-schedule-sync-contracts.d.ts +89 -0
- package/dist/cloud/lifeops-schedule-sync-contracts.d.ts.map +1 -0
- package/dist/cloud/lifeops-schedule-sync-contracts.js +39 -0
- package/dist/cloud/lifeops-schedule-sync-contracts.js.map +10 -0
- package/dist/cloud/managed-payment-clients.d.ts +166 -0
- package/dist/cloud/managed-payment-clients.d.ts.map +1 -0
- package/dist/cloud/managed-payment-clients.js +238 -0
- package/dist/cloud/managed-payment-clients.js.map +11 -0
- package/dist/cloud/null-observer.d.ts +35 -0
- package/dist/cloud/null-observer.d.ts.map +1 -0
- package/dist/cloud/null-observer.js +45 -0
- package/dist/cloud/null-observer.js.map +10 -0
- package/dist/cloud/setup-observer.d.ts +98 -0
- package/dist/cloud/setup-observer.d.ts.map +1 -0
- package/dist/cloud/setup-observer.js +2 -0
- package/dist/cloud/setup-observer.js.map +9 -0
- package/dist/cloud/validate-url.d.ts.map +1 -1
- package/dist/cloud/validate-url.js +2 -1
- package/dist/cloud/validate-url.js.map +3 -3
- package/dist/cloud/x402-payment-handler.d.ts +85 -0
- package/dist/cloud/x402-payment-handler.d.ts.map +1 -0
- package/dist/cloud/x402-payment-handler.js +119 -0
- package/dist/cloud/x402-payment-handler.js.map +10 -0
- package/dist/cloud-setup.d.ts +36 -0
- package/dist/cloud-setup.d.ts.map +1 -0
- package/dist/{onboarding.js → cloud-setup.js} +139 -139
- package/dist/cloud-setup.js.map +14 -0
- package/dist/cloud-voice-catalog.d.ts +65 -0
- package/dist/cloud-voice-catalog.d.ts.map +1 -0
- package/dist/cloud-voice-catalog.js +278 -0
- package/dist/cloud-voice-catalog.js.map +12 -0
- package/dist/index.browser.d.ts +11 -0
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5416 -8405
- package/dist/index.js.map +48 -116
- package/dist/index.node.d.ts +8 -1
- package/dist/index.node.d.ts.map +1 -1
- package/dist/init.js +17 -4
- package/dist/init.js.map +4 -4
- package/dist/lib/cloud-connection.d.ts +0 -1
- package/dist/lib/cloud-connection.d.ts.map +1 -1
- package/dist/lib/cloud-connection.js +14 -91
- package/dist/lib/cloud-connection.js.map +7 -7
- package/dist/lib/cloud-secrets.d.ts +5 -18
- package/dist/lib/cloud-secrets.d.ts.map +1 -1
- package/dist/lib/cloud-secrets.js +8 -36
- package/dist/lib/cloud-secrets.js.map +3 -3
- package/dist/lib/config-like.d.ts +1 -1
- package/dist/lib/config-like.d.ts.map +1 -1
- package/dist/lib/config-like.js +3 -3
- package/dist/lib/config-like.js.map +3 -3
- package/dist/lib/credential-type-map.d.ts +1 -1
- package/dist/lib/credential-type-map.js.map +1 -1
- package/dist/lib/http.d.ts +0 -11
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js.map +2 -2
- package/dist/lib/server-cloud-tts.d.ts +12 -25
- package/dist/lib/server-cloud-tts.d.ts.map +1 -1
- package/dist/lib/server-cloud-tts.js +31 -329
- package/dist/lib/server-cloud-tts.js.map +4 -7
- package/dist/lib/tts-debug.d.ts +5 -3
- package/dist/lib/tts-debug.d.ts.map +1 -1
- package/dist/lib/tts-debug.js +1 -34
- package/dist/lib/tts-debug.js.map +3 -4
- package/dist/models/embeddings.d.ts.map +1 -1
- package/dist/models/embeddings.js +79 -69
- package/dist/models/embeddings.js.map +6 -6
- package/dist/models/image.d.ts.map +1 -1
- package/dist/models/image.js +42 -15
- package/dist/models/image.js.map +6 -6
- package/dist/models/index.js +676 -166
- package/dist/models/index.js.map +11 -12
- package/dist/models/research.d.ts.map +1 -1
- package/dist/models/research.js +24 -7
- package/dist/models/research.js.map +6 -6
- package/dist/models/speech.d.ts +61 -3
- package/dist/models/speech.d.ts.map +1 -1
- package/dist/models/speech.js +173 -17
- package/dist/models/speech.js.map +5 -5
- package/dist/models/text.d.ts +106 -1
- package/dist/models/text.d.ts.map +1 -1
- package/dist/models/text.js +452 -82
- package/dist/models/text.js.map +7 -8
- package/dist/models/tokenization.d.ts.map +1 -1
- package/dist/models/tokenization.js.map +2 -2
- package/dist/models/transcription.d.ts.map +1 -1
- package/dist/models/transcription.js +20 -6
- package/dist/models/transcription.js.map +5 -5
- package/dist/node/index.node.js +2828 -5838
- package/dist/node/index.node.js.map +47 -116
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +376 -5050
- package/dist/plugin.js.map +16 -92
- package/dist/providers/openai.js +11 -2
- package/dist/providers/openai.js.map +3 -3
- package/dist/register-routes.js +376 -5050
- package/dist/register-routes.js.map +16 -92
- package/dist/routes/cloud-billing-routes.d.ts.map +1 -1
- package/dist/routes/cloud-billing-routes.js +17 -60
- package/dist/routes/cloud-billing-routes.js.map +8 -7
- package/dist/routes/cloud-coding-container-routes.d.ts +8 -0
- package/dist/routes/cloud-coding-container-routes.d.ts.map +1 -0
- package/dist/routes/cloud-coding-container-routes.js +214 -0
- package/dist/routes/cloud-coding-container-routes.js.map +11 -0
- package/dist/routes/cloud-compat-routes.d.ts.map +1 -1
- package/dist/routes/cloud-compat-routes.js +17 -60
- package/dist/routes/cloud-compat-routes.js.map +8 -7
- package/dist/routes/cloud-features-routes.js +2 -2
- package/dist/routes/cloud-features-routes.js.map +4 -4
- package/dist/routes/cloud-relay-routes.d.ts +2 -1
- package/dist/routes/cloud-relay-routes.d.ts.map +1 -1
- package/dist/routes/cloud-relay-routes.js +84 -2
- package/dist/routes/cloud-relay-routes.js.map +5 -4
- package/dist/routes/cloud-routes-autonomous.d.ts +3 -4
- package/dist/routes/cloud-routes-autonomous.d.ts.map +1 -1
- package/dist/routes/cloud-routes-autonomous.js +11 -4893
- package/dist/routes/cloud-routes-autonomous.js.map +8 -87
- package/dist/routes/cloud-routes.d.ts +2 -2
- package/dist/routes/cloud-routes.d.ts.map +1 -1
- package/dist/routes/cloud-routes.js +343 -5058
- package/dist/routes/cloud-routes.js.map +13 -90
- package/dist/routes/cloud-status-routes-autonomous.d.ts +1 -2
- package/dist/routes/cloud-status-routes-autonomous.d.ts.map +1 -1
- package/dist/routes/cloud-status-routes-autonomous.js +4 -51
- package/dist/routes/cloud-status-routes-autonomous.js.map +5 -5
- package/dist/routes/cloud-status-routes.js +14 -90
- package/dist/routes/cloud-status-routes.js.map +7 -7
- package/dist/routes/home-remote-runner-access-url.d.ts +16 -0
- package/dist/routes/home-remote-runner-access-url.d.ts.map +1 -0
- package/dist/routes/home-remote-runner-access-url.js +91 -0
- package/dist/routes/home-remote-runner-access-url.js.map +10 -0
- package/dist/routes/travel-provider-relay-routes.d.ts +9 -0
- package/dist/routes/travel-provider-relay-routes.d.ts.map +1 -0
- package/dist/routes/travel-provider-relay-routes.js +358 -0
- package/dist/routes/travel-provider-relay-routes.js.map +14 -0
- package/dist/services/cloud-auth.d.ts +1 -1
- package/dist/services/cloud-auth.d.ts.map +1 -1
- package/dist/services/cloud-auth.js +7 -2
- package/dist/services/cloud-auth.js.map +4 -4
- package/dist/services/cloud-backup.js.map +2 -2
- package/dist/services/cloud-bootstrap.d.ts.map +1 -1
- package/dist/services/cloud-bootstrap.js.map +2 -2
- package/dist/services/cloud-bridge.js.map +3 -3
- package/dist/services/cloud-container.d.ts +5 -1
- package/dist/services/cloud-container.d.ts.map +1 -1
- package/dist/services/cloud-container.js +52 -1
- package/dist/services/cloud-container.js.map +4 -4
- package/dist/services/cloud-credential-provider.js.map +2 -2
- package/dist/services/cloud-model-registry.js.map +2 -2
- package/dist/types/cloud.d.ts +1 -0
- package/dist/types/cloud.d.ts.map +1 -1
- package/dist/types/cloud.js.map +2 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/cloud-sdk/client.d.ts.map +1 -1
- package/dist/utils/cloud-sdk/client.js +136 -4
- package/dist/utils/cloud-sdk/client.js.map +5 -5
- package/dist/utils/cloud-sdk/http.js.map +1 -1
- package/dist/utils/cloud-sdk/public-routes.d.ts +186 -0
- package/dist/utils/cloud-sdk/public-routes.d.ts.map +1 -1
- package/dist/utils/cloud-sdk/public-routes.js +99 -1
- package/dist/utils/cloud-sdk/public-routes.js.map +3 -3
- package/dist/utils/cloud-sdk/types.d.ts +0 -2
- package/dist/utils/cloud-sdk/types.d.ts.map +1 -1
- package/dist/utils/cloud-sdk/types.js.map +1 -1
- package/dist/utils/config.d.ts +10 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -2
- package/dist/utils/config.js.map +3 -3
- package/dist/utils/events.d.ts +23 -2
- package/dist/utils/events.d.ts.map +1 -1
- package/dist/utils/events.js +5 -3
- package/dist/utils/events.js.map +3 -3
- package/dist/utils/sdk-client.d.ts.map +1 -1
- package/dist/utils/sdk-client.js +17 -4
- package/dist/utils/sdk-client.js.map +4 -4
- package/dist/utils/waifu-metering.d.ts +108 -0
- package/dist/utils/waifu-metering.d.ts.map +1 -0
- package/dist/utils/waifu-metering.js +166 -0
- package/dist/utils/waifu-metering.js.map +10 -0
- package/package.json +51 -22
- package/src/cloud/auth-service-types.ts +24 -0
- package/src/cloud/base-url.ts +6 -62
- package/src/cloud/clack-observer.ts +189 -0
- package/src/cloud/duffel-client.ts +847 -0
- package/src/cloud/index.ts +10 -0
- package/src/cloud/lifeops-schedule-sync-client.ts +245 -0
- package/src/cloud/lifeops-schedule-sync-contracts.ts +124 -0
- package/src/cloud/managed-payment-clients.ts +374 -0
- package/src/cloud/null-observer.ts +45 -0
- package/src/cloud/setup-observer.ts +125 -0
- package/src/cloud/validate-url.ts +7 -1
- package/src/cloud/x402-payment-handler.ts +215 -0
- package/src/cloud-setup.ts +531 -0
- package/src/cloud-voice-catalog.test.ts +254 -0
- package/src/cloud-voice-catalog.ts +246 -0
- package/src/index.browser.ts +29 -0
- package/src/index.node.ts +31 -1
- package/src/index.ts +76 -4
- package/src/lib/cloud-connection.ts +2 -4
- package/src/lib/cloud-secrets.ts +10 -54
- package/src/lib/config-like.ts +1 -1
- package/src/lib/credential-type-map.ts +2 -2
- package/src/lib/http.ts +0 -17
- package/src/lib/server-cloud-tts.ts +33 -341
- package/src/lib/tts-debug.ts +5 -34
- package/src/models/embeddings.ts +140 -76
- package/src/models/image.ts +29 -14
- package/src/models/research.ts +11 -1
- package/src/models/speech.ts +269 -23
- package/src/models/text.ts +704 -110
- package/src/models/tokenization.ts +2 -2
- package/src/models/transcription.ts +7 -3
- package/src/plugin.ts +38 -0
- package/src/routes/cloud-billing-routes.ts +4 -14
- package/src/routes/cloud-coding-container-routes.ts +198 -0
- package/src/routes/cloud-compat-routes.ts +4 -14
- package/src/routes/cloud-features-routes.ts +1 -1
- package/src/routes/cloud-relay-routes.ts +47 -1
- package/src/routes/cloud-routes-autonomous.ts +7 -10
- package/src/routes/cloud-routes.ts +68 -7
- package/src/routes/cloud-status-routes-autonomous.ts +6 -2
- package/src/routes/home-remote-runner-access-url.ts +83 -0
- package/src/routes/travel-provider-relay-routes.ts +193 -0
- package/src/services/cloud-auth.ts +9 -2
- package/src/services/cloud-bootstrap.ts +1 -3
- package/src/services/cloud-bridge.ts +1 -1
- package/src/services/cloud-container.ts +93 -0
- package/src/services/cloud-credential-provider.ts +1 -1
- package/src/services/cloud-model-registry.ts +1 -1
- package/src/types/cloud.ts +22 -0
- package/src/types/index.ts +19 -0
- package/src/utils/cloud-sdk/client.ts +42 -3
- package/src/utils/cloud-sdk/public-routes.ts +168 -0
- package/src/utils/cloud-sdk/types.ts +0 -2
- package/src/utils/config.ts +20 -1
- package/src/utils/events.ts +30 -2
- package/src/utils/sdk-client.ts +5 -1
- package/src/utils/waifu-metering.ts +302 -0
- package/dist/onboarding.d.ts +0 -35
- package/dist/onboarding.d.ts.map +0 -1
- package/dist/onboarding.js.map +0 -14
- package/src/onboarding.ts +0 -396
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import { logger } from "@elizaos/core";
|
|
2
|
+
import {
|
|
3
|
+
PaymentRequiredError,
|
|
4
|
+
parseX402Response,
|
|
5
|
+
} from "./x402-payment-handler.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Config
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export class DuffelConfigError extends Error {
|
|
12
|
+
readonly code = "DUFFEL_NOT_CONFIGURED" as const;
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "DuffelConfigError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Direct mode hits api.duffel.com with the user's own DUFFEL_API_KEY.
|
|
20
|
+
* Cloud mode hits the local Eliza Cloud relay which performs the upstream
|
|
21
|
+
* Duffel call, applies creator markup, and meters against the user's
|
|
22
|
+
* Cloud credit balance. Cloud mode is the default. */
|
|
23
|
+
export type DuffelMode = "cloud" | "direct";
|
|
24
|
+
|
|
25
|
+
export interface DuffelConfig {
|
|
26
|
+
mode: DuffelMode;
|
|
27
|
+
/** Required when mode === "direct". */
|
|
28
|
+
apiKey: string | null;
|
|
29
|
+
/** Required when mode === "cloud". Local Eliza agent API base, e.g.
|
|
30
|
+
* http://127.0.0.1:31337. The relay path is appended internally. */
|
|
31
|
+
cloudRelayBaseUrl: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DUFFEL_API_BASE_DEFAULT = "https://api.duffel.com";
|
|
35
|
+
const DUFFEL_API_VERSION = "v2";
|
|
36
|
+
const DEFAULT_CLOUD_RELAY_BASE = "http://127.0.0.1:31337";
|
|
37
|
+
|
|
38
|
+
interface DuffelTelemetrySpan {
|
|
39
|
+
success(meta?: Record<string, unknown>): void;
|
|
40
|
+
failure(meta?: Record<string, unknown>): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const NOOP_DUFFEL_SPAN: DuffelTelemetrySpan = {
|
|
44
|
+
success() {},
|
|
45
|
+
failure() {},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Honour the mockoon redirect (or any explicit `LIFEOPS_DUFFEL_API_BASE`
|
|
49
|
+
* override) so direct-mode tests can swap Duffel for a local mock. */
|
|
50
|
+
function getDuffelApiBase(): string {
|
|
51
|
+
return process.env.LIFEOPS_DUFFEL_API_BASE?.trim() || DUFFEL_API_BASE_DEFAULT;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveDirectFlag(env: NodeJS.ProcessEnv): boolean {
|
|
55
|
+
const value = env.ELIZA_DUFFEL_DIRECT?.trim().toLowerCase();
|
|
56
|
+
return value === "1" || value === "true";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveLocalApiBase(env: NodeJS.ProcessEnv): string {
|
|
60
|
+
const port = env.ELIZA_API_PORT?.trim();
|
|
61
|
+
if (port && /^\d+$/.test(port)) {
|
|
62
|
+
return `http://127.0.0.1:${port}`;
|
|
63
|
+
}
|
|
64
|
+
return DEFAULT_CLOUD_RELAY_BASE;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readDuffelConfigFromEnv(
|
|
68
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
69
|
+
): DuffelConfig {
|
|
70
|
+
if (resolveDirectFlag(env)) {
|
|
71
|
+
const apiKey = env.DUFFEL_API_KEY?.trim();
|
|
72
|
+
if (!apiKey) {
|
|
73
|
+
throw new DuffelConfigError(
|
|
74
|
+
"Duffel direct mode requested but DUFFEL_API_KEY is not set.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return { mode: "direct", apiKey, cloudRelayBaseUrl: null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
mode: "cloud",
|
|
82
|
+
apiKey: null,
|
|
83
|
+
cloudRelayBaseUrl: resolveLocalApiBase(env),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Input types
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
export interface SearchFlightsRequest {
|
|
92
|
+
/** IATA airport code for origin, e.g. "JFK". */
|
|
93
|
+
origin: string;
|
|
94
|
+
/** IATA airport code for destination, e.g. "LHR". */
|
|
95
|
+
destination: string;
|
|
96
|
+
/** ISO 8601 date string (YYYY-MM-DD). */
|
|
97
|
+
departureDate: string;
|
|
98
|
+
/**
|
|
99
|
+
* ISO 8601 date string for return leg.
|
|
100
|
+
* Omit or pass undefined for one-way search.
|
|
101
|
+
*/
|
|
102
|
+
returnDate?: string;
|
|
103
|
+
/** Number of adult passengers (default 1). */
|
|
104
|
+
passengers?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Response types
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export interface DuffelSegment {
|
|
112
|
+
origin: string;
|
|
113
|
+
destination: string;
|
|
114
|
+
departingAt: string;
|
|
115
|
+
arrivingAt: string;
|
|
116
|
+
carrierIataCode: string;
|
|
117
|
+
flightNumber: string;
|
|
118
|
+
duration: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface DuffelSlice {
|
|
122
|
+
origin: string;
|
|
123
|
+
destination: string;
|
|
124
|
+
duration: string;
|
|
125
|
+
segments: DuffelSegment[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface DuffelOfferPassenger {
|
|
129
|
+
id: string;
|
|
130
|
+
type: string;
|
|
131
|
+
givenName: string | null;
|
|
132
|
+
familyName: string | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface DuffelPaymentRequirements {
|
|
136
|
+
requiresInstantPayment: boolean;
|
|
137
|
+
priceGuaranteeExpiresAt: string | null;
|
|
138
|
+
paymentRequiredBy: string | null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface DuffelOffer {
|
|
142
|
+
id: string;
|
|
143
|
+
totalAmount: string;
|
|
144
|
+
totalCurrency: string;
|
|
145
|
+
passengerCount: number;
|
|
146
|
+
slices: DuffelSlice[];
|
|
147
|
+
expiresAt: string | null;
|
|
148
|
+
/** Raw cabin class reported by Duffel for the first slice. */
|
|
149
|
+
cabinClass: string | null;
|
|
150
|
+
passengers: DuffelOfferPassenger[];
|
|
151
|
+
paymentRequirements: DuffelPaymentRequirements | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Cost envelope returned by the cloud relay. The breakdown is computed
|
|
156
|
+
* server-side (commandment 2 — no client-side math) and the local code
|
|
157
|
+
* forwards it to the UI for display. In direct mode all values are zero
|
|
158
|
+
* because no markup is charged.
|
|
159
|
+
*
|
|
160
|
+
* `markupPercent` is a *display-only* derived field: it equals
|
|
161
|
+
* `platformFeeUsd / totalUsd`, computed once at the boundary so UI
|
|
162
|
+
* components don't repeat the division. The pricing decision itself is
|
|
163
|
+
* still Cloud-side — this is the same pattern as a "discount %" badge
|
|
164
|
+
* derived from a server-supplied price.
|
|
165
|
+
*/
|
|
166
|
+
export interface DuffelCallCost {
|
|
167
|
+
/** Net cost charged to the user's Cloud credit balance, in USD. */
|
|
168
|
+
totalUsd: number;
|
|
169
|
+
/** Portion that flows to the creator as markup, in USD. */
|
|
170
|
+
creatorMarkupUsd: number;
|
|
171
|
+
/** Eliza Cloud platform fee portion, in USD. */
|
|
172
|
+
platformFeeUsd: number;
|
|
173
|
+
/** Display-only ratio of platform fee to total (e.g. 0.2 for 20%).
|
|
174
|
+
* Derived from server values, never used for pricing. Null when
|
|
175
|
+
* totalUsd is zero (direct mode or free call). */
|
|
176
|
+
markupPercent: number | null;
|
|
177
|
+
/** Whether the call was metered (true in cloud mode, false in direct mode). */
|
|
178
|
+
metered: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const DIRECT_MODE_COST: DuffelCallCost = {
|
|
182
|
+
totalUsd: 0,
|
|
183
|
+
creatorMarkupUsd: 0,
|
|
184
|
+
platformFeeUsd: 0,
|
|
185
|
+
markupPercent: null,
|
|
186
|
+
metered: false,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export interface SearchFlightsResult {
|
|
190
|
+
offerRequestId: string;
|
|
191
|
+
offers: DuffelOffer[];
|
|
192
|
+
cost: DuffelCallCost;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface DuffelOrderPassenger {
|
|
196
|
+
id: string;
|
|
197
|
+
givenName: string | null;
|
|
198
|
+
familyName: string | null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface DuffelOrderPaymentStatus {
|
|
202
|
+
awaitingPayment: boolean;
|
|
203
|
+
paymentRequiredBy: string | null;
|
|
204
|
+
priceGuaranteeExpiresAt: string | null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface DuffelOrderDocument {
|
|
208
|
+
type: string | null;
|
|
209
|
+
uniqueIdentifier: string | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface DuffelOrder {
|
|
213
|
+
id: string;
|
|
214
|
+
bookingReference: string | null;
|
|
215
|
+
totalAmount: string;
|
|
216
|
+
totalCurrency: string;
|
|
217
|
+
slices: DuffelSlice[];
|
|
218
|
+
passengers: DuffelOrderPassenger[];
|
|
219
|
+
paymentStatus: DuffelOrderPaymentStatus | null;
|
|
220
|
+
documents: DuffelOrderDocument[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface DuffelPayment {
|
|
224
|
+
id: string;
|
|
225
|
+
orderId: string;
|
|
226
|
+
status: string;
|
|
227
|
+
currency: string;
|
|
228
|
+
amount: string;
|
|
229
|
+
type: string;
|
|
230
|
+
failureReason: string | null;
|
|
231
|
+
createdAt: string | null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface DuffelOrderPassengerInput {
|
|
235
|
+
id: string;
|
|
236
|
+
title?: string;
|
|
237
|
+
gender?: string;
|
|
238
|
+
givenName: string;
|
|
239
|
+
familyName: string;
|
|
240
|
+
bornOn: string;
|
|
241
|
+
email?: string;
|
|
242
|
+
phoneNumber?: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface CreateDuffelOrderRequest {
|
|
246
|
+
selectedOffers: ReadonlyArray<string>;
|
|
247
|
+
passengers: ReadonlyArray<DuffelOrderPassengerInput>;
|
|
248
|
+
type: "hold" | "instant";
|
|
249
|
+
payment?: {
|
|
250
|
+
type: "balance";
|
|
251
|
+
amount: string;
|
|
252
|
+
currency: string;
|
|
253
|
+
};
|
|
254
|
+
metadata?: Readonly<Record<string, string>>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Internal Duffel API response shapes (minimal — only fields we use)
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
interface DuffelApiOffer {
|
|
262
|
+
id: string;
|
|
263
|
+
total_amount: string;
|
|
264
|
+
total_currency: string;
|
|
265
|
+
expires_at: string | null;
|
|
266
|
+
slices: Array<{
|
|
267
|
+
origin: { iata_code: string };
|
|
268
|
+
destination: { iata_code: string };
|
|
269
|
+
duration: string;
|
|
270
|
+
segments: Array<{
|
|
271
|
+
origin: { iata_code: string };
|
|
272
|
+
destination: { iata_code: string };
|
|
273
|
+
departing_at: string;
|
|
274
|
+
arriving_at: string;
|
|
275
|
+
operating_carrier: { iata_code: string };
|
|
276
|
+
flight_number: string | null;
|
|
277
|
+
duration: string;
|
|
278
|
+
}>;
|
|
279
|
+
fare_brand_name: string | null;
|
|
280
|
+
}>;
|
|
281
|
+
passengers: Array<{
|
|
282
|
+
id?: string;
|
|
283
|
+
type?: string;
|
|
284
|
+
given_name?: string | null;
|
|
285
|
+
family_name?: string | null;
|
|
286
|
+
}>;
|
|
287
|
+
payment_requirements?: {
|
|
288
|
+
requires_instant_payment?: boolean;
|
|
289
|
+
price_guarantee_expires_at?: string | null;
|
|
290
|
+
payment_required_by?: string | null;
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface DuffelOfferRequestResponse {
|
|
295
|
+
data: {
|
|
296
|
+
id: string;
|
|
297
|
+
offers: DuffelApiOffer[];
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface DuffelOfferResponse {
|
|
302
|
+
data: DuffelApiOffer;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
interface DuffelApiOrder {
|
|
306
|
+
id: string;
|
|
307
|
+
booking_reference?: string | null;
|
|
308
|
+
total_amount: string;
|
|
309
|
+
total_currency: string;
|
|
310
|
+
slices: DuffelApiOffer["slices"];
|
|
311
|
+
passengers: Array<{
|
|
312
|
+
id?: string;
|
|
313
|
+
given_name?: string | null;
|
|
314
|
+
family_name?: string | null;
|
|
315
|
+
}>;
|
|
316
|
+
payment_status?: {
|
|
317
|
+
awaiting_payment?: boolean;
|
|
318
|
+
payment_required_by?: string | null;
|
|
319
|
+
price_guarantee_expires_at?: string | null;
|
|
320
|
+
};
|
|
321
|
+
documents?: Array<{
|
|
322
|
+
type?: string | null;
|
|
323
|
+
unique_identifier?: string | null;
|
|
324
|
+
}>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface DuffelOrderResponse {
|
|
328
|
+
data: DuffelApiOrder;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
interface DuffelPaymentResponse {
|
|
332
|
+
data: {
|
|
333
|
+
id: string;
|
|
334
|
+
order_id: string;
|
|
335
|
+
status: string;
|
|
336
|
+
currency: string;
|
|
337
|
+
amount: string;
|
|
338
|
+
type: string;
|
|
339
|
+
failure_reason?: string | null;
|
|
340
|
+
created_at?: string | null;
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Helpers
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
function buildHeaders(apiKey: string): Record<string, string> {
|
|
349
|
+
return {
|
|
350
|
+
Authorization: `Bearer ${apiKey}`,
|
|
351
|
+
"Content-Type": "application/json",
|
|
352
|
+
"Duffel-Version": DUFFEL_API_VERSION,
|
|
353
|
+
Accept: "application/json",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function mapOffer(raw: DuffelApiOffer): DuffelOffer {
|
|
358
|
+
const cabinClass = raw.slices[0]?.fare_brand_name ?? null;
|
|
359
|
+
|
|
360
|
+
const slices: DuffelSlice[] = raw.slices.map((slice) => ({
|
|
361
|
+
origin: slice.origin.iata_code,
|
|
362
|
+
destination: slice.destination.iata_code,
|
|
363
|
+
duration: slice.duration,
|
|
364
|
+
segments: slice.segments.map((seg) => ({
|
|
365
|
+
origin: seg.origin.iata_code,
|
|
366
|
+
destination: seg.destination.iata_code,
|
|
367
|
+
departingAt: seg.departing_at,
|
|
368
|
+
arrivingAt: seg.arriving_at,
|
|
369
|
+
carrierIataCode: seg.operating_carrier.iata_code,
|
|
370
|
+
flightNumber: seg.flight_number ?? "",
|
|
371
|
+
duration: seg.duration,
|
|
372
|
+
})),
|
|
373
|
+
}));
|
|
374
|
+
const passengers: DuffelOfferPassenger[] = raw.passengers.map(
|
|
375
|
+
(passenger, index) => ({
|
|
376
|
+
id: passenger.id?.trim() || `passenger_${index}`,
|
|
377
|
+
type: passenger.type?.trim() || "adult",
|
|
378
|
+
givenName: passenger.given_name?.trim() || null,
|
|
379
|
+
familyName: passenger.family_name?.trim() || null,
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
const paymentRequirements = raw.payment_requirements
|
|
383
|
+
? {
|
|
384
|
+
requiresInstantPayment:
|
|
385
|
+
raw.payment_requirements.requires_instant_payment !== false,
|
|
386
|
+
priceGuaranteeExpiresAt:
|
|
387
|
+
raw.payment_requirements.price_guarantee_expires_at ?? null,
|
|
388
|
+
paymentRequiredBy: raw.payment_requirements.payment_required_by ?? null,
|
|
389
|
+
}
|
|
390
|
+
: null;
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
id: raw.id,
|
|
394
|
+
totalAmount: raw.total_amount,
|
|
395
|
+
totalCurrency: raw.total_currency,
|
|
396
|
+
passengerCount: passengers.length > 0 ? passengers.length : 1,
|
|
397
|
+
slices,
|
|
398
|
+
expiresAt: raw.expires_at,
|
|
399
|
+
cabinClass,
|
|
400
|
+
passengers,
|
|
401
|
+
paymentRequirements,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function mapOrder(raw: DuffelApiOrder): DuffelOrder {
|
|
406
|
+
return {
|
|
407
|
+
id: raw.id,
|
|
408
|
+
bookingReference: raw.booking_reference ?? null,
|
|
409
|
+
totalAmount: raw.total_amount,
|
|
410
|
+
totalCurrency: raw.total_currency,
|
|
411
|
+
slices: raw.slices.map((slice) => ({
|
|
412
|
+
origin: slice.origin.iata_code,
|
|
413
|
+
destination: slice.destination.iata_code,
|
|
414
|
+
duration: slice.duration,
|
|
415
|
+
segments: slice.segments.map((segment) => ({
|
|
416
|
+
origin: segment.origin.iata_code,
|
|
417
|
+
destination: segment.destination.iata_code,
|
|
418
|
+
departingAt: segment.departing_at,
|
|
419
|
+
arrivingAt: segment.arriving_at,
|
|
420
|
+
carrierIataCode: segment.operating_carrier.iata_code,
|
|
421
|
+
flightNumber: segment.flight_number ?? "",
|
|
422
|
+
duration: segment.duration,
|
|
423
|
+
})),
|
|
424
|
+
})),
|
|
425
|
+
passengers: raw.passengers.map((passenger, index) => ({
|
|
426
|
+
id: passenger.id?.trim() || `passenger_${index}`,
|
|
427
|
+
givenName: passenger.given_name?.trim() || null,
|
|
428
|
+
familyName: passenger.family_name?.trim() || null,
|
|
429
|
+
})),
|
|
430
|
+
paymentStatus: raw.payment_status
|
|
431
|
+
? {
|
|
432
|
+
awaitingPayment: raw.payment_status.awaiting_payment === true,
|
|
433
|
+
paymentRequiredBy: raw.payment_status.payment_required_by ?? null,
|
|
434
|
+
priceGuaranteeExpiresAt:
|
|
435
|
+
raw.payment_status.price_guarantee_expires_at ?? null,
|
|
436
|
+
}
|
|
437
|
+
: null,
|
|
438
|
+
documents: (raw.documents ?? []).map((document) => ({
|
|
439
|
+
type: document.type ?? null,
|
|
440
|
+
uniqueIdentifier: document.unique_identifier ?? null,
|
|
441
|
+
})),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function mapPayment(raw: DuffelPaymentResponse["data"]): DuffelPayment {
|
|
446
|
+
return {
|
|
447
|
+
id: raw.id,
|
|
448
|
+
orderId: raw.order_id,
|
|
449
|
+
status: raw.status,
|
|
450
|
+
currency: raw.currency,
|
|
451
|
+
amount: raw.amount,
|
|
452
|
+
type: raw.type,
|
|
453
|
+
failureReason: raw.failure_reason ?? null,
|
|
454
|
+
createdAt: raw.created_at ?? null,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Cost meta envelope returned by the Eliza Cloud relay alongside the
|
|
460
|
+
* Duffel payload. Defined server-side; the client trusts whatever Cloud
|
|
461
|
+
* returns and never recomputes (commandment 2).
|
|
462
|
+
*/
|
|
463
|
+
interface RelayCostMeta {
|
|
464
|
+
total_usd?: number;
|
|
465
|
+
creator_markup_usd?: number;
|
|
466
|
+
platform_fee_usd?: number;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
interface RelayMeta {
|
|
470
|
+
cost?: RelayCostMeta;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
interface RelayEnvelope {
|
|
474
|
+
_meta?: RelayMeta;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function readRelayCost(envelope: unknown): DuffelCallCost {
|
|
478
|
+
if (
|
|
479
|
+
envelope === null ||
|
|
480
|
+
typeof envelope !== "object" ||
|
|
481
|
+
!("_meta" in envelope)
|
|
482
|
+
) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
"Duffel cloud relay response missing _meta envelope. Update Eliza Cloud or set ELIZA_DUFFEL_DIRECT=1.",
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const meta = (envelope as RelayEnvelope)._meta;
|
|
488
|
+
const cost = meta?.cost;
|
|
489
|
+
if (
|
|
490
|
+
!cost ||
|
|
491
|
+
typeof cost.total_usd !== "number" ||
|
|
492
|
+
typeof cost.creator_markup_usd !== "number" ||
|
|
493
|
+
typeof cost.platform_fee_usd !== "number"
|
|
494
|
+
) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
"Duffel cloud relay returned malformed _meta.cost. Refusing to proceed without billing receipt.",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const totalUsd = cost.total_usd;
|
|
500
|
+
const platformFeeUsd = cost.platform_fee_usd;
|
|
501
|
+
const markupPercent = totalUsd > 0 ? platformFeeUsd / totalUsd : null;
|
|
502
|
+
return {
|
|
503
|
+
totalUsd,
|
|
504
|
+
creatorMarkupUsd: cost.creator_markup_usd,
|
|
505
|
+
platformFeeUsd,
|
|
506
|
+
markupPercent,
|
|
507
|
+
metered: true,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
interface DuffelFetchResult<T> {
|
|
512
|
+
data: T;
|
|
513
|
+
cost: DuffelCallCost;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
interface DuffelRequest {
|
|
517
|
+
config: DuffelConfig;
|
|
518
|
+
method: "GET" | "POST";
|
|
519
|
+
/** Path on api.duffel.com (e.g. "/air/offer_requests"). */
|
|
520
|
+
directPath: string;
|
|
521
|
+
/** Path on the local cloud relay (e.g. "/api/cloud/travel-providers/duffel/offer-requests"). */
|
|
522
|
+
cloudRelayPath: string;
|
|
523
|
+
body?: unknown;
|
|
524
|
+
operation: string;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function duffelFetch<T>(
|
|
528
|
+
args: DuffelRequest,
|
|
529
|
+
): Promise<DuffelFetchResult<T>> {
|
|
530
|
+
const { config, method, directPath, cloudRelayPath, body, operation } = args;
|
|
531
|
+
|
|
532
|
+
const isCloud = config.mode === "cloud";
|
|
533
|
+
const url = isCloud
|
|
534
|
+
? `${config.cloudRelayBaseUrl ?? ""}${cloudRelayPath}`
|
|
535
|
+
: `${getDuffelApiBase()}${directPath}`;
|
|
536
|
+
|
|
537
|
+
const headers = isCloud
|
|
538
|
+
? { "Content-Type": "application/json", Accept: "application/json" }
|
|
539
|
+
: buildHeaders(config.apiKey ?? "");
|
|
540
|
+
|
|
541
|
+
const span = NOOP_DUFFEL_SPAN;
|
|
542
|
+
|
|
543
|
+
let response: Response;
|
|
544
|
+
try {
|
|
545
|
+
response = await fetch(url, {
|
|
546
|
+
method,
|
|
547
|
+
headers,
|
|
548
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
549
|
+
signal: AbortSignal.timeout(30_000),
|
|
550
|
+
});
|
|
551
|
+
} catch (error) {
|
|
552
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
553
|
+
logger.error(
|
|
554
|
+
{
|
|
555
|
+
boundary: "elizacloud",
|
|
556
|
+
integration: "duffel",
|
|
557
|
+
operation,
|
|
558
|
+
mode: config.mode,
|
|
559
|
+
err: error instanceof Error ? error : undefined,
|
|
560
|
+
},
|
|
561
|
+
`[elizacloud-duffel] Duffel ${operation} network error: ${msg}`,
|
|
562
|
+
);
|
|
563
|
+
span.failure({ error, errorKind: "network_error" });
|
|
564
|
+
throw new Error(`Duffel ${operation} failed: ${msg}`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (response.status === 402 && isCloud) {
|
|
568
|
+
// x402 payment-required: surface as a typed PaymentRequiredError so
|
|
569
|
+
// the action layer can route the user to the wallet top-up flow
|
|
570
|
+
// rather than treating this as a generic HTTP failure. The Cloud
|
|
571
|
+
// billing layer emits 402 only when the user's credit balance can't
|
|
572
|
+
// cover the call — see docs/cloud-travel-billing.md.
|
|
573
|
+
const requirements = await parseX402Response(response);
|
|
574
|
+
logger.warn(
|
|
575
|
+
{
|
|
576
|
+
boundary: "elizacloud",
|
|
577
|
+
integration: "duffel",
|
|
578
|
+
operation,
|
|
579
|
+
mode: config.mode,
|
|
580
|
+
statusCode: 402,
|
|
581
|
+
requirementCount: requirements?.length ?? 0,
|
|
582
|
+
},
|
|
583
|
+
`[elizacloud-duffel] Duffel ${operation} returned 402 payment-required`,
|
|
584
|
+
);
|
|
585
|
+
span.failure({ statusCode: 402, errorKind: "payment_required" });
|
|
586
|
+
if (!requirements || requirements.length === 0) {
|
|
587
|
+
throw new PaymentRequiredError(
|
|
588
|
+
[],
|
|
589
|
+
`Duffel ${operation} requires payment but the upstream did not advertise any payment options.`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
throw new PaymentRequiredError(requirements);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!response.ok) {
|
|
596
|
+
const errorBody = await response.text().catch(() => "");
|
|
597
|
+
const errorMsg = errorBody || `HTTP ${response.status}`;
|
|
598
|
+
logger.warn(
|
|
599
|
+
{
|
|
600
|
+
boundary: "elizacloud",
|
|
601
|
+
integration: "duffel",
|
|
602
|
+
operation,
|
|
603
|
+
mode: config.mode,
|
|
604
|
+
statusCode: response.status,
|
|
605
|
+
},
|
|
606
|
+
`[elizacloud-duffel] Duffel ${operation} HTTP error: ${errorMsg}`,
|
|
607
|
+
);
|
|
608
|
+
span.failure({ statusCode: response.status, errorKind: "http_error" });
|
|
609
|
+
throw new Error(
|
|
610
|
+
`Duffel ${operation} failed (${response.status}): ${errorMsg}`,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const payload = (await response.json()) as T;
|
|
615
|
+
span.success({ statusCode: response.status });
|
|
616
|
+
|
|
617
|
+
const cost = isCloud ? readRelayCost(payload) : DIRECT_MODE_COST;
|
|
618
|
+
return { data: payload, cost };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
// Public API
|
|
623
|
+
// ---------------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Search for available flight offers via the Duffel Offer Requests API.
|
|
627
|
+
*
|
|
628
|
+
* Throws `DuffelConfigError` when DUFFEL_API_KEY is absent.
|
|
629
|
+
* One-way search when `returnDate` is omitted; return search when provided.
|
|
630
|
+
*
|
|
631
|
+
*/
|
|
632
|
+
export async function searchFlights(
|
|
633
|
+
request: SearchFlightsRequest,
|
|
634
|
+
config?: DuffelConfig,
|
|
635
|
+
): Promise<SearchFlightsResult> {
|
|
636
|
+
const resolvedConfig = config ?? readDuffelConfigFromEnv();
|
|
637
|
+
const passengerCount = Math.max(1, Math.round(request.passengers ?? 1));
|
|
638
|
+
|
|
639
|
+
const slices: Array<{
|
|
640
|
+
origin: string;
|
|
641
|
+
destination: string;
|
|
642
|
+
departure_date: string;
|
|
643
|
+
}> = [
|
|
644
|
+
{
|
|
645
|
+
origin: request.origin.toUpperCase().trim(),
|
|
646
|
+
destination: request.destination.toUpperCase().trim(),
|
|
647
|
+
departure_date: request.departureDate,
|
|
648
|
+
},
|
|
649
|
+
];
|
|
650
|
+
if (request.returnDate) {
|
|
651
|
+
slices.push({
|
|
652
|
+
origin: request.destination.toUpperCase().trim(),
|
|
653
|
+
destination: request.origin.toUpperCase().trim(),
|
|
654
|
+
departure_date: request.returnDate,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const requestBody = {
|
|
659
|
+
data: {
|
|
660
|
+
slices,
|
|
661
|
+
passengers: Array.from({ length: passengerCount }, () => ({
|
|
662
|
+
type: "adult",
|
|
663
|
+
})),
|
|
664
|
+
cabin_class: "economy",
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
logger.info(
|
|
669
|
+
{
|
|
670
|
+
boundary: "elizacloud",
|
|
671
|
+
integration: "duffel",
|
|
672
|
+
origin: request.origin,
|
|
673
|
+
destination: request.destination,
|
|
674
|
+
},
|
|
675
|
+
`[elizacloud-duffel] Searching flights ${request.origin} → ${request.destination} on ${request.departureDate}`,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
const { data: responseData, cost } =
|
|
679
|
+
await duffelFetch<DuffelOfferRequestResponse>({
|
|
680
|
+
config: resolvedConfig,
|
|
681
|
+
method: "POST",
|
|
682
|
+
directPath: "/air/offer_requests?return_offers=true",
|
|
683
|
+
cloudRelayPath: "/api/cloud/travel-providers/duffel/offer-requests",
|
|
684
|
+
body: requestBody,
|
|
685
|
+
operation: "offer_request",
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const offers = responseData.data.offers.map(mapOffer);
|
|
689
|
+
|
|
690
|
+
logger.info(
|
|
691
|
+
{
|
|
692
|
+
boundary: "elizacloud",
|
|
693
|
+
integration: "duffel",
|
|
694
|
+
offerRequestId: responseData.data.id,
|
|
695
|
+
offerCount: offers.length,
|
|
696
|
+
costUsd: cost.totalUsd,
|
|
697
|
+
},
|
|
698
|
+
`[elizacloud-duffel] Duffel returned ${offers.length} offers for request ${responseData.data.id}`,
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
offerRequestId: responseData.data.id,
|
|
703
|
+
offers,
|
|
704
|
+
cost,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Retrieve a single flight offer by ID.
|
|
710
|
+
*
|
|
711
|
+
* Use after `searchFlights` to get live pricing and full details for a
|
|
712
|
+
* specific offer before presenting it to the user for approval.
|
|
713
|
+
*
|
|
714
|
+
*/
|
|
715
|
+
export async function getOffer(
|
|
716
|
+
id: string,
|
|
717
|
+
config?: DuffelConfig,
|
|
718
|
+
): Promise<DuffelOffer> {
|
|
719
|
+
const resolvedConfig = config ?? readDuffelConfigFromEnv();
|
|
720
|
+
|
|
721
|
+
if (!id || id.trim().length === 0) {
|
|
722
|
+
throw new Error("Duffel getOffer: offer id is required");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const { data: responseData } = await duffelFetch<DuffelOfferResponse>({
|
|
726
|
+
config: resolvedConfig,
|
|
727
|
+
method: "GET",
|
|
728
|
+
directPath: `/air/offers/${encodeURIComponent(id.trim())}`,
|
|
729
|
+
cloudRelayPath: `/api/cloud/travel-providers/duffel/offers/${encodeURIComponent(id.trim())}`,
|
|
730
|
+
operation: "offer_retrieve",
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
return mapOffer(responseData.data);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export async function createOrder(
|
|
737
|
+
request: CreateDuffelOrderRequest,
|
|
738
|
+
config?: DuffelConfig,
|
|
739
|
+
): Promise<DuffelOrder> {
|
|
740
|
+
const resolvedConfig = config ?? readDuffelConfigFromEnv();
|
|
741
|
+
if (request.selectedOffers.length !== 1) {
|
|
742
|
+
throw new Error(
|
|
743
|
+
"Duffel createOrder: exactly one selected offer is required",
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
if (request.passengers.length === 0) {
|
|
747
|
+
throw new Error("Duffel createOrder: at least one passenger is required");
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const data: Record<string, unknown> = {
|
|
751
|
+
type: request.type,
|
|
752
|
+
selected_offers: [...request.selectedOffers],
|
|
753
|
+
passengers: request.passengers.map((passenger) => ({
|
|
754
|
+
id: passenger.id,
|
|
755
|
+
title: passenger.title,
|
|
756
|
+
gender: passenger.gender,
|
|
757
|
+
given_name: passenger.givenName,
|
|
758
|
+
family_name: passenger.familyName,
|
|
759
|
+
born_on: passenger.bornOn,
|
|
760
|
+
email: passenger.email,
|
|
761
|
+
phone_number: passenger.phoneNumber,
|
|
762
|
+
})),
|
|
763
|
+
};
|
|
764
|
+
if (request.payment) {
|
|
765
|
+
data.payments = [
|
|
766
|
+
{
|
|
767
|
+
type: request.payment.type,
|
|
768
|
+
amount: request.payment.amount,
|
|
769
|
+
currency: request.payment.currency,
|
|
770
|
+
},
|
|
771
|
+
];
|
|
772
|
+
}
|
|
773
|
+
if (request.metadata && Object.keys(request.metadata).length > 0) {
|
|
774
|
+
data.metadata = request.metadata;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const { data: response } = await duffelFetch<DuffelOrderResponse>({
|
|
778
|
+
config: resolvedConfig,
|
|
779
|
+
method: "POST",
|
|
780
|
+
directPath: "/air/orders",
|
|
781
|
+
cloudRelayPath: "/api/cloud/travel-providers/duffel/orders",
|
|
782
|
+
body: { data },
|
|
783
|
+
operation: "order_create",
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
return mapOrder(response.data);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export async function getOrder(
|
|
790
|
+
orderId: string,
|
|
791
|
+
config?: DuffelConfig,
|
|
792
|
+
): Promise<DuffelOrder> {
|
|
793
|
+
const resolvedConfig = config ?? readDuffelConfigFromEnv();
|
|
794
|
+
if (!orderId || orderId.trim().length === 0) {
|
|
795
|
+
throw new Error("Duffel getOrder: order id is required");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const { data: response } = await duffelFetch<DuffelOrderResponse>({
|
|
799
|
+
config: resolvedConfig,
|
|
800
|
+
method: "GET",
|
|
801
|
+
directPath: `/air/orders/${encodeURIComponent(orderId.trim())}`,
|
|
802
|
+
cloudRelayPath: `/api/cloud/travel-providers/duffel/orders/${encodeURIComponent(orderId.trim())}`,
|
|
803
|
+
operation: "order_retrieve",
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
return mapOrder(response.data);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export async function createPayment(
|
|
810
|
+
args: {
|
|
811
|
+
orderId: string;
|
|
812
|
+
amount: string;
|
|
813
|
+
currency: string;
|
|
814
|
+
},
|
|
815
|
+
config?: DuffelConfig,
|
|
816
|
+
): Promise<DuffelPayment> {
|
|
817
|
+
const resolvedConfig = config ?? readDuffelConfigFromEnv();
|
|
818
|
+
if (args.orderId.trim().length === 0) {
|
|
819
|
+
throw new Error("Duffel createPayment: order id is required");
|
|
820
|
+
}
|
|
821
|
+
if (args.amount.trim().length === 0) {
|
|
822
|
+
throw new Error("Duffel createPayment: amount is required");
|
|
823
|
+
}
|
|
824
|
+
if (args.currency.trim().length === 0) {
|
|
825
|
+
throw new Error("Duffel createPayment: currency is required");
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const { data: response } = await duffelFetch<DuffelPaymentResponse>({
|
|
829
|
+
config: resolvedConfig,
|
|
830
|
+
method: "POST",
|
|
831
|
+
directPath: "/air/payments",
|
|
832
|
+
cloudRelayPath: "/api/cloud/travel-providers/duffel/payments",
|
|
833
|
+
body: {
|
|
834
|
+
data: {
|
|
835
|
+
order_id: args.orderId.trim(),
|
|
836
|
+
payment: {
|
|
837
|
+
type: "balance",
|
|
838
|
+
amount: args.amount.trim(),
|
|
839
|
+
currency: args.currency.trim().toUpperCase(),
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
operation: "payment_create",
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
return mapPayment(response.data);
|
|
847
|
+
}
|