@voyant-travel/framework 0.1.0 → 0.2.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/LICENSE +201 -0
- package/dist/composition.d.ts +150 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +404 -0
- package/dist/create-app.d.ts +52 -0
- package/dist/create-app.d.ts.map +1 -0
- package/dist/create-app.js +59 -0
- package/dist/discover-modules.d.ts +80 -0
- package/dist/discover-modules.d.ts.map +1 -0
- package/dist/discover-modules.js +104 -0
- package/dist/discover-modules.test.d.ts +2 -0
- package/dist/discover-modules.test.d.ts.map +1 -0
- package/dist/discover-modules.test.js +76 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/manifest.d.ts +28 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +56 -0
- package/dist/runtime-packages.generated.d.ts +3 -0
- package/dist/runtime-packages.generated.d.ts.map +1 -0
- package/dist/runtime-packages.generated.js +22 -0
- package/package.json +37 -40
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standard Voyant runtime composition registry — the package-owned half of
|
|
3
|
+
* `OPERATOR_RUNTIME_MANIFEST`'s factories.
|
|
4
|
+
*
|
|
5
|
+
* agent-quality: file-size exception -- this is the framework's single
|
|
6
|
+
* composition source of truth (the FrameworkProviders contract + one factory
|
|
7
|
+
* entry per standard module/extension). Keeping the provider interface + the
|
|
8
|
+
* registry in one file is intentional; the length scales with the standard
|
|
9
|
+
* module count, not with logic complexity — mirroring the deployment's
|
|
10
|
+
* `composition.ts` rationale.
|
|
11
|
+
*
|
|
12
|
+
* Workstream B of the consolidated-deployments RFC relocates the deployment's
|
|
13
|
+
* `operatorComposition` registry here, one classified-as-standard family group
|
|
14
|
+
* at a time (see `operator-registry-classification.md`). A deployment spreads
|
|
15
|
+
* this into its own registry:
|
|
16
|
+
*
|
|
17
|
+
* export const operatorComposition: CompositionRegistry<OperatorCapabilities> = {
|
|
18
|
+
* modules: { ...frameworkComposition.modules, ...deploymentLocal },
|
|
19
|
+
* extensions: { ...frameworkComposition.extensions, ...deploymentLocal },
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* so `composeFromManifest` always sees one complete registry while the
|
|
23
|
+
* deployment object shrinks PR by PR.
|
|
24
|
+
*
|
|
25
|
+
* The factories read only the framework-owned {@link FrameworkProviders} surface
|
|
26
|
+
* off `ctx.capabilities` — never a deployment file, never a baked provider
|
|
27
|
+
* choice (Netopia et al. stay injected). Because `OperatorCapabilities extends
|
|
28
|
+
* FrameworkProviders` and factory params are contravariant, a
|
|
29
|
+
* `ModuleFactory<FrameworkProviders>` slots cleanly into the deployment's wider
|
|
30
|
+
* `CompositionRegistry<OperatorCapabilities>`.
|
|
31
|
+
*
|
|
32
|
+
* Provider field types are anchored to the package option types they feed
|
|
33
|
+
* (`NonNullable<XOptions["field"]>`) or to a package service (`typeof
|
|
34
|
+
* relationshipsService`), so they can't drift from the contracts the factories
|
|
35
|
+
* pass them into.
|
|
36
|
+
*/
|
|
37
|
+
import { actionLedgerHonoModule } from "@voyant-travel/action-ledger";
|
|
38
|
+
import { bookingsSupplierExtension, createBookingsHonoModule, } from "@voyant-travel/bookings";
|
|
39
|
+
import { bookingsExtrasRoutes } from "@voyant-travel/bookings/extras";
|
|
40
|
+
import { createBookingRequirementsHonoModule, } from "@voyant-travel/bookings/requirements";
|
|
41
|
+
import { createCatalogSearchHonoModule, executeSemanticSearch, } from "@voyant-travel/catalog";
|
|
42
|
+
import { createCommerceHonoModules, createCommerceStorefrontOfferResolvers, } from "@voyant-travel/commerce";
|
|
43
|
+
import { distributionBookingExtension, distributionHonoModule, externalRefsHonoModule, suppliersHonoModule, } from "@voyant-travel/distribution";
|
|
44
|
+
import { bookingsCreateExtension, createBookingTaxHonoExtension, createFinanceHonoModule, } from "@voyant-travel/finance";
|
|
45
|
+
import { createPublicDocumentDeliveryHonoModule } from "@voyant-travel/hono";
|
|
46
|
+
import { identityHonoModule } from "@voyant-travel/identity";
|
|
47
|
+
import { inventoryBookingExtension, inventoryHonoModule } from "@voyant-travel/inventory";
|
|
48
|
+
import { inventoryAuthoringExtension } from "@voyant-travel/inventory/authoring/extension";
|
|
49
|
+
import { inventoryExtrasRoutes } from "@voyant-travel/inventory/extras";
|
|
50
|
+
import { CONTRACT_DOCUMENT_ROUTE_PATHS, createLegalHonoModule, } from "@voyant-travel/legal";
|
|
51
|
+
import { createDefaultBookingDocumentAttachment, createNotificationService, createNotificationsHonoModule, notificationsService, } from "@voyant-travel/notifications";
|
|
52
|
+
import { operationsHonoModule } from "@voyant-travel/operations";
|
|
53
|
+
import { resolveBookingTaxSettings, updateBookingTaxSettings, } from "@voyant-travel/operator-settings";
|
|
54
|
+
import { createOperatorSettingsHonoModule } from "@voyant-travel/operator-settings/hono-module";
|
|
55
|
+
import { createQuotesHonoModule, quotesBookingExtension } from "@voyant-travel/quotes";
|
|
56
|
+
import { createRelationshipsHonoModule, } from "@voyant-travel/relationships";
|
|
57
|
+
import { createStorefrontHonoModule, } from "@voyant-travel/storefront";
|
|
58
|
+
import { createCustomerPortalHonoModule } from "@voyant-travel/storefront/customer-portal";
|
|
59
|
+
import { createStorefrontVerificationHonoModule, } from "@voyant-travel/storefront/verification";
|
|
60
|
+
import { createTripsHonoModule } from "@voyant-travel/trips";
|
|
61
|
+
import { Hono } from "hono";
|
|
62
|
+
/**
|
|
63
|
+
* Combined "extras" surface — inventory + bookings package extras routes mounted
|
|
64
|
+
* on one module. Pure composition of package route sets (no providers); the
|
|
65
|
+
* deployment used to build this inline.
|
|
66
|
+
*/
|
|
67
|
+
const extrasHonoModule = {
|
|
68
|
+
module: { name: "extras" },
|
|
69
|
+
routes: new Hono().route("/", inventoryExtrasRoutes).route("/", bookingsExtrasRoutes),
|
|
70
|
+
};
|
|
71
|
+
// Stable absolute route matchers for the lazy `operator/*` standard families
|
|
72
|
+
// (Tier 4). The framework owns the URL contract; the deployment injects only
|
|
73
|
+
// the `load` closure that wires its providers into the route bundle.
|
|
74
|
+
const CATALOG_BOOKING_ROUTE_PATHS = [
|
|
75
|
+
"/v1/admin/catalog/quote",
|
|
76
|
+
"/v1/admin/catalog/book",
|
|
77
|
+
"/v1/admin/catalog/drafts/:id",
|
|
78
|
+
"/v1/admin/catalog/holds/place",
|
|
79
|
+
"/v1/admin/catalog/holds/release",
|
|
80
|
+
"/v1/admin/catalog/slots",
|
|
81
|
+
"/v1/admin/catalog/orders",
|
|
82
|
+
"/v1/admin/catalog/orders/:id",
|
|
83
|
+
"/v1/admin/catalog/orders/:id/cancel",
|
|
84
|
+
"/v1/admin/bookings/:id/catalog-snapshot",
|
|
85
|
+
"/v1/public/catalog/quote",
|
|
86
|
+
"/v1/public/catalog/book",
|
|
87
|
+
"/v1/public/catalog/drafts/:id",
|
|
88
|
+
"/v1/public/catalog/holds/place",
|
|
89
|
+
"/v1/public/catalog/holds/release",
|
|
90
|
+
"/v1/public/catalog/slots",
|
|
91
|
+
];
|
|
92
|
+
const CATALOG_CONTENT_ROUTE_PATHS = [
|
|
93
|
+
"/v1/admin/products/:id/content",
|
|
94
|
+
"/v1/public/products/:id/content",
|
|
95
|
+
"/v1/admin/cruises/:id/content",
|
|
96
|
+
"/v1/public/cruises/:id/content",
|
|
97
|
+
"/v1/admin/accommodations/:id/content",
|
|
98
|
+
"/v1/public/accommodations/:id/content",
|
|
99
|
+
];
|
|
100
|
+
const MEDIA_ROUTE_PATHS = [
|
|
101
|
+
"/v1/admin/products/:id/brochure/generate",
|
|
102
|
+
"/v1/uploads",
|
|
103
|
+
"/v1/admin/uploads",
|
|
104
|
+
"/v1/uploads/video",
|
|
105
|
+
"/v1/admin/uploads/video",
|
|
106
|
+
"/v1/media/*",
|
|
107
|
+
"/v1/admin/media/*",
|
|
108
|
+
];
|
|
109
|
+
const PAYMENT_LINK_ROUTE_PATHS = [
|
|
110
|
+
"/v1/public/payment-link-config",
|
|
111
|
+
"/v1/public/payment-link/:sessionId/retry",
|
|
112
|
+
"/v1/public/payment-link/resolve",
|
|
113
|
+
"/v1/public/payment-link/:sessionId/start-card",
|
|
114
|
+
"/v1/public/payment-link/:sessionId/trip-summary",
|
|
115
|
+
"/v1/public/payment-link/:sessionId/booking-summary",
|
|
116
|
+
"/v1/public/bookings/:bookingId/checkout-status",
|
|
117
|
+
];
|
|
118
|
+
function optionalDateTime(value) {
|
|
119
|
+
if (!value)
|
|
120
|
+
return null;
|
|
121
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
122
|
+
}
|
|
123
|
+
function toCheckoutNotificationDelivery(delivery) {
|
|
124
|
+
if (!delivery)
|
|
125
|
+
return null;
|
|
126
|
+
return {
|
|
127
|
+
id: delivery.id,
|
|
128
|
+
templateSlug: delivery.templateSlug,
|
|
129
|
+
channel: delivery.channel,
|
|
130
|
+
provider: delivery.provider,
|
|
131
|
+
status: delivery.status,
|
|
132
|
+
toAddress: delivery.toAddress,
|
|
133
|
+
subject: delivery.subject,
|
|
134
|
+
sentAt: optionalDateTime(delivery.sentAt),
|
|
135
|
+
failedAt: optionalDateTime(delivery.failedAt),
|
|
136
|
+
errorMessage: delivery.errorMessage,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function toCheckoutReminderRun(run) {
|
|
140
|
+
return {
|
|
141
|
+
id: run.id,
|
|
142
|
+
reminderRuleId: run.reminderRuleId,
|
|
143
|
+
reminderRuleSlug: run.reminderRule.slug,
|
|
144
|
+
reminderRuleName: run.reminderRule.name,
|
|
145
|
+
targetType: run.targetType,
|
|
146
|
+
targetId: run.targetId,
|
|
147
|
+
bookingId: run.links.bookingId,
|
|
148
|
+
paymentSessionId: run.links.paymentSessionId,
|
|
149
|
+
notificationDeliveryId: run.links.notificationDeliveryId,
|
|
150
|
+
status: run.status,
|
|
151
|
+
deliveryStatus: run.delivery?.status ?? null,
|
|
152
|
+
channel: run.delivery?.channel ?? run.reminderRule.channel,
|
|
153
|
+
provider: run.delivery?.provider ?? run.reminderRule.provider ?? null,
|
|
154
|
+
recipient: run.recipient,
|
|
155
|
+
scheduledFor: run.scheduledFor,
|
|
156
|
+
processedAt: run.processedAt,
|
|
157
|
+
errorMessage: run.errorMessage,
|
|
158
|
+
relativeDaysFromDueDate: null,
|
|
159
|
+
createdAt: run.createdAt,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Standard module/extension factories owned by the framework. Keyed by the same
|
|
164
|
+
* manifest specifiers as `FRAMEWORK_RUNTIME_MANIFEST`; a deployment spreads this
|
|
165
|
+
* into its registry (see file header).
|
|
166
|
+
*
|
|
167
|
+
* - Tier 1: the pure singleton modules — no providers, no deployment imports.
|
|
168
|
+
* - Tier 2: capability-shaped `@voyant-travel/*` modules — read injected
|
|
169
|
+
* providers off `ctx.capabilities`.
|
|
170
|
+
*/
|
|
171
|
+
export const frameworkComposition = {
|
|
172
|
+
modules: {
|
|
173
|
+
// Tier 1 — pure singletons.
|
|
174
|
+
"@voyant-travel/action-ledger": () => actionLedgerHonoModule,
|
|
175
|
+
"@voyant-travel/relationships": ({ capabilities }) => createRelationshipsHonoModule({ customFields: capabilities.customFields }),
|
|
176
|
+
"@voyant-travel/quotes": () => createQuotesHonoModule(),
|
|
177
|
+
"@voyant-travel/operations": () => operationsHonoModule,
|
|
178
|
+
"@voyant-travel/identity": () => identityHonoModule,
|
|
179
|
+
"@voyant-travel/distribution": () => [
|
|
180
|
+
externalRefsHonoModule,
|
|
181
|
+
distributionHonoModule,
|
|
182
|
+
suppliersHonoModule,
|
|
183
|
+
],
|
|
184
|
+
"@voyant-travel/commerce": () => createCommerceHonoModules(),
|
|
185
|
+
"@voyant-travel/inventory": () => inventoryHonoModule,
|
|
186
|
+
"@voyant-travel/inventory/extras": () => extrasHonoModule,
|
|
187
|
+
"@voyant-travel/bookings/requirements": ({ capabilities }) => createBookingRequirementsHonoModule({
|
|
188
|
+
publicRoutes: {
|
|
189
|
+
resolveProductSnapshot: capabilities.resolveBookingRequirementsProductSnapshot,
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
// Tier 2 — capability-shaped modules (providers injected via ctx).
|
|
193
|
+
"@voyant-travel/catalog": ({ capabilities }) => createCatalogSearchHonoModule({
|
|
194
|
+
resolveRuntime: capabilities.resolveCatalogRuntime,
|
|
195
|
+
executeSearch: ({ adapter, embeddings, slice, request }) => executeSemanticSearch({
|
|
196
|
+
adapter,
|
|
197
|
+
embeddings: embeddings,
|
|
198
|
+
slice,
|
|
199
|
+
request,
|
|
200
|
+
}),
|
|
201
|
+
}),
|
|
202
|
+
"@voyant-travel/storefront": ({ capabilities }) => createStorefrontHonoModule({
|
|
203
|
+
offers: createCommerceStorefrontOfferResolvers(),
|
|
204
|
+
// Async booking-bootstrap intents (queued write pipeline, RFC
|
|
205
|
+
// voyant#1687 §3.2) — the handler runs on the app bus with
|
|
206
|
+
// outbox-grade retries; the */2min cron sweeps stale intents.
|
|
207
|
+
bookingIntents: { resolveDb: capabilities.resolveDb },
|
|
208
|
+
intake: { persistence: capabilities.storefrontIntakePersistence },
|
|
209
|
+
}),
|
|
210
|
+
"@voyant-travel/finance": ({ capabilities }) => createFinanceHonoModule({
|
|
211
|
+
resolveDocumentDownloadUrl: (bindings, storageKey) => capabilities.resolveDocumentDownloadUrl(bindings, storageKey),
|
|
212
|
+
resolveInvoiceExchangeRateResolver: capabilities.createInvoiceExchangeRateResolver,
|
|
213
|
+
resolveInvoiceSettlementPollers: capabilities.createInvoiceSettlementPollers,
|
|
214
|
+
invoiceDueDateResolver: ({ issueDate, dueDate, bookingPaymentSchedule }) => bookingPaymentSchedule && dueDate < issueDate ? issueDate : dueDate,
|
|
215
|
+
resolveNotificationDispatcher: (bindings) => {
|
|
216
|
+
const providers = capabilities.resolveNotificationProviders(bindings);
|
|
217
|
+
if (providers.length === 0)
|
|
218
|
+
return null;
|
|
219
|
+
const dispatcher = createNotificationService(providers);
|
|
220
|
+
return {
|
|
221
|
+
sendInvoiceNotification: async (db, invoiceId, input) => toCheckoutNotificationDelivery(await notificationsService.sendInvoiceNotification(db, dispatcher, invoiceId, input)),
|
|
222
|
+
sendPaymentSessionNotification: async (db, paymentSessionId, input) => toCheckoutNotificationDelivery(await notificationsService.sendPaymentSessionNotification(db, dispatcher, paymentSessionId, input)),
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
resolvePaymentStarters: () => ({
|
|
226
|
+
netopia: capabilities.netopiaCheckoutStarter,
|
|
227
|
+
}),
|
|
228
|
+
resolveBankTransferDetails: capabilities.resolveBankTransferDetails,
|
|
229
|
+
resolvePublicCheckoutBaseUrl: capabilities.resolvePublicCheckoutBaseUrl,
|
|
230
|
+
listBookingReminderRuns: async (db, bookingId, query) => {
|
|
231
|
+
const result = await notificationsService.listReminderRuns(db, {
|
|
232
|
+
bookingId,
|
|
233
|
+
status: query.status,
|
|
234
|
+
limit: query.limit,
|
|
235
|
+
offset: query.offset,
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
data: result.data.map(toCheckoutReminderRun),
|
|
239
|
+
total: result.total,
|
|
240
|
+
limit: result.limit,
|
|
241
|
+
offset: result.offset,
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
"@voyant-travel/bookings": ({ capabilities }) => createBookingsHonoModule({
|
|
246
|
+
resolveTravelSnapshot: (db, personId, { kms }) => capabilities.relationshipsService.loadPersonTravelSnapshot(db, personId, { kms }),
|
|
247
|
+
resolveBillingPerson: async (db, contact, ctx) => {
|
|
248
|
+
const person = await capabilities.relationshipsService.upsertPersonFromContact(db, contact, {
|
|
249
|
+
source: ctx.source,
|
|
250
|
+
sourceRef: ctx.sourceRef,
|
|
251
|
+
});
|
|
252
|
+
return person?.id ?? null;
|
|
253
|
+
},
|
|
254
|
+
resolveTravelerPerson: async (db, contact, ctx) => {
|
|
255
|
+
const person = await capabilities.relationshipsService.upsertPersonFromContact(db, contact, {
|
|
256
|
+
source: ctx.source,
|
|
257
|
+
sourceRef: ctx.sourceRef,
|
|
258
|
+
requireContactPoint: true,
|
|
259
|
+
});
|
|
260
|
+
return person?.id ?? null;
|
|
261
|
+
},
|
|
262
|
+
resolveBillingPersonById: async (db, personId) => (await capabilities.relationshipsService.getPersonById(db, personId)) != null,
|
|
263
|
+
resolveBillingOrganizationById: async (db, organizationId) => (await capabilities.relationshipsService.getOrganizationById(db, organizationId)) != null,
|
|
264
|
+
closePaymentSchedulesForBooking: capabilities.closePaymentSchedulesForBooking,
|
|
265
|
+
customFields: capabilities.customFields,
|
|
266
|
+
}),
|
|
267
|
+
"@voyant-travel/public-document-delivery": ({ capabilities }) => createPublicDocumentDeliveryHonoModule({
|
|
268
|
+
// Same storage backend as legal documents; the unknown-bindings
|
|
269
|
+
// adapter keeps the provider contract uniform (the narrow-bindings
|
|
270
|
+
// `createDocumentStorage` is retired in the deployment).
|
|
271
|
+
resolveStorage: capabilities.createOperatorDocumentStorage,
|
|
272
|
+
}),
|
|
273
|
+
"@voyant-travel/notifications": ({ capabilities }) => createNotificationsHonoModule({
|
|
274
|
+
resolveProviders: capabilities.resolveNotificationProviders,
|
|
275
|
+
resolvePublicCheckoutBaseUrl: capabilities.resolvePublicCheckoutBaseUrl,
|
|
276
|
+
resolveDocumentAttachmentResolver: (bindings) => async (document) => {
|
|
277
|
+
if (document.storageKey) {
|
|
278
|
+
const contentBase64 = await capabilities.readDocumentContentBase64(bindings, document.storageKey);
|
|
279
|
+
if (contentBase64) {
|
|
280
|
+
return {
|
|
281
|
+
filename: document.name,
|
|
282
|
+
contentBase64,
|
|
283
|
+
contentType: document.mimeType ?? undefined,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const path = await capabilities.resolveDocumentDownloadUrl(bindings, document.storageKey);
|
|
287
|
+
if (path) {
|
|
288
|
+
return {
|
|
289
|
+
filename: document.name,
|
|
290
|
+
path,
|
|
291
|
+
contentType: document.mimeType ?? undefined,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return createDefaultBookingDocumentAttachment(document);
|
|
296
|
+
},
|
|
297
|
+
resolveDb: capabilities.resolveDb,
|
|
298
|
+
autoConfirmAndDispatch: { enabled: true, templateSlug: "booking-confirmation" },
|
|
299
|
+
}),
|
|
300
|
+
"@voyant-travel/legal": ({ capabilities }) => createLegalHonoModule({
|
|
301
|
+
resolveDb: capabilities.resolveDb,
|
|
302
|
+
resolveDocumentDownloadUrl: (bindings, storageKey) => capabilities.resolveDocumentDownloadUrl(bindings, storageKey),
|
|
303
|
+
resolveDocumentStorage: capabilities.createOperatorDocumentStorage,
|
|
304
|
+
resolveDocumentGenerator: capabilities.resolveContractDocumentGenerator,
|
|
305
|
+
resolveBookingPiiService: capabilities.createBookingPiiService,
|
|
306
|
+
autoGenerateContractOnConfirmed: capabilities.autoGenerateContractOnConfirmed,
|
|
307
|
+
}),
|
|
308
|
+
"@voyant-travel/storefront/customer-portal": ({ capabilities }) => createCustomerPortalHonoModule({
|
|
309
|
+
resolveDocumentDownloadUrl: (bindings, storageKey) => capabilities.resolveDocumentDownloadUrl(bindings, storageKey),
|
|
310
|
+
}),
|
|
311
|
+
"@voyant-travel/storefront/verification": ({ capabilities }) => createStorefrontVerificationHonoModule({
|
|
312
|
+
resolveProviders: capabilities.resolveNotificationProviders,
|
|
313
|
+
email: { subject: "Your verification code" },
|
|
314
|
+
}),
|
|
315
|
+
"@voyant-travel/trips": ({ capabilities }) => createTripsHonoModule({
|
|
316
|
+
...capabilities.createTripsRoutesOptions(),
|
|
317
|
+
publicRoutes: true,
|
|
318
|
+
}),
|
|
319
|
+
// Standard settings module — schema-owning + lazy absolute-path routes, no
|
|
320
|
+
// providers (reads its own tables). Stage 2 of the operator-settings
|
|
321
|
+
// extraction; previously a deployment-local module.
|
|
322
|
+
"@voyant-travel/operator-settings": () => createOperatorSettingsHonoModule(),
|
|
323
|
+
// Tier 4 — lazy `operator/*` standard families. The framework owns the
|
|
324
|
+
// manifest entry + path matchers; the deployment injects the `load` closure
|
|
325
|
+
// that wires its providers into the package-owned route bundle.
|
|
326
|
+
"@voyant-travel/flights": ({ capabilities }) => ({
|
|
327
|
+
module: { name: "flights" },
|
|
328
|
+
lazyAdminRoutes: capabilities.loadFlightAdminRoutes,
|
|
329
|
+
}),
|
|
330
|
+
"operator/mcp": ({ capabilities }) => ({
|
|
331
|
+
module: { name: "mcp" },
|
|
332
|
+
lazyAdminRoutes: capabilities.loadMcpAdminRoutes,
|
|
333
|
+
}),
|
|
334
|
+
"operator/catalog-booking": ({ capabilities }) => ({
|
|
335
|
+
module: { name: "catalog-booking" },
|
|
336
|
+
lazyRoutes: {
|
|
337
|
+
paths: CATALOG_BOOKING_ROUTE_PATHS,
|
|
338
|
+
load: capabilities.loadCatalogBookingRoutes,
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
341
|
+
"operator/catalog-content": ({ capabilities }) => ({
|
|
342
|
+
module: { name: "catalog-content" },
|
|
343
|
+
lazyRoutes: {
|
|
344
|
+
paths: CATALOG_CONTENT_ROUTE_PATHS,
|
|
345
|
+
load: capabilities.loadCatalogContentRoutes,
|
|
346
|
+
},
|
|
347
|
+
}),
|
|
348
|
+
"operator/media": ({ capabilities }) => ({
|
|
349
|
+
module: { name: "media" },
|
|
350
|
+
lazyRoutes: { paths: MEDIA_ROUTE_PATHS, load: capabilities.loadMediaRoutes },
|
|
351
|
+
}),
|
|
352
|
+
"operator/payment-link": ({ capabilities }) => ({
|
|
353
|
+
module: { name: "payment-link" },
|
|
354
|
+
lazyRoutes: { paths: PAYMENT_LINK_ROUTE_PATHS, load: capabilities.loadPaymentLinkRoutes },
|
|
355
|
+
}),
|
|
356
|
+
"operator/contract-document": ({ capabilities }) => ({
|
|
357
|
+
module: { name: "contract-document" },
|
|
358
|
+
lazyRoutes: {
|
|
359
|
+
paths: CONTRACT_DOCUMENT_ROUTE_PATHS,
|
|
360
|
+
load: capabilities.loadContractDocumentRoutes,
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
},
|
|
364
|
+
extensions: {
|
|
365
|
+
// Tier 3 — pure singleton extensions (no providers).
|
|
366
|
+
"@voyant-travel/bookings/booking-supplier-extension": () => bookingsSupplierExtension,
|
|
367
|
+
"@voyant-travel/finance/bookings-create-extension": () => bookingsCreateExtension,
|
|
368
|
+
"@voyant-travel/inventory/booking-extension": () => inventoryBookingExtension,
|
|
369
|
+
"@voyant-travel/inventory/authoring/extension": () => inventoryAuthoringExtension,
|
|
370
|
+
"@voyant-travel/quotes/booking-extension": () => quotesBookingExtension,
|
|
371
|
+
"@voyant-travel/distribution": () => distributionBookingExtension,
|
|
372
|
+
// Injection-shaped extensions — deployment-specific builders/readers via ctx.
|
|
373
|
+
"@voyant-travel/distribution/channel-push-extension": ({ capabilities }) => capabilities.createChannelPushExtension(),
|
|
374
|
+
// Booking-tax settings are read straight from the standard
|
|
375
|
+
// @voyant-travel/operator-settings package — no per-deployment injection.
|
|
376
|
+
"@voyant-travel/finance/booking-tax-extension": () => createBookingTaxHonoExtension({ resolveBookingTaxSettings, updateBookingTaxSettings }),
|
|
377
|
+
// Tier 4 — lazy `operator/*` standard extensions. The framework owns the
|
|
378
|
+
// `extension` metadata + publicPath; the deployment injects builders/loaders.
|
|
379
|
+
"operator/booking-schedule-extension": ({ capabilities }) => capabilities.createBookingScheduleExtension(),
|
|
380
|
+
"operator/quote-version-snapshot-extension": ({ capabilities }) => capabilities.createQuoteVersionSnapshotExtension(),
|
|
381
|
+
"operator/booking-maintenance-extension": ({ capabilities }) => ({
|
|
382
|
+
extension: { name: "booking-maintenance", module: "bookings" },
|
|
383
|
+
lazyAdminRoutes: capabilities.loadBookingMaintenanceRoutes,
|
|
384
|
+
}),
|
|
385
|
+
"operator/action-ledger-health-extension": ({ capabilities }) => ({
|
|
386
|
+
extension: { name: "action-ledger-health", module: "action-ledger" },
|
|
387
|
+
lazyAdminRoutes: capabilities.loadActionLedgerHealthRoutes,
|
|
388
|
+
}),
|
|
389
|
+
"operator/proposal-extension": ({ capabilities }) => ({
|
|
390
|
+
extension: { name: "proposal", module: "quote-versions" },
|
|
391
|
+
publicPath: "proposals",
|
|
392
|
+
lazyAdminRoutes: capabilities.loadProposalAdminRoutes,
|
|
393
|
+
lazyPublicRoutes: capabilities.loadProposalPublicRoutes,
|
|
394
|
+
}),
|
|
395
|
+
"operator/catalog-offers-extension": ({ capabilities }) => ({
|
|
396
|
+
extension: { name: "catalog-offers", module: "catalog" },
|
|
397
|
+
lazyAdminRoutes: capabilities.loadCatalogOffersRoutes,
|
|
398
|
+
}),
|
|
399
|
+
"operator/catalog-checkout-extension": ({ capabilities }) => ({
|
|
400
|
+
extension: { name: "catalog-checkout", module: "catalog" },
|
|
401
|
+
lazyPublicRoutes: capabilities.loadCatalogCheckoutRoutes,
|
|
402
|
+
}),
|
|
403
|
+
},
|
|
404
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createVoyantApp` — the framework's config-driven front door (Workstream B
|
|
3
|
+
* convergence).
|
|
4
|
+
*
|
|
5
|
+
* A standard deployment no longer hand-assembles a manifest + registry. It calls
|
|
6
|
+
* this with its injected {@link FrameworkProviders} and any deployment-local
|
|
7
|
+
* module/extension additions; the framework merges them onto the standard set
|
|
8
|
+
* (`FRAMEWORK_RUNTIME_MANIFEST` + `frameworkComposition`) and delegates to the
|
|
9
|
+
* lower-level `createApp` in `@voyant-travel/hono`.
|
|
10
|
+
*
|
|
11
|
+
* export const app = createVoyantApp<CloudflareBindings, OperatorProviders>({
|
|
12
|
+
* providers: buildOperatorProviders(),
|
|
13
|
+
* modules: deploymentLocalModules, // e.g. invitations, operator-settings
|
|
14
|
+
* db, workflows, outbox, publicPaths, // unchanged VoyantApp config
|
|
15
|
+
* })
|
|
16
|
+
*
|
|
17
|
+
* This is NOT the rejected "assembly kit": providers stay injected (never baked),
|
|
18
|
+
* and the assembly is the framework-owned standard set — not a bottom-up file dump.
|
|
19
|
+
*
|
|
20
|
+
* Scope (D.1): the standard profile is FIXED. This always mounts the full
|
|
21
|
+
* `FRAMEWORK_RUNTIME_MANIFEST` and only APPENDS the deployment's local
|
|
22
|
+
* `modules`/`extensions`; it does not consume `voyant.config` to remove standard
|
|
23
|
+
* modules. Subsetting the standard set is a later workstream — see `manifest.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { type CreateAppConfig, type VoyantBindings } from "@voyant-travel/hono";
|
|
26
|
+
import type { ExtensionFactory, ModuleFactory } from "@voyant-travel/hono/composition";
|
|
27
|
+
import { type FrameworkProviders } from "./composition.js";
|
|
28
|
+
/**
|
|
29
|
+
* Config for {@link createVoyantApp}: the injected providers + deployment-local
|
|
30
|
+
* additions, plus everything else `createApp` takes (db, auth, workflows,
|
|
31
|
+
* outbox, publicPaths, …) — minus the `manifest`/`registry`/`capabilities` the
|
|
32
|
+
* framework now assembles for you.
|
|
33
|
+
*/
|
|
34
|
+
export interface CreateVoyantAppConfig<TBindings extends VoyantBindings, TProviders extends FrameworkProviders> extends Omit<CreateAppConfig<TBindings, TProviders>, "manifest" | "registry" | "capabilities"> {
|
|
35
|
+
/** The deployment-injected provider surface (a `FrameworkProviders` superset). */
|
|
36
|
+
providers: TProviders;
|
|
37
|
+
/** Deployment-local module factories, appended after the standard set. */
|
|
38
|
+
modules?: Record<string, ModuleFactory<TProviders>>;
|
|
39
|
+
/** Deployment-local extension factories, appended after the standard set. */
|
|
40
|
+
extensions?: Record<string, ExtensionFactory<TProviders>>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Assemble the standard manifest + registry (framework-owned) with the
|
|
44
|
+
* deployment's local additions, then build the app via `@voyant-travel/hono`'s
|
|
45
|
+
* `createApp`. Standard families keep their `FRAMEWORK_RUNTIME_MANIFEST` order;
|
|
46
|
+
* local additions mount after them.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createVoyantApp<TBindings extends VoyantBindings, TProviders extends FrameworkProviders>(config: CreateVoyantAppConfig<TBindings, TProviders>): import("hono").Hono<{
|
|
49
|
+
Bindings: TBindings;
|
|
50
|
+
Variables: import("@voyant-travel/hono").VoyantVariables;
|
|
51
|
+
}, import("hono/types").BlankSchema, "/"> & import("@voyant-travel/hono/app").VoyantAppExtensions<TBindings>;
|
|
52
|
+
//# sourceMappingURL=create-app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-app.d.ts","sourceRoot":"","sources":["../src/create-app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAa,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAC1F,OAAO,KAAK,EAEV,gBAAgB,EAChB,aAAa,EACd,MAAM,iCAAiC,CAAA;AACxC,OAAO,EAAE,KAAK,kBAAkB,EAAwB,MAAM,kBAAkB,CAAA;AAGhF;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB,CACpC,SAAS,SAAS,cAAc,EAChC,UAAU,SAAS,kBAAkB,CACrC,SAAQ,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,cAAc,CAAC;IAC9F,kFAAkF;IAClF,SAAS,EAAE,UAAU,CAAA;IACrB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC,CAAA;IACnD,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAA;CAC1D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,SAAS,SAAS,cAAc,EAChC,UAAU,SAAS,kBAAkB,EACrC,MAAM,EAAE,qBAAqB,CAAC,SAAS,EAAE,UAAU,CAAC;;;6GA2BrD"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createVoyantApp` — the framework's config-driven front door (Workstream B
|
|
3
|
+
* convergence).
|
|
4
|
+
*
|
|
5
|
+
* A standard deployment no longer hand-assembles a manifest + registry. It calls
|
|
6
|
+
* this with its injected {@link FrameworkProviders} and any deployment-local
|
|
7
|
+
* module/extension additions; the framework merges them onto the standard set
|
|
8
|
+
* (`FRAMEWORK_RUNTIME_MANIFEST` + `frameworkComposition`) and delegates to the
|
|
9
|
+
* lower-level `createApp` in `@voyant-travel/hono`.
|
|
10
|
+
*
|
|
11
|
+
* export const app = createVoyantApp<CloudflareBindings, OperatorProviders>({
|
|
12
|
+
* providers: buildOperatorProviders(),
|
|
13
|
+
* modules: deploymentLocalModules, // e.g. invitations, operator-settings
|
|
14
|
+
* db, workflows, outbox, publicPaths, // unchanged VoyantApp config
|
|
15
|
+
* })
|
|
16
|
+
*
|
|
17
|
+
* This is NOT the rejected "assembly kit": providers stay injected (never baked),
|
|
18
|
+
* and the assembly is the framework-owned standard set — not a bottom-up file dump.
|
|
19
|
+
*
|
|
20
|
+
* Scope (D.1): the standard profile is FIXED. This always mounts the full
|
|
21
|
+
* `FRAMEWORK_RUNTIME_MANIFEST` and only APPENDS the deployment's local
|
|
22
|
+
* `modules`/`extensions`; it does not consume `voyant.config` to remove standard
|
|
23
|
+
* modules. Subsetting the standard set is a later workstream — see `manifest.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { createApp } from "@voyant-travel/hono";
|
|
26
|
+
import { frameworkComposition } from "./composition.js";
|
|
27
|
+
import { FRAMEWORK_RUNTIME_MANIFEST } from "./manifest.js";
|
|
28
|
+
/**
|
|
29
|
+
* Assemble the standard manifest + registry (framework-owned) with the
|
|
30
|
+
* deployment's local additions, then build the app via `@voyant-travel/hono`'s
|
|
31
|
+
* `createApp`. Standard families keep their `FRAMEWORK_RUNTIME_MANIFEST` order;
|
|
32
|
+
* local additions mount after them.
|
|
33
|
+
*/
|
|
34
|
+
export function createVoyantApp(config) {
|
|
35
|
+
const { providers, modules = {}, extensions = {}, ...rest } = config;
|
|
36
|
+
const registry = {
|
|
37
|
+
// The framework factories read only the `FrameworkProviders` slice; a
|
|
38
|
+
// `ModuleFactory<FrameworkProviders>` is assignable to
|
|
39
|
+
// `ModuleFactory<TProviders>` (factory params are contravariant), so the
|
|
40
|
+
// standard set composes against the deployment's wider providers.
|
|
41
|
+
modules: {
|
|
42
|
+
...frameworkComposition.modules,
|
|
43
|
+
...modules,
|
|
44
|
+
},
|
|
45
|
+
extensions: {
|
|
46
|
+
...frameworkComposition.extensions,
|
|
47
|
+
...extensions,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return createApp({
|
|
51
|
+
...rest,
|
|
52
|
+
manifest: {
|
|
53
|
+
modules: [...FRAMEWORK_RUNTIME_MANIFEST.modules, ...Object.keys(modules)],
|
|
54
|
+
extensions: [...FRAMEWORK_RUNTIME_MANIFEST.extensions, ...Object.keys(extensions)],
|
|
55
|
+
},
|
|
56
|
+
registry,
|
|
57
|
+
capabilities: providers,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment-local module + extension discovery (consolidated-deployments RFC —
|
|
3
|
+
* the "20%" extension seams: *custom module* and *custom route on an existing
|
|
4
|
+
* module*, neither requiring a fork).
|
|
5
|
+
*
|
|
6
|
+
* A deployment drops a unit into a conventional directory and it is auto-mounted
|
|
7
|
+
* — no edit to a framework-owned file, no hand-wiring in the deployment's
|
|
8
|
+
* composition:
|
|
9
|
+
*
|
|
10
|
+
* - `src/modules/<name>/index.ts` → a new module (own routes + schema)
|
|
11
|
+
* - `src/extensions/<name>/index.ts` → a {@link HonoExtension} adding routes to
|
|
12
|
+
* an EXISTING module's surface
|
|
13
|
+
*
|
|
14
|
+
* Discovery is **build-time**: the deployment feeds a Vite
|
|
15
|
+
* `import.meta.glob("./modules/*\/index.ts", { eager: true })` (statically
|
|
16
|
+
* compiled to static imports — Workers-safe) into {@link modulesFromGlob} /
|
|
17
|
+
* {@link extensionsFromGlob}, which key each unit by its `<name>` directory
|
|
18
|
+
* segment.
|
|
19
|
+
*
|
|
20
|
+
* // src/api/composition.ts (deployment-owned)
|
|
21
|
+
* const modules = modulesFromGlob<OperatorCapabilities>(
|
|
22
|
+
* import.meta.glob("../modules/*\/index.ts", { eager: true }),
|
|
23
|
+
* )
|
|
24
|
+
* const extensions = extensionsFromGlob<OperatorCapabilities>(
|
|
25
|
+
* import.meta.glob("../extensions/*\/index.ts", { eager: true }),
|
|
26
|
+
* )
|
|
27
|
+
*
|
|
28
|
+
* // src/modules/loyalty/index.ts
|
|
29
|
+
* export default defineDeploymentModule({ module: { name: "loyalty" }, adminRoutes })
|
|
30
|
+
* // src/extensions/booking-notes/index.ts
|
|
31
|
+
* export default defineDeploymentExtension({
|
|
32
|
+
* extension: { module: "bookings" }, adminRoutes, // → /v1/admin/bookings/*
|
|
33
|
+
* })
|
|
34
|
+
*
|
|
35
|
+
* Schema owned by a custom module/extension (`<dir>/<name>/schema.ts`) is picked
|
|
36
|
+
* up by the deployment's drizzle config glob (a *deployment* migration source,
|
|
37
|
+
* applied after the framework bundle by the D.1 collector).
|
|
38
|
+
*/
|
|
39
|
+
import type { ExtensionFactory, ModuleFactory } from "@voyant-travel/hono/composition";
|
|
40
|
+
import type { HonoExtension, HonoModule } from "@voyant-travel/hono/module";
|
|
41
|
+
import type { FrameworkProviders } from "./composition.js";
|
|
42
|
+
/** A deployment module declaration: a ready unit, or a factory that builds one. */
|
|
43
|
+
export type DeploymentModuleDeclaration<TProviders> = HonoModule | HonoModule[] | ModuleFactory<TProviders>;
|
|
44
|
+
/** A deployment extension declaration: a ready unit, or a factory that builds one. */
|
|
45
|
+
export type DeploymentExtensionDeclaration<TProviders> = HonoExtension | ExtensionFactory<TProviders>;
|
|
46
|
+
/**
|
|
47
|
+
* Normalize a deployment-local module declaration into a {@link ModuleFactory}.
|
|
48
|
+
* Accepts a ready {@link HonoModule} (or array) or a factory; returns a factory
|
|
49
|
+
* either way. Use it as the `export default` of a `src/modules/<name>/index.ts`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function defineDeploymentModule<TProviders = FrameworkProviders>(declaration: DeploymentModuleDeclaration<TProviders>): ModuleFactory<TProviders>;
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a deployment-local extension declaration into an
|
|
54
|
+
* {@link ExtensionFactory}. Accepts a ready {@link HonoExtension} or a factory.
|
|
55
|
+
* Use it as the `export default` of a `src/extensions/<name>/index.ts`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function defineDeploymentExtension<TProviders = FrameworkProviders>(declaration: DeploymentExtensionDeclaration<TProviders>): ExtensionFactory<TProviders>;
|
|
58
|
+
/** A Vite `import.meta.glob(..., { eager: true })` result: path → module namespace. */
|
|
59
|
+
export type EagerModuleGlob = Record<string, unknown>;
|
|
60
|
+
/**
|
|
61
|
+
* Build a deployment-local **module** registry from a Vite `import.meta.glob`
|
|
62
|
+
* (eager) of `src/modules/<name>/index.ts` files. Each entry's `default` export
|
|
63
|
+
* is a {@link HonoModule} or {@link ModuleFactory} (wrap with
|
|
64
|
+
* {@link defineDeploymentModule}); the registry key is the `<name>` directory
|
|
65
|
+
* segment, which becomes the module's composition specifier.
|
|
66
|
+
*
|
|
67
|
+
* @throws if a matched file has no default export.
|
|
68
|
+
*/
|
|
69
|
+
export declare function modulesFromGlob<TProviders = FrameworkProviders>(glob: EagerModuleGlob): Record<string, ModuleFactory<TProviders>>;
|
|
70
|
+
/**
|
|
71
|
+
* Build a deployment-local **extension** registry from a Vite `import.meta.glob`
|
|
72
|
+
* (eager) of `src/extensions/<name>/index.ts` files. Each entry's `default`
|
|
73
|
+
* export is a {@link HonoExtension} or {@link ExtensionFactory} (wrap with
|
|
74
|
+
* {@link defineDeploymentExtension}); the registry key is the `<name>` directory
|
|
75
|
+
* segment.
|
|
76
|
+
*
|
|
77
|
+
* @throws if a matched file has no default export.
|
|
78
|
+
*/
|
|
79
|
+
export declare function extensionsFromGlob<TProviders = FrameworkProviders>(glob: EagerModuleGlob): Record<string, ExtensionFactory<TProviders>>;
|
|
80
|
+
//# sourceMappingURL=discover-modules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover-modules.d.ts","sourceRoot":"","sources":["../src/discover-modules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAC3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,mFAAmF;AACnF,MAAM,MAAM,2BAA2B,CAAC,UAAU,IAC9C,UAAU,GACV,UAAU,EAAE,GACZ,aAAa,CAAC,UAAU,CAAC,CAAA;AAE7B,sFAAsF;AACtF,MAAM,MAAM,8BAA8B,CAAC,UAAU,IACjD,aAAa,GACb,gBAAgB,CAAC,UAAU,CAAC,CAAA;AAEhC;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,GAAG,kBAAkB,EACpE,WAAW,EAAE,2BAA2B,CAAC,UAAU,CAAC,GACnD,aAAa,CAAC,UAAU,CAAC,CAE3B;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,GAAG,kBAAkB,EACvE,WAAW,EAAE,8BAA8B,CAAC,UAAU,CAAC,GACtD,gBAAgB,CAAC,UAAU,CAAC,CAE9B;AAED,uFAAuF;AACvF,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAErD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,GAAG,kBAAkB,EAC7D,IAAI,EAAE,eAAe,GACpB,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC,CAI3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,GAAG,kBAAkB,EAChE,IAAI,EAAE,eAAe,GACpB,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAO9C"}
|