@voyant-travel/hono 0.109.1
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/README.md +58 -0
- package/dist/app-workflows.d.ts +31 -0
- package/dist/app-workflows.d.ts.map +1 -0
- package/dist/app-workflows.js +110 -0
- package/dist/app.d.ts +45 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +403 -0
- package/dist/auth/crypto.d.ts +16 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +66 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +3 -0
- package/dist/auth/require-user.d.ts +3 -0
- package/dist/auth/require-user.d.ts.map +1 -0
- package/dist/auth/require-user.js +8 -0
- package/dist/auth/session-jwt.d.ts +7 -0
- package/dist/auth/session-jwt.d.ts.map +1 -0
- package/dist/auth/session-jwt.js +23 -0
- package/dist/composition.d.ts +67 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +46 -0
- package/dist/document-download.d.ts +30 -0
- package/dist/document-download.d.ts.map +1 -0
- package/dist/document-download.js +102 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/lib/db-selector.d.ts +24 -0
- package/dist/lib/db-selector.d.ts.map +1 -0
- package/dist/lib/db-selector.js +28 -0
- package/dist/lib/execution-ctx.d.ts +16 -0
- package/dist/lib/execution-ctx.d.ts.map +1 -0
- package/dist/lib/execution-ctx.js +16 -0
- package/dist/lib/public-paths.d.ts +19 -0
- package/dist/lib/public-paths.d.ts.map +1 -0
- package/dist/lib/public-paths.js +27 -0
- package/dist/lib/request-event-bus.d.ts +21 -0
- package/dist/lib/request-event-bus.d.ts.map +1 -0
- package/dist/lib/request-event-bus.js +43 -0
- package/dist/middleware/auth.d.ts +10 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +280 -0
- package/dist/middleware/body-size.d.ts +7 -0
- package/dist/middleware/body-size.d.ts.map +1 -0
- package/dist/middleware/body-size.js +20 -0
- package/dist/middleware/cors.d.ts +6 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +94 -0
- package/dist/middleware/db.d.ts +43 -0
- package/dist/middleware/db.d.ts.map +1 -0
- package/dist/middleware/db.js +78 -0
- package/dist/middleware/error-boundary.d.ts +5 -0
- package/dist/middleware/error-boundary.d.ts.map +1 -0
- package/dist/middleware/error-boundary.js +76 -0
- package/dist/middleware/idempotency-key.d.ts +97 -0
- package/dist/middleware/idempotency-key.d.ts.map +1 -0
- package/dist/middleware/idempotency-key.js +235 -0
- package/dist/middleware/index.d.ts +14 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +13 -0
- package/dist/middleware/logger.d.ts +5 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +27 -0
- package/dist/middleware/metrics.d.ts +55 -0
- package/dist/middleware/metrics.d.ts.map +1 -0
- package/dist/middleware/metrics.js +94 -0
- package/dist/middleware/public-cache.d.ts +44 -0
- package/dist/middleware/public-cache.d.ts.map +1 -0
- package/dist/middleware/public-cache.js +205 -0
- package/dist/middleware/rate-limit.d.ts +214 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +240 -0
- package/dist/middleware/request-db.d.ts +42 -0
- package/dist/middleware/request-db.d.ts.map +1 -0
- package/dist/middleware/request-db.js +62 -0
- package/dist/middleware/require-actor.d.ts +28 -0
- package/dist/middleware/require-actor.d.ts.map +1 -0
- package/dist/middleware/require-actor.js +89 -0
- package/dist/middleware/require-permission.d.ts +9 -0
- package/dist/middleware/require-permission.d.ts.map +1 -0
- package/dist/middleware/require-permission.js +62 -0
- package/dist/middleware/security-headers.d.ts +10 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +19 -0
- package/dist/module.d.ts +41 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +1 -0
- package/dist/plugin.d.ts +66 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +37 -0
- package/dist/public-capability.d.ts +46 -0
- package/dist/public-capability.d.ts.map +1 -0
- package/dist/public-capability.js +140 -0
- package/dist/public-document-delivery.d.ts +111 -0
- package/dist/public-document-delivery.d.ts.map +1 -0
- package/dist/public-document-delivery.js +234 -0
- package/dist/types.d.ts +318 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/validation.d.ts +36 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +106 -0
- package/package.json +156 -0
package/dist/app.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { createContainer, createEventBus, createQueryRunner, } from "@voyant-travel/core";
|
|
2
|
+
import { createOutboxEventStore } from "@voyant-travel/db/outbox";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { containerToServiceResolver, makeFrameworkLogger, wireWorkflowRuntime, } from "./app-workflows.js";
|
|
5
|
+
import { createPathDbSelector } from "./lib/db-selector.js";
|
|
6
|
+
import { tryGetExecutionCtx } from "./lib/execution-ctx.js";
|
|
7
|
+
import { matchesPublicPath, normalizePathname } from "./lib/public-paths.js";
|
|
8
|
+
import { requestScopedEventBus } from "./lib/request-event-bus.js";
|
|
9
|
+
import { requireAuth } from "./middleware/auth.js";
|
|
10
|
+
import { DEFAULT_REQUEST_BODY_LIMIT_BYTES, requestBodyLimit } from "./middleware/body-size.js";
|
|
11
|
+
import { cors } from "./middleware/cors.js";
|
|
12
|
+
import { db } from "./middleware/db.js";
|
|
13
|
+
import { handleApiError, requestId } from "./middleware/error-boundary.js";
|
|
14
|
+
import { logger } from "./middleware/logger.js";
|
|
15
|
+
import { metrics } from "./middleware/metrics.js";
|
|
16
|
+
import { publicResponseCache } from "./middleware/public-cache.js";
|
|
17
|
+
import { rateLimit, resolveRateLimitStore, } from "./middleware/rate-limit.js";
|
|
18
|
+
import { requireActor } from "./middleware/require-actor.js";
|
|
19
|
+
import { securityHeaders } from "./middleware/security-headers.js";
|
|
20
|
+
import { expandHonoPlugins } from "./plugin.js";
|
|
21
|
+
function resolveSurfaceMountPath(prefix, path, fallback) {
|
|
22
|
+
const normalized = path?.trim();
|
|
23
|
+
if (!normalized) {
|
|
24
|
+
return `${prefix}/${fallback}`;
|
|
25
|
+
}
|
|
26
|
+
if (normalized === "/") {
|
|
27
|
+
return prefix;
|
|
28
|
+
}
|
|
29
|
+
return `${prefix}/${normalized.replace(/^\/+|\/+$/g, "")}`;
|
|
30
|
+
}
|
|
31
|
+
const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
32
|
+
function resolveConfiguredRateLimitStore(config, env) {
|
|
33
|
+
if (!config?.store)
|
|
34
|
+
return undefined;
|
|
35
|
+
return typeof config.store === "function" ? config.store(env) : config.store;
|
|
36
|
+
}
|
|
37
|
+
function buildRateLimitPolicy(config, env, bucket, defaults) {
|
|
38
|
+
return {
|
|
39
|
+
bucket,
|
|
40
|
+
...defaults,
|
|
41
|
+
store: resolveConfiguredRateLimitStore(config, env) ?? resolveRateLimitStore({ env }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function createApp(config) {
|
|
45
|
+
const app = new Hono();
|
|
46
|
+
app.onError(handleApiError);
|
|
47
|
+
// Expand plugins into their constituent modules/extensions before mounting
|
|
48
|
+
const expanded = config.plugins ? expandHonoPlugins(config.plugins) : null;
|
|
49
|
+
const allModules = [...(config.modules ?? []), ...(expanded?.modules ?? [])];
|
|
50
|
+
const allExtensions = [...(config.extensions ?? []), ...(expanded?.extensions ?? [])];
|
|
51
|
+
const eventBus = config.eventBus ?? createEventBus();
|
|
52
|
+
const query = typeof config.query === "function"
|
|
53
|
+
? config.query
|
|
54
|
+
: config.query
|
|
55
|
+
? createQueryRunner(config.query)
|
|
56
|
+
: undefined;
|
|
57
|
+
// Module container — registered services are resolvable from routes
|
|
58
|
+
const container = createContainer();
|
|
59
|
+
for (const mod of allModules) {
|
|
60
|
+
if (mod.module.service !== undefined) {
|
|
61
|
+
container.register(mod.module.name, mod.module.service);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const sub of expanded?.subscribers ?? []) {
|
|
65
|
+
eventBus.subscribe(sub.event, sub.handler, { inline: sub.inline ?? false });
|
|
66
|
+
}
|
|
67
|
+
// ---- Workflow runtime wiring (synchronous setup; manifest registration
|
|
68
|
+
// + EventBus forwarder run inside the lazy bootstrap below) ----
|
|
69
|
+
//
|
|
70
|
+
// We collect `workflows` + `eventFilters` from every module and plugin
|
|
71
|
+
// here so the failure mode for "duplicate workflow id across modules"
|
|
72
|
+
// surfaces at construction time (per architecture doc §18, the workflow
|
|
73
|
+
// runtime is fail-closed).
|
|
74
|
+
const collectedWorkflows = [];
|
|
75
|
+
const collectedFilters = [];
|
|
76
|
+
for (const mod of allModules) {
|
|
77
|
+
if (mod.module.workflows)
|
|
78
|
+
collectedWorkflows.push(...mod.module.workflows);
|
|
79
|
+
if (mod.module.eventFilters)
|
|
80
|
+
collectedFilters.push(...mod.module.eventFilters);
|
|
81
|
+
}
|
|
82
|
+
for (const plugin of config.plugins ?? []) {
|
|
83
|
+
if (plugin.workflows)
|
|
84
|
+
collectedWorkflows.push(...plugin.workflows);
|
|
85
|
+
if (plugin.eventFilters)
|
|
86
|
+
collectedFilters.push(...plugin.eventFilters);
|
|
87
|
+
}
|
|
88
|
+
// Validate duplicate workflow ids across modules + plugins. Same id from
|
|
89
|
+
// re-imports (HMR / shared bundles) is fine because identity is by id —
|
|
90
|
+
// we only flag genuinely-different definitions sharing an id.
|
|
91
|
+
if (config.workflows && collectedWorkflows.length > 0) {
|
|
92
|
+
const seen = new Map();
|
|
93
|
+
for (const wf of collectedWorkflows) {
|
|
94
|
+
const existing = seen.get(wf.id);
|
|
95
|
+
if (existing && existing !== wf) {
|
|
96
|
+
throw new Error(`[voyant] duplicate workflow id "${wf.id}" registered by multiple modules/plugins. ` +
|
|
97
|
+
`Workflow ids must be unique across the app — use a module-scoped prefix ` +
|
|
98
|
+
`(e.g. "${wf.id.includes(".") ? wf.id : `<module>.${wf.id}`}").`);
|
|
99
|
+
}
|
|
100
|
+
seen.set(wf.id, wf);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Workflow driver construction is **deferred** to the lazy bootstrap
|
|
104
|
+
// path so CF-edge users (whose driver options come from `env.*`
|
|
105
|
+
// bindings only available at request time) can pass a function-of-
|
|
106
|
+
// bindings shape — `(env) => createCloudflareEdgeDriver({...})` —
|
|
107
|
+
// rather than constructing at module-load time. Mode 2 / InMemory
|
|
108
|
+
// users pass a direct factory; the framework adapts both shapes.
|
|
109
|
+
// See reviewer feedback P2.1 + architecture doc §6.3.
|
|
110
|
+
let workflowDriver;
|
|
111
|
+
let bootstrapPromise = null;
|
|
112
|
+
function ensureRuntimeBootstrapped(bindings) {
|
|
113
|
+
if (!bootstrapPromise) {
|
|
114
|
+
bootstrapPromise = (async () => {
|
|
115
|
+
const ctx = { bindings, container, eventBus };
|
|
116
|
+
// ---- Workflow runtime FIRST — fail-closed manifest registration
|
|
117
|
+
// and EventBus forwarder must be in place before any module
|
|
118
|
+
// bootstrap can emit. Otherwise a `module.bootstrap` that
|
|
119
|
+
// emits an event during its own bootstrap would route through
|
|
120
|
+
// a bus with no workflow forwarder yet, silently losing the
|
|
121
|
+
// event. Per architecture doc §21.22 + reviewer feedback P2.3.
|
|
122
|
+
if (config.workflows) {
|
|
123
|
+
// `driver` is always a function-of-bindings (per
|
|
124
|
+
// VoyantWorkflowsConfig — see types.ts + reviewer feedback P2.1).
|
|
125
|
+
// Mode 2 / InMemory users wrap with `() => createXxxDriver({...})`.
|
|
126
|
+
// CF-edge users use `(env) => createCloudflareEdgeDriver({ env.* })`.
|
|
127
|
+
// We invoke with bindings, then the resulting DriverFactory
|
|
128
|
+
// with framework deps.
|
|
129
|
+
const factoryDeps = {
|
|
130
|
+
services: containerToServiceResolver(container),
|
|
131
|
+
logger: makeFrameworkLogger(config.logger),
|
|
132
|
+
};
|
|
133
|
+
const factory = config.workflows.driver(bindings);
|
|
134
|
+
workflowDriver = factory(factoryDeps);
|
|
135
|
+
await wireWorkflowRuntime({
|
|
136
|
+
modules: allModules.map((m) => m.module),
|
|
137
|
+
collectedWorkflows,
|
|
138
|
+
collectedFilters,
|
|
139
|
+
driver: workflowDriver,
|
|
140
|
+
environment: config.workflows.environment ?? "development",
|
|
141
|
+
projectId: config.workflows.projectId ?? "default",
|
|
142
|
+
eventBus,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Run each bootstrap in isolation — a single failing plugin/module/extension
|
|
146
|
+
// must not poison the cached promise and kill the whole app's request pipeline.
|
|
147
|
+
const runIsolated = async (label, fn) => {
|
|
148
|
+
if (!fn)
|
|
149
|
+
return;
|
|
150
|
+
try {
|
|
151
|
+
await fn(ctx);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
console.error(`[voyant] bootstrap failed for ${label}: ${message}`);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
for (const plugin of config.plugins ?? []) {
|
|
159
|
+
await runIsolated(`plugin:${plugin.name}`, plugin.bootstrap);
|
|
160
|
+
}
|
|
161
|
+
for (const mod of allModules) {
|
|
162
|
+
await runIsolated(`module:${mod.module.name}`, mod.module.bootstrap);
|
|
163
|
+
}
|
|
164
|
+
for (const ext of allExtensions) {
|
|
165
|
+
await runIsolated(`extension:${ext.extension.module}/${ext.extension.name}`, ext.extension.bootstrap);
|
|
166
|
+
}
|
|
167
|
+
})();
|
|
168
|
+
}
|
|
169
|
+
return bootstrapPromise;
|
|
170
|
+
}
|
|
171
|
+
// Request ID header
|
|
172
|
+
app.use("*", requestId);
|
|
173
|
+
// Structured logger
|
|
174
|
+
app.use("*", logger(config.logger));
|
|
175
|
+
// Per-request metrics → Analytics Engine (no-op without the binding).
|
|
176
|
+
// Mounted before the cache middleware so cache hits are measured too.
|
|
177
|
+
if (config.metrics !== false) {
|
|
178
|
+
app.use("*", metrics());
|
|
179
|
+
}
|
|
180
|
+
// CORS (allowlist via env CORS_ALLOWLIST)
|
|
181
|
+
app.use("*", cors());
|
|
182
|
+
if (config.securityHeaders !== false) {
|
|
183
|
+
app.use("*", securityHeaders(config.securityHeaders));
|
|
184
|
+
}
|
|
185
|
+
if (config.requestBodyLimit !== false) {
|
|
186
|
+
app.use("*", requestBodyLimit({
|
|
187
|
+
maxBytes: config.requestBodyLimit?.maxBytes ?? DEFAULT_REQUEST_BODY_LIMIT_BYTES,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
if (config.rateLimit !== false) {
|
|
191
|
+
const rateLimitConfig = config.rateLimit;
|
|
192
|
+
if (rateLimitConfig?.auth !== false) {
|
|
193
|
+
const authRule = rateLimitConfig?.auth ?? { max: 10, windowSeconds: 60 };
|
|
194
|
+
app.use("/auth/*", async (c, next) => {
|
|
195
|
+
if (c.req.method !== "POST")
|
|
196
|
+
return next();
|
|
197
|
+
return rateLimit(buildRateLimitPolicy(rateLimitConfig, c.env, "auth", authRule))(c, next);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (rateLimitConfig?.publicWrite !== false) {
|
|
201
|
+
const publicWriteRule = rateLimitConfig?.publicWrite ?? { max: 60, windowSeconds: 60 };
|
|
202
|
+
app.use("*", async (c, next) => {
|
|
203
|
+
if (!WRITE_METHODS.has(c.req.method))
|
|
204
|
+
return next();
|
|
205
|
+
const pathname = normalizePathname(new URL(c.req.url).pathname);
|
|
206
|
+
const isPublicWrite = pathname.startsWith("/v1/public/") ||
|
|
207
|
+
matchesPublicPath(pathname, config.publicPaths ?? []);
|
|
208
|
+
if (!isPublicWrite)
|
|
209
|
+
return next();
|
|
210
|
+
return rateLimit(buildRateLimitPolicy(rateLimitConfig, c.env, "public-write", publicWriteRule))(c, next);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Shared response cache for the public surface. Mounted BEFORE the
|
|
215
|
+
// runtime bootstrap on purpose: a cache hit skips module-graph
|
|
216
|
+
// instantiation, auth, and the per-request db client entirely — the
|
|
217
|
+
// hit path allocates nothing but the cached body. Only responses a
|
|
218
|
+
// route explicitly marks `Cache-Control: public, s-maxage=…` are
|
|
219
|
+
// stored (see middleware docs).
|
|
220
|
+
if (config.publicCache !== false) {
|
|
221
|
+
app.use("*", publicResponseCache(config.publicCache ?? {}));
|
|
222
|
+
}
|
|
223
|
+
// Per-request outbox store factory: emits happen in route handlers,
|
|
224
|
+
// after the db middleware ran, so the request's db client is resolved
|
|
225
|
+
// lazily at capture time. Outbox writes are single statements — the
|
|
226
|
+
// cheap http client on non-transactional surfaces handles them fine.
|
|
227
|
+
const buildOutboxStore = config.outbox
|
|
228
|
+
? (c) => createOutboxEventStore(() => {
|
|
229
|
+
const requestDb = c.get("db");
|
|
230
|
+
if (!requestDb) {
|
|
231
|
+
throw new Error("[voyant] outbox capture needs the per-request db — emit ran before the db middleware");
|
|
232
|
+
}
|
|
233
|
+
return requestDb;
|
|
234
|
+
})
|
|
235
|
+
: undefined;
|
|
236
|
+
app.use("*", async (c, next) => {
|
|
237
|
+
c.set("container", container);
|
|
238
|
+
// Request-scoped bus: emits defer non-`inline` subscribers past the
|
|
239
|
+
// response via waitUntil, so handlers doing outbound HTTP (CMS sync,
|
|
240
|
+
// e-invoicing) no longer add their latency to every mutation.
|
|
241
|
+
//
|
|
242
|
+
// With `outbox: true`, emits are also durable (persisted before
|
|
243
|
+
// delivery, retried by `drainOutbox` from @voyant-travel/db/outbox) —
|
|
244
|
+
// INCLUDING on runtimes without an ExecutionContext (Node/headless),
|
|
245
|
+
// where emits await handlers inline but still capture through the
|
|
246
|
+
// store. Only when there's neither a scheduler nor a store does the
|
|
247
|
+
// raw bus go on the context unwrapped.
|
|
248
|
+
const executionCtx = tryGetExecutionCtx(c);
|
|
249
|
+
const outboxStore = buildOutboxStore?.(c);
|
|
250
|
+
c.set("eventBus", executionCtx || outboxStore
|
|
251
|
+
? requestScopedEventBus(eventBus, executionCtx ? (pending) => executionCtx.waitUntil(pending) : undefined, outboxStore)
|
|
252
|
+
: eventBus);
|
|
253
|
+
if (config.link) {
|
|
254
|
+
c.set("link", config.link);
|
|
255
|
+
}
|
|
256
|
+
if (query) {
|
|
257
|
+
c.set("query", query);
|
|
258
|
+
}
|
|
259
|
+
// Bootstrap (fires once, idempotent) — resolves the workflow driver
|
|
260
|
+
// with c.env-supplied bindings on the first request, so deferred
|
|
261
|
+
// driver construction sees real runtime bindings (reviewer P2.1).
|
|
262
|
+
await ensureRuntimeBootstrapped(c.env);
|
|
263
|
+
if (workflowDriver) {
|
|
264
|
+
// Surfaced on `c.var.workflowDriver` so HTTP route handlers can
|
|
265
|
+
// call `driver.trigger(...)` directly without re-resolving from
|
|
266
|
+
// the container. Also used by the optional HTTP ingest adapter
|
|
267
|
+
// (`mountHttpIngestAdapter` from `@voyant-travel/workflows/http-ingest`).
|
|
268
|
+
c.set("workflowDriver", workflowDriver);
|
|
269
|
+
}
|
|
270
|
+
return next();
|
|
271
|
+
});
|
|
272
|
+
// Health check (public, no auth)
|
|
273
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
274
|
+
// App-owned auth handler (must be before auth middleware — these routes are public)
|
|
275
|
+
const authHandler = config.auth?.handler;
|
|
276
|
+
if (authHandler) {
|
|
277
|
+
app.all("/auth/*", async (c) => {
|
|
278
|
+
const authApp = authHandler(c.env);
|
|
279
|
+
return authApp.fetch(c.req.raw, c.env, c.executionCtx);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// Transactional surface map: a request must be served by a
|
|
283
|
+
// transaction-capable db client when its path belongs to (a) a module
|
|
284
|
+
// declaring `requiresTransactionalDb`, (b) a module targeted by an
|
|
285
|
+
// extension that declares it (extensions mount under the target
|
|
286
|
+
// module's prefix — e.g. catalog-authoring's compose routes live under
|
|
287
|
+
// /v1/admin/products), or (c) a template-supplied extra path
|
|
288
|
+
// (`dbTransactionalPaths` — for additionalRoutes / adapter-wired flows
|
|
289
|
+
// like the catalog booking engine whose transactionality depends on
|
|
290
|
+
// starter wiring).
|
|
291
|
+
const txModuleNames = new Set(allModules.filter((m) => m.module.requiresTransactionalDb).map((m) => m.module.name));
|
|
292
|
+
for (const ext of allExtensions) {
|
|
293
|
+
if (ext.extension.requiresTransactionalDb)
|
|
294
|
+
txModuleNames.add(ext.extension.module);
|
|
295
|
+
}
|
|
296
|
+
const txRequiringModules = [...txModuleNames];
|
|
297
|
+
const txPrefixes = [...(config.dbTransactionalPaths ?? [])];
|
|
298
|
+
for (const name of txModuleNames) {
|
|
299
|
+
// `/v1/public/<name>` is added unconditionally (not only when the
|
|
300
|
+
// module mounts publicRoutes): other modules mounted at the public
|
|
301
|
+
// root can serve paths under a flagged module's segment — e.g.
|
|
302
|
+
// storefront (publicPath "/") handles
|
|
303
|
+
// /v1/public/bookings/sessions/bootstrap, which reaches
|
|
304
|
+
// bookings' transactional reserve flow.
|
|
305
|
+
txPrefixes.push(`/v1/admin/${name}`, `/v1/${name}`, `/v1/public/${name}`);
|
|
306
|
+
}
|
|
307
|
+
for (const mod of allModules) {
|
|
308
|
+
if (txModuleNames.has(mod.module.name) && mod.publicRoutes) {
|
|
309
|
+
txPrefixes.push(resolveSurfaceMountPath("/v1/public", mod.publicPath, mod.module.name));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const ext of allExtensions) {
|
|
313
|
+
if (txModuleNames.has(ext.extension.module) && ext.publicRoutes) {
|
|
314
|
+
txPrefixes.push(resolveSurfaceMountPath("/v1/public", ext.publicPath, ext.extension.module));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// With a `dbTransactional` factory, requests are routed per surface:
|
|
318
|
+
// transactional prefixes get it, everything else gets the cheap
|
|
319
|
+
// default (typically neon-http — no per-request connection handshake).
|
|
320
|
+
// Without it, `config.db` serves everything as before.
|
|
321
|
+
const dbSource = config.dbTransactional
|
|
322
|
+
? createPathDbSelector({
|
|
323
|
+
defaultFactory: config.db,
|
|
324
|
+
transactionalFactory: config.dbTransactional,
|
|
325
|
+
transactionalPrefixes: txPrefixes,
|
|
326
|
+
})
|
|
327
|
+
: config.db;
|
|
328
|
+
// Auth middleware for all other routes
|
|
329
|
+
app.use("*", requireAuth(dbSource, { publicPaths: config.publicPaths, auth: config.auth }));
|
|
330
|
+
// DB middleware — sets c.var.db for all downstream handlers.
|
|
331
|
+
// Pass the list of modules that need interactive transactions so the
|
|
332
|
+
// middleware can throw a clear startup-style error on first request if
|
|
333
|
+
// the wired adapter is neon-http (which doesn't support db.transaction).
|
|
334
|
+
app.use("*", db(dbSource, { requiresTransactionalDb: txRequiringModules }));
|
|
335
|
+
// Actor guards for the two API surfaces
|
|
336
|
+
app.use("/v1/admin/*", requireActor("staff"));
|
|
337
|
+
app.use("/v1/public/*", requireActor("customer", "partner", "supplier"));
|
|
338
|
+
const requireLegacyActor = requireActor("staff");
|
|
339
|
+
app.use("/v1/*", (c, next) => {
|
|
340
|
+
const pathname = new URL(c.req.url).pathname;
|
|
341
|
+
if (pathname.startsWith("/v1/admin/") || pathname.startsWith("/v1/public/")) {
|
|
342
|
+
return next();
|
|
343
|
+
}
|
|
344
|
+
return requireLegacyActor(c, next);
|
|
345
|
+
});
|
|
346
|
+
// Admin capability discovery — GET /v1/admin/_meta/capabilities. A built-in
|
|
347
|
+
// framework route (like /health), mounted only when the deployment supplies
|
|
348
|
+
// the operation catalogue via `config.adminMeta` (from
|
|
349
|
+
// `@voyant-travel/admin-contracts`) — keeping `@voyant-travel/hono` decoupled from it.
|
|
350
|
+
// Guarded by the `/v1/admin/*` staff actor guard above.
|
|
351
|
+
if (config.adminMeta) {
|
|
352
|
+
const adminMeta = config.adminMeta;
|
|
353
|
+
app.get("/v1/admin/_meta/capabilities", (c) => c.json({
|
|
354
|
+
contractVersion: adminMeta.contractVersion,
|
|
355
|
+
...(adminMeta.deploymentVersion ? { deploymentVersion: adminMeta.deploymentVersion } : {}),
|
|
356
|
+
modules: allModules.map((m) => m.module.name),
|
|
357
|
+
operations: adminMeta.operations,
|
|
358
|
+
actor: c.get("actor"),
|
|
359
|
+
scopes: c.get("scopes"),
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
// Mount module routes
|
|
363
|
+
for (const mod of allModules) {
|
|
364
|
+
if (mod.adminRoutes) {
|
|
365
|
+
app.route(`/v1/admin/${mod.module.name}`, mod.adminRoutes);
|
|
366
|
+
}
|
|
367
|
+
if (mod.publicRoutes) {
|
|
368
|
+
app.route(resolveSurfaceMountPath("/v1/public", mod.publicPath, mod.module.name), mod.publicRoutes);
|
|
369
|
+
}
|
|
370
|
+
if (mod.routes) {
|
|
371
|
+
app.route(`/v1/${mod.module.name}`, mod.routes);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Mount extension routes
|
|
375
|
+
for (const ext of allExtensions) {
|
|
376
|
+
if (ext.adminRoutes) {
|
|
377
|
+
app.route(`/v1/admin/${ext.extension.module}`, ext.adminRoutes);
|
|
378
|
+
}
|
|
379
|
+
if (ext.publicRoutes) {
|
|
380
|
+
app.route(resolveSurfaceMountPath("/v1/public", ext.publicPath, ext.extension.module), ext.publicRoutes);
|
|
381
|
+
}
|
|
382
|
+
if (ext.routes) {
|
|
383
|
+
app.route(`/v1/${ext.extension.module}`, ext.routes);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Additional routes
|
|
387
|
+
if (config.additionalRoutes) {
|
|
388
|
+
config.additionalRoutes(app);
|
|
389
|
+
}
|
|
390
|
+
// Attach `ready()` directly to the Hono instance. Fires the lazy
|
|
391
|
+
// bootstrap with the supplied bindings (or `{}` for back-compat with
|
|
392
|
+
// Mode 2 / InMemory drivers that ignore them). Production code never
|
|
393
|
+
// calls `ready()` — the first request triggers the same boot via
|
|
394
|
+
// `ensureRuntimeBootstrapped(c.env)`. Tests + Mode 2 sibling processes
|
|
395
|
+
// use this so the time wheel + manifest registration happen without
|
|
396
|
+
// traffic; CF-edge users that want eager boot must pass the real `env`
|
|
397
|
+
// (otherwise the memoized bootstrap promise locks in a driver built
|
|
398
|
+
// from `{}` and every later request reuses that broken instance).
|
|
399
|
+
const augmented = app;
|
|
400
|
+
augmented.eventBus = eventBus;
|
|
401
|
+
augmented.ready = (bindings) => ensureRuntimeBootstrapped(bindings ?? {});
|
|
402
|
+
return augmented;
|
|
403
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function randomBytesHex(lengthBytes: number): string;
|
|
2
|
+
export declare function sha256Hex(input: string | Uint8Array): Promise<string>;
|
|
3
|
+
export declare function constantTimeEqual(a: string, b: string): boolean;
|
|
4
|
+
export declare function generateNumericCode(length: number): string;
|
|
5
|
+
/**
|
|
6
|
+
* SHA-256 hash a string using Web Crypto API.
|
|
7
|
+
* Returns the hash as a base64url string without padding,
|
|
8
|
+
* matching Better Auth's `defaultKeyHasher` format.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sha256Base64Url(input: string): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Unsign a Better Auth session cookie.
|
|
13
|
+
* Better Auth signs cookies as: encodeURIComponent(value + "." + base64(HMAC-SHA256(value, secret)))
|
|
14
|
+
*/
|
|
15
|
+
export declare function unsignCookie(rawCookieValue: string, secret: string): Promise<string | null>;
|
|
16
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/auth/crypto.ts"],"names":[],"mappings":"AAAA,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAK3E;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUpE;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwBjG"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export function randomBytesHex(lengthBytes) {
|
|
2
|
+
const bytes = new Uint8Array(lengthBytes);
|
|
3
|
+
crypto.getRandomValues(bytes);
|
|
4
|
+
return Array.from(bytes)
|
|
5
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
6
|
+
.join("");
|
|
7
|
+
}
|
|
8
|
+
export async function sha256Hex(input) {
|
|
9
|
+
const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
10
|
+
const hash = await crypto.subtle.digest("SHA-256", data.buffer);
|
|
11
|
+
const arr = Array.from(new Uint8Array(hash));
|
|
12
|
+
return arr.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
13
|
+
}
|
|
14
|
+
export function constantTimeEqual(a, b) {
|
|
15
|
+
const length = Math.max(a.length, b.length, 1);
|
|
16
|
+
let diff = a.length === b.length ? 0 : 1;
|
|
17
|
+
for (let i = 0; i < length; i++) {
|
|
18
|
+
diff |= (a.charCodeAt(i) | 0) ^ (b.charCodeAt(i) | 0);
|
|
19
|
+
}
|
|
20
|
+
return diff === 0;
|
|
21
|
+
}
|
|
22
|
+
export function generateNumericCode(length) {
|
|
23
|
+
const max = 10 ** length;
|
|
24
|
+
const buf = new Uint32Array(1);
|
|
25
|
+
crypto.getRandomValues(buf);
|
|
26
|
+
const code = Number((buf[0] ?? 0) % max);
|
|
27
|
+
return String(code).padStart(length, "0");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* SHA-256 hash a string using Web Crypto API.
|
|
31
|
+
* Returns the hash as a base64url string without padding,
|
|
32
|
+
* matching Better Auth's `defaultKeyHasher` format.
|
|
33
|
+
*/
|
|
34
|
+
export async function sha256Base64Url(input) {
|
|
35
|
+
const data = new TextEncoder().encode(input);
|
|
36
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
37
|
+
const bytes = new Uint8Array(hashBuffer);
|
|
38
|
+
let binary = "";
|
|
39
|
+
for (const byte of bytes) {
|
|
40
|
+
binary += String.fromCharCode(byte);
|
|
41
|
+
}
|
|
42
|
+
const base64 = btoa(binary);
|
|
43
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Unsign a Better Auth session cookie.
|
|
47
|
+
* Better Auth signs cookies as: encodeURIComponent(value + "." + base64(HMAC-SHA256(value, secret)))
|
|
48
|
+
*/
|
|
49
|
+
export async function unsignCookie(rawCookieValue, secret) {
|
|
50
|
+
const decoded = decodeURIComponent(rawCookieValue);
|
|
51
|
+
const lastDot = decoded.lastIndexOf(".");
|
|
52
|
+
if (lastDot < 1)
|
|
53
|
+
return null;
|
|
54
|
+
const value = decoded.substring(0, lastDot);
|
|
55
|
+
const signature = decoded.substring(lastDot + 1);
|
|
56
|
+
if (signature.length !== 44 || !signature.endsWith("="))
|
|
57
|
+
return null;
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
60
|
+
const sigBinStr = atob(signature);
|
|
61
|
+
const sigBytes = new Uint8Array(sigBinStr.length);
|
|
62
|
+
for (let i = 0; i < sigBinStr.length; i++)
|
|
63
|
+
sigBytes[i] = sigBinStr.charCodeAt(i);
|
|
64
|
+
const valid = await crypto.subtle.verify("HMAC", key, sigBytes, encoder.encode(value));
|
|
65
|
+
return valid ? value : null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { constantTimeEqual, generateNumericCode, randomBytesHex, sha256Base64Url, sha256Hex, unsignCookie, } from "./crypto.js";
|
|
2
|
+
export { requireUserId } from "./require-user.js";
|
|
3
|
+
export type { SessionAuthContext } from "./session-jwt.js";
|
|
4
|
+
export { extractBearerToken, verifySession } from "./session-jwt.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,SAAS,EACT,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-user.d.ts","sourceRoot":"","sources":["../../src/auth/require-user.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAInC,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAOhD"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface SessionAuthContext {
|
|
2
|
+
userId: string;
|
|
3
|
+
sessionId?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function verifySession(token: string, secretKey: string): Promise<SessionAuthContext>;
|
|
6
|
+
export declare function extractBearerToken(authHeader: string | undefined): string | null;
|
|
7
|
+
//# sourceMappingURL=session-jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-jwt.d.ts","sourceRoot":"","sources":["../../src/auth/session-jwt.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAWjG;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWhF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { verifySessionClaims } from "@voyant-travel/utils/session-claims";
|
|
2
|
+
export async function verifySession(token, secretKey) {
|
|
3
|
+
const payload = await verifySessionClaims(token, secretKey);
|
|
4
|
+
if (!payload) {
|
|
5
|
+
throw new Error("Invalid or expired token");
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
userId: payload.userId,
|
|
9
|
+
sessionId: payload.sessionId,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function extractBearerToken(authHeader) {
|
|
13
|
+
if (!authHeader)
|
|
14
|
+
return null;
|
|
15
|
+
const parts = authHeader.trim().split(/\s+/);
|
|
16
|
+
if (parts.length !== 2)
|
|
17
|
+
return null;
|
|
18
|
+
const scheme = parts[0];
|
|
19
|
+
const token = parts[1];
|
|
20
|
+
if (!scheme || !token || !/^bearer$/i.test(scheme))
|
|
21
|
+
return null;
|
|
22
|
+
return token;
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { HonoExtension, HonoModule } from "./module.js";
|
|
2
|
+
/**
|
|
3
|
+
* Manifest-driven runtime composition.
|
|
4
|
+
*
|
|
5
|
+
* The `voyant.config.ts` manifest already drives the migration/schema side
|
|
6
|
+
* (see `@voyant-travel/cli` `db doctor` and `docs/architecture/migration-resilience-rfc.md`).
|
|
7
|
+
* This module lets the SAME manifest drive runtime composition: instead of a
|
|
8
|
+
* template hand-listing `createApp({ modules, extensions })`, it registers a
|
|
9
|
+
* factory per manifest entry and derives the arrays from the manifest.
|
|
10
|
+
*
|
|
11
|
+
* The factories receive a typed **capability container** — the template's
|
|
12
|
+
* deployment-specific capabilities (storage, FX, notification providers,
|
|
13
|
+
* document-download resolvers, …) gathered in one place. Because Voyant runs
|
|
14
|
+
* on Cloudflare Workers (per-request `bindings`), capabilities are typically
|
|
15
|
+
* bindings-deferred closures (`(bindings) => T`), so the container is a plain
|
|
16
|
+
* typed value resolved per request rather than a boot-time singleton.
|
|
17
|
+
*/
|
|
18
|
+
/** A manifest entry: a bare specifier or `{ resolve, options }`. */
|
|
19
|
+
export type CompositionEntry = string | {
|
|
20
|
+
resolve: string;
|
|
21
|
+
options?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
/** The subset of `VoyantConfig` this composer reads. */
|
|
24
|
+
export interface CompositionManifest {
|
|
25
|
+
modules?: CompositionEntry[];
|
|
26
|
+
extensions?: CompositionEntry[];
|
|
27
|
+
}
|
|
28
|
+
/** Context handed to every factory: the capability container + per-entry options. */
|
|
29
|
+
export interface CompositionContext<TCapabilities> {
|
|
30
|
+
capabilities: TCapabilities;
|
|
31
|
+
options: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
export type ModuleFactory<TCapabilities> = (ctx: CompositionContext<TCapabilities>) => HonoModule | HonoModule[];
|
|
34
|
+
export type ExtensionFactory<TCapabilities> = (ctx: CompositionContext<TCapabilities>) => HonoExtension;
|
|
35
|
+
/**
|
|
36
|
+
* Maps manifest specifiers to the factory that builds the runtime unit. Keys
|
|
37
|
+
* MUST match the `voyant.config.ts` `modules` / `extensions` specifiers.
|
|
38
|
+
*/
|
|
39
|
+
export interface CompositionRegistry<TCapabilities> {
|
|
40
|
+
modules: Record<string, ModuleFactory<TCapabilities>>;
|
|
41
|
+
extensions?: Record<string, ExtensionFactory<TCapabilities>>;
|
|
42
|
+
}
|
|
43
|
+
export interface ComposedApp {
|
|
44
|
+
modules: HonoModule[];
|
|
45
|
+
extensions: HonoExtension[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Derive the `createApp({ modules, extensions })` arrays from a manifest by
|
|
49
|
+
* looking each entry up in the registry, **preserving manifest order** (mount
|
|
50
|
+
* + hook-registration order is significant). Throws if the manifest lists an
|
|
51
|
+
* entry the registry has no factory for — so "added to the manifest but not
|
|
52
|
+
* wired" fails loudly at boot rather than silently dropping a module.
|
|
53
|
+
*/
|
|
54
|
+
export declare function composeFromManifest<TCapabilities>(manifest: CompositionManifest, registry: CompositionRegistry<TCapabilities>, capabilities: TCapabilities): ComposedApp;
|
|
55
|
+
export interface ManifestRegistryDiff {
|
|
56
|
+
/** In the manifest but with no registered factory. */
|
|
57
|
+
missingFactories: string[];
|
|
58
|
+
/** Registered factories not referenced by the manifest. */
|
|
59
|
+
orphanFactories: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pure parity check between a manifest's entries and a registry's keys, for
|
|
63
|
+
* tooling (`voyant db doctor`). Reports manifest entries with no factory and
|
|
64
|
+
* factories the manifest never references.
|
|
65
|
+
*/
|
|
66
|
+
export declare function diffManifestRegistry(manifestEntries: CompositionEntry[], registryKeys: string[]): ManifestRegistryDiff;
|
|
67
|
+
//# sourceMappingURL=composition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE5D;;;;;;;;;;;;;;;GAeG;AAEH,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA;AAE9F,wDAAwD;AACxD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAChC;AAED,qFAAqF;AACrF,MAAM,WAAW,kBAAkB,CAAC,aAAa;IAC/C,YAAY,EAAE,aAAa,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,MAAM,MAAM,aAAa,CAAC,aAAa,IAAI,CACzC,GAAG,EAAE,kBAAkB,CAAC,aAAa,CAAC,KACnC,UAAU,GAAG,UAAU,EAAE,CAAA;AAC9B,MAAM,MAAM,gBAAgB,CAAC,aAAa,IAAI,CAC5C,GAAG,EAAE,kBAAkB,CAAC,aAAa,CAAC,KACnC,aAAa,CAAA;AAElB;;;GAGG;AACH,MAAM,WAAW,mBAAmB,CAAC,aAAa;IAChD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC,CAAA;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAC,CAAA;CAC7D;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,UAAU,EAAE,aAAa,EAAE,CAAA;CAC5B;AAUD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAC/C,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,mBAAmB,CAAC,aAAa,CAAC,EAC5C,YAAY,EAAE,aAAa,GAC1B,WAAW,CA0Bb;AAED,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,2DAA2D;IAC3D,eAAe,EAAE,MAAM,EAAE,CAAA;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,gBAAgB,EAAE,EACnC,YAAY,EAAE,MAAM,EAAE,GACrB,oBAAoB,CAOtB"}
|