@x12i/map-store-client 0.1.2 → 0.1.4

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 (80) hide show
  1. package/README.md +147 -1
  2. package/dist/attackSurfaces.d.ts.map +1 -1
  3. package/dist/attackSurfaces.js +17 -3
  4. package/dist/attackSurfaces.js.map +1 -1
  5. package/dist/bagCatalog.js +3 -3
  6. package/dist/bagCatalog.js.map +1 -1
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/bootstrap.js +1 -15
  9. package/dist/bootstrap.js.map +1 -1
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +3 -0
  12. package/dist/errors.js.map +1 -1
  13. package/dist/estate/analytics.d.ts +41 -0
  14. package/dist/estate/analytics.d.ts.map +1 -0
  15. package/dist/estate/analytics.js +115 -0
  16. package/dist/estate/analytics.js.map +1 -0
  17. package/dist/estate/census.d.ts +115 -0
  18. package/dist/estate/census.d.ts.map +1 -0
  19. package/dist/estate/census.js +266 -0
  20. package/dist/estate/census.js.map +1 -0
  21. package/dist/estate/client.d.ts +66 -0
  22. package/dist/estate/client.d.ts.map +1 -0
  23. package/dist/estate/client.js +10 -0
  24. package/dist/estate/client.js.map +1 -0
  25. package/dist/estate/content.d.ts +77 -0
  26. package/dist/estate/content.d.ts.map +1 -0
  27. package/dist/estate/content.js +226 -0
  28. package/dist/estate/content.js.map +1 -0
  29. package/dist/estate/contract.d.ts +64 -0
  30. package/dist/estate/contract.d.ts.map +1 -0
  31. package/dist/estate/contract.js +258 -0
  32. package/dist/estate/contract.js.map +1 -0
  33. package/dist/estate/decisions.d.ts +32 -0
  34. package/dist/estate/decisions.d.ts.map +1 -0
  35. package/dist/estate/decisions.js +113 -0
  36. package/dist/estate/decisions.js.map +1 -0
  37. package/dist/estate/ids.d.ts +3 -0
  38. package/dist/estate/ids.d.ts.map +1 -0
  39. package/dist/estate/ids.js +6 -0
  40. package/dist/estate/ids.js.map +1 -0
  41. package/dist/estate/index.d.ts +26 -0
  42. package/dist/estate/index.d.ts.map +1 -0
  43. package/dist/estate/index.js +17 -0
  44. package/dist/estate/index.js.map +1 -0
  45. package/dist/estate/localStories.d.ts +28 -0
  46. package/dist/estate/localStories.d.ts.map +1 -0
  47. package/dist/estate/localStories.js +980 -0
  48. package/dist/estate/localStories.js.map +1 -0
  49. package/dist/estate/pack.d.ts +45 -0
  50. package/dist/estate/pack.d.ts.map +1 -0
  51. package/dist/estate/pack.js +83 -0
  52. package/dist/estate/pack.js.map +1 -0
  53. package/dist/estate/predictedAttack.d.ts +79 -0
  54. package/dist/estate/predictedAttack.d.ts.map +1 -0
  55. package/dist/estate/predictedAttack.js +321 -0
  56. package/dist/estate/predictedAttack.js.map +1 -0
  57. package/dist/estate/stories.d.ts +85 -0
  58. package/dist/estate/stories.d.ts.map +1 -0
  59. package/dist/estate/stories.js +215 -0
  60. package/dist/estate/stories.js.map +1 -0
  61. package/dist/fetch.d.ts +2 -0
  62. package/dist/fetch.d.ts.map +1 -1
  63. package/dist/fetch.js +35 -4
  64. package/dist/fetch.js.map +1 -1
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/queries.d.ts +98 -0
  70. package/dist/queries.d.ts.map +1 -0
  71. package/dist/queries.js +180 -0
  72. package/dist/queries.js.map +1 -0
  73. package/dist/types.d.ts +1 -1
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js.map +1 -1
  76. package/dist/working.d.ts +11 -1
  77. package/dist/working.d.ts.map +1 -1
  78. package/dist/working.js +23 -15
  79. package/dist/working.js.map +1 -1
  80. package/package.json +14 -1
@@ -0,0 +1,980 @@
1
+ import { getTemplate, validateInput } from '@x12i/storyflowix';
2
+ import { predictionDiagram, predictionStepsFrom, tacticColumn } from './predictedAttack';
3
+ function asRecord(value) {
4
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
5
+ }
6
+ function list(value) {
7
+ return Array.isArray(value) ? value : [];
8
+ }
9
+ function text(value) {
10
+ if (typeof value === 'string')
11
+ return value.trim() || null;
12
+ if (typeof value === 'number' && Number.isFinite(value))
13
+ return String(value);
14
+ return null;
15
+ }
16
+ function count(value) {
17
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
18
+ }
19
+ /** Strings in a list, or the `value` of `{ value }` claim items. */
20
+ function texts(value) {
21
+ return list(value).flatMap((item) => {
22
+ const own = text(item) ?? text(asRecord(item)?.value);
23
+ return own ? [own] : [];
24
+ });
25
+ }
26
+ /** `NO_PATH_EVIDENCE` or `no-nrts-cidr-match` as words. */
27
+ function human(code) {
28
+ const raw = text(code);
29
+ if (!raw)
30
+ return null;
31
+ if (!/^[A-Za-z0-9_\-:]+$/.test(raw) || !/[_-]/.test(raw))
32
+ return raw;
33
+ const words = raw.replace(/[_-]+/g, ' ').trim().toLowerCase();
34
+ return words ? words[0].toUpperCase() + words.slice(1) : raw;
35
+ }
36
+ /** First sentence, clipped for a node label. */
37
+ function brief(value, max = 80) {
38
+ const first = value.split(/(?<=[.!?])\s/)[0] ?? value;
39
+ return first.length > max ? `${first.slice(0, max - 1).trimEnd()}…` : first;
40
+ }
41
+ function tone(code) {
42
+ const raw = (text(code) ?? '').toUpperCase();
43
+ if (/^(REACHABLE|EXPLOITABLE|EXPEDITE|CRITICAL|CONFIRMED|HIGH)$/.test(raw))
44
+ return 'crit';
45
+ if (/^(BLOCKED|NOT_REACHABLE|NOT_EXPLOITABLE|CLOSED|RESOLVED|SAFE)$/.test(raw))
46
+ return 'ok';
47
+ if (/^(POSSIBLE|SCHEDULE|UNCONFIRMED|MEDIUM|AMBIGUOUS|UNRESOLVED|UNRESOLVED_IDENTITY)$/.test(raw))
48
+ return 'warn';
49
+ return 'neutral';
50
+ }
51
+ function facts(rows) {
52
+ return rows.flatMap(([label, value]) => {
53
+ const shown = human(value) ?? (Array.isArray(value) ? texts(value).join(', ') || null : null);
54
+ return shown ? [[label, shown]] : [];
55
+ });
56
+ }
57
+ function rowsOf(contents, contentType) {
58
+ return contents.get(contentType) ?? [];
59
+ }
60
+ function rowOf(contents, contentType, match) {
61
+ const rows = rowsOf(contents, contentType);
62
+ return (match ? rows.find(match) : rows[0]) ?? null;
63
+ }
64
+ function analysis(contents, analysisType) {
65
+ return rowOf(contents, 'analysis', (row) => row.analysisType === analysisType);
66
+ }
67
+ function narrative(row, kind) {
68
+ const items = list(asRecord(row?.itemContext)?.narratives);
69
+ for (const item of items) {
70
+ const rec = asRecord(item);
71
+ if (rec?.kind === kind)
72
+ return text(rec.text);
73
+ }
74
+ return null;
75
+ }
76
+ function securityContext(contents) {
77
+ return rowOf(contents, 'security-context');
78
+ }
79
+ function subjectName(contents, fallback) {
80
+ const inv = rowOf(contents, 'inventory');
81
+ const identity = asRecord(securityContext(contents)?.identity);
82
+ const named = text(inv?.displayName) ??
83
+ text(inv?.subjectDisplayName) ??
84
+ text(inv?.title) ??
85
+ text(identity?.displayName) ??
86
+ texts(inv?.addresses)[0] ??
87
+ texts(identity?.addresses)[0];
88
+ if (named)
89
+ return named;
90
+ const absence = narrative(inv, 'absence');
91
+ const referred = absence?.match(/Refer to it as ([^.]+)\./)?.[1];
92
+ return referred?.trim() || fallback;
93
+ }
94
+ function assessmentGaps(row) {
95
+ return texts(asRecord(row?.assessment)?.gaps);
96
+ }
97
+ function ledger(contents) {
98
+ const sc = securityContext(contents);
99
+ const fromContext = list(sc?.completenessLedger);
100
+ const rows = fromContext.length ? fromContext : list(rowOf(contents, 'field-completeness')?.completenessLedger);
101
+ return rows.flatMap((item) => (asRecord(item) ? [item] : []));
102
+ }
103
+ function zoneEntry(contents) {
104
+ return ledger(contents).find((item) => item.dimension === 'network-zone') ?? null;
105
+ }
106
+ function ledgerGaps(contents) {
107
+ return ledger(contents).flatMap((item) => {
108
+ if (item.status !== 'MISSING')
109
+ return [];
110
+ const what = human(item.dimension) ?? 'A field';
111
+ const why = human(item.reasonCode);
112
+ return [why ? `${what} is missing: ${why.toLowerCase()}.` : `${what} is missing.`];
113
+ });
114
+ }
115
+ function used(contents, types) {
116
+ return types.filter((type) => rowsOf(contents, type).length > 0);
117
+ }
118
+ function uniqueGaps(gaps, max = 8) {
119
+ return [...new Set(gaps.map((gap) => gap.trim()).filter(Boolean))].slice(0, max);
120
+ }
121
+ function verdictCounts(sc) {
122
+ const summary = asRecord(asRecord(asRecord(sc?.exploitability)?.context)?.exploitabilityContextSummary);
123
+ return { findings: count(summary?.findingCount), cves: count(summary?.cveCount), verdicts: asRecord(summary?.localVerdicts) };
124
+ }
125
+ function assetTriage(contents) {
126
+ return rowOf(contents, 'triage', (row) => row.urgency != null || row.decisionType === 'asset-triage');
127
+ }
128
+ function assetSituation(contents) {
129
+ const sc = securityContext(contents);
130
+ const inv = rowOf(contents, 'inventory');
131
+ if (!sc && !inv)
132
+ return { needs: ['The security context or inventory row for this asset.'] };
133
+ const name = subjectName(contents, 'This asset');
134
+ const triage = assetTriage(contents);
135
+ const pathGap = analysis(contents, 'path-gap');
136
+ const identity = asRecord(sc?.identity);
137
+ const classification = asRecord(sc?.classification);
138
+ const summary = asRecord(classification?.summary);
139
+ const role = ['deviceWorkloadType', 'functionalRoles', 'networkFunction', 'businessEnvironment']
140
+ .flatMap((key) => texts(summary?.[key]))
141
+ .map((value) => human(value) ?? value)[0];
142
+ const zone = zoneEntry(contents);
143
+ const reach = asRecord(sc?.reachability);
144
+ const reachCode = pathGap?.pathState ?? reach?.evaluationStatus;
145
+ const traffic = asRecord(sc?.observedTrafficSummary);
146
+ const exploit = asRecord(sc?.exploitability);
147
+ const { findings, cves, verdicts } = verdictCounts(sc);
148
+ const exploitable = count(verdicts?.EXPLOITABLE) ?? 0;
149
+ const possible = count(verdicts?.POSSIBLE) ?? 0;
150
+ const exploitTitle = exploitable
151
+ ? `${exploitable} exploitable finding${exploitable === 1 ? '' : 's'}`
152
+ : possible
153
+ ? `${possible} possibly exploitable`
154
+ : human(exploit?.evaluationStatus) ?? 'Exploitability unknown';
155
+ const urgency = triage?.urgency;
156
+ const checks = list(triage?.checks).flatMap((item) => {
157
+ const question = text(asRecord(item)?.question);
158
+ return question ? [question] : [];
159
+ });
160
+ const evidence = list(triage?.priorityEvidence).flatMap((item, index) => {
161
+ const rec = asRecord(item);
162
+ const fact = text(rec?.fact);
163
+ if (!fact)
164
+ return [];
165
+ return [
166
+ {
167
+ id: `situation-evidence-${index}`,
168
+ title: human(rec?.factor) ?? 'Evidence',
169
+ subtitle: brief(fact, 56),
170
+ link: { label: 'evidence' },
171
+ details: { summary: fact },
172
+ },
173
+ ];
174
+ });
175
+ const decision = text(triage?.summary) ?? text(asRecord(triage?.assessment)?.summary);
176
+ const steps = [
177
+ {
178
+ id: 'role',
179
+ title: role ?? 'Role not classified',
180
+ subtitle: human(identity?.identityStatus ?? inv?.identityStatus) ?? undefined,
181
+ tag: name,
182
+ details: {
183
+ summary: text(asRecord(classification?.explanation)?.whyInsufficient) ??
184
+ narrative(inv, 'identity') ??
185
+ `${name} as the inventory records it.`,
186
+ facts: facts([
187
+ ['Identity', identity?.identityStatus ?? inv?.identityStatus],
188
+ ['Addresses', texts(identity?.addresses ?? inv?.addresses).slice(0, 4)],
189
+ ['Classification', classification?.processingStatus],
190
+ ]),
191
+ },
192
+ attachments: zone
193
+ ? {
194
+ context: [
195
+ {
196
+ id: 'situation-zone',
197
+ title: 'Zone',
198
+ subtitle: zone.status === 'MISSING' ? 'Not placed' : human(zone.status) ?? undefined,
199
+ details: { summary: human(zone.reasonCode) ?? 'No zone reason is recorded.' },
200
+ },
201
+ ],
202
+ }
203
+ : undefined,
204
+ },
205
+ {
206
+ id: 'reach',
207
+ title: human(reachCode) ?? 'Reachability unknown',
208
+ subtitle: traffic ? `Traffic: ${(human(traffic.status) ?? 'unknown').toLowerCase()}` : undefined,
209
+ tone: tone(reachCode),
210
+ link: { label: 'then' },
211
+ details: {
212
+ summary: text(asRecord(pathGap?.assessment)?.summary) ?? 'No route or hop record is stored for this asset.',
213
+ facts: facts([
214
+ ['Evaluation', reach?.evaluationStatus],
215
+ ['Path state', pathGap?.pathState],
216
+ ['Observed traffic', traffic?.status],
217
+ ]),
218
+ },
219
+ },
220
+ {
221
+ id: 'exploit',
222
+ title: exploitTitle,
223
+ subtitle: findings != null ? `${findings} finding${findings === 1 ? '' : 's'}, ${cves ?? 0} CVE${cves === 1 ? '' : 's'}` : undefined,
224
+ tone: exploitable ? 'crit' : possible ? 'warn' : 'neutral',
225
+ link: { label: 'then' },
226
+ details: {
227
+ summary: texts(exploit?.limitations)[0] ?? 'Local exploitability as the security context reports it.',
228
+ facts: facts([
229
+ ['Evaluation', exploit?.evaluationStatus],
230
+ ['Findings', findings],
231
+ ['CVEs', cves],
232
+ ]),
233
+ list: texts(exploit?.limitations).length
234
+ ? { title: 'Limitations', items: texts(exploit?.limitations) }
235
+ : undefined,
236
+ },
237
+ attachments: evidence.length ? { findings: evidence.slice(0, 3) } : undefined,
238
+ },
239
+ {
240
+ id: 'decide',
241
+ title: human(urgency) ?? 'No decision yet',
242
+ subtitle: triage?.confidence ? `${human(triage.confidence)} confidence` : undefined,
243
+ tone: tone(urgency),
244
+ link: { label: 'so' },
245
+ details: {
246
+ summary: decision ?? 'No triage decision is stored for this asset.',
247
+ list: checks.length ? { title: 'Next checks', items: checks } : undefined,
248
+ },
249
+ },
250
+ ];
251
+ return {
252
+ input: {
253
+ title: `${name}: what to do about it`,
254
+ subtitle: 'Role, reachability, exploitability and the decision, from stored content.',
255
+ overview: decision ? brief(decision, 260) : undefined,
256
+ glance: facts([
257
+ ['Asset', name],
258
+ ['Role', role],
259
+ ['Reachability', reachCode],
260
+ ['Findings', findings],
261
+ ['Urgency', urgency],
262
+ ]),
263
+ steps,
264
+ },
265
+ sources: used(contents, ['inventory', 'security-context', 'analysis', 'triage']),
266
+ gaps: uniqueGaps([...assessmentGaps(pathGap), ...ledgerGaps(contents)]),
267
+ };
268
+ }
269
+ const ROUTE_VERDICTS = new Set(['REACHABLE', 'BLOCKED', 'NO_PATH_EVIDENCE', 'UNKNOWN_MISSING_ROUTE']);
270
+ function routeVerdict(contents) {
271
+ const pathGap = analysis(contents, 'path-gap');
272
+ const pathState = text(pathGap?.pathState);
273
+ if (pathState && ROUTE_VERDICTS.has(pathState)) {
274
+ return { code: pathState, why: text(asRecord(pathGap?.assessment)?.summary) ?? text(pathGap?.note) ?? '' };
275
+ }
276
+ const device = analysis(contents, 'device-reachability');
277
+ const deviceState = text(device?.reachability);
278
+ if (deviceState && ROUTE_VERDICTS.has(deviceState))
279
+ return { code: deviceState, why: text(device?.evidence) ?? '' };
280
+ const reach = asRecord(securityContext(contents)?.reachability) ?? asRecord(rowOf(contents, 'reachability-claims')?.reachability);
281
+ if (!reach)
282
+ return null;
283
+ const codes = texts(reach.reasonCodes);
284
+ if (codes.includes('NO_REACHABILITY_CLAIMS')) {
285
+ return { code: 'NO_PATH_EVIDENCE', why: texts(reach.limitations)[0] ?? 'No reachability claim is stored.' };
286
+ }
287
+ return {
288
+ code: 'UNKNOWN_MISSING_ROUTE',
289
+ why: `Reachability is ${(human(reach.evaluationStatus) ?? 'recorded').toLowerCase()}, but no route or hop is stored.`,
290
+ };
291
+ }
292
+ function hopsOf(contents) {
293
+ for (const row of rowsOf(contents, 'reachability-paths')) {
294
+ for (const key of ['hops', 'path', 'steps']) {
295
+ const hops = list(row[key]).flatMap((item) => (asRecord(item) ? [item] : []));
296
+ if (hops.length)
297
+ return hops;
298
+ }
299
+ }
300
+ return [];
301
+ }
302
+ function routeEvidence(contents, objectType) {
303
+ const verdict = routeVerdict(contents);
304
+ if (!verdict) {
305
+ return {
306
+ needs: [
307
+ `A route verdict for this ${objectType}: a path-gap analysis, a device reachability analysis, or reachability on the security context.`,
308
+ ],
309
+ };
310
+ }
311
+ const name = subjectName(contents, `This ${objectType}`);
312
+ const pathGap = analysis(contents, 'path-gap');
313
+ const reach = asRecord(securityContext(contents)?.reachability);
314
+ const hops = hopsOf(contents).map((hop, index) => ({
315
+ id: `hop-${index}`,
316
+ title: text(hop.label) ?? text(hop.name) ?? text(hop.title) ?? text(hop.id) ?? `Hop ${index + 1}`,
317
+ column: `Hop ${index + 1}`,
318
+ link: text(hop.allows) ? { label: text(hop.allows) } : undefined,
319
+ }));
320
+ const gapTexts = uniqueGaps([...assessmentGaps(pathGap), ...texts(reach?.limitations)], 5);
321
+ const gapCodes = texts(reach?.reasonCodes);
322
+ const gaps = [
323
+ ...gapTexts.map((gap, index) => ({
324
+ id: `route-gap-${index}`,
325
+ title: brief(gap, 60),
326
+ tag: 'MISSING_EVIDENCE',
327
+ details: { summary: gap },
328
+ })),
329
+ ...gapCodes.map((code, index) => ({
330
+ id: `route-code-${index}`,
331
+ title: human(code) ?? code,
332
+ tag: code,
333
+ })),
334
+ ];
335
+ return {
336
+ input: {
337
+ title: `Can ${name} be reached?`,
338
+ subtitle: 'Published hops and the route verdict, from stored content.',
339
+ overview: verdict.why ? brief(verdict.why, 260) : undefined,
340
+ glance: facts([
341
+ ['Subject', name],
342
+ ['Verdict', verdict.code],
343
+ ['Published hops', hops.length],
344
+ ['Open evidence', gaps.length],
345
+ ]),
346
+ steps: [
347
+ ...hops,
348
+ {
349
+ id: 'verdict',
350
+ title: verdict.code,
351
+ column: 'Verdict',
352
+ tone: verdict.code === 'REACHABLE' ? 'crit' : verdict.code === 'BLOCKED' ? 'ok' : 'neutral',
353
+ details: { summary: verdict.why || 'The stored route verdict.' },
354
+ attachments: gaps.length ? { gap: gaps.slice(0, 6) } : undefined,
355
+ },
356
+ ],
357
+ },
358
+ sources: used(contents, ['analysis', 'security-context', 'reachability-claims', 'reachability-paths']),
359
+ gaps: gapTexts,
360
+ };
361
+ }
362
+ function proposalsOf(contents) {
363
+ const out = [];
364
+ const triage = rowOf(contents, 'triage');
365
+ const decided = triage?.effective === true ? triage : null;
366
+ const urgency = text(triage?.urgency);
367
+ if (urgency) {
368
+ out.push({
369
+ label: human(urgency) ?? urgency,
370
+ detail: text(triage?.summary) ?? text(asRecord(triage?.assessment)?.summary) ?? '',
371
+ source: 'Triage',
372
+ decision: decided,
373
+ });
374
+ }
375
+ for (const model of rowsOf(contents, 'triage-model')) {
376
+ for (const item of list(model.treatmentCandidates)) {
377
+ const value = text(asRecord(item)?.value) ?? text(item);
378
+ if (value)
379
+ out.push({ label: brief(value, 48), detail: value, source: 'Triage model', decision: null });
380
+ }
381
+ const suggested = text(model.suggestedUrgency);
382
+ if (suggested)
383
+ out.push({ label: human(suggested) ?? suggested, detail: text(asRecord(model.assessment)?.summary) ?? '', source: 'Triage model', decision: null });
384
+ }
385
+ for (const row of rowsOf(contents, 'mitigation')) {
386
+ for (const step of list(row.interruptionSteps)) {
387
+ const value = text(step) ?? text(asRecord(step)?.title) ?? text(asRecord(step)?.value);
388
+ if (value)
389
+ out.push({ label: brief(value, 48), detail: value, source: 'Mitigation', decision: null });
390
+ }
391
+ }
392
+ return out;
393
+ }
394
+ function responseWork(contents, objectType) {
395
+ const proposals = proposalsOf(contents);
396
+ const triage = rowOf(contents, 'triage');
397
+ if (!proposals.length) {
398
+ const why = texts(triage?.requests)[0] ?? text(triage?.reason);
399
+ return {
400
+ needs: [
401
+ 'A response proposal: a triage urgency, a triage-model treatment candidate, or a mitigation step.',
402
+ ...(why ? [`Triage says: ${why}.`] : []),
403
+ ],
404
+ };
405
+ }
406
+ const name = subjectName(contents, `This ${objectType}`);
407
+ const workState = text(triage?.workState);
408
+ return {
409
+ input: {
410
+ title: `Response work for ${name}`,
411
+ subtitle: 'From proposal to decision to task, from stored content.',
412
+ overview: `${proposals.length} response${proposals.length === 1 ? ' is' : 's are'} proposed. ${proposals.some((p) => p.decision) ? 'A decision is effective.' : 'None is decided yet, so every row stays dashed.'}`,
413
+ glance: facts([
414
+ ['Subject', name],
415
+ ['Proposals', proposals.length],
416
+ ['Work state', workState],
417
+ ]),
418
+ root: { id: 'subject', title: name, subtitle: human(objectType) ?? objectType },
419
+ threads: proposals.slice(0, 8).map((proposal, index) => ({
420
+ id: `work-${index}`,
421
+ label: proposal.label,
422
+ style: proposal.decision ? undefined : 'dashed',
423
+ tone: proposal.decision ? undefined : 'info',
424
+ nodes: [
425
+ {
426
+ id: `work-${index}-proposal`,
427
+ title: proposal.label,
428
+ subtitle: `Proposed by ${proposal.source.toLowerCase()}`,
429
+ details: proposal.detail ? { summary: proposal.detail } : undefined,
430
+ },
431
+ proposal.decision
432
+ ? {
433
+ id: `work-${index}-decision`,
434
+ title: human(proposal.decision.disposition) ?? 'Decided',
435
+ subtitle: 'Effective',
436
+ tone: 'ok',
437
+ }
438
+ : null,
439
+ null,
440
+ ],
441
+ })),
442
+ sinks: workState ? [{ id: 'work-state', title: human(workState) ?? workState, subtitle: 'Work state' }] : undefined,
443
+ },
444
+ sources: used(contents, ['triage', 'triage-model', 'mitigation']),
445
+ };
446
+ }
447
+ function factorName(value) {
448
+ const lower = value.toLowerCase();
449
+ if (/reachab|path|route/.test(lower))
450
+ return 'Path';
451
+ if (/exploit/.test(lower))
452
+ return 'Exploit evidence';
453
+ if (/component|version|patch|install/.test(lower))
454
+ return 'Patch status';
455
+ if (/identity|hostname|binding/.test(lower))
456
+ return 'Identity';
457
+ if (/expos|public/.test(lower))
458
+ return 'Exposure';
459
+ return 'Other evidence';
460
+ }
461
+ function evidenceGaps(contents) {
462
+ const factors = new Map();
463
+ const add = (value, code, name = factorName(value), closesWith) => {
464
+ const factor = factors.get(name) ?? { name, reasons: [] };
465
+ if (!factor.reasons.some((reason) => reason.text === value)) {
466
+ factor.reasons.push({ text: value, code, ...(closesWith ? { closesWith } : {}) });
467
+ }
468
+ factors.set(name, factor);
469
+ };
470
+ for (const model of rowsOf(contents, 'triage-model')) {
471
+ for (const item of list(model.evidenceRequests)) {
472
+ const rec = asRecord(item);
473
+ const value = text(rec?.value);
474
+ if (value)
475
+ add(value, rec?.epistemicState === 'HYPOTHESIS' ? 'UNCONFIRMED' : 'MISSING_EVIDENCE', undefined, text(rec?.closesWith));
476
+ }
477
+ }
478
+ const applicability = analysis(contents, 'applicability');
479
+ for (const gap of assessmentGaps(applicability))
480
+ add(gap, 'MISSING_EVIDENCE');
481
+ for (const item of list(applicability?.affectednessHypotheses)) {
482
+ const value = text(asRecord(item)?.value);
483
+ if (value)
484
+ add(value, 'UNCONFIRMED');
485
+ }
486
+ if (list(applicability?.conflictingAssessments).length > 1) {
487
+ add('Assessments of this finding disagree with each other.', 'CONFLICTING', 'Exploit evidence');
488
+ }
489
+ const admission = analysis(contents, 'assessment-admission');
490
+ for (const code of texts(admission?.gaps))
491
+ add(human(code) ?? code, 'MISSING_EVIDENCE', 'Patch status');
492
+ const ova = rowOf(contents, 'open-vuln-assessment');
493
+ if (asRecord(ova?.intelStamp)?.status === 'SOURCE_UNAVAILABLE') {
494
+ add('A vulnerability intelligence source was unavailable for this finding.', 'MISSING_INTEGRATION', 'Exploit evidence');
495
+ }
496
+ if (!factors.size)
497
+ return { needs: ['Open factors on this finding: triage-model evidence requests or analysis gaps.'] };
498
+ const name = subjectName(contents, 'This finding');
499
+ const steps = [...factors.values()].slice(0, 6).map((factor, index) => ({
500
+ id: `factor-${index}`,
501
+ title: factor.name,
502
+ subtitle: `${factor.reasons.length} open`,
503
+ attachments: {
504
+ gap: factor.reasons.slice(0, 3).map((reason, at) => ({
505
+ id: `factor-${index}-gap-${at}`,
506
+ title: brief(reason.text, 60),
507
+ tag: reason.code,
508
+ details: { summary: reason.text, ...(reason.closesWith ? { closesWith: reason.closesWith } : {}) },
509
+ })),
510
+ },
511
+ }));
512
+ return {
513
+ input: {
514
+ title: `What is still open on ${name}`,
515
+ subtitle: 'Each open factor and why it is open, from stored content.',
516
+ glance: facts([
517
+ ['Finding', name],
518
+ ['Open factors', factors.size],
519
+ ]),
520
+ steps,
521
+ },
522
+ sources: used(contents, ['triage-model', 'analysis', 'open-vuln-assessment']),
523
+ };
524
+ }
525
+ function alertActivity(contents) {
526
+ const inv = rowOf(contents, 'inventory');
527
+ if (!inv)
528
+ return { needs: ['The inventory row for this alert.'] };
529
+ const name = text(inv.title) ?? subjectName(contents, 'This alert');
530
+ const pressure = analysis(contents, 'alert-pressure');
531
+ const correlation = analysis(contents, 'alert-correlation');
532
+ const observation = rowOf(contents, 'observation');
533
+ const triage = rowOf(contents, 'triage');
534
+ const overview = text(rowOf(contents, 'overview')?.summary);
535
+ const correlated = text(correlation?.correlation);
536
+ const steps = [
537
+ {
538
+ id: 'pressure',
539
+ title: human(pressure?.pressure) ?? 'Pressure unknown',
540
+ subtitle: pressure?.severity ? `Severity ${(human(pressure.severity) ?? '').toLowerCase()}` : undefined,
541
+ column: 'Pressure',
542
+ tone: tone(pressure?.severity),
543
+ details: {
544
+ summary: text(pressure?.evidence) ?? 'No pressure analysis is stored.',
545
+ facts: facts([
546
+ ['Confidence', pressure?.confidence],
547
+ ['Grounding', pressure?.groundingStatus],
548
+ ]),
549
+ },
550
+ },
551
+ {
552
+ id: 'observation',
553
+ title: observation?.observed == null ? 'Not observed' : human(String(observation.observed)) ?? 'Observed',
554
+ subtitle: observation ? `Coverage ${(human(observation.coverage) ?? 'unknown').toLowerCase()}` : undefined,
555
+ column: 'Observation',
556
+ link: { label: 'then' },
557
+ },
558
+ {
559
+ id: 'response',
560
+ title: human(triage?.disposition) ?? 'No response',
561
+ subtitle: triage?.workState ? `Work ${(human(triage.workState) ?? '').toLowerCase()}` : undefined,
562
+ column: 'Response',
563
+ link: { label: 'then' },
564
+ details: {
565
+ summary: triage ? `Readiness: ${(human(triage.readiness) ?? 'unknown').toLowerCase()}.` : 'No triage is stored.',
566
+ facts: facts([
567
+ ['Assessment', triage?.assessmentState],
568
+ ['Urgency', triage?.urgency],
569
+ ]),
570
+ },
571
+ },
572
+ {
573
+ id: 'correlation',
574
+ title: human(correlated) ?? 'Not correlated',
575
+ column: 'Correlation',
576
+ tone: tone(correlated),
577
+ link: { label: 'then' },
578
+ details: {
579
+ summary: text(asRecord(correlation?.assessment)?.summary) ?? 'No correlation analysis is stored.',
580
+ list: texts(correlation?.correlationInterpretations).length
581
+ ? { title: 'Interpretations', items: texts(correlation?.correlationInterpretations) }
582
+ : undefined,
583
+ },
584
+ attachments: correlated && correlated !== 'CONFIRMED'
585
+ ? {
586
+ skipped: [
587
+ {
588
+ id: 'skipped-join',
589
+ title: 'Target, service and time join',
590
+ subtitle: 'Not made',
591
+ details: { summary: text(correlation?.note) ?? 'The alert is not joined to a target, service and time.' },
592
+ },
593
+ ],
594
+ }
595
+ : undefined,
596
+ },
597
+ {
598
+ id: 'detection',
599
+ title: text(inv.ruleId) ?? name,
600
+ subtitle: human(inv.family) ?? undefined,
601
+ column: 'Detection',
602
+ link: { label: 'from' },
603
+ details: {
604
+ summary: `Recorded ${text(inv.occurredAt) ?? 'at an unknown time'}.`,
605
+ facts: facts([
606
+ ['Severity', inv.severity],
607
+ ['Connector', inv.connectorId],
608
+ ]),
609
+ },
610
+ },
611
+ ];
612
+ return {
613
+ input: {
614
+ title: `What happened with ${name}`,
615
+ subtitle: 'Pressure, observation, response, correlation and detection, from stored content.',
616
+ overview: overview ? brief(overview, 260) : undefined,
617
+ glance: facts([
618
+ ['Alert', name],
619
+ ['Family', inv.family],
620
+ ['Correlation', correlated],
621
+ ['Disposition', triage?.disposition],
622
+ ]),
623
+ steps,
624
+ },
625
+ sources: used(contents, ['inventory', 'analysis', 'observation', 'triage', 'overview']),
626
+ gaps: uniqueGaps(assessmentGaps(correlation)),
627
+ };
628
+ }
629
+ function identityGap(contents) {
630
+ const sc = securityContext(contents);
631
+ const inv = rowOf(contents, 'inventory');
632
+ if (!sc && !inv)
633
+ return { needs: ['The inventory row or security context for this subject.'] };
634
+ const name = subjectName(contents, 'This subject');
635
+ const identity = asRecord(sc?.identity);
636
+ const addresses = texts(identity?.addresses ?? inv?.addresses);
637
+ const status = identity?.identityStatus ?? inv?.identityStatus;
638
+ const link = analysis(contents, 'identity-link');
639
+ const bindings = list(sc?.objectBindings).flatMap((item, index) => {
640
+ const rec = asRecord(item);
641
+ const title = text(rec?.kind) ?? text(rec?.source) ?? text(rec?.objectId);
642
+ return title ? [{ id: `binding-${index}`, title: human(title) ?? title, subtitle: 'Possible binding' }] : [];
643
+ });
644
+ const zone = zoneEntry(contents);
645
+ const traffic = asRecord(sc?.observedTrafficSummary);
646
+ const { findings } = verdictCounts(sc);
647
+ const correlation = asRecord(traffic?.vulnerableServiceCorrelation);
648
+ const linked = findings ?? list(correlation?.findingRefs).length;
649
+ const steps = [
650
+ {
651
+ id: 'address',
652
+ title: addresses[0] ?? 'No address',
653
+ subtitle: addresses.length > 1 ? `+${addresses.length - 1} more` : undefined,
654
+ details: { summary: narrative(inv, 'absence') ?? narrative(inv, 'identity') ?? 'Addresses published for this subject.' },
655
+ },
656
+ {
657
+ id: 'identity',
658
+ title: human(status) ?? 'Identity unknown',
659
+ tone: tone(status),
660
+ link: { label: 'names' },
661
+ details: {
662
+ summary: text(link?.evidence) ?? 'No identity link analysis is stored.',
663
+ list: texts(link?.gaps).length ? { title: 'Why it is open', items: texts(link?.gaps) } : undefined,
664
+ },
665
+ attachments: bindings.length ? { binding: bindings.slice(0, 4) } : undefined,
666
+ },
667
+ {
668
+ id: 'zone',
669
+ title: zone ? (zone.status === 'MISSING' ? 'No zone' : human(zone.status) ?? 'Zone') : 'Zone unknown',
670
+ subtitle: human(zone?.reasonCode) ?? undefined,
671
+ link: { label: 'placed in' },
672
+ },
673
+ {
674
+ id: 'traffic',
675
+ title: human(traffic?.status) ?? 'Traffic unknown',
676
+ subtitle: traffic ? `${count(traffic.totalObservationCount) ?? 0} observations` : undefined,
677
+ link: { label: 'talks' },
678
+ },
679
+ {
680
+ id: 'findings',
681
+ title: `${linked} linked finding${linked === 1 ? '' : 's'}`,
682
+ subtitle: 'Cannot be acted on until the identity binds',
683
+ link: { label: 'carries' },
684
+ },
685
+ ];
686
+ return {
687
+ input: {
688
+ title: `Who is ${name}?`,
689
+ subtitle: 'Address, identity, zone, traffic and linked findings, from stored content.',
690
+ overview: text(rowOf(contents, 'overview')?.summary) ?? undefined,
691
+ glance: facts([
692
+ ['Subject', name],
693
+ ['Identity', status],
694
+ ['Addresses', addresses.length],
695
+ ['Linked findings', linked],
696
+ ]),
697
+ steps,
698
+ },
699
+ sources: used(contents, ['inventory', 'security-context', 'analysis', 'overview']),
700
+ gaps: uniqueGaps([...texts(link?.gaps), ...ledgerGaps(contents)]),
701
+ };
702
+ }
703
+ function assetFindings(contents) {
704
+ const sc = securityContext(contents);
705
+ const assessments = list(asRecord(asRecord(sc?.exploitability)?.context)?.assessments).flatMap((item) => asRecord(item) ? [item] : []);
706
+ if (!assessments.length) {
707
+ const counted = count(assetTriage(contents)?.findingCount);
708
+ return {
709
+ needs: [
710
+ 'Per-finding assessments on this asset\u2019s security context: applicability, path, attackability and decision.',
711
+ ...(counted
712
+ ? [`Triage counts ${counted} finding${counted === 1 ? '' : 's'} on this asset, but no per-finding rows are stored with it.`]
713
+ : []),
714
+ ],
715
+ };
716
+ }
717
+ const name = subjectName(contents, 'This asset');
718
+ const attack = asRecord(sc?.attackability);
719
+ const attackCode = texts(attack?.reasonCodes)[0] ?? text(attack?.evaluationStatus);
720
+ return {
721
+ input: {
722
+ title: `Findings on ${name}`,
723
+ subtitle: 'Applicability, path, attackability and decision per finding, from stored content.',
724
+ glance: facts([
725
+ ['Asset', name],
726
+ ['Findings', assessments.length],
727
+ ['Attackability', attack?.evaluationStatus],
728
+ ]),
729
+ root: { id: 'asset', title: name },
730
+ threads: assessments.slice(0, 10).map((row, index) => {
731
+ const label = text(row.cveId) ?? texts(row.cveIds)[0] ?? `Finding ${index + 1}`;
732
+ const verdict = text(row.verdict);
733
+ return {
734
+ id: `finding-${index}`,
735
+ label,
736
+ style: verdict === 'EXPLOITABLE' ? undefined : 'dashed',
737
+ tone: tone(verdict),
738
+ nodes: [
739
+ {
740
+ id: `finding-${index}-applies`,
741
+ title: human(verdict) ?? 'Not evaluated',
742
+ subtitle: human(row.evaluationStatus) ?? undefined,
743
+ details: { summary: text(row.findingId) ?? label },
744
+ },
745
+ null,
746
+ attackCode ? { id: `finding-${index}-attack`, title: human(attackCode) ?? attackCode } : null,
747
+ null,
748
+ ],
749
+ };
750
+ }),
751
+ },
752
+ sources: ['security-context'],
753
+ };
754
+ }
755
+ function techniqueSet(contents) {
756
+ const row = rowOf(contents, 'mitre-techniques');
757
+ const techniques = list(row?.techniques).flatMap((item) => (asRecord(item) ? [item] : []));
758
+ if (!techniques.length)
759
+ return { needs: ['MITRE ATT&CK techniques on this asset. The stored technique list is empty.'] };
760
+ const name = subjectName(contents, 'This asset');
761
+ return {
762
+ input: {
763
+ title: `Techniques that apply to ${name}`,
764
+ subtitle: 'From stored content.',
765
+ steps: techniques.slice(0, 12).map((tech, index) => {
766
+ const tactic = asRecord(tech.tactic);
767
+ const tacticName = text(tactic?.name) ?? text(tactic?.label) ?? (typeof tech.tactic === 'string' ? text(tech.tactic) : null) ?? texts(tech.tactics)[0];
768
+ const tacticId = text(tech.tacticId) ?? text(tactic?.id);
769
+ const name = tacticName && /^TA\d{4}$/i.test(tacticName) ? tacticName : human(tacticName) ?? tacticName;
770
+ const cves = texts(tech.cveIds);
771
+ return {
772
+ id: `technique-${index}`,
773
+ title: text(tech.name) ?? text(tech.title) ?? text(tech.id) ?? `Technique ${index + 1}`,
774
+ tag: text(tech.id) ?? text(tech.techniqueId) ?? undefined,
775
+ column: tacticColumn(name, tacticId),
776
+ ...(cves.length
777
+ ? { attachments: { cve: cves.slice(0, 4).map((cve, at) => ({ id: `technique-${index}-cve-${at}`, title: cve })) } }
778
+ : {}),
779
+ };
780
+ }),
781
+ },
782
+ sources: ['mitre-techniques'],
783
+ };
784
+ }
785
+ function funnelLeaks(contents) {
786
+ const data = asRecord(rowOf(contents, 'bootstrap')?.data);
787
+ const coverage = asRecord(data?.objectCoverage);
788
+ const canonical = count(coverage?.canonicalObjectCount);
789
+ const published = count(coverage?.publishedObjectContextCount);
790
+ const both = count(coverage?.bothPlanes);
791
+ const reachOnly = count(coverage?.reachabilityOnly) ?? 0;
792
+ const exploitOnly = count(coverage?.exploitabilityOnly) ?? 0;
793
+ const neither = count(coverage?.neitherPlaneWithExplicitReason);
794
+ if (canonical == null || published == null || both == null) {
795
+ return { needs: ['Object coverage counts on the security map bootstrap.'] };
796
+ }
797
+ const anyPlane = both + reachOnly + exploitOnly;
798
+ return {
799
+ input: {
800
+ title: 'Where objects drop out of the security map',
801
+ subtitle: 'Object coverage from the bootstrap row.',
802
+ entries: [{ id: 'inventory', title: 'Inventory rows', value: count(coverage?.inventoryRowCount) ?? canonical }],
803
+ steps: [
804
+ { id: 'canonical', title: 'Canonical objects', value: canonical },
805
+ { id: 'published', title: 'Published contexts', value: published },
806
+ {
807
+ id: 'any-plane',
808
+ title: 'On at least one plane',
809
+ value: anyPlane,
810
+ attachments: neither != null
811
+ ? { leak: [{ id: 'leak-neither', title: 'On neither plane', subtitle: `${neither} with a reason`, link: { label: `-${neither}` } }] }
812
+ : undefined,
813
+ },
814
+ {
815
+ id: 'both',
816
+ title: 'On both planes',
817
+ value: both,
818
+ kind: 'outcome',
819
+ badge: anyPlane ? `${Math.round((both / anyPlane) * 100)}% of covered` : undefined,
820
+ attachments: reachOnly
821
+ ? { leak: [{ id: 'leak-reach-only', title: 'Reachability only', subtitle: `${reachOnly} objects`, link: { label: `-${reachOnly}` } }] }
822
+ : undefined,
823
+ },
824
+ ],
825
+ },
826
+ sources: ['bootstrap'],
827
+ };
828
+ }
829
+ function relationCounts(contents) {
830
+ const out = new Map();
831
+ for (const row of rowsOf(contents, 'entity-relations')) {
832
+ for (const item of list(row.relations)) {
833
+ const kind = text(asRecord(item)?.peerKind) ?? 'other';
834
+ out.set(kind, (out.get(kind) ?? 0) + 1);
835
+ }
836
+ }
837
+ return out;
838
+ }
839
+ function relationText(counts) {
840
+ return [...counts.entries()].map(([kind, n]) => `${n} ${kind}${n === 1 ? '' : 's'}`).join(', ');
841
+ }
842
+ function predictionSource(row, fallbackTitle) {
843
+ const steps = predictionStepsFrom(row);
844
+ const summary = text(row.summary) ?? texts(row.summary).join('\n\n');
845
+ return {
846
+ title: text(row.title) ?? fallbackTitle,
847
+ summary: summary || null,
848
+ decision: text(row.decision),
849
+ status: text(row.status),
850
+ score: count(row.score),
851
+ asOf: text(row.asOf),
852
+ support: text(row.support),
853
+ assumedTransitionsAreEdges: row.assumedTransitionsAreEdges === false ? false : row.assumedTransitionsAreEdges === true ? true : null,
854
+ gaps: texts(row.gaps),
855
+ reasons: texts(row.reasons),
856
+ evidence: [...texts(row.evidence), ...list(row.evidence).flatMap((item) => {
857
+ const id = text(asRecord(item)?.evidenceId);
858
+ return id ? [id] : [];
859
+ })],
860
+ assetRefs: texts(row.assetRefs),
861
+ steps,
862
+ };
863
+ }
864
+ function attackPath(contents, objectType) {
865
+ const predicted = rowOf(contents, 'predicted-attack');
866
+ if (!predicted)
867
+ return { needs: [`A predicted-attack row for this ${objectType}.`] };
868
+ const name = subjectName(contents, `This ${objectType}`);
869
+ const diagram = predictionDiagram(predictionSource(predicted, `Predicted attack on ${name}`));
870
+ if (diagram)
871
+ return { input: diagram.input, sources: ['predicted-attack'], drawTemplate: diagram.template };
872
+ const reasons = texts(predicted.reasons).map((reason) => human(reason) ?? reason);
873
+ return {
874
+ needs: [
875
+ `The stored prediction has support ${(human(predicted.support) ?? 'unknown').toLowerCase()}${reasons.length ? `: ${reasons.join(', ').toLowerCase()}` : ''}.`,
876
+ 'The installed Storyflowix build has no predicted-attack template, so this row cannot be drawn yet.',
877
+ ],
878
+ };
879
+ }
880
+ const BUILDERS = {
881
+ 'asset-situation': assetSituation,
882
+ 'asset-findings': assetFindings,
883
+ 'route-evidence': routeEvidence,
884
+ 'response-work': responseWork,
885
+ 'evidence-gaps': evidenceGaps,
886
+ 'alert-activity': alertActivity,
887
+ 'identity-gap': identityGap,
888
+ 'technique-set': techniqueSet,
889
+ 'funnel-leaks': funnelLeaks,
890
+ 'attack-path': attackPath,
891
+ 'device-policy': (contents) => {
892
+ const counts = relationCounts(contents);
893
+ return {
894
+ needs: [
895
+ 'Firewall rules or flows for this device, each with ingress, policy, egress and verdict.',
896
+ counts.size ? `Stored relations link it to ${relationText(counts)}, with no rule rows.` : 'No relations are stored for it.',
897
+ ],
898
+ };
899
+ },
900
+ 'zone-placement': (contents) => {
901
+ const counts = relationCounts(contents);
902
+ return {
903
+ needs: [
904
+ 'The assets placed in this zone, each with identity, placement, exposure and decision.',
905
+ counts.size ? `Stored relations link it to ${relationText(counts)}, not to assets.` : 'No relations are stored for it.',
906
+ ],
907
+ };
908
+ },
909
+ 'vuln-spread': () => ({ needs: ['The assets this vulnerability matches, with a component match per asset.'] }),
910
+ };
911
+ /**
912
+ * Draw `template` from stored rows. A drawing Storyflowix rejects is reported as missing.
913
+ * Templates with no builder are missing too: only the agent's function or a stored drawing can fill them.
914
+ */
915
+ export function buildLocalStory(template, objectType, contents) {
916
+ const build = BUILDERS[template];
917
+ if (!build)
918
+ return { status: 'missing', needs: ['The agent\u2019s story function or a stored drawing. The app does not draw this story itself.'] };
919
+ const outcome = build(contents, objectType);
920
+ if ('needs' in outcome)
921
+ return { status: 'missing', needs: outcome.needs };
922
+ const drawTemplate = outcome.drawTemplate ?? template;
923
+ const definition = getTemplate(drawTemplate);
924
+ if (definition) {
925
+ const check = validateInput(definition, outcome.input);
926
+ if (!check.ok) {
927
+ return { status: 'missing', needs: check.errors.map((error) => `Drawing rejected: ${error.message}`) };
928
+ }
929
+ }
930
+ return {
931
+ status: 'built',
932
+ input: outcome.input,
933
+ sources: outcome.sources,
934
+ gaps: outcome.gaps ?? [],
935
+ drawTemplate: outcome.drawTemplate,
936
+ };
937
+ }
938
+ function blockData(value) {
939
+ const rec = asRecord(value);
940
+ return asRecord(asRecord(rec?.json)?.data) ?? asRecord(rec?.data);
941
+ }
942
+ const contentCache = new Map();
943
+ /**
944
+ * Every stored content row on one record, by content type.
945
+ * One `listRecords` read per record, with `representation: 'both'`.
946
+ */
947
+ export function loadRecordContents(client, objectType, recordId) {
948
+ const type = objectType.trim().toLowerCase();
949
+ const id = recordId.trim();
950
+ const key = `${type}\u0000${id}`;
951
+ const hit = contentCache.get(key);
952
+ if (hit)
953
+ return hit;
954
+ const request = client
955
+ .listRecords(type, { filter: { recordId: id }, limit: 1, representation: 'both' })
956
+ .then((body) => {
957
+ const top = asRecord(body);
958
+ const item = Array.isArray(top?.items) ? asRecord(top.items[0]) : null;
959
+ const contents = asRecord(item?.contents) ?? asRecord(top?.contents) ?? {};
960
+ const out = new Map();
961
+ for (const [contentType, raw] of Object.entries(contents)) {
962
+ const blocks = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
963
+ const rows = blocks.flatMap((block) => {
964
+ const data = blockData(block);
965
+ return data ? [data] : [];
966
+ });
967
+ if (rows.length)
968
+ out.set(contentType, rows);
969
+ }
970
+ return out;
971
+ });
972
+ const settled = request.catch((err) => {
973
+ if (contentCache.get(key) === settled)
974
+ contentCache.delete(key);
975
+ throw err;
976
+ });
977
+ contentCache.set(key, settled);
978
+ return settled;
979
+ }
980
+ //# sourceMappingURL=localStories.js.map