@surfiran/public-api-client 0.1.0

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 ADDED
@@ -0,0 +1,125 @@
1
+ # Public API Client
2
+
3
+ TypeScript types and a fetch-based client for SURFIRAN CRM Public API V1 (`/api/v1/*`). Zero runtime dependencies beyond `fetch`.
4
+
5
+ ## Contract Source
6
+
7
+ ```text
8
+ server/openapi/public-api-v1.yaml (v0.6.0)
9
+ ```
10
+
11
+ ## Current Runtime Scope
12
+
13
+ Active read endpoints:
14
+
15
+ ```http
16
+ GET /api/v1/homepage
17
+ GET /api/v1/site-navigation
18
+ GET /api/v1/sitemap
19
+ GET /api/v1/search-suggestions
20
+ GET /api/v1/tour-categories
21
+ GET /api/v1/tours
22
+ GET /api/v1/tours/:slug
23
+ GET /api/v1/tours/:slug/departures
24
+ GET /api/v1/tours/:slug/availability
25
+ GET /api/v1/destinations
26
+ GET /api/v1/destinations/:slug
27
+ GET /api/v1/travel-knowledge
28
+ ```
29
+
30
+ Active narrow public write endpoints:
31
+
32
+ ```http
33
+ POST /api/v1/inquiries
34
+ POST /api/v1/booking-intents
35
+ ```
36
+
37
+ Blocked endpoint:
38
+
39
+ ```http
40
+ GET /api/v1/booking-intents/:id
41
+ ```
42
+
43
+ `POST /api/v1/booking-intents` creates a public booking-intent record only. It does not create bookings, traveler records, payments, uploads, inventory holds, supplier-private records, BFF behavior, or Next.js behavior.
44
+
45
+ `GET /api/v1/booking-intents/:id` remains blocked at `501 PUBLIC_API_ROUTE_NOT_IMPLEMENTED` and is intentionally not exposed by this client.
46
+
47
+ ## Exported Types
48
+
49
+ All types are aligned with the OpenAPI specification. Key exports:
50
+
51
+ - `PublicApiErrorCode` - union of current public error code strings
52
+ - `PublicApiV1Path` - union of active client paths
53
+ - `PublicApiV1RouteMap` - maps supported `GET` and `POST` client operations to response types
54
+ - `PublicTourListItem`, `PublicTourDetail`
55
+ - `PublicDepartureListItem`
56
+ - `PublicAvailabilityListItem`, `PublicAvailabilityListResponse`
57
+ - `PublicDestinationListItem`, `PublicDestinationDetail`
58
+ - `PublicTourListResponse`, `PublicTourDetailResponse`
59
+ - `PublicDepartureListResponse`
60
+ - `PublicDestinationListResponse`, `PublicDestinationDetailResponse`
61
+ - `PublicInquiryRequest`, `PublicInquiryResponse`
62
+ - `PublicBookingIntentRequest`, `PublicBookingIntentResponse`
63
+ - `PublicTravelKnowledgeItem`, `PublicTravelKnowledgeResponse`
64
+ - `PublicError`, `PublicErrorEnvelope`
65
+ - `PublicApiNotImplementedError`, `PublicApiValidationError`
66
+ - `PublicApiRateLimitError`, `PublicApiAbuseBlockedError`
67
+ - `PublicApiIdempotencyConflictError`
68
+
69
+ ## Client Functions
70
+
71
+ ```ts
72
+ import { createPublicApiClient } from '@surfiran/public-api-client';
73
+
74
+ const client = createPublicApiClient({ baseUrl: 'https://crm.example.com' });
75
+
76
+ const homepage = await client.getPublicHomepage();
77
+ const siteNavigation = await client.getPublicSiteNavigation();
78
+ const sitemap = await client.getPublicSitemap({ type: 'tour' });
79
+ const suggestions = await client.getPublicSearchSuggestions({ q: 'iran' });
80
+ const categories = await client.listPublicTourCategories({ limit: 20 });
81
+ const tours = await client.listPublicTours({ limit: 20 });
82
+ const tour = await client.getPublicTourBySlug('iran-hiking-tour');
83
+ const departures = await client.listPublicTourDepartures('iran-hiking-tour', { limit: 20 });
84
+ const availability = await client.listPublicTourAvailability('iran-hiking-tour', { limit: 20 });
85
+ const destinations = await client.listPublicDestinations({ limit: 20 });
86
+ const destination = await client.getPublicDestinationBySlug('tehran');
87
+ const travelKnowledge = await client.getPublicTravelKnowledge({ type: 'city', q: 'shiraz' });
88
+ ```
89
+
90
+ Public write calls require caller-provided idempotency keys where the Public API contract requires them:
91
+
92
+ ```ts
93
+ const inquiry = await client.createPublicInquiry(
94
+ { name: 'Traveler', email: 'traveler@example.com', message: 'I am interested in Iran tours.' },
95
+ 'client-generated-idempotency-key',
96
+ );
97
+
98
+ const bookingIntent = await client.createPublicBookingIntent(
99
+ { tourSlug: 'iran-hiking-tour', travelerCount: 2 },
100
+ 'client-generated-idempotency-key',
101
+ );
102
+ ```
103
+
104
+ ## Boundaries
105
+
106
+ This package must never:
107
+
108
+ - Import CRM server modules or connect to MariaDB
109
+ - Call internal CRM routes
110
+ - Export raw CRM row types or sensitive fields
111
+ - Expose `storage_path`, upload-private fields, passenger data, supplier-private data, payment data, audit data, tokens, cookies, or internal notes
112
+ - Add a client method for `GET /api/v1/booking-intents/:id` until that route is separately approved and implemented
113
+
114
+ ## Media Projection Boundary
115
+
116
+ `heroImage` and `gallery` remain public-safe placeholders until a separate Public API media projection design is approved. This package must not infer public media URLs from `file_uploads.storage_path` or authenticated upload preview URLs.
117
+
118
+ ## Next.js Usage
119
+
120
+ This client is already consumed from Next.js in production: `apps/web/lib/public*.ts` (tours, destinations, sitemap, search suggestions, site navigation) and multiple `apps/web/components/*` build on `createPublicApiClient` via the `@surfiran/public-api-client` path alias. There is no remaining gate blocking this consumption.
121
+
122
+ ## Next Steps
123
+
124
+ - Keep this package aligned with `server/openapi/public-api-v1.yaml`
125
+ - Wire code generation only after an owner-approved codegen plan
@@ -0,0 +1,56 @@
1
+ import type { GetPublicSearchSuggestionsParams, GetPublicSitemapParams, GetPublicTravelKnowledgeParams, ListPublicDeparturesParams, ListPublicDestinationsParams, ListPublicTourAccommodationsParams, ListPublicTourAvailabilityParams, ListPublicTourCategoriesParams, ListPublicTourExperiencesParams, ListPublicTravellerStoriesParams, ListPublicTourQuestionsParams, ListPublicTourReviewsParams, ListPublicTourItineraryParams, ListPublicTourRouteParams, ListPublicToursParams, PublicApiResult, PublicAvailabilityListResponse, PublicBookingIntentRequest, PublicBookingIntentResponse, PublicBrochureLeadRequest, PublicBrochureLeadResponse, PublicDepartureListResponse, PublicDestinationDetailResponse, PublicDestinationListResponse, PublicHomepageResponse, PublicInquiryRequest, PublicInquiryResponse, PublicSearchSuggestionsResponse, PublicSitemapResponse, PublicSiteChromeContentResponse, PublicSiteHomeContentResponse, PublicSiteNavigationResponse, PublicSiteTrustPageContentResponse, PublicTourAccommodationListResponse, PublicTourCategoryListResponse, PublicTourDetailResponse, PublicTourExperienceListResponse, PublicTravellerStoryListResponse, PublicTourListResponse, PublicTourQuestionListResponse, PublicTourReviewsSummaryResponse, PublicTourReviewListResponse, PublicTourItineraryResponse, PublicTourRouteResponse, PublicTravelKnowledgeResponse, PublicTravelStatusResponse } from './types.js';
2
+ export type PublicApiClientOptions = {
3
+ baseUrl: string;
4
+ fetchImpl?: typeof fetch;
5
+ requestId?: string;
6
+ /**
7
+ * Bounds every request issued by this client. Defaults to 10000ms, matching
8
+ * the existing AbortController-based timeout precedent in
9
+ * server/services/webhookDispatcher.ts.
10
+ */
11
+ timeoutMs?: number;
12
+ };
13
+ export declare const createPublicApiClient: (options: PublicApiClientOptions) => {
14
+ listPublicTours(params?: ListPublicToursParams): Promise<PublicApiResult<PublicTourListResponse>>;
15
+ getPublicTourBySlug(slug: string): Promise<PublicApiResult<PublicTourDetailResponse>>;
16
+ listPublicTourDepartures(slug: string, params?: ListPublicDeparturesParams): Promise<PublicApiResult<PublicDepartureListResponse>>;
17
+ listPublicTourQuestions(slug: string, params?: ListPublicTourQuestionsParams): Promise<PublicApiResult<PublicTourQuestionListResponse>>;
18
+ listPublicTourExperiences(slug: string, params?: ListPublicTourExperiencesParams): Promise<PublicApiResult<PublicTourExperienceListResponse>>;
19
+ /** Published stories across every tour, plus the tour-less ones — the homepage rail. */
20
+ listPublicTravellerStories(params?: ListPublicTravellerStoriesParams): Promise<PublicApiResult<PublicTravellerStoryListResponse>>;
21
+ listPublicTourAccommodations(slug: string, params?: ListPublicTourAccommodationsParams): Promise<PublicApiResult<PublicTourAccommodationListResponse>>;
22
+ getPublicTourReviewsSummary(slug: string): Promise<PublicApiResult<PublicTourReviewsSummaryResponse>>;
23
+ listPublicTourReviews(slug: string, params?: ListPublicTourReviewsParams): Promise<PublicApiResult<PublicTourReviewListResponse>>;
24
+ listPublicTourRoute(slug: string, params?: ListPublicTourRouteParams): Promise<PublicApiResult<PublicTourRouteResponse>>;
25
+ listPublicTourItinerary(slug: string, params?: ListPublicTourItineraryParams): Promise<PublicApiResult<PublicTourItineraryResponse>>;
26
+ listPublicTourAvailability(slug: string, params?: ListPublicTourAvailabilityParams): Promise<PublicApiResult<PublicAvailabilityListResponse>>;
27
+ listPublicDestinations(params?: ListPublicDestinationsParams): Promise<PublicApiResult<PublicDestinationListResponse>>;
28
+ getPublicDestinationBySlug(slug: string): Promise<PublicApiResult<PublicDestinationDetailResponse>>;
29
+ createPublicInquiry(body: PublicInquiryRequest, idempotencyKey?: string): Promise<PublicApiResult<PublicInquiryResponse>>;
30
+ createPublicBookingIntent(body: PublicBookingIntentRequest, idempotencyKey?: string): Promise<PublicApiResult<PublicBookingIntentResponse>>;
31
+ createPublicBrochureLead(body: PublicBrochureLeadRequest, idempotencyKey?: string): Promise<PublicApiResult<PublicBrochureLeadResponse>>;
32
+ getPublicHomepage(): Promise<PublicApiResult<PublicHomepageResponse>>;
33
+ getPublicSiteNavigation(): Promise<PublicApiResult<PublicSiteNavigationResponse>>;
34
+ getPublicSitemap(params?: GetPublicSitemapParams): Promise<PublicApiResult<PublicSitemapResponse>>;
35
+ getPublicSearchSuggestions(params: GetPublicSearchSuggestionsParams): Promise<PublicApiResult<PublicSearchSuggestionsResponse>>;
36
+ listPublicTourCategories(params?: ListPublicTourCategoriesParams): Promise<PublicApiResult<PublicTourCategoryListResponse>>;
37
+ getPublicTravelStatus(): Promise<PublicApiResult<PublicTravelStatusResponse>>;
38
+ getPublicSiteContentHome(): Promise<PublicApiResult<PublicSiteHomeContentResponse>>;
39
+ getPublicSiteContentHeader(): Promise<PublicApiResult<PublicSiteChromeContentResponse>>;
40
+ getPublicSiteContentFooter(): Promise<PublicApiResult<PublicSiteChromeContentResponse>>;
41
+ getPublicSiteContentAbout(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
42
+ getPublicSiteContentContact(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
43
+ getPublicSiteContentFaqs(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
44
+ getPublicSiteContentPrivacy(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
45
+ getPublicSiteContentTerms(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
46
+ getPublicSiteContentResponsibleTravel(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
47
+ getPublicSiteContentIranTravelStatus(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
48
+ getPublicSiteContentIranVisaInformation(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
49
+ getPublicSiteContentMoneyInIran(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
50
+ getPublicSiteContentTravelInsurance(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
51
+ getPublicSiteContentHealthSafetySupport(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
52
+ getPublicSiteContentBookingCancellationPolicy(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
53
+ getPublicSiteContentCookiePolicy(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
54
+ getPublicSiteContentAccessibilityStatement(): Promise<PublicApiResult<PublicSiteTrustPageContentResponse>>;
55
+ getPublicTravelKnowledge(params: GetPublicTravelKnowledgeParams): Promise<PublicApiResult<PublicTravelKnowledgeResponse>>;
56
+ };
package/dist/client.js ADDED
@@ -0,0 +1,288 @@
1
+ const DEFAULT_TIMEOUT_MS = 10000;
2
+ const trimTrailingSlash = (value) => value.replace(/\/+$/, '');
3
+ const isAbortError = (err) => err instanceof Error && err.name === 'AbortError';
4
+ const toTimeoutResult = (status, requestId) => ({
5
+ ok: false,
6
+ error: {
7
+ code: 'PUBLIC_API_CLIENT_TIMEOUT',
8
+ message: 'Public API request timed out.',
9
+ status,
10
+ requestId,
11
+ },
12
+ status,
13
+ requestId,
14
+ });
15
+ const toPublicError = async (response) => {
16
+ try {
17
+ const body = await response.json();
18
+ if (body?.error?.code && body?.error?.message && body?.error?.status)
19
+ return body;
20
+ }
21
+ catch {
22
+ // Fall through to safe public error below.
23
+ }
24
+ return {
25
+ error: {
26
+ code: 'PUBLIC_API_CLIENT_ERROR',
27
+ message: 'Public API request failed.',
28
+ status: response.status,
29
+ requestId: response.headers.get('x-request-id'),
30
+ },
31
+ };
32
+ };
33
+ const requestJson = async (url, options) => {
34
+ const fetcher = options.fetchImpl || fetch;
35
+ const headers = new Headers();
36
+ if (options.requestId)
37
+ headers.set('x-request-id', options.requestId);
38
+ const controller = new AbortController();
39
+ const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
40
+ let response;
41
+ try {
42
+ response = await fetcher(url, { headers, signal: controller.signal });
43
+ }
44
+ catch (err) {
45
+ if (isAbortError(err))
46
+ return toTimeoutResult(0, options.requestId || null);
47
+ throw err;
48
+ }
49
+ finally {
50
+ clearTimeout(timeoutId);
51
+ }
52
+ const requestId = response.headers.get('x-request-id') || options.requestId || null;
53
+ if (!response.ok) {
54
+ const publicError = await toPublicError(response);
55
+ return {
56
+ ok: false,
57
+ error: { ...publicError.error, requestId: publicError.error.requestId || requestId },
58
+ status: response.status,
59
+ requestId,
60
+ };
61
+ }
62
+ const data = await response.json();
63
+ return { ok: true, data, status: response.status, requestId };
64
+ };
65
+ const toPaginationQueryString = (params) => {
66
+ const search = new URLSearchParams();
67
+ if (params?.limit !== undefined)
68
+ search.set('limit', String(params.limit));
69
+ if (params?.cursor)
70
+ search.set('cursor', params.cursor);
71
+ const value = search.toString();
72
+ return value ? `?${value}` : '';
73
+ };
74
+ const toQueryString = (params) => {
75
+ const search = new URLSearchParams();
76
+ if (params?.limit !== undefined)
77
+ search.set('limit', String(params.limit));
78
+ if (params?.cursor)
79
+ search.set('cursor', params.cursor);
80
+ if (params?.destination)
81
+ search.set('destination', params.destination);
82
+ if (params?.minDuration !== undefined)
83
+ search.set('minDuration', String(params.minDuration));
84
+ if (params?.maxDuration !== undefined)
85
+ search.set('maxDuration', String(params.maxDuration));
86
+ const value = search.toString();
87
+ return value ? `?${value}` : '';
88
+ };
89
+ const toSitemapQueryString = (params) => {
90
+ const search = new URLSearchParams();
91
+ if (params?.type)
92
+ search.set('type', params.type);
93
+ const value = search.toString();
94
+ return value ? `?${value}` : '';
95
+ };
96
+ const toSearchSuggestionsQueryString = (params) => {
97
+ const search = new URLSearchParams();
98
+ search.set('q', params.q);
99
+ if (params.limit !== undefined)
100
+ search.set('limit', String(params.limit));
101
+ if (params.type)
102
+ search.set('type', params.type);
103
+ return `?${search.toString()}`;
104
+ };
105
+ const toTravelKnowledgeQueryString = (params) => {
106
+ const search = new URLSearchParams();
107
+ search.set('type', params.type);
108
+ search.set('q', params.q);
109
+ if (params.limit !== undefined)
110
+ search.set('limit', String(params.limit));
111
+ return `?${search.toString()}`;
112
+ };
113
+ const postJson = async (url, body, options, extraHeaders) => {
114
+ const fetcher = options.fetchImpl || fetch;
115
+ const headers = new Headers({ 'Content-Type': 'application/json' });
116
+ if (options.requestId)
117
+ headers.set('x-request-id', options.requestId);
118
+ if (extraHeaders) {
119
+ for (const [key, value] of Object.entries(extraHeaders)) {
120
+ headers.set(key, value);
121
+ }
122
+ }
123
+ const controller = new AbortController();
124
+ const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
125
+ let response;
126
+ try {
127
+ response = await fetcher(url, {
128
+ method: 'POST',
129
+ headers,
130
+ body: JSON.stringify(body),
131
+ signal: controller.signal,
132
+ });
133
+ }
134
+ catch (err) {
135
+ if (isAbortError(err))
136
+ return toTimeoutResult(0, options.requestId || null);
137
+ throw err;
138
+ }
139
+ finally {
140
+ clearTimeout(timeoutId);
141
+ }
142
+ const requestId = response.headers.get('x-request-id') || options.requestId || null;
143
+ if (!response.ok) {
144
+ const publicError = await toPublicError(response);
145
+ return {
146
+ ok: false,
147
+ error: { ...publicError.error, requestId: publicError.error.requestId || requestId },
148
+ status: response.status,
149
+ requestId,
150
+ };
151
+ }
152
+ const data = await response.json();
153
+ return { ok: true, data, status: response.status, requestId };
154
+ };
155
+ export const createPublicApiClient = (options) => {
156
+ const baseUrl = trimTrailingSlash(options.baseUrl);
157
+ return {
158
+ listPublicTours(params) {
159
+ return requestJson(`${baseUrl}/api/v1/tours${toQueryString(params)}`, options);
160
+ },
161
+ getPublicTourBySlug(slug) {
162
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}`, options);
163
+ },
164
+ listPublicTourDepartures(slug, params) {
165
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/departures${toPaginationQueryString(params)}`, options);
166
+ },
167
+ listPublicTourQuestions(slug, params) {
168
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/questions${toPaginationQueryString(params)}`, options);
169
+ },
170
+ listPublicTourExperiences(slug, params) {
171
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/experiences${toPaginationQueryString(params)}`, options);
172
+ },
173
+ /** Published stories across every tour, plus the tour-less ones — the homepage rail. */
174
+ listPublicTravellerStories(params) {
175
+ return requestJson(`${baseUrl}/api/v1/traveller-stories${toPaginationQueryString(params)}`, options);
176
+ },
177
+ listPublicTourAccommodations(slug, params) {
178
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/accommodations${toPaginationQueryString(params)}`, options);
179
+ },
180
+ getPublicTourReviewsSummary(slug) {
181
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/reviews-summary`, options);
182
+ },
183
+ listPublicTourReviews(slug, params) {
184
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/reviews${toPaginationQueryString(params)}`, options);
185
+ },
186
+ listPublicTourRoute(slug, params) {
187
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/route${toPaginationQueryString(params)}`, options);
188
+ },
189
+ listPublicTourItinerary(slug, params) {
190
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/itinerary${toPaginationQueryString(params)}`, options);
191
+ },
192
+ listPublicTourAvailability(slug, params) {
193
+ return requestJson(`${baseUrl}/api/v1/tours/${encodeURIComponent(slug)}/availability${toPaginationQueryString(params)}`, options);
194
+ },
195
+ listPublicDestinations(params) {
196
+ return requestJson(`${baseUrl}/api/v1/destinations${toPaginationQueryString(params)}`, options);
197
+ },
198
+ getPublicDestinationBySlug(slug) {
199
+ return requestJson(`${baseUrl}/api/v1/destinations/${encodeURIComponent(slug)}`, options);
200
+ },
201
+ createPublicInquiry(body, idempotencyKey) {
202
+ const extra = idempotencyKey ? { 'idempotency-key': idempotencyKey } : undefined;
203
+ return postJson(`${baseUrl}/api/v1/inquiries`, body, options, extra);
204
+ },
205
+ createPublicBookingIntent(body, idempotencyKey) {
206
+ const extra = idempotencyKey ? { 'idempotency-key': idempotencyKey } : undefined;
207
+ return postJson(`${baseUrl}/api/v1/booking-intents`, body, options, extra);
208
+ },
209
+ createPublicBrochureLead(body, idempotencyKey) {
210
+ const extra = idempotencyKey ? { 'idempotency-key': idempotencyKey } : undefined;
211
+ return postJson(`${baseUrl}/api/v1/brochure-leads`, body, options, extra);
212
+ },
213
+ getPublicHomepage() {
214
+ return requestJson(`${baseUrl}/api/v1/homepage`, options);
215
+ },
216
+ getPublicSiteNavigation() {
217
+ return requestJson(`${baseUrl}/api/v1/site-navigation`, options);
218
+ },
219
+ getPublicSitemap(params) {
220
+ return requestJson(`${baseUrl}/api/v1/sitemap${toSitemapQueryString(params)}`, options);
221
+ },
222
+ getPublicSearchSuggestions(params) {
223
+ return requestJson(`${baseUrl}/api/v1/search-suggestions${toSearchSuggestionsQueryString(params)}`, options);
224
+ },
225
+ listPublicTourCategories(params) {
226
+ return requestJson(`${baseUrl}/api/v1/tour-categories${toPaginationQueryString(params)}`, options);
227
+ },
228
+ getPublicTravelStatus() {
229
+ return requestJson(`${baseUrl}/api/v1/travel-status`, options);
230
+ },
231
+ // Published CMS site content. One method per approved surface — there is no
232
+ // key/entity parameter, so a caller cannot request an unapproved CMS entity.
233
+ getPublicSiteContentHome() {
234
+ return requestJson(`${baseUrl}/api/v1/site-content/home`, options);
235
+ },
236
+ getPublicSiteContentHeader() {
237
+ return requestJson(`${baseUrl}/api/v1/site-content/header`, options);
238
+ },
239
+ getPublicSiteContentFooter() {
240
+ return requestJson(`${baseUrl}/api/v1/site-content/footer`, options);
241
+ },
242
+ getPublicSiteContentAbout() {
243
+ return requestJson(`${baseUrl}/api/v1/site-content/about`, options);
244
+ },
245
+ getPublicSiteContentContact() {
246
+ return requestJson(`${baseUrl}/api/v1/site-content/contact`, options);
247
+ },
248
+ getPublicSiteContentFaqs() {
249
+ return requestJson(`${baseUrl}/api/v1/site-content/faqs`, options);
250
+ },
251
+ getPublicSiteContentPrivacy() {
252
+ return requestJson(`${baseUrl}/api/v1/site-content/privacy`, options);
253
+ },
254
+ getPublicSiteContentTerms() {
255
+ return requestJson(`${baseUrl}/api/v1/site-content/terms`, options);
256
+ },
257
+ getPublicSiteContentResponsibleTravel() {
258
+ return requestJson(`${baseUrl}/api/v1/site-content/responsible-travel`, options);
259
+ },
260
+ getPublicSiteContentIranTravelStatus() {
261
+ return requestJson(`${baseUrl}/api/v1/site-content/iran-travel-status`, options);
262
+ },
263
+ getPublicSiteContentIranVisaInformation() {
264
+ return requestJson(`${baseUrl}/api/v1/site-content/iran-visa-information`, options);
265
+ },
266
+ getPublicSiteContentMoneyInIran() {
267
+ return requestJson(`${baseUrl}/api/v1/site-content/money-in-iran`, options);
268
+ },
269
+ getPublicSiteContentTravelInsurance() {
270
+ return requestJson(`${baseUrl}/api/v1/site-content/travel-insurance`, options);
271
+ },
272
+ getPublicSiteContentHealthSafetySupport() {
273
+ return requestJson(`${baseUrl}/api/v1/site-content/health-safety-support`, options);
274
+ },
275
+ getPublicSiteContentBookingCancellationPolicy() {
276
+ return requestJson(`${baseUrl}/api/v1/site-content/booking-cancellation-policy`, options);
277
+ },
278
+ getPublicSiteContentCookiePolicy() {
279
+ return requestJson(`${baseUrl}/api/v1/site-content/cookie-policy`, options);
280
+ },
281
+ getPublicSiteContentAccessibilityStatement() {
282
+ return requestJson(`${baseUrl}/api/v1/site-content/accessibility-statement`, options);
283
+ },
284
+ getPublicTravelKnowledge(params) {
285
+ return requestJson(`${baseUrl}/api/v1/travel-knowledge${toTravelKnowledgeQueryString(params)}`, options);
286
+ },
287
+ };
288
+ };
@@ -0,0 +1,7 @@
1
+ export type GeneratedPublicApiVersion = '0.8.0';
2
+ export type GeneratedPublicApiPath = '/homepage' | '/tours' | '/tours/{slug}' | '/tours/{slug}/availability' | '/tours/{slug}/departures' | '/tours/{slug}/questions' | '/tours/{slug}/experiences' | '/traveller-stories' | '/tours/{slug}/accommodations' | '/tours/{slug}/reviews-summary' | '/tours/{slug}/reviews' | '/tours/{slug}/route' | '/tours/{slug}/itinerary' | '/destinations' | '/destinations/{slug}' | '/sitemap' | '/tour-categories' | '/site-navigation' | '/search-suggestions' | '/travel-status' | '/travel-knowledge' | '/site-content/home' | '/site-content/header' | '/site-content/footer' | '/site-content/about' | '/site-content/contact' | '/site-content/faqs' | '/site-content/privacy' | '/site-content/terms' | '/site-content/responsible-travel' | '/site-content/iran-travel-status' | '/site-content/iran-visa-information' | '/site-content/money-in-iran' | '/site-content/travel-insurance' | '/site-content/health-safety-support' | '/site-content/booking-cancellation-policy' | '/site-content/cookie-policy' | '/site-content/accessibility-statement' | '/inquiries' | '/booking-intents' | '/brochure-leads' | '/booking-intents/{id}';
3
+ export type GeneratedPublicApiOperation = 'GET /homepage' | 'GET /tours' | 'GET /tours/{slug}' | 'GET /tours/{slug}/availability' | 'GET /tours/{slug}/departures' | 'GET /tours/{slug}/questions' | 'GET /tours/{slug}/experiences' | 'GET /traveller-stories' | 'GET /tours/{slug}/accommodations' | 'GET /tours/{slug}/reviews-summary' | 'GET /tours/{slug}/reviews' | 'GET /tours/{slug}/route' | 'GET /tours/{slug}/itinerary' | 'GET /destinations' | 'GET /destinations/{slug}' | 'GET /sitemap' | 'GET /tour-categories' | 'GET /site-navigation' | 'GET /search-suggestions' | 'GET /travel-status' | 'GET /travel-knowledge' | 'GET /site-content/home' | 'GET /site-content/header' | 'GET /site-content/footer' | 'GET /site-content/about' | 'GET /site-content/contact' | 'GET /site-content/faqs' | 'GET /site-content/privacy' | 'GET /site-content/terms' | 'GET /site-content/responsible-travel' | 'GET /site-content/iran-travel-status' | 'GET /site-content/iran-visa-information' | 'GET /site-content/money-in-iran' | 'GET /site-content/travel-insurance' | 'GET /site-content/health-safety-support' | 'GET /site-content/booking-cancellation-policy' | 'GET /site-content/cookie-policy' | 'GET /site-content/accessibility-statement' | 'POST /inquiries' | 'POST /booking-intents' | 'POST /brochure-leads' | 'GET /booking-intents/{id}';
4
+ export type GeneratedPublicApiActiveOperation = 'GET /homepage' | 'GET /tours' | 'GET /tours/{slug}' | 'GET /tours/{slug}/availability' | 'GET /tours/{slug}/departures' | 'GET /tours/{slug}/questions' | 'GET /tours/{slug}/experiences' | 'GET /traveller-stories' | 'GET /tours/{slug}/accommodations' | 'GET /tours/{slug}/reviews-summary' | 'GET /tours/{slug}/reviews' | 'GET /tours/{slug}/route' | 'GET /tours/{slug}/itinerary' | 'GET /destinations' | 'GET /destinations/{slug}' | 'GET /sitemap' | 'GET /tour-categories' | 'GET /site-navigation' | 'GET /search-suggestions' | 'GET /travel-status' | 'GET /travel-knowledge' | 'GET /site-content/home' | 'GET /site-content/header' | 'GET /site-content/footer' | 'GET /site-content/about' | 'GET /site-content/contact' | 'GET /site-content/faqs' | 'GET /site-content/privacy' | 'GET /site-content/terms' | 'GET /site-content/responsible-travel' | 'GET /site-content/iran-travel-status' | 'GET /site-content/iran-visa-information' | 'GET /site-content/money-in-iran' | 'GET /site-content/travel-insurance' | 'GET /site-content/health-safety-support' | 'GET /site-content/booking-cancellation-policy' | 'GET /site-content/cookie-policy' | 'GET /site-content/accessibility-statement' | 'POST /inquiries' | 'POST /booking-intents' | 'POST /brochure-leads';
5
+ export type GeneratedPublicApiBlockedOperation = 'GET /booking-intents/{id}';
6
+ export type GeneratedPublicApiOperationId = 'getPublicHomepage' | 'listPublicTours' | 'getPublicTourBySlug' | 'listPublicTourAvailability' | 'listPublicTourDepartures' | 'listPublicTourQuestions' | 'listPublicTourExperiences' | 'listPublicTravellerStories' | 'listPublicTourAccommodations' | 'getPublicTourReviewsSummary' | 'listPublicTourReviews' | 'listPublicTourRoute' | 'listPublicTourItinerary' | 'listPublicDestinations' | 'getPublicDestinationBySlug' | 'getPublicSitemap' | 'listPublicTourCategories' | 'getPublicSiteNavigation' | 'getPublicSearchSuggestions' | 'getPublicTravelStatus' | 'listPublicTravelKnowledge' | 'getPublicSiteContentHome' | 'getPublicSiteContentHeader' | 'getPublicSiteContentFooter' | 'getPublicSiteContentAbout' | 'getPublicSiteContentContact' | 'getPublicSiteContentFaqs' | 'getPublicSiteContentPrivacy' | 'getPublicSiteContentTerms' | 'getPublicSiteContentResponsibleTravel' | 'getPublicSiteContentIranTravelStatus' | 'getPublicSiteContentIranVisaInformation' | 'getPublicSiteContentMoneyInIran' | 'getPublicSiteContentTravelInsurance' | 'getPublicSiteContentHealthSafetySupport' | 'getPublicSiteContentBookingCancellationPolicy' | 'getPublicSiteContentCookiePolicy' | 'getPublicSiteContentAccessibilityStatement' | 'createPublicInquiry' | 'createPublicBookingIntent' | 'createPublicBrochureLead' | 'getBookingIntent';
7
+ export type GeneratedPublicApiSchemaName = 'PublicHomepageResponse' | 'PublicHomepageStats' | 'PublicHomepageSeo' | 'PublicTourListResponse' | 'PublicTourDetailResponse' | 'PublicAvailabilityListResponse' | 'PublicDepartureListResponse' | 'PublicTourQuestionListResponse' | 'PublicTourExperienceListResponse' | 'PublicTravellerStoryListResponse' | 'PublicTourAccommodationListResponse' | 'PublicTourReviewsSummaryResponse' | 'PublicTourReviewListResponse' | 'PublicTourRouteResponse' | 'PublicDestinationListResponse' | 'PublicDestinationDetailResponse' | 'PublicSitemapResponse' | 'PublicSitemapEntry' | 'PublicTourCategoryListResponse' | 'PublicSearchSuggestionsResponse' | 'PublicTourCategoryListItem' | 'PublicSeo' | 'PublicSearchSuggestion' | 'PublicTourListItem' | 'PublicTourDetail' | 'PublicAvailabilityListItem' | 'PublicDepartureListItem' | 'PublicTourQuestionListItem' | 'PublicTourExperienceFrame' | 'PublicTourExperienceListItem' | 'PublicTravellerStoryListItem' | 'PublicTourAccommodationListItem' | 'PublicTourReviewsAggregate' | 'PublicTourRatingBreakdownItem' | 'PublicTourReviewsSummary' | 'PublicTourReviewListItem' | 'PublicTourRouteStop' | 'PublicTourItineraryDay' | 'PublicTourItineraryResponse' | 'PublicDestinationListItem' | 'PublicDestinationDetail' | 'PublicNavLink' | 'PublicStaticLink' | 'PublicSiteNavigationResponse' | 'PublicSiteRouteKey' | 'PublicSiteCta' | 'PublicSiteHomeSection' | 'PublicSiteHomeContent' | 'PublicSiteHomeContentResponse' | 'PublicSiteNavItem' | 'PublicSiteSocialLink' | 'PublicSiteChromeContent' | 'PublicSiteChromeContentResponse' | 'PublicSiteTrustTitledItem' | 'PublicSiteTrustSection' | 'PublicSiteTrustPageSeo' | 'PublicSiteTrustPageContent' | 'PublicSiteTrustPageContentResponse' | 'CreateInquiryRequest' | 'CreateInquiryResponse' | 'CreateBrochureLeadRequest' | 'CreateBrochureLeadResponse' | 'CreateBookingIntentRequest' | 'PublicBookingIntentStatus' | 'PublicBookingIntentPublicStatus' | 'PublicBookingIntentLinks' | 'CreateBookingIntentSuccessResponse' | 'GetBookingIntentStatusResponse' | 'BookingIntentConflictErrorResponse' | 'BookingIntentResponse' | 'PublicError' | 'PublicApiError' | 'PublicApiNotImplementedError' | 'PublicApiValidationError' | 'PublicApiRateLimitError' | 'PublicApiAbuseBlockedError' | 'PublicApiInternalError' | 'PublicApiNotFoundError' | 'PublicApiTravelStatusUnavailableError' | 'PublicErrorEnvelope' | 'PaginationMeta' | 'PublicTravelStatusState' | 'PublicTravelStatusReviewerLabel' | 'PublicTravelStatusFreshness' | 'PublicOfficialSourceCard' | 'PublicSurfiranOperationalUpdate' | 'PublicTravelStatusCorrectionNotice' | 'PublicTravelStatusHandoff' | 'PublicTravelStatusSection' | 'PublicTravelStatusResponse' | 'PublicTravelKnowledgeItem' | 'PublicTravelKnowledgeResponse';
@@ -0,0 +1,4 @@
1
+ // AUTO-GENERATED FILE. DO NOT EDIT.
2
+ // Source: server/openapi/public-api-v1.yaml
3
+ // Generator: scripts/generate-public-api-v1-types.cjs
4
+ export {};
@@ -0,0 +1,4 @@
1
+ export { createPublicApiClient } from './client.js';
2
+ export type { PublicApiErrorCode, PublicApiV1Path, PublicApiV1RouteMap, PublicError, PublicErrorEnvelope, PublicApiError, PublicApiNotImplementedError, PublicApiValidationError, PublicApiRateLimitError, PublicApiAbuseBlockedError, PublicTourListItem, PublicTourDetail, PublicDepartureListItem, PublicAvailabilityStatus, PublicAvailabilityListItem, PublicAvailabilityListResponse, PublicDestinationListItem, PublicDestinationDetail, PublicTourListResponse, PublicTourDetailResponse, PublicDepartureListResponse, PublicDestinationListResponse, PublicDestinationDetailResponse, PublicApiResult, ListPublicToursParams, ListPublicDeparturesParams, ListPublicTourAvailabilityParams, ListPublicDestinationsParams, PublicInquiryRequest, PublicInquiryResponse, PublicBookingIntentRequest, PublicBookingIntentData, PublicBookingIntentResponse, PublicHomepageStats, PublicHomepageSeo, PublicHomepageResponse, PublicSitemapEntry, PublicSitemapResponse, PublicNavLink, PublicStaticLink, PublicSiteNavigationResponse, PublicTourCategoryListItem, PublicTourCategoryListResponse, PublicSearchSuggestion, PublicSearchSuggestionsResponse, PublicSiteRouteKey, PublicSiteCta, PublicSiteHomeSectionType, PublicSiteHomeSection, PublicSiteHomeContent, PublicSiteHomeContentResponse, PublicSiteNavItem, PublicSiteSocialPlatform, PublicSiteSocialLink, PublicSiteChromeContent, PublicSiteChromeContentResponse, PublicSiteTrustSectionType, PublicSiteTrustTitledItem, PublicSiteTrustParagraphGroup, PublicSiteTrustTitledItemList, PublicSiteTrustBulletList, PublicSiteTrustSection, PublicSiteTrustPageSeo, PublicSiteTrustPageContent, PublicSiteTrustPageContentResponse, PublicTravelStatusState, PublicTravelStatusFreshness, PublicSurfiranOperationalUpdate, PublicTravelStatusResponse, PublicTravelKnowledgeType, PublicTravelKnowledgeItem, PublicTravelKnowledgeResponse, GetPublicSitemapParams, GetPublicSearchSuggestionsParams, GetPublicTravelKnowledgeParams, ListPublicTourCategoriesParams, PublicTravellerStoryListItem, PublicTravellerStoryListResponse, ListPublicTravellerStoriesParams, PublicTourQuestionListItem, PublicTourQuestionListResponse, ListPublicTourQuestionsParams, PublicTourExperienceFrame, PublicTourExperienceListItem, PublicTourExperienceListResponse, ListPublicTourExperiencesParams, PublicTourAccommodationListItem, PublicTourAccommodationListResponse, ListPublicTourAccommodationsParams, PublicTourReviewsAggregate, PublicTourRatingBreakdownItem, PublicTourReviewsSummary, PublicTourReviewsSummaryResponse, PublicTourReviewListItem, PublicTourReviewListResponse, PublicTourReviewsLimit, ListPublicTourReviewsParams, PublicTourRouteStop, PublicTourRouteResponse, ListPublicTourRouteParams, PublicItineraryMealCode, PublicTourItineraryDay, PublicTourItineraryResponse, ListPublicTourItineraryParams, PublicBrochureLeadRequest, PublicBrochureLeadResponse, } from './types.js';
3
+ export type { PublicApiClientOptions } from './client.js';
4
+ export type { GeneratedPublicApiVersion, GeneratedPublicApiPath, GeneratedPublicApiOperation, GeneratedPublicApiActiveOperation, GeneratedPublicApiBlockedOperation, GeneratedPublicApiOperationId, GeneratedPublicApiSchemaName, } from './generated/public-api-v1.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createPublicApiClient } from './client.js';
@@ -0,0 +1,748 @@
1
+ export type PublicApiErrorCode = 'PUBLIC_API_ROUTE_NOT_IMPLEMENTED' | 'PUBLIC_API_INTERNAL_ERROR' | 'PUBLIC_API_TOUR_NOT_FOUND' | 'PUBLIC_API_DESTINATION_NOT_FOUND' | 'PUBLIC_API_INVALID_QUERY' | 'PUBLIC_API_BOOKING_INTENT_VALIDATION_ERROR' | 'PUBLIC_API_IDEMPOTENCY_CONFLICT' | 'PUBLIC_API_RATE_LIMITED' | 'PUBLIC_API_ABUSE_BLOCKED' | 'PUBLIC_API_TRAVEL_STATUS_UNAVAILABLE' | 'PUBLIC_API_SITE_CONTENT_UNAVAILABLE' | 'PUBLIC_API_CLIENT_TIMEOUT';
2
+ export type PublicApiV1Path = '/homepage' | '/site-navigation' | '/sitemap' | '/search-suggestions' | '/tour-categories' | '/tours' | '/tours/{slug}' | '/tours/{slug}/departures' | '/tours/{slug}/availability' | '/tours/{slug}/reviews' | '/destinations' | '/destinations/{slug}' | '/inquiries' | '/booking-intents' | '/travel-status' | '/travel-knowledge' | '/site-content/home' | '/site-content/header' | '/site-content/footer' | '/site-content/about' | '/site-content/contact' | '/site-content/faqs' | '/site-content/privacy' | '/site-content/terms' | '/site-content/responsible-travel' | '/site-content/iran-travel-status' | '/site-content/iran-visa-information' | '/site-content/money-in-iran' | '/site-content/travel-insurance' | '/site-content/health-safety-support' | '/site-content/booking-cancellation-policy' | '/site-content/cookie-policy' | '/site-content/accessibility-statement';
3
+ export type PublicError = {
4
+ code: string;
5
+ message: string;
6
+ status: number;
7
+ requestId: string | null;
8
+ };
9
+ export type PublicErrorEnvelope = {
10
+ error: PublicError;
11
+ };
12
+ export type PublicApiError = {
13
+ error: PublicError & {
14
+ code: 'PUBLIC_API_INTERNAL_ERROR' | 'PUBLIC_API_TOUR_NOT_FOUND' | 'PUBLIC_API_DESTINATION_NOT_FOUND';
15
+ };
16
+ };
17
+ export type PublicApiNotImplementedError = {
18
+ error: PublicError & {
19
+ code: 'PUBLIC_API_ROUTE_NOT_IMPLEMENTED';
20
+ status: 501;
21
+ };
22
+ };
23
+ export type PublicApiValidationError = {
24
+ error: PublicError & {
25
+ code: 'PUBLIC_API_INVALID_QUERY' | 'PUBLIC_API_BOOKING_INTENT_VALIDATION_ERROR';
26
+ status: 400;
27
+ };
28
+ };
29
+ export type PublicApiRateLimitError = {
30
+ error: PublicError & {
31
+ code: 'PUBLIC_API_RATE_LIMITED';
32
+ status: 429;
33
+ };
34
+ };
35
+ export type PublicApiAbuseBlockedError = {
36
+ error: PublicError & {
37
+ code: 'PUBLIC_API_ABUSE_BLOCKED';
38
+ status: 403;
39
+ };
40
+ };
41
+ export type PublicApiIdempotencyConflictError = {
42
+ error: PublicError & {
43
+ code: 'PUBLIC_API_IDEMPOTENCY_CONFLICT';
44
+ status: 409;
45
+ };
46
+ };
47
+ export type PublicTourListItem = {
48
+ id: number;
49
+ slug: string;
50
+ title: string;
51
+ summary: string | null;
52
+ durationDays: number | null;
53
+ durationNights: number | null;
54
+ startingPriceDisplay: string | null;
55
+ currency: string | null;
56
+ heroImage: string | null;
57
+ primaryDestinations: string[];
58
+ /**
59
+ * Public trip-theme labels derived from `tour_packages.style` (for example
60
+ * "Cultural", "Family"). Already served by `GET /api/v1/tours`; declared
61
+ * optional so a response projected before this field existed still typechecks.
62
+ * This is a theme, not a product type — see `travelFormat` for group/private.
63
+ */
64
+ categoryLabels?: string[];
65
+ /**
66
+ * Published activity level (`tour_packages.difficulty`), e.g. "Moderate".
67
+ * Optional so a response projected before this field existed still typechecks.
68
+ */
69
+ activityLevel?: string | null;
70
+ /**
71
+ * Countries this tour route touches (`tour_packages.countries`), filtered
72
+ * server-side to a recognized-country whitelist. Always an array — empty
73
+ * when the tour is Iran-only or nothing is published yet, never null or
74
+ * absent, so a consumer never needs a null check before reading `.length`.
75
+ */
76
+ countries: string[];
77
+ /**
78
+ * True when staff have tagged the tour `beyond-iran` in the CRM — an
79
+ * explicit editorial claim that the route belongs in the "Iran and beyond"
80
+ * grid even when its `countries` alone cannot express it (an Iran-only
81
+ * tribal route, for example). Owner directive 2026-08-07: full Home design
82
+ * application. Optional so a response projected before this field existed
83
+ * still typechecks; absent means false.
84
+ */
85
+ beyondIran?: boolean;
86
+ /**
87
+ * How this route is run — `"Small Group"` or `"Private"` — from
88
+ * `tour_packages.travel_format` (draft migration
89
+ * `20260804_tour_packages_travel_format.sql`, not yet executed in
90
+ * production, so this is `null` for every tour today). Null means not
91
+ * published, never a guessed default. Optional so a response projected
92
+ * before this field existed still typechecks. "Tailor-Made" is not a value
93
+ * here — it has no fixed tour to attach to; it is the `/inquire` flow.
94
+ */
95
+ travelFormat?: string | null;
96
+ /**
97
+ * Owner directive 2026-08-07 (full Home design application): staff-authored
98
+ * "was" price display for a current deal
99
+ * (`tour_packages.previous_price_display`, draft migration
100
+ * `20260807_tour_packages_deal_pricing_group_size.sql`), e.g.
101
+ * `"From 2220 EUR"`. Null when no deal is published; never computed from
102
+ * commercial internals. Optional so a response projected before this field
103
+ * existed still typechecks.
104
+ */
105
+ previousPriceDisplay?: string | null;
106
+ /**
107
+ * Staff-authored deal ribbon label (`tour_packages.deal_label`), e.g.
108
+ * `"−15% off"` or `"Sale"`. Null when no deal is published.
109
+ */
110
+ dealLabel?: string | null;
111
+ /**
112
+ * Staff-authored savings pill text (`tour_packages.savings_note`), e.g.
113
+ * `"Save €330 · booked by 31 Aug"`. Null when no deal is published.
114
+ */
115
+ savingsNote?: string | null;
116
+ /**
117
+ * Published tour-level maximum group size (`tour_packages.max_group_size`),
118
+ * the "Group / Max N" card fact. A marketing fact, deliberately distinct
119
+ * from departure-level operational capacity (`tour_departures.max_pax`),
120
+ * which stays excluded from every public projection. Null when not
121
+ * published.
122
+ */
123
+ maxGroupSize?: number | null;
124
+ status: string;
125
+ updatedAt: string | null;
126
+ };
127
+ export type PublicTourDetail = PublicTourListItem & {
128
+ description: string | null;
129
+ route: string[];
130
+ highlights: string[];
131
+ includedItems: string[];
132
+ excludedItems: string[];
133
+ gallery: string[];
134
+ seoTitle: string | null;
135
+ seoDescription: string | null;
136
+ };
137
+ export type PublicDepartureListItem = {
138
+ id: number;
139
+ tourSlug: string;
140
+ startDate: string | null;
141
+ endDate: string | null;
142
+ availabilityStatus: 'Scheduled' | 'Guaranteed' | 'Sold Out';
143
+ priceDisplay: string | null;
144
+ currency: string | null;
145
+ bookingStatus: string | null;
146
+ };
147
+ export type PublicTourQuestionListItem = {
148
+ id: number;
149
+ askerName: string | null;
150
+ askedAt: string | null;
151
+ question: string;
152
+ answer: string;
153
+ answeredByName: string | null;
154
+ answeredByRole: string | null;
155
+ answeredAt: string;
156
+ /** Computed from (answeredAt - askedAt) at read time. Never a stored value. */
157
+ answerTimeHours: number;
158
+ };
159
+ export type PublicTourExperienceFrame = {
160
+ id: number;
161
+ frameType: 'image' | 'video';
162
+ /** Null means awaiting footage — render the placeholder, never a stand-in photo. */
163
+ assetUrl: string | null;
164
+ posterUrl: string | null;
165
+ durationMs: number;
166
+ caption: string | null;
167
+ };
168
+ export type PublicTourExperienceListItem = {
169
+ id: number;
170
+ title: string;
171
+ placeName: string | null;
172
+ dayNumber: number | null;
173
+ durationSeconds: number | null;
174
+ frames: PublicTourExperienceFrame[];
175
+ };
176
+ export type PublicTourAccommodationListItem = {
177
+ id: number;
178
+ name: string;
179
+ cityName: string | null;
180
+ category: string | null;
181
+ /** Required contractual substitution wording, e.g. "a similar 4-star". */
182
+ substitutionLabel: string;
183
+ descriptionOneLine: string | null;
184
+ photoUrl: string | null;
185
+ website: string | null;
186
+ nightsLabel: string;
187
+ };
188
+ export type PublicTourReviewsAggregate = {
189
+ /** Null when reviewCount is 0 — never a fabricated or zero rating. */
190
+ averageRating: number | null;
191
+ reviewCount: number;
192
+ source: string | null;
193
+ /** Most recent staff entry among included reviews — the "as of" honesty marker. */
194
+ lastVerifiedAt: string | null;
195
+ };
196
+ export type PublicTourRatingBreakdownItem = {
197
+ dimension: 'guide' | 'itinerary' | 'accommodation' | 'transport' | 'value';
198
+ score: number;
199
+ reviewCount: number;
200
+ };
201
+ export type PublicTourReviewsSummary = {
202
+ aggregate: PublicTourReviewsAggregate;
203
+ /** Only dimensions a staff member has actually rated appear here. */
204
+ breakdown: PublicTourRatingBreakdownItem[];
205
+ };
206
+ export type PublicTourReviewListItem = {
207
+ authorName: string | null;
208
+ partyType: string | null;
209
+ reviewDate: string | null;
210
+ rating: number;
211
+ title: string;
212
+ body: string;
213
+ deepLink: string | null;
214
+ };
215
+ /** Meal codes a day can include. Always returned in B,L,D order. */
216
+ export type PublicItineraryMealCode = 'B' | 'L' | 'D';
217
+ /**
218
+ * One traveller-facing day of the authored itinerary. Projected from the CRM's
219
+ * `tour_packages.itinerary` JSON; internal fields (guide notes, transport
220
+ * notes, per-day media ids, internal identifiers) are never included.
221
+ *
222
+ * `title` and `description` are non-nullable because a day lacking either is
223
+ * omitted from the response rather than returned incomplete — so a consumer
224
+ * never has to guard against a blank day card.
225
+ */
226
+ export type PublicTourItineraryDay = {
227
+ /** Staff-authored day number, and the stable public identifier for the day. */
228
+ dayNumber: number;
229
+ title: string;
230
+ description: string;
231
+ /** Where guests sleep that night. Null when not authored. */
232
+ overnightLocation: string | null;
233
+ /** Traveller-facing route line, e.g. "Tehran → Kashan". */
234
+ routeSummary: string | null;
235
+ meals: PublicItineraryMealCode[];
236
+ /** Staff-typed stay line for the day. Null when not authored. */
237
+ accommodationNote: string | null;
238
+ highlights: string[];
239
+ includedActivities: string[];
240
+ optionalActivities: string[];
241
+ landmarks: string[];
242
+ };
243
+ export type PublicTourRouteStop = {
244
+ id: number;
245
+ name: string;
246
+ cityName: string | null;
247
+ nights: number | null;
248
+ metaNote: string | null;
249
+ /** Null means no verified coordinate yet — the stop still renders in the ordered list. */
250
+ latitude: number | null;
251
+ longitude: number | null;
252
+ isStart: boolean;
253
+ isEnd: boolean;
254
+ };
255
+ export type PublicAvailabilityStatus = 'available' | 'full' | 'unconstrained' | 'not_found' | 'expired' | 'unavailable';
256
+ export type PublicAvailabilityListItem = {
257
+ tourSlug: string;
258
+ departureId: number;
259
+ startDate: string | null;
260
+ endDate: string | null;
261
+ availabilityStatus: PublicAvailabilityStatus;
262
+ availabilityLabel?: string;
263
+ capacityStatus: PublicAvailabilityStatus;
264
+ bookingStatus: 'open' | 'closed' | null;
265
+ currency: string | null;
266
+ priceDisplay: string | null;
267
+ promoLabel?: string | null;
268
+ roomBasis?: 'twin_share' | 'double_share' | 'single_room' | 'triple_share' | 'family_room' | null;
269
+ remainingCapacityBucket: 'available' | 'limited' | 'full' | 'unconstrained' | 'unknown';
270
+ /**
271
+ * Owner directive 2026-08-07: full Home design application — limited-state
272
+ * remaining places (1–4) are published; raw capacity stays private.
273
+ * Present ONLY when remainingCapacityBucket === 'limited'.
274
+ */
275
+ remainingPlaces?: number;
276
+ holdable: boolean;
277
+ publicUrl?: string;
278
+ ctaUrl?: string;
279
+ updatedAt: string | null;
280
+ };
281
+ export type PublicDestinationListItem = {
282
+ id: number;
283
+ slug: string;
284
+ name: string;
285
+ summary: string | null;
286
+ heroImage: string | null;
287
+ updatedAt: string | null;
288
+ };
289
+ export type PublicDestinationDetail = PublicDestinationListItem & {
290
+ description: string | null;
291
+ seoTitle: string | null;
292
+ seoDescription: string | null;
293
+ };
294
+ export type PublicTourListResponse = {
295
+ data: PublicTourListItem[];
296
+ nextCursor: string | null;
297
+ total: number;
298
+ };
299
+ export type PublicTourDetailResponse = {
300
+ data: PublicTourDetail;
301
+ };
302
+ export type PublicDepartureListResponse = {
303
+ data: PublicDepartureListItem[];
304
+ nextCursor: string | null;
305
+ total: number;
306
+ };
307
+ export type PublicTourQuestionListResponse = {
308
+ data: PublicTourQuestionListItem[];
309
+ total: number;
310
+ };
311
+ export type PublicTourExperienceListResponse = {
312
+ data: PublicTourExperienceListItem[];
313
+ total: number;
314
+ };
315
+ export type PublicTravellerStoryListItem = PublicTourExperienceListItem & {
316
+ /**
317
+ * Public slug of the story's tour. Null when the story belongs to no tour, or
318
+ * when its tour is not public — the rail renders the story unlinked rather
319
+ * than linking to a page that would 404.
320
+ */
321
+ tourSlug: string | null;
322
+ };
323
+ export type PublicTravellerStoryListResponse = {
324
+ data: PublicTravellerStoryListItem[];
325
+ total: number;
326
+ };
327
+ export type PublicTourAccommodationListResponse = {
328
+ data: PublicTourAccommodationListItem[];
329
+ total: number;
330
+ };
331
+ export type PublicTourReviewsSummaryResponse = {
332
+ data: PublicTourReviewsSummary;
333
+ };
334
+ export type PublicTourReviewListResponse = {
335
+ data: PublicTourReviewListItem[];
336
+ total: number;
337
+ };
338
+ export type PublicTourRouteResponse = {
339
+ data: PublicTourRouteStop[];
340
+ total: number;
341
+ };
342
+ /** Empty `data` means the tour has no publishable itinerary content. */
343
+ export type PublicTourItineraryResponse = {
344
+ data: PublicTourItineraryDay[];
345
+ total: number;
346
+ };
347
+ export type PublicAvailabilityListResponse = {
348
+ data: PublicAvailabilityListItem[];
349
+ nextCursor: string | null;
350
+ total: number;
351
+ };
352
+ export type PublicDestinationListResponse = {
353
+ data: PublicDestinationListItem[];
354
+ nextCursor: string | null;
355
+ total: number;
356
+ };
357
+ export type PublicDestinationDetailResponse = {
358
+ data: PublicDestinationDetail;
359
+ };
360
+ export type PublicInquiryRequest = {
361
+ name: string;
362
+ email: string;
363
+ message: string;
364
+ tourSlug?: string | null;
365
+ /**
366
+ * Optional requested departure date (YYYY-MM-DD) when the inquiry was
367
+ * submitted from a specific public departure card. No departure identifier or
368
+ * commercial data is sent.
369
+ */
370
+ departureDate?: string | null;
371
+ };
372
+ export type PublicInquiryResponse = {
373
+ data: {
374
+ accepted: boolean;
375
+ inquiryId: string;
376
+ };
377
+ };
378
+ export type PublicBookingIntentRequest = {
379
+ tourSlug: string;
380
+ departureId?: number | null;
381
+ travelerCount: number;
382
+ leadTravelerName?: string | null;
383
+ leadTravelerEmail?: string | null;
384
+ leadTravelerPhone?: string | null;
385
+ notes?: string | null;
386
+ };
387
+ export type PublicBookingIntentStatus = 'received' | 'needs_review' | 'expired' | 'cancelled' | 'converted_internal';
388
+ export type PublicBookingIntentData = {
389
+ publicId: string;
390
+ status: PublicBookingIntentStatus;
391
+ tourSlug: string;
392
+ departureId: number | null;
393
+ travelerCount: number;
394
+ createdAt: string;
395
+ updatedAt: string;
396
+ expiresAt: string | null;
397
+ priceAtCapture: {
398
+ amount: number;
399
+ currency: string;
400
+ } | null;
401
+ };
402
+ export type PublicBookingIntentResponse = {
403
+ data: PublicBookingIntentData;
404
+ };
405
+ export type PublicBrochureLeadRequest = {
406
+ email: string;
407
+ tourSlug: string;
408
+ /** Separate GDPR double opt-in for the newsletter only, never implied by the brochure request itself. */
409
+ newsletterConsent?: boolean;
410
+ sourceUrl?: string | null;
411
+ utmSource?: string | null;
412
+ utmMedium?: string | null;
413
+ utmCampaign?: string | null;
414
+ };
415
+ export type PublicBrochureLeadResponse = {
416
+ data: {
417
+ accepted: boolean;
418
+ leadId: string;
419
+ };
420
+ };
421
+ export type PublicHomepageStats = {
422
+ publishedTours: number;
423
+ publishedDestinations: number;
424
+ };
425
+ export type PublicHomepageSeo = {
426
+ title: string;
427
+ description: string;
428
+ };
429
+ export type PublicHomepageResponse = {
430
+ data: {
431
+ featuredTours: PublicTourListItem[];
432
+ popularDestinations: PublicDestinationListItem[];
433
+ stats: PublicHomepageStats;
434
+ seo: PublicHomepageSeo;
435
+ };
436
+ };
437
+ export type PublicSitemapEntry = {
438
+ path: string;
439
+ type: 'tour' | 'destination';
440
+ slug: string;
441
+ lastModified: string | null;
442
+ priority: number;
443
+ changeFrequency: 'weekly' | 'monthly';
444
+ };
445
+ export type PublicSitemapResponse = {
446
+ data: PublicSitemapEntry[];
447
+ total: number;
448
+ };
449
+ export type PublicNavLink = {
450
+ slug: string;
451
+ label: string;
452
+ };
453
+ export type PublicStaticLink = {
454
+ key: string;
455
+ label: string;
456
+ href: string;
457
+ };
458
+ export type PublicSiteNavigationResponse = {
459
+ primaryDestinations: PublicNavLink[];
460
+ featuredTourLinks: PublicNavLink[];
461
+ staticLinks: PublicStaticLink[];
462
+ footerLinks: PublicStaticLink[];
463
+ };
464
+ export type PublicTourCategoryListItem = {
465
+ id: number;
466
+ slug: string;
467
+ name: string;
468
+ tourCount: number;
469
+ };
470
+ export type PublicTourCategoryListResponse = {
471
+ data: PublicTourCategoryListItem[];
472
+ nextCursor: string | null;
473
+ total: number;
474
+ };
475
+ export type PublicSearchSuggestion = {
476
+ type: 'tour' | 'destination';
477
+ label: string;
478
+ slug: string;
479
+ path: string;
480
+ };
481
+ export type PublicSearchSuggestionsResponse = {
482
+ data: PublicSearchSuggestion[];
483
+ total: number;
484
+ query: string;
485
+ };
486
+ export type PublicTravelKnowledgeType = 'city' | 'landmark' | 'activity' | 'accommodation_label' | 'theme';
487
+ export type PublicTravelKnowledgeItem = {
488
+ type: PublicTravelKnowledgeType;
489
+ slug: string;
490
+ canonicalName: string;
491
+ publicSafeSummary?: string | null;
492
+ category?: string | null;
493
+ city?: string | null;
494
+ region?: string | null;
495
+ country?: string | null;
496
+ };
497
+ export type PublicTravelKnowledgeResponse = {
498
+ data: PublicTravelKnowledgeItem[];
499
+ meta: {
500
+ type: PublicTravelKnowledgeType;
501
+ count: number;
502
+ truncated: boolean;
503
+ };
504
+ };
505
+ export type PublicTravelStatusState = 'current' | 'review_due_soon' | 'stale' | 'source_unavailable' | 'conflicting_official_sources' | 'retired_source' | 'emergency_correction_active';
506
+ export type PublicTravelStatusReviewerLabel = 'Travel advisory review team' | 'Visa and entry review team' | 'Travel practicalities review team' | 'SURFIRAN operations team' | 'SURFIRAN';
507
+ export type PublicTravelStatusFreshness = {
508
+ reviewedAt: string | null;
509
+ nextReviewBy: string | null;
510
+ reviewedByRole: PublicTravelStatusReviewerLabel;
511
+ };
512
+ export type PublicOfficialSourceCard = {
513
+ sourceKey: string;
514
+ sourceCategory: 'official-travel-advice' | 'official-visa-entry' | 'regulated-practical-constraint';
515
+ sourceName: string;
516
+ issuingOrganization: string;
517
+ jurisdiction: string;
518
+ canonicalUrl: string;
519
+ applicability: string;
520
+ summary: string | null;
521
+ freshness: PublicTravelStatusFreshness;
522
+ };
523
+ export type PublicSurfiranOperationalUpdate = {
524
+ summary: string | null;
525
+ unavailable: boolean;
526
+ freshness: PublicTravelStatusFreshness | null;
527
+ };
528
+ export type PublicTravelStatusCorrectionNotice = {
529
+ message: string;
530
+ correctedAt: string;
531
+ };
532
+ export type PublicTravelStatusHandoff = {
533
+ route: '/inquire';
534
+ message: string;
535
+ };
536
+ export type PublicTravelStatusSection = {
537
+ scope: 'advisory' | 'visa' | 'border' | 'payment' | 'insurance' | 'aviation' | 'communications';
538
+ state: PublicTravelStatusState;
539
+ officialSources: PublicOfficialSourceCard[];
540
+ conflictDisclosure: string | null;
541
+ };
542
+ export type PublicTravelStatusResponse = {
543
+ data: {
544
+ overallState: PublicTravelStatusState;
545
+ sections: PublicTravelStatusSection[];
546
+ surfiranOperationalUpdate: PublicSurfiranOperationalUpdate;
547
+ correctionNotice: PublicTravelStatusCorrectionNotice | null;
548
+ handoff: PublicTravelStatusHandoff;
549
+ freshness: PublicTravelStatusFreshness;
550
+ };
551
+ };
552
+ /** Closed public route vocabulary. Routing is resolved by the rendering layer. */
553
+ export type PublicSiteRouteKey = 'home' | 'tours' | 'tour-categories' | 'destinations' | 'about' | 'contact' | 'inquire' | 'faqs' | 'privacy' | 'terms' | 'responsible-travel' | 'iran-travel-status' | 'iran-visa-information' | 'money-in-iran' | 'travel-insurance' | 'health-safety-support' | 'booking-cancellation-policy' | 'cookie-policy' | 'accessibility-statement';
554
+ export type PublicSiteCta = {
555
+ label: string;
556
+ routeKey: PublicSiteRouteKey;
557
+ };
558
+ export type PublicSiteHomeSectionType = 'feature' | 'tour_highlight' | 'destination_highlight';
559
+ export type PublicSiteHomeSection = {
560
+ type: PublicSiteHomeSectionType;
561
+ heading: string | null;
562
+ summary: string | null;
563
+ };
564
+ export type PublicSiteHomeContent = {
565
+ pageTitle: string;
566
+ heroHeading: string;
567
+ heroIntroduction: string;
568
+ cta: PublicSiteCta | null;
569
+ sections: PublicSiteHomeSection[];
570
+ };
571
+ export type PublicSiteHomeContentResponse = {
572
+ data: PublicSiteHomeContent | null;
573
+ };
574
+ export type PublicSiteNavItem = {
575
+ label: string;
576
+ routeKey: PublicSiteRouteKey;
577
+ };
578
+ export type PublicSiteSocialPlatform = 'facebook' | 'instagram' | 'twitter' | 'linkedin' | 'youtube' | 'tiktok';
579
+ export type PublicSiteSocialLink = {
580
+ platform: PublicSiteSocialPlatform;
581
+ url: string;
582
+ };
583
+ export type PublicSiteChromeContent = {
584
+ navItems: PublicSiteNavItem[];
585
+ cta: PublicSiteCta | null;
586
+ contactEmail: string | null;
587
+ contactPhone: string | null;
588
+ socialLinks: PublicSiteSocialLink[];
589
+ };
590
+ export type PublicSiteChromeContentResponse = {
591
+ data: PublicSiteChromeContent | null;
592
+ };
593
+ /** Closed discriminator over the three plain-text About/Contact section shapes. */
594
+ export type PublicSiteTrustSectionType = 'paragraph_group' | 'titled_item_list' | 'bullet_list';
595
+ export type PublicSiteTrustTitledItem = {
596
+ title: string;
597
+ body: string;
598
+ };
599
+ export type PublicSiteTrustParagraphGroup = {
600
+ type: 'paragraph_group';
601
+ title: string;
602
+ paragraphs: string[];
603
+ };
604
+ export type PublicSiteTrustTitledItemList = {
605
+ type: 'titled_item_list';
606
+ title: string;
607
+ intro: string | null;
608
+ items: PublicSiteTrustTitledItem[];
609
+ };
610
+ export type PublicSiteTrustBulletList = {
611
+ type: 'bullet_list';
612
+ title: string;
613
+ bullets: string[];
614
+ };
615
+ export type PublicSiteTrustSection = PublicSiteTrustParagraphGroup | PublicSiteTrustTitledItemList | PublicSiteTrustBulletList;
616
+ export type PublicSiteTrustPageSeo = {
617
+ title: string | null;
618
+ description: string | null;
619
+ ogTitle: string | null;
620
+ ogDescription: string | null;
621
+ robotsIndex: boolean;
622
+ robotsFollow: boolean;
623
+ };
624
+ /**
625
+ * Published About or Contact page content. Page copy only — the public inquiry
626
+ * form stays entirely application-owned and is never described or configured
627
+ * by this contract.
628
+ */
629
+ export type PublicSiteTrustPageContent = {
630
+ eyebrow: string;
631
+ pageHeading: string;
632
+ pageIntro: string;
633
+ cta: PublicSiteCta | null;
634
+ sections: PublicSiteTrustSection[];
635
+ seo: PublicSiteTrustPageSeo;
636
+ };
637
+ export type PublicSiteTrustPageContentResponse = {
638
+ data: PublicSiteTrustPageContent | null;
639
+ };
640
+ export type PublicApiV1RouteMap = {
641
+ 'GET /homepage': PublicHomepageResponse;
642
+ 'GET /site-navigation': PublicSiteNavigationResponse;
643
+ 'GET /sitemap': PublicSitemapResponse;
644
+ 'GET /search-suggestions': PublicSearchSuggestionsResponse;
645
+ 'GET /tour-categories': PublicTourCategoryListResponse;
646
+ 'GET /tours': PublicTourListResponse;
647
+ 'GET /tours/{slug}': PublicTourDetailResponse;
648
+ 'GET /tours/{slug}/departures': PublicDepartureListResponse;
649
+ 'GET /tours/{slug}/availability': PublicAvailabilityListResponse;
650
+ 'GET /tours/{slug}/reviews': PublicTourReviewListResponse;
651
+ 'GET /destinations': PublicDestinationListResponse;
652
+ 'GET /destinations/{slug}': PublicDestinationDetailResponse;
653
+ 'POST /inquiries': PublicInquiryResponse;
654
+ 'POST /booking-intents': PublicBookingIntentResponse;
655
+ 'POST /brochure-leads': PublicBrochureLeadResponse;
656
+ 'GET /travel-status': PublicTravelStatusResponse;
657
+ 'GET /travel-knowledge': PublicTravelKnowledgeResponse;
658
+ 'GET /site-content/home': PublicSiteHomeContentResponse;
659
+ 'GET /site-content/header': PublicSiteChromeContentResponse;
660
+ 'GET /site-content/footer': PublicSiteChromeContentResponse;
661
+ 'GET /site-content/about': PublicSiteTrustPageContentResponse;
662
+ 'GET /site-content/contact': PublicSiteTrustPageContentResponse;
663
+ 'GET /site-content/faqs': PublicSiteTrustPageContentResponse;
664
+ 'GET /site-content/privacy': PublicSiteTrustPageContentResponse;
665
+ 'GET /site-content/terms': PublicSiteTrustPageContentResponse;
666
+ 'GET /site-content/responsible-travel': PublicSiteTrustPageContentResponse;
667
+ 'GET /site-content/iran-travel-status': PublicSiteTrustPageContentResponse;
668
+ 'GET /site-content/iran-visa-information': PublicSiteTrustPageContentResponse;
669
+ 'GET /site-content/money-in-iran': PublicSiteTrustPageContentResponse;
670
+ 'GET /site-content/travel-insurance': PublicSiteTrustPageContentResponse;
671
+ 'GET /site-content/health-safety-support': PublicSiteTrustPageContentResponse;
672
+ 'GET /site-content/booking-cancellation-policy': PublicSiteTrustPageContentResponse;
673
+ 'GET /site-content/cookie-policy': PublicSiteTrustPageContentResponse;
674
+ 'GET /site-content/accessibility-statement': PublicSiteTrustPageContentResponse;
675
+ };
676
+ export type PublicApiResult<T> = {
677
+ ok: true;
678
+ data: T;
679
+ status: number;
680
+ requestId: string | null;
681
+ } | {
682
+ ok: false;
683
+ error: PublicError;
684
+ status: number;
685
+ requestId: string | null;
686
+ };
687
+ export type ListPublicToursParams = {
688
+ limit?: number;
689
+ cursor?: string;
690
+ /** Itinerary-derived destination slug, matched server-side. */
691
+ destination?: string;
692
+ /** Inclusive lower bound on `durationDays`. */
693
+ minDuration?: number;
694
+ /** Inclusive upper bound on `durationDays`. */
695
+ maxDuration?: number;
696
+ };
697
+ export type ListPublicDeparturesParams = {
698
+ limit?: number;
699
+ cursor?: string;
700
+ };
701
+ export type ListPublicTourQuestionsParams = {
702
+ limit?: number;
703
+ };
704
+ export type ListPublicTourExperiencesParams = {
705
+ limit?: number;
706
+ };
707
+ export type ListPublicTravellerStoriesParams = {
708
+ limit?: number;
709
+ };
710
+ export type ListPublicTourAccommodationsParams = {
711
+ limit?: number;
712
+ };
713
+ export type PublicTourReviewsLimit = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
714
+ export type ListPublicTourReviewsParams = {
715
+ /** Maximum review cards to return. Defaults to 3 when omitted; accepted range is 1 through 12. */
716
+ limit?: PublicTourReviewsLimit;
717
+ };
718
+ export type ListPublicTourRouteParams = {
719
+ limit?: number;
720
+ };
721
+ export type ListPublicTourItineraryParams = {
722
+ limit?: number;
723
+ };
724
+ export type ListPublicDestinationsParams = {
725
+ limit?: number;
726
+ cursor?: string;
727
+ };
728
+ export type GetPublicSitemapParams = {
729
+ type?: 'tour' | 'destination';
730
+ };
731
+ export type GetPublicSearchSuggestionsParams = {
732
+ q: string;
733
+ limit?: number;
734
+ type?: 'tour' | 'destination';
735
+ };
736
+ export type ListPublicTourCategoriesParams = {
737
+ limit?: number;
738
+ cursor?: string;
739
+ };
740
+ export type ListPublicTourAvailabilityParams = {
741
+ limit?: number;
742
+ cursor?: string;
743
+ };
744
+ export type GetPublicTravelKnowledgeParams = {
745
+ type: PublicTravelKnowledgeType;
746
+ q: string;
747
+ limit?: number;
748
+ };
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // TypeScript interfaces aligned with server/openapi/public-api-v1.yaml v0.8.0
2
+ // Zero runtime code. Types only.
3
+ export {};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@surfiran/public-api-client",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript types and client for SURFIRAN CRM Public API V1",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./generated": {
14
+ "types": "./dist/generated/public-api-v1.d.ts",
15
+ "import": "./dist/generated/public-api-v1.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "generate": "node ../../scripts/generate-public-api-v1-types.cjs",
24
+ "build": "rm -rf dist && tsc -p tsconfig.json",
25
+ "prepack": "npm run typecheck && npm test && npm run build",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "verify:generated": "npm run generate && git diff --exit-code -- src/generated/public-api-v1.ts"
29
+ },
30
+ "dependencies": {},
31
+ "devDependencies": {
32
+ "typescript": "^5.2.2",
33
+ "vitest": "^4.1.8"
34
+ }
35
+ }