@databehave/server 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 (162) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/dist/admin/admin-routes.d.ts +42 -0
  5. package/dist/admin/admin-routes.d.ts.map +1 -0
  6. package/dist/admin/admin-routes.js +472 -0
  7. package/dist/admin/admin-routes.js.map +1 -0
  8. package/dist/admin/admin-types.d.ts +170 -0
  9. package/dist/admin/admin-types.d.ts.map +1 -0
  10. package/dist/admin/admin-types.js +30 -0
  11. package/dist/admin/admin-types.js.map +1 -0
  12. package/dist/admin/banner.d.ts +11 -0
  13. package/dist/admin/banner.d.ts.map +1 -0
  14. package/dist/admin/banner.js +5 -0
  15. package/dist/admin/banner.js.map +1 -0
  16. package/dist/admin/header-parser.d.ts +14 -0
  17. package/dist/admin/header-parser.d.ts.map +1 -0
  18. package/dist/admin/header-parser.js +171 -0
  19. package/dist/admin/header-parser.js.map +1 -0
  20. package/dist/admin/inject.d.ts +48 -0
  21. package/dist/admin/inject.d.ts.map +1 -0
  22. package/dist/admin/inject.js +167 -0
  23. package/dist/admin/inject.js.map +1 -0
  24. package/dist/admin/overrides-store.d.ts +18 -0
  25. package/dist/admin/overrides-store.d.ts.map +1 -0
  26. package/dist/admin/overrides-store.js +100 -0
  27. package/dist/admin/overrides-store.js.map +1 -0
  28. package/dist/admin/resolve-cors.d.ts +22 -0
  29. package/dist/admin/resolve-cors.d.ts.map +1 -0
  30. package/dist/admin/resolve-cors.js +28 -0
  31. package/dist/admin/resolve-cors.js.map +1 -0
  32. package/dist/admin/scenarios-store.d.ts +15 -0
  33. package/dist/admin/scenarios-store.d.ts.map +1 -0
  34. package/dist/admin/scenarios-store.js +109 -0
  35. package/dist/admin/scenarios-store.js.map +1 -0
  36. package/dist/admin/ui.css +2 -0
  37. package/dist/admin/ui.html +13 -0
  38. package/dist/admin/ui.js +240 -0
  39. package/dist/bin-helpers.d.ts +46 -0
  40. package/dist/bin-helpers.d.ts.map +1 -0
  41. package/dist/bin-helpers.js +61 -0
  42. package/dist/bin-helpers.js.map +1 -0
  43. package/dist/bin.d.ts +3 -0
  44. package/dist/bin.d.ts.map +1 -0
  45. package/dist/bin.js +57 -0
  46. package/dist/bin.js.map +1 -0
  47. package/dist/cors.d.ts +57 -0
  48. package/dist/cors.d.ts.map +1 -0
  49. package/dist/cors.js +167 -0
  50. package/dist/cors.js.map +1 -0
  51. package/dist/define.d.ts +26 -0
  52. package/dist/define.d.ts.map +1 -0
  53. package/dist/define.js +55 -0
  54. package/dist/define.js.map +1 -0
  55. package/dist/index.d.ts +21 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +19 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/json-config.d.ts +170 -0
  60. package/dist/json-config.d.ts.map +1 -0
  61. package/dist/json-config.js +684 -0
  62. package/dist/json-config.js.map +1 -0
  63. package/dist/jsonc.d.ts +24 -0
  64. package/dist/jsonc.d.ts.map +1 -0
  65. package/dist/jsonc.js +125 -0
  66. package/dist/jsonc.js.map +1 -0
  67. package/dist/log.d.ts +57 -0
  68. package/dist/log.d.ts.map +1 -0
  69. package/dist/log.js +103 -0
  70. package/dist/log.js.map +1 -0
  71. package/dist/middleware/request-validation.d.ts +58 -0
  72. package/dist/middleware/request-validation.d.ts.map +1 -0
  73. package/dist/middleware/request-validation.js +328 -0
  74. package/dist/middleware/request-validation.js.map +1 -0
  75. package/dist/mock-mode.d.ts +95 -0
  76. package/dist/mock-mode.d.ts.map +1 -0
  77. package/dist/mock-mode.js +95 -0
  78. package/dist/mock-mode.js.map +1 -0
  79. package/dist/openapi/auto-schema.d.ts +83 -0
  80. package/dist/openapi/auto-schema.d.ts.map +1 -0
  81. package/dist/openapi/auto-schema.js +232 -0
  82. package/dist/openapi/auto-schema.js.map +1 -0
  83. package/dist/openapi/generate.d.ts +20 -0
  84. package/dist/openapi/generate.d.ts.map +1 -0
  85. package/dist/openapi/generate.js +121 -0
  86. package/dist/openapi/generate.js.map +1 -0
  87. package/dist/openapi/loader.d.ts +18 -0
  88. package/dist/openapi/loader.d.ts.map +1 -0
  89. package/dist/openapi/loader.js +25 -0
  90. package/dist/openapi/loader.js.map +1 -0
  91. package/dist/openapi/register.d.ts +58 -0
  92. package/dist/openapi/register.d.ts.map +1 -0
  93. package/dist/openapi/register.js +166 -0
  94. package/dist/openapi/register.js.map +1 -0
  95. package/dist/openapi/seed.d.ts +16 -0
  96. package/dist/openapi/seed.d.ts.map +1 -0
  97. package/dist/openapi/seed.js +58 -0
  98. package/dist/openapi/seed.js.map +1 -0
  99. package/dist/openapi/translate.d.ts +55 -0
  100. package/dist/openapi/translate.d.ts.map +1 -0
  101. package/dist/openapi/translate.js +319 -0
  102. package/dist/openapi/translate.js.map +1 -0
  103. package/dist/openapi/walker.d.ts +44 -0
  104. package/dist/openapi/walker.d.ts.map +1 -0
  105. package/dist/openapi/walker.js +15 -0
  106. package/dist/openapi/walker.js.map +1 -0
  107. package/dist/request.d.ts +19 -0
  108. package/dist/request.d.ts.map +1 -0
  109. package/dist/request.js +66 -0
  110. package/dist/request.js.map +1 -0
  111. package/dist/response.d.ts +13 -0
  112. package/dist/response.d.ts.map +1 -0
  113. package/dist/response.js +43 -0
  114. package/dist/response.js.map +1 -0
  115. package/dist/route-key.d.ts +60 -0
  116. package/dist/route-key.d.ts.map +1 -0
  117. package/dist/route-key.js +105 -0
  118. package/dist/route-key.js.map +1 -0
  119. package/dist/run.d.ts +27 -0
  120. package/dist/run.d.ts.map +1 -0
  121. package/dist/run.js +87 -0
  122. package/dist/run.js.map +1 -0
  123. package/dist/server.d.ts +60 -0
  124. package/dist/server.d.ts.map +1 -0
  125. package/dist/server.js +497 -0
  126. package/dist/server.js.map +1 -0
  127. package/dist/types.d.ts +274 -0
  128. package/dist/types.d.ts.map +1 -0
  129. package/dist/types.js +9 -0
  130. package/dist/types.js.map +1 -0
  131. package/dist/validation/validate.d.ts +38 -0
  132. package/dist/validation/validate.d.ts.map +1 -0
  133. package/dist/validation/validate.js +372 -0
  134. package/dist/validation/validate.js.map +1 -0
  135. package/docs/admin/error-mode.md +14 -0
  136. package/docs/admin/headers.md +63 -0
  137. package/docs/admin/index.md +111 -0
  138. package/docs/admin/overrides.md +23 -0
  139. package/docs/admin/rest-api.md +90 -0
  140. package/docs/admin/scenarios.md +28 -0
  141. package/docs/cli.md +76 -0
  142. package/docs/config/admin.md +62 -0
  143. package/docs/config/cors.md +72 -0
  144. package/docs/config/endpoints.md +162 -0
  145. package/docs/config/index.md +107 -0
  146. package/docs/config/log.md +43 -0
  147. package/docs/config/mock-mode.md +84 -0
  148. package/docs/config/openapi.md +8 -0
  149. package/docs/config/schema.md +34 -0
  150. package/docs/config/server.md +32 -0
  151. package/docs/config/validation.md +161 -0
  152. package/docs/design.md +517 -0
  153. package/docs/errors.md +192 -0
  154. package/docs/getting-started.md +146 -0
  155. package/docs/index.md +23 -0
  156. package/docs/openapi/auto-schema.md +146 -0
  157. package/docs/openapi/fallback.md +242 -0
  158. package/docs/openapi/keywords.md +78 -0
  159. package/docs/recipes.md +258 -0
  160. package/docs/stability.md +118 -0
  161. package/docs/troubleshooting.md +191 -0
  162. package/package.json +80 -0
@@ -0,0 +1,167 @@
1
+ import { parseHeaderMode } from './header-parser.js';
2
+ import { DESTROY_SOCKET_SENTINEL } from './admin-types.js';
3
+ const TAG_HEADER = 'x-mock-injected';
4
+ /**
5
+ * Pure resolver: header overrides beat sticky overrides. Returns
6
+ * `'none'` when nothing applies and `'error'` for malformed headers.
7
+ */
8
+ export const resolveMode = (store, method, path, headers) => {
9
+ const fromHeader = parseHeaderMode(headers);
10
+ if (fromHeader.kind === 'error')
11
+ return { kind: 'error', message: fromHeader.message };
12
+ if (fromHeader.kind === 'mode') {
13
+ return { kind: 'mode', mode: fromHeader.mode, source: 'header' };
14
+ }
15
+ const sticky = store.resolve(method, path);
16
+ if (sticky)
17
+ return { kind: 'mode', mode: sticky.mode, source: 'sticky' };
18
+ return { kind: 'none' };
19
+ };
20
+ const buildResponse = (mode, source) => {
21
+ const tagHeaders = {
22
+ [TAG_HEADER]: `${mode.kind}:${source}`,
23
+ };
24
+ switch (mode.kind) {
25
+ case 'http-status':
26
+ return {
27
+ status: mode.status,
28
+ json: { error: true, status: mode.status, injected: mode.kind },
29
+ headers: tagHeaders,
30
+ };
31
+ case 'business-failure':
32
+ return {
33
+ status: 200,
34
+ json: {
35
+ success: false,
36
+ message: mode.message,
37
+ ...(mode.extra ?? {}),
38
+ },
39
+ headers: { ...tagHeaders, 'content-type': 'application/json' },
40
+ };
41
+ case 'custom-body': {
42
+ const status = mode.status ?? 200;
43
+ const contentType = mode.contentType ?? 'application/json';
44
+ if (typeof mode.body === 'string' && !contentType.includes('json')) {
45
+ return {
46
+ status,
47
+ text: mode.body,
48
+ headers: { ...tagHeaders, 'content-type': contentType },
49
+ };
50
+ }
51
+ const payload = typeof mode.body === 'string' ? mode.body : JSON.stringify(mode.body);
52
+ return {
53
+ status,
54
+ raw: payload,
55
+ headers: { ...tagHeaders, 'content-type': contentType },
56
+ };
57
+ }
58
+ case 'empty-body':
59
+ return {
60
+ status: mode.status ?? 204,
61
+ empty: true,
62
+ headers: tagHeaders,
63
+ };
64
+ case 'malformed-json':
65
+ return {
66
+ status: mode.status ?? 200,
67
+ raw: '{',
68
+ headers: { ...tagHeaders, 'content-type': 'application/json' },
69
+ };
70
+ /* v8 ignore start -- defensive: delay/hang/destroy never reach here */
71
+ case 'delay':
72
+ case 'hang':
73
+ case 'destroy':
74
+ // delay/hang are handled before this function; destroy is dispatched
75
+ // through the sentinel branch. Reaching here is a bug.
76
+ return {
77
+ status: 500,
78
+ json: {
79
+ error: true,
80
+ message: `inject: ${mode.kind} should be handled before buildResponse`,
81
+ },
82
+ headers: tagHeaders,
83
+ };
84
+ /* v8 ignore stop */
85
+ }
86
+ };
87
+ const buildDestroyDisabledResponse = () => ({
88
+ status: 503,
89
+ json: {
90
+ error: 'x-mock-destroy disabled by admin.allowDestroy:false',
91
+ },
92
+ headers: { [TAG_HEADER]: 'destroy-disabled' },
93
+ });
94
+ /**
95
+ * Sentinel response: shaped as a normal empty 204 so anything that
96
+ * inspects the public type sees a valid response, but tagged with
97
+ * {@link DESTROY_SOCKET_SENTINEL} so `server.listen()` can drop the
98
+ * socket instead of writing the body.
99
+ */
100
+ const buildDestroySentinel = () => {
101
+ const base = {
102
+ status: 204,
103
+ empty: true,
104
+ headers: { [TAG_HEADER]: 'destroy' },
105
+ };
106
+ Object.defineProperty(base, DESTROY_SOCKET_SENTINEL, {
107
+ value: true,
108
+ enumerable: false,
109
+ writable: false,
110
+ configurable: false,
111
+ });
112
+ return base;
113
+ };
114
+ export const createInjectHook = (opts) => {
115
+ const { store } = opts;
116
+ const bypass = opts.bypassPathPrefixes ?? ['/databehave'];
117
+ const allowDestroy = opts.allowDestroy !== false;
118
+ const logger = opts.logger ?? console;
119
+ const shouldBypass = (path) => bypass.some((p) => path.startsWith(p));
120
+ return async (req) => {
121
+ if (shouldBypass(req.path))
122
+ return undefined;
123
+ const resolved = resolveMode(store, req.method, req.path, req.headers);
124
+ if (resolved.kind === 'none')
125
+ return undefined;
126
+ if (resolved.kind === 'error') {
127
+ return {
128
+ status: 400,
129
+ json: { error: 'invalid x-mock-* header', detail: resolved.message },
130
+ headers: { [TAG_HEADER]: 'header-error' },
131
+ };
132
+ }
133
+ const { mode, source } = resolved;
134
+ if (mode.kind === 'hang') {
135
+ logger.warn(`[@databehave/server/admin] hang triggered on ${req.method} ${req.path} ` +
136
+ `(source=${source}) — request will never resolve; close the client to recover.`);
137
+ /* v8 ignore start -- promise never resolves; return is unreachable */
138
+ await new Promise(() => {
139
+ /* never resolves */
140
+ });
141
+ return undefined;
142
+ /* v8 ignore stop */
143
+ }
144
+ if (mode.kind === 'destroy') {
145
+ if (!allowDestroy) {
146
+ logger.warn(`[@databehave/server/admin] destroy requested on ${req.method} ${req.path} ` +
147
+ `(source=${source}) but admin.allowDestroy:false — responding 503.`);
148
+ return buildDestroyDisabledResponse();
149
+ }
150
+ return buildDestroySentinel();
151
+ }
152
+ if (mode.kind === 'delay') {
153
+ await new Promise((r) => setTimeout(r, mode.ms));
154
+ if (mode.then === undefined)
155
+ return undefined;
156
+ return buildResponse(mode.then, source);
157
+ }
158
+ return buildResponse(mode, source);
159
+ };
160
+ };
161
+ /** True when the response carries the internal destroy-socket sentinel. */
162
+ export const isDestroySocketSentinel = (res) => {
163
+ if (!res)
164
+ return false;
165
+ return res[DESTROY_SOCKET_SENTINEL] === true;
166
+ };
167
+ //# sourceMappingURL=inject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inject.js","sourceRoot":"","sources":["../../src/admin/inject.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { ErrorMode, StickyMatcher, StickyOverride } from './admin-types.js';
2
+ export interface OverridesStoreLogger {
3
+ warn(message: string): void;
4
+ info?(message: string): void;
5
+ }
6
+ export interface OverridesStore {
7
+ add(input: {
8
+ matcher: StickyMatcher;
9
+ mode: ErrorMode;
10
+ description?: string;
11
+ }): StickyOverride;
12
+ remove(id: string): boolean;
13
+ list(): readonly StickyOverride[];
14
+ clear(): number;
15
+ resolve(method: string, path: string): StickyOverride | undefined;
16
+ }
17
+ export declare const createOverridesStore: (logger?: OverridesStoreLogger) => OverridesStore;
18
+ //# sourceMappingURL=overrides-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overrides-store.d.ts","sourceRoot":"","sources":["../../src/admin/overrides-store.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEhF,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,KAAK,EAAE;QACT,OAAO,EAAE,aAAa,CAAA;QACtB,IAAI,EAAE,SAAS,CAAA;QACf,WAAW,CAAC,EAAE,MAAM,CAAA;KACrB,GAAG,cAAc,CAAA;IAClB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,IAAI,IAAI,SAAS,cAAc,EAAE,CAAA;IACjC,KAAK,IAAI,MAAM,CAAA;IACf,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAA;CAClE;AAED,eAAO,MAAM,oBAAoB,GAC/B,SAAQ,oBAA8B,KACrC,cAkGF,CAAA"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * In-memory sticky overrides registry. Lost on process restart by
3
+ * design — this tool is for ad-hoc dev sessions, not persistence.
4
+ *
5
+ * Resolution priority (first hit wins): `exact` > `path` > `global`.
6
+ * When two stickies live at the same priority level, the **last** one
7
+ * added wins (and a warning is logged) so the dev's most-recent click
8
+ * in the UI is always the active rule.
9
+ */
10
+ import { randomUUID } from 'node:crypto';
11
+ export const createOverridesStore = (logger = console) => {
12
+ // Insertion-ordered. `Map.values()` yields oldest → newest, so
13
+ // when iterating we keep track of the *last* hit per priority bucket.
14
+ const byId = new Map();
15
+ const sameMatcher = (a, b) => {
16
+ if (a.kind !== b.kind)
17
+ return false;
18
+ if (a.kind === 'exact' && b.kind === 'exact') {
19
+ return (a.method.toUpperCase() === b.method.toUpperCase() && a.path === b.path);
20
+ }
21
+ if (a.kind === 'path' && b.kind === 'path')
22
+ return a.path === b.path;
23
+ return a.kind === 'global' && b.kind === 'global';
24
+ };
25
+ const describeMatcher = (m) => {
26
+ if (m.kind === 'exact')
27
+ return `${m.method.toUpperCase()} ${m.path}`;
28
+ if (m.kind === 'path')
29
+ return `* ${m.path}`;
30
+ return '* (global)';
31
+ };
32
+ const add = ({ matcher, mode, description }) => {
33
+ // Detect same-matcher collision so the warning is informative.
34
+ for (const existing of byId.values()) {
35
+ if (sameMatcher(existing.matcher, matcher)) {
36
+ logger.warn(`[mock-server/overrides] duplicate matcher ${describeMatcher(matcher)} ` +
37
+ `— new rule shadows ${existing.id} (last-wins). Remove the old one if unintended.`);
38
+ }
39
+ }
40
+ const override = {
41
+ id: randomUUID(),
42
+ matcher: matcher.kind === 'exact'
43
+ ? {
44
+ kind: 'exact',
45
+ method: matcher.method.toUpperCase(),
46
+ path: matcher.path,
47
+ }
48
+ : matcher.kind === 'global' && matcher.methods && matcher.methods.length > 0
49
+ ? {
50
+ kind: 'global',
51
+ methods: matcher.methods.map((m) => m.toUpperCase()),
52
+ }
53
+ : matcher.kind === 'global'
54
+ ? { kind: 'global' }
55
+ : matcher,
56
+ mode,
57
+ createdAt: new Date().toISOString(),
58
+ ...(description !== undefined ? { description } : {}),
59
+ };
60
+ byId.set(override.id, override);
61
+ return override;
62
+ };
63
+ const remove = (id) => byId.delete(id);
64
+ const list = () => [...byId.values()];
65
+ const clear = () => {
66
+ const n = byId.size;
67
+ byId.clear();
68
+ return n;
69
+ };
70
+ const resolve = (method, path) => {
71
+ const m = method.toUpperCase();
72
+ // Walk newest → oldest so the last-added rule wins per bucket.
73
+ const all = [...byId.values()].reverse();
74
+ let exactHit;
75
+ let pathHit;
76
+ let globalHit;
77
+ for (const o of all) {
78
+ if (o.matcher.kind === 'exact') {
79
+ if (!exactHit && o.matcher.method === m && o.matcher.path === path) {
80
+ exactHit = o;
81
+ }
82
+ }
83
+ else if (o.matcher.kind === 'path') {
84
+ if (!pathHit && o.matcher.path === path)
85
+ pathHit = o;
86
+ }
87
+ else {
88
+ if (!globalHit
89
+ && (!o.matcher.methods
90
+ || o.matcher.methods.length === 0
91
+ || o.matcher.methods.includes(m))) {
92
+ globalHit = o;
93
+ }
94
+ }
95
+ }
96
+ return exactHit ?? pathHit ?? globalHit;
97
+ };
98
+ return { add, remove, list, clear, resolve };
99
+ };
100
+ //# sourceMappingURL=overrides-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overrides-store.js","sourceRoot":"","sources":["../../src/admin/overrides-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Resolve the static CORS header bag added to admin-route responses.
3
+ *
4
+ * The policy is intentionally tiny: admin endpoints are dev-only and
5
+ * never see browser credentials, so a full `Access-Control-*`
6
+ * negotiation would be overkill. We just need to decide whether the
7
+ * UI / a bookmarklet on a different origin can XHR into them.
8
+ *
9
+ * Defaults:
10
+ * - `'auto'` + loopback-only bind → `'*'` (UI on any localhost port works)
11
+ * - `'auto'` + bind: 'any' → `'same-origin'` (LAN-exposed: lock down)
12
+ * - `'any'` → `'*'`
13
+ * - `'same-origin'` → no CORS headers added
14
+ * - `{ origin: 'x' | ['x', ...] }` → first allowed origin (string join with ', ')
15
+ *
16
+ * Returned object is a `Record<string, string>` ready to splat into
17
+ * `MockResponse.headers`. Empty object when no CORS headers
18
+ * should be added (same-origin).
19
+ */
20
+ import type { AdminModeCors, AdminModeConfig } from './admin-types.js';
21
+ export declare const resolveAdminCors: (bind: NonNullable<AdminModeConfig["bind"]>, cors: AdminModeCors) => Record<string, string>;
22
+ //# sourceMappingURL=resolve-cors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-cors.d.ts","sourceRoot":"","sources":["../../src/admin/resolve-cors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAuBtE,eAAO,MAAM,gBAAgB,GAC3B,MAAM,WAAW,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,EAC1C,MAAM,aAAa,KAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAQvB,CAAA"}
@@ -0,0 +1,28 @@
1
+ const PERMISSIVE_METHODS = 'GET,POST,DELETE,OPTIONS';
2
+ const PERMISSIVE_HEADERS = 'content-type';
3
+ const wildcardHeaders = () => ({
4
+ 'access-control-allow-origin': '*',
5
+ 'access-control-allow-methods': PERMISSIVE_METHODS,
6
+ 'access-control-allow-headers': PERMISSIVE_HEADERS,
7
+ });
8
+ const explicitOriginHeaders = (origin) => {
9
+ const value = Array.isArray(origin) ? origin.join(', ') : origin;
10
+ return {
11
+ 'access-control-allow-origin': value,
12
+ 'access-control-allow-methods': PERMISSIVE_METHODS,
13
+ 'access-control-allow-headers': PERMISSIVE_HEADERS,
14
+ vary: 'Origin',
15
+ };
16
+ };
17
+ export const resolveAdminCors = (bind, cors) => {
18
+ if (cors === 'same-origin')
19
+ return {};
20
+ if (cors === 'any')
21
+ return wildcardHeaders();
22
+ if (cors === 'auto') {
23
+ return bind === 'loopback-only' ? wildcardHeaders() : {};
24
+ }
25
+ // explicit { origin } form
26
+ return explicitOriginHeaders(cors.origin);
27
+ };
28
+ //# sourceMappingURL=resolve-cors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-cors.js","sourceRoot":"","sources":["../../src/admin/resolve-cors.ts"],"names":[],"mappings":"AAqBA,MAAM,kBAAkB,GAAG,yBAAyB,CAAA;AACpD,MAAM,kBAAkB,GAAG,cAAc,CAAA;AAEzC,MAAM,eAAe,GAAG,GAA2B,EAAE,CAAC,CAAC;IACrD,6BAA6B,EAAE,GAAG;IAClC,8BAA8B,EAAE,kBAAkB;IAClD,8BAA8B,EAAE,kBAAkB;CACnD,CAAC,CAAA;AAEF,MAAM,qBAAqB,GAAG,CAC5B,MAAkC,EACV,EAAE;IAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,MAAiB,CAAA;IAC5E,OAAO;QACL,6BAA6B,EAAE,KAAK;QACpC,8BAA8B,EAAE,kBAAkB;QAClD,8BAA8B,EAAE,kBAAkB;QAClD,IAAI,EAAE,QAAQ;KACf,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,IAA0C,EAC1C,IAAmB,EACK,EAAE;IAC1B,IAAI,IAAI,KAAK,aAAa;QAAE,OAAO,EAAE,CAAA;IACrC,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,eAAe,EAAE,CAAA;IAC5C,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1D,CAAC;IACD,2BAA2B;IAC3B,OAAO,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AAC3C,CAAC,CAAA"}
@@ -0,0 +1,15 @@
1
+ import type { Scenario, ScenarioSummary, StickyOverride } from './admin-types.js';
2
+ export declare const isValidScenarioName: (name: string) => boolean;
3
+ export interface ScenariosStore {
4
+ readonly dir: string;
5
+ list(): Promise<ScenarioSummary[]>;
6
+ get(name: string): Promise<Scenario | null>;
7
+ save(name: string, overrides: readonly StickyOverride[]): Promise<Scenario>;
8
+ remove(name: string): Promise<boolean>;
9
+ }
10
+ interface CreateScenariosStoreOptions {
11
+ readonly dir: string;
12
+ }
13
+ export declare const createScenariosStore: (opts: CreateScenariosStoreOptions) => ScenariosStore;
14
+ export {};
15
+ //# sourceMappingURL=scenarios-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scenarios-store.d.ts","sourceRoot":"","sources":["../../src/admin/scenarios-store.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAIjF,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,OAA6B,CAAA;AAEhF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;IAClC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAC3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,cAAc,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC3E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACvC;AAED,UAAU,2BAA2B;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAKD,eAAO,MAAM,oBAAoB,GAC/B,MAAM,2BAA2B,KAChC,cAgFF,CAAA"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * File-backed scenarios registry.
3
+ *
4
+ * A *scenario* is a named JSON snapshot of one or more sticky
5
+ * overrides. Scenarios live as `<name>.json` files inside the
6
+ * configured directory (default `<cwd>/mock-scenarios`). Names are
7
+ * sanitised — only `[A-Za-z0-9_-]`, max 64 chars — so they map
8
+ * directly to safe filenames and round-trip through HTTP path
9
+ * segments without escaping.
10
+ *
11
+ * The directory is opt-in: a missing dir yields an empty list rather
12
+ * than throwing. The first save creates the dir.
13
+ *
14
+ * Writes are atomic: write to `<name>.json.<tmp>` then rename, so a
15
+ * partial write never produces a half-parsed file.
16
+ */
17
+ import { randomBytes } from 'node:crypto';
18
+ import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
19
+ import { join } from 'node:path';
20
+ const NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
21
+ export const isValidScenarioName = (name) => NAME_RE.test(name);
22
+ const isENOENT = (err) => typeof err === 'object' && err !== null && err.code === 'ENOENT';
23
+ export const createScenariosStore = (opts) => {
24
+ const { dir } = opts;
25
+ const filePath = (name) => join(dir, `${name}.json`);
26
+ const list = async () => {
27
+ let entries;
28
+ try {
29
+ entries = await readdir(dir);
30
+ }
31
+ catch (err) {
32
+ if (isENOENT(err))
33
+ return [];
34
+ throw err;
35
+ }
36
+ const out = [];
37
+ for (const entry of entries) {
38
+ if (!entry.endsWith('.json'))
39
+ continue;
40
+ const name = entry.slice(0, -5);
41
+ if (!isValidScenarioName(name))
42
+ continue;
43
+ try {
44
+ const full = join(dir, entry);
45
+ const [text, st] = await Promise.all([readFile(full, 'utf8'), stat(full)]);
46
+ const parsed = JSON.parse(text);
47
+ out.push({
48
+ name,
49
+ count: Array.isArray(parsed.overrides) ? parsed.overrides.length : 0,
50
+ created: parsed.created ?? st.mtime.toISOString(),
51
+ });
52
+ }
53
+ catch {
54
+ // skip unreadable / malformed files; the UI still works.
55
+ }
56
+ }
57
+ out.sort((a, b) => a.name.localeCompare(b.name));
58
+ return out;
59
+ };
60
+ const get = async (name) => {
61
+ if (!isValidScenarioName(name))
62
+ return null;
63
+ try {
64
+ const text = await readFile(filePath(name), 'utf8');
65
+ const parsed = JSON.parse(text);
66
+ return {
67
+ name,
68
+ overrides: Array.isArray(parsed.overrides) ? parsed.overrides : [],
69
+ ...(parsed.created !== undefined ? { created: parsed.created } : {}),
70
+ };
71
+ }
72
+ catch (err) {
73
+ if (isENOENT(err))
74
+ return null;
75
+ return null;
76
+ }
77
+ };
78
+ const save = async (name, overrides) => {
79
+ if (!isValidScenarioName(name)) {
80
+ throw new Error(`@databehave/server: invalid scenario name: ${JSON.stringify(name)}`);
81
+ }
82
+ await mkdir(dir, { recursive: true });
83
+ const scenario = {
84
+ name,
85
+ overrides: [...overrides],
86
+ created: new Date().toISOString(),
87
+ };
88
+ const final = filePath(name);
89
+ const tmp = `${final}.${randomBytes(6).toString('hex')}.tmp`;
90
+ await writeFile(tmp, JSON.stringify(scenario, null, 2), 'utf8');
91
+ await rename(tmp, final);
92
+ return scenario;
93
+ };
94
+ const remove = async (name) => {
95
+ if (!isValidScenarioName(name))
96
+ return false;
97
+ try {
98
+ await unlink(filePath(name));
99
+ return true;
100
+ }
101
+ catch (err) {
102
+ if (isENOENT(err))
103
+ return false;
104
+ throw err;
105
+ }
106
+ };
107
+ return { dir, list, get, save, remove };
108
+ };
109
+ //# sourceMappingURL=scenarios-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scenarios-store.js","sourceRoot":"","sources":["../../src/admin/scenarios-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ *{box-sizing:border-box}html,body{color:#1f2328;background:#f6f8fa;margin:0;padding:0;font:13px/1.45 -apple-system,BlinkMacSystemFont,Segoe UI,system-ui,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}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{z-index:30;color:#fff;background:#24292f;align-items:center;gap:12px;padding:10px 16px;display:flex;position:sticky;top:0}.top-bar h1{flex:1;margin:0;font-size:15px;font-weight:600}.top-bar .url{color:#adbac7;font-family:ui-monospace,Menlo,monospace;font-size:11px}.top-bar .badge{color:#fff;background:#1f883d;border-radius:10px;padding:2px 8px;font-size:11px;font-weight:600}.top-bar button,.top-bar select{color:#24292f;background:#fff;border:1px solid #57606a;border-radius:6px;padding:4px 10px;font-size:12px}.top-bar button.ghost{color:#fff;background:0 0;border-color:#57606a}.filter-bar{z-index:25;background:#fff;border-bottom:1px solid #d0d7de;flex-wrap:wrap;align-items:center;gap:16px;padding:10px 16px;display:flex;position:sticky;top:48px}.filter-bar input[type=text]{border:1px solid #d0d7de;border-radius:6px;flex:1;min-width:260px;padding:6px 10px}.filter-bar label{align-items:center;gap:4px;font-size:12px;display:inline-flex}.main{max-width:1300px;margin:0 auto;padding:12px 16px}.right-panel{z-index:40;background:#fff;border-left:1px solid #d0d7de;flex-direction:column;width:360px;max-width:90vw;height:100vh;transition:transform .18s;display:flex;position:fixed;top:0;right:0;transform:translate(100%);box-shadow:-4px 0 12px #0000000f}.right-panel.open{transform:translate(0)}.right-panel header{border-bottom:1px solid #d0d7de;align-items:center;gap:8px;padding:12px 16px;display:flex}.right-panel header h2{flex:1;margin:0;font-size:14px}.right-panel .list{flex:1;padding:8px;overflow-y:auto}.pill-float{color:#fff;z-index:35;background:#cf222e;border:none;border-radius:999px;padding:8px 14px;font-size:12px;font-weight:600;position:fixed;bottom:16px;right:16px;box-shadow:0 2px 8px #00000026}.toast-stack{z-index:50;flex-direction:column;gap:6px;display:flex;position:fixed;bottom:60px;right:16px}.toast{color:#fff;cursor:pointer;border-radius:6px;min-width:180px;padding:8px 12px;font-size:12px;box-shadow:0 2px 8px #00000026}.toast.success{background:#1f883d}.toast.error{background:#cf222e}.toast.info{background:#0969da}.modal-backdrop{z-index:60;background:#0006;justify-content:center;align-items:center;display:flex;position:fixed;top:0;bottom:0;left:0;right:0}.modal{background:#fff;border-radius:8px;width:92%;max-width:520px;padding:20px}.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}.group{background:#fff;border:1px solid #d0d7de;border-radius:8px;margin-bottom:10px;overflow:hidden}.group-header{cursor:pointer;background:#f6f8fa;border-bottom:1px solid #eaeef2;align-items:center;gap:10px;padding:10px 14px;display:flex}.group-header h3{text-transform:capitalize;flex:1;margin:0;font-size:13px}.group-header .count{color:#57606a;font-size:11px}.group-body{display:none}.group.open .group-body{display:block}.op-row{cursor:pointer;border-bottom:1px solid #eaeef2;align-items:center;gap:10px;padding:8px 14px;display:flex}.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{color:#6e7781;letter-spacing:.02em;margin-left:6px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:10px;font-weight:500}.op-row .summary{color:#57606a;text-overflow:ellipsis;white-space:nowrap;max-width:380px;font-size:11px;overflow:hidden}.op-row .dot{background:0 0;border:1px solid #d0d7de;border-radius:50%;width:10px;height:10px}.op-row .dot.active{border-color:#0000}.method-badge{text-align:center;color:#fff;border-radius:4px;min-width:56px;padding:3px 8px;font-family:ui-monospace,Menlo,monospace;font-size:11px;font-weight:700;display:inline-block}.op-card{border-top:1px solid #eaeef2;padding:0}.spec-zone{background:#fff;border-bottom:1px solid #eaeef2;padding:14px 18px}.spec-zone h4{color:#57606a;text-transform:uppercase;letter-spacing:.5px;margin:0 0 6px;font-size:12px}.spec-zone .desc{color:#1f2328;font-size:12px}.spec-zone table{border-collapse:collapse;width:100%;margin-top:4px;font-size:12px}.spec-zone th,.spec-zone td{text-align:left;border-bottom:1px solid #eaeef2;padding:4px 6px}.spec-zone th{color:#57606a;font-weight:600}.mock-zone{background:#fff8c5;border-left:4px solid #d4a72c;padding:14px 18px}.mock-zone h4{color:#7d4e00;text-transform:uppercase;letter-spacing:.5px;margin:0 0 8px;font-size:12px}.mock-zone .row{grid-template-columns:160px 1fr;align-items:center;gap:10px;margin-bottom:8px;display:grid}.mock-zone label{color:#4a3500;font-size:12px;font-weight:500}.mock-zone input[type=text],.mock-zone input[type=number],.mock-zone select,.mock-zone textarea{background:#fff;border:1px solid #d4a72c;border-radius:4px;width:100%;padding:4px 8px}.mock-zone textarea{min-height:70px;font-family:ui-monospace,Menlo,monospace}.mock-zone .radio-group{flex-wrap:wrap;gap:12px;display:flex}.mock-zone .radio-group label{align-items:center;gap:4px;font-weight:400;display:inline-flex}.mock-zone .actions{gap:8px;margin-top:10px;display:flex}.btn{color:#1f2328;background:#fff;border:1px solid #d0d7de;border-radius:6px;padding:6px 12px}.btn.primary{color:#fff;background:#1f883d;border-color:#0000;font-weight:600}.btn.danger{color:#fff;background:#cf222e;border-color:#0000}.btn.ghost{background:0 0}.try-it{background:#f6f8fa;border-top:1px solid #eaeef2;padding:14px 18px}.try-it summary{cursor:pointer;font-size:13px;font-weight:600}.try-it .field{margin:6px 0}.try-it .response{background:#fff;border:1px solid #d0d7de;border-radius:6px;max-height:320px;margin-top:8px;padding:10px;font-family:ui-monospace,Menlo,monospace;font-size:12px;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;margin-left:6px;font-style:italic}.schema-tree .req{color:#cf222e}.override-item{cursor:pointer;border-bottom:1px solid #eaeef2;align-items:center;gap:8px;padding:8px;display:flex}.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{border-radius:50%;width:10px;height:10px}.global-active-hint{color:#4a2370;background:#f3e8ff;border-left:3px solid #6f42c1;border-radius:4px;align-items:center;gap:8px;margin-bottom:8px;padding:6px 10px;font-size:12px;display:flex}.global-active-hint .btn{padding:2px 8px;font-size:11px}.scope-badge{letter-spacing:.5px;text-transform:uppercase;color:#fff;border-radius:4px;padding:2px 6px;font-size:10px;font-weight:700;display:inline-block}.scope-badge.scope-global{background:#6f42c1}.override-item.is-global{background:#faf5ff}.override-item.is-global:hover{background:#f3e8ff}.top-bar .global-override-btn{align-items:center;gap:6px;display:inline-flex}.top-bar .global-badge{background:#6f42c1}.global-override-modal{max-width:560px}.global-override-header{align-items:center;gap:8px;margin-bottom:4px;display:flex}.global-override-header h2{flex:1;margin:0;font-size:16px}.global-override-header .close-x{color:#57606a;background:#fff;border:1px solid #d0d7de;padding:2px 8px;font-size:18px;line-height:1}.global-override-desc{color:#57606a;margin:0 0 12px;font-size:12px}.global-override-modal .row{grid-template-columns:140px 1fr;align-items:center;gap:10px;margin-bottom:8px;display:grid}.global-override-modal label{color:#1f2328;font-size:12px;font-weight:500}.global-override-modal input[type=text],.global-override-modal input[type=number],.global-override-modal select,.global-override-modal textarea{background:#fff;border:1px solid #d0d7de;border-radius:4px;width:100%;padding:4px 8px}.global-override-modal textarea{min-height:70px;font-family:ui-monospace,Menlo,monospace}.global-override-modal .actions{gap:8px;margin-top:12px;display:flex}.global-override-modal .method-checkboxes{flex-wrap:wrap;align-items:center;gap:6px 12px;display:flex}.global-override-modal .method-checkbox{color:#1f2328;cursor:pointer;align-items:center;gap:4px;font-size:12px;font-weight:500;display:inline-flex}.global-override-modal .method-checkbox input[type=checkbox]{width:auto;margin:0}.global-override-list{border-top:1px solid #eaeef2;margin-top:16px;padding-top:12px}.global-override-list h3{color:#57606a;text-transform:uppercase;letter-spacing:.5px;margin:0 0 8px;font-size:12px}
2
+ /*$vite$:1*/
@@ -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>