@databehave/admin 0.0.0-stage → 0.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 (63) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +39 -2
  4. package/assets/ui.css +1 -0
  5. package/assets/ui.html +13 -0
  6. package/assets/ui.js +233 -0
  7. package/dist/admin-plugin.d.ts +23 -0
  8. package/dist/admin-plugin.d.ts.map +1 -0
  9. package/dist/admin-plugin.js +71 -0
  10. package/dist/admin-plugin.js.map +1 -0
  11. package/dist/admin-routes.d.ts +22 -0
  12. package/dist/admin-routes.d.ts.map +1 -0
  13. package/dist/admin-routes.js +316 -0
  14. package/dist/admin-routes.js.map +1 -0
  15. package/dist/admin-types.d.ts +98 -0
  16. package/dist/admin-types.d.ts.map +1 -0
  17. package/dist/admin-types.js +2 -0
  18. package/dist/admin-types.js.map +1 -0
  19. package/dist/banner.d.ts +10 -0
  20. package/dist/banner.d.ts.map +1 -0
  21. package/dist/banner.js +5 -0
  22. package/dist/banner.js.map +1 -0
  23. package/dist/fault-plugin.d.ts +29 -0
  24. package/dist/fault-plugin.d.ts.map +1 -0
  25. package/dist/fault-plugin.js +32 -0
  26. package/dist/fault-plugin.js.map +1 -0
  27. package/dist/header-parser.d.ts +14 -0
  28. package/dist/header-parser.d.ts.map +1 -0
  29. package/dist/header-parser.js +171 -0
  30. package/dist/header-parser.js.map +1 -0
  31. package/dist/index.d.ts +10 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +5 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/inject.d.ts +35 -0
  36. package/dist/inject.d.ts.map +1 -0
  37. package/dist/inject.js +155 -0
  38. package/dist/inject.js.map +1 -0
  39. package/dist/open-browser-plugin.d.ts +6 -0
  40. package/dist/open-browser-plugin.d.ts.map +1 -0
  41. package/dist/open-browser-plugin.js +32 -0
  42. package/dist/open-browser-plugin.js.map +1 -0
  43. package/dist/overrides-store.d.ts +21 -0
  44. package/dist/overrides-store.d.ts.map +1 -0
  45. package/dist/overrides-store.js +114 -0
  46. package/dist/overrides-store.js.map +1 -0
  47. package/dist/resolve-cors.d.ts +6 -0
  48. package/dist/resolve-cors.d.ts.map +1 -0
  49. package/dist/resolve-cors.js +39 -0
  50. package/dist/resolve-cors.js.map +1 -0
  51. package/dist/scenarios-store.d.ts +8 -0
  52. package/dist/scenarios-store.d.ts.map +1 -0
  53. package/dist/scenarios-store.js +136 -0
  54. package/dist/scenarios-store.js.map +1 -0
  55. package/dist/scenarios.d.ts +9 -0
  56. package/dist/scenarios.d.ts.map +1 -0
  57. package/dist/scenarios.js +2 -0
  58. package/dist/scenarios.js.map +1 -0
  59. package/dist/validation.d.ts +17 -0
  60. package/dist/validation.d.ts.map +1 -0
  61. package/dist/validation.js +169 -0
  62. package/dist/validation.js.map +1 -0
  63. package/package.json +52 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ Upcoming package version: **0.1.0**. This section describes planned release contents, not a published release.
6
+
7
+ ### Runtime requirements
8
+
9
+ - Require Node.js 24 or 26; support and test majors 24 and 26. Node.js 18, 20 and 22 are unsupported.
10
+
11
+ ### Added
12
+
13
+ - Explicit fault-injection, admin UI/API and optional browser-opening plugins extracted from server ownership.
14
+ - Route discovery through the resolved HTTP catalog, feature-owned CORS and default loopback bind restrictions.
15
+ - Public scenario-store contract and explicit file adapter, validated atomic fault-state replacement and versioned saves.
16
+ - Cancellation-aware delay/hang faults and explicit transport termination.
17
+
18
+ See [migration](../../MIGRATION.md) and [release procedures](../../docs/releasing.md).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ochairo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,40 @@
1
- # Temporary Holding Version
1
+ # @databehave/admin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Requires Node.js 24 or 26; supported majors are 24 and 26. See the
4
+ [plugin author guide](../../docs/plugins.md) and [migration guide](../../MIGRATION.md).
5
+
6
+ Optional fault injection and a native Web Components admin panel. Importing or using `@databehave/server` does not import or build this package's UI assets.
7
+
8
+ ```ts
9
+ import { createHost } from '@databehave/core'
10
+ import { httpPlugin, nodeHttpPlugin, routesPlugin } from '@databehave/server'
11
+ import { adminPlugin, faultInjectionPlugin, fileScenariosStore } from '@databehave/admin'
12
+
13
+ const host = createHost({ plugins: [
14
+ httpPlugin(),
15
+ routesPlugin({ 'GET /health': () => ({ json: { ok: true } }) }),
16
+ faultInjectionPlugin({ allowDestroy: false }),
17
+ adminPlugin({ scenarios: fileScenariosStore({ dir: './mock-scenarios' }) }),
18
+ nodeHttpPlugin({ host: '127.0.0.1', port: 3000 }),
19
+ ] })
20
+ await host.start()
21
+ // Admin: http://127.0.0.1:3000/databehave
22
+ // Later: await host.close()
23
+ ```
24
+
25
+ `faultInjectionPlugin` owns per-instance overrides and `x-mock-*` request interception. Headers take precedence over exact, path and global sticky overrides. Set `headers: false` to disable request headers while keeping sticky overrides. `allowDestroy: false` converts destroy requests to 503; otherwise HTTP performs transport termination. Delays and hangs cancel when the client disconnects or the host closes. HEAD uses the effective handler method for fault matching.
26
+
27
+ `faultControlCapability` exposes `add`, `remove`, `list`, `clear`, and atomic `replace`. Control plugins register excluded URL prefixes with `excludePathPrefix` during setup. Admin registers its own prefix so global faults cannot break the control API.
28
+
29
+ `adminPlugin` requires HTTP and fault control. Its options are `path` (default `/databehave`), `ui` (default true), `bind` (`loopback-only` or explicit `any`), `cors`, `scenarios` (a `ScenariosStore` provider), and `openapiBody` (an optional callback returning prepared JSON text for schema details). The route list always comes from HTTP's resolved catalog, including explicit-route precedence; document text does not define routes. `ui: false` omits HTML and asset routes. Omitting `scenarios` disables scenario endpoints. `ScenariosStore` has `list`, `get`, `save` and `remove` methods, with no filesystem or host lifecycle requirement. An external plugin can supply its own store and own any resources it needs. Admin validates complete provider results before replacing live overrides.
30
+
31
+ All bind guards run before any listener opens. The default accepts `127.0.0.1`, `::1`, and `localhost`; `bind: 'any'` explicitly permits other addresses. `cors: 'auto'` permits wildcard origins for the loopback default and uses same-origin for exposed admin. Browser opening is separate and opt-in: add `openAdminBrowserPlugin()` to open the panel after the listener binds. It has no effect with `ui: false`. Omit this plugin in headless environments.
32
+
33
+ `fileScenariosStore({ dir })` uses `<name>.json` under the explicitly chosen directory. Names contain only letters, numbers, `_` and `-`, at most 64 characters. Existing valid unversioned scenario files remain readable and are never automatically rewritten. New saves use `version: 1`. Invalid JSON, invalid overrides and unsupported versions fail explicitly on reads/loads; list skips unreadable or invalid entries. Invalid later entries cannot partially replace live state or overwrite a saved scenario. Save writes an owned temporary file then renames it atomically; failures clean that temporary file. Saving or loading is an explicit admin action.
34
+
35
+ The previous server `admin` configuration, server-owned assets, and admin-specific transport sentinel were removed. Compose these plugins explicitly. Build assets with `pnpm --filter @databehave/admin build`; published assets are available through `@databehave/admin/assets/*`. `pnpm --filter @databehave/admin typecheck` includes strict browser TypeScript, and `test` runs Node unit/integration tests. Browser verification is owned by the workspace's Playwright runner.
36
+
37
+ Admin owns its CORS policy even when general `corsPlugin()` is present. Restrictive modes reject disallowed,
38
+ malformed and opaque `Origin` values before mutation, including simple requests without a browser preflight.
39
+ Allowlist responses reflect one permitted origin. Origin-less local clients remain allowed. This policy is separate
40
+ from authentication. Both preflights and ordinary responses honor it regardless of plugin composition order.
package/assets/ui.css ADDED
@@ -0,0 +1 @@
1
+ *{box-sizing:border-box}html,body{margin:0;padding:0;font:13px/1.45 -apple-system,BlinkMacSystemFont,Segoe UI,system-ui,sans-serif;color:#1f2328;background:#f6f8fa}button{font:inherit;cursor:pointer}input,select,textarea{font:inherit;color:inherit}code,pre,.mono{font-family:ui-monospace,SF Mono,Menlo,monospace}a{color:#0969da}.app{min-height:100vh;padding-bottom:60px}.top-bar{position:sticky;top:0;z-index:30;display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:10px 16px;background:#24292f;color:#fff}.top-bar>*{min-width:0;max-width:100%}.top-bar h1{margin:0;font-size:15px;font-weight:600;flex:1}.top-bar .url{overflow-wrap:anywhere;font-size:11px;color:#adbac7;font-family:ui-monospace,Menlo,monospace}.top-bar .badge{background:#1f883d;color:#fff;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}.top-bar button,.top-bar select{background:#fff;color:#24292f;border:1px solid #57606a;border-radius:6px;padding:4px 10px;font-size:12px}.top-bar button.ghost{background:transparent;color:#fff;border-color:#57606a}.filter-bar{position:sticky;top:48px;z-index:25;background:#fff;border-bottom:1px solid #d0d7de;padding:10px 16px;display:flex;gap:16px;align-items:center;flex-wrap:wrap}.filter-bar input[type=text]{padding:6px 10px;border:1px solid #d0d7de;border-radius:6px;min-width:260px;flex:1}.filter-bar label{display:inline-flex;align-items:center;gap:4px;font-size:12px}.main{padding:12px 16px;max-width:1300px;margin:0 auto}.right-panel{position:fixed;top:0;right:0;width:360px;max-width:90vw;height:100vh;background:#fff;border-left:1px solid #d0d7de;box-shadow:-4px 0 12px #0000000f;visibility:hidden;z-index:40;display:flex;flex-direction:column}.right-panel.open{visibility:visible}.right-panel header{padding:12px 16px;border-bottom:1px solid #d0d7de;display:flex;align-items:center;gap:8px}.right-panel header h2{margin:0;font-size:14px;flex:1}.right-panel .list{flex:1;overflow-y:auto;padding:8px}.pill-float{position:fixed;right:16px;bottom:16px;background:#cf222e;color:#fff;padding:8px 14px;border-radius:999px;font-size:12px;font-weight:600;border:none;box-shadow:0 2px 8px #00000026;z-index:35}.toast-stack{max-width:calc(100vw - 32px);overflow-wrap:anywhere;position:fixed;right:16px;bottom:60px;display:flex;flex-direction:column;gap:6px;z-index:50}.toast{padding:8px 12px;border-radius:6px;font-size:12px;color:#fff;box-shadow:0 2px 8px #00000026;cursor:pointer;min-width:180px}.toast.success{background:#1f883d}.toast.error{background:#cf222e}.toast.info{background:#0969da}.modal-backdrop{position:fixed;inset:0;background:#0006;z-index:60;display:flex;align-items:center;justify-content:center}.modal{background:#fff;border-radius:8px;padding:20px;max-width:520px;width:92%}.modal h2{margin-top:0}.modal table{width:100%;font-size:12px}.modal kbd{background:#f6f8fa;border:1px solid #d0d7de;border-radius:3px;padding:1px 6px;font-family:ui-monospace,Menlo,monospace;font-size:11px}@media(max-width:768px){.top-bar,.filter-bar{position:static}.filter-bar input[type=text]{min-width:0;width:100%;max-width:100%;flex-basis:100%}}.group{background:#fff;border:1px solid #d0d7de;border-radius:8px;margin-bottom:10px;overflow:hidden}.group-header{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;background:#f6f8fa;border-bottom:1px solid #eaeef2}.group-header h3{margin:0;font-size:13px;flex:1;text-transform:capitalize}.group-header .count{font-size:11px;color:#57606a}.group-body{display:none}.group.open .group-body{display:block}.op-row{display:flex;align-items:center;gap:10px;padding:8px 14px;border-bottom:1px solid #eaeef2;cursor:pointer}.op-row:last-child{border-bottom:none}.op-row:hover{background:#f6f8fa}.op-row.focused{background:#ddf4ff}.op-row .path{flex:1;font-family:ui-monospace,Menlo,monospace;font-size:12px}.op-row .path .no-oas-pill{margin-left:6px;color:#6e7781;font-size:10px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-weight:500;letter-spacing:.02em}.op-row .summary{color:#57606a;font-size:11px;max-width:380px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.op-row .dot{width:10px;height:10px;border-radius:50%;background:transparent;border:1px solid #d0d7de}.op-row .dot.active{border-color:transparent}.method-badge{display:inline-block;min-width:56px;text-align:center;padding:3px 8px;border-radius:4px;color:#fff;font-weight:700;font-size:11px;font-family:ui-monospace,Menlo,monospace}.op-card{padding:0;border-top:1px solid #eaeef2}.spec-zone{background:#fff;padding:14px 18px;border-bottom:1px solid #eaeef2}.spec-zone h4{margin:0 0 6px;font-size:12px;color:#57606a;text-transform:uppercase;letter-spacing:.5px}.spec-zone .desc{font-size:12px;color:#1f2328}.spec-zone table{width:100%;border-collapse:collapse;font-size:12px;margin-top:4px}.spec-zone th,.spec-zone td{text-align:left;padding:4px 6px;border-bottom:1px solid #eaeef2}.spec-zone th{color:#57606a;font-weight:600}.mock-zone{background:#fff8c5;border-left:4px solid #d4a72c;padding:14px 18px}.mock-zone h4{margin:0 0 8px;font-size:12px;color:#7d4e00;text-transform:uppercase;letter-spacing:.5px}.mock-zone .row{display:grid;grid-template-columns:160px 1fr;gap:10px;align-items:center;margin-bottom:8px}.mock-zone label{font-size:12px;color:#4a3500;font-weight:500}.mock-zone input[type=text],.mock-zone input[type=number],.mock-zone select,.mock-zone textarea{width:100%;padding:4px 8px;border:1px solid #d4a72c;border-radius:4px;background:#fff}.mock-zone textarea{font-family:ui-monospace,Menlo,monospace;min-height:70px}.mock-zone .radio-group{display:flex;gap:12px;flex-wrap:wrap}.mock-zone .radio-group label{display:inline-flex;align-items:center;gap:4px;font-weight:400}.mock-zone .actions{display:flex;gap:8px;margin-top:10px}.btn{padding:6px 12px;border-radius:6px;border:1px solid #d0d7de;background:#fff;color:#1f2328}.btn.primary{background:#1f883d;color:#fff;border-color:transparent;font-weight:600}.btn.danger{background:#cf222e;color:#fff;border-color:transparent}.btn.ghost{background:transparent}.try-it{padding:14px 18px;border-top:1px solid #eaeef2;background:#f6f8fa}.try-it summary{cursor:pointer;font-weight:600;font-size:13px}.try-it .field{margin:6px 0}.try-it .response{margin-top:8px;background:#fff;border:1px solid #d0d7de;border-radius:6px;padding:10px;font-family:ui-monospace,Menlo,monospace;font-size:12px;max-height:320px;overflow:auto}.schema-tree{font-family:ui-monospace,Menlo,monospace;font-size:12px}.schema-tree details{margin-left:12px}.schema-tree summary{cursor:pointer}.schema-tree .key{color:#6f42c1}.schema-tree .type{color:#1f883d;font-style:italic;margin-left:6px}.schema-tree .req{color:#cf222e}.override-item{display:flex;align-items:center;gap:8px;padding:8px;border-bottom:1px solid #eaeef2;cursor:pointer}.override-item:hover{background:#f6f8fa}.override-item .info{flex:1;font-size:12px}.override-item .info .route{font-family:ui-monospace,Menlo,monospace}.override-item .dot{width:10px;height:10px;border-radius:50%}.global-active-hint{display:flex;align-items:center;gap:8px;padding:6px 10px;margin-bottom:8px;background:#f3e8ff;border-left:3px solid #6f42c1;font-size:12px;color:#4a2370;border-radius:4px}.global-active-hint .btn{padding:2px 8px;font-size:11px}.scope-badge{display:inline-block;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:#fff}.scope-badge.scope-global{background:#6f42c1}.override-item.is-global{background:#faf5ff}.override-item.is-global:hover{background:#f3e8ff}.top-bar .global-override-btn{display:inline-flex;align-items:center;gap:6px}.top-bar .global-badge{background:#6f42c1}.global-override-modal{max-width:560px}.global-override-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.global-override-header h2{margin:0;flex:1;font-size:16px}.global-override-header .close-x{font-size:18px;line-height:1;padding:2px 8px;color:#57606a;border:1px solid #d0d7de;background:#fff}.global-override-desc{font-size:12px;color:#57606a;margin:0 0 12px}.global-override-modal .row{display:grid;grid-template-columns:140px 1fr;gap:10px;align-items:center;margin-bottom:8px}.global-override-modal label{font-size:12px;color:#1f2328;font-weight:500}.global-override-modal input[type=text],.global-override-modal input[type=number],.global-override-modal select,.global-override-modal textarea{width:100%;padding:4px 8px;border:1px solid #d0d7de;border-radius:4px;background:#fff}.global-override-modal textarea{font-family:ui-monospace,Menlo,monospace;min-height:70px}.global-override-modal .actions{display:flex;gap:8px;margin-top:12px}.global-override-modal .method-checkboxes{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center}.global-override-modal .method-checkbox{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;color:#1f2328;cursor:pointer}.global-override-modal .method-checkbox input[type=checkbox]{width:auto;margin:0}.global-override-list{margin-top:16px;padding-top:12px;border-top:1px solid #eaeef2}.global-override-list h3{margin:0 0 8px;font-size:12px;color:#57606a;text-transform:uppercase;letter-spacing:.5px}
package/assets/ui.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Mock Injection</title>
7
+ <script type="module" crossorigin src="./ui.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./ui.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
package/assets/ui.js ADDED
@@ -0,0 +1,233 @@
1
+ var F=Object.defineProperty;var U=(a,i,e)=>i in a?F(a,i,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[i]=e;var d=(a,i,e)=>U(a,typeof i!="symbol"?i+"":i,e);(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))t(s);new MutationObserver(s=>{for(const n of s)if(n.type==="childList")for(const o of n.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&t(o)}).observe(document,{childList:!0,subtree:!0});function e(s){const n={};return s.integrity&&(n.integrity=s.integrity),s.referrerPolicy&&(n.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?n.credentials="include":s.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function t(s){if(s.ep)return;s.ep=!0;const n=e(s);fetch(s.href,n)}})();const L=["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS"],B=(a,i)=>i.size>0?i.has(a):L.includes(a),G=()=>({search:"",methods:new Set(L),activeOnly:!1,rightPanelOpen:!1,helpOpen:!1,globalOpen:!1,expandedKey:null,focusedKey:null,routes:[],doc:null,overrides:[],scenarios:[],scenariosEnabled:!1,loading:!0}),q=(a,i)=>`${a.toUpperCase()} ${i}`,Q=(...a)=>{let i=G();const e=new Set;return{get:()=>i,set:o=>{i={...i,...o};for(const r of e)r(i)},subscribe:o=>(e.add(o),()=>{e.delete(o)})}},z=(a,i)=>{const e=[];for(const t of a){const s=i?.paths?.[t.path]?.[t.method.toLowerCase()]??{},n=s.tags?.[0],o=t.path.split("/").filter(Boolean)[0]??"other",r=n??o;e.push({method:t.method.toUpperCase(),path:t.path,op:{...s,...t.summary&&!s.summary?{summary:t.summary}:{}},groupKey:r.toLowerCase(),groupLabel:r,source:t.source??"openapi"})}return e},J=(a,i)=>{const e=new Map;for(const t of a){const s=e.get(t.groupKey)??{key:t.groupKey,label:t.groupLabel,ops:[]};s.ops.push(t),e.set(t.groupKey,s)}for(const t of e.values())t.ops.sort((s,n)=>s.path===n.path?s.method.localeCompare(n.method):s.path.localeCompare(n.path));return[...e.values()].sort((t,s)=>t.label.localeCompare(s.label))},V=a=>{const i=new Map;for(const e of a){if(e.matcher.kind!=="exact")continue;const t=q(e.matcher.method,e.matcher.path);i.set(t,(i.get(t)??0)+1)}return i},m=typeof window<"u"&&window.__DATABEHAVE_BASE__||"";let k;const v=async a=>{const i=await a.text(),e=i?JSON.parse(i):void 0;if(!a.ok){const t=typeof e=="object"&&e!==null?e.error:void 0;throw new Error(t??`HTTP ${a.status}`)}return e},f={async listOverrides(){const a=await fetch(m+"/overrides");return(await v(a)).overrides??[]},async addOverride(a){const i=await fetch(m+"/overrides",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(a)});return(await v(i)).override},async removeOverride(a){await v(await fetch(m+"/overrides/"+encodeURIComponent(a),{method:"DELETE"}))},async clearOverrides(){await v(await fetch(m+"/overrides",{method:"DELETE"}))},async listRoutes(){const a=await fetch(m+"/openapi-routes");return v(a)},async fetchOpenApi(){if(k!==void 0)return k;try{const a=await fetch(m+"/openapi.json");return a.ok?(k=await a.json(),k):(k=null,null)}catch{return k=null,null}},async listScenarios(){const a=await fetch(m+"/scenarios");return a.status===404?[]:(await v(a)).scenarios??[]},async saveScenario(a){await v(await fetch(m+"/scenarios",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:a})}))},async loadScenario(a){await v(await fetch(m+"/scenarios/"+encodeURIComponent(a)+"/load",{method:"POST"}))},async deleteScenario(a){await v(await fetch(m+"/scenarios/"+encodeURIComponent(a),{method:"DELETE"}))}},c=Q();let X=0;const O=new Set;let g=[];const Z=a=>(O.add(a),()=>{O.delete(a)}),T=()=>{for(const a of O)a(g)},b=(a,i)=>{const e=++X;g=[...g,{id:e,kind:a,message:i}],T(),setTimeout(()=>{g=g.filter(t=>t.id!==e),T()},3e3)},W=a=>{g=g.filter(i=>i.id!==a),T()},Y=()=>g,w=async()=>{try{c.set({overrides:await f.listOverrides()})}catch{}},C=async()=>{if(c.get().scenariosEnabled)try{c.set({scenarios:await f.listScenarios()})}catch{}},P=async(a,i,e)=>{const t=await f.addOverride({matcher:a,mode:i,...e!==void 0?{description:e}:{}});return b("success","Override applied"),await w(),t},E=async a=>{await f.removeOverride(a),b("success","Override removed"),await w()},ee=async()=>{if(window.confirm("Clear ALL sticky overrides?"))try{await f.clearOverrides(),b("success","Cleared"),await w()}catch(a){b("error",a instanceof Error?a.message:String(a))}},te=async a=>{try{await f.saveScenario(a),b("success","Saved: "+a),await C()}catch(i){b("error",i instanceof Error?i.message:String(i))}},se=async a=>{try{await f.loadScenario(a),b("success","Loaded: "+a),await w()}catch(i){b("error",i instanceof Error?i.message:String(i))}},ae=async a=>{try{await f.deleteScenario(a),b("success","Deleted: "+a),await C()}catch(i){b("error",i instanceof Error?i.message:String(i))}},M=()=>c.set({globalOpen:!0,rightPanelOpen:!1}),ne={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},h=a=>a==null?"":String(a).replace(/[&<>"']/g,i=>ne[i]??i);class ie extends HTMLElement{constructor(){super(...arguments);d(this,"_unsub");d(this,"_open",!1)}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){const e=c.get().scenarios;this.innerHTML=`
2
+ <div style="position:relative">
3
+ <button class="ghost" type="button" data-toggle>Scenarios (${e.length})</button>
4
+ ${this._open?`
5
+ <div style="position:absolute;top:110%;right:0;background:#fff;color:#1f2328;border:1px solid #d0d7de;border-radius:6px;padding:8px;min-width:240px;z-index:50;box-shadow:0 2px 8px rgba(0,0,0,0.1)">
6
+ <button class="btn" style="width:100%;margin-bottom:8px" type="button" data-save>Save current as…</button>
7
+ ${e.map(t=>`
8
+ <div style="display:flex;gap:4px;align-items:center;padding:4px 0;border-top:1px solid #eaeef2">
9
+ <span style="flex:1;font-size:12px">${h(t.name)} <small>(${t.count})</small></span>
10
+ <button class="btn" type="button" data-load="${h(t.name)}">Load</button>
11
+ <button class="btn danger" type="button" data-delete="${h(t.name)}">Delete</button>
12
+ </div>`).join("")}
13
+ </div>`:""}
14
+ </div>`,this.querySelector("[data-toggle]")?.addEventListener("click",()=>{this._open=!this._open,this._render()}),this.querySelector("[data-save]")?.addEventListener("click",()=>{const t=window.prompt("Scenario name (a-z, 0-9, _ -):");t&&te(t),this._open=!1,this._render()});for(const t of this.querySelectorAll("[data-load]"))t.addEventListener("click",()=>{se(t.dataset.load??""),this._open=!1,this._render()});for(const t of this.querySelectorAll("[data-delete]"))t.addEventListener("click",()=>{ae(t.dataset.delete??"")})}}customElements.define("dbh-scenarios-menu",ie);class re extends HTMLElement{constructor(){super(...arguments);d(this,"_unsub")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){const e=c.get(),t=e.overrides.length,s=e.overrides.filter(o=>o.matcher.kind==="global").length,n=typeof window<"u"?window.location.origin:"";this.innerHTML=`
15
+ <div class="top-bar">
16
+ <h1>Mock Injection</h1>
17
+ <span class="url">${n}</span>
18
+ <span class="badge">${t}</span>
19
+ <button class="ghost" type="button" data-clear>Clear all</button>
20
+ <button class="ghost global-override-btn" type="button" data-global title="Global override">
21
+ Global override${s>0?`<span class="badge global-badge">${s}</span>`:""}
22
+ </button>
23
+ ${e.scenariosEnabled?"<dbh-scenarios-menu></dbh-scenarios-menu>":""}
24
+ <button class="ghost" type="button" data-help title="?">?</button>
25
+ <button class="ghost" type="button" data-panel>Active overrides</button>
26
+ </div>`,this.querySelector("[data-clear]")?.addEventListener("click",()=>{ee()}),this.querySelector("[data-global]")?.addEventListener("click",()=>M()),this.querySelector("[data-help]")?.addEventListener("click",()=>c.set({helpOpen:!0})),this.querySelector("[data-panel]")?.addEventListener("click",()=>c.set({rightPanelOpen:!c.get().rightPanelOpen}))}}customElements.define("dbh-top-bar",re);class oe extends HTMLElement{constructor(){super(...arguments);d(this,"_unsub");d(this,"_rendered",!1);d(this,"searchInput")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._sync())}disconnectedCallback(){this._unsub?.()}_render(){const e=c.get();this.innerHTML=`<div class="filter-bar"><input type="text" placeholder="Search path or summary…" value="${de(e.search)}" data-search />`+L.map(t=>`<label><input type="checkbox" data-method="${t}"${e.methods.has(t)?" checked":""}/> ${t}</label>`).join("")+`<label><input type="checkbox" data-active${e.activeOnly?" checked":""}/> Active overrides only</label></div>`,this.searchInput=this.querySelector("[data-search]")??void 0,this.searchInput?.addEventListener("input",t=>{const s=t.target.value;c.set({search:s})});for(const t of this.querySelectorAll("[data-method]"))t.addEventListener("change",()=>{const s=new Set(c.get().methods),n=t.dataset.method??"";t.checked?s.add(n):s.delete(n),c.set({methods:s})});this.querySelector("[data-active]")?.addEventListener("change",t=>{c.set({activeOnly:t.target.checked})}),this._rendered=!0}_sync(){if(!this._rendered)return;const e=c.get();this.searchInput&&this.searchInput.value!==e.search&&(this.searchInput.value=e.search);for(const s of this.querySelectorAll("[data-method]"))s.checked=e.methods.has(s.dataset.method??"");const t=this.querySelector("[data-active]");t&&(t.checked=e.activeOnly)}}const de=a=>a.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;");customElements.define("dbh-filter-bar",oe);const le={GET:"#1f883d",POST:"#0969da",PUT:"#bf8700",DELETE:"#cf222e",PATCH:"#1f6feb"},A=a=>a==="http-status"||a==="business-failure"||a==="destroy"?"#cf222e":a==="delay"||a==="hang"?"#bf8700":"#0969da";class ce extends HTMLElement{static get observedAttributes(){return["method"]}connectedCallback(){this._render()}attributeChangedCallback(){this.isConnected&&this._render()}_render(){const i=(this.getAttribute("method")??"").toUpperCase(),e=le[i]??"#57606a";this.innerHTML=`<span class="method-badge" style="background:${e}">${i}</span>`}}customElements.define("dbh-method-badge",ce);class he extends HTMLElement{setRow(i){const{op:e,override:t,expanded:s,focused:n}=i,o=t?A(t.mode.kind):"transparent",r=e.source==="handler";this.innerHTML=`
27
+ <div class="op-row${n?" focused":""}${s?" expanded":""}" data-key="${h(e.method+" "+e.path)}">
28
+ <dbh-method-badge method="${h(e.method)}"></dbh-method-badge>
29
+ <span class="path">${h(e.path)}${r?' <span class="no-oas-pill" title="No OpenAPI spec for this route">(no OAS)</span>':""}</span>
30
+ <span class="summary">${h(e.op.summary??"")}</span>
31
+ <span class="dot${t?" active":""}" style="${t?`background:${o}`:""}" title="${h(t?.mode.kind??"")}"></span>
32
+ </div>`,this.querySelector(".op-row")?.addEventListener("click",()=>{const p=e.method+" "+e.path,u=c.get().expandedKey;c.set({expandedKey:u===p?null:p})})}}customElements.define("dbh-operation-row",he);const ue=a=>a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),pe=a=>a?a.split(/\n{2,}/).map(e=>`<p>${ue(e.trim()).replace(/`([^`]+)`/g,(o,r)=>`<code>${r}</code>`).replace(/\n/g,"<br/>")}</p>`).join(""):"",H={string:"string",integer:0,number:0,boolean:!0,null:null},_=(a,i,e=0,t=new Set)=>{if(!a||e>8)return null;if(a.example!==void 0)return a.example;if(a.default!==void 0)return a.default;if(a.enum&&a.enum.length>0)return a.enum[0];if(a.$ref){if(t.has(a.$ref))return null;const s=new Set(t);s.add(a.$ref);const n=a.$ref.replace(/^#\/components\/schemas\//,""),o=i?.components?.schemas?.[n];return _(o,i,e+1,s)}if(a.oneOf?.[0])return _(a.oneOf[0],i,e+1,t);if(a.anyOf?.[0])return _(a.anyOf[0],i,e+1,t);if(a.allOf&&a.allOf.length>0){const s={};for(const n of a.allOf){const o=_(n,i,e+1,t);o&&typeof o=="object"&&!Array.isArray(o)&&Object.assign(s,o)}return s}if(a.type==="object"||a.properties){const s={},n=a.properties??{};for(const[o,r]of Object.entries(n))s[o]=_(r,i,e+1,t);return s}return a.type==="array"?[_(a.items,i,e+1,t)]:typeof a.type=="string"&&a.type in H?H[a.type]:null},be=(a,i)=>{const e=a.replace(/^#\/components\/schemas\//,"");return i?.components?.schemas?.[e]},K=a=>a?a.$ref?a.$ref.split("/").pop()??"ref":a.type==="array"?`${K(a.items)}[]`:a.type??(a.properties?"object":"any"):"any",x=(a,i,e=0,t,s)=>{if(!a)return'<span class="type">any</span>';if(e>6)return'<span class="type">…</span>';const n=a.$ref?be(a.$ref,i)??a:a,o=t?`<span class="key">${h(t)}${s?'<span class="req">*</span>':""}: </span>`:"",r=`<span class="type">${h(K(n))}</span>`,l=e<2?" open":"";return n.type==="object"||n.properties?`<details${l}><summary>${o}${r}</summary>`+Object.entries(n.properties??{}).map(([p,u])=>x(u,i,e+1,p,n.required?.includes(p))).join("")+"</details>":n.type==="array"?`<details${l}><summary>${o}${r}</summary>`+x(n.items,i,e+1)+"</details>":`<div style="margin-left:${e?14:0}px">${o}${r}</div>`};class me extends HTMLElement{constructor(){super(...arguments);d(this,"_op",null);d(this,"_doc",null)}setOp(e,t){this._op=e,this._doc=t,this._render()}_render(){const e=this._op;if(!e){this.innerHTML="";return}if(e.source==="handler"){this.innerHTML=`
33
+ <div class="spec-zone">
34
+ <h4>Spec</h4>
35
+ <div class="desc" style="color:#57606a;font-style:italic">
36
+ No OpenAPI spec for this route. Inject controls and Try-it-out still
37
+ work; request/response schemas are unavailable.
38
+ </div>
39
+ </div>`;return}const t=this._doc,s=e.op.requestBody?.content?.["application/json"],n=s?.schema,o=s?s.example??_(n,t):null,r=o!=null?JSON.stringify(o,null,2):"",l=e.op.parameters??[],p=e.op.responses??{};this.innerHTML=`
40
+ <div class="spec-zone">
41
+ <h4>Spec</h4>
42
+ ${e.op.description?`<div class="desc">${pe(e.op.description)}</div>`:""}
43
+ ${l.length>0?`
44
+ <div style="margin-top:8px">
45
+ <h4>Parameters</h4>
46
+ <table>
47
+ <thead><tr><th>name</th><th>in</th><th>type</th><th>required</th><th>description</th></tr></thead>
48
+ <tbody>
49
+ ${l.map(u=>`
50
+ <tr>
51
+ <td><code>${h(u.name)}</code></td>
52
+ <td>${h(u.in)}</td>
53
+ <td>${h(u.schema?.type??"any")}</td>
54
+ <td>${u.required?"yes":""}</td>
55
+ <td>${h(u.description??"")}</td>
56
+ </tr>`).join("")}
57
+ </tbody>
58
+ </table>
59
+ </div>`:""}
60
+ ${n?`
61
+ <div style="margin-top:8px">
62
+ <h4>Request body</h4>
63
+ <div class="schema-tree">${x(n,t)}</div>
64
+ ${r?'<button class="btn ghost" style="margin-top:6px" type="button" data-copy>Copy as JSON example</button>':""}
65
+ </div>`:""}
66
+ ${Object.keys(p).length>0?`
67
+ <div style="margin-top:8px">
68
+ <h4>Responses</h4>
69
+ ${Object.entries(p).map(([u,y])=>{const S=y.content?.["application/json"]?.schema;return`
70
+ <details style="margin-top:4px">
71
+ <summary><strong>${h(u)}</strong> — ${h(y.description??"")}</summary>
72
+ ${S?`<div class="schema-tree">${x(S,t)}</div>`:""}
73
+ </details>`}).join("")}
74
+ </div>`:""}
75
+ </div>`,this.querySelector("[data-copy]")?.addEventListener("click",()=>{navigator.clipboard?.writeText(r),b("info","Copied")})}}customElements.define("dbh-spec-zone",me);const N=a=>{const i=new TextEncoder().encode(a);let e="";for(const t of i)e+=String.fromCharCode(t);return btoa(e)},I=a=>{for(let i=0;i<a.length;i++)if(a.charCodeAt(i)>127)return!1;return!0},fe=["http-status","business-failure","custom-body","empty-body","malformed-json","delay","hang","destroy"];class ye extends HTMLElement{constructor(){super(...arguments);d(this,"_op",null);d(this,"_existing");d(this,"_globalActive",!1);d(this,"headerOverride",null);d(this,"_scope","exact");d(this,"_kind","http-status");d(this,"_status",500);d(this,"_message","");d(this,"_body","{}");d(this,"_contentType","application/json");d(this,"_ms",1e3);d(this,"_description","")}setOp(e){const t=this._existing?.id===e.existing?.id&&this._op?.path===e.op.path&&this._op?.method===e.op.method;this._op=e.op,this._existing=e.existing,this._globalActive=e.globalActive,t||this._syncFromExisting(),this._render()}_syncFromExisting(){const e=this._existing;if(!e){this._scope="exact",this._kind="http-status",this._status=500,this._message="",this._body="{}",this._contentType="application/json",this._ms=1e3,this._description="";return}(e.matcher.kind==="exact"||e.matcher.kind==="path")&&(this._scope=e.matcher.kind),this._kind=e.mode.kind,this._description=e.description??"";const t=e.mode;t.kind==="http-status"?this._status=t.status:t.kind==="business-failure"?this._message=t.message:t.kind==="custom-body"?(this._status=t.status??200,this._body=JSON.stringify(t.body,null,2),this._contentType=t.contentType??"application/json"):t.kind==="empty-body"?this._status=t.status??204:t.kind==="delay"&&(this._ms=t.ms)}_renderFieldsHtml(){const e=this._kind;return e==="http-status"||e==="empty-body"||e==="malformed-json"?`<div class="row"><label>Status</label><input type="number" data-field="status" value="${this._status}" /></div>`:e==="business-failure"?`<div class="row"><label>Message</label><input type="text" data-field="message" value="${h(this._message)}" placeholder="Optional message returned with the failure" /></div>`:e==="custom-body"?`
76
+ <div class="row"><label>Status</label><input type="number" data-field="status" value="${this._status}" /></div>
77
+ <div class="row"><label>Content-Type</label><input type="text" data-field="contentType" value="${h(this._contentType)}" /></div>
78
+ <div class="row"><label>Body</label><textarea data-field="body">${h(this._body)}</textarea></div>`:e==="delay"?`<div class="row"><label>Delay (ms)</label><input type="number" data-field="ms" value="${this._ms}" /></div>`:""}_render(){if(!this._op){this.innerHTML="";return}const e=this._existing;this.innerHTML=`
79
+ <div class="mock-zone">
80
+ <h4>Inject response</h4>
81
+ ${this._globalActive?`
82
+ <div class="global-active-hint" role="note">
83
+ <span>A global override is active. Manage it from the top bar.</span>
84
+ <button type="button" class="btn ghost" data-open-global>Open</button>
85
+ </div>`:""}
86
+ <div class="row">
87
+ <label>Mode</label>
88
+ <select data-mode>${fe.map(t=>`<option value="${t}"${t===this._kind?" selected":""}>${t}</option>`).join("")}</select>
89
+ </div>
90
+ <div data-fields>${this._renderFieldsHtml()}</div>
91
+ <div class="row">
92
+ <label>Description</label>
93
+ <input type="text" data-field="description" value="${h(this._description)}" />
94
+ </div>
95
+ <div class="actions">
96
+ <button class="btn primary" type="button" data-submit>${e?"Update":"Apply"}</button>
97
+ ${e?`<button class="btn danger" type="button" data-remove="${h(e.id)}">Remove</button>`:""}
98
+ </div>
99
+ </div>`,this._wire()}_wire(){this.querySelector("[data-open-global]")?.addEventListener("click",()=>M());const e=this.querySelector("[data-mode]");e?.addEventListener("change",()=>{this._kind=e.value;const s=this.querySelector("[data-fields]");s&&(s.innerHTML=this._renderFieldsHtml(),this._wireFields())}),this._wireFields(),this.querySelector("[data-submit]")?.addEventListener("click",()=>this._submit());const t=this.querySelector("[data-remove]");t?.addEventListener("click",()=>{E(t.dataset.remove??"")})}_wireFields(){const e=this.querySelector('[data-field="description"]');e?.addEventListener("input",()=>{this._description=e.value});for(const t of this.querySelectorAll("[data-fields] [data-field]"))t.addEventListener("input",()=>{const s=t.dataset.field;s==="status"?this._status=Number(t.value):s==="ms"?this._ms=Number(t.value):s==="message"?this._message=t.value:s==="body"?this._body=t.value:s==="contentType"&&(this._contentType=t.value)})}_submit(){const e=this._op;if(!e)return;const t=this._scope==="exact"?{kind:"exact",method:e.method,path:e.path}:{kind:"path",path:e.path};let s,n=null;const o=this._kind;switch(o){case"http-status":s={kind:o,status:this._status};break;case"business-failure":s={kind:o,message:this._message},I(this._message)||(n={name:"x-mock-business-failure-b64",value:N(this._message)});break;case"custom-body":{let r;try{r=JSON.parse(this._body)}catch{r=this._body}s={kind:o,status:this._status,body:r,contentType:this._contentType};break}case"empty-body":s={kind:o,status:this._status};break;case"malformed-json":s={kind:o,status:this._status};break;case"delay":s={kind:o,ms:this._ms};break;case"hang":s={kind:o};break;case"destroy":s={kind:o};break}this.headerOverride=n,this.dispatchEvent(new CustomEvent("dbh-header-override",{detail:n,bubbles:!0})),(async()=>{const r=this._existing;if(r&&t.kind===r.matcher.kind)try{await E(r.id)}catch{}try{await P(t,s,this._description||void 0)}catch(l){b("error",l instanceof Error?l.message:String(l))}})()}}customElements.define("dbh-inject-error-panel",ye);const ve=(a,i)=>{const e=a.op.requestBody?.content?.["application/json"]?.schema;return e?JSON.stringify(_(e,i),null,2):""};class _e extends HTMLElement{constructor(){super(...arguments);d(this,"_op",null);d(this,"_doc",null);d(this,"extraHeader",null);d(this,"_pvals",{});d(this,"_freeQuery",[]);d(this,"_body","");d(this,"_busy",!1)}setOp(e,t){this._op=e,this._doc=t,this._pvals={};for(const s of e.op.parameters??[])this._pvals[s.name]=String(s.schema?.default??"");this._body=ve(e,t),this._freeQuery=e.source==="handler"?[{key:"",value:""}]:[],this._render()}_render(){const e=this._op;if(!e){this.innerHTML="";return}const t=e.op.parameters??[],s=e.method!=="GET"&&e.method!=="HEAD",n=e.source==="handler";this.innerHTML=`
100
+ <details class="try-it">
101
+ <summary>Try it out</summary>
102
+ ${t.map(r=>`
103
+ <div class="field">
104
+ <label>${h(r.name)} <small>(${h(r.in)}${r.required?", required":""})</small></label>
105
+ <input type="text" data-pname="${h(r.name)}" value="${h(this._pvals[r.name]??"")}" />
106
+ </div>`).join("")}
107
+ ${n?`
108
+ <div class="field">
109
+ <label>query params <small>(free-form)</small></label>
110
+ <div data-free-query>
111
+ ${this._freeQuery.map((r,l)=>`
112
+ <div class="free-q-row" style="display:flex;gap:6px;margin-bottom:4px">
113
+ <input type="text" data-fq-key="${l}" placeholder="name" value="${h(r.key)}" style="flex:1" />
114
+ <input type="text" data-fq-val="${l}" placeholder="value" value="${h(r.value)}" style="flex:2" />
115
+ <button class="btn ghost" type="button" data-fq-rm="${l}" title="Remove">−</button>
116
+ </div>`).join("")}
117
+ <button class="btn ghost" type="button" data-fq-add>+ Add query param</button>
118
+ </div>
119
+ </div>`:""}
120
+ ${s?`
121
+ <div class="field">
122
+ <label>body${n?" <small>(free-form)</small>":""}</label>
123
+ <textarea data-body style="width:100%;min-height:100px;font-family:ui-monospace,Menlo,monospace">${h(this._body)}</textarea>
124
+ </div>`:""}
125
+ <button class="btn primary" type="button" data-send${this._busy?" disabled":""}>${this._busy?"…":"Send"}</button>
126
+ <dbh-response-viewer></dbh-response-viewer>
127
+ </details>`;for(const r of this.querySelectorAll("[data-pname]"))r.addEventListener("input",()=>{this._pvals[r.dataset.pname??""]=r.value});for(const r of this.querySelectorAll("[data-fq-key]"))r.addEventListener("input",()=>{const l=Number(r.dataset.fqKey);this._freeQuery[l]&&(this._freeQuery[l].key=r.value)});for(const r of this.querySelectorAll("[data-fq-val]"))r.addEventListener("input",()=>{const l=Number(r.dataset.fqVal);this._freeQuery[l]&&(this._freeQuery[l].value=r.value)});for(const r of this.querySelectorAll("[data-fq-rm]"))r.addEventListener("click",()=>{const l=Number(r.dataset.fqRm);this._freeQuery.splice(l,1),this._freeQuery.length===0&&this._freeQuery.push({key:"",value:""}),this._render()});this.querySelector("[data-fq-add]")?.addEventListener("click",()=>{this._freeQuery.push({key:"",value:""}),this._render()});const o=this.querySelector("[data-body]");o?.addEventListener("input",()=>{this._body=o.value}),this.querySelector("[data-send]")?.addEventListener("click",()=>{this._send()})}_buildUrl(){const e=this._op;if(!e)return"";const t=e.op.parameters??[];let s=e.path;const n=new URLSearchParams;for(const r of t){const l=this._pvals[r.name];l&&(r.in==="path"?s=s.replace(`{${r.name}}`,encodeURIComponent(l)):r.in==="query"&&n.set(r.name,l))}for(const r of this._freeQuery)r.key&&n.append(r.key,r.value);const o=n.toString();return o?`${s}?${o}`:s}async _send(){const e=this._op;if(!e)return;this._busy=!0;const t=this.querySelector("dbh-response-viewer");t?.setResult(null);const s=this.querySelector("[data-send]");s&&(s.disabled=!0,s.textContent="…");const n=performance.now();try{const o={};for(const y of e.op.parameters??[])y.in==="header"&&this._pvals[y.name]&&(o[y.name]=this._pvals[y.name]);this.extraHeader&&(o[this.extraHeader.name]=this.extraHeader.value);const r={method:e.method,headers:o};e.method!=="GET"&&e.method!=="HEAD"&&this._body&&(o["content-type"]=o["content-type"]??"application/json",r.body=this._body);const l=await fetch(this._buildUrl(),r),p=await l.text(),u={status:l.status,headers:{},body:p,elapsedMs:Math.round(performance.now()-n)};l.headers.forEach((y,S)=>{u.headers[S]=y}),t?.setResult(u)}catch(o){t?.setResult({error:o instanceof Error?o.message:String(o)})}finally{this._busy=!1,s&&(s.disabled=!1,s.textContent="Send")}}}customElements.define("dbh-try-it-out",_e);const R=(a,i)=>{if(!a.methods||a.methods.length===0)return!0;const e=i.toUpperCase();return a.methods.some(t=>t.toUpperCase()===e)};class ge extends HTMLElement{constructor(){super(...arguments);d(this,"_spec");d(this,"_inject");d(this,"_try")}connectedCallback(){this.childElementCount===0&&(this.innerHTML=`
128
+ <div class="op-card">
129
+ <dbh-spec-zone></dbh-spec-zone>
130
+ <dbh-inject-error-panel></dbh-inject-error-panel>
131
+ <dbh-try-it-out></dbh-try-it-out>
132
+ </div>`,this._spec=this.querySelector("dbh-spec-zone"),this._inject=this.querySelector("dbh-inject-error-panel"),this._try=this.querySelector("dbh-try-it-out"),this._inject.addEventListener("dbh-header-override",e=>{this._try.extraHeader=e.detail}))}setData(e){this._spec||this.connectedCallback();const{op:t,doc:s,overrides:n}=e,o=n.find(l=>l.matcher.kind==="exact"&&l.matcher.method===t.method&&l.matcher.path===t.path),r=n.some(l=>l.matcher.kind==="global"&&R(l.matcher,t.method));this._spec.setOp(t,s),this._inject.setOp({op:t,existing:o,globalActive:r}),this._try.setOp(t,s)}}customElements.define("dbh-operation-card",ge);class ke extends HTMLElement{constructor(){super(...arguments);d(this,"_label","");d(this,"_ops",[]);d(this,"_doc",null);d(this,"_overrides",[]);d(this,"_expandedKey",null);d(this,"_focusedKey",null);d(this,"_open",!0)}setData(e){this._label=e.label,this._ops=e.ops,this._doc=e.doc,this._overrides=e.overrides,this._expandedKey=e.expandedKey,this._focusedKey=e.focusedKey,this._render()}_render(){const e=this._ops.reduce((s,n)=>s+(this._overrides.some(o=>o.matcher.kind==="exact"&&o.matcher.method===n.method&&o.matcher.path===n.path)?1:0),0);this.innerHTML=`
133
+ <div class="group${this._open?" open":""}">
134
+ <div class="group-header" data-header>
135
+ <h3>${h(this._label)}</h3>
136
+ <span class="count">${this._ops.length} ops${e?` · ${e} active`:""}</span>
137
+ <span>${this._open?"−":"+"}</span>
138
+ </div>
139
+ <div class="group-body" data-body></div>
140
+ </div>`,this.querySelector("[data-header]")?.addEventListener("click",()=>{this._open=!this._open,this._render()});const t=this.querySelector("[data-body]");if(t)for(const s of this._ops){const n=`${s.method} ${s.path}`,o=this._overrides.find(l=>l.matcher.kind==="exact"&&l.matcher.method===s.method&&l.matcher.path===s.path),r=document.createElement("dbh-operation-row");if(r.setRow({op:s,override:o,expanded:this._expandedKey===n,focused:this._focusedKey===n}),t.appendChild(r),this._expandedKey===n){const l=document.createElement("dbh-operation-card");t.appendChild(l),l.setData({op:s,doc:this._doc,overrides:this._overrides})}}}}customElements.define("dbh-tag-group",ke);const $e=a=>{if(a.matcher.kind==="exact")return`${a.matcher.method} ${a.matcher.path}`;if(a.matcher.kind==="path")return`* ${a.matcher.path}`;const i=a.matcher.methods;return i&&i.length>0?`${i.join("|")} (global)`:"* (global)"},j=a=>a.matcher.kind==="global"?0:a.matcher.kind==="path"?1:2;class Ee extends HTMLElement{constructor(){super(...arguments);d(this,"_unsub")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){const e=c.get(),t=[...e.overrides].sort((s,n)=>j(s)-j(n));this.innerHTML=`
141
+ <aside class="right-panel${e.rightPanelOpen?" open":""}">
142
+ <header>
143
+ <h2>Active overrides (${e.overrides.length})</h2>
144
+ <button class="btn ghost" type="button" data-close>×</button>
145
+ </header>
146
+ <div class="list">
147
+ ${e.overrides.length===0?'<p style="padding:12px;color:#57606a">No active overrides</p>':t.map(s=>`
148
+ <div class="override-item${s.matcher.kind==="global"?" is-global":""}" data-jump="${h(s.id)}">
149
+ <span class="dot" style="background:${A(s.mode.kind)}"></span>
150
+ ${s.matcher.kind==="global"?'<span class="scope-badge scope-global">GLOBAL</span>':""}
151
+ <div class="info">
152
+ <div class="route">${h($e(s))}</div>
153
+ <small>${h(s.mode.kind)}${s.description?` — ${h(s.description)}`:""}</small>
154
+ </div>
155
+ <button class="btn danger" type="button" data-remove="${h(s.id)}">Remove</button>
156
+ </div>`).join("")}
157
+ </div>
158
+ </aside>`,this.querySelector("[data-close]")?.addEventListener("click",()=>c.set({rightPanelOpen:!1}));for(const s of this.querySelectorAll("[data-jump]"))s.addEventListener("click",()=>{const n=s.dataset.jump??"",o=c.get().overrides.find(l=>l.id===n);if(!o)return;if(o.matcher.kind==="global"){M();return}if(o.matcher.kind!=="exact"){c.set({rightPanelOpen:!1});return}const r=q(o.matcher.method,o.matcher.path);c.set({expandedKey:r,focusedKey:r,rightPanelOpen:!1}),setTimeout(()=>{const l=Array.from(document.querySelectorAll("[data-key]")).find(p=>p.getAttribute("data-key")===r);l&&"scrollIntoView"in l&&l.scrollIntoView({behavior:"smooth",block:"center"})},50)});for(const s of this.querySelectorAll("[data-remove]"))s.addEventListener("click",n=>{n.stopPropagation(),E(s.dataset.remove??"")})}}customElements.define("dbh-active-overrides-panel",Ee);class we extends HTMLElement{constructor(){super(...arguments);d(this,"_unsub")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){const t=c.get().overrides.length;if(t===0){this.innerHTML="";return}this.innerHTML=`<button class="pill-float" type="button">${t} Active overrides</button>`,this.querySelector("button")?.addEventListener("click",()=>c.set({rightPanelOpen:!0}))}}customElements.define("dbh-floating-pill",we);class Se extends HTMLElement{constructor(){super(...arguments);d(this,"_unsub")}connectedCallback(){this._render(Y()),this._unsub=Z(e=>this._render(e))}disconnectedCallback(){this._unsub?.()}_render(e){this.innerHTML='<div class="toast-stack">'+e.map(t=>`<div class="toast ${h(t.kind)}" data-id="${t.id}">${h(t.message)}</div>`).join("")+"</div>";for(const t of this.querySelectorAll(".toast"))t.addEventListener("click",()=>{const s=Number(t.dataset.id);Number.isFinite(s)&&W(s)})}}customElements.define("dbh-toasts",Se);class xe extends HTMLElement{constructor(){super(...arguments);d(this,"_unsub")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){if(!c.get().helpOpen){this.innerHTML="";return}this.innerHTML=`
159
+ <div class="modal-backdrop">
160
+ <div class="modal">
161
+ <h2>Keyboard shortcuts</h2>
162
+ <table>
163
+ <tbody>
164
+ <tr><td><kbd>/</kbd> or <kbd>Ctrl/⌘ + K</kbd></td><td>Focus search</td></tr>
165
+ <tr><td><kbd>Esc</kbd></td><td>Close panel / blur search</td></tr>
166
+ <tr><td><kbd>?</kbd></td><td>Open this help</td></tr>
167
+ <tr><td><kbd>↑</kbd> / <kbd>↓</kbd></td><td>Navigate operation list</td></tr>
168
+ <tr><td><kbd>Enter</kbd></td><td>Expand / collapse selected</td></tr>
169
+ </tbody>
170
+ </table>
171
+ <div style="margin-top:12px;text-align:right">
172
+ <button class="btn" type="button" data-ok>OK</button>
173
+ </div>
174
+ </div>
175
+ </div>`;const t=()=>c.set({helpOpen:!1});this.querySelector(".modal-backdrop")?.addEventListener("click",t),this.querySelector(".modal")?.addEventListener("click",s=>s.stopPropagation()),this.querySelector("[data-ok]")?.addEventListener("click",t)}}customElements.define("dbh-help-modal",xe);const Oe=["http-status","business-failure","custom-body","empty-body","malformed-json","delay","hang","destroy"],$=["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"];class Te extends HTMLElement{constructor(){super(...arguments);d(this,"_unsub");d(this,"_kind","http-status");d(this,"_status",503);d(this,"_message","");d(this,"_body","{}");d(this,"_contentType","application/json");d(this,"_ms",1e3);d(this,"_description","");d(this,"_methods",new Set($));d(this,"_lastExistingId")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_globals(){return c.get().overrides.filter(e=>e.matcher.kind==="global")}_syncFromExisting(e,t){if(t&&!(e&&e.id===this._lastExistingId))if(this._lastExistingId=e?.id,e){this._kind=e.mode.kind,this._description=e.description??"";const s=e.matcher.kind==="global"?e.matcher.methods:void 0;this._methods=new Set(s&&s.length>0?s.map(o=>o.toUpperCase()):$);const n=e.mode;n.kind==="http-status"?this._status=n.status:n.kind==="business-failure"?this._message=n.message:n.kind==="custom-body"?(this._status=n.status??200,this._body=JSON.stringify(n.body,null,2),this._contentType=n.contentType??"application/json"):n.kind==="empty-body"?this._status=n.status??204:n.kind==="delay"&&(this._ms=n.ms)}else this._kind="http-status",this._status=503,this._message="",this._body="{}",this._contentType="application/json",this._ms=1e3,this._description="",this._methods=new Set($)}_renderFieldsHtml(){const e=this._kind;return e==="http-status"||e==="empty-body"||e==="malformed-json"?`<div class="row"><label>Status</label><input type="number" data-field="status" value="${this._status}" /></div>`:e==="business-failure"?`<div class="row"><label>Message</label><input type="text" data-field="message" value="${h(this._message)}" /></div>`:e==="custom-body"?`
176
+ <div class="row"><label>Status</label><input type="number" data-field="status" value="${this._status}" /></div>
177
+ <div class="row"><label>Content-Type</label><input type="text" data-field="contentType" value="${h(this._contentType)}" /></div>
178
+ <div class="row"><label>Body</label><textarea data-field="body">${h(this._body)}</textarea></div>`:e==="delay"?`<div class="row"><label>Delay (ms)</label><input type="number" data-field="ms" value="${this._ms}" /></div>`:""}_render(){const e=c.get().globalOpen;if(!e){this.innerHTML="",this._lastExistingId=void 0;return}const t=this._globals(),s=t[0];this._syncFromExisting(s,e),this.innerHTML=`
179
+ <div class="modal-backdrop">
180
+ <div class="modal global-override-modal">
181
+ <header class="global-override-header">
182
+ <h2>Global override</h2>
183
+ <button type="button" class="btn ghost close-x" data-close aria-label="Close">×</button>
184
+ </header>
185
+ <p class="global-override-desc">Applies to every route on this server. Use to simulate the backend being down, slow, etc.</p>
186
+ <div class="row">
187
+ <label>Mode</label>
188
+ <select data-mode>${Oe.map(n=>`<option value="${n}"${n===this._kind?" selected":""}>${n}</option>`).join("")}</select>
189
+ </div>
190
+ <div class="row global-override-methods">
191
+ <label>Methods</label>
192
+ <div class="method-checkboxes">
193
+ ${$.map(n=>`
194
+ <label class="method-checkbox">
195
+ <input type="checkbox" data-method="${n}"${this._methods.has(n)?" checked":""} />
196
+ <span>${n}</span>
197
+ </label>`).join("")}
198
+ </div>
199
+ </div>
200
+ <div data-fields>${this._renderFieldsHtml()}</div>
201
+ <div class="row">
202
+ <label>Description</label>
203
+ <input type="text" data-field="description" value="${h(this._description)}" />
204
+ </div>
205
+ <div class="actions">
206
+ <button type="button" class="btn primary" data-submit>${s?"Update":"Apply"}</button>
207
+ ${s?`<button type="button" class="btn danger" data-remove="${h(s.id)}">Remove</button>`:""}
208
+ </div>
209
+ ${t.length>0?`
210
+ <div class="global-override-list">
211
+ <h3>Active global overrides (${t.length})</h3>
212
+ ${t.map(n=>{const o=n.matcher.kind==="global"?n.matcher.methods:void 0,r=o&&o.length>0?`${o.join("|")} (global)`:"* (global)";return`
213
+ <div class="override-item">
214
+ <span class="dot" style="background:${A(n.mode.kind)}"></span>
215
+ <div class="info">
216
+ <div class="route">${h(r)}</div>
217
+ <small>${h(n.mode.kind)}${n.description?` — ${h(n.description)}`:""}</small>
218
+ </div>
219
+ <button type="button" class="btn danger" data-remove="${h(n.id)}">Remove</button>
220
+ </div>`}).join("")}
221
+ </div>`:""}
222
+ </div>
223
+ </div>`,this._wire()}_wire(){const e=()=>c.set({globalOpen:!1});this.querySelector(".modal-backdrop")?.addEventListener("click",e),this.querySelector(".modal")?.addEventListener("click",s=>s.stopPropagation()),this.querySelector("[data-close]")?.addEventListener("click",e);const t=this.querySelector("[data-mode]");t?.addEventListener("change",()=>{this._kind=t.value;const s=this.querySelector("[data-fields]");s&&(s.innerHTML=this._renderFieldsHtml(),this._wireFields())}),this._wireFields();for(const s of this.querySelectorAll("[data-method]"))s.addEventListener("change",()=>{const n=s.dataset.method??"";s.checked?this._methods.add(n):this._methods.delete(n)});this.querySelector("[data-submit]")?.addEventListener("click",()=>this._submit());for(const s of this.querySelectorAll("[data-remove]"))s.addEventListener("click",()=>{E(s.dataset.remove??"")})}_wireFields(){const e=this.querySelector('[data-field="description"]');e?.addEventListener("input",()=>{this._description=e.value});for(const t of this.querySelectorAll("[data-fields] [data-field]"))t.addEventListener("input",()=>{const s=t.dataset.field;s==="status"?this._status=Number(t.value):s==="ms"?this._ms=Number(t.value):s==="message"?this._message=t.value:s==="body"?this._body=t.value:s==="contentType"&&(this._contentType=t.value)})}_submit(){let e;const t=this._kind;switch(t){case"http-status":e={kind:t,status:this._status};break;case"business-failure":e={kind:t,message:this._message},I(this._message)||N(this._message);break;case"custom-body":{let r;try{r=JSON.parse(this._body)}catch{r=this._body}e={kind:t,status:this._status,body:r,contentType:this._contentType};break}case"empty-body":e={kind:t,status:this._status};break;case"malformed-json":e={kind:t,status:this._status};break;case"delay":e={kind:t,ms:this._ms};break;case"hang":e={kind:t};break;case"destroy":e={kind:t};break}const s=$.length,n=$.filter(r=>this._methods.has(r));if(n.length===0){b("error","Select at least one HTTP method.");return}const o=n.length===s?{kind:"global"}:{kind:"global",methods:n};(async()=>{const r=this._globals()[0];if(r)try{await E(r.id)}catch{}try{await P(o,e,this._description||void 0)}catch(l){b("error",l instanceof Error?l.message:String(l))}})()}}customElements.define("dbh-global-override-panel",Te);class Le extends HTMLElement{constructor(){super(...arguments);d(this,"_filter");d(this,"_main");d(this,"_unsub");d(this,"_debounce");d(this,"_debouncedSearch","");d(this,"_onKeyDown",e=>this._handleKey(e))}connectedCallback(){this.innerHTML=`
224
+ <div class="app">
225
+ <dbh-top-bar></dbh-top-bar>
226
+ <dbh-filter-bar></dbh-filter-bar>
227
+ <main class="main" data-main></main>
228
+ <dbh-active-overrides-panel></dbh-active-overrides-panel>
229
+ <dbh-floating-pill></dbh-floating-pill>
230
+ <dbh-toasts></dbh-toasts>
231
+ <dbh-help-modal></dbh-help-modal>
232
+ <dbh-global-override-panel></dbh-global-override-panel>
233
+ </div>`,this._filter=this.querySelector("dbh-filter-bar"),this._main=this.querySelector("[data-main]"),this._unsub=c.subscribe(()=>this._onStateChange()),window.addEventListener("keydown",this._onKeyDown),(async()=>{const[e,t]=await Promise.all([f.listRoutes(),f.fetchOpenApi()]);c.set({routes:e.routes,scenariosEnabled:e.features.scenarios,doc:t??null,loading:!1}),await w(),await C(),this._renderList()})()}disconnectedCallback(){this._unsub?.(),window.removeEventListener("keydown",this._onKeyDown),this._debounce&&clearTimeout(this._debounce)}_onStateChange(){c.get().search!==this._debouncedSearch?(this._debounce&&clearTimeout(this._debounce),this._debounce=setTimeout(()=>{this._debouncedSearch=c.get().search,this._renderList()},150)):this._renderList()}_filtered(){const e=c.get(),t=z(e.routes,e.doc),s=this._debouncedSearch.trim().toLowerCase();return t.filter(n=>!(!B(n.method,e.methods)||s&&!n.path.toLowerCase().includes(s)&&!n.op.summary?.toLowerCase().includes(s)||e.activeOnly&&!e.overrides.some(r=>r.matcher.kind==="exact"&&r.matcher.method===n.method&&r.matcher.path===n.path||r.matcher.kind==="path"&&r.matcher.path===n.path||r.matcher.kind==="global"&&R(r.matcher,n.method))))}_renderList(){const e=c.get(),t=this._filtered(),s=J(t,V(e.overrides));if(this._main.innerHTML="",s.length===0){const n=document.createElement("p");n.style.color="#57606a",n.textContent="No operations match these filters.",this._main.appendChild(n);return}for(const n of s){const o=document.createElement("dbh-tag-group");this._main.appendChild(o),o.setData({label:n.label,ops:n.ops,doc:e.doc,overrides:e.overrides,expandedKey:e.expandedKey,focusedKey:e.focusedKey})}}_handleKey(e){const t=e.target,s=!!t&&(t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.tagName==="SELECT"),n=this._filter?.searchInput,o=c.get();if(e.key==="/"&&!s)e.preventDefault(),n?.focus();else if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="k")e.preventDefault(),n?.focus();else if(e.key==="?"&&!s)e.preventDefault(),c.set({helpOpen:!0});else if(e.key==="Escape")o.helpOpen?c.set({helpOpen:!1}):o.globalOpen?c.set({globalOpen:!1}):o.rightPanelOpen?c.set({rightPanelOpen:!1}):o.expandedKey?c.set({expandedKey:null}):n?.blur();else if((e.key==="ArrowDown"||e.key==="ArrowUp")&&document.activeElement===n){e.preventDefault();const r=this._filtered().map(u=>q(u.method,u.path));if(r.length===0)return;const l=o.focusedKey?r.indexOf(o.focusedKey):-1,p=e.key==="ArrowDown"?Math.min(r.length-1,l+1):Math.max(0,l-1);c.set({focusedKey:r[p]??null})}else e.key==="Enter"&&document.activeElement===n&&o.focusedKey&&(e.preventDefault(),c.set({expandedKey:o.expandedKey===o.focusedKey?null:o.focusedKey}))}}customElements.define("dbh-app",Le);const D=document.getElementById("root");if(D){const a=document.createElement("dbh-app");D.appendChild(a)}
@@ -0,0 +1,23 @@
1
+ import { type Plugin } from '@databehave/core';
2
+ import type { ScenariosStore } from './scenarios.js';
3
+ import type { AdminModeCors } from './admin-types.js';
4
+ /** Admin presentation and explicitly scoped persistence. Presence opts into the feature. */
5
+ export interface AdminOptions {
6
+ readonly path?: string;
7
+ readonly ui?: boolean;
8
+ readonly bind?: 'loopback-only' | 'any';
9
+ readonly cors?: AdminModeCors;
10
+ /** Explicit storage provider; omission disables scenario endpoints. */
11
+ readonly scenarios?: ScenariosStore;
12
+ /** Optional prepared document for rich UI details; never used to discover routes. */
13
+ readonly openapiBody?: () => string | undefined;
14
+ }
15
+ /** Presentation metadata available to optional browser-opening plugins. */
16
+ export interface AdminInfo {
17
+ readonly path: string;
18
+ readonly ui: boolean;
19
+ }
20
+ export declare const adminCapability: import("@databehave/core").Capability<AdminInfo>;
21
+ /** Register the admin REST API and optional assets, consuming HTTP and fault control. */
22
+ export declare const adminPlugin: (options?: AdminOptions) => Plugin;
23
+ //# sourceMappingURL=admin-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-plugin.d.ts","sourceRoot":"","sources":["../src/admin-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAI1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,GAAG,KAAK,CAAA;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAA;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAA;IACnC,qFAAqF;IACrF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;CAChD;AAED,2EAA2E;AAC3E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;CACrB;AAED,eAAO,MAAM,eAAe,kDAA+C,CAAA;AAE3E,yFAAyF;AACzF,eAAO,MAAM,WAAW,GAAI,UAAS,YAAiB,KAAG,MAyDxD,CAAA"}
@@ -0,0 +1,71 @@
1
+ import { capability } from '@databehave/core';
2
+ import { httpCapability } from '@databehave/server';
3
+ import { createAdminRoutes } from './admin-routes.js';
4
+ import { faultControlCapability } from './fault-plugin.js';
5
+ import { isAdminOriginAllowed, resolveAdminCors } from './resolve-cors.js';
6
+ export const adminCapability = capability('databehave.admin', 1);
7
+ /** Register the admin REST API and optional assets, consuming HTTP and fault control. */
8
+ export const adminPlugin = (options = {}) => {
9
+ const path = (options.path ?? '/databehave').replace(/\/$/, '');
10
+ if (!path.startsWith('/') || !/^\/[A-Za-z0-9_./~-]+$/.test(path) || path.includes('..')) {
11
+ throw new Error('admin.path must start with "/" and contain safe URL path characters');
12
+ }
13
+ const bind = options.bind ?? 'loopback-only';
14
+ const cors = options.cors ?? 'auto';
15
+ const ownsPath = (requestPath) => requestPath === path || requestPath.startsWith(`${path}/`);
16
+ return {
17
+ id: 'databehave.admin', apiVersion: 1,
18
+ requires: [httpCapability, faultControlCapability], provides: [adminCapability],
19
+ setup(ctx) {
20
+ const http = ctx.use(httpCapability);
21
+ const faults = ctx.use(faultControlCapability);
22
+ faults.excludePathPrefix(path);
23
+ ctx.provide(adminCapability, { path, ui: options.ui !== false });
24
+ http.registerBindGuard({ owner: 'databehave.admin', check({ host }) {
25
+ if (bind !== 'any' && host !== '127.0.0.1' && host !== '::1' && host !== 'localhost') {
26
+ throw new Error(`admin is enabled with loopback-only binding; refusing ${host}`);
27
+ }
28
+ } });
29
+ http.registerMiddleware({ owner: 'databehave.admin', order: ctx.order, stage: 'preflight', handle(request) {
30
+ if (!ownsPath(request.path))
31
+ return;
32
+ if (!isAdminOriginAllowed(bind, cors, request.headers.origin, request.url)) {
33
+ return { status: 403, json: { error: 'Admin origin is not allowed' } };
34
+ }
35
+ if (request.method === 'options') {
36
+ const target = http.resolveRoute(request.headers['access-control-request-method'] ?? 'options', request.path);
37
+ return target ? { status: 204, empty: true } : { status: 404, json: { error: 'Admin route not found' } };
38
+ }
39
+ } });
40
+ http.registerMiddleware({ owner: 'databehave.admin', order: ctx.order, stage: 'decoration', handle(request, response) {
41
+ if (!ownsPath(request.path))
42
+ return;
43
+ const headers = new Headers(response.headers);
44
+ for (const name of [...headers.keys()])
45
+ if (name.startsWith('access-control-'))
46
+ headers.delete(name);
47
+ if (isAdminOriginAllowed(bind, cors, request.headers.origin, request.url)) {
48
+ for (const [name, value] of Object.entries(resolveAdminCors(bind, cors, request.headers.origin)))
49
+ headers.set(name, value);
50
+ }
51
+ const vary = headers.get('vary')?.split(',').map(value => value.trim()) ?? [];
52
+ if (!vary.some(value => value.toLowerCase() === 'origin' || value === '*'))
53
+ vary.push('Origin');
54
+ headers.set('vary', vary.join(', '));
55
+ return { ...response, headers: Object.fromEntries(headers) };
56
+ } });
57
+ const routes = createAdminRoutes({ store: faults, basePath: path,
58
+ catalog: () => http.routes,
59
+ ...(options.ui !== undefined ? { ui: options.ui } : {}),
60
+ ...(options.openapiBody ? { openapiBody: options.openapiBody } : {}),
61
+ ...(options.scenarios ? { scenarios: options.scenarios } : {}),
62
+ });
63
+ http.registerRoutes({ owner: 'databehave.admin', order: ctx.order, materialize: () => Object.keys(routes).flatMap((key) => {
64
+ const handler = routes[key];
65
+ return handler ? [{ key, handler, priority: 'explicit', source: 'admin', cors: false }] : [];
66
+ }),
67
+ });
68
+ },
69
+ };
70
+ };
71
+ //# sourceMappingURL=admin-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-plugin.js","sourceRoot":"","sources":["../src/admin-plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { Handler, RouteKey } from '@databehave/server';
2
+ import type { ScenariosStore } from './scenarios.js';
3
+ import type { ResolvedRoute } from '@databehave/server';
4
+ import type { FaultControl } from './fault-plugin.js';
5
+ /** Internal HTTP adapter options; the plugin owns registration and dependencies. */
6
+ export interface AdminRoutesOptions {
7
+ readonly store: Pick<FaultControl, 'add' | 'remove' | 'list' | 'clear' | 'replace'>;
8
+ /** Mount point. Default '/databehave'. Must start with '/'. */
9
+ readonly basePath?: string;
10
+ /** Serve the UI HTML page. Default true. */
11
+ readonly ui?: boolean;
12
+ /** CORS headers added to every admin response. Empty = same-origin. */
13
+ readonly corsHeaders?: Readonly<Record<string, string>>;
14
+ /** Raw OpenAPI document text — used to populate the route picker. */
15
+ readonly openapiBody?: () => string | undefined;
16
+ /** Resolved HTTP routes; read only after HTTP activation. */
17
+ readonly catalog?: () => readonly ResolvedRoute[];
18
+ /** Optional scenarios store. When omitted, scenarios endpoints are not mounted. */
19
+ readonly scenarios?: ScenariosStore;
20
+ }
21
+ export declare const createAdminRoutes: (opts: AdminRoutesOptions) => Partial<Record<RouteKey, Handler>>;
22
+ //# sourceMappingURL=admin-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-routes.d.ts","sourceRoot":"","sources":["../src/admin-routes.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EACV,OAAO,EAGP,QAAQ,EACT,MAAM,oBAAoB,CAAA;AAE3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAkCrD,oFAAoF;AACpF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;IACnF,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO,CAAA;IACrB,uEAAuE;IACvE,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACvD,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,SAAS,aAAa,EAAE,CAAA;IACjD,mFAAmF;IACnF,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAA;CACpC;AAED,eAAO,MAAM,iBAAiB,GAC5B,MAAM,kBAAkB,KACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAsRnC,CAAA"}