@c9up/station 0.1.11 → 0.1.13

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.
@@ -26,10 +26,10 @@ import type {
26
26
  DatabaseConnection,
27
27
  DateColumnConfig,
28
28
  } from "@c9up/atlas";
29
- import { inProduction } from "./nodeEnv.js";
30
29
  import { ResourceRegistry } from "./ResourceRegistry.js";
31
- import { setStation } from "./services/main.js";
30
+ import { clearStation, getStation, setStation } from "./services/main.js";
32
31
  import type { AuditEvent, Resource, ResourceAction } from "./types.js";
32
+ import { inProduction } from "./vendor/nodeEnv.js";
33
33
  // note: AuditEvent + ResourceAction are used by the CRUD handlers below;
34
34
  // the imports stay in one block for clarity.
35
35
  import { buildFormViewModel } from "./views/form.js";
@@ -287,7 +287,7 @@ type RuneValidationResult =
287
287
  /**
288
288
  * A built rune schema — only the result-based check is consumed.
289
289
  *
290
- * `validateResult`, NOT `validate`: rune reserves `validate()` for the VineJS
290
+ * `validateResult`, NOT `validate`: rune reserves `validate()` for the
291
291
  * contract (async, throwing). Reading `.valid` off a Promise gives `undefined`,
292
292
  * so keeping the old name would have made every admin write fail closed with an
293
293
  * empty error list.
@@ -548,11 +548,14 @@ export function resourcesNeedValidation(
548
548
  * NEVER `in` on an instance (`@Column() declare` makes `in` always false —
549
549
  * mirror `BaseRepository.#hydrate`).
550
550
  *
551
- * Per-column rule mapping tolerates the form-post wire shape (values arrive as
552
- * strings — rune does NOT coerce): booleans `.parse(isCheckedValue)`, numbers
551
+ * Per-column rule mapping tolerates the form-post wire shape, where every
552
+ * value arrives as a string: booleans `.parse(isCheckedValue)`, numbers
553
553
  * `.parse` string→Number (NaN falls through to a type error), everything else
554
- * `rules.string()`. `.parse`/`.optional`/`.nullable` force rune's TS path, so
555
- * no native `.node` is touched.
554
+ * `rules.string()`. The explicit `.parse` is deliberate even though rune now
555
+ * coerces both itself — a checkbox posts the values IT chooses (`isCheckedValue`
556
+ * is the column's own truthy spelling), which is a wider set than any
557
+ * general-purpose coercion should accept. `.parse`/`.optional`/`.nullable`
558
+ * force rune's TS path, so no native `.node` is touched.
556
559
  *
557
560
  * Returns the built schema plus the set of boolean columns — the handler
558
561
  * default-fills an ABSENT boolean key to `false` before validating (an
@@ -653,7 +656,7 @@ function fieldErrors(
653
656
  const out: Record<string, string> = {};
654
657
  for (const e of errors) {
655
658
  if (e.field === "_root") continue;
656
- // Collapse a nested VineJS-style path (`profile.name`) to its top-level
659
+ // Collapse a nested dotted path (`profile.name`) to its top-level
657
660
  // key — `form.ts` keys errors by column propertyKey. `split` always yields
658
661
  // a non-empty array, so `[0]` is a string (no fallback needed).
659
662
  const [key = e.field] = e.field.split(".");
@@ -834,6 +837,9 @@ let auditCloneWarnEmitted = false;
834
837
  const TEMPLATES_ROOT = fileURLToPath(new URL("../templates/", import.meta.url));
835
838
 
836
839
  export default class StationProvider {
840
+ /** What this provider bound, so shutdown only clears its own. */
841
+ #owned: ResourceRegistry | undefined;
842
+
837
843
  #contexts: Map<Resource, ResourceContext> = new Map();
838
844
  #viewRenderer: InkerViewRenderer | undefined;
839
845
  /**
@@ -871,7 +877,8 @@ export default class StationProvider {
871
877
  async boot(): Promise<void> {
872
878
  // Force-resolve so `setStation` runs even if no preload touches the
873
879
  // singleton. Mirrors AuroraProvider.boot().
874
- await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
880
+ this.#owned =
881
+ await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
875
882
  }
876
883
 
877
884
  async start(): Promise<void> {
@@ -879,8 +886,14 @@ export default class StationProvider {
879
886
 
880
887
  const registry =
881
888
  await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
882
- const resources = registry.all();
883
- if (resources.length === 0) return;
889
+
890
+ // NOT gated on the registry being non-empty any more. Providers start
891
+ // BEFORE preloads run, and a preload is where the docs say to write
892
+ // `station.register(...)`, so reading a snapshot here and giving up when
893
+ // it was empty meant an application that followed the documentation got
894
+ // no admin at all. The surface is prepared here and each resource is
895
+ // mounted as it is declared — the shape upstream's Transmit uses for the
896
+ // same ordering problem.
884
897
 
885
898
  // 54.7 — read the optional `station` config block. Defaults bake
886
899
  // in `requireAuth: true` when @c9up/warden is detected later in
@@ -894,67 +907,94 @@ export default class StationProvider {
894
907
  if (!peers) return;
895
908
  const { router, atlas, rune } = peers;
896
909
 
897
- // 57.7 fail-closed: a write-capable admin (create/edit) validates + guards
898
- // mass-assignment through a rune-derived schema. If atlas is present (CRUD
899
- // is live) but `@c9up/rune` is NOT installed, refuse to boot rather than
900
- // silently accept unvalidated bodies — there is NO fallback to the deleted
901
- // key-filter (`feedback_security_first`). Read-only admins need no rune.
902
- if (rune === null && resourcesNeedValidation(resources)) {
903
- throw new Error(
904
- "[station] @c9up/rune is required to validate admin write forms (create/edit) but is not installed. Add @c9up/rune (a Station peer dependency) — Station will NOT fall back to unvalidated mass-assignment. See https://ream.dev/modules/station#mass-assignment.",
905
- );
906
- }
907
-
908
910
  // Phase 1a2 — resolve the SHARED inker renderer (AdonisJS/Edge parity)
909
911
  // and mount Station's package templates as the `station` disk. Unlike
910
912
  // warden (whose absence keeps the open dev-preview path), a view engine
911
913
  // is a HARD render requirement (D2): once an admin surface exists there
912
- // is no page without it. Fail LOUD at boot when @c9up/inker is not wired
913
- // — mirrors #resolveDb's loud-on-missing posture — rather than degrade
914
- // silently or 500 on the first request.
915
- const renderer = await this.#resolveViewRenderer();
916
- // Mount once: `station::<template>` now resolves to this package's
917
- // templates/ dir on the host's single inker engine (edge.mount parity).
918
- renderer.mount("station", TEMPLATES_ROOT);
919
- this.#viewRenderer = renderer;
914
+ // is no page without it.
915
+ //
916
+ // The failure is HELD rather than thrown: with no resource declared
917
+ // there is no admin surface, and an application that installed the
918
+ // provider without ever registering a resource must not be forced to
919
+ // wire a view engine. It is rethrown on the first resource, which is the
920
+ // moment the requirement becomes real.
921
+ let rendererFailure: unknown;
922
+ try {
923
+ const renderer = await this.#resolveViewRenderer();
924
+ // Mount once: `station::<template>` now resolves to this package's
925
+ // templates/ dir on the host's single inker engine (edge.mount parity).
926
+ renderer.mount("station", TEMPLATES_ROOT);
927
+ this.#viewRenderer = renderer;
928
+ } catch (err) {
929
+ rendererFailure = err;
930
+ }
920
931
 
921
932
  // Phase 1b — wire the warden auth gate (or warn-once when it stays open).
922
933
  await this.#configureAuth(userConfig);
923
934
 
924
- // Phase 2 — build per-resource context ONCE. `#resolveDb()` is
925
- // loud: if the host installed `@c9up/atlas` but didn't register
926
- // `@c9up/atlas/provider` in `reamrc.ts`, AC11's "surface
927
- // AtlasProvider misconfiguration" intent kicks in.
928
935
  const db = await this.#resolveDb();
929
- for (const resource of resources) {
936
+
937
+ // Phase 2 — mount every resource, now and as they arrive. The shell (the
938
+ // login surface, the `/admin` index) goes up with the first one: an
939
+ // application with no resources has no admin, and had none before.
940
+ let shellMounted = false;
941
+ registry.useMounter((resource) => {
942
+ if (!shellMounted) {
943
+ if (rendererFailure !== undefined) throw rendererFailure;
944
+ try {
945
+ this.#registerShellRoutes(router);
946
+ } catch (err) {
947
+ // The router proxy may still throw "Router accessed before
948
+ // initialization" on first property access (a boot-ordering
949
+ // hazard where the proxy module imported but Ignitor's
950
+ // `setRouter` never fired). That is a legitimate degraded-host
951
+ // shape — give up quietly. Anything else propagates.
952
+ if (isRouterProxyUninit(err)) return;
953
+ throw err;
954
+ }
955
+ shellMounted = true;
956
+ }
957
+ // 57.7 fail-closed: a write-capable admin (create/edit) validates +
958
+ // guards mass-assignment through a rune-derived schema. If atlas is
959
+ // present (CRUD is live) but `@c9up/rune` is NOT installed, refuse
960
+ // rather than silently accept unvalidated bodies — there is NO
961
+ // fallback to the deleted key-filter. Read-only admins need no rune.
962
+ // Checked per resource, so the refusal names the one at fault.
963
+ if (rune === null && resourcesNeedValidation([resource])) {
964
+ throw new Error(
965
+ `[station] @c9up/rune is required to validate the write forms of the '${resource.name}' admin (create/edit) but is not installed. Add @c9up/rune (a Station peer dependency) — Station will NOT fall back to unvalidated mass-assignment. See https://ream.dev/modules/station#mass-assignment.`,
966
+ );
967
+ }
930
968
  this.#contexts.set(
931
969
  resource,
932
970
  buildResourceContext(resource, db, atlas, rune),
933
971
  );
934
- }
972
+ this.#registerResourceRoutes(router, resource);
973
+ });
974
+
975
+ this.#started = true;
976
+ }
935
977
 
936
- // 56.5 + 54.6 boot-time warn-onces. We surface the "auth wired but no
937
- // permissions seeded" and "no audit sink" gaps loud-and-once so a
938
- // half-wired install can't ship to prod without operators noticing.
939
- // (CSRF is no longer a boot-time warn — enforcement is request-time and
940
- // honest now: every write route fail-closes on `ctx.request.csrfProtected`.)
978
+ /**
979
+ * The gaps worth reporting once, when the full set of resources is known.
980
+ *
981
+ * `ready()` runs after every preload, so this is the first moment the whole
982
+ * admin surface exists. Nothing is mounted here — the routes were built as
983
+ * each resource was declared.
984
+ */
985
+ async ready(): Promise<void> {
986
+ if (!this.#started) return;
987
+ const registry =
988
+ await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
989
+ const resources = registry.all();
990
+ if (resources.length === 0) return;
991
+ // 56.5 + 54.6 — "auth wired but no permissions seeded" and "no audit
992
+ // sink" are surfaced loud-and-once so a half-wired install cannot ship to
993
+ // prod without operators noticing. (CSRF is not a boot-time warn:
994
+ // enforcement is request-time, and every write route fail-closes on
995
+ // `ctx.request.csrfProtected`.)
941
996
  this.#warnSeedPermissionsOnce(resources);
942
997
  this.#warnAuditGapsOnce(resources);
943
-
944
- // Phase 3 — route registration. The router proxy may still throw
945
- // "Router accessed before initialization" on first property access
946
- // (boot ordering hazard where the proxy module imported but
947
- // Ignitor's `setRouter` never fired). That's another legitimate
948
- // degraded-host shape — silent return. Anything else (slug
949
- // collision, future validation) propagates.
950
- try {
951
- this.#registerAdminRoutes(router, resources);
952
- } catch (err) {
953
- if (isRouterProxyUninit(err)) return;
954
- throw err;
955
- }
956
-
957
- this.#started = true;
958
998
  }
959
999
 
960
1000
  /**
@@ -986,7 +1026,11 @@ export default class StationProvider {
986
1026
  const rune = await this.#loadRune();
987
1027
  return { router, atlas, rune };
988
1028
  } catch (err) {
989
- if (isModuleNotFound(err)) return null;
1029
+ // ONLY when atlas itself is what is missing. An atlas that is
1030
+ // installed but cannot load — a dependency of its own gone, a broken
1031
+ // build — is not a host without atlas, and disabling the admin over
1032
+ // it hides the actual fault.
1033
+ if (isPackageMissing(err, "@c9up/atlas")) return null;
990
1034
  throw err;
991
1035
  }
992
1036
  }
@@ -1001,7 +1045,7 @@ export default class StationProvider {
1001
1045
  try {
1002
1046
  return loadBearingCast<RuneModule>(await import("@c9up/rune"));
1003
1047
  } catch (err) {
1004
- if (isModuleNotFound(err)) return null;
1048
+ if (isPackageMissing(err, "@c9up/rune")) return null;
1005
1049
  throw err;
1006
1050
  }
1007
1051
  }
@@ -1198,10 +1242,20 @@ export default class StationProvider {
1198
1242
  }
1199
1243
 
1200
1244
  /** Phase 3 — mount the login surface + per-resource CRUD routes. */
1201
- #registerAdminRoutes(
1202
- router: StationRouter,
1203
- resources: ReadonlyArray<Resource>,
1204
- ): void {
1245
+ /** Wrap a handler in the auth gate when one is configured. */
1246
+ #gate(
1247
+ handler: (ctx: StationHttpContext) => Promise<void>,
1248
+ ): (ctx: StationHttpContext) => Promise<void> {
1249
+ return this.#authConfig.requireAuth ? this.#withAuth(handler) : handler;
1250
+ }
1251
+
1252
+ /**
1253
+ * The routes that belong to the admin itself rather than to a resource.
1254
+ *
1255
+ * Registered once, while the provider starts — before the HTTP server is
1256
+ * listening, and before any resource has necessarily been declared.
1257
+ */
1258
+ #registerShellRoutes(router: StationRouter): void {
1205
1259
  // 54.7 — mount login surface first when auth is required, so
1206
1260
  // `/admin/login` is reachable even when the auth gate redirects every
1207
1261
  // other path to it.
@@ -1211,22 +1265,30 @@ export default class StationProvider {
1211
1265
  router.post("/admin/logout", this.#buildLogoutHandler());
1212
1266
  }
1213
1267
 
1214
- const gate = (
1215
- handler: (ctx: StationHttpContext) => Promise<void>,
1216
- ): ((ctx: StationHttpContext) => Promise<void>) =>
1217
- this.#authConfig.requireAuth ? this.#withAuth(handler) : handler;
1218
-
1219
- // `/admin` index — send the operator to the first listable resource (or the
1220
- // login surface). Without it, the post-login redirect("/admin") lands on a 404.
1268
+ // `/admin` index — send the operator to the first listable resource (or
1269
+ // the login surface). Without it, the post-login redirect("/admin") lands
1270
+ // on a 404.
1271
+ //
1272
+ // The registry is read WHEN THE REQUEST ARRIVES, not when this route is
1273
+ // built: resources are declared in preloads, so a list captured here
1274
+ // would name whatever happened to exist first — or nothing at all.
1221
1275
  router.get(
1222
1276
  "/admin",
1223
- gate(async (ctx) => {
1224
- const home = resources.find((r) => r.actions.includes("list"));
1277
+ this.#gate(async (ctx) => {
1278
+ const registry =
1279
+ await this.app.container.resolve<ResourceRegistry>(ResourceRegistry);
1280
+ const home = registry.all().find((r) => r.actions.includes("list"));
1225
1281
  ctx.response.redirect(home ? `/admin/${home.name}` : "/admin/login");
1226
1282
  }),
1227
1283
  );
1284
+ }
1228
1285
 
1229
- for (const resource of resources) {
1286
+ /** Every route one resource contributes, mounted the moment it is declared. */
1287
+ #registerResourceRoutes(router: StationRouter, resource: Resource): void {
1288
+ const gate = (
1289
+ handler: (ctx: StationHttpContext) => Promise<void>,
1290
+ ): ((ctx: StationHttpContext) => Promise<void>) => this.#gate(handler);
1291
+ {
1230
1292
  const slug = resource.name;
1231
1293
  if (resource.actions.includes("list")) {
1232
1294
  router.get(`/admin/${slug}`, gate(this.#buildListHandler(resource)));
@@ -1310,9 +1372,15 @@ export default class StationProvider {
1310
1372
  );
1311
1373
  }
1312
1374
 
1313
- async ready(): Promise<void> {}
1314
-
1315
- async shutdown(): Promise<void> {}
1375
+ async shutdown(): Promise<void> {
1376
+ // Release the module-level singleton, while it is still ours. A stopped
1377
+ // application left a dead resource registry reachable through `services/main`, and
1378
+ // with two applications in one process the survivor's binding must not
1379
+ // be the one cleared.
1380
+ if (this.#owned !== undefined && getStation() === this.#owned)
1381
+ clearStation();
1382
+ this.#owned = undefined;
1383
+ }
1316
1384
 
1317
1385
  #requireContext(resource: Resource): ResourceContext {
1318
1386
  const ctx = this.#contexts.get(resource);
@@ -1344,7 +1412,7 @@ export default class StationProvider {
1344
1412
  * Fail-closed response for invalid write input (57.7, AC4). Content-negotiated:
1345
1413
  *
1346
1414
  * - **JSON/XHR** → `422` with `{ error, code: "E_VALIDATION_ERROR", messages }`
1347
- * (Adonis/Vine parity — API clients get the machine-readable errors).
1415
+ * (Adonis parity — API clients get the machine-readable errors).
1348
1416
  * - **HTML with a session** → the AdonisJS web idiom (PRG): flash the old
1349
1417
  * input + per-field errors, then `redirect().back()` to the form `GET`,
1350
1418
  * which re-reads the flash and re-renders with values + errors. No inline
@@ -1367,7 +1435,7 @@ export default class StationProvider {
1367
1435
  ): Promise<void> {
1368
1436
  if (wantsJsonResponse(ctx)) {
1369
1437
  ctx.response.status(422);
1370
- // AdonisJS/VineJS parity: `{ errors: [{ field, rule, message }] }` —
1438
+ // AdonisJS parity: `{ errors: [{ field, rule, message }] }` —
1371
1439
  // the same shape ream's own `E_VALIDATION_ERROR` handler emits.
1372
1440
  ctx.response.json({
1373
1441
  errors: errors.map((e) => ({
@@ -2155,6 +2223,35 @@ export function isModuleNotFound(err: unknown): boolean {
2155
2223
  return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
2156
2224
  }
2157
2225
 
2226
+ /**
2227
+ * Is THIS package the one that is missing?
2228
+ *
2229
+ * The code alone does not say. `@c9up/atlas` installed but missing one of its
2230
+ * OWN dependencies raises the same `ERR_MODULE_NOT_FOUND`, and station read
2231
+ * that as "atlas is absent" and disabled the admin — silently, which is the
2232
+ * whole point of the degraded-host path and exactly wrong here: the peer IS
2233
+ * there and something inside it is broken, which the operator has to be told.
2234
+ *
2235
+ * Node distinguishes the two in the message it writes:
2236
+ *
2237
+ * Cannot find package '@c9up/atlas' imported from … ← the peer is absent
2238
+ * Cannot find module '/…/node_modules/@c9up/atlas/…' ← something inside it
2239
+ *
2240
+ * A bare-specifier miss names the PACKAGE; a miss inside an installed package
2241
+ * names a path. Anything that is not unmistakably the first is re-thrown.
2242
+ */
2243
+ export function isPackageMissing(err: unknown, specifier: string): boolean {
2244
+ if (!isModuleNotFound(err)) return false;
2245
+ const message = err instanceof Error ? err.message : String(err);
2246
+ // The exact forms Node emits for a bare specifier that resolves to nothing.
2247
+ return (
2248
+ message.includes(`Cannot find package '${specifier}'`) ||
2249
+ message.includes(`Cannot find module '${specifier}'`) ||
2250
+ // The CJS wording, kept for a host that transpiles.
2251
+ message.includes(`Cannot find module "${specifier}"`)
2252
+ );
2253
+ }
2254
+
2158
2255
  /** Ream's router proxy throws this exact string before Ignitor wires it. */
2159
2256
  function isRouterProxyUninit(err: unknown): boolean {
2160
2257
  return (
@@ -23,6 +23,18 @@ export function getStation(): ResourceRegistry | undefined {
23
23
  return instance;
24
24
  }
25
25
 
26
+ /**
27
+ * @internal Release the singleton, so a shut-down application does not leave a
28
+ * dead resource registry reachable through `services/main`.
29
+ *
30
+ * The caller checks ownership first (`getStation() === mine`): two applications
31
+ * share this module in one process, and the one shutting down must not clear
32
+ * what the other has since bound.
33
+ */
34
+ export function clearStation(): void {
35
+ instance = undefined;
36
+ }
37
+
26
38
  const station: ResourceRegistry = new Proxy({} as ResourceRegistry, {
27
39
  get(_target, prop) {
28
40
  if (!instance) {
@@ -1,13 +1,20 @@
1
+ // Generated from scripts/vendor/nodeEnv.ts — do not edit.
2
+ //
3
+ // This package is published and built from its own repository, so the file
4
+ // has to exist here rather than be imported. `pnpm vendor:sync` rewrites it,
5
+ // and `pnpm vendor:check` fails if this copy has drifted from the original.
6
+
1
7
  /**
2
- * Reading `NODE_ENV`, with the aliases people actually set.
8
+ * `NODE_ENV`, read the way every runtime actually spells it.
3
9
  *
4
- * `NODE_ENV=prod` is ordinary in a Dockerfile or a platform dashboard. Read
5
- * verbatim it answers "not production" — and here that decides whether an
6
- * admin panel mounts with no authorisation at all.
10
+ * `NODE_ENV=prod` is ordinary in a Dockerfile or a platform dashboard, and a
11
+ * bare `=== "production"` answers "not production" for it. What that costs
12
+ * differs per package, so the consequence is stated where the decision is
13
+ * taken, not here.
7
14
  *
8
- * Duplicated rather than imported: station's peers are all optional, and a
9
- * safety decision that only holds when one of them is installed is not a
10
- * decision.
15
+ * The tables mirror the framework's (`@c9up/ream`, src/env/nodeEnv.ts), which
16
+ * is the origin rather than a peer: it formats to different rules and exposes
17
+ * `currentNodeEnv()` instead of `inProduction()`.
11
18
  */
12
19
 
13
20
  const DEV_ENVS = ["dev", "develop", "development"];
package/dist/nodeEnv.d.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Reading `NODE_ENV`, with the aliases people actually set.
3
- *
4
- * `NODE_ENV=prod` is ordinary in a Dockerfile or a platform dashboard. Read
5
- * verbatim it answers "not production" — and here that decides whether an
6
- * admin panel mounts with no authorisation at all.
7
- *
8
- * Duplicated rather than imported: station's peers are all optional, and a
9
- * safety decision that only holds when one of them is installed is not a
10
- * decision.
11
- */
12
- /** The canonical name for whatever `NODE_ENV` holds. */
13
- export declare function normalizeNodeEnv(value: string | undefined): string;
14
- /** Whether this process is running in production, under any spelling. */
15
- export declare function inProduction(): boolean;
16
- //# sourceMappingURL=nodeEnv.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nodeEnv.d.ts","sourceRoot":"","sources":["../src/nodeEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAOlE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"nodeEnv.js","sourceRoot":"","sources":["../src/nodeEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC;AACnD,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AACzC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEtC,wDAAwD;AACxD,MAAM,UAAU,gBAAgB,CAAC,KAAyB;IACzD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAChC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,aAAa,CAAC;IACjD,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IACjD,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IAC3C,OAAO,GAAG,CAAC;AACZ,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY;IAC3B,OAAO,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC;AAChE,CAAC"}