@slicedearth/whoisleuth-cli 2.2.0 → 2.3.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 (77) hide show
  1. package/README.md +2 -1
  2. package/cli/arguments.mjs +389 -1745
  3. package/cli/artifact-validation/case-response.mjs +55 -30
  4. package/cli/artifact-validation/signed-review.mjs +30 -12
  5. package/cli/bulk-checkpoint.mjs +34 -3
  6. package/cli/case-pack.mjs +39 -92
  7. package/cli/command-argument-grammar.mjs +211 -0
  8. package/cli/command-reference.mjs +200 -213
  9. package/cli/completion.mjs +5 -1
  10. package/cli/discovery-observation-snapshot.mjs +16 -3
  11. package/cli/discovery-scan-command-runner.mjs +19 -0
  12. package/cli/discovery-scan.mjs +6 -0
  13. package/cli/domain-control-observations.mjs +1 -1
  14. package/cli/inline-command-families.mjs +1 -0
  15. package/cli/input.mjs +116 -0
  16. package/cli/interchange-report.mjs +14 -0
  17. package/cli/lookalike-calibration.mjs +7 -2
  18. package/cli/lookup-brief.mjs +4 -2
  19. package/cli/lookup-browser-view.mjs +1 -1
  20. package/cli/mail-header-review.mjs +467 -0
  21. package/cli/review-command-runner.mjs +25 -0
  22. package/cli/risk-calibration.mjs +19 -17
  23. package/cli/runner.mjs +7 -1
  24. package/docs/cli-reference.md +8 -1
  25. package/docs/cli.md +12 -1
  26. package/lib/bounded-json.mjs +9 -6
  27. package/lib/dns-convergence-review.mjs +1 -0
  28. package/lib/dns-intelligence.mjs +37 -3
  29. package/lib/dnssec-chain-validation.mjs +6 -0
  30. package/lib/domain-change-packet.mjs +42 -14
  31. package/lib/domain-control-flight-recorder.mjs +4 -1
  32. package/lib/domain-control-manifest.mjs +16 -16
  33. package/lib/domain-posture-parsers.mjs +114 -3
  34. package/lib/domain-posture.mjs +34 -3
  35. package/lib/evidence-export.mjs +60 -11
  36. package/lib/evidence-report.mjs +2 -2
  37. package/lib/html-signals.mjs +39 -33
  38. package/lib/local-geoip-evidence.mjs +19 -3
  39. package/lib/lookup-response.mjs +5 -4
  40. package/lib/lookup-source-progress.mjs +27 -4
  41. package/lib/nameserver-preflight-review.mjs +1 -0
  42. package/lib/rdap-capabilities.mjs +1 -1
  43. package/lib/registry-insights.mjs +35 -22
  44. package/lib/risk-calibration-summary.mjs +1 -3
  45. package/lib/security-txt.mjs +33 -1
  46. package/lib/static-html-analysis.mjs +69 -13
  47. package/lib/website-technology.mjs +79 -11
  48. package/lib/zone-intent-review.mjs +16 -6
  49. package/package.json +1 -1
  50. package/packages/cases/case-migration-model.mjs +71 -22
  51. package/packages/cases/case-record-contracts.mjs +5 -20
  52. package/packages/cases/case-record-core.mjs +10 -21
  53. package/packages/cases/case-record-decisions.mjs +72 -0
  54. package/packages/cases/case-record-model.mjs +1 -0
  55. package/packages/cases/case-record-operations.mjs +6 -4
  56. package/packages/cases/case-record-projection.mjs +218 -0
  57. package/packages/cases/case-report.mjs +11 -6
  58. package/packages/cases/case-response-model.mjs +43 -6
  59. package/packages/cases/case-response-packet.mjs +78 -27
  60. package/packages/cases/case-storage-model.mjs +6 -2
  61. package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
  62. package/packages/comparison/page-similarity.mjs +3 -1
  63. package/packages/contracts/case-portability.mjs +171 -51
  64. package/packages/contracts/cli-command-semantics.mjs +93 -0
  65. package/packages/contracts/risk-calibration.mjs +15 -10
  66. package/packages/contracts/workspace-portability.mjs +44 -7
  67. package/packages/evidence/domain-control-runtime.mjs +46 -6
  68. package/packages/interchange/external-findings-import.mjs +3 -2
  69. package/packages/investigation/lookup-asset-graph.mjs +35 -27
  70. package/packages/workspace/brand-profile-model.mjs +118 -2
  71. package/packages/workspace/bulk-review-model.mjs +13 -7
  72. package/packages/workspace/bulk-session-model.mjs +5 -4
  73. package/packages/workspace/detection-rule-model.mjs +70 -22
  74. package/packages/workspace/relationship-observation-model.mjs +4 -1
  75. package/packages/workspace/website-snapshot-model.mjs +106 -8
  76. package/packages/workspace/workspace-archive.mjs +2 -0
  77. package/cli/evidence-command-arguments.mjs +0 -156
@@ -266,7 +266,7 @@ fi
266
266
  `;
267
267
  }
268
268
  function fishCompletion() {
269
- const commandLines = CLI_COMMANDS.map((command) => (`complete -c whoisleuth -n '__fish_use_subcommand' -a '${command}' -d '${COMMAND_DESCRIPTIONS[command]}'`));
269
+ const commandLines = CLI_COMMANDS.map((command) => (`__whoisleuth_add_command '${command}' '${COMMAND_DESCRIPTIONS[command]}'`));
270
270
  const optionGroups = new Map();
271
271
  for (const definition of CLI_COMMAND_REGISTRY) {
272
272
  for (const option of definition.grammar.options) {
@@ -373,6 +373,9 @@ function __whoisleuth_command_is
373
373
  test (count \$words) -ge 2; or return 1
374
374
  contains -- \$words[2] $argv
375
375
  end
376
+ function __whoisleuth_add_command
377
+ complete -c whoisleuth -n '__fish_use_subcommand' -a \$argv[1] -d \$argv[2]
378
+ end
376
379
  function __whoisleuth_integer_values
377
380
  set -l value $argv[1]
378
381
  while test $value -le $argv[2]
@@ -434,6 +437,7 @@ end
434
437
  complete -c whoisleuth -f
435
438
  ${[HELP_META_ACTION, VERSION_META_ACTION].map((action) => (`complete -c whoisleuth -n '__fish_use_subcommand' -s ${shortMetaAlias(action).slice(1)} -l ${longMetaAlias(action).slice(2)}`)).join('\n')}
436
439
  ${commandLines.join('\n')}
440
+ functions -e __whoisleuth_add_command
437
441
  ${positionalEnumLines.join('\n')}
438
442
  complete -c whoisleuth -n '__whoisleuth_file_position' -F
439
443
  ${optionLines.join('\n')}
@@ -84,7 +84,11 @@ function normalizeObservation(value) {
84
84
  return null;
85
85
  const latestRegistrationState = item.latestRegistrationState;
86
86
  const latestDnsState = item.latestDnsState;
87
+ const collectionOrigin = item.collectionOrigin === undefined
88
+ ? 'current_run'
89
+ : item.collectionOrigin;
87
90
  if (!['observed', 'unavailable'].includes(String(latestRegistrationState))
91
+ || !['current_run', 'resumed_checkpoint'].includes(String(collectionOrigin))
88
92
  || typeof latestDnsState !== 'string' || !latestDnsState || latestDnsState.length > 40
89
93
  || /[\u0000-\u001f\u007f]/u.test(latestDnsState))
90
94
  return null;
@@ -96,6 +100,7 @@ function normalizeObservation(value) {
96
100
  registrationObservedAt,
97
101
  dnsObservedAt,
98
102
  latestAttemptAt: normalizedTimestamp(item.latestAttemptAt, 'Discovery latest-attempt time') ?? '',
103
+ collectionOrigin: collectionOrigin,
99
104
  latestAttemptState: item.latestAttemptState,
100
105
  latestRegistrationState: latestRegistrationState,
101
106
  latestDnsState,
@@ -167,6 +172,13 @@ async function readExisting(path) {
167
172
  }
168
173
  }
169
174
  function currentAttempt(domain, item, previous, generatedAt, deep) {
175
+ const suppliedObservedAt = item.observedAt === null || item.observedAt === undefined
176
+ ? null
177
+ : normalizedTimestamp(item.observedAt, 'Discovery checkpoint observation time');
178
+ if (item.collectionOrigin === 'resumed_checkpoint' && !suppliedObservedAt) {
179
+ throw new CliUsageError('Discovery resume requires a valid observation time for retained checkpoint evidence.');
180
+ }
181
+ const attemptAt = suppliedObservedAt ?? generatedAt;
170
182
  const availability = item.ok ? record(record(item.result).availability) : {};
171
183
  const currentAvailabilityState = availabilityState(item) || 'unknown';
172
184
  const registrationObserved = item.ok && currentAvailabilityState !== 'unknown';
@@ -177,9 +189,9 @@ function currentAttempt(domain, item, previous, generatedAt, deep) {
177
189
  const dns = dnsComplete || (dnsRetainable && !previousDnsRetainable)
178
190
  ? currentDns
179
191
  : previousDnsRetainable ? previous.dns : currentDns;
180
- const registrationObservedAt = registrationObserved ? generatedAt : previous?.registrationObservedAt ?? null;
192
+ const registrationObservedAt = registrationObserved ? attemptAt : previous?.registrationObservedAt ?? null;
181
193
  const dnsObservedAt = dnsComplete || (dnsRetainable && !previousDnsRetainable)
182
- ? generatedAt
194
+ ? attemptAt
183
195
  : previous?.dnsObservedAt ?? null;
184
196
  const unavailableComponents = [];
185
197
  if (!registrationObserved)
@@ -204,7 +216,8 @@ function currentAttempt(domain, item, previous, generatedAt, deep) {
204
216
  observedAt: latestObservedAt(registrationObservedAt, dnsObservedAt),
205
217
  registrationObservedAt,
206
218
  dnsObservedAt,
207
- latestAttemptAt: generatedAt,
219
+ latestAttemptAt: attemptAt,
220
+ collectionOrigin: item.collectionOrigin ?? 'current_run',
208
221
  latestAttemptState: attemptState,
209
222
  latestRegistrationState: registrationObserved ? 'observed' : 'unavailable',
210
223
  latestDnsState: deep ? currentDns.status : 'not_requested',
@@ -62,6 +62,9 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
62
62
  const classify = dependencies.classifyQuery || classifyQuery;
63
63
  const allowlist = await readAllowlist(args.allowlistSource, dependencies, context, classify);
64
64
  const checkpointWriter = dependencies.createBulkCheckpointWriter || createBulkCheckpointWriter;
65
+ const collectionContext = resolverServers.length
66
+ ? { dnsResolver: 'analyst_selected', resolverServers: [...resolverServers] }
67
+ : { dnsResolver: 'system_default', resolverServers: [] };
65
68
  const checkpoint = args.checkpoint
66
69
  ? await checkpointWriter({
67
70
  path: args.checkpoint,
@@ -72,6 +75,20 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
72
75
  ...(dependencies.now ? { now: dependencies.now } : {}),
73
76
  })
74
77
  : null;
78
+ for (const item of checkpoint?.initialResults ?? []) {
79
+ const retainedContext = item.collectionContext;
80
+ const contextMatches = retainedContext
81
+ ? retainedContext.dnsResolver === collectionContext.dnsResolver
82
+ && retainedContext.resolverServers.length === collectionContext.resolverServers.length
83
+ && retainedContext.resolverServers.every((server, index) => server === collectionContext.resolverServers[index])
84
+ : collectionContext.dnsResolver === 'system_default';
85
+ if (!contextMatches) {
86
+ throw new CliUsageError('Bulk checkpoint collection context does not match the current DNS resolver selection.');
87
+ }
88
+ if (item.observedAt === null || item.observedAt === undefined) {
89
+ throw new CliUsageError('Discovery resume requires an observation time for every retained checkpoint result.');
90
+ }
91
+ }
75
92
  const indicator = context.beginProgress(`Collecting 0 of ${queries.length} generated candidates`);
76
93
  let completed = checkpoint?.initialResults.length || 0;
77
94
  const resumedItems = new Map((checkpoint?.initialResults ?? []).map((item) => [item.index, item]));
@@ -95,6 +112,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
95
112
  ...item,
96
113
  observedAt: context.now(),
97
114
  collectionOrigin: 'current_run',
115
+ collectionContext,
98
116
  };
99
117
  settledItems.set(item.index, observed);
100
118
  completed += 1;
@@ -107,6 +125,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
107
125
  ...item,
108
126
  observedAt: null,
109
127
  collectionOrigin: 'current_run',
128
+ collectionContext,
110
129
  });
111
130
  }
112
131
  finally {
@@ -125,6 +125,12 @@ function buildScanItem(candidate, item, allowlist, relationshipIds = []) {
125
125
  dnsSummary: bulkDnsSummary(item),
126
126
  review: { lane: review.lane, reasons: review.reasons },
127
127
  relationshipIds: [...relationshipIds],
128
+ observedAt: item.observedAt ?? null,
129
+ collectionOrigin: item.collectionOrigin ?? 'current_run',
130
+ collectionContext: item.collectionContext ?? {
131
+ dnsResolver: 'system_default',
132
+ resolverServers: [],
133
+ },
128
134
  ...(item.ok ? {
129
135
  availability: result.availability,
130
136
  diagnostics: result.diagnostics,
@@ -115,7 +115,7 @@ export function domainControlObservationFromSavedLookup(document) {
115
115
  field('registry_nameservers', 'Registry RDAP', rdapState, list(rdapParsed.nameservers, hostname)),
116
116
  field('whois_nameservers', 'WHOIS', diagnostic(document, 'whois'), list(record(record(document.whois).parsed).nameservers, hostname)),
117
117
  field('delegated_nameservers', 'DNS', dnsState, list(dnsRecords.ns, hostname)),
118
- field('delegation_ds', 'DNS delegation', state(delegation.status ?? dns.status), list(delegationRecords.ds ?? rdapParsed.dsData, dsValue)),
118
+ field('delegation_ds', 'DNS delegation', state(delegation.status), list(delegationRecords.ds, dsValue)),
119
119
  field('mail_exchangers', 'DNS', dnsState, list(dnsRecords.mx, mxValue)),
120
120
  field('caa_policy', 'DNS', dnsState, list(record(dns.caaPolicy).records ?? dnsRecords.caa, caaValue)),
121
121
  field('tls_certificate', 'TLS', tlsState, list([certificate.fingerprintSha256 ?? tls.fingerprintSha256], (item) => text(item, 128))),
@@ -17,6 +17,7 @@ const REVIEW_INLINE_COMMANDS = Object.freeze([
17
17
  'compare',
18
18
  'page-compare',
19
19
  'mail-review',
20
+ 'mail-headers',
20
21
  'review-evidence',
21
22
  'brief',
22
23
  'case-pack',
package/cli/input.mjs CHANGED
@@ -1,6 +1,46 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { constants } from 'node:fs';
3
+ import { open } from 'node:fs/promises';
2
4
  import { decodeBoundedUtf8, readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
3
5
  import { CliUsageError } from "./errors.mjs";
6
+ function headerEnd(bytes) {
7
+ for (let index = 0; index < bytes.length - 1; index += 1) {
8
+ if (bytes[index] === 0x0a && bytes[index + 1] === 0x0a)
9
+ return index;
10
+ if (index < bytes.length - 3
11
+ && bytes[index] === 0x0d && bytes[index + 1] === 0x0a
12
+ && bytes[index + 2] === 0x0d && bytes[index + 3] === 0x0a)
13
+ return index;
14
+ }
15
+ return -1;
16
+ }
17
+ function decodeHeaderStorage(storage, length, maximumBytes, label) {
18
+ const end = headerEnd(storage.subarray(0, length));
19
+ if ((end < 0 && length > maximumBytes) || end > maximumBytes) {
20
+ throw new CliUsageError(`${label} is limited to ${maximumBytes} bytes before the message body.`);
21
+ }
22
+ return decodeBoundedUtf8(storage.subarray(0, end < 0 ? length : end), label);
23
+ }
24
+ function appendUntilHeaderEnd(storage, offset, input) {
25
+ let nextOffset = offset;
26
+ for (const byte of input) {
27
+ if (nextOffset >= storage.length)
28
+ return Object.freeze({ offset: nextOffset, complete: false });
29
+ storage[nextOffset] = byte;
30
+ nextOffset += 1;
31
+ const lfBoundary = nextOffset >= 2
32
+ && storage[nextOffset - 2] === 0x0a
33
+ && storage[nextOffset - 1] === 0x0a;
34
+ const crlfBoundary = nextOffset >= 4
35
+ && storage[nextOffset - 4] === 0x0d
36
+ && storage[nextOffset - 3] === 0x0a
37
+ && storage[nextOffset - 2] === 0x0d
38
+ && storage[nextOffset - 1] === 0x0a;
39
+ if (lfBoundary || crlfBoundary)
40
+ return Object.freeze({ offset: nextOffset, complete: true });
41
+ }
42
+ return Object.freeze({ offset: nextOffset, complete: false });
43
+ }
4
44
  function abortError(signal) {
5
45
  return signal.reason instanceof Error
6
46
  ? signal.reason
@@ -60,3 +100,79 @@ export async function readCliTextInput(source, stdin, options) {
60
100
  options.signal?.removeEventListener('abort', onAbort);
61
101
  }
62
102
  }
103
+ /**
104
+ * Read only the RFC-style header block from a selected regular file or stdin.
105
+ * The fixed retained allocation includes delimiter look-ahead but accumulates
106
+ * only bytes through the first header/body boundary. A regular file may be
107
+ * larger than the header limit while parsing stays tied to the admitted prefix.
108
+ */
109
+ export async function readCliHeaderInput(source, stdin, options) {
110
+ if (!Number.isSafeInteger(options.maximumBytes) || options.maximumBytes < 1 || options.maximumBytes > 1024 * 1024) {
111
+ throw new TypeError('Header input has an invalid byte boundary.');
112
+ }
113
+ if (options.signal?.aborted)
114
+ throw abortError(options.signal);
115
+ const storage = Buffer.allocUnsafe(options.maximumBytes + 4);
116
+ if (source) {
117
+ let handle;
118
+ try {
119
+ handle = await open(source, constants.O_RDONLY | constants.O_NONBLOCK);
120
+ const before = await handle.stat();
121
+ if (!before.isFile())
122
+ throw new CliUsageError(`${options.label} must be a regular file.`);
123
+ let offset = 0;
124
+ let complete = false;
125
+ const chunk = Buffer.allocUnsafe(Math.min(4 * 1024, storage.length));
126
+ while (offset < storage.length && !complete) {
127
+ if (options.signal?.aborted)
128
+ throw abortError(options.signal);
129
+ const result = await handle.read(chunk, 0, Math.min(chunk.length, storage.length - offset), null);
130
+ if (result.bytesRead === 0)
131
+ break;
132
+ ({ offset, complete } = appendUntilHeaderEnd(storage, offset, chunk.subarray(0, result.bytesRead)));
133
+ }
134
+ chunk.fill(0);
135
+ const after = await handle.stat();
136
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
137
+ throw new CliUsageError(`${options.label} changed while it was being read.`);
138
+ }
139
+ return decodeHeaderStorage(storage, offset, options.maximumBytes, options.label);
140
+ }
141
+ catch (error) {
142
+ if (error instanceof CliUsageError || options.signal?.aborted)
143
+ throw error;
144
+ const message = error instanceof Error ? error.message : `${options.label} could not be read.`;
145
+ throw new CliUsageError(message);
146
+ }
147
+ finally {
148
+ await handle?.close();
149
+ }
150
+ }
151
+ if (!stdin || stdin.isTTY)
152
+ return '';
153
+ let offset = 0;
154
+ const onAbort = () => {
155
+ if ('destroy' in stdin && typeof stdin.destroy === 'function')
156
+ stdin.destroy(abortError(options.signal));
157
+ };
158
+ options.signal?.addEventListener('abort', onAbort, { once: true });
159
+ try {
160
+ for await (const chunk of stdin) {
161
+ if (options.signal?.aborted)
162
+ throw abortError(options.signal);
163
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
164
+ const retained = appendUntilHeaderEnd(storage, offset, bytes);
165
+ offset = retained.offset;
166
+ if (retained.complete)
167
+ break;
168
+ if (offset >= storage.length)
169
+ break;
170
+ }
171
+ if (options.signal?.aborted)
172
+ throw abortError(options.signal);
173
+ return decodeHeaderStorage(storage, offset, options.maximumBytes, options.label);
174
+ }
175
+ finally {
176
+ options.signal?.removeEventListener('abort', onAbort);
177
+ }
178
+ }
@@ -198,6 +198,19 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
198
198
  export function formatInterchangeFidelityReport(report) {
199
199
  const browser = report.compatibility.browser;
200
200
  const cli = report.compatibility.cli;
201
+ const nextAction = !report.recognised
202
+ ? 'Choose a supported WHOISleuth export and run verify-artifact before trying to import it.'
203
+ : !report.artifact.versionSupported
204
+ ? 'Use a WHOISleuth release that supports this exact version, or re-export it with its last supported release.'
205
+ : !report.verification.assuranceSatisfied
206
+ ? 'Do not import or share this artefact yet; run verify-artifact and resolve the reported structure or integrity failure.'
207
+ : report.compatibility.fullyImportable === false
208
+ ? 'Review unsupported, blocked, skipped or pruned records before selecting any import.'
209
+ : browser?.import === 'supported'
210
+ ? 'Open the browser import preview and review the proposed additions and updates before applying them.'
211
+ : cli?.read === 'supported'
212
+ ? 'Use the declared offline CLI reader; browser import is not supported for this artefact.'
213
+ : 'Retain the verification result and use only the operations declared above.';
201
214
  return [
202
215
  'Interchange fidelity report',
203
216
  `Recognised ${report.recognised ? 'yes' : 'no'}`,
@@ -219,6 +232,7 @@ export function formatInterchangeFidelityReport(report) {
219
232
  '',
220
233
  `Preserved ${report.compatibility.preservedFieldGroups.join(', ') || 'none declared'}`,
221
234
  `Excluded ${report.compatibility.excludedFieldGroups.join(', ') || 'none declared'}`,
235
+ `Next action ${nextAction}`,
222
236
  '',
223
237
  ...report.limitations.map((item) => `Limitation: ${item}`),
224
238
  '',
@@ -6,7 +6,9 @@ const LOOKALIKE_CALIBRATION_VERSION = 1;
6
6
  const MAX_LOOKALIKE_CALIBRATION_BYTES = 2 * 1024 * 1024;
7
7
  const MAX_LOOKALIKE_CALIBRATION_RECORDS = 5_000;
8
8
  const MIN_LOOKALIKE_CALIBRATION_SAMPLE = 20;
9
- const DISPOSITIONS = ['suspicious', 'confirmed_abuse', 'false_positive', 'expected', 'closed_no_action'];
9
+ // Version 1 intentionally admits reviewed Case dispositions only. This local
10
+ // schema remains an independent compatibility decision from the live Case model.
11
+ const DISPOSITIONS = Object.freeze(['suspicious', 'confirmed_abuse', 'false_positive', 'expected', 'closed_no_action']);
10
12
  function record(value) {
11
13
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
12
14
  }
@@ -22,7 +24,10 @@ function ratio(value, total) {
22
24
  return total ? Number((value / total).toFixed(4)) : 0;
23
25
  }
24
26
  function emptyCounts() {
25
- return { suspicious: 0, confirmed_abuse: 0, false_positive: 0, expected: 0, closed_no_action: 0 };
27
+ const counts = {};
28
+ for (const disposition of DISPOSITIONS)
29
+ counts[disposition] = 0;
30
+ return counts;
26
31
  }
27
32
  function buildLookalikeCalibration(raw, generatedAt = new Date().toISOString()) {
28
33
  if (Buffer.byteLength(raw, 'utf8') > MAX_LOOKALIKE_CALIBRATION_BYTES) {
@@ -111,8 +111,10 @@ export function buildCliLookupBrief(input, generatedAt = new Date().toISOString(
111
111
  const facts = [
112
112
  ['registration-state', 'Registration state', availability.state, availabilitySource.id],
113
113
  ['registrar', 'Registrar', record(preferred.registrar).name ?? preferred.registrar, preferredRegistrationSource.id],
114
- ['created', 'Created', record(preferred.lifecycle).createdIso ?? preferred.createdDateIso, preferredRegistrationSource.id],
115
- ['expires', 'Expires', record(preferred.lifecycle).expiryIso ?? preferred.expiryDateIso, preferredRegistrationSource.id],
114
+ ['created', 'Created', record(preferred.lifecycle).createdDateIso
115
+ ?? record(preferred.lifecycle).createdIso ?? preferred.createdDateIso, preferredRegistrationSource.id],
116
+ ['expires', 'Expires', record(preferred.lifecycle).expiryDateIso
117
+ ?? record(preferred.lifecycle).expiryIso ?? preferred.expiryDateIso, preferredRegistrationSource.id],
116
118
  ['name-servers', 'Name servers', joinedText(preferred.nameservers), preferredRegistrationSource.id],
117
119
  ['website-activity', 'Website activity', availability.activityStatus, httpSource.id],
118
120
  ['page-title', 'Page title', availability.pageTitle, httpSource.id],
@@ -2,7 +2,7 @@ import { formatTerminalLookup, safeTerminalValue } from "./formatters/terminal.m
2
2
  import { presentTerminalOutput, wrapTerminalOutput, } from "./terminal-presentation.mjs";
3
3
  const MIN_LOOKUP_BROWSER_HEIGHT = 12;
4
4
  const MAX_LOOKUP_BROWSER_HEIGHT = 80;
5
- const MAX_LOOKUP_BROWSER_PANELS = 9;
5
+ const MAX_LOOKUP_BROWSER_PANELS = 16;
6
6
  const MAX_LOOKUP_BROWSER_SEARCH_BYTES = 256;
7
7
  const MAX_LOOKUP_BROWSER_SEARCH_SCALARS = 80;
8
8
  const MAX_LOOKUP_BROWSER_SEARCH_MATCHES = 256;