@x12i/map-store-playground 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.
- package/README.md +40 -0
- package/package.json +25 -0
- package/src/catalog.js +781 -0
- package/src/renderers.js +238 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `@x12i/map-store-playground`
|
|
2
|
+
|
|
3
|
+
Catalog and domain response previews for map-store `/map-store/v1`, for use with [`@x12i/api-playground`](https://www.npmjs.com/package/@x12i/api-playground) and [`@x12i/api-playground-kit`](https://www.npmjs.com/package/@x12i/api-playground-kit).
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
| Mount path | Contents |
|
|
8
|
+
|------------|----------|
|
|
9
|
+
| `/@map-store-playground/catalog.js` | Endpoint groups + `/map-store/v1` routes |
|
|
10
|
+
| `/@map-store-playground/renderers.js` | Overview panels (`renderResponse`, `presenceFor`) |
|
|
11
|
+
|
|
12
|
+
## Mount in your playground server
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { createPlaygroundServer } from "@x12i/api-playground-kit/create-playground-server.js";
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const pkg = dirname(require.resolve("@x12i/map-store-playground/package.json"));
|
|
21
|
+
|
|
22
|
+
createPlaygroundServer({
|
|
23
|
+
root: /* your app dir */,
|
|
24
|
+
mounts: [{ urlPrefix: "/@map-store-playground/", fsRoot: join(pkg, "src") }],
|
|
25
|
+
proxies: [
|
|
26
|
+
{
|
|
27
|
+
pathPrefix: "/map-store",
|
|
28
|
+
targetOrigin: process.env.MAP_STORE_ORIGIN ?? "http://127.0.0.1:9230",
|
|
29
|
+
errorCode: "map-store-proxy-failed",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
pathPrefix: "/health",
|
|
33
|
+
targetOrigin: process.env.MAP_STORE_ORIGIN ?? "http://127.0.0.1:9230",
|
|
34
|
+
errorCode: "map-store-proxy-failed",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Product-specific catalogs (e.g. Synapix UI flows / how-to guides) stay in the consumer app.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x12i/map-store-playground",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Map-store /map-store/v1 catalog and response previews for @x12i/api-playground",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
"./catalog.js": "./src/catalog.js",
|
|
13
|
+
"./renderers.js": "./src/renderers.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@x12i/api-playground": ">=0.4.4"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/catalog.js
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map-store /map-store/v1 catalog for @x12i/api-playground.
|
|
3
|
+
* @typedef {import("/@api-playground/types.js").PlaygroundEndpoint} PlaygroundEndpoint
|
|
4
|
+
* @typedef {import("/@api-playground/types.js").EndpointGroup} EndpointGroup
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @type {EndpointGroup[]} */
|
|
8
|
+
export const groups = [
|
|
9
|
+
{ id: "session", label: "Session" },
|
|
10
|
+
{ id: "working", label: "Working products" },
|
|
11
|
+
{ id: "objects", label: "Objects" },
|
|
12
|
+
{ id: "investigation", label: "Investigation packets" },
|
|
13
|
+
{ id: "counts", label: "Counts (Countex)" },
|
|
14
|
+
{ id: "advanced", label: "Advanced / lab" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const HONESTY = {
|
|
18
|
+
opaqueTokens:
|
|
19
|
+
"Generation tokens in responses (snapshotSetId, analysisSnapshotId, liveGenerationId) are opaque — never send them back on product URLs.",
|
|
20
|
+
noMutablePins: "Never use current, latest, or scs:… as request ids or pins.",
|
|
21
|
+
paging: "HTTP 200 on an index page alone is not enough — walk pageVariants / pageCount and reconcile to expectedItemCount / census.",
|
|
22
|
+
mixedGen: "Mixed generation → 409 → re-bootstrap.",
|
|
23
|
+
observed: "Observed communication ≠ REACHABLE / authoritative reachability.",
|
|
24
|
+
incomplete: "Partial / not-evaluated / unavailable / incomplete-publication ≠ green “safe”.",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @returns {PlaygroundEndpoint}
|
|
30
|
+
*/
|
|
31
|
+
function ep(opts) {
|
|
32
|
+
return {
|
|
33
|
+
persistsData: false,
|
|
34
|
+
mayUseBrowser: false,
|
|
35
|
+
responseViews: ["json"],
|
|
36
|
+
submitLabel: "Send request",
|
|
37
|
+
avoidWhen: [],
|
|
38
|
+
requirements: ["Reachable map-store station"],
|
|
39
|
+
limitations: [],
|
|
40
|
+
sideEffects: [],
|
|
41
|
+
examples: [],
|
|
42
|
+
...opts,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {object} p
|
|
48
|
+
* @returns {PlaygroundEndpoint}
|
|
49
|
+
*/
|
|
50
|
+
function workingProduct(p) {
|
|
51
|
+
const path = `/map-store/v1/working/${p.id}`;
|
|
52
|
+
return ep({
|
|
53
|
+
id: `working-${p.id}`,
|
|
54
|
+
group: "working",
|
|
55
|
+
name: p.name,
|
|
56
|
+
method: "GET",
|
|
57
|
+
path,
|
|
58
|
+
family: "working",
|
|
59
|
+
summary: p.summary,
|
|
60
|
+
purpose: p.purpose,
|
|
61
|
+
useWhen: p.useWhen,
|
|
62
|
+
avoidWhen: p.avoidWhen || [
|
|
63
|
+
"Do not invent joins client-side — only use published rows.",
|
|
64
|
+
HONESTY.opaqueTokens,
|
|
65
|
+
],
|
|
66
|
+
internalFlow: [
|
|
67
|
+
{ title: "Station org", description: "Map-store reads the configured Memorix org; the client never sends org/map/pin ids." },
|
|
68
|
+
{ title: "Load product", description: `Fetches working/${p.id} for the current covering (and live generation when applicable).` },
|
|
69
|
+
{
|
|
70
|
+
title: "Optional page",
|
|
71
|
+
description: "Pass ?variant= to fetch a pageVariant. Walk all variants when reconciling counts.",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
requirements: ["memorixReachable", "estateReady", ...(p.requirements || [])],
|
|
75
|
+
limitations: [
|
|
76
|
+
HONESTY.paging,
|
|
77
|
+
HONESTY.mixedGen,
|
|
78
|
+
HONESTY.opaqueTokens,
|
|
79
|
+
...(p.limitations || []),
|
|
80
|
+
],
|
|
81
|
+
requestUi: {
|
|
82
|
+
variant: {
|
|
83
|
+
label: "variant",
|
|
84
|
+
description: "Optional pageVariant id from a prior page’s pageVariants list. Leave empty for the product index/default page.",
|
|
85
|
+
placeholder: "",
|
|
86
|
+
section: "common",
|
|
87
|
+
order: 1,
|
|
88
|
+
required: false,
|
|
89
|
+
type: "query",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
inputs: [
|
|
93
|
+
{
|
|
94
|
+
name: "variant",
|
|
95
|
+
required: false,
|
|
96
|
+
meaning: "Page variant for paged working products",
|
|
97
|
+
type: "query",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
responseDescription: p.responseDescription,
|
|
101
|
+
responseViews: p.responseViews || ["preview", "json"],
|
|
102
|
+
submitLabel: "Fetch product",
|
|
103
|
+
examples: p.examples || [
|
|
104
|
+
{
|
|
105
|
+
name: "Default page",
|
|
106
|
+
description: `GET ${path}`,
|
|
107
|
+
request: {},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @type {PlaygroundEndpoint[]} */
|
|
114
|
+
export const endpoints = [
|
|
115
|
+
ep({
|
|
116
|
+
id: "process-health",
|
|
117
|
+
group: "session",
|
|
118
|
+
name: "Process health",
|
|
119
|
+
method: "GET",
|
|
120
|
+
path: "/health",
|
|
121
|
+
family: "session",
|
|
122
|
+
summary: "Process liveness (always 200 when the process is up).",
|
|
123
|
+
purpose:
|
|
124
|
+
"Probe that the map-store process is alive. Body includes memorixReachable / estateReady for banners even when Memorix is down — unlike /map-store/v1/health, this stays HTTP 200.",
|
|
125
|
+
useWhen: [
|
|
126
|
+
"Supervisors / load balancers need a cheap liveness probe",
|
|
127
|
+
"You want Memorix reachability hints without treating the estate as ready",
|
|
128
|
+
],
|
|
129
|
+
avoidWhen: [
|
|
130
|
+
"Do not gate the Synapix session on this alone — prefer GET /map-store/v1/health for estate readiness",
|
|
131
|
+
],
|
|
132
|
+
internalFlow: [
|
|
133
|
+
{ title: "Probe", description: "Returns process ok plus Memorix probe fields (may be cached briefly)." },
|
|
134
|
+
],
|
|
135
|
+
requestUi: {},
|
|
136
|
+
inputs: [],
|
|
137
|
+
responseDescription:
|
|
138
|
+
"JSON with ok: true plus station/Memorix fields (memorixReachable, estateReady, userMessage, …).",
|
|
139
|
+
responseViews: ["preview", "json"],
|
|
140
|
+
submitLabel: "Check process",
|
|
141
|
+
examples: [{ name: "Liveness", request: {} }],
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
ep({
|
|
145
|
+
id: "estate-health",
|
|
146
|
+
group: "session",
|
|
147
|
+
name: "Estate health",
|
|
148
|
+
method: "GET",
|
|
149
|
+
path: "/map-store/v1/health",
|
|
150
|
+
family: "session",
|
|
151
|
+
summary: "Station + Memorix readiness for the published estate client.",
|
|
152
|
+
purpose:
|
|
153
|
+
"Session gate for Synapix. Requires memorixReachable and estateReady. Returns 503 when not configured, station mismatch, or Memorix unreachable — with userMessage and retryAfterMs.",
|
|
154
|
+
useWhen: [
|
|
155
|
+
"Before bootstrap / product walks",
|
|
156
|
+
"Polling while showing a Memorix-down banner",
|
|
157
|
+
],
|
|
158
|
+
avoidWhen: [
|
|
159
|
+
"Do not treat stationOk alone as ready",
|
|
160
|
+
"Do not call Memorix :5100 from the browser",
|
|
161
|
+
],
|
|
162
|
+
internalFlow: [
|
|
163
|
+
{ title: "Station", description: "Validates Memorix URL/org configuration." },
|
|
164
|
+
{ title: "Probe Memorix", description: "Fast reachability probe; cache TTL avoids hammering Memorix." },
|
|
165
|
+
{ title: "Status", description: "200 when ready; 503 otherwise with the same field shape." },
|
|
166
|
+
],
|
|
167
|
+
requirements: ["Map-store process listening"],
|
|
168
|
+
requestUi: {},
|
|
169
|
+
inputs: [],
|
|
170
|
+
responseDescription:
|
|
171
|
+
"Health body: memorixReachable, estateReady, userMessage, memorixUrl, memorixOrgId, …",
|
|
172
|
+
responseViews: ["preview", "json"],
|
|
173
|
+
submitLabel: "Check estate",
|
|
174
|
+
examples: [{ name: "Session gate", request: {} }],
|
|
175
|
+
}),
|
|
176
|
+
|
|
177
|
+
ep({
|
|
178
|
+
id: "bootstrap",
|
|
179
|
+
group: "session",
|
|
180
|
+
name: "Bootstrap",
|
|
181
|
+
method: "GET",
|
|
182
|
+
path: "/map-store/v1/bootstrap",
|
|
183
|
+
family: "session",
|
|
184
|
+
summary: "Covering + live pins, census, and nested bootstrap / liveBootstrap bodies.",
|
|
185
|
+
purpose:
|
|
186
|
+
"Start every session here. No ids in the request. Returns the station envelope (not a flat Security Map bootstrap): snapshotSetId, analysisSnapshotId, liveGenerationId, liveAvailability, bootstrap, liveBootstrap.",
|
|
187
|
+
useWhen: [
|
|
188
|
+
"Opening a new UI session or refresh cycle",
|
|
189
|
+
"Reading typed census / producerIdentity / assessmentCoverage",
|
|
190
|
+
"Confirming liveAvailability before treating live overlays as present",
|
|
191
|
+
],
|
|
192
|
+
avoidWhen: [
|
|
193
|
+
HONESTY.opaqueTokens,
|
|
194
|
+
HONESTY.noMutablePins,
|
|
195
|
+
"Never treat missing/null liveAvailability as healthy live overlay",
|
|
196
|
+
],
|
|
197
|
+
internalFlow: [
|
|
198
|
+
{ title: "Pointers", description: "Map-store loads current working pointers for the station org." },
|
|
199
|
+
{
|
|
200
|
+
title: "Envelope",
|
|
201
|
+
description: "Returns pins + nested bootstrap and liveBootstrap (or null).",
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
limitations: [
|
|
205
|
+
HONESTY.opaqueTokens,
|
|
206
|
+
"liveBootstrap may also be fetched as working/live-bootstrap",
|
|
207
|
+
"Incomplete publication → 503 incomplete-publication",
|
|
208
|
+
],
|
|
209
|
+
requestUi: {},
|
|
210
|
+
inputs: [],
|
|
211
|
+
responseDescription:
|
|
212
|
+
"Envelope with snapshotSetId, analysisSnapshotId, liveGenerationId, liveAvailability, bootstrap {…}, liveBootstrap {…|null}. Unwrap nested bootstrap for census / producer identity.",
|
|
213
|
+
responseViews: ["preview", "json"],
|
|
214
|
+
submitLabel: "Bootstrap",
|
|
215
|
+
examples: [{ name: "Session bootstrap", request: {} }],
|
|
216
|
+
}),
|
|
217
|
+
|
|
218
|
+
workingProduct({
|
|
219
|
+
id: "live-bootstrap",
|
|
220
|
+
name: "Live bootstrap",
|
|
221
|
+
summary: "Live clock / OM status overlay (working product).",
|
|
222
|
+
purpose:
|
|
223
|
+
"Fetch the live-generation working product. Also embedded on GET /bootstrap as liveBootstrap. Two clocks: covering-stable vs live overlay — never treat live as covering-stable reachability.",
|
|
224
|
+
useWhen: [
|
|
225
|
+
"Need liveGenerationId / omStatus after bootstrap",
|
|
226
|
+
"Refreshing live overlay without re-walking stable products",
|
|
227
|
+
],
|
|
228
|
+
avoidWhen: [
|
|
229
|
+
HONESTY.observed,
|
|
230
|
+
"Do not treat live overlay as authoritative REACHABLE",
|
|
231
|
+
],
|
|
232
|
+
limitations: [HONESTY.observed],
|
|
233
|
+
responseDescription: "Wrapped { snapshotSetId, liveGenerationId, product, data } — unwrap data.",
|
|
234
|
+
}),
|
|
235
|
+
|
|
236
|
+
workingProduct({
|
|
237
|
+
id: "publication-manifest",
|
|
238
|
+
name: "Publication manifest",
|
|
239
|
+
summary: "Expected product set for this covering pin.",
|
|
240
|
+
purpose: "Lists which working products the publisher intends for this bag — useful before on-demand walks.",
|
|
241
|
+
useWhen: ["Discovering which products should exist", "Debugging incomplete-publication"],
|
|
242
|
+
responseDescription: "Manifest rows inside data (unwrap).",
|
|
243
|
+
}),
|
|
244
|
+
|
|
245
|
+
workingProduct({
|
|
246
|
+
id: "estate-object-inventory",
|
|
247
|
+
name: "Estate object inventory",
|
|
248
|
+
summary: "Typed inventory rows (assets, unresolved subjects, findings, alerts, …).",
|
|
249
|
+
purpose:
|
|
250
|
+
"Primary estate inventory. Page all variants; filter by objectType. Findings/alerts are not hosts.",
|
|
251
|
+
useWhen: ["Entities / Findings lists", "Reconciling typed census counts"],
|
|
252
|
+
limitations: [HONESTY.paging, "Display server-provided counts — do not hardcode tiles"],
|
|
253
|
+
responseDescription: "Inventory pages in data; walk pageVariants.",
|
|
254
|
+
}),
|
|
255
|
+
|
|
256
|
+
workingProduct({
|
|
257
|
+
id: "object-pre-ai-context",
|
|
258
|
+
name: "Object pre-AI context",
|
|
259
|
+
summary: "Placement, stories, objectType per object (pre-AI).",
|
|
260
|
+
purpose: "Published pre-AI context used for honesty copy (placement candidates, consequence stories).",
|
|
261
|
+
useWhen: ["Object detail placement / stories", "Deep-load for report planes"],
|
|
262
|
+
responseDescription: "Pre-AI context rows; often object-scoped pages.",
|
|
263
|
+
}),
|
|
264
|
+
|
|
265
|
+
workingProduct({
|
|
266
|
+
id: "object-security-context",
|
|
267
|
+
name: "Object security-context (working)",
|
|
268
|
+
summary: "Estate page-walk of OSC rows.",
|
|
269
|
+
purpose:
|
|
270
|
+
"Paged working product of object security-context. Prefer GET /objects/{id}/security-context for one object.",
|
|
271
|
+
useWhen: ["Bulk OSC walks", "Comparing to object join route"],
|
|
272
|
+
avoidWhen: ["Prefer the object join route for single-object detail"],
|
|
273
|
+
responseDescription: "OSC page payload in data.",
|
|
274
|
+
}),
|
|
275
|
+
|
|
276
|
+
workingProduct({
|
|
277
|
+
id: "object-classification",
|
|
278
|
+
name: "Object classification",
|
|
279
|
+
summary: "Classification candidates (may be missing on some bags).",
|
|
280
|
+
purpose:
|
|
281
|
+
"Classification product. May return incomplete-publication / missing — degrade honestly; never invent sole strongest label as truth.",
|
|
282
|
+
useWhen: ["Showing classification candidates on object detail"],
|
|
283
|
+
limitations: ["May 503 incomplete-publication on current bags", HONESTY.incomplete],
|
|
284
|
+
responseDescription: "Classification rows or incomplete-publication error.",
|
|
285
|
+
}),
|
|
286
|
+
|
|
287
|
+
workingProduct({
|
|
288
|
+
id: "object-exploitability-assessments",
|
|
289
|
+
name: "Object exploitability assessments",
|
|
290
|
+
summary: "EPA rows (attachment ≠ local asset exploitability).",
|
|
291
|
+
purpose: "Published EPA. Finding/alert EPA must not be promoted to “asset locally exploitable”.",
|
|
292
|
+
useWhen: ["EPA sections on findings/alerts/assets"],
|
|
293
|
+
avoidWhen: ["Do not promote finding EPA to asset exploitability"],
|
|
294
|
+
responseDescription: "EPA rows in data.",
|
|
295
|
+
}),
|
|
296
|
+
|
|
297
|
+
workingProduct({
|
|
298
|
+
id: "object-live-context",
|
|
299
|
+
name: "Object live-context",
|
|
300
|
+
summary: "Live overlay per object.",
|
|
301
|
+
purpose: "Live context rows. Observed ≠ REACHABLE.",
|
|
302
|
+
useWhen: ["Live overlay on object detail"],
|
|
303
|
+
limitations: [HONESTY.observed],
|
|
304
|
+
responseDescription: "Live context pages in data.",
|
|
305
|
+
}),
|
|
306
|
+
|
|
307
|
+
workingProduct({
|
|
308
|
+
id: "object-evidence-lineage",
|
|
309
|
+
name: "Object evidence lineage",
|
|
310
|
+
summary: "Object-scoped evidence lineage pages.",
|
|
311
|
+
purpose: "Lineage scoped to objects (≠ estate rollup evidence-lineage).",
|
|
312
|
+
useWhen: ["Object detail evidence lineage"],
|
|
313
|
+
responseDescription: "Object-scoped lineage pages.",
|
|
314
|
+
}),
|
|
315
|
+
|
|
316
|
+
workingProduct({
|
|
317
|
+
id: "evidence-lineage",
|
|
318
|
+
name: "Estate evidence-lineage",
|
|
319
|
+
summary: "Estate rollup lineage product.",
|
|
320
|
+
purpose: "Estate-level evidence lineage (not object-scoped).",
|
|
321
|
+
useWhen: ["Estate evidence rollups"],
|
|
322
|
+
avoidWhen: ["Do not confuse with object-evidence-lineage"],
|
|
323
|
+
responseDescription: "Estate lineage payload.",
|
|
324
|
+
}),
|
|
325
|
+
|
|
326
|
+
workingProduct({
|
|
327
|
+
id: "object-agent-context",
|
|
328
|
+
name: "Object agent-context",
|
|
329
|
+
summary: "Published agent context rows when present.",
|
|
330
|
+
purpose: "Agent context product for objects when the bag includes it.",
|
|
331
|
+
useWhen: ["Inspecting published agent context"],
|
|
332
|
+
responseDescription: "Agent context rows.",
|
|
333
|
+
}),
|
|
334
|
+
|
|
335
|
+
workingProduct({
|
|
336
|
+
id: "object-field-completeness",
|
|
337
|
+
name: "Object field-completeness",
|
|
338
|
+
summary: "Field completeness / gap accounting per object.",
|
|
339
|
+
purpose: "Gap accounting — incomplete ≠ safe.",
|
|
340
|
+
useWhen: ["Honesty about missing fields"],
|
|
341
|
+
limitations: [HONESTY.incomplete],
|
|
342
|
+
responseDescription: "Completeness / gap rows.",
|
|
343
|
+
}),
|
|
344
|
+
|
|
345
|
+
workingProduct({
|
|
346
|
+
id: "object-reachability-claims",
|
|
347
|
+
name: "Object reachability-claims",
|
|
348
|
+
summary: "Published reachability claim rows.",
|
|
349
|
+
purpose: "Claims only — not the same as candidate-reachability paths or observed edges.",
|
|
350
|
+
useWhen: ["Showing published claims"],
|
|
351
|
+
responseDescription: "Claim rows in data.",
|
|
352
|
+
}),
|
|
353
|
+
|
|
354
|
+
workingProduct({
|
|
355
|
+
id: "object-reachability-paths",
|
|
356
|
+
name: "Object reachability-paths",
|
|
357
|
+
summary: "Published reachability path rows.",
|
|
358
|
+
purpose: "Path product distinct from candidate-reachability.",
|
|
359
|
+
useWhen: ["Published path inspection"],
|
|
360
|
+
avoidWhen: ["Do not equate with candidate-reachability or live observation edges"],
|
|
361
|
+
responseDescription: "Path rows in data.",
|
|
362
|
+
}),
|
|
363
|
+
|
|
364
|
+
workingProduct({
|
|
365
|
+
id: "candidate-reachability",
|
|
366
|
+
name: "Candidate reachability",
|
|
367
|
+
summary: "Hypothesized / candidate path product.",
|
|
368
|
+
purpose: "Candidate reachability for predicted/conditional surfaces — not observed traffic.",
|
|
369
|
+
useWhen: ["Predicted / conditional path candidates"],
|
|
370
|
+
limitations: [HONESTY.observed],
|
|
371
|
+
responseDescription: "Candidate path pages.",
|
|
372
|
+
}),
|
|
373
|
+
|
|
374
|
+
workingProduct({
|
|
375
|
+
id: "live-observation-edges",
|
|
376
|
+
name: "Live observation edges",
|
|
377
|
+
summary: "Observed communication edges (large).",
|
|
378
|
+
purpose: "Full edge page walk. Observed ≠ REACHABLE.",
|
|
379
|
+
useWhen: ["Evidence / observed communication views"],
|
|
380
|
+
limitations: [HONESTY.observed, "Can be large — page carefully"],
|
|
381
|
+
responseDescription: "Edge pages in data.",
|
|
382
|
+
}),
|
|
383
|
+
|
|
384
|
+
workingProduct({
|
|
385
|
+
id: "investigation-context",
|
|
386
|
+
name: "Investigation context",
|
|
387
|
+
summary: "Published investigation context product.",
|
|
388
|
+
purpose: "Investigation context working product (packets also have dedicated HTTP routes).",
|
|
389
|
+
useWhen: ["Browsing investigation context rows"],
|
|
390
|
+
responseDescription: "Context payload in data.",
|
|
391
|
+
}),
|
|
392
|
+
|
|
393
|
+
workingProduct({
|
|
394
|
+
id: "investigation-unknown",
|
|
395
|
+
name: "Investigation unknown",
|
|
396
|
+
summary: "Published unknowns product.",
|
|
397
|
+
purpose: "Investigation unknown rows from the bag.",
|
|
398
|
+
useWhen: ["Surfacing published unknowns"],
|
|
399
|
+
responseDescription: "Unknowns payload.",
|
|
400
|
+
}),
|
|
401
|
+
|
|
402
|
+
workingProduct({
|
|
403
|
+
id: "known-unknowns",
|
|
404
|
+
name: "Known unknowns",
|
|
405
|
+
summary: "Published known-unknowns product.",
|
|
406
|
+
purpose: "Known-unknowns working product.",
|
|
407
|
+
useWhen: ["Honesty about what remains unresolved"],
|
|
408
|
+
responseDescription: "Known-unknowns payload.",
|
|
409
|
+
}),
|
|
410
|
+
|
|
411
|
+
ep({
|
|
412
|
+
id: "object-security-context-join",
|
|
413
|
+
group: "objects",
|
|
414
|
+
name: "Object security-context (join)",
|
|
415
|
+
method: "GET",
|
|
416
|
+
path: "/map-store/v1/objects/{objectId}/security-context",
|
|
417
|
+
family: "objects",
|
|
418
|
+
summary: "Preferred single-object join: context + classification + preAi + live overlay.",
|
|
419
|
+
purpose:
|
|
420
|
+
"Primary object-detail route. Returns context, classification, preAiContext, liveOverlay, retrieval metadata for one objectId.",
|
|
421
|
+
useWhen: [
|
|
422
|
+
"Object detail pages",
|
|
423
|
+
"Prefer over walking working/object-security-context for one id",
|
|
424
|
+
],
|
|
425
|
+
avoidWhen: [
|
|
426
|
+
HONESTY.opaqueTokens,
|
|
427
|
+
"Station may not expose /objects/{id}/evidence-lineage or /live-context — use working products",
|
|
428
|
+
],
|
|
429
|
+
internalFlow: [
|
|
430
|
+
{ title: "Lookup", description: "Joins stable OSC with classification, pre-AI, and live overlay when present." },
|
|
431
|
+
{ title: "Mixed generation", description: "409 mixed-generation → re-bootstrap." },
|
|
432
|
+
],
|
|
433
|
+
limitations: [HONESTY.mixedGen, HONESTY.observed],
|
|
434
|
+
requestUi: {
|
|
435
|
+
objectId: {
|
|
436
|
+
label: "objectId",
|
|
437
|
+
description: "Published object id (opaque). Example lab shape: object:10.70.40.35,10.70.40.8,10.70.40.9",
|
|
438
|
+
placeholder: "object:…",
|
|
439
|
+
example: "object:10.70.40.35,10.70.40.8,10.70.40.9",
|
|
440
|
+
section: "primary",
|
|
441
|
+
order: 1,
|
|
442
|
+
required: true,
|
|
443
|
+
type: "path",
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
inputs: [
|
|
447
|
+
{
|
|
448
|
+
name: "objectId",
|
|
449
|
+
required: true,
|
|
450
|
+
meaning: "Object identifier from inventory / OSC",
|
|
451
|
+
type: "path",
|
|
452
|
+
example: "object:10.70.40.35,10.70.40.8,10.70.40.9",
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
responseDescription:
|
|
456
|
+
"JSON with snapshotSetId, liveGenerationId, context, liveOverlay, classification, preAiContext, retrieval[], liveAuthority.",
|
|
457
|
+
responseViews: ["preview", "json"],
|
|
458
|
+
submitLabel: "Fetch object context",
|
|
459
|
+
examples: [
|
|
460
|
+
{
|
|
461
|
+
name: "Sample lab object",
|
|
462
|
+
request: { objectId: "object:10.70.40.35,10.70.40.8,10.70.40.9" },
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
}),
|
|
466
|
+
|
|
467
|
+
ep({
|
|
468
|
+
id: "investigation-packets-index",
|
|
469
|
+
group: "investigation",
|
|
470
|
+
name: "Investigation packets (index)",
|
|
471
|
+
method: "GET",
|
|
472
|
+
path: "/map-store/v1/investigation-packets",
|
|
473
|
+
family: "investigation",
|
|
474
|
+
summary: "Index of security investigation packets.",
|
|
475
|
+
purpose:
|
|
476
|
+
"Lists investigation packets from the security-investigation-packet product. Packets are not Tier-1 AI conclusions.",
|
|
477
|
+
useWhen: ["Evidence & packets surfaces", "Picking a packetId for detail"],
|
|
478
|
+
avoidWhen: ["Do not treat packets as authoritative AI verdicts"],
|
|
479
|
+
internalFlow: [
|
|
480
|
+
{
|
|
481
|
+
title: "Product",
|
|
482
|
+
description: "Reads working security-investigation-packet index for the current covering.",
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
requestUi: {},
|
|
486
|
+
inputs: [],
|
|
487
|
+
responseDescription: "snapshotSetId, analysisSnapshotId, liveGenerationId, index.",
|
|
488
|
+
responseViews: ["preview", "json"],
|
|
489
|
+
submitLabel: "List packets",
|
|
490
|
+
examples: [{ name: "Packet index", request: {} }],
|
|
491
|
+
}),
|
|
492
|
+
|
|
493
|
+
ep({
|
|
494
|
+
id: "investigation-packet",
|
|
495
|
+
group: "investigation",
|
|
496
|
+
name: "Investigation packet (by id)",
|
|
497
|
+
method: "GET",
|
|
498
|
+
path: "/map-store/v1/investigation-packets/{packetId}",
|
|
499
|
+
family: "investigation",
|
|
500
|
+
summary: "One investigation packet by id (variant).",
|
|
501
|
+
purpose: "Fetches a single packet. Not a Tier-1 AI conclusion.",
|
|
502
|
+
useWhen: ["Packet detail after listing the index"],
|
|
503
|
+
avoidWhen: ["Do not treat as AI triage narrative"],
|
|
504
|
+
internalFlow: [
|
|
505
|
+
{
|
|
506
|
+
title: "Variant",
|
|
507
|
+
description: "Loads security-investigation-packet with variant=packetId.",
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
requestUi: {
|
|
511
|
+
packetId: {
|
|
512
|
+
label: "packetId",
|
|
513
|
+
description: "Packet id from the index response.",
|
|
514
|
+
placeholder: "packet id",
|
|
515
|
+
section: "primary",
|
|
516
|
+
order: 1,
|
|
517
|
+
required: true,
|
|
518
|
+
type: "path",
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
inputs: [
|
|
522
|
+
{
|
|
523
|
+
name: "packetId",
|
|
524
|
+
required: true,
|
|
525
|
+
meaning: "Investigation packet identifier",
|
|
526
|
+
type: "path",
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
responseDescription: "snapshotSetId, analysisSnapshotId, liveGenerationId, packet.",
|
|
530
|
+
responseViews: ["json"],
|
|
531
|
+
submitLabel: "Fetch packet",
|
|
532
|
+
}),
|
|
533
|
+
|
|
534
|
+
ep({
|
|
535
|
+
id: "counts-index",
|
|
536
|
+
group: "counts",
|
|
537
|
+
name: "Counts API index",
|
|
538
|
+
method: "GET",
|
|
539
|
+
path: "/map-store/v1/counts",
|
|
540
|
+
family: "counts",
|
|
541
|
+
summary: "Describe Countex counters on the map-store API tier.",
|
|
542
|
+
purpose:
|
|
543
|
+
"Map-store hosts @x12i/countex. The browser never imports Countex — only these HTTP routes. Lists counter ids and related paths.",
|
|
544
|
+
useWhen: ["Discovering count routes", "Confirming Countex is wired on the station"],
|
|
545
|
+
avoidWhen: ["Do not add Countex to the browser bundle"],
|
|
546
|
+
internalFlow: [
|
|
547
|
+
{
|
|
548
|
+
title: "API tier",
|
|
549
|
+
description: "neo-services-map-store keeps an in-process Countex engine (memory adapters) rebuilt from Memorix inventory.",
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
requirements: ["map-store with Memorix", "@x12i/countex on the station"],
|
|
553
|
+
requestUi: {},
|
|
554
|
+
inputs: [],
|
|
555
|
+
responseDescription: "engine, counters[], cachedSnapshotSetId, routes.",
|
|
556
|
+
responseViews: ["preview", "json"],
|
|
557
|
+
submitLabel: "Describe counts API",
|
|
558
|
+
examples: [
|
|
559
|
+
{
|
|
560
|
+
name: "Index",
|
|
561
|
+
request: {},
|
|
562
|
+
response: {
|
|
563
|
+
engine: "@x12i/countex",
|
|
564
|
+
counters: ["estate_by_object_type"],
|
|
565
|
+
routes: {
|
|
566
|
+
byObjectType: "GET /map-store/v1/counts/by-object-type",
|
|
567
|
+
query: "POST /map-store/v1/counts/query",
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
}),
|
|
573
|
+
|
|
574
|
+
ep({
|
|
575
|
+
id: "counts-by-object-type",
|
|
576
|
+
group: "counts",
|
|
577
|
+
name: "Count estate by objectType",
|
|
578
|
+
method: "GET",
|
|
579
|
+
path: "/map-store/v1/counts/by-object-type",
|
|
580
|
+
family: "counts",
|
|
581
|
+
summary: "Walk inventory → Countex → walked vs bootstrap typed census.",
|
|
582
|
+
purpose:
|
|
583
|
+
"API-tier recount: map-store walks estate-object-inventory pages, ingests into @x12i/countex (estate_by_object_type), and returns reconcile deltas vs bootstrap typedRecordCensus. Use this instead of hardcoding census tiles or counting only in the browser.",
|
|
584
|
+
useWhen: [
|
|
585
|
+
"Reconciling Entities/Findings totals to published census",
|
|
586
|
+
"Home KPI honesty checks",
|
|
587
|
+
],
|
|
588
|
+
avoidWhen: [
|
|
589
|
+
"Do not re-implement Countex in the UI",
|
|
590
|
+
"Do not treat process-local memory counts as durable analytics across stations",
|
|
591
|
+
],
|
|
592
|
+
internalFlow: [
|
|
593
|
+
{ title: "Pointers", description: "Load current Memorix covering pin." },
|
|
594
|
+
{ title: "Walk", description: "Page estate-object-inventory (optional maxPages)." },
|
|
595
|
+
{ title: "Countex", description: "ingestBatch + flush + query groupBy metric." },
|
|
596
|
+
{ title: "Reconcile", description: "Compare walked counts to bootstrap typed census." },
|
|
597
|
+
],
|
|
598
|
+
limitations: [
|
|
599
|
+
HONESTY.paging,
|
|
600
|
+
"Cached per snapshotSetId until force=1",
|
|
601
|
+
"In-process memory adapters — rebuild after station restart",
|
|
602
|
+
],
|
|
603
|
+
requestUi: {
|
|
604
|
+
force: {
|
|
605
|
+
label: "force",
|
|
606
|
+
description: "Set 1/true to rebuild even if this pin was already counted.",
|
|
607
|
+
section: "common",
|
|
608
|
+
order: 1,
|
|
609
|
+
required: false,
|
|
610
|
+
type: "query",
|
|
611
|
+
placeholder: "1",
|
|
612
|
+
},
|
|
613
|
+
maxPages: {
|
|
614
|
+
label: "maxPages",
|
|
615
|
+
description: "Cap inventory page walk (default 500).",
|
|
616
|
+
section: "common",
|
|
617
|
+
order: 2,
|
|
618
|
+
required: false,
|
|
619
|
+
type: "query",
|
|
620
|
+
placeholder: "50",
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
inputs: [
|
|
624
|
+
{ name: "force", required: false, meaning: "Force rebuild", type: "query" },
|
|
625
|
+
{ name: "maxPages", required: false, meaning: "Max inventory pages", type: "query" },
|
|
626
|
+
],
|
|
627
|
+
responseDescription:
|
|
628
|
+
"ingested, counts (Countex table), typedCensus, reconcile.byType / walkedTotal / censusTotal.",
|
|
629
|
+
responseViews: ["preview", "json"],
|
|
630
|
+
submitLabel: "Recount by objectType",
|
|
631
|
+
examples: [
|
|
632
|
+
{
|
|
633
|
+
name: "Force rebuild",
|
|
634
|
+
request: { force: "1" },
|
|
635
|
+
response: {
|
|
636
|
+
engine: "@x12i/countex",
|
|
637
|
+
counterId: "estate_by_object_type",
|
|
638
|
+
ingested: 1481,
|
|
639
|
+
reconcile: {
|
|
640
|
+
walkedTotal: 1481,
|
|
641
|
+
censusTotal: 1481,
|
|
642
|
+
byType: {
|
|
643
|
+
"endpoint-asset": { walked: 506, census: 506, delta: 0 },
|
|
644
|
+
finding: { walked: 392, census: 392, delta: 0 },
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
}),
|
|
651
|
+
|
|
652
|
+
ep({
|
|
653
|
+
id: "counts-query",
|
|
654
|
+
group: "counts",
|
|
655
|
+
name: "Countex query",
|
|
656
|
+
method: "POST",
|
|
657
|
+
path: "/map-store/v1/counts/query",
|
|
658
|
+
family: "counts",
|
|
659
|
+
summary: "Run a Countex QuerySpec against the last estate rebuild.",
|
|
660
|
+
purpose:
|
|
661
|
+
"POST a Countex QuerySpec (scope, counterId, timeframe, groupBy, …). If no cache exists, map-store rebuilds from inventory first. Browser sends JSON only — no Countex import.",
|
|
662
|
+
useWhen: ["Custom groupings / filters after a by-object-type rebuild"],
|
|
663
|
+
avoidWhen: ["Do not point this at Memorix or embed Countex in the browser"],
|
|
664
|
+
internalFlow: [
|
|
665
|
+
{ title: "Ensure engine", description: "Rebuild estate counters if needed." },
|
|
666
|
+
{ title: "Query", description: "countex.query(body)." },
|
|
667
|
+
],
|
|
668
|
+
requestUi: {
|
|
669
|
+
body: {
|
|
670
|
+
label: "JSON body (QuerySpec)",
|
|
671
|
+
description: "Full Countex QuerySpec JSON.",
|
|
672
|
+
section: "primary",
|
|
673
|
+
order: 1,
|
|
674
|
+
required: true,
|
|
675
|
+
type: "json",
|
|
676
|
+
example: JSON.stringify(
|
|
677
|
+
{
|
|
678
|
+
scope: "scs:…",
|
|
679
|
+
counterId: "estate_by_object_type",
|
|
680
|
+
timeframe: { type: "day", from: "2026-09-13", to: "2026-09-13" },
|
|
681
|
+
groupBy: ["metric"],
|
|
682
|
+
format: "table",
|
|
683
|
+
orderBy: "-count",
|
|
684
|
+
resolveLabels: true,
|
|
685
|
+
},
|
|
686
|
+
null,
|
|
687
|
+
2,
|
|
688
|
+
),
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
inputs: [{ name: "body", required: true, meaning: "Countex QuerySpec", type: "json" }],
|
|
692
|
+
responseDescription: "{ engine, result } where result is Countex QueryResult.",
|
|
693
|
+
responseViews: ["json"],
|
|
694
|
+
submitLabel: "Run Countex query",
|
|
695
|
+
examples: [
|
|
696
|
+
{
|
|
697
|
+
name: "By metric (day)",
|
|
698
|
+
request: {
|
|
699
|
+
body: JSON.stringify({
|
|
700
|
+
scope: "scs:47282bd63cadb10939ceff72",
|
|
701
|
+
counterId: "estate_by_object_type",
|
|
702
|
+
timeframe: { type: "day", from: "2026-09-13", to: "2026-09-13" },
|
|
703
|
+
groupBy: ["metric"],
|
|
704
|
+
format: "table",
|
|
705
|
+
orderBy: "-count",
|
|
706
|
+
resolveLabels: true,
|
|
707
|
+
}),
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
}),
|
|
712
|
+
|
|
713
|
+
ep({
|
|
714
|
+
id: "legacy-consume",
|
|
715
|
+
group: "advanced",
|
|
716
|
+
name: "Legacy local consume (lab)",
|
|
717
|
+
method: "GET",
|
|
718
|
+
path: "/map-store/v1/tenants/{tenant}/consume/{snapshotSetId}/{product}",
|
|
719
|
+
family: "advanced",
|
|
720
|
+
summary: "NOT Memorix — lab-only local cache. Synapix must not use this.",
|
|
721
|
+
purpose:
|
|
722
|
+
"Lab-only local consume store on the map-store process — not a Memorix read. Synapix estate data always comes from Memorix via /map-store/v1/working/*, /bootstrap, object join, and investigation-packets — never this route.",
|
|
723
|
+
useWhen: ["Debugging an old local consume cache in lab"],
|
|
724
|
+
avoidWhen: [
|
|
725
|
+
"Never use this for the published Memorix estate / Synapix UI",
|
|
726
|
+
HONESTY.noMutablePins,
|
|
727
|
+
"Prefer /map-store/v1/working/{product}",
|
|
728
|
+
],
|
|
729
|
+
internalFlow: [
|
|
730
|
+
{
|
|
731
|
+
title: "Local store",
|
|
732
|
+
description: "Looks up a row in the local consume cache; 404 with guidance to use working/* when missing.",
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
limitations: ["Not a Memorix writer", "Returns 404 consume-not-found when empty"],
|
|
736
|
+
requestUi: {
|
|
737
|
+
tenant: {
|
|
738
|
+
label: "tenant",
|
|
739
|
+
description: "Legacy tenant segment (lab).",
|
|
740
|
+
section: "primary",
|
|
741
|
+
order: 1,
|
|
742
|
+
required: true,
|
|
743
|
+
type: "path",
|
|
744
|
+
default: "default",
|
|
745
|
+
},
|
|
746
|
+
snapshotSetId: {
|
|
747
|
+
label: "snapshotSetId",
|
|
748
|
+
description: "Must not be current/latest — opaque pin if you truly need this lab route.",
|
|
749
|
+
section: "primary",
|
|
750
|
+
order: 2,
|
|
751
|
+
required: true,
|
|
752
|
+
type: "path",
|
|
753
|
+
},
|
|
754
|
+
product: {
|
|
755
|
+
label: "product",
|
|
756
|
+
description: "Consume product id.",
|
|
757
|
+
section: "primary",
|
|
758
|
+
order: 3,
|
|
759
|
+
required: true,
|
|
760
|
+
type: "path",
|
|
761
|
+
},
|
|
762
|
+
variant: {
|
|
763
|
+
label: "variant",
|
|
764
|
+
description: "Optional page variant.",
|
|
765
|
+
section: "common",
|
|
766
|
+
order: 4,
|
|
767
|
+
required: false,
|
|
768
|
+
type: "query",
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
inputs: [
|
|
772
|
+
{ name: "tenant", required: true, meaning: "Legacy tenant", type: "path", default: "default" },
|
|
773
|
+
{ name: "snapshotSetId", required: true, meaning: "Opaque snapshot set id", type: "path" },
|
|
774
|
+
{ name: "product", required: true, meaning: "Product id", type: "path" },
|
|
775
|
+
{ name: "variant", required: false, meaning: "Page variant", type: "query" },
|
|
776
|
+
],
|
|
777
|
+
responseDescription: "Local consume row JSON, or 404 with gaps pointing at working/*.",
|
|
778
|
+
responseViews: ["json"],
|
|
779
|
+
submitLabel: "Fetch (lab only)",
|
|
780
|
+
}),
|
|
781
|
+
];
|
package/src/renderers.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain response panels for map-store.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {unknown} v
|
|
7
|
+
* @returns {Record<string, unknown> | null}
|
|
8
|
+
*/
|
|
9
|
+
function asRecord(v) {
|
|
10
|
+
return v && typeof v === "object" && !Array.isArray(v) ? /** @type {Record<string, unknown>} */ (v) : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {unknown} body
|
|
15
|
+
* @param {HTMLElement} overviewEl
|
|
16
|
+
* @param {HTMLElement} jsonEl
|
|
17
|
+
* @param {HTMLElement} genericEl
|
|
18
|
+
* @param {string} endpointId
|
|
19
|
+
*/
|
|
20
|
+
export function renderResponse(body, overviewEl, jsonEl, genericEl, endpointId) {
|
|
21
|
+
const text = safeJson(body);
|
|
22
|
+
jsonEl.textContent = text;
|
|
23
|
+
genericEl.textContent = text;
|
|
24
|
+
overviewEl.innerHTML = buildPreviewHtml(body, endpointId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {unknown} body
|
|
29
|
+
* @param {string} endpointId
|
|
30
|
+
*/
|
|
31
|
+
function buildPreviewHtml(body, endpointId) {
|
|
32
|
+
if (body == null) return `<p class="tryit-guidance">Empty body.</p>`;
|
|
33
|
+
if (typeof body !== "object") return `<pre class="code-block"><code>${escape(String(body))}</code></pre>`;
|
|
34
|
+
|
|
35
|
+
/** @type {Record<string, unknown>} */
|
|
36
|
+
const o = /** @type {Record<string, unknown>} */ (body);
|
|
37
|
+
|
|
38
|
+
if (endpointId === "counts-by-object-type" || endpointId === "uc-count-object-types") {
|
|
39
|
+
const reconcile = asRecord(o.reconcile);
|
|
40
|
+
const byType = reconcile && asRecord(reconcile.byType);
|
|
41
|
+
const rows = [
|
|
42
|
+
["engine", fmt(o.engine)],
|
|
43
|
+
["counterId", fmt(o.counterId)],
|
|
44
|
+
["snapshotSetId", fmt(o.snapshotSetId)],
|
|
45
|
+
["ingested", fmt(o.ingested)],
|
|
46
|
+
["pagesWalked", fmt(o.pagesWalked)],
|
|
47
|
+
["walkedTotal", fmt(reconcile?.walkedTotal)],
|
|
48
|
+
["censusTotal", fmt(reconcile?.censusTotal)],
|
|
49
|
+
];
|
|
50
|
+
if (byType) {
|
|
51
|
+
for (const [k, v] of Object.entries(byType).slice(0, 12)) {
|
|
52
|
+
const rec = asRecord(v);
|
|
53
|
+
rows.push([`byType.${k}`, rec ? `walked=${rec.walked} census=${rec.census} Δ=${rec.delta}` : fmt(v)]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return kvGrid(rows);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (endpointId === "counts-index") {
|
|
60
|
+
return kvGrid([
|
|
61
|
+
["engine", fmt(o.engine)],
|
|
62
|
+
["counters", Array.isArray(o.counters) ? o.counters.join(", ") : fmt(o.counters)],
|
|
63
|
+
["cachedSnapshotSetId", fmt(o.cachedSnapshotSetId)],
|
|
64
|
+
["rebuiltAt", fmt(o.rebuiltAt)],
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
endpointId === "process-health" ||
|
|
70
|
+
endpointId === "estate-health" ||
|
|
71
|
+
endpointId === "ui-session-gate"
|
|
72
|
+
) {
|
|
73
|
+
return kvGrid([
|
|
74
|
+
["memorixReachable", fmt(o.memorixReachable)],
|
|
75
|
+
["estateReady", fmt(o.estateReady)],
|
|
76
|
+
["stationOk", fmt(o.stationOk ?? o.ok)],
|
|
77
|
+
["userMessage", fmt(o.userMessage)],
|
|
78
|
+
["memorixUrl", fmt(o.memorixUrl)],
|
|
79
|
+
["memorixOrgId", fmt(o.memorixOrgId)],
|
|
80
|
+
["retryAfterMs", fmt(o.retryAfterMs)],
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (endpointId === "bootstrap" || endpointId === "ui-bootstrap" || endpointId === "ui-home") {
|
|
85
|
+
const boot = /** @type {Record<string, unknown>|null} */ (o.bootstrap && typeof o.bootstrap === "object" ? o.bootstrap : null);
|
|
86
|
+
const census = boot && typeof boot.typedCensus === "object" ? /** @type {Record<string, unknown>} */ (boot.typedCensus) : null;
|
|
87
|
+
const coverage = boot && typeof boot.assessmentCoverage === "object" ? /** @type {Record<string, unknown>} */ (boot.assessmentCoverage) : null;
|
|
88
|
+
const typed =
|
|
89
|
+
census ||
|
|
90
|
+
(coverage && typeof coverage.typedRecordCensus === "object"
|
|
91
|
+
? /** @type {Record<string, unknown>} */ (coverage.typedRecordCensus)
|
|
92
|
+
: null);
|
|
93
|
+
const rows = [
|
|
94
|
+
["snapshotSetId", fmt(o.snapshotSetId)],
|
|
95
|
+
["analysisSnapshotId", fmt(o.analysisSnapshotId)],
|
|
96
|
+
["liveGenerationId", fmt(o.liveGenerationId)],
|
|
97
|
+
["liveAvailability", fmt(o.liveAvailability)],
|
|
98
|
+
["liveUnavailableReason", fmt(o.liveUnavailableReason)],
|
|
99
|
+
["liveBootstrap", o.liveBootstrap == null ? "null" : "present"],
|
|
100
|
+
];
|
|
101
|
+
if (typed) {
|
|
102
|
+
rows.push(
|
|
103
|
+
["census.endpoint-asset", fmt(typed["endpoint-asset"] ?? typed.endpointAsset)],
|
|
104
|
+
["census.unresolved-subject", fmt(typed["unresolved-subject"] ?? typed.unresolvedSubject)],
|
|
105
|
+
["census.finding", fmt(typed.finding)],
|
|
106
|
+
["census.alert", fmt(typed.alert)],
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (coverage != null) rows.push(["assessmentCoverage", safeJson(coverage).slice(0, 400)]);
|
|
110
|
+
return kvGrid(rows) + `<p class="field-hint" style="margin-top:12px">Generation tokens are opaque — do not send them back on product URLs.</p>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (endpointId === "object-security-context-join" || endpointId === "ui-object-join") {
|
|
114
|
+
return kvGrid([
|
|
115
|
+
["snapshotSetId", fmt(o.snapshotSetId)],
|
|
116
|
+
["liveGenerationId", fmt(o.liveGenerationId)],
|
|
117
|
+
["context", o.context == null ? "null" : "present"],
|
|
118
|
+
["classification", o.classification == null ? "null" : "present"],
|
|
119
|
+
["preAiContext", o.preAiContext == null ? "null" : "present"],
|
|
120
|
+
["liveOverlay", o.liveOverlay == null ? "null" : "present"],
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (endpointId === "investigation-packets-index" || endpointId === "ui-packets-index" || endpointId === "ui-evidence") {
|
|
125
|
+
const index = o.index;
|
|
126
|
+
let count = "—";
|
|
127
|
+
if (Array.isArray(index)) count = String(index.length);
|
|
128
|
+
else if (index && typeof index === "object") {
|
|
129
|
+
const items = /** @type {Record<string, unknown>} */ (index).items ?? /** @type {Record<string, unknown>} */ (index).packets;
|
|
130
|
+
if (Array.isArray(items)) count = String(items.length);
|
|
131
|
+
else count = "object";
|
|
132
|
+
}
|
|
133
|
+
return kvGrid([
|
|
134
|
+
["snapshotSetId", fmt(o.snapshotSetId)],
|
|
135
|
+
["analysisSnapshotId", fmt(o.analysisSnapshotId)],
|
|
136
|
+
["liveGenerationId", fmt(o.liveGenerationId)],
|
|
137
|
+
["index size", count],
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (String(endpointId).startsWith("working-") || String(endpointId).startsWith("ui-")) {
|
|
142
|
+
if (o.product != null || o.data != null) {
|
|
143
|
+
return kvGrid([
|
|
144
|
+
["product", fmt(o.product)],
|
|
145
|
+
["variant", fmt(o.variant)],
|
|
146
|
+
["snapshotSetId", fmt(o.snapshotSetId)],
|
|
147
|
+
["liveGenerationId", fmt(o.liveGenerationId)],
|
|
148
|
+
["data", o.data == null ? "null" : summarizeData(o.data)],
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return kvGrid(
|
|
154
|
+
Object.keys(o)
|
|
155
|
+
.slice(0, 12)
|
|
156
|
+
.map((k) => [k, summarizeValue(o[k])]),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Presence flags for createResponseView.setAvailableViews.
|
|
162
|
+
* @param {unknown} body
|
|
163
|
+
* @param {string[]} views
|
|
164
|
+
*/
|
|
165
|
+
export function presenceFor(body, views) {
|
|
166
|
+
const has = body != null;
|
|
167
|
+
return {
|
|
168
|
+
fetch: has && views.includes("preview"),
|
|
169
|
+
overview: has && views.includes("preview"),
|
|
170
|
+
generic: has,
|
|
171
|
+
parsed: has,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {unknown} data
|
|
177
|
+
*/
|
|
178
|
+
function summarizeData(data) {
|
|
179
|
+
if (Array.isArray(data)) return `array(${data.length})`;
|
|
180
|
+
if (data && typeof data === "object") {
|
|
181
|
+
const keys = Object.keys(/** @type {object} */ (data));
|
|
182
|
+
const pageVariants = /** @type {Record<string, unknown>} */ (data).pageVariants;
|
|
183
|
+
const extra = Array.isArray(pageVariants) ? `, pageVariants=${pageVariants.length}` : "";
|
|
184
|
+
return `object{${keys.slice(0, 8).join(", ")}${keys.length > 8 ? ",…" : ""}}${extra}`;
|
|
185
|
+
}
|
|
186
|
+
return fmt(data);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {unknown} v
|
|
191
|
+
*/
|
|
192
|
+
function summarizeValue(v) {
|
|
193
|
+
if (v == null) return "null";
|
|
194
|
+
if (typeof v === "object") return Array.isArray(v) ? `array(${v.length})` : "object";
|
|
195
|
+
return fmt(v);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {[string, string][]} rows
|
|
200
|
+
*/
|
|
201
|
+
function kvGrid(rows) {
|
|
202
|
+
const filtered = rows.filter(([, v]) => v !== "" && v !== "undefined");
|
|
203
|
+
return `<table class="doc-table"><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody>${filtered
|
|
204
|
+
.map(
|
|
205
|
+
([k, v]) =>
|
|
206
|
+
`<tr><td><code>${escape(k)}</code></td><td class="mono">${escape(v)}</td></tr>`,
|
|
207
|
+
)
|
|
208
|
+
.join("")}</tbody></table>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {unknown} v
|
|
213
|
+
*/
|
|
214
|
+
function fmt(v) {
|
|
215
|
+
if (v === undefined) return "";
|
|
216
|
+
if (v === null) return "null";
|
|
217
|
+
if (typeof v === "object") return safeJson(v);
|
|
218
|
+
return String(v);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {unknown} body
|
|
223
|
+
*/
|
|
224
|
+
function safeJson(body) {
|
|
225
|
+
try {
|
|
226
|
+
return JSON.stringify(body, null, 2);
|
|
227
|
+
} catch {
|
|
228
|
+
return String(body);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function escape(value) {
|
|
233
|
+
return String(value)
|
|
234
|
+
.replace(/&/g, "&")
|
|
235
|
+
.replace(/</g, "<")
|
|
236
|
+
.replace(/>/g, ">")
|
|
237
|
+
.replace(/"/g, """);
|
|
238
|
+
}
|