clearotron 0.2.2 → 0.2.3
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/.env.example +28 -0
- package/.nvmrc +1 -1
- package/INSTALL.md +13 -11
- package/README.md +11 -16
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +4 -0
- package/bin/onboard.mjs +195 -19
- package/bin/start.mjs +154 -8
- package/build-info.json +2 -2
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +5 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +14 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/driver/CHANGELOG.md +56 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/dev-portal.mjs +2 -2
- package/driver/driver.config.mjs +1 -1
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +18 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +5 -5
- package/driver/plain-register.mjs +120 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +40 -1
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +26 -7
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +121 -1
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +2 -2
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +2 -2
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stages.mjs +4 -4
- package/driver/suite-census.json +134 -38
- package/driver/unit-inventory.mjs +184 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +2 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +28 -10
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-CEYvXdqH.js} +226 -49
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/live-surface-check.mjs +38 -1
- package/scripts/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- package/scripts/strip-tracker-citations.mjs +29 -6
- package/scripts/test-run.mjs +16 -0
- package/shared/anon-overlay.mjs +3 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/invocation.mjs +28 -0
- package/shared/names-in-force.mjs +2 -0
- package/shared/node-floor.mjs +97 -0
- package/shared/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
package/.env.example
CHANGED
|
@@ -508,3 +508,31 @@ CLEAROTRON_REQUESTER_WHATSAPP=
|
|
|
508
508
|
# requester notice reaches. Read by driver/stages.mjs (whatsappRouting).
|
|
509
509
|
# effect: deployment
|
|
510
510
|
CLEAROTRON_WHATSAPP_OPERATOR_COPY=
|
|
511
|
+
|
|
512
|
+
# ── The two switches that reveal accounts an install deliberately does not offer ──────────────────
|
|
513
|
+
#
|
|
514
|
+
# NEITHER IS FOR AN OPERATOR. Both are set by the product's own commands, and a row here exists because
|
|
515
|
+
# product code reads them, not because copying this file should set them. Leave both unset. Setting
|
|
516
|
+
# either on a real deployment puts accounts in a picker that a clean install is meant not to have.
|
|
517
|
+
#
|
|
518
|
+
# `1` makes the profile loader return the three test-suite fixture accounts in the roster
|
|
519
|
+
# it resolves. They are marked `testFixture` and are refused otherwise on every install route, because a
|
|
520
|
+
# packaging exclusion protects only the tarball and `git clone && npm install` is a documented way in.
|
|
521
|
+
# Set by scripts/test-run.mjs, once, for every child — most checks that need a fixture do not call the
|
|
522
|
+
# loader directly, so there is no argument to pass. An explicit `includeTestFixtures` argument beats
|
|
523
|
+
# this, distinguished on `undefined`, so a check asking what a customer's install resolves is never
|
|
524
|
+
# handed the suite's answer instead. Read by driver/profiles.mjs.
|
|
525
|
+
# effect: harness
|
|
526
|
+
CLEAROTRON_TEST_FIXTURE_PROFILES=
|
|
527
|
+
|
|
528
|
+
# `1` makes the profile loader return the BUNDLED demo account (demo-brand-owner, marked `demoData`).
|
|
529
|
+
# A fresh install resolves `generic` alone — owner ruling 2026-09-08, so nobody has to clean demo
|
|
530
|
+
# material out of an environment they just made — and the demo brings its own account when it runs.
|
|
531
|
+
# Set by `clearotron demo`, by `clearotron start --demo`, and by scripts/test-run.mjs.
|
|
532
|
+
#
|
|
533
|
+
# The gate is on the LAYER, not the flag: a `demoData` account in a deployment's OWN configured store is
|
|
534
|
+
# that deployment's choice and stays offered whatever this is set to, and the admission wall still
|
|
535
|
+
# refuses a real clearance under it. An explicit `includeDemo` argument beats this, on the same rule.
|
|
536
|
+
# Read by driver/profiles.mjs.
|
|
537
|
+
# effect: harness
|
|
538
|
+
CLEAROTRON_DEMO_PROFILES=
|
package/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
22
|
|
1
|
+
22.13
|
package/INSTALL.md
CHANGED
|
@@ -38,13 +38,15 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
|
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Skip this on any machine that already builds software.
|
|
41
|
-
- **Node.js >= 22**, and npm. A hard floor
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
- **Node.js >= 22.13**, and npm. A hard floor, and the minor matters: the free US register runs on
|
|
42
|
+
`node:sqlite`, which is not a built-in module before 22.13 — measured across releases, 2026-09-08. On
|
|
43
|
+
Node 20, or on 22.0 through 22.12, the install succeeds and the first US search fails with
|
|
44
|
+
`ERR_UNKNOWN_BUILTIN_MODULE`, saying nothing about Node. `package.json` declares the floor, the
|
|
45
|
+
install refuses below it before writing anything, and `nvm use` picks the pin up.
|
|
46
|
+
- **macOS, Linux, or native Windows for the demo; WSL2 for a clearance.** `npx clearotron
|
|
45
47
|
demo` runs anywhere Node does, native Windows included. A real clearance does not: the engine resolves
|
|
46
48
|
the reasoning CLI the POSIX way, so a native-Windows clearance refuses at preflight even with the CLI
|
|
47
|
-
on `PATH`. On Windows, `wsl --install`, then install Node 22 **inside** the Linux distribution and work
|
|
49
|
+
on `PATH`. On Windows, `wsl --install`, then install Node 22.13 or newer **inside** the Linux distribution and work
|
|
48
50
|
through this page from there. A *hosted*
|
|
49
51
|
deployment needs Linux for one further thing, the systemd outbox trigger —
|
|
50
52
|
[driver/systemd/README.md](driver/systemd/README.md).
|
|
@@ -459,9 +461,9 @@ matches, the neutral Generic default applies.
|
|
|
459
461
|
|
|
460
462
|
- **Bundled with the package** (`driver/profiles/`): `generic.json` (the Generic default) and
|
|
461
463
|
`demo-brand-owner.json`, the account the demo runs as, so you can run and read the machinery
|
|
462
|
-
immediately. `driver/profiles/README.md` documents every field. (A clone
|
|
463
|
-
|
|
464
|
-
|
|
464
|
+
immediately. `driver/profiles/README.md` documents every field. (A clone of the repository carries
|
|
465
|
+
three more, marked `testFixture` in their own files: the test suite reads them, no install offers
|
|
466
|
+
them, and they are excluded from the published package as well.)
|
|
465
467
|
- **Your real customers live outside the repo.** Point `CLEAROTRON_CUSTOMERS_DIR` at your own private
|
|
466
468
|
config store and the engine loads *those* accounts instead. **Same engine, different config path** —
|
|
467
469
|
the code carries no customer identities.
|
|
@@ -480,8 +482,8 @@ working examples in `driver/profiles/`:
|
|
|
480
482
|
the engine attaches to the profile it loads. One ships beside a bundled demo customer.
|
|
481
483
|
- **Project overlays** — `projects/<customer-key>/<slug>.json`. A project is one engagement under a
|
|
482
484
|
customer: a launch screening, a flagship clearance, a regional push. Each may carry its own
|
|
483
|
-
`<slug>.context.md` beside it. `projects/
|
|
484
|
-
|
|
485
|
+
`<slug>.context.md` beside it. `projects/demo-brand-owner/japan-and-korea-app-launch.json` is the
|
|
486
|
+
shipped example.
|
|
485
487
|
|
|
486
488
|
### What a project may and may not change
|
|
487
489
|
|
|
@@ -536,7 +538,7 @@ Copy or author the customers, context packs and project overlays you want; assum
|
|
|
536
538
|
"forwarder": "alex",
|
|
537
539
|
"forwarderEmail": "alex@example.com",
|
|
538
540
|
"forwarderDomain": "example.com",
|
|
539
|
-
"marks": [{ "ref": "TM-0001", "name": "
|
|
541
|
+
"marks": [{ "ref": "TM-0001", "name": "IRONWHISK", "classes": [9, 42] }],
|
|
540
542
|
"goods": "cloud software for weather analytics",
|
|
541
543
|
"jurisdictions": ["US"],
|
|
542
544
|
"product": "knockout-search"
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="LICENSE"><img src="https://img.shields.io/badge/licence-AGPL--3.0--only-860F09?style=flat-square" alt="Licence: AGPL-3.0-only"></a>
|
|
10
|
-
<a href=".nvmrc"><img src="https://img.shields.io/badge/node-%E2%89%A5%2022-250902?style=flat-square" alt="Node 22+"></a>
|
|
10
|
+
<a href=".nvmrc"><img src="https://img.shields.io/badge/node-%E2%89%A5%2022.13-250902?style=flat-square" alt="Node 22.13+"></a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
Give it a mark, its classes and a territory. Clearotron searches the trademark registers and the open
|
|
@@ -35,8 +35,9 @@ calls to us.
|
|
|
35
35
|
npm install -g clearotron
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Node 22 or newer, on macOS
|
|
39
|
-
|
|
38
|
+
Node 22.13 or newer, on macOS or Linux. That puts `clearotron` on your `PATH`; every command below
|
|
39
|
+
works in that short form. **On Windows the demo above runs natively; a real clearance needs WSL2** —
|
|
40
|
+
the engine resolves the reasoning CLI the POSIX way and refuses at preflight otherwise.
|
|
40
41
|
|
|
41
42
|
That command gives you the **stable** release — the one that has run a real clearance end to end before it
|
|
42
43
|
was published. If you want the newest code instead, a beta is published whenever there is something worth
|
|
@@ -49,17 +50,6 @@ npm install -g clearotron@beta
|
|
|
49
50
|
What each channel promises, and when a stable is cut: [docs/RELEASES.md](docs/RELEASES.md). If you are not
|
|
50
51
|
sure, the first command is the one you want.
|
|
51
52
|
|
|
52
|
-
**Or run it from source**, which is what you want if you intend to change it:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
git clone https://github.com/CordilleraSarl/Clearotron
|
|
56
|
-
cd clearotron
|
|
57
|
-
npm install # every workspace
|
|
58
|
-
npm run build -w portal-ui # the browser bundle is not committed — build it once
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
From a clone the commands are `npx clearotron …`, run from that directory.
|
|
62
|
-
|
|
63
53
|
## Quick start
|
|
64
54
|
|
|
65
55
|
Check the install before it does anything. `doctor` only reads — it writes nothing, calls nobody, and
|
|
@@ -135,13 +125,18 @@ credential.
|
|
|
135
125
|
|
|
136
126
|
## Development
|
|
137
127
|
|
|
128
|
+
A clone is the working tree, not a way to install the product — install it from the package above.
|
|
129
|
+
|
|
138
130
|
```bash
|
|
139
131
|
git clone https://github.com/CordilleraSarl/Clearotron
|
|
140
132
|
cd Clearotron
|
|
141
|
-
npm install
|
|
142
|
-
npm
|
|
133
|
+
npm install # every workspace
|
|
134
|
+
npm run build -w portal-ui # the browser bundle is not committed — build it once
|
|
135
|
+
npm test # the offline suite — no credentials, no network
|
|
143
136
|
```
|
|
144
137
|
|
|
138
|
+
From a clone the commands are `npx clearotron …`, run from that directory.
|
|
139
|
+
|
|
145
140
|
`npm test` is the whole verification story for someone with no credentials, and it is the first thing
|
|
146
141
|
[CONTRIBUTING.md](CONTRIBUTING.md) asks of a contributor.
|
|
147
142
|
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -6193,7 +6193,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
|
6193
6193
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
6194
6194
|
```
|
|
6195
6195
|
|
|
6196
|
-
## undici@
|
|
6196
|
+
## undici@7.29.1
|
|
6197
6197
|
|
|
6198
6198
|
- **Licence declared:** `MIT`
|
|
6199
6199
|
- **Repository:** https://github.com/nodejs/undici
|
package/bin/clearotron.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { dirname, join } from "node:path";
|
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { constants as SIG } from "node:os";
|
|
24
24
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
25
|
+
import { nodeFloorVerdict, nodeFloorRefusal } from "../shared/node-floor.mjs"; // — one floor, read from package.json
|
|
25
26
|
import { invocationPrefix } from "../shared/invocation.mjs"; // — print a command the reader can type
|
|
26
27
|
|
|
27
28
|
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -133,6 +134,27 @@ const [verb, ...rest] = process.argv.slice(2);
|
|
|
133
134
|
process.exit(2);
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// — THE RUNTIME FLOOR, REFUSED HERE, AND THE ONE THING THIS DISPATCHER LOOKS AT.
|
|
138
|
+
//
|
|
139
|
+
// This file's header says it implements nothing and does not inspect what it forwards, and that rule
|
|
140
|
+
// is right: a second, weaker copy of a check that already exists is worse than no copy. This is not
|
|
141
|
+
// that. It is not about the verb — it is about whether ANY verb can run, which is prior to dispatch
|
|
142
|
+
// and cannot live inside a child that will not start.
|
|
143
|
+
//
|
|
144
|
+
// It exists because the checks we had were in `doctor` and `setup`, which are things you run AFTER
|
|
145
|
+
// `npm install` succeeds. A reader whose runtime is too old meets a failure inside a command instead,
|
|
146
|
+
// and on one report it was an engine door exiting 1 with its message lost. `npx clearotron demo` and
|
|
147
|
+
// `clearotron start` never reached the sentence written for them.
|
|
148
|
+
//
|
|
149
|
+
// `--version` and `--help` above are DELIBERATELY on the near side of this. They are the two things a
|
|
150
|
+
// person runs while writing a bug report, and refusing them would take away the answer to the first
|
|
151
|
+
// question we ask.
|
|
152
|
+
const floor = nodeFloorVerdict();
|
|
153
|
+
if (!floor.ok) {
|
|
154
|
+
console.error(`clearotron: ${nodeFloorRefusal(floor)}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
// — EVERY VERB ANSWERS --help, INCLUDING THE TWO WHOSE CHILDREN REFUSE IT.
|
|
137
159
|
//
|
|
138
160
|
// `run` and `run-queue` dispatch to driver/pipeline.mjs and driver/runner.mjs, orchestrator entry points
|
package/bin/example.mjs
CHANGED
|
@@ -95,6 +95,10 @@ const die = (...lines) => { console.error(`\n${lines.join("\n")}\n`); process.ex
|
|
|
95
95
|
// already walked a container — every child holding a meta.json and a run/ — and `bin/start.mjs` seeds the
|
|
96
96
|
// portal's archive from it. A bare run directory here would have left that call finding nothing and the
|
|
97
97
|
// installed portal serving an empty archive, silently. Same directory, both readers, no second mechanism.
|
|
98
|
+
// THE PLAYER IS THE DEMO, so it asks for the demo account the same way `start --demo` does. Without
|
|
99
|
+
// this the roster it resolves is a fresh install's — `generic` alone — and the account whose reports
|
|
100
|
+
// it is about would not be there to name.
|
|
101
|
+
process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
|
|
98
102
|
const DEMO_ROOT = join(REPO, "demo");
|
|
99
103
|
// The rule lives in `driver/demo-container.mjs` — ONE definition, because it used to be three and they
|
|
100
104
|
// disagreed. That file records what a knockout demo carries instead of a report.md, and why this line
|
package/bin/onboard.mjs
CHANGED
|
@@ -64,11 +64,16 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
64
64
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, chmodSync } from "node:fs"; // read the process table here; moved that to shared/process-table.mjs
|
|
65
65
|
import { homedir, userInfo } from "node:os";
|
|
66
66
|
import { invocationPrefix } from "../shared/invocation.mjs"; // — one rule for how the reader invokes us
|
|
67
|
+
import { nodeFloorVerdict } from "../shared/node-floor.mjs"; // — the floor is package.json engines, not a constant here
|
|
67
68
|
import { invocationForm } from "../shared/invocation.mjs"; // — and WHY that form
|
|
68
69
|
import { standFrom } from "../shared/invocation.mjs"; // is this tree one npm replaces?
|
|
69
70
|
import { installShim } from "../shared/verb-shim.mjs"; // — the verb goes on PATH
|
|
70
71
|
import { styleFor, banner } from "../shared/tty-style.mjs"; // — weight where the meaning is
|
|
71
|
-
import { bracketAsciiCells } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
|
|
72
|
+
import { bracketAsciiCells, BRAND } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
|
|
73
|
+
// ONE CLASSIFIER, shared with `bin/start.mjs`. The wizard asks the question; the launcher enforces the
|
|
74
|
+
// answer. Two copies of "what does this domain admit" is a wizard that consents to one rule and a
|
|
75
|
+
// launcher that builds another.
|
|
76
|
+
import { classifyStaffDomain, domainOfEmail, staffDomainRefusal, staffGrantSentence } from "../shared/staff-domain.mjs";
|
|
72
77
|
import { join, dirname, isAbsolute, resolve } from "node:path";
|
|
73
78
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
74
79
|
import { delimiter } from "node:path";
|
|
@@ -121,7 +126,6 @@ const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed
|
|
|
121
126
|
// command is applying. Writes stay on ENV_PATH: a writer that followed the file backwards would keep an
|
|
122
127
|
// install in the directory npm replaces forever.
|
|
123
128
|
const READ_ENV_PATH = () => activeEnvPath({ repoRoot: REPO });
|
|
124
|
-
const NODE_FLOOR = 22;
|
|
125
129
|
|
|
126
130
|
const argv = process.argv.slice(2);
|
|
127
131
|
const has = (n) => argv.includes(n);
|
|
@@ -587,6 +591,80 @@ export const AMBIENT_KEYS = [
|
|
|
587
591
|
// ── helpers ──────────────────────────────────────────────────────────────────────────────────────────
|
|
588
592
|
const present = (v) => typeof v === "string" && v.trim() !== "";
|
|
589
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Ask who signs in, and take a real yes for the access rule that address implies.
|
|
596
|
+
*
|
|
597
|
+
* ── WHY THIS QUESTION EXISTS AT ALL ─────────────────────────────────────────────────────────────────
|
|
598
|
+
*
|
|
599
|
+
* Setup never asked for an address. `clearotron start` therefore took one from `--user`, from
|
|
600
|
+
* `PORTAL_LOCAL_USER`, or from the local account as `<account>@localhost` — and derived a STAFF RULE
|
|
601
|
+
* from everything after its `@`. On the local-account default that rule is `localhost`: one machine,
|
|
602
|
+
* one identity, nothing granted to anyone. Given a real address it is that address's whole domain, and
|
|
603
|
+
* the settings page then reports it back as "Anyone at <domain> — a rule, not a person".
|
|
604
|
+
*
|
|
605
|
+
* An outside install reached exactly that state. Nobody typed the address into this wizard, because
|
|
606
|
+
* this wizard had no prompt for it; an assistant filled one into the environment file on the operator's
|
|
607
|
+
* behalf, and the install granted a documentation domain. They read their own settings page and
|
|
608
|
+
* reported it as a back door. They were right to: a grant to a group had been made, by nobody.
|
|
609
|
+
*
|
|
610
|
+
* So the address is asked for here, where a person is definitionally present, and the grant it implies
|
|
611
|
+
* is shown in the words the settings page uses BEFORE it is written. `start` no longer guesses: it
|
|
612
|
+
* refuses a rule wider than one machine unless `PORTAL_STAFF_DOMAINS` says so in writing, which is what
|
|
613
|
+
* a yes here writes.
|
|
614
|
+
*
|
|
615
|
+
* THE DEFAULT IS THE LOCAL ACCOUNT AND NOTHING ELSE — not the git author, not the hostname, not a shell
|
|
616
|
+
* variable. An identity that becomes an access rule comes from the person, or from the one source that
|
|
617
|
+
* cannot name a second person.
|
|
618
|
+
*
|
|
619
|
+
* `io` IS INJECTED for the reason `offerUsptoSync`'s is: the branch that matters is the one where a
|
|
620
|
+
* reader pressed Enter at the grant question and NOTHING must be granted, and closed over a terminal
|
|
621
|
+
* that branch is asserted nowhere. Returns the `.env` keys to write and nothing else.
|
|
622
|
+
*/
|
|
623
|
+
export async function askSignIn(io, { localAccount = "user", staffLabel = "Staff", envPath = "" } = {}) {
|
|
624
|
+
const { askValue, confirm, say = () => {}, ok = () => {}, info = () => {},
|
|
625
|
+
warn = () => {}, problem = () => {} } = io;
|
|
626
|
+
const localDefault = `${localAccount}@localhost`;
|
|
627
|
+
prose("The portal admits one address on this install, and that address is also what decides who is an",
|
|
628
|
+
"administrator. Enter accepts the local-account form, which is this machine and nobody else.");
|
|
629
|
+
for (;;) {
|
|
630
|
+
const typed = String(await askValue("Sign-in address:", { def: localDefault })).trim().toLowerCase();
|
|
631
|
+
if (!typed.includes("@") || typed.indexOf("@") !== typed.lastIndexOf("@")) {
|
|
632
|
+
problem(`"${typed}" is not a single email address. The portal refuses a multi-@ identity outright, `
|
|
633
|
+
+ "so this would sign in and then be denied at the door.");
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const domain = domainOfEmail(typed);
|
|
637
|
+
const verdict = classifyStaffDomain(domain);
|
|
638
|
+
if (verdict === "public" || verdict === "reserved") {
|
|
639
|
+
// The classifier's own sentence, never a second copy: `clearotron start` prints these same words
|
|
640
|
+
// when it meets the same domain, and two wordings of one refusal is how a reader comes to believe
|
|
641
|
+
// they have met two different problems.
|
|
642
|
+
problem(staffDomainRefusal(domain));
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (verdict === "narrow") {
|
|
646
|
+
ok(`${typed} — this machine only. No domain rule is written, and nobody else is granted anything.`);
|
|
647
|
+
return { PORTAL_LOCAL_USER: typed };
|
|
648
|
+
}
|
|
649
|
+
// `wide`: a real domain, so the grant is a grant. Stated first, in the settings page's own words,
|
|
650
|
+
// and Enter is NOT a yes — the default is no, because this is the one answer in the wizard that
|
|
651
|
+
// admits people the reader has never met.
|
|
652
|
+
say("");
|
|
653
|
+
warn(`${typed} makes this a rule about ${domain}, not about you.`);
|
|
654
|
+
say(` ${staffGrantSentence(domain, { staffLabel })}`);
|
|
655
|
+
say("");
|
|
656
|
+
say(` Everyone at ${domain} who gets past this install's sign-in door would see every brand owner`);
|
|
657
|
+
say(" on it — every clearance, every report, every configuration. On a laptop that is only you,");
|
|
658
|
+
say(" because only one address can sign in. Behind a company login it is the whole domain.");
|
|
659
|
+
say("");
|
|
660
|
+
if (await confirm(`Grant ${domain} that, and write it down as PORTAL_STAFF_DOMAINS?`, false))
|
|
661
|
+
return { PORTAL_LOCAL_USER: typed, PORTAL_STAFF_DOMAINS: domain };
|
|
662
|
+
info(`nothing granted${envPath ? `, and nothing written to ${envPath}` : ""}. ${typed} would sign in `
|
|
663
|
+
+ "and every page would refuse it, because signing in is not being enrolled — so choose the "
|
|
664
|
+
+ "local-account form, or answer yes above.");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
590
668
|
/**
|
|
591
669
|
* An engine binary, resolved the way the engine resolves it — and the trap that resolution carries.
|
|
592
670
|
*
|
|
@@ -998,9 +1076,9 @@ export async function runCheck() {
|
|
|
998
1076
|
}
|
|
999
1077
|
|
|
1000
1078
|
say("\n Node");
|
|
1001
|
-
const
|
|
1002
|
-
if (
|
|
1003
|
-
else problem(`node ${
|
|
1079
|
+
const nodeV = nodeFloorVerdict();
|
|
1080
|
+
if (nodeV.ok) ok(`node ${nodeV.current}`);
|
|
1081
|
+
else problem(`node ${nodeV.current} — this engine needs ${nodeV.required} or newer; node:sqlite is not a built-in module before then`);
|
|
1004
1082
|
|
|
1005
1083
|
// Read the file up here rather than at the `.env` heading below: the engine section is the first that
|
|
1006
1084
|
// needs `effective()`, and which ENGINE is configured decides which binary variable to check. Reading
|
|
@@ -1170,6 +1248,46 @@ export async function runCheck() {
|
|
|
1170
1248
|
+ "clearotron doctor --probe-engine to find out.");
|
|
1171
1249
|
}
|
|
1172
1250
|
|
|
1251
|
+
// AND WHETHER THE ENGINE AGREES, which is a different question from the one above and the reason an
|
|
1252
|
+
// outside user gave up on this product. This command reads its OWN environment; the portal's New
|
|
1253
|
+
// clearance screen reads what the engine recorded when it last started. Both were right and they
|
|
1254
|
+
// said opposite things, so a reader who checked the confident-looking one first was told the engine
|
|
1255
|
+
// was fine while no search would start. Reported here in the same words the configuration page uses,
|
|
1256
|
+
// because a doctor that cannot see a contradiction the product ships with is the wrong doctor.
|
|
1257
|
+
try {
|
|
1258
|
+
const { readFlagSnapshot, postureDisagreement } = await import("../driver/flag-snapshot.mjs");
|
|
1259
|
+
// THE READ-SIDE ACCESSOR, because this is a read: `config.poolRoot` throws on a box with no pool
|
|
1260
|
+
// configured, and `readFlagSnapshot(null)` already answers "no capture" for exactly that case.
|
|
1261
|
+
const snap = readFlagSnapshot(config.poolRootOrNull);
|
|
1262
|
+
const rows = snap
|
|
1263
|
+
? postureDisagreement(snap, { flags: {}, engine: engineInventory(invEnv) })
|
|
1264
|
+
: null;
|
|
1265
|
+
// NULL IS NOT AGREEMENT and neither is an empty pool — a box with no capture has nothing to
|
|
1266
|
+
// disagree with, and saying so beats printing a clean bill nobody measured.
|
|
1267
|
+
const clash = (rows ?? []).find((r) => r.what === "engine program");
|
|
1268
|
+
if (clash) {
|
|
1269
|
+
problem(`The engine that last ran and this machine disagree about the engine program: the last run `
|
|
1270
|
+
+ `recorded it as ${clash.capture}, this machine reads it as ${clash.live}. A NEW search will `
|
|
1271
|
+
+ `refuse while that is true. Restart the engine service so it re-reads its PATH, or install the `
|
|
1272
|
+
+ `CLI where the service can see it.`);
|
|
1273
|
+
}
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
// WHAT ACTUALLY REACHES THIS CATCH, established by driving it rather than by reading it.
|
|
1276
|
+
//
|
|
1277
|
+
// Not a box with no pool: `poolRootOrNull` answers null and `readFlagSnapshot(null)` answers null,
|
|
1278
|
+
// neither throwing. And NOT an unreadable capture either — `readFlagSnapshot` has its own try and
|
|
1279
|
+
// returns null for a corrupt file, so a damaged capture is already indistinguishable from an absent
|
|
1280
|
+
// one by the time this code sees it. That is worth knowing and is not this change's to fix.
|
|
1281
|
+
//
|
|
1282
|
+
// So this catch covers an import that has broken or an unexpected throw out of the comparison —
|
|
1283
|
+
// the check having stopped running. A doctor silent about its own failure is the defect the rest of
|
|
1284
|
+
// this change is about, an absence rendered as a clean bill, so it says so. A caution rather than a
|
|
1285
|
+
// problem: the engine may be perfectly fine and it is this check that is broken.
|
|
1286
|
+
info(`Could not compare this machine against what the engine last recorded (${e?.message ?? e}). `
|
|
1287
|
+
+ `That comparison is what catches a settings page reading healthy while a search will not start, `
|
|
1288
|
+
+ `so this run has not checked it either way.`);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1173
1291
|
// item 5 — WHICH BILLING LANE, reported rather than left to be inferred from a variable's
|
|
1174
1292
|
// absence. `--check` named the engine and its binary and never said how the box pays, so the two
|
|
1175
1293
|
// states that matter — metered per token, or drawn against a subscription — were indistinguishable
|
|
@@ -1366,7 +1484,7 @@ export async function runCheck() {
|
|
|
1366
1484
|
// Both lines were honest about their own source and neither said what it was. The first reads this
|
|
1367
1485
|
// command's env file; the second reads THIS PROCESS's resolution, and a CLI is started by a login
|
|
1368
1486
|
// shell that carries none of the units' `EnvironmentFile`. The deployment was correct and served
|
|
1369
|
-
//
|
|
1487
|
+
// the configured roster throughout.
|
|
1370
1488
|
//
|
|
1371
1489
|
// THE UNITS' ENV IS NOT PASSED INTO `profileStoreResolution`, deliberately. `PROFILES_OVERLAY_DIR`
|
|
1372
1490
|
// is captured when profiles.mjs LOADS, so handing it the units' value would set `live` with no
|
|
@@ -1418,17 +1536,38 @@ export async function runCheck() {
|
|
|
1418
1536
|
const demo = keys.filter((k) => roster.get(k)?.demoData === true);
|
|
1419
1537
|
// `generic` is the universal fallback the module requires by name, not a brand owner somebody
|
|
1420
1538
|
// onboarded — counting it would tell an operator with an empty store that they have one.
|
|
1421
|
-
|
|
1539
|
+
//
|
|
1540
|
+
// A DEMO ACCOUNT IS NOT AN ONBOARDED OWNER EITHER, and until this line it was counted as one. A
|
|
1541
|
+
// fresh install used to ship the demo account into every roster, so `doctor` reported "1 brand
|
|
1542
|
+
// owner(s) resolve here: demo-brand-owner (DEMO DATA)" on a machine where nobody had onboarded
|
|
1543
|
+
// anything — and never named `generic`, which is the account that actually rates a run there.
|
|
1544
|
+
// The reader is told they have a customer and not told what they are running on. Both halves
|
|
1545
|
+
// wrong from one list.
|
|
1546
|
+
//
|
|
1547
|
+
// SINCE 2026-09-08 A FRESH INSTALL RESOLVES `generic` ALONE (owner ruling): nobody should have to
|
|
1548
|
+
// clean demo material out of an environment they just created. So the demo branch below no longer
|
|
1549
|
+
// fires on a plain install — it fires inside the demo, which asks for its own account. It is kept
|
|
1550
|
+
// rather than deleted because it is still reachable, and a reader who meets the demo account
|
|
1551
|
+
// there is owed the same two facts: it is fiction, and a real clearance under it is refused at
|
|
1552
|
+
// the admission wall.
|
|
1553
|
+
//
|
|
1554
|
+
// Three states, told apart, because they mean three different things to whoever is reading:
|
|
1555
|
+
// an onboarded roster, the house default alone, and the house default beside what the demo
|
|
1556
|
+
// brought with it.
|
|
1557
|
+
const owners = keys.filter((k) => k !== "generic" && !demo.includes(k));
|
|
1422
1558
|
if (!owners.length) {
|
|
1423
|
-
|
|
1424
|
-
+
|
|
1559
|
+
const base = "`generic` is the account this install rates under — the house default, and the "
|
|
1560
|
+
+ "only one a clean install has";
|
|
1561
|
+
if (demo.length) {
|
|
1562
|
+
info(`${base}. The demo brought one with it, marked DEMO DATA: ${demo.join(", ")} — fiction `
|
|
1563
|
+
+ "rather than an account anybody onboarded, and a real clearance under one is refused");
|
|
1564
|
+
} else {
|
|
1565
|
+
info(`${base}. An empty store is a working install on Generic defaults; it is also what a `
|
|
1566
|
+
+ "store pointed at the wrong directory looks like");
|
|
1567
|
+
}
|
|
1425
1568
|
} else {
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
// The demo marker is the member-level half: naming the store is not the
|
|
1429
|
-
// same as saying the accounts in it are fiction, and a real clearance under one is refused at
|
|
1430
|
-
// the admission wall — which an operator should learn here rather than from that refusal.
|
|
1431
|
-
if (demo.length) info(`${line} — accounts marked DEMO DATA cannot start a real clearance`);
|
|
1569
|
+
const line = `${owners.length} brand owner(s) resolve here: ${owners.join(", ")}`;
|
|
1570
|
+
if (demo.length) info(`${line}. The demo brought one with it, marked DEMO DATA: ${demo.join(", ")} — not counted above, and a real clearance under one is refused`);
|
|
1432
1571
|
else ok(line);
|
|
1433
1572
|
}
|
|
1434
1573
|
try {
|
|
@@ -2310,6 +2449,27 @@ export async function runCheck() {
|
|
|
2310
2449
|
// unreachable; repeating it as a second failure teaches the reader that this section
|
|
2311
2450
|
// double-counts. It still states what was NOT established, which is the whole job.
|
|
2312
2451
|
else if (v.kind === "could-not-look") info(v.message);
|
|
2452
|
+
// NO ACCESS IN FRONT OF THE CLIENT DOOR IS A POSTURE, NOT A FAULT (owner ruling 2026-09-08).
|
|
2453
|
+
//
|
|
2454
|
+
// This is the CLIENT connector's address, and how a client reaches it is the client's decision:
|
|
2455
|
+
// "client access sitting behind OAuth is totally up to a client — plenty might just run it token
|
|
2456
|
+
// based on their own laptop." A door answering with its own Bearer challenge and no Access front
|
|
2457
|
+
// is a supported shape, not a misconfiguration, so raising it made `doctor` exit 1 on a healthy
|
|
2458
|
+
// deployment and taught its reader to skim the one command that must never be skimmed.
|
|
2459
|
+
//
|
|
2460
|
+
// It is still SAID rather than dropped, and it still states what was not established: an
|
|
2461
|
+
// audience that was never compared is not an audience that agreed. What changed is that the
|
|
2462
|
+
// sentence no longer calls a client's own arrangement a finding about this install.
|
|
2463
|
+
//
|
|
2464
|
+
// Scoped deliberately to `not-fronted` on THIS address. A configured audience that DISAGREES
|
|
2465
|
+
// with the one the edge issues is still a fault, and the portal and ops surfaces are untouched —
|
|
2466
|
+
// they are checked elsewhere and Access in front of them is not optional.
|
|
2467
|
+
else if (v.kind === "not-fronted") {
|
|
2468
|
+
info(`nothing is fronting this hostname with Access — ${read.why}. That is this client door's `
|
|
2469
|
+
+ "posture rather than a fault: a client may reach it with its own token and no Access in "
|
|
2470
|
+
+ "front, which is a supported shape. The configured audience was not compared against this "
|
|
2471
|
+
+ "address, so nothing here says the two agree.");
|
|
2472
|
+
}
|
|
2313
2473
|
else problem(v.message);
|
|
2314
2474
|
}
|
|
2315
2475
|
}
|
|
@@ -2534,13 +2694,13 @@ try {
|
|
|
2534
2694
|
|
|
2535
2695
|
// 1 ── Node
|
|
2536
2696
|
say(" Node");
|
|
2537
|
-
const
|
|
2538
|
-
if (
|
|
2539
|
-
problem(`node ${
|
|
2697
|
+
const nodeV = nodeFloorVerdict();
|
|
2698
|
+
if (!nodeV.ok) {
|
|
2699
|
+
problem(`node ${nodeV.current} — this engine needs ${nodeV.required} or newer. Upgrade Node and run setup again.`);
|
|
2540
2700
|
aborted = "node";
|
|
2541
2701
|
throw new Error("node floor");
|
|
2542
2702
|
}
|
|
2543
|
-
ok(`node ${
|
|
2703
|
+
ok(`node ${nodeV.current}`);
|
|
2544
2704
|
|
|
2545
2705
|
// 2 ── the engine: which one, which binary, and PROOF that it can run a turn
|
|
2546
2706
|
//
|
|
@@ -3175,6 +3335,22 @@ try {
|
|
|
3175
3335
|
say(" fallback — falls through by name, so an empty store is a working install. Your own customers");
|
|
3176
3336
|
say(" are added here by name; the bundled demo customers never show through into your roster.");
|
|
3177
3337
|
|
|
3338
|
+
// 7c ── WHO SIGNS IN, AND WHAT THAT ADDRESS GRANTS
|
|
3339
|
+
//
|
|
3340
|
+
// Setup never asked for an address, so `clearotron start` derived one — and derived a staff-domain
|
|
3341
|
+
// rule from everything after its `@`. The whole reasoning, and the install that granted a
|
|
3342
|
+
// documentation domain to a reader who had never been asked, is on `askSignIn` above.
|
|
3343
|
+
section("Who signs in");
|
|
3344
|
+
const localAccount = (() => {
|
|
3345
|
+
try { return userInfo().username || "user"; } catch { return "user"; }
|
|
3346
|
+
})();
|
|
3347
|
+
// ONE CALL SITE. The loop itself lives in `askSignIn` so that the branch that matters — a reader who
|
|
3348
|
+
// pressed Enter at the grant question, and a staff rule that must therefore NOT be written — is
|
|
3349
|
+
// reachable without a terminal. The same seam and the same reason as `offerUsptoSync`.
|
|
3350
|
+
Object.assign(candidate, await askSignIn(
|
|
3351
|
+
{ askValue, confirm, say, ok, info, warn, problem },
|
|
3352
|
+
{ localAccount, staffLabel: `${BRAND.name} staff`, envPath: ENV_PATH }));
|
|
3353
|
+
|
|
3178
3354
|
// 8 ── the engine's own preflight over the whole candidate
|
|
3179
3355
|
//
|
|
3180
3356
|
// — THIS STEP CHECKS THE REGISTER PROVIDER AND NOTHING ELSE. `preflightCandidate` pins
|