@jango-blockchained/hoox-shared 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +17 -0
  2. package/dist/analytics.d.ts +23 -2
  3. package/dist/analytics.d.ts.map +1 -1
  4. package/dist/analytics.js +14 -3
  5. package/dist/api-client.d.ts +12 -2
  6. package/dist/api-client.d.ts.map +1 -1
  7. package/dist/api-client.js +74 -7
  8. package/dist/colors.d.ts +63 -25
  9. package/dist/colors.d.ts.map +1 -1
  10. package/dist/colors.js +60 -20
  11. package/dist/config.d.ts +34 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +61 -6
  14. package/dist/cron-handler.d.ts +34 -0
  15. package/dist/cron-handler.d.ts.map +1 -0
  16. package/dist/cron-handler.js +48 -0
  17. package/dist/d1/index.js +50 -6
  18. package/dist/d1/repository.d.ts +5 -0
  19. package/dist/d1/repository.d.ts.map +1 -1
  20. package/dist/d1/repository.js +297 -0
  21. package/dist/d1/schemas.js +81 -0
  22. package/dist/errors.d.ts +1 -0
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +48 -5
  25. package/dist/exchanges/base-exchange-client.d.ts +68 -0
  26. package/dist/exchanges/base-exchange-client.d.ts.map +1 -0
  27. package/dist/exchanges/base-exchange-client.js +120 -0
  28. package/dist/exchanges/index.d.ts +3 -0
  29. package/dist/exchanges/index.d.ts.map +1 -0
  30. package/dist/exchanges/index.js +120 -0
  31. package/dist/exchanges/types.d.ts +19 -0
  32. package/dist/exchanges/types.d.ts.map +1 -0
  33. package/dist/exchanges/types.js +1 -0
  34. package/dist/health.js +48 -5
  35. package/dist/index.d.ts +30 -11
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1058 -73
  38. package/dist/kvUtils.js +48 -5
  39. package/dist/middleware/auth.d.ts +46 -7
  40. package/dist/middleware/auth.d.ts.map +1 -1
  41. package/dist/middleware/auth.js +151 -0
  42. package/dist/middleware/cors.d.ts +28 -2
  43. package/dist/middleware/cors.d.ts.map +1 -1
  44. package/dist/middleware/cors.js +77 -0
  45. package/dist/middleware/index.d.ts +2 -2
  46. package/dist/middleware/index.d.ts.map +1 -1
  47. package/dist/middleware/index.js +361 -101
  48. package/dist/middleware/logger.d.ts +2 -1
  49. package/dist/middleware/logger.d.ts.map +1 -1
  50. package/dist/middleware/logger.js +202 -0
  51. package/dist/middleware/rate-limit.d.ts +3 -1
  52. package/dist/middleware/rate-limit.d.ts.map +1 -1
  53. package/dist/middleware/rate-limit.js +181 -0
  54. package/dist/middleware/security-headers.js +58 -0
  55. package/dist/middleware/validate.js +56 -0
  56. package/dist/operator-transport.d.ts +61 -0
  57. package/dist/operator-transport.d.ts.map +1 -0
  58. package/dist/operator-transport.js +87 -0
  59. package/dist/path-utils.d.ts +205 -0
  60. package/dist/path-utils.d.ts.map +1 -0
  61. package/dist/path-utils.js +188 -0
  62. package/dist/queue-handler.d.ts +43 -0
  63. package/dist/queue-handler.d.ts.map +1 -0
  64. package/dist/queue-handler.js +46 -0
  65. package/dist/router.d.ts +2 -2
  66. package/dist/router.d.ts.map +1 -1
  67. package/dist/router.js +15 -8
  68. package/dist/schemas/index.js +7 -4
  69. package/dist/schemas/registry.js +387 -0
  70. package/dist/schemas/types.js +1 -0
  71. package/dist/schemas/validators.d.ts.map +1 -1
  72. package/dist/schemas/validators.js +290 -0
  73. package/dist/service-bindings.d.ts +68 -26
  74. package/dist/service-bindings.d.ts.map +1 -1
  75. package/dist/service-bindings.js +219 -2
  76. package/dist/session.d.ts +9 -5
  77. package/dist/session.d.ts.map +1 -1
  78. package/dist/session.js +179 -4
  79. package/dist/sse.d.ts +12 -3
  80. package/dist/sse.d.ts.map +1 -1
  81. package/dist/sse.js +70 -7
  82. package/dist/stores/config-store.d.ts.map +1 -1
  83. package/dist/stores/config-store.js +129 -6
  84. package/dist/stores/service-store.d.ts +50 -1
  85. package/dist/stores/service-store.d.ts.map +1 -1
  86. package/dist/stores/service-store.js +162 -27
  87. package/dist/test-utils.d.ts +132 -0
  88. package/dist/test-utils.d.ts.map +1 -0
  89. package/dist/test-utils.js +128 -0
  90. package/dist/types/router.d.ts +18 -8
  91. package/dist/types/router.d.ts.map +1 -1
  92. package/dist/types.d.ts +124 -5
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js +11 -0
  95. package/dist/wizard/engine.d.ts.map +1 -1
  96. package/dist/wizard/engine.js +501 -0
  97. package/dist/wizard/index.js +2 -1
  98. package/dist/wizard/persistence.js +31 -0
  99. package/dist/wizard/presets.js +199 -0
  100. package/dist/wizard/provisioner.js +1 -0
  101. package/dist/wizard/types.js +1 -0
  102. package/package.json +46 -12
package/README.md CHANGED
@@ -20,6 +20,23 @@ This package provides the shared foundation for all Hoox workers. It includes:
20
20
 
21
21
  → [Architecture Overview](../../docs/devops/architecture/overview.md)
22
22
 
23
+ ## Public API
24
+
25
+ This package is published as `@jango-blockchained/hoox-shared` and exposes a layered import surface via the package `exports` map. All subpaths resolve to compiled `dist/*.js` and `dist/*.d.ts` files — the package tarball does **not** include TypeScript sources.
26
+
27
+ | Import | Resolves to | Notes |
28
+ | ------------------------------------------------- | ------------------------- | ------------------------------------------------------------------- |
29
+ | `@jango-blockchained/hoox-shared` | `dist/index.js` | Main barrel — everything in `src/index.ts` |
30
+ | `@jango-blockchained/hoox-shared/<subdir>` | `dist/<subdir>/index.js` | Sub-barrel for the named subdirectory |
31
+ | `@jango-blockchained/hoox-shared/<subdir>/<file>` | `dist/<subdir>/<file>.js` | Specific submodule file |
32
+ | `@jango-blockchained/hoox-shared/stores/<store>` | `dist/stores/<store>.js` | Zustand stores — deep-import only (no barrel) |
33
+ | `@jango-blockchained/hoox-shared/types` | `dist/types/index.d.ts` | Type-only re-exports |
34
+ | `@jango-blockchained/hoox-shared/<anything-else>` | `dist/<anything-else>.js` | Catch-all for top-level modules (e.g. `errors`, `router`, `kvKeys`) |
35
+
36
+ The named subdirectories with deep-import support are: `middleware`, `d1`, `schemas`, `wizard`, `exchanges`. Each one re-exports its submodules from its `index.ts` barrel, so the bare-subdir form (`<subdir>`) and the deep form (`<subdir>/<file>`) return the same symbols — the bare form is just the pre-bundled rollup.
37
+
38
+ **Recommendation:** prefer the bare form for subdirectories (`hoox-shared/middleware`) unless you specifically need a single submodule to keep the import graph small. The bare form gives you a stable, curated surface; the deep form is for tree-shaking-conscious or very specific use cases.
39
+
23
40
  ## Development
24
41
 
25
42
  ```bash
@@ -5,10 +5,30 @@
5
5
  /**
6
6
  * Environment with optional analytics service binding.
7
7
  * Workers that support analytics tracking should extend this type.
8
+ *
9
+ * Note on auth: `trackAnalytics` reads `INTERNAL_KEY_BINDING` from the
10
+ * passed env to authenticate with the analytics worker (its `/track/*`
11
+ * routes are gated by `requireInternalAuth`). We don't declare the
12
+ * property here because Cloudflare's generated `Env` type already
13
+ * includes it as a required string, and declaring it again as
14
+ * `INTERNAL_KEY_BINDING?: string` would conflict with workers that
15
+ * `extends Cloudflare.Env, AnalyticsEnv` (TS2320). The property is
16
+ * accessed dynamically; production envs from wrangler always have it,
17
+ * and test mocks add it explicitly via spread.
8
18
  */
9
19
  export interface AnalyticsEnv {
10
20
  ANALYTICS_SERVICE?: Fetcher;
11
- [key: string]: unknown;
21
+ }
22
+ /**
23
+ * Optional configuration for trackAnalytics.
24
+ */
25
+ export interface TrackAnalyticsOptions {
26
+ /**
27
+ * Custom indexes to attach to the Analytics Engine data point.
28
+ * Use this to tag rows with stable identifiers (e.g. probe_id)
29
+ * so they can be SQL-queried later.
30
+ */
31
+ indexes?: readonly string[];
12
32
  }
13
33
  /**
14
34
  * Track an analytics event by forwarding to the analytics worker.
@@ -17,6 +37,7 @@ export interface AnalyticsEnv {
17
37
  * @param env - Worker environment with optional ANALYTICS_SERVICE binding
18
38
  * @param endpoint - Analytics endpoint path (e.g., "/track/api-call")
19
39
  * @param body - JSON-serializable payload to send
40
+ * @param options - Optional config (currently supports `indexes`)
20
41
  */
21
- export declare function trackAnalytics(env: AnalyticsEnv, endpoint: string, body: Record<string, unknown>): Promise<void>;
42
+ export declare function trackAnalytics(env: AnalyticsEnv, endpoint: string, body: Record<string, unknown>, options?: TrackAnalyticsOptions): Promise<void>;
22
43
  //# sourceMappingURL=analytics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,YAAY,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,IAAI,CAAC,CAaf"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,YAAY,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
package/dist/analytics.js CHANGED
@@ -16,14 +16,25 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/analytics.ts
19
- async function trackAnalytics(env, endpoint, body) {
19
+ async function trackAnalytics(env, endpoint, body, options) {
20
20
  if (!env.ANALYTICS_SERVICE)
21
21
  return;
22
22
  try {
23
+ const payload = {
24
+ ...body,
25
+ ...options?.indexes ? { indexes: [...options.indexes] } : {}
26
+ };
27
+ const headers = {
28
+ "Content-Type": "application/json"
29
+ };
30
+ const internalKey = env.INTERNAL_KEY_BINDING;
31
+ if (internalKey) {
32
+ headers["X-Internal-Auth-Key"] = internalKey;
33
+ }
23
34
  await env.ANALYTICS_SERVICE.fetch(new Request(`http://localhost${endpoint}`, {
24
35
  method: "POST",
25
- headers: { "Content-Type": "application/json" },
26
- body: JSON.stringify(body)
36
+ headers,
37
+ body: JSON.stringify(payload)
27
38
  }));
28
39
  } catch (e) {
29
40
  console.error("Analytics tracking failed:", e);
@@ -2,11 +2,15 @@
2
2
  * API Client — Bun fetch wrapper with AbortController timeouts, retry logic,
3
3
  * and connection-aware error handling for the Hoox TUI dashboard.
4
4
  *
5
+ * Auth headers come from the operator transport profile (Bearer + optional
6
+ * Cloudflare Access service-token headers). See `operator-transport.ts`.
7
+ *
5
8
  * All fetch calls include a 5s timeout via AbortController.
6
9
  * Network errors (fetch failures, timeouts) are retried with exponential backoff.
7
10
  * HTTP 401/403 errors are NOT retried — they indicate auth/config issues.
8
11
  * HTTP 429 (rate limit) triggers a backing-off warning.
9
12
  */
13
+ import { type OperatorTransportEnv, type OperatorTransportProfile } from "./operator-transport";
10
14
  export declare class WorkerAPIError extends Error {
11
15
  status: number;
12
16
  retryable: boolean;
@@ -16,6 +20,12 @@ export declare class WorkerAPIError extends Error {
16
20
  retryable?: boolean;
17
21
  });
18
22
  }
23
+ export interface HooxFetchOptions extends RequestInit {
24
+ /** Injected transport profile (tests / multi-tenant callers). */
25
+ transport?: OperatorTransportProfile;
26
+ /** Env override when resolving transport (tests). */
27
+ transportEnv?: OperatorTransportEnv;
28
+ }
19
29
  /**
20
30
  * Perform an API fetch with timeout, retry logic, and error handling.
21
31
  *
@@ -26,9 +36,9 @@ export declare class WorkerAPIError extends Error {
26
36
  * - HTTP 429 (rate limited): NO retry — fail with rate limit info.
27
37
  * - HTTP 5xx: retry once.
28
38
  *
29
- * @param path API path (e.g. "/workers")
39
+ * @param path API path (e.g. "/v1/workers")
30
40
  * @param options Fetch options (merged with auth header and timeout signal)
31
41
  * @returns Parsed JSON response body
32
42
  */
33
- export declare function hooxFetch<T = unknown>(path: string, options?: RequestInit): Promise<T>;
43
+ export declare function hooxFetch<T = unknown>(path: string, options?: HooxFetchOptions): Promise<T>;
34
44
  //# sourceMappingURL=api-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmBH,qBAAa,cAAe,SAAQ,KAAK;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;gBAGjB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE;CAOtE;AAyCD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAC,CAAC,GAAG,OAAO,EACzC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,CAAC,CAAC,CA8FZ"}
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAgB9B,qBAAa,cAAe,SAAQ,KAAK;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;gBAGjB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE;CAOtE;AA+CD,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,iEAAiE;IACjE,SAAS,CAAC,EAAE,wBAAwB,CAAC;IACrC,qDAAqD;IACrD,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAAC,CAAC,GAAG,OAAO,EACzC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,CAAC,CAAC,CAsGZ"}
@@ -15,6 +15,69 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
+ // src/operator-transport.ts
19
+ function stripTrailingSlashes(url) {
20
+ return url.replace(/\/+$/, "");
21
+ }
22
+ function parseTransport(raw) {
23
+ if (!raw)
24
+ return null;
25
+ const v = raw.trim().toLowerCase();
26
+ if (VALID_TRANSPORTS.has(v)) {
27
+ return v;
28
+ }
29
+ return null;
30
+ }
31
+ function resolveOperatorTransportProfile(env = process.env, options = {}) {
32
+ const accessClientId = env.CF_ACCESS_CLIENT_ID?.trim() ?? "";
33
+ const accessClientSecret = env.CF_ACCESS_CLIENT_SECRET?.trim() ?? "";
34
+ const hasAccess = Boolean(accessClientId && accessClientSecret);
35
+ const explicit = parseTransport(env.HOOX_TRANSPORT) ?? parseTransport(options.configTransport);
36
+ const transport = explicit ?? (hasAccess ? "access" : "public");
37
+ const apiFromEnv = env.HOOX_API_URL?.trim();
38
+ const apiFromConfig = options.configApiUrl?.trim();
39
+ const tokenFromEnv = env.HOOX_API_TOKEN?.trim();
40
+ const tokenFromConfig = options.configApiToken?.trim();
41
+ return {
42
+ transport,
43
+ apiBase: stripTrailingSlashes(apiFromEnv || apiFromConfig || DEFAULT_API_BASE),
44
+ bearerToken: tokenFromEnv || tokenFromConfig || "",
45
+ accessClientId,
46
+ accessClientSecret
47
+ };
48
+ }
49
+ function buildOperatorAuthHeaders(profile) {
50
+ const headers = {};
51
+ if (profile.bearerToken) {
52
+ headers.Authorization = `Bearer ${profile.bearerToken}`;
53
+ }
54
+ const useAccessHeaders = profile.transport === "access" || Boolean(profile.accessClientId) && Boolean(profile.accessClientSecret);
55
+ if (useAccessHeaders && profile.accessClientId && profile.accessClientSecret) {
56
+ headers["CF-Access-Client-Id"] = profile.accessClientId;
57
+ headers["CF-Access-Client-Secret"] = profile.accessClientSecret;
58
+ }
59
+ return headers;
60
+ }
61
+ function hasOperatorClientCredentials(profile) {
62
+ if (profile.bearerToken)
63
+ return true;
64
+ return Boolean(profile.accessClientId && profile.accessClientSecret);
65
+ }
66
+ function operatorUrl(profile, path) {
67
+ const base = profile.apiBase.replace(/\/+$/, "");
68
+ const p = path.startsWith("/") ? path : `/${path}`;
69
+ return `${base}${p}`;
70
+ }
71
+ var DEFAULT_API_BASE = "http://localhost:8787", VALID_TRANSPORTS;
72
+ var init_operator_transport = __esm(() => {
73
+ VALID_TRANSPORTS = new Set([
74
+ "public",
75
+ "access",
76
+ "mtls",
77
+ "tunnel"
78
+ ]);
79
+ });
80
+
18
81
  // src/api-client.ts
19
82
  var exports_api_client = {};
20
83
  __export(exports_api_client, {
@@ -40,20 +103,25 @@ function isNetworkError(error) {
40
103
  function sleep(ms) {
41
104
  return new Promise((resolve) => setTimeout(resolve, ms));
42
105
  }
106
+ function activeProfile(override) {
107
+ return override ?? resolveOperatorTransportProfile();
108
+ }
43
109
  async function hooxFetch(path, options) {
110
+ const profile = options?.transport ?? (options?.transportEnv ? resolveOperatorTransportProfile(options.transportEnv) : activeProfile());
111
+ const { transport: _t, transportEnv: _e, ...fetchInit } = options ?? {};
44
112
  let lastError;
45
113
  for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
46
114
  const controller = new AbortController;
47
115
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
48
116
  try {
49
- const url = `${API_BASE}${path}`;
117
+ const url = operatorUrl(profile, path);
50
118
  const headers = {
51
119
  "Content-Type": "application/json",
52
- ...API_TOKEN ? { Authorization: `Bearer ${API_TOKEN}` } : {},
53
- ...options?.headers
120
+ ...buildOperatorAuthHeaders(profile),
121
+ ...fetchInit.headers
54
122
  };
55
123
  const response = await fetch(url, {
56
- ...options,
124
+ ...fetchInit,
57
125
  headers,
58
126
  signal: controller.signal
59
127
  });
@@ -101,10 +169,9 @@ async function hooxFetch(path, options) {
101
169
  retryable: false
102
170
  });
103
171
  }
104
- var API_BASE, API_TOKEN, FETCH_TIMEOUT_MS = 5000, MAX_RETRIES = 3, BASE_RETRY_DELAY_MS = 1000, MAX_RETRY_DELAY_MS = 16000, WorkerAPIError;
172
+ var FETCH_TIMEOUT_MS = 5000, MAX_RETRIES = 3, BASE_RETRY_DELAY_MS = 1000, MAX_RETRY_DELAY_MS = 16000, WorkerAPIError;
105
173
  var init_api_client = __esm(() => {
106
- API_BASE = process.env.HOOX_API_URL || "http://localhost:8787";
107
- API_TOKEN = process.env.HOOX_API_TOKEN || "";
174
+ init_operator_transport();
108
175
  WorkerAPIError = class WorkerAPIError extends Error {
109
176
  status;
110
177
  retryable;
package/dist/colors.d.ts CHANGED
@@ -1,34 +1,72 @@
1
1
  /**
2
- * Color Tokens — Maps hoox landing page design system to terminal-safe RGBA hex values.
2
+ * Color Tokens — Hoox TUI / shared design system (terminal-safe hex).
3
3
  *
4
- * Design DNA:
5
- * - Dark monochrome background: oklch(0.08 0 0) → #0D1117
6
- * - Orange accent: oklch(0.7 0.2 45) → #E8780A
7
- * - Squared edges (no rounding — editorial aesthetic)
4
+ * Design DNA (2026 polish):
5
+ * - Near-black canvas (not warm grey)
6
+ * - Cool indigo / cyan accent (Grok Build–adjacent; less orange)
7
+ * - Squared edges (no rounding — editorial HUD aesthetic)
8
8
  *
9
- * Usage: import { Colors } from "@hoox/shared"
9
+ * Usage: import { Colors, ConnectionStatusColor } from "@jango-blockchained/hoox-shared"
10
10
  * <text fg={Colors.accent}>Important</text>
11
11
  */
12
12
  export declare const Colors: {
13
- readonly background: "#0D1117";
14
- readonly foreground: "#EEEEEE";
15
- readonly card: "#1C1C1F";
16
- readonly border: "#484848";
17
- readonly muted: "#A0A0A0";
18
- readonly "muted-foreground": "#6E6E6E";
19
- readonly dim: "#3B3B3D";
20
- readonly accent: "#E8780A";
21
- readonly "accent-dim": "#B85E08";
22
- readonly success: "#00FF88";
23
- readonly warning: "#FFAA00";
24
- readonly error: "#FF4444";
25
- readonly info: "#4488FF";
26
- readonly text: "#EEEEEE";
27
- readonly "text-muted": "#A0A0A0";
28
- readonly "text-dim": "#6E6E6E";
29
- readonly panel: "#1C1C1F";
30
- readonly divider: "#484848";
31
- readonly highlight: "#E8780A";
13
+ readonly background: "#050508";
14
+ readonly foreground: "#E8E8F0";
15
+ readonly card: "#0A0A0F";
16
+ readonly border: "#232330";
17
+ readonly muted: "#8B8B9E";
18
+ readonly "muted-foreground": "#6E6E84";
19
+ readonly dim: "#3C3C50";
20
+ readonly accent: "#818CF8";
21
+ readonly "accent-dim": "#6366F1";
22
+ readonly success: "#34D399";
23
+ readonly warning: "#FBBF24";
24
+ readonly error: "#FB7185";
25
+ readonly info: "#38BDF8";
26
+ readonly text: "#E8E8F0";
27
+ readonly "text-muted": "#8B8B9E";
28
+ readonly panel: "#0A0A0F";
29
+ readonly divider: "#232330";
30
+ readonly highlight: "#22D3EE";
31
+ /** Dialog / overlay dim only — not a surface color */
32
+ readonly backdrop: "#000000";
32
33
  };
33
34
  export type ColorKey = keyof typeof Colors;
35
+ /**
36
+ * Cool spectrum for animated brackets (cyan → indigo → violet → magenta → back).
37
+ * Used by TUI chrome (`CoolBrackets`) similar to Grok Build accent motion.
38
+ */
39
+ export declare const CoolBracketPalette: readonly ["#22D3EE", "#38BDF8", "#60A5FA", "#818CF8", "#A78BFA", "#C084FC", "#E879F9", "#A78BFA", "#818CF8", "#38BDF8"];
40
+ export type CoolBracketColor = (typeof CoolBracketPalette)[number];
41
+ /** Connection pill colors (status bar). */
42
+ export declare const ConnectionStatusColor: {
43
+ readonly connected: "#34D399";
44
+ readonly polling: "#22D3EE";
45
+ readonly reconnecting: "#FBBF24";
46
+ readonly offline: "#FB7185";
47
+ };
48
+ export type ConnectionStatusKey = keyof typeof ConnectionStatusColor;
49
+ /** Worker / service health colors. */
50
+ export declare const WorkerStatusColor: {
51
+ readonly operational: "#34D399";
52
+ readonly degraded: "#FBBF24";
53
+ readonly down: "#FB7185";
54
+ };
55
+ export type WorkerStatusKey = keyof typeof WorkerStatusColor;
56
+ /** Log stream level colors. `debug` uses muted (readable), not dim. */
57
+ export declare const LogLevelColor: {
58
+ readonly error: "#FB7185";
59
+ readonly warn: "#FBBF24";
60
+ readonly info: "#E8E8F0";
61
+ readonly debug: "#8B8B9E";
62
+ };
63
+ export type LogLevelColorKey = keyof typeof LogLevelColor;
64
+ /** Alert severity colors. */
65
+ export declare const AlertSeverityColor: {
66
+ readonly info: "#38BDF8";
67
+ readonly warning: "#FBBF24";
68
+ readonly error: "#FB7185";
69
+ readonly critical: "#FB7185";
70
+ };
71
+ export type AlertSeverityColorKey = keyof typeof AlertSeverityColor;
34
72
  //# sourceMappingURL=colors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../src/colors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;CA2BT,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,MAAM,CAAC"}
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../src/colors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;IA2BjB,sDAAsD;;CAE9C,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,MAAM,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,yHAWrB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnE,2CAA2C;AAC3C,eAAO,MAAM,qBAAqB;;;;;CAKxB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,qBAAqB,CAAC;AAErE,sCAAsC;AACtC,eAAO,MAAM,iBAAiB;;;;CAIpB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,iBAAiB,CAAC;AAE7D,uEAAuE;AACvE,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,aAAa,CAAC;AAE1D,6BAA6B;AAC7B,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAAG,MAAM,OAAO,kBAAkB,CAAC"}
package/dist/colors.js CHANGED
@@ -17,26 +17,66 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/colors.ts
19
19
  var Colors = {
20
- background: "#0D1117",
21
- foreground: "#EEEEEE",
22
- card: "#1C1C1F",
23
- border: "#484848",
24
- muted: "#A0A0A0",
25
- "muted-foreground": "#6E6E6E",
26
- dim: "#3B3B3D",
27
- accent: "#E8780A",
28
- "accent-dim": "#B85E08",
29
- success: "#00FF88",
30
- warning: "#FFAA00",
31
- error: "#FF4444",
32
- info: "#4488FF",
33
- text: "#EEEEEE",
34
- "text-muted": "#A0A0A0",
35
- "text-dim": "#6E6E6E",
36
- panel: "#1C1C1F",
37
- divider: "#484848",
38
- highlight: "#E8780A"
20
+ background: "#050508",
21
+ foreground: "#E8E8F0",
22
+ card: "#0A0A0F",
23
+ border: "#232330",
24
+ muted: "#8B8B9E",
25
+ "muted-foreground": "#6E6E84",
26
+ dim: "#3C3C50",
27
+ accent: "#818CF8",
28
+ "accent-dim": "#6366F1",
29
+ success: "#34D399",
30
+ warning: "#FBBF24",
31
+ error: "#FB7185",
32
+ info: "#38BDF8",
33
+ text: "#E8E8F0",
34
+ "text-muted": "#8B8B9E",
35
+ panel: "#0A0A0F",
36
+ divider: "#232330",
37
+ highlight: "#22D3EE",
38
+ backdrop: "#000000"
39
+ };
40
+ var CoolBracketPalette = [
41
+ "#22D3EE",
42
+ "#38BDF8",
43
+ "#60A5FA",
44
+ "#818CF8",
45
+ "#A78BFA",
46
+ "#C084FC",
47
+ "#E879F9",
48
+ "#A78BFA",
49
+ "#818CF8",
50
+ "#38BDF8"
51
+ ];
52
+ var ConnectionStatusColor = {
53
+ connected: Colors.success,
54
+ polling: Colors.highlight,
55
+ reconnecting: Colors.warning,
56
+ offline: Colors.error
57
+ };
58
+ var WorkerStatusColor = {
59
+ operational: Colors.success,
60
+ degraded: Colors.warning,
61
+ down: Colors.error
62
+ };
63
+ var LogLevelColor = {
64
+ error: Colors.error,
65
+ warn: Colors.warning,
66
+ info: Colors.foreground,
67
+ debug: Colors.muted
68
+ };
69
+ var AlertSeverityColor = {
70
+ info: Colors.info,
71
+ warning: Colors.warning,
72
+ error: Colors.error,
73
+ critical: Colors.error
39
74
  };
40
75
  export {
41
- Colors
76
+ WorkerStatusColor,
77
+ LogLevelColor,
78
+ CoolBracketPalette,
79
+ ConnectionStatusColor,
80
+ Colors,
81
+ AlertSeverityColor
42
82
  };
package/dist/config.d.ts CHANGED
@@ -1,6 +1,17 @@
1
+ /** Owner rwx only — config dir may hold secrets and certs. */
2
+ export declare const HOOX_DIR_MODE = 448;
3
+ /** Owner rw only — config.json may contain apiToken. */
4
+ export declare const HOOX_CONFIG_FILE_MODE = 384;
5
+ /** Operator transport stored in config (mirrors HOOX_TRANSPORT). */
6
+ export type HooxConfigTransport = "public" | "access" | "mtls" | "tunnel";
1
7
  export interface HooxConfig {
2
8
  apiUrl: string;
3
9
  apiToken: string;
10
+ /**
11
+ * Operator transport preference for TUI/CLI remote sessions.
12
+ * Env `HOOX_TRANSPORT` overrides this when set.
13
+ */
14
+ transport?: HooxConfigTransport;
4
15
  refreshIntervalMs: number;
5
16
  theme: "dark" | "light";
6
17
  activeExchanges: string[];
@@ -16,7 +27,29 @@ export interface HooxConfig {
16
27
  /** Read config from disk, merging with defaults and env vars */
17
28
  export declare function readConfigSync(): HooxConfig;
18
29
  export declare function readConfig(): Promise<HooxConfig>;
19
- /** Write config to disk */
30
+ /**
31
+ * Ensure `~/.hoox` exists with owner-only permissions.
32
+ * Best-effort chmod on existing dirs (may fail on some FS / Windows).
33
+ */
34
+ export declare function ensureHooxDirSecure(dir?: string): void;
35
+ /**
36
+ * Write a path with owner-only file mode (0o600).
37
+ * Used by config writers and other secret-bearing files under ~/.hoox.
38
+ */
39
+ export declare function writeSecureFileSync(filePath: string, contents: string, mode?: number): void;
40
+ /**
41
+ * True when group or other have any access bits set (Unix permission triad).
42
+ * Pure helper so tests do not depend on real filesystem after mock.module.
43
+ */
44
+ export declare function isUnixModeGroupOrWorldAccessible(mode: number): boolean;
45
+ /**
46
+ * True when group or other have any access bits set (Unix).
47
+ * Always false when the file is missing or mode is unavailable.
48
+ */
49
+ export declare function isConfigWorldOrGroupReadable(filePath?: string): boolean;
50
+ /** Human-readable warning when config perms are too open. */
51
+ export declare function formatConfigPermissionWarning(filePath?: string): string | null;
52
+ /** Write config to disk with owner-only permissions. */
20
53
  export declare function writeConfigSync(config: HooxConfig): void;
21
54
  export declare function writeConfig(config: HooxConfig): Promise<void>;
22
55
  /** Validate config — returns array of error messages */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE;QACb,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,KAAK,EAAE,OAAO,CAAC;QACf,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;IACF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAkBD,gEAAgE;AAChE,wBAAgB,cAAc,IAAI,UAAU,CAsB3C;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC,CAEtD;AAED,2BAA2B;AAC3B,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CASxD;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnE;AAED,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,MAAM,EAAE,CAepE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAyBA,8DAA8D;AAC9D,eAAO,MAAM,aAAa,MAAQ,CAAC;AACnC,wDAAwD;AACxD,eAAO,MAAM,qBAAqB,MAAQ,CAAC;AAE3C,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE1E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE;QACb,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,KAAK,EAAE,OAAO,CAAC;QACf,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;IACF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAmBD,gEAAgE;AAChE,wBAAgB,cAAc,IAAI,UAAU,CAsB3C;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC,CAEtD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAgB,GAAG,IAAI,CAS/D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,MAA8B,GACnC,IAAI,CAON;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,GAAE,MAAoB,GAC7B,OAAO,CAOT;AAED,6DAA6D;AAC7D,wBAAgB,6BAA6B,CAC3C,QAAQ,GAAE,MAAoB,GAC7B,MAAM,GAAG,IAAI,CAOf;AAED,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAWxD;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnE;AASD,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,MAAM,EAAE,CAqBpE"}
package/dist/config.js CHANGED
@@ -16,14 +16,24 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // src/config.ts
19
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
19
+ import {
20
+ readFileSync,
21
+ writeFileSync,
22
+ existsSync,
23
+ mkdirSync,
24
+ chmodSync,
25
+ statSync
26
+ } from "fs";
20
27
  import { join } from "path";
21
28
  import { homedir } from "os";
22
29
  var HOX_DIR = join(homedir(), ".hoox");
23
30
  var CONFIG_PATH = join(HOX_DIR, "config.json");
31
+ var HOOX_DIR_MODE = 448;
32
+ var HOOX_CONFIG_FILE_MODE = 384;
24
33
  var DEFAULT_CONFIG = {
25
34
  apiUrl: "http://localhost:8787",
26
35
  apiToken: "",
36
+ transport: "public",
27
37
  refreshIntervalMs: 500,
28
38
  theme: "dark",
29
39
  activeExchanges: ["binance", "bybit", "mexc"],
@@ -56,12 +66,41 @@ function readConfigSync() {
56
66
  async function readConfig() {
57
67
  return readConfigSync();
58
68
  }
69
+ function ensureHooxDirSecure(dir = HOX_DIR) {
70
+ if (!existsSync(dir)) {
71
+ mkdirSync(dir, { recursive: true, mode: HOOX_DIR_MODE });
72
+ }
73
+ try {
74
+ chmodSync(dir, HOOX_DIR_MODE);
75
+ } catch {}
76
+ }
77
+ function writeSecureFileSync(filePath, contents, mode = HOOX_CONFIG_FILE_MODE) {
78
+ writeFileSync(filePath, contents, { encoding: "utf-8", mode });
79
+ try {
80
+ chmodSync(filePath, mode);
81
+ } catch {}
82
+ }
83
+ function isUnixModeGroupOrWorldAccessible(mode) {
84
+ return (mode & 63) !== 0;
85
+ }
86
+ function isConfigWorldOrGroupReadable(filePath = CONFIG_PATH) {
87
+ try {
88
+ if (!existsSync(filePath))
89
+ return false;
90
+ return isUnixModeGroupOrWorldAccessible(statSync(filePath).mode & 511);
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+ function formatConfigPermissionWarning(filePath = CONFIG_PATH) {
96
+ if (!isConfigWorldOrGroupReadable(filePath))
97
+ return null;
98
+ return `Warning: ${filePath} is group/world-accessible. ` + `Run: chmod 600 ${filePath} (and chmod 700 on the parent dir). ` + `This file may contain apiToken / operator secrets.`;
99
+ }
59
100
  function writeConfigSync(config) {
60
101
  try {
61
- if (!existsSync(HOX_DIR)) {
62
- mkdirSync(HOX_DIR, { recursive: true });
63
- }
64
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
102
+ ensureHooxDirSecure(HOX_DIR);
103
+ writeSecureFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), HOOX_CONFIG_FILE_MODE);
65
104
  } catch (err) {
66
105
  console.error("Failed to write config:", err);
67
106
  }
@@ -69,6 +108,12 @@ function writeConfigSync(config) {
69
108
  async function writeConfig(config) {
70
109
  writeConfigSync(config);
71
110
  }
111
+ var VALID_TRANSPORTS = new Set([
112
+ "public",
113
+ "access",
114
+ "mtls",
115
+ "tunnel"
116
+ ]);
72
117
  function validateConfig(config) {
73
118
  const errors = [];
74
119
  if (config.apiUrl && !config.apiUrl.startsWith("http")) {
@@ -80,12 +125,22 @@ function validateConfig(config) {
80
125
  if (config.theme && !["dark", "light"].includes(config.theme)) {
81
126
  errors.push('theme must be "dark" or "light"');
82
127
  }
128
+ if (config.transport !== undefined && !VALID_TRANSPORTS.has(config.transport)) {
129
+ errors.push('transport must be "public" | "access" | "mtls" | "tunnel"');
130
+ }
83
131
  return errors;
84
132
  }
85
133
  export {
134
+ writeSecureFileSync,
86
135
  writeConfigSync,
87
136
  writeConfig,
88
137
  validateConfig,
89
138
  readConfigSync,
90
- readConfig
139
+ readConfig,
140
+ isUnixModeGroupOrWorldAccessible,
141
+ isConfigWorldOrGroupReadable,
142
+ formatConfigPermissionWarning,
143
+ ensureHooxDirSecure,
144
+ HOOX_DIR_MODE,
145
+ HOOX_CONFIG_FILE_MODE
91
146
  };
@@ -0,0 +1,34 @@
1
+ import type { ScheduledEvent } from "@cloudflare/workers-types";
2
+ export interface CronHandlerOptions<Env = unknown> {
3
+ /** Name of the worker for logging context */
4
+ name: string;
5
+ /** Handler function to execute */
6
+ handler: (event: ScheduledEvent, env: Env, ctx: ExecutionContext) => Promise<void> | void;
7
+ /** Optional logger instance */
8
+ logger?: {
9
+ info(message: string, data?: Record<string, unknown>): void;
10
+ error(message: string, data?: Record<string, unknown>): void;
11
+ };
12
+ }
13
+ /**
14
+ * Creates a standardized cron handler with consistent logging and error handling.
15
+ *
16
+ * Usage:
17
+ * ```typescript
18
+ * const cronHandler = createCronHandler({
19
+ * name: "agent-worker",
20
+ * handler: async (event, env, ctx) => {
21
+ * await runAgentRoutine(env);
22
+ * },
23
+ * logger: createLogger({ service: "agent-worker" })
24
+ * });
25
+ *
26
+ * export default {
27
+ * async scheduled(event, env, ctx) {
28
+ * return await cronHandler(event, env, ctx);
29
+ * }
30
+ * };
31
+ * ```
32
+ */
33
+ export declare function createCronHandler<Env = unknown>(options: CronHandlerOptions<Env>): (event: ScheduledEvent, env: Env, ctx: ExecutionContext) => Promise<void>;
34
+ //# sourceMappingURL=cron-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-handler.d.ts","sourceRoot":"","sources":["../src/cron-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE,MAAM,WAAW,kBAAkB,CAAC,GAAG,GAAG,OAAO;IAC/C,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,OAAO,EAAE,CACP,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,+BAA+B;IAC/B,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC9D,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAG,OAAO,EAC7C,OAAO,EAAE,kBAAkB,CAAC,GAAG,CAAC,IAK9B,OAAO,cAAc,EACrB,KAAK,GAAG,EACR,KAAK,gBAAgB,KACpB,OAAO,CAAC,IAAI,CAAC,CA6BjB"}