@harperfast/harper 5.3.0-beta.1 → 5.3.0-beta.2

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 (126) hide show
  1. package/components/DESIGN.md +421 -0
  2. package/components/mcp/DESIGN.md +109 -0
  3. package/components/mcp/audit.ts +21 -17
  4. package/config/DESIGN.md +306 -0
  5. package/dataLayer/DESIGN.md +179 -0
  6. package/dataLayer/restoreMarker.ts +92 -25
  7. package/dist/components/mcp/audit.d.ts +2 -1
  8. package/dist/components/mcp/audit.js +21 -17
  9. package/dist/components/mcp/audit.js.map +1 -1
  10. package/dist/dataLayer/restoreMarker.d.ts +21 -8
  11. package/dist/dataLayer/restoreMarker.js +94 -27
  12. package/dist/dataLayer/restoreMarker.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +6 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/resources/RecordEncoder.js +25 -5
  17. package/dist/resources/RecordEncoder.js.map +1 -1
  18. package/dist/resources/Table.js +48 -2
  19. package/dist/resources/Table.js.map +1 -1
  20. package/dist/resources/analytics/write.d.ts +3 -0
  21. package/dist/resources/analytics/write.js +39 -13
  22. package/dist/resources/analytics/write.js.map +1 -1
  23. package/dist/resources/crdt.d.ts +10 -0
  24. package/dist/resources/crdt.js +22 -0
  25. package/dist/resources/crdt.js.map +1 -1
  26. package/dist/resources/databases.js +1 -1
  27. package/dist/resources/databases.js.map +1 -1
  28. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +2 -2
  29. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +43 -24
  30. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  31. package/dist/resources/indexes/fullTextDerivedIndex.d.ts +81 -0
  32. package/dist/resources/indexes/fullTextDerivedIndex.js +1004 -0
  33. package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -0
  34. package/dist/resources/indexes/fullTextNativeBinding.d.ts +78 -0
  35. package/dist/resources/indexes/fullTextNativeBinding.js +85 -0
  36. package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -0
  37. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +24 -0
  38. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +149 -0
  39. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -0
  40. package/dist/resources/recordLockCoordinator.d.ts +5 -5
  41. package/dist/resources/recordLockCoordinator.js +48 -16
  42. package/dist/resources/recordLockCoordinator.js.map +1 -1
  43. package/dist/resources/transactionBroadcast.js +4 -6
  44. package/dist/resources/transactionBroadcast.js.map +1 -1
  45. package/dist/security/auth.js +59 -23
  46. package/dist/security/auth.js.map +1 -1
  47. package/dist/security/deferredAuthentication.d.ts +11 -0
  48. package/dist/security/deferredAuthentication.js +25 -3
  49. package/dist/security/deferredAuthentication.js.map +1 -1
  50. package/dist/security/jsLoader.js +3 -0
  51. package/dist/security/jsLoader.js.map +1 -1
  52. package/dist/server/REST.js +6 -3
  53. package/dist/server/REST.js.map +1 -1
  54. package/dist/server/mqtt.js +5 -1
  55. package/dist/server/mqtt.js.map +1 -1
  56. package/dist/server/serverHelpers/serverUtilities.d.ts +3 -3
  57. package/dist/server/serverHelpers/serverUtilities.js +14 -3
  58. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  59. package/dist/server/serverHelpers/uwsServer.js +4 -1
  60. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  61. package/dist/server/serverHelpers/webSocketCloseReason.d.ts +2 -0
  62. package/dist/server/serverHelpers/webSocketCloseReason.js +29 -0
  63. package/dist/server/serverHelpers/webSocketCloseReason.js.map +1 -0
  64. package/dist/utility/logging/harper_logger.js +1 -0
  65. package/dist/utility/logging/harper_logger.js.map +1 -1
  66. package/index.ts +3 -0
  67. package/npm-shrinkwrap.json +104 -104
  68. package/package.json +6 -5
  69. package/resources/DESIGN.md +568 -3
  70. package/resources/RecordEncoder.ts +27 -5
  71. package/resources/Table.ts +49 -3
  72. package/resources/analytics/DESIGN.md +38 -0
  73. package/resources/analytics/write.ts +40 -14
  74. package/resources/crdt.ts +22 -0
  75. package/resources/databases.ts +1 -1
  76. package/resources/indexes/DESIGN.md +833 -0
  77. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +44 -25
  78. package/resources/indexes/fullTextDerivedIndex.ts +1165 -0
  79. package/resources/indexes/fullTextNativeBinding.ts +146 -0
  80. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +181 -0
  81. package/resources/record-locks.md +1407 -0
  82. package/resources/recordLockCoordinator.ts +61 -22
  83. package/resources/scheduler/DESIGN.md +40 -0
  84. package/resources/transactionBroadcast.ts +4 -4
  85. package/security/DESIGN.md +175 -0
  86. package/security/auth.ts +53 -24
  87. package/security/deferredAuthentication.ts +24 -2
  88. package/security/jsLoader.ts +3 -0
  89. package/server/DESIGN.md +264 -0
  90. package/server/REST.ts +6 -3
  91. package/server/mqtt.ts +6 -4
  92. package/server/serverHelpers/serverUtilities.ts +14 -3
  93. package/server/serverHelpers/uwsServer.ts +4 -1
  94. package/server/serverHelpers/webSocketCloseReason.ts +25 -0
  95. package/studio/web/assets/{Chat-D3j-1yY1.js → Chat-DADFFGe_.js} +1 -1
  96. package/studio/web/assets/{FloatingChat-BxJGYcfB.js → FloatingChat-D_mI-rZ7.js} +3 -3
  97. package/studio/web/assets/{apiToken-CT55oWOe.js → apiToken-c2NiSDHa.js} +1 -1
  98. package/studio/web/assets/{applications-D9Ct9_vm.js → applications-DktUqh7G.js} +1 -1
  99. package/studio/web/assets/{cssMode-DV8H7VwA.js → cssMode-Cs_75Xhw.js} +1 -1
  100. package/studio/web/assets/{editor-uatc0unt.js → editor-19b-Y1IN.js} +1 -1
  101. package/studio/web/assets/{html-Bm6D6paN.js → html-DiYEQMpB.js} +1 -1
  102. package/studio/web/assets/{htmlMode-CEn7tpLG.js → htmlMode-CmR0y7P_.js} +1 -1
  103. package/studio/web/assets/{index-BIXW6Pu4.js → index-Dm0rfkJ7.js} +5 -5
  104. package/studio/web/assets/{index.lazy-UI7L-Vrk.js → index.lazy-7vqt2CC3.js} +1 -1
  105. package/studio/web/assets/{javascript-CJ0G3AFZ.js → javascript-BWtCFuOt.js} +1 -1
  106. package/studio/web/assets/{jsonMode-DQADAYEa.js → jsonMode-Buzzbv9y.js} +1 -1
  107. package/studio/web/assets/{languageServices-CAQJXWcI.js → languageServices-SqsFWfTM.js} +1 -1
  108. package/studio/web/assets/{lspLanguageFeatures-CCQ8P5sY.js → lspLanguageFeatures-EMV5cmjo.js} +1 -1
  109. package/studio/web/assets/{notifications-Cvb3P1lB.js → notifications-CAB-LZWT.js} +1 -1
  110. package/studio/web/assets/{notifications-BbxTU6Aw.js → notifications-DRzmSRxM.js} +1 -1
  111. package/studio/web/assets/{profile-Yyb7gsvL.js → profile-BNKAl79n.js} +1 -1
  112. package/studio/web/assets/{regions-OgjGHlU5.js → regions-CUow_Zw2.js} +1 -1
  113. package/studio/web/assets/{register-6qwNEOY3.js → register-Dkt3WUMp.js} +2 -2
  114. package/studio/web/assets/{setComponentFile-BilDMtgB.js → setComponentFile-BZRfMD0N.js} +1 -1
  115. package/studio/web/assets/{setup-J6qJ7OIU.js → setup-D_yiEPO2.js} +2 -2
  116. package/studio/web/assets/{status-0RWGcfyD.js → status-DhHh1Ge-.js} +1 -1
  117. package/studio/web/assets/{toggleHighContrast-BIn-vErT.js → toggleHighContrast-D7L1PDtV.js} +1 -1
  118. package/studio/web/assets/{tsMode-DgUXku4d.js → tsMode-CCwLk1YS.js} +1 -1
  119. package/studio/web/assets/{typescript-C9orXcsM.js → typescript-BP1j1mjn.js} +1 -1
  120. package/studio/web/assets/{useEntityRestURL-BEoXXbUB.js → useEntityRestURL-D7bnYxLw.js} +1 -1
  121. package/studio/web/assets/{workers-JVzSDmgx.js → workers-tOuCNT17.js} +1 -1
  122. package/studio/web/assets/{xml-Cq-S8S4X.js → xml-BSG_3mQT.js} +1 -1
  123. package/studio/web/assets/{yaml-sfoRdh1M.js → yaml-DxiLprBB.js} +1 -1
  124. package/studio/web/index.html +1 -1
  125. package/utility/DESIGN.md +55 -0
  126. package/utility/logging/harper_logger.ts +1 -0
package/server/DESIGN.md CHANGED
@@ -372,3 +372,267 @@ The `@table(cacheControl:)` value is persisted on the primary-key attribute (lik
372
372
  - New protocol plugins implement the `Server` interface (in `Server.ts`) and register via `onRequest`/`onUpgrade`/`onWebSocket`.
373
373
  - Always pass `name` when registering a listener with `before`/`after` — anonymous entries can't be ordered against.
374
374
  - Tests live in `../unitTests/server/`.
375
+
376
+ ---
377
+
378
+ ## The dispatched API operation is carried on async context, never on the request (`server/serverHelpers/operationAuthorizationState.ts`)
379
+
380
+ `verifyPermsAST`'s token-scope check has to be told which top-level API operation the caller
381
+ invoked, because the scope is written in that namespace (`sql`, `export_local`, ...). Two things
382
+ make that awkward:
383
+
384
+ 1. On the **direct-SQL** path, the object handed to `checkASTPermissions` _is_ the client's request
385
+ body, and this check is the only gate there (`chooseOperation`'s `sql` branch is mutually
386
+ exclusive with its `verifyPerms` call). Any field read off that object is therefore a way to
387
+ name whichever operation the caller's scope happens to allow and run arbitrary SQL under it.
388
+ `jsonMessage.operation` is safe only because dispatch already routed on that same field, so it
389
+ cannot disagree with the operation running. Never add another.
390
+ 2. A **job** re-parses its SQL from the nested `search_operation` in a _different_ async context —
391
+ `executeJob` persists the request and hands off to the job runner, and `jobProcess.ts` re-enters
392
+ from the `hdb_job` record. So a store established around the originating request cannot reach
393
+ it, and the re-parse would be judged as `sql` rather than as the job's own operation.
394
+
395
+ The carrier is therefore established **in the job worker**, by `runWithDispatchedOperation`, from
396
+ the same `request.operation` that `getOperationFunction` just resolved the handler from. That
397
+ identity is the whole basis for trusting it: the value naming the operation and the value selecting
398
+ the code cannot diverge. A new carrier must preserve that property — an added request property, a
399
+ `search_operation` field, or a persisted `parsed_sql_object` would not.
400
+
401
+ This lives in the same `AsyncLocalStorage` as the auth bypass rather than a second store, so
402
+ `processAST` reads the state once. `runWithOperationAuthorizationBypass` **preserves** an existing
403
+ carrier on both branches. That is deliberate and was initially got wrong: its enforced branch is not
404
+ a bypass, so a job handler dispatching a nested _authorized_ operation lands there, and dropping the
405
+ carrier would judge that job's re-parsed SQL as the inner `sql` and refuse it partway through its own
406
+ work. The consequence to know is the other direction — a nested dispatch inside a job is judged
407
+ against the **outer** job's operation for any `evaluateSQL` that does not pass through
408
+ `chooseOperation`. It allocates only when a carrier is present; with none, two shared frozen objects
409
+ serve the common path. All four stores are frozen, so `getOperationAuthorizationState()` cannot hand
410
+ a mutable one to a caller.
411
+
412
+ It has four call sites, and they are not all dispatch wrappers: `server.operation()`
413
+ (`serverUtilities.ts`), the ITC path (`registeredOperations.ts`), the legacy SQL engine
414
+ (`sqlEngine/diff/differential.ts`), and Harper's own `hdb_job` query (`server/jobs/jobs.ts`) — that
415
+ last one **is** reached from the ops-API dispatch, via `search_jobs_by_start_date` →
416
+ `handleGetJobsByStartDate` → `getJobsInDateRange`.
417
+
418
+ Harper's own internal SQL takes the bypass, not the carrier. `getJobsInDateRange` runs a fixed
419
+ `system.hdb_job` query through `evaluateSQL` beneath a handler the caller was already authorized for,
420
+ and `SqlSearchObject` hardcodes `operation: 'sql'` — so the same mismatch applies, but the answer
421
+ differs, and the reason is easy to get backwards. `verifyPermsAST`'s super_user early return is
422
+ `isSuperUser && !isSuSystemOperation`, so a `system` schema is **exempt** from it and the table check
423
+ genuinely runs. A carrier would therefore put Harper's own query through `hasPermissions` on
424
+ `system.hdb_job`, which passes only because `appendSystemTablesToRole` grants `system.*.read` to a
425
+ hydrated super_user — a super_user principal without an appended `permission.system` (an
426
+ impersonation payload, or any path that skips user-cache hydration) would start getting 403s on an
427
+ operation it is entitled to. The bypass also states the actual intent: the statement is Harper's, not
428
+ the caller's. Wrap the individual statement, not the function — a later caller-dependent statement
429
+ must not inherit it.
430
+
431
+ A second body field has to be neutralized for any of this to hold: `evaluateSQL` trusts a supplied
432
+ `parsed_sql_object` verbatim and skips parsing, `chooseOperation` overwrites only the **top-level**
433
+ one, and `dataLayer/export.ts` hands the nested `search_operation` straight to `evaluateSQL`. So a
434
+ body-supplied `search_operation.parsed_sql_object` carrying `permissions_checked: true` would run an
435
+ arbitrary AST with the check skipped. `chooseOperation` deletes it, forcing the worker to re-parse
436
+ from the `sql` string that dispatch authorized — the nested object is never overwritten the way the
437
+ top-level one is, because nothing downstream should read one at all.
438
+
439
+ What is untestable is not the carrier's contract — unit tests cover that by calling
440
+ `runWithDispatchedOperation` directly — but that `jobProcess` is what establishes it. Delete that call
441
+ and those tests stay green. The carrier only changes an outcome through `tokenScopeDenial`, which is
442
+ inert unless the principal carries `tokenOperations`, and that property has exactly one origin: an
443
+ OIDC trust-policy exchange, for which there is no integration harness.
444
+
445
+ Three different mechanisms are easy to conflate here. `tokenOperations` above is the **OIDC token
446
+ operation scope** (#2174). An **inline-role scoped token** (`create_authentication_tokens` with a
447
+ `role` object) is not the same thing and cannot substitute, because `createScopedToken` mints it
448
+ `super_user: false`, so it cannot invoke a `requires_su` operation such as `export_local` at all.
449
+ **Table permissions** are a third, and also cannot substitute — see the system-schema exemption
450
+ above. See #2298.
451
+
452
+ ## `universalHeaders` (`http.securityHeaders`): ownership, precedence, and per-thread scope
453
+
454
+ `server/http.ts` exports `universalHeaders: [string, string][]`, applied to responses in the
455
+ Node, Bun, and uWS (`#914`, `HARPER_UWS_HTTP`) request handlers alike. `http.securityHeaders`
456
+ config populates it via `applySecurityHeaders()`, called from `handleApplication()` on load and
457
+ on `scope.options.on('change', ...)`. Three invariants to preserve:
458
+
459
+ - **Ownership tracking.** Other components may push entries onto the same shared array, so a
460
+ hot-reload can't clear-and-rebuild it. `applySecurityHeaders` tracks the exact `[name, value]`
461
+ tuples it previously pushed in a module-level `ownedSecurityHeaders` array and splices only
462
+ those out (by reference, via `indexOf`) before re-adding the new set. Any future feature that
463
+ pushes into `universalHeaders` from a hot-reloadable source should follow the same "track what
464
+ I added, only remove what I added" pattern.
465
+ - **Root scope owns the config.** `'http'` is a `TRUSTED_RESOURCE_PLUGINS` key, so an application
466
+ `config.yaml` with an `http:` block re-invokes `handleApplication`. A module-level guard makes
467
+ only the _first_ invocation (the root config, which loads before applications) own
468
+ `applySecurityHeaders` and its change listener; later invocations still refresh `httpOptions`
469
+ but cannot wipe root-configured headers.
470
+ - **App wins on conflicts.** Universal headers are _defaults_: `applyUniversalHeaders()` (a shared
471
+ helper used by all three transports) only sets a header when `has(name)` is false, and the
472
+ direct-to-`nodeResponse` paths (handlesHeaders, error) check `hasHeader` first. A route that sets
473
+ `X-Frame-Options: DENY` is never loosened by a configured `SAMEORIGIN`. Response paths covered:
474
+ normal writeHead, `handlesHeaders` streams (e.g. the static component's `send()`, which writes
475
+ its own headers directly — universal headers are pre-set on `nodeResponse` / the Bun
476
+ `responseHeaders` shim so the stream can still override its own names), the thrown-error path,
477
+ and the `status === -1` cascade — on Node via the Fastify `'unhandled'` event bridge, on Bun/uWS
478
+ via `injectToFastify` (or the bare-404 fallback when no Fastify instance is registered for the
479
+ port). Each `status === -1` branch builds a **fresh** `Headers` object from the fallback
480
+ response rather than reusing the request's original `headers`, so `applyUniversalHeaders()` must
481
+ be called again on whichever object actually gets returned — applying it only once, before the
482
+ `status === -1` branch, is a trap that silently drops universal headers on every unhandled/404
483
+ response. CI first caught this on the uWS shard (the integration suite's only unauthenticated
484
+ 404 case landed there); the same bug existed unnoticed on Bun's parallel `status === -1`
485
+ branches (`getBunHTTPServer`'s bare-404 return and `bunDelegateToNodeServer`'s two `Response`s)
486
+ and is fixed alongside it in `harper-1568-fix2`.
487
+
488
+ **Why the operations API doesn't get these headers in normal mode**: ops requests _do_ flow
489
+ through the Harper-native `requestHandler` (`httpServer()` calls `getServer()` for every
490
+ registration, including Fastify's non-function listener) and cascade to Fastify via the
491
+ `status === -1` branch, which copies `response.headers` onto `nodeResponse`. But the ops API runs
492
+ on the **main thread**, and the main thread loads components with `resources.isWorker = false`
493
+ (`server/loadRootComponents.js`), so the componentLoader's `resources.isWorker &&
494
+ extensionModule.handleApplication` gate (`components/componentLoader.ts`) means http's
495
+ `handleApplication` never runs there — the main thread's `universalHeaders` array stays empty.
496
+ `universalHeaders` is per-thread module state, populated only where the http component loads.
497
+ Corollary: with `threads: 0` the ops API shares the worker where `handleApplication` _did_ run,
498
+ so ops responses **will** carry the headers there (benign).
499
+
500
+ ## Under Bun, the main HTTP port is served by `node:http`, not `Bun.serve`
501
+
502
+ Worth knowing before debugging anything Bun-specific on the HTTP path: `getBunHTTPServer()` builds
503
+ the `Bun.serve()` fetch config, but `onWebSocket()` calls `getHTTPServer()` unconditionally — it has
504
+ a uWS branch and no Bun branch, because Bun native WebSockets are unimplemented (nothing ever sets
505
+ `config.websocket`, so WS relies on the Node `ws` server attached to an `http.Server`). MQTT's
506
+ `handleApplication` registers WS on the default port before REST's `httpServer()` call for that same
507
+ port, so `httpServers[port]` is already a Node server by then and `getBunHTTPServer` early-returns
508
+ without registering a serve config. The port is bound by `registerServer()`'s Node server via
509
+ `listenOnPortsBun`'s trailing "non-HTTP servers" loop, and the fetch handler is never invoked for it
510
+ (only the exclusive operations port reaches `Bun.serve`). Consequence: on Bun the `Request`/`Response`
511
+ fetch path is dead code for the main port, and its divergences show up as `node:http`-emulation
512
+ divergences instead.
513
+
514
+ One such divergence, `#2210`: Bun's `node:http` never derives keep-alive from the request. For a
515
+ `Connection: close` request `shouldKeepAlive` stays `true`, and neither a `Connection: close` response
516
+ header nor `response.socket.end()` closes the connection — a **stream-ended** response (an async
517
+ source ended through `pipeline()`; a direct `response.end()` is fine) delivers its full body and
518
+ terminal chunk, then holds the connection until Bun's own idle timeout — a chunked-aware client
519
+ completes the message and can walk away, but the un-honored close still violates RFC 9112 §9.6 and
520
+ strands the socket; a raw client waiting on the FIN (and the HTTP/1.0 case below, which has no
521
+ terminal chunk to stop at) hangs outright. An HTTP/1.0 client hangs the same way
522
+ without asking to close at all, since 1.0 persistence needs both an explicit `keep-alive` and a length
523
+ to read to — so a 1.0 response that got no `Content-Length` is close-delimited, the same line Node
524
+ draws (Node closes it at ~7ms; Bun never does). An explicit `close` token wins over `keep-alive` on
525
+ both versions. A 1.0 `keep-alive` request whose response _did_ get a
526
+ `Content-Length` (`body.size` on a blob, `server/http.ts:698-709`) is left open, which is again what
527
+ Node does and what Bun then handles correctly.
528
+
529
+ `pipeBodyToResponse()` therefore ends `request.socket` itself for those shapes
530
+ (`endConnectionIfClientExpectsClose`, `isBun`-gated, HTTP/1 only, clean path only — the error path
531
+ already closes because `pipeline()` destroys the response with the stream error). Ending the
532
+ _request's_ socket is the only remedy that works after a clean stream end on Bun: a `Connection:
533
+ close` response header, `response.socket.end()` and `response.destroy()` were all measured as no-ops
534
+ there. `socket.end()` is graceful, so it does not truncate — 8 MB over plain TCP and 6 MB over TLS to
535
+ a deliberately slow reader each arrive whole. The
536
+ `Content-Length` check reads `response.hasHeader()`, which Bun populates from the `writeHead(status,
537
+ headers)` fast path this file uses (Node does not, but the branch is Bun-only). A
538
+ keep-alive arm pins the other direction (such a client keeps its connection and reuses it); the two
539
+ HTTP/1.0 arms are Node/Bun-only, because uWS does not route an HTTP/1.0 request to the resource at
540
+ all.
541
+
542
+ ## Per-worker UDS mirrors are separate server instances — port-keyed wiring does not reach them (`server/http.ts`)
543
+
544
+ With `tls.unixDomainSockets: true`, every secure port gets a per-worker cleartext mirror
545
+ (`<worker>-<port>.sock`) so a fronting proxy (symphony) can terminate TLS and route to a specific
546
+ worker. The mirror is a **separate** `http.Server` instance registered in `SERVERS[udsPath]` — it is
547
+ _not_ `httpServers[port]` — so anything wired by port key (upgrade listeners, uWS `wsHandler`,
548
+ mTLS flags, socket options) must be explicitly propagated to it. `getHTTPServer()` exposes the
549
+ mirror as `server.udsMirror` (Node) / `server.udsMirrorUwsConfig` (HARPER_UWS_UDS) for exactly this;
550
+ `onWebSocket()` uses those to attach the `'upgrade'` dispatch and uWS `wsHandler`. Two lessons paid
551
+ for in production (WS handshakes died with a zero-byte close on the mirrors while SSE worked):
552
+
553
+ - A Node HTTP server with **no** `'upgrade'` listener destroys upgrade sockets with no response and
554
+ no log — a silent per-server default that makes a missing listener look like a network problem.
555
+ - `enableProxyProtocol()`'s data interception must hand the socket **back to the original
556
+ listeners** once the PROXY header decision is made (it re-attaches them and removes its wrapper).
557
+ A permanent wrapper breaks protocol handoffs: Node's upgrade path removes its parser's `'data'`
558
+ listener _by reference_ before ws takes over, so a lingering wrapper keeps feeding the freed HTTP
559
+ parser — which the parser pool can re-issue to another connection, injecting one connection's
560
+ WS frames into another's request stream (`Parse Error: Data after 'Connection: close'`).
561
+
562
+ The h2c mirror (`HARPER_H2C_UDS`) is exempt: HTTP/1.1 `Upgrade` doesn't exist in h2, and the
563
+ fronting proxy routes WS to the h1 mirror by ALPN.
564
+
565
+ Known limitation on uWS-served transports (`HARPER_UWS_HTTP` ports, `HARPER_UWS_UDS` mirrors):
566
+ uWS accepts WebSocket handshakes natively in `app.ws()`, so `server.upgrade()` middleware never
567
+ runs pre-handshake there (auth is unaffected — it runs in the WS connection chain on both paths,
568
+ matching Node's upgrade-then-authorize order). No core component registers custom upgrade
569
+ middleware; `onUpgrade()`/`installUwsWsHandler()` warn when one is registered for a uWS-served
570
+ port so the gap is visible instead of silent.
571
+
572
+ ## A worker that misses an ITC ack gets its OS thread state logged (`server/threads/manageThreads.js`)
573
+
574
+ `broadcastWithAcknowledgement` already times out (30 s) on a worker whose port stays open but never acks, and that shape is almost always a blocked event loop — a native lock, a runaway synchronous call — which nothing inside the worker can report (harper-pro#788: a restarted node's single http worker went byte-silent while main kept serving `cluster_status`, and the app log only said "not acknowledged by worker thread(s) 2"). So each worker posts its Linux thread id (`readlink /proc/thread-self`) to main once at startup, before anything else runs on it, and the timeout branch reads that thread's kernel state from `/proc/self/task/<tid>`: state, `wchan`, the syscall number (the first token only — the rest of that file is argument registers and stack/instruction pointers), CPU ticks, and context-switch counts, plus two cross-platform signals main already has, `worker.performance.eventLoopUtilization()` and the age of the last 1 s resource report. It samples again a second later and logs the deltas: no CPU ticks, no context switches and `event loop active +1000ms` is "parked on a lock"; ticks climbing with state `R` is "spinning". It is deliberately main-thread-only and best-effort: `workers` and the tid live on the main thread's `Worker` objects, every `/proc` field is reported individually (a hardened container may deny `wchan`/`syscall` while `stat` stays readable), a follow-up sample whose `starttime` differs from the first is discarded (the tid may have been recycled), one diagnostic runs per worker with a 30 s cooldown so concurrent timeouts on the same worker don't multiply reads, and nothing here runs when acks arrive on time. It does not name the lock owner; that still needs a native stack from the next occurrence.
575
+
576
+ ## `chooseOperation` authorizes the invoked operation against the authenticated principal (`server/serverHelpers/serverUtilities.ts`)
577
+
578
+ `verifyPerms` takes a request-shaped object and reads _both_ halves of the permission question off it: the principal from `hdb_user`, and the tables from `schema`/`database`/`table`/`records`. `chooseOperation` used to hand it `json.search_operation` — a caller-supplied field — which made both halves body-controlled. Fixing one half and not the other is not a fix: with an empty `search_operation` the table map is empty, and `hasPermissions` iterating nothing authorizes everything. Regression cover: `integrationTests/security/choose-operation-authz.test.ts`.
579
+
580
+ Four rules hold this together, and all four are load-bearing:
581
+
582
+ **The principal comes from authentication.** Authentication sets only the _top-level_ `hdb_user`, and `validateRequestBodyProperties` inspects only top-level keys, so a nested `hdb_user` must be overwritten, never backfilled `if (!...)`. All four callers of `chooseOperation` (`serverHandlers`, `serverUtilities.operation`, `registeredOperations` worker forwarding, MCP) set the top-level principal before dispatch, which is why this belongs here rather than only at the HTTP boundary.
583
+
584
+ **`search_operation` is the permission subject only for the operations that consume it.** `dataLayer/export.ts` is its sole consumer (`export_local`, `export_to_s3`); for any other operation the substitution checks the nested tables while the handler runs against the top-level ones, so it is gated on the operation name. It must also be an object naming one of export's supported operations (`search_by_value`/`search_by_hash`/`search_by_conditions`/`sql`) — a primitive, `{}`, or an unsupported operation is a request-time 400, not a wrapped 500 or an asynchronously-failed job.
585
+
586
+ **One check cannot authorize both the outer export and its nested query.** The outer op's own `verifyPerms` returns before any table check — `export_local`/`export_to_s3` are `requires_su`, and a role that lists the operation in `operations` is granted at gate 2 (an explicit listing of an SU-only operation is a deliberate grant). The job worker then runs `search_operation` through `searchByValue`/`searchByHash`/`searchByConditions`, none of which check permissions. So the outer invocation is authorized first, and then the nested search is authorized additively against its _real_ search handler (`getOperationFunction(search_operation)`) and the authenticated principal — otherwise a role granted `export_local` could export a table it holds no grant on. A nested `sql` search takes the SQL branch instead, but the same two-part shape holds: the outer export op runs through `verifyPerms` (so its `requires_su` gate, the `operations` allowlist, and the export token scope all apply, exactly as on the non-SQL path — SQL must not be a way around the requires_su gate), the statement must be a `SELECT` because export is read-only, and `checkASTPermissions` then covers the statement's tables. A direct `sql` call has no outer job op, so there the `operations` allowlist alone is the operation-invocation check.
587
+
588
+ **`parsed_sql_object` is dispatch state, never client input.** The export worker re-reads it off the same caller-supplied nested object (`evaluateSQL`), and it carries `permissions_checked`, so a body-supplied one runs an AST no check ever saw. It is deleted from the nested object at dispatch, and stripped from the top-level object before this dispatch's own parse is assigned. Only the direct-SQL path consumes the top-level `parsed_sql_object`; a job re-parses off `search_operation`, so setting it for a job would be inert. The bypass/`apiOperation` decision is carried on async-context state (`getOperationAuthorizationState`), not on the request body, and `processAST` honors the denial `checkASTPermissions` computes — a `PermissionResponseObject` has no `length`, so the guard tests the object itself rather than `.length` (which always refused nothing).
589
+
590
+ The SQL and job paths are additive rather than exclusive: `verifyPermsAST` validates only the statement's tables and attributes, never the `operations` allowlist or `requires_su`, and a table-free statement gives it nothing to validate — so the allowlist check and the AST check both run for a SQL-carrying request, and the nested-search check runs alongside the outer export check for a job.
591
+
592
+ ## `withNodeAdapter()`'s response is the body `PassThrough` it resolves with (`server/serverHelpers/NodeAdapterResponse.ts`)
593
+
594
+ `Request.withNodeAdapter(handler)` gives third-party Node middleware an `IncomingMessage`/`ServerResponse` pair and resolves `{ status, headers, body }` once headers are committed. The response is `NodeAdapterResponse extends PassThrough`, and that same stream is the resolved `body`: `write()`'s return value, `'drain'`, `'finish'`, `'close'`, `writableEnded`/`writableFinished` and destroy propagation are Node's own rather than events forwarded from a second stream, which is what `Readable.pipe`, `compression`'s buffered `res.on('drain')` and Next.js's response writer depend on past the high-water mark (#2527). Invariants that middleware exercises and the unit test `unitTests/server/serverHelpers/nodeAdapterMiddleware.test.js` pins against the real `compression` (1.8 and the 1.7.4 Next.js vendors), `send`, `on-finished` and `on-headers`:
595
+
596
+ - **Headers commit exactly once, through `this.writeHead`.** `write()`, `end()`, `flushHeaders()` and `_implicitHeader()` all reach `this.writeHead(this.statusCode)` by property lookup, so a `writeHead` that `on-headers` replaced on the instance runs its listeners (the ones that set `Content-Encoding` and remove `Content-Length`) before the promise resolves. After commit, `setHeader`/`appendHeader`/`removeHeader` and a second `writeHead` throw `ERR_HTTP_HEADERS_SENT` as Node's do; `_header` (which `compression` ≤ 1.7 tests instead of `headersSent`) and `finished` (which `on-finished` tests) derive from that state.
597
+ - **The adapter owns the `'error'` listener.** A `destroy(err)` right after `writeHead()` emits before the awaiting caller can attach one; the error stays in the stream's `errored` state for `pipeline()`, `finished()` or async iteration. Client disconnect (`Request.signal`) destroys the response without an error after headers (a plain premature close, which `pipeBodyToResponse` treats as routine) and rejects the promise with the abort reason before them; a handler that throws or rejects before ending the response destroys it, and one that fails after `end()` is logged at warn.
598
+ - **Header names are case-insensitive on removal too.** `Headers.delete` lowercases like `set`/`get`/`has`; the inherited `Map.delete` silently left `send`'s `Content-Length` on a gzip body (truncated transfers).
599
+ - **Express is not a target.** `express`'s `app.handle()` replaces the response's prototype with one rooted at `http.ServerResponse.prototype`, which no Writable-derived response survives, and would do the same to the request `Proxy`'s target, Harper's real `IncomingMessage`. Middleware that duck-types the response (Next.js, `compression`, `send`, `serve-static`, `finalhandler`, h3, fastify) is the supported surface.
600
+
601
+ ## `manageThreads` has two different `workerCount`s (`server/threads/manageThreads.js`)
602
+
603
+ The module-global `let workerCount` and the per-worker `workerData.workerCount` share a name and
604
+ nothing else. `getWorkerCount()` (and therefore the `server.workerCount` a component reads) resolves
605
+ only `workerData.workerCount`, frozen at spawn — it never reads the global; on the main thread it
606
+ answers `isMainWorker ? 1 : undefined`. The global's one and only reader is `restartWorkers`' default
607
+ `maxWorkersDown = Math.max(Math.floor(workerCount / 8), 1)`, so it is the _serving topology_ the
608
+ rolling-restart throttle is sized from, nothing more.
609
+
610
+ That makes the global writable only by a start that declares the topology. It used to be assigned
611
+ unconditionally from `options.threadCount` inside the `workerData` literal, so every job worker — which
612
+ passes no `threadCount` — set it to `undefined`, and the next rolling restart computed `NaN` (harper#2491).
613
+ `NaN` defeats the guard below it (`NaN < 1` is false) _and_ every throttle comparison, so the restart
614
+ took the whole pool down at once. A string does the same thing for the same reason. So the fix is the conditional write plus a consumer guard that clamps anything not a usable number. `Infinity` is exempt: it is the deliberate "all at once" sentinel `shutdownWorkers` passes, and `shutdownWorkersNow` depends on it to mark every worker synchronously before the first await. A literal `0` is also left alone, because it reads as a ratio rather than as garbage — it still reaches the ratio branch and stops the restart after one worker (harper#2601).
615
+
616
+ `workerData.workerCount` must stay exactly what it is for each start, `undefined` for job workers
617
+ included: an earlier attempt to give job workers the serving count instead broke the Windows
618
+ integration shard with ECONNREFUSED across the job tests. A job worker that believes it is part of the
619
+ pool behaves differently.
620
+
621
+ ## A WebSocket close reason must be bounded to 123 bytes (`server/serverHelpers/webSocketCloseReason.ts`)
622
+
623
+ `ws` throws a `RangeError` when a close reason exceeds 123 bytes — the control-frame payload minus the
624
+ status code — and every close site Harper has reaches it from a rejection handler, where that throw
625
+ surfaces as an unhandled rejection rather than a failed close. Two of the reasons are outside Harper's
626
+ control: a `server.getUser` override's rejection text, and `request.pathname` in REST's no-resource
627
+ close. So every `ws.close()` carrying a dynamic reason goes through `toCloseReason()`, which truncates
628
+ on a code-point boundary (harper#2703).
629
+
630
+ The `ClassName: message` in a close reason is deliberate and not something to sanitize: the class name is
631
+ Harper's error code, and `errorToString` is the correct renderer for client-visible error text — see
632
+ AGENTS.md, "An error's class name is its error code". Only an internal fault's _message_ is replaced, by
633
+ `AUTHENTICATION_ERROR_MSGS.GENERIC_AUTH_FAIL`. The three terminal HTTP handlers (Node and Bun in
634
+ `server/http.ts`, uWS in `server/serverHelpers/uwsServer.ts`) must agree on that rendering; uWS rendered
635
+ the bare message until harper#2703, so the same error carried an error code on two runtimes and not the third.
636
+
637
+ REST settles a credential rejection _before_ its route lookup, so a rejected client gets the unauthorized
638
+ close rather than `1011 No resource was found` — which would otherwise disclose whether the resource exists.
package/server/REST.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  assertNoDeferredCredentialRejection,
26
26
  settleDeferredCredentialRejection,
27
27
  } from '../security/deferredAuthentication.ts';
28
+ import { toCloseReason } from './serverHelpers/webSocketCloseReason.ts';
28
29
 
29
30
  import { Request } from '../server/serverHelpers/Request.ts';
30
31
  import { RequestTarget } from '../resources/RequestTarget';
@@ -585,14 +586,16 @@ export function handleApplication(scope: import('../components/Scope.ts').Scope)
585
586
  });
586
587
  try {
587
588
  await chainCompletion;
589
+ // before the route lookup: the same credential is a 401 over HTTP, so a rejected client
590
+ // must not learn from the close code whether the resource exists
591
+ assertNoDeferredCredentialRejection(request);
588
592
  const url = request.url.slice(1);
589
593
  const entry = resources.getMatch(url, 'ws');
590
594
  recordActionBinary(Boolean(entry), 'connection', 'ws', 'connect');
591
595
  if (!entry) {
592
596
  // TODO: Ideally we would like to have a 404 response before upgrading to WebSocket protocol, probably
593
- return ws.close(1011, `No resource was found to handle ${request.pathname}`);
597
+ return ws.close(1011, toCloseReason(`No resource was found to handle ${request.pathname}`));
594
598
  } else {
595
- assertNoDeferredCredentialRejection(request);
596
599
  request.handlerPath = entry.path;
597
600
  recordAction(
598
601
  (action) => ({
@@ -632,7 +635,7 @@ export function handleApplication(scope: import('../components/Scope.ts').Scope)
632
635
  ws.close(
633
636
  HTTP_TO_WEBSOCKET_CLOSE_CODES[error.statusCode] || // try to return a helpful code
634
637
  1011, // otherwise generic internal error
635
- errorToString(error)
638
+ toCloseReason(errorToString(error))
636
639
  );
637
640
  }
638
641
  ws.close();
package/server/mqtt.ts CHANGED
@@ -19,8 +19,10 @@ import { forComponent as loggerForComponent } from '../utility/logging/harper_lo
19
19
  import { EventEmitter } from 'events';
20
20
  import { verifyCertificate } from '../security/certificateVerification/index.ts';
21
21
  import { registerShutdownDrain } from '../components/shutdownDrain.ts';
22
+ import { toCloseReason } from './serverHelpers/webSocketCloseReason.ts';
22
23
  import {
23
24
  assertNoDeferredCredentialRejection,
25
+ getAuthenticationRejectedInPlace,
24
26
  getDeferredCredentialRejection,
25
27
  } from '../security/deferredAuthentication.ts';
26
28
 
@@ -86,10 +88,10 @@ export function handleApplication(scope: import('../components/Scope.ts').Scope)
86
88
  });
87
89
  authenticated.catch((error) => {
88
90
  mqttLog.info?.('Closing MQTT WebSocket connection, authentication was rejected', error);
89
- ws.close(
90
- WEBSOCKET_UNAUTHORIZED_CLOSE_CODE,
91
- getDeferredCredentialRejection(request)?.message ?? 'Unauthorized'
92
- );
91
+ // read the records rather than the error: both hold a client-safe message, while an
92
+ // arbitrary chain rejection landing here would not
93
+ const rejection = getDeferredCredentialRejection(request) ?? getAuthenticationRejectedInPlace(request);
94
+ ws.close(WEBSOCKET_UNAUTHORIZED_CLOSE_CODE, toCloseReason(rejection?.message ?? 'Unauthorized'));
93
95
  });
94
96
  const { onMessage, onClose } = onSocket(
95
97
  ws,
@@ -89,9 +89,9 @@ export type OperationFunctionName = ValueOf<typeof terms.OPERATIONS_ENUM>;
89
89
  * name — still stripped, since this runs ahead of the validation that now rejects it). `value` /
90
90
  * `values` carry .env secrets from set_env_value; `value` / `envelope` carry secrets from
91
91
  * set_secret. `token` is the login-purpose token (login) and the CI identity token
92
- * (exchange_oidc_token), and is also stripped defensively — no operation declares a top-level `token`,
93
- * but validation allows unknown keys, so a mistyped `harper deploy setup token=…` must not log a live
94
- * credential. `refresh_token` is the 30-day credential (refresh_operation_token).
92
+ * (exchange_oidc_token), and is also stripped defensively — no operation declares a top-level
93
+ * `token`, but validation allows unknown keys, so a mistyped `harper deploy setup token=…` must not
94
+ * log a live credential. `refresh_token` is the 30-day credential (refresh_operation_token).
95
95
  *
96
96
  * Redaction runs *before* the handler, so a rejected request logs a still-spendable credential —
97
97
  * which is why a new secret-bearing field belongs here rather than left to the default (harper#1527
@@ -111,10 +111,21 @@ export const UNLOGGABLE_OPERATION_FIELDS = [
111
111
  'refresh_token',
112
112
  ];
113
113
 
114
+ const UNLOGGABLE_FIELDS_BY_OPERATION = new Map<string, ReadonlySet<string>>([
115
+ ['add_ssh_key', new Set(['key'])],
116
+ ['update_ssh_key', new Set(['key'])],
117
+ ]);
118
+
114
119
  /** Callers gate this on log level: it allocates, and the operations log is often off. */
115
120
  export function redactForOperationLog(body: Record<string, any>): Record<string, any> {
116
121
  const clean = { ...body };
117
122
  for (const field of UNLOGGABLE_OPERATION_FIELDS) delete clean[field];
123
+ const operationFields = UNLOGGABLE_FIELDS_BY_OPERATION.get(body.operation);
124
+ if (operationFields) {
125
+ for (const field of Object.keys(clean)) {
126
+ if (operationFields.has(field.toLowerCase())) delete clean[field];
127
+ }
128
+ }
118
129
  return clean;
119
130
  }
120
131
 
@@ -24,6 +24,7 @@ import { UwsRequest, UwsRequestBody } from './Request.ts';
24
24
  import { Headers } from './Headers.ts';
25
25
  import { when } from '../../utility/when.ts';
26
26
  import { ClientError } from '../../utility/errors/hdbError.ts';
27
+ import { errorToString } from '../../utility/logging/harper_logger.ts';
27
28
 
28
29
  // uWS has no npm package; it's installed from a GitHub tag and is platform/ABI-specific.
29
30
  // Imported lazily so harper builds/loads on platforms without a uWS binary.
@@ -160,7 +161,9 @@ export async function createUwsServer(options: UwsServerOptions): Promise<{ app:
160
161
  res.cork(() => {
161
162
  res.writeStatus(statusText(status));
162
163
  res.writeHeader('content-type', 'text/plain');
163
- res.end(String((error && error.message) || error));
164
+ // the same renderer as the Node and Bun terminal handlers: the class name is the
165
+ // error code a client branches on, and it must not depend on the runtime
166
+ res.end(errorToString(error));
164
167
  });
165
168
  }
166
169
  );
@@ -0,0 +1,25 @@
1
+ // A close frame's payload is at most 125 bytes, two of which are the status code, and `ws` throws a
2
+ // RangeError past that (node_modules/ws/lib/sender.js). Both close sites below are reached from a
3
+ // rejection handler, where such a throw would surface as an unhandled rejection rather than a failed
4
+ // close, so a message Harper does not control — a `server.getUser` override's, for instance — must be
5
+ // bounded before it gets there.
6
+ const MAX_CLOSE_REASON_BYTES = 123;
7
+
8
+ /** Bounds `text` to what a close frame accepts, truncating on a code-point boundary. */
9
+ export function toCloseReason(text: string | undefined): string {
10
+ const reason = text ?? '';
11
+ // a UTF-16 code unit never encodes to fewer than one byte, so an over-long string is over the byte
12
+ // limit too — testing that first keeps an unbounded message off byteLength's whole-string scan
13
+ if (reason.length <= MAX_CLOSE_REASON_BYTES && Buffer.byteLength(reason, 'utf8') <= MAX_CLOSE_REASON_BYTES)
14
+ return reason;
15
+ let bytes = 0;
16
+ let end = 0;
17
+ // iterating the string yields whole code points, so a surrogate pair is never split
18
+ for (const character of reason) {
19
+ const size = Buffer.byteLength(character, 'utf8');
20
+ if (bytes + size > MAX_CLOSE_REASON_BYTES) break;
21
+ bytes += size;
22
+ end += character.length;
23
+ }
24
+ return reason.slice(0, end);
25
+ }
@@ -1,4 +1,4 @@
1
- import{r as e,t}from"./rolldown-runtime-hePW80VL.js";import{S as n,_ as r,a as i,b as a,c as o,d as s,f as c,g as l,h as u,l as d,m as f,n as p,o as m,p as h,r as g,s as _,t as v,v as y,x as b,y as x}from"./vendor-core-c2JRRJpV.js";import{i as S,t as C}from"./button-BIsUKRZq.js";import{a as ee}from"./vendor-datadog-CLUcJXOo.js";import{r as te}from"./vendor-react-CJV_K1u4.js";import{L as ne,k as re,q as ie,z as ae}from"./vendor-tanstack-DxzraizX.js";import{n as oe}from"./setSessionStorage-B0bf71m4.js";import{It as se}from"./vendor-ui-BUjK0h8a.js";import{t as w}from"./createLucideIcon-CzW9508A.js";import{n as ce,r as le,t as ue}from"./chevron-up-DtKGqDn3.js";import{c as de,f as fe,g as pe,h as me,i as he,l as ge,m as _e,p as ve,r as ye,t as be,u as xe}from"./setComponentFile-BilDMtgB.js";import{t as Se}from"./x-DIzaLEdK.js";import{n as Ce}from"./setLocalStorage-D_kflv4U.js";import{t as we}from"./useLocalStorage-BqMR3D8_.js";import{An as Te,Dr as Ee,En as De,H as Oe,It as ke,Mr as Ae,Mt as je,Nt as Me,Or as Ne,Pt as Pe,Tr as Fe,V as Ie,Z as Le,br as Re,c as ze,cr as Be,d as Ve,f as He,fr as Ue,ft as We,hr as Ge,in as Ke,m as qe,or as Je,ur as Ye,ut as Xe,yr as Ze,yt as Qe}from"./index-BIXW6Pu4.js";import{t as $e}from"./useEntityRestURL-BEoXXbUB.js";import{n as et,t as tt}from"./FloatingChat-BxJGYcfB.js";import{n as nt}from"./getAnalytics-GHK8ORfM.js";var rt=w(`between-horizontal-start`,[[`rect`,{width:`13`,height:`7`,x:`8`,y:`3`,rx:`1`,key:`pkso9a`}],[`path`,{d:`m2 9 3 3-3 3`,key:`1agib5`}],[`rect`,{width:`13`,height:`7`,x:`8`,y:`14`,rx:`1`,key:`1q5fc1`}]]),it=w(`book`,[[`path`,{d:`M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20`,key:`k3hazp`}]]),at=w(`chart-area`,[[`path`,{d:`M3 3v16a2 2 0 0 0 2 2h16`,key:`c24i48`}],[`path`,{d:`M7 11.207a.5.5 0 0 1 .146-.353l2-2a.5.5 0 0 1 .708 0l3.292 3.292a.5.5 0 0 0 .708 0l4.292-4.292a.5.5 0 0 1 .854.353V16a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1z`,key:`q0gr47`}]]),ot=w(`circle-x`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m15 9-6 6`,key:`1uzhvr`}],[`path`,{d:`m9 9 6 6`,key:`z0biqf`}]]),st=w(`file-pen`,[[`path`,{d:`M12.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v9.34`,key:`o6klzx`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}],[`path`,{d:`M10.378 12.622a1 1 0 0 1 3 3.003L8.36 20.637a2 2 0 0 1-.854.506l-2.867.837a.5.5 0 0 1-.62-.62l.836-2.869a2 2 0 0 1 .506-.853z`,key:`zhnas1`}]]),ct=w(`logs`,[[`path`,{d:`M3 5h1`,key:`1mv5vm`}],[`path`,{d:`M3 12h1`,key:`lp3yf2`}],[`path`,{d:`M3 19h1`,key:`w6f3n9`}],[`path`,{d:`M8 5h1`,key:`1nxr5w`}],[`path`,{d:`M8 12h1`,key:`1con00`}],[`path`,{d:`M8 19h1`,key:`k7p10e`}],[`path`,{d:`M13 5h8`,key:`a7qcls`}],[`path`,{d:`M13 12h8`,key:`h98zly`}],[`path`,{d:`M13 19h8`,key:`c3s6r1`}]]),lt=w(`message-square-heart`,[[`path`,{d:`M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z`,key:`18887p`}],[`path`,{d:`M7.5 9.5c0 .687.265 1.383.697 1.844l3.009 3.264a1.14 1.14 0 0 0 .407.314 1 1 0 0 0 .783-.004 1.14 1.14 0 0 0 .398-.31l3.008-3.264A2.77 2.77 0 0 0 16.5 9.5 2.5 2.5 0 0 0 12 8a2.5 2.5 0 0 0-4.5 1.5`,key:`1faxuh`}]]),ut=w(`send`,[[`path`,{d:`M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z`,key:`1ffxy3`}],[`path`,{d:`m21.854 2.147-10.94 10.939`,key:`12cjpa`}]]),dt=w(`wrench`,[[`path`,{d:`M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z`,key:`1ngwbx`}]]);async function ft(){await S.delete(`/Chat/Messages/`)}var T=e(ee(),1),E=te();function pt({setMessages:e}){let[t,n]=(0,T.useState)(!1),r=(0,T.useCallback)(async()=>{if(!t){n(!0);try{await ft(),e([])}catch(e){console.error(`Failed to clear chat:`,e)}finally{n(!1)}}},[t,e]);return(0,E.jsxs)(`button`,{type:`button`,className:`clear-chat-button gap-1`,onClick:r,disabled:t,title:`Clear chat`,children:[t?(0,E.jsx)(Ze,{className:`animate-spin`,size:18}):(0,E.jsx)(Ye,{size:18}),`Clear`]})}async function mt(){let{data:e}=await S.get(`/Chat/Messages/`);return e}var ht=`vercel.ai.error`,gt=Symbol.for(ht),_t,vt,D=class e extends (vt=Error,_t=gt,vt){constructor({name:e,message:t,cause:n}){super(t),this[_t]=!0,this.name=e,this.cause=n}static isInstance(t){return e.hasMarker(t,ht)}static hasMarker(e,t){let n=Symbol.for(t);return typeof e==`object`&&!!e&&n in e&&typeof e[n]==`boolean`&&e[n]===!0}};function yt(e){return e==null?`unknown error`:typeof e==`string`?e:e instanceof Error?e.toString():JSON.stringify(e)}var bt=`AI_InvalidArgumentError`,xt=`vercel.ai.error.${bt}`,St=Symbol.for(xt),Ct,wt,Tt=class extends (wt=D,Ct=St,wt){constructor({message:e,cause:t,argument:n}){super({name:bt,message:e,cause:t}),this[Ct]=!0,this.argument=n}static isInstance(e){return D.hasMarker(e,xt)}},Et=`AI_JSONParseError`,Dt=`vercel.ai.error.${Et}`,Ot=Symbol.for(Dt),kt,At,jt=class extends (At=D,kt=Ot,At){constructor({text:e,cause:t}){super({name:Et,message:`JSON parsing failed: Text: ${e}.
1
+ import{r as e,t}from"./rolldown-runtime-hePW80VL.js";import{S as n,_ as r,a as i,b as a,c as o,d as s,f as c,g as l,h as u,l as d,m as f,n as p,o as m,p as h,r as g,s as _,t as v,v as y,x as b,y as x}from"./vendor-core-c2JRRJpV.js";import{i as S,t as C}from"./button-BIsUKRZq.js";import{a as ee}from"./vendor-datadog-CLUcJXOo.js";import{r as te}from"./vendor-react-CJV_K1u4.js";import{L as ne,k as re,q as ie,z as ae}from"./vendor-tanstack-DxzraizX.js";import{n as oe}from"./setSessionStorage-B0bf71m4.js";import{It as se}from"./vendor-ui-BUjK0h8a.js";import{t as w}from"./createLucideIcon-CzW9508A.js";import{n as ce,r as le,t as ue}from"./chevron-up-DtKGqDn3.js";import{c as de,f as fe,g as pe,h as me,i as he,l as ge,m as _e,p as ve,r as ye,t as be,u as xe}from"./setComponentFile-BZRfMD0N.js";import{t as Se}from"./x-DIzaLEdK.js";import{n as Ce}from"./setLocalStorage-D_kflv4U.js";import{t as we}from"./useLocalStorage-BqMR3D8_.js";import{An as Te,Dr as Ee,En as De,H as Oe,It as ke,Mr as Ae,Mt as je,Nt as Me,Or as Ne,Pt as Pe,Tr as Fe,V as Ie,Z as Le,br as Re,c as ze,cr as Be,d as Ve,f as He,fr as Ue,ft as We,hr as Ge,in as Ke,m as qe,or as Je,ur as Ye,ut as Xe,yr as Ze,yt as Qe}from"./index-Dm0rfkJ7.js";import{t as $e}from"./useEntityRestURL-D7bnYxLw.js";import{n as et,t as tt}from"./FloatingChat-D_mI-rZ7.js";import{n as nt}from"./getAnalytics-GHK8ORfM.js";var rt=w(`between-horizontal-start`,[[`rect`,{width:`13`,height:`7`,x:`8`,y:`3`,rx:`1`,key:`pkso9a`}],[`path`,{d:`m2 9 3 3-3 3`,key:`1agib5`}],[`rect`,{width:`13`,height:`7`,x:`8`,y:`14`,rx:`1`,key:`1q5fc1`}]]),it=w(`book`,[[`path`,{d:`M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20`,key:`k3hazp`}]]),at=w(`chart-area`,[[`path`,{d:`M3 3v16a2 2 0 0 0 2 2h16`,key:`c24i48`}],[`path`,{d:`M7 11.207a.5.5 0 0 1 .146-.353l2-2a.5.5 0 0 1 .708 0l3.292 3.292a.5.5 0 0 0 .708 0l4.292-4.292a.5.5 0 0 1 .854.353V16a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1z`,key:`q0gr47`}]]),ot=w(`circle-x`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m15 9-6 6`,key:`1uzhvr`}],[`path`,{d:`m9 9 6 6`,key:`z0biqf`}]]),st=w(`file-pen`,[[`path`,{d:`M12.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v9.34`,key:`o6klzx`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}],[`path`,{d:`M10.378 12.622a1 1 0 0 1 3 3.003L8.36 20.637a2 2 0 0 1-.854.506l-2.867.837a.5.5 0 0 1-.62-.62l.836-2.869a2 2 0 0 1 .506-.853z`,key:`zhnas1`}]]),ct=w(`logs`,[[`path`,{d:`M3 5h1`,key:`1mv5vm`}],[`path`,{d:`M3 12h1`,key:`lp3yf2`}],[`path`,{d:`M3 19h1`,key:`w6f3n9`}],[`path`,{d:`M8 5h1`,key:`1nxr5w`}],[`path`,{d:`M8 12h1`,key:`1con00`}],[`path`,{d:`M8 19h1`,key:`k7p10e`}],[`path`,{d:`M13 5h8`,key:`a7qcls`}],[`path`,{d:`M13 12h8`,key:`h98zly`}],[`path`,{d:`M13 19h8`,key:`c3s6r1`}]]),lt=w(`message-square-heart`,[[`path`,{d:`M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z`,key:`18887p`}],[`path`,{d:`M7.5 9.5c0 .687.265 1.383.697 1.844l3.009 3.264a1.14 1.14 0 0 0 .407.314 1 1 0 0 0 .783-.004 1.14 1.14 0 0 0 .398-.31l3.008-3.264A2.77 2.77 0 0 0 16.5 9.5 2.5 2.5 0 0 0 12 8a2.5 2.5 0 0 0-4.5 1.5`,key:`1faxuh`}]]),ut=w(`send`,[[`path`,{d:`M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z`,key:`1ffxy3`}],[`path`,{d:`m21.854 2.147-10.94 10.939`,key:`12cjpa`}]]),dt=w(`wrench`,[[`path`,{d:`M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z`,key:`1ngwbx`}]]);async function ft(){await S.delete(`/Chat/Messages/`)}var T=e(ee(),1),E=te();function pt({setMessages:e}){let[t,n]=(0,T.useState)(!1),r=(0,T.useCallback)(async()=>{if(!t){n(!0);try{await ft(),e([])}catch(e){console.error(`Failed to clear chat:`,e)}finally{n(!1)}}},[t,e]);return(0,E.jsxs)(`button`,{type:`button`,className:`clear-chat-button gap-1`,onClick:r,disabled:t,title:`Clear chat`,children:[t?(0,E.jsx)(Ze,{className:`animate-spin`,size:18}):(0,E.jsx)(Ye,{size:18}),`Clear`]})}async function mt(){let{data:e}=await S.get(`/Chat/Messages/`);return e}var ht=`vercel.ai.error`,gt=Symbol.for(ht),_t,vt,D=class e extends (vt=Error,_t=gt,vt){constructor({name:e,message:t,cause:n}){super(t),this[_t]=!0,this.name=e,this.cause=n}static isInstance(t){return e.hasMarker(t,ht)}static hasMarker(e,t){let n=Symbol.for(t);return typeof e==`object`&&!!e&&n in e&&typeof e[n]==`boolean`&&e[n]===!0}};function yt(e){return e==null?`unknown error`:typeof e==`string`?e:e instanceof Error?e.toString():JSON.stringify(e)}var bt=`AI_InvalidArgumentError`,xt=`vercel.ai.error.${bt}`,St=Symbol.for(xt),Ct,wt,Tt=class extends (wt=D,Ct=St,wt){constructor({message:e,cause:t,argument:n}){super({name:bt,message:e,cause:t}),this[Ct]=!0,this.argument=n}static isInstance(e){return D.hasMarker(e,xt)}},Et=`AI_JSONParseError`,Dt=`vercel.ai.error.${Et}`,Ot=Symbol.for(Dt),kt,At,jt=class extends (At=D,kt=Ot,At){constructor({text:e,cause:t}){super({name:Et,message:`JSON parsing failed: Text: ${e}.
2
2
  Error message: ${yt(t)}`,cause:t}),this[kt]=!0,this.text=e}static isInstance(e){return D.hasMarker(e,Dt)}},Mt=`AI_TypeValidationError`,Nt=`vercel.ai.error.${Mt}`,Pt=Symbol.for(Nt),Ft,It,O=class e extends (It=D,Ft=Pt,It){constructor({value:e,cause:t,context:n}){let r=`Type validation failed`;if(n?.field&&(r+=` for ${n.field}`),n?.entityName||n?.entityId){r+=` (`;let e=[];n.entityName&&e.push(n.entityName),n.entityId&&e.push(`id: "${n.entityId}"`),r+=e.join(`, `),r+=`)`}super({name:Mt,message:`${r}: Value: ${JSON.stringify(e)}.
3
3
  Error message: ${yt(t)}`,cause:t}),this[Ft]=!0,this.value=e,this.context=n}static isInstance(e){return D.hasMarker(e,Nt)}static wrap({value:t,cause:n,context:r}){return e.isInstance(n)&&n.value===t&&n.context?.field===r?.field&&n.context?.entityName===r?.entityName&&n.context?.entityId===r?.entityId?n:new e({value:t,cause:n,context:r})}},Lt=class extends Error{constructor(e,t){super(e),this.name=`ParseError`,this.type=t.type,this.field=t.field,this.value=t.value,this.line=t.line}},Rt=10,zt=13,k=32;function Bt(e){}function Vt(e){if(typeof e==`function`)throw TypeError("`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?");let{onEvent:t=Bt,onError:n=Bt,onRetry:r=Bt,onComment:i,maxBufferSize:a}=e,o=[],s=0,c=!0,l,u=``,d=0,f,p=!1;function m(e){if(p)throw Error("Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.");if(c&&(c=!1,e.charCodeAt(0)===239&&e.charCodeAt(1)===187&&e.charCodeAt(2)===191&&(e=e.slice(3))),o.length===0){let t=g(e);t!==``&&(o.push(t),s=t.length),h();return}if(e.indexOf(`
4
4
  `)===-1&&e.indexOf(`\r`)===-1){o.push(e),s+=e.length,h();return}o.push(e);let t=o.join(``);o.length=0,s=0;let n=g(t);n!==``&&(o.push(n),s=n.length),h()}function h(){a!==void 0&&(s+u.length<=a||(p=!0,o.length=0,s=0,l=void 0,u=``,d=0,f=void 0,n(new Lt(`Buffered data exceeded max buffer size of ${a} characters`,{type:`max-buffer-size-exceeded`}))))}function g(e){let n=0;if(e.indexOf(`\r`)===-1){let r=e.indexOf(`