@mcpherson-ai/observa-local-node 0.1.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/README.md +103 -0
- package/SHA256SUMS +33 -0
- package/artifacts/mcphersonai-observa-adapter-n8n-0.1.0.tgz +0 -0
- package/artifacts/mcphersonai-observa-domain-generic-0.1.0.tgz +0 -0
- package/artifacts/mcphersonai-observa-hosted-transport-0.1.0.tgz +0 -0
- package/artifacts/mcphersonai-observa-n8n-h1-binding-0.1.0.tgz +0 -0
- package/artifacts/mcphersonai-observa-node-0.1.0.tgz +0 -0
- package/distribution/observa-cli/bin/observa-n8n-hosted.mjs +114 -0
- package/distribution/observa-cli/bin/observa.mjs +331 -0
- package/distribution/observa-cli/integrations/n8n/observa-external-hook.cjs +132 -0
- package/distribution/observa-cli/keys/observa-beta-1.public.json +7 -0
- package/distribution/observa-cli/src/artifact.mjs +143 -0
- package/distribution/observa-cli/src/config.mjs +162 -0
- package/distribution/observa-cli/src/errors.mjs +21 -0
- package/distribution/observa-cli/src/hosted-delivery.mjs +148 -0
- package/distribution/observa-cli/src/index.mjs +39 -0
- package/distribution/observa-cli/src/install.mjs +443 -0
- package/distribution/observa-cli/src/lock.mjs +57 -0
- package/distribution/observa-cli/src/manifest-schema.mjs +208 -0
- package/distribution/observa-cli/src/manifest-verify.mjs +122 -0
- package/distribution/observa-cli/src/n8n-hook.mjs +70 -0
- package/distribution/observa-cli/src/pair.mjs +149 -0
- package/distribution/observa-cli/src/re-pair.mjs +171 -0
- package/distribution/observa-cli/src/service.mjs +218 -0
- package/distribution/observa-cli/src/state.mjs +87 -0
- package/distribution/observa-cli/src/status.mjs +191 -0
- package/distribution/observa-cli/src/vocabulary.mjs +47 -0
- package/npm-distribution-provenance.json +1 -0
- package/package.json +36 -0
- package/runtime-adapters/n8n/src/strict-json.mjs +138 -0
- package/sdk/contracts/canonical.mjs +289 -0
- package/sdk/contracts/entry-boundary.mjs +417 -0
- package/sdk/contracts/errors.mjs +334 -0
- package/sdk/contracts/stable-primitives.mjs +141 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Observa Local Node
|
|
2
|
+
|
|
3
|
+
**Mode: SHADOW_ONLY. Authority: NONE. Enforcement: OFF.**
|
|
4
|
+
|
|
5
|
+
Observa Local Node observes and verifies what your workflow runtime actually did
|
|
6
|
+
and records tamper-evident evidence about it. It has no authority to allow,
|
|
7
|
+
deny, approve, block, retry, or change anything in your systems, and this
|
|
8
|
+
build contains no code path that could.
|
|
9
|
+
|
|
10
|
+
Observa Local Node is an **invite-only beta**. This package delivers bytes; it
|
|
11
|
+
activates nothing by itself. Installation requires the signed installation
|
|
12
|
+
manifest issued with your invite, and the installer verifies that manifest
|
|
13
|
+
against keys shipped in this package before composing anything.
|
|
14
|
+
|
|
15
|
+
- Product: Observa Local Node 0.1.3
|
|
16
|
+
- Composition: `observa-local-node-n8n-generic-shadow-beta` (beta channel)
|
|
17
|
+
- Runtime components bundled: 5 (installed for you; never
|
|
18
|
+
used as separate packages)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install -g @mcpherson-ai/observa-local-node@beta
|
|
24
|
+
observa --version
|
|
25
|
+
observa install --manifest <your-signed-manifest.json>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Install by the `beta` dist-tag: this is a beta channel and no
|
|
29
|
+
`latest` release exists, so a bare package name would not resolve.
|
|
30
|
+
|
|
31
|
+
The approved component artifacts ship inside this package and are found
|
|
32
|
+
automatically; every artifact is verified by manifest-pinned SHA-256 before a
|
|
33
|
+
byte of it is staged. Observa installs into `~/.observa/installation`
|
|
34
|
+
(override: `OBSERVA_HOME`) and touches nothing else. Installation is fully
|
|
35
|
+
offline: the installer has no registry or network surface at all.
|
|
36
|
+
|
|
37
|
+
## Observe n8n automatically
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
observa config set n8n_loopback_credential '<choose any local secret, 24+ characters>'
|
|
41
|
+
observa n8n-setup
|
|
42
|
+
observa start
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That credential is yours: it authenticates n8n's calls to Observa's own
|
|
46
|
+
loopback endpoint on this machine. It is stored owner-only, never transmitted
|
|
47
|
+
by the installer, and never appears in any Observa output.
|
|
48
|
+
|
|
49
|
+
`n8n-setup` writes an owner-only environment file for n8n's supported global
|
|
50
|
+
backend hook. Configure your n8n service/container to load that environment
|
|
51
|
+
before n8n starts. The hook observes ordinary workflows automatically — do
|
|
52
|
+
not add an Observa node to a workflow. It reads execution metadata only and
|
|
53
|
+
never reads item data, node parameters, credentials, or workflow static data.
|
|
54
|
+
|
|
55
|
+
## Hosted evidence delivery (if you received a pairing code)
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
observa pair --base-url <HOSTED_BASE_URL> --code-stdin
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Paste the single-use pairing code, then send end-of-input (Ctrl-D). The
|
|
62
|
+
resulting connector credential is stored owner-only and never displayed.
|
|
63
|
+
Without a pairing code, everything above still works fully locally.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
observa --help # full usage
|
|
69
|
+
observa status # non-secret installation facts
|
|
70
|
+
observa diagnose # integrity/signature/config checks (exit 1 on FAIL)
|
|
71
|
+
observa start # start Observa's own local observer
|
|
72
|
+
observa stop # stop it (never your n8n)
|
|
73
|
+
observa update --manifest <newer-signed-manifest.json>
|
|
74
|
+
observa rollback # return to the retained previous release
|
|
75
|
+
observa uninstall --yes [--purge-config]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## What ships in this package
|
|
79
|
+
|
|
80
|
+
| file | internal component | role |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `artifacts/mcphersonai-observa-node-0.1.0.tgz` | `@mcphersonai/observa-node@0.1.0` | verification |
|
|
83
|
+
| `artifacts/mcphersonai-observa-adapter-n8n-0.1.0.tgz` | `@mcphersonai/observa-adapter-n8n@0.1.0` | runtime-adapter |
|
|
84
|
+
| `artifacts/mcphersonai-observa-n8n-h1-binding-0.1.0.tgz` | `@mcphersonai/observa-n8n-h1-binding@0.1.0` | integration-binding |
|
|
85
|
+
| `artifacts/mcphersonai-observa-domain-generic-0.1.0.tgz` | `@mcphersonai/observa-domain-generic@0.1.0` | domain-contract |
|
|
86
|
+
| `artifacts/mcphersonai-observa-hosted-transport-0.1.0.tgz` | `@mcphersonai/observa-hosted-transport@0.1.0` | transport-client |
|
|
87
|
+
|
|
88
|
+
Plus the `observa` CLI, `SHA256SUMS`, and
|
|
89
|
+
`npm-distribution-provenance.json` (exact source identity of every shipped byte).
|
|
90
|
+
|
|
91
|
+
## What this package will not do
|
|
92
|
+
|
|
93
|
+
- It cannot allow, deny, approve or enforce anything: SHADOW_ONLY is the only
|
|
94
|
+
mode in its vocabulary, and a manifest asking for any other word is refused.
|
|
95
|
+
- It never modifies, restarts or reconfigures your n8n or any other system.
|
|
96
|
+
- It asks for no system credentials, and has no field anywhere in which one
|
|
97
|
+
could be sent to us.
|
|
98
|
+
- It runs no install scripts, has zero dependencies, and phones nothing home.
|
|
99
|
+
|
|
100
|
+
## If something refuses
|
|
101
|
+
|
|
102
|
+
Every refusal prints `REFUSED <CODE>`. Send your Observa contact the code
|
|
103
|
+
and the output of `observa diagnose` — neither contains secrets.
|
package/SHA256SUMS
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
757c317514d4082617a6d8be6a726e744561fc992ec7d57873c3a1029726f869 README.md
|
|
2
|
+
daf644c3cc22f4b588af1f242dd02dd0946b6cfa869db1466da33dfb235871d0 artifacts/mcphersonai-observa-adapter-n8n-0.1.0.tgz
|
|
3
|
+
7f326e4b744f8eeaef7d12d781d82a6f8cf8cea5327ed4c9b22c180bd0ab5e04 artifacts/mcphersonai-observa-domain-generic-0.1.0.tgz
|
|
4
|
+
b6f2ccbbb0526aa08686c8725dd5d2e78aee58e607f15e380170c057a08881e8 artifacts/mcphersonai-observa-hosted-transport-0.1.0.tgz
|
|
5
|
+
5f4bc40bf964255491a21c066d1578b9867b892689c609e4475c53210f7b6b72 artifacts/mcphersonai-observa-n8n-h1-binding-0.1.0.tgz
|
|
6
|
+
00e96d00c9ed9d8d3ca98cde4b9e14c8eecc9adc360174afb7ca95f03db61e3c artifacts/mcphersonai-observa-node-0.1.0.tgz
|
|
7
|
+
70c520a6c4a2478a60b98bdc2018328c01d1c797c9048f664b507b4ee570632e distribution/observa-cli/bin/observa-n8n-hosted.mjs
|
|
8
|
+
b2f67dcbf449e858933b5a19775adc8107bd66b6fc3ba2134e36b16eb892287a distribution/observa-cli/bin/observa.mjs
|
|
9
|
+
0caa044f718d6305d60d53a8fb76255b84c7d08ae70ecb7d285a8d1f37c7d3fd distribution/observa-cli/integrations/n8n/observa-external-hook.cjs
|
|
10
|
+
e6f3353b323702df54e96210bd0f1363f89636d2c0b216057698b383ecf40c8c distribution/observa-cli/keys/observa-beta-1.public.json
|
|
11
|
+
31c5bf2495a237be149ab3b1bb72a0707d13f342bb438de1604dc06fe7faf71b distribution/observa-cli/src/artifact.mjs
|
|
12
|
+
6935ff8d65887154f6e4527cb99e49dae88252f3f423dcc8e38a7a96b261af85 distribution/observa-cli/src/config.mjs
|
|
13
|
+
3ac787dccc3c6a09e7f800ba31e4e3f67fe21f160226a51ed36228d440503c7f distribution/observa-cli/src/errors.mjs
|
|
14
|
+
8deccb19fca28ea4ebb84d2da43c346a216bf958444376a64b06a59336ec6138 distribution/observa-cli/src/hosted-delivery.mjs
|
|
15
|
+
a89aeeeaa04f099949a40ea3b3cf678aa4114824042df661e1faa71d5b8ce97e distribution/observa-cli/src/index.mjs
|
|
16
|
+
2bccc8ca0bfd37382f00d032fa6dedce7ca2b6819019ab5d4c97e8995e58d1bc distribution/observa-cli/src/install.mjs
|
|
17
|
+
0dffd6446a9428fc9f3e45e468988a9b13cfc99a5ad3179a1f1b01029bc5cfbc distribution/observa-cli/src/lock.mjs
|
|
18
|
+
eccce81a4bd746805d31ee3a270dfd34083a40746293b1307f58288d0d8d79d0 distribution/observa-cli/src/manifest-schema.mjs
|
|
19
|
+
94dd7980032fe70636ef96e089cd96aba2ac1307a369ae7ed52aa3c04267f91b distribution/observa-cli/src/manifest-verify.mjs
|
|
20
|
+
6ae519f0e59dccd14a6bc6655c65846b1b9084bf28f720b9a5c24bba563d05a0 distribution/observa-cli/src/n8n-hook.mjs
|
|
21
|
+
ce2acc3d335467e7af2531a5950a34c3ed3dacdddb41601df20ff278efe7da63 distribution/observa-cli/src/pair.mjs
|
|
22
|
+
3ccc90a750ae941d9dffd7f8e19be9c0cc404016ee3f442fe5a8ee43abcf9664 distribution/observa-cli/src/re-pair.mjs
|
|
23
|
+
01fc22e4fc893f58d5eef11d726b4044fe2f1ad7430d9acb322e964b60d246e1 distribution/observa-cli/src/service.mjs
|
|
24
|
+
07258afc26ec033d471af3a4bca60c1f2c74d898d55d295baa52a5eceaaf2073 distribution/observa-cli/src/state.mjs
|
|
25
|
+
09dfc6c6e8668c8be4089c0bb6f85c49afdd54479489860c56b03198f011d03e distribution/observa-cli/src/status.mjs
|
|
26
|
+
5077b323eef9aab9fcb71aae5dd3b5e8ca15ea9b71a489068c2df109da77c2f2 distribution/observa-cli/src/vocabulary.mjs
|
|
27
|
+
5c7b7ae62dd01649983b6635b9b1f0549bbc2199502b384efbef19dd538f2cb6 npm-distribution-provenance.json
|
|
28
|
+
2022669eda7348bcccbbfc69025cadf99df0f5a97c7b8a756499bbbf53d3cd26 package.json
|
|
29
|
+
d3e88b83d1226878130e9fbfbd33fd29634d4cb0c49b4d6a2b6dbc9ebbdb63e1 runtime-adapters/n8n/src/strict-json.mjs
|
|
30
|
+
b28e8f6a53fe945ff1e2610a1f9d333aa9fa62ffe375e253aeadaed74a5eb72d sdk/contracts/canonical.mjs
|
|
31
|
+
78aa76d66bf8606c2bcfe5e50004139fe8c2168fc3a9b653534fc41f2471a4e9 sdk/contracts/entry-boundary.mjs
|
|
32
|
+
d5aa1bbe72f3fa4ba9edf47ad7dc2113df3877fa852251221c8c0abee60545d7 sdk/contracts/errors.mjs
|
|
33
|
+
88da8b531a26fc8d1b7d68f622e1e135b0addbf75057fd161b773cb449044c87 sdk/contracts/stable-primitives.mjs
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The Local Node service WITH hosted evidence delivery — the v0.1.1 start
|
|
3
|
+
// path when `observa pair` has configured Hosted delivery.
|
|
4
|
+
//
|
|
5
|
+
// This composes the SEALED pieces exactly as the closed binding's harness
|
|
6
|
+
// does, without touching one of their bytes: the sealed adapter's own
|
|
7
|
+
// config, observer and loopback server, with the existing hosted transport
|
|
8
|
+
// client INJECTED at the seam the sealed observer published
|
|
9
|
+
// (`new RuntimeObserver({ transport })`). The sealed binary's own `serve`
|
|
10
|
+
// verb remains the local-receipts-only path and remains untouched.
|
|
11
|
+
//
|
|
12
|
+
// The sealed runtime modules load from the INSTALLED release root
|
|
13
|
+
// (OBSERVA_RELEASE_LIB, set by `observa start`) — the bytes the signed
|
|
14
|
+
// manifest verified — because this bin ships inside the CLI package, away
|
|
15
|
+
// from the runtime artifacts. In the repository the two roots coincide.
|
|
16
|
+
//
|
|
17
|
+
// Environment contract (set by `observa start`, exactly like the sealed
|
|
18
|
+
// serve verb's own env): the sealed OBSERVA_N8N_* variables, plus
|
|
19
|
+
// OBSERVA_RELEASE_LIB the installed release lib root
|
|
20
|
+
// OBSERVA_HOSTED_BASE_URL where Hosted Observa listens
|
|
21
|
+
// OBSERVA_HOSTED_CREDENTIAL the mgd1 connector credential
|
|
22
|
+
// The credential arrives in the environment from the supervisor, exactly as
|
|
23
|
+
// the loopback credential already does, and is never written to a page, a
|
|
24
|
+
// log line, or an error by this process.
|
|
25
|
+
//
|
|
26
|
+
// AUTHORITY: NONE. ENFORCEMENT: OFF. This process observes and delivers
|
|
27
|
+
// evidence; the acknowledgements it receives are receipt acknowledgements
|
|
28
|
+
// with no instruction field to obey.
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
31
|
+
import {
|
|
32
|
+
capabilityReportFrom, loadHostedRuntime,
|
|
33
|
+
} from "../src/hosted-delivery.mjs";
|
|
34
|
+
import { HOSTED_BASE_URL_PATTERN } from "../src/pair.mjs";
|
|
35
|
+
|
|
36
|
+
const MGD1_TOKEN_PATTERN = /^mgd1_[a-f0-9]{32}\.[A-Za-z0-9_-]{43}$/;
|
|
37
|
+
|
|
38
|
+
const out = (line) => process.stdout.write(`${line}\n`);
|
|
39
|
+
const err = (line) => process.stderr.write(`${line}\n`);
|
|
40
|
+
|
|
41
|
+
function defaultLibRoot() {
|
|
42
|
+
// Repository/source convenience only: the CLI package sits two levels
|
|
43
|
+
// under the release root there. Installed services always receive
|
|
44
|
+
// OBSERVA_RELEASE_LIB from the supervisor.
|
|
45
|
+
return fileURLToPath(new URL("../../..", import.meta.url));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function serveHosted(env = process.env) {
|
|
49
|
+
const baseUrl = env.OBSERVA_HOSTED_BASE_URL;
|
|
50
|
+
const credential = env.OBSERVA_HOSTED_CREDENTIAL;
|
|
51
|
+
if (typeof baseUrl !== "string" || !HOSTED_BASE_URL_PATTERN.test(baseUrl)) {
|
|
52
|
+
err("REFUSED HOSTED_BASE_URL_SHAPE_REFUSED");
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (typeof credential !== "string" || !MGD1_TOKEN_PATTERN.test(credential)) {
|
|
57
|
+
err("REFUSED HOSTED_CREDENTIAL_SHAPE_REFUSED");
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const libRoot = env.OBSERVA_RELEASE_LIB ?? defaultLibRoot();
|
|
62
|
+
|
|
63
|
+
const runtime = await loadHostedRuntime(libRoot);
|
|
64
|
+
const load = (relative) =>
|
|
65
|
+
import(pathToFileURL(join(libRoot, ...relative.split("/"))).href);
|
|
66
|
+
const [{ configFromEnv, describeConfig }, { RuntimeObserver }, serverModule] =
|
|
67
|
+
await Promise.all([
|
|
68
|
+
load("runtime-adapters/n8n/src/config.mjs"),
|
|
69
|
+
load("runtime-adapters/n8n/src/observer.mjs"),
|
|
70
|
+
load("runtime-adapters/n8n/src/server.mjs"),
|
|
71
|
+
]);
|
|
72
|
+
const { createObservationServer, listen, ROUTES } = serverModule;
|
|
73
|
+
|
|
74
|
+
const config = configFromEnv(env);
|
|
75
|
+
const client = runtime.clientModule.createHostedTransportClient({
|
|
76
|
+
baseUrl,
|
|
77
|
+
credential,
|
|
78
|
+
installationId: config.installationId,
|
|
79
|
+
});
|
|
80
|
+
const transport = runtime.clientModule.createHostedObservationTransport({ client });
|
|
81
|
+
const observer = new RuntimeObserver({
|
|
82
|
+
receiptPath: config.receiptPath,
|
|
83
|
+
transport,
|
|
84
|
+
});
|
|
85
|
+
const server = createObservationServer({ observer, config });
|
|
86
|
+
const address = await listen(server, config);
|
|
87
|
+
|
|
88
|
+
out("observa n8n local node (hosted delivery) — authority NONE, enforcement OFF");
|
|
89
|
+
out(`listening on http://${config.host}:${address.port} (loopback only)`);
|
|
90
|
+
out(`routes: ${ROUTES.ATTEMPT} ${ROUTES.COMPLETION} ${ROUTES.STATUS}`);
|
|
91
|
+
// describe() and describeConfig() are secret-free by contract.
|
|
92
|
+
out(`config: ${JSON.stringify(describeConfig(config))}`);
|
|
93
|
+
out(`hosted transport: ${JSON.stringify(transport.describe())}`);
|
|
94
|
+
|
|
95
|
+
// One capability report at startup: which contracts this installation
|
|
96
|
+
// speaks. Delivery failure is a STATUS, logged as such — the local
|
|
97
|
+
// observer keeps running either way, exactly like every other delivery.
|
|
98
|
+
const capability = await client.sendCapabilityReport(capabilityReportFrom(runtime));
|
|
99
|
+
out(`capability report delivery: ${capability.status}${capability.code ? ` (${capability.code})` : ""}`);
|
|
100
|
+
|
|
101
|
+
return { server, observer, client, transport };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Run only when executed directly; importing this file must stay
|
|
105
|
+
// side-effect free so preflight-style clean-environment imports succeed.
|
|
106
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
107
|
+
const verb = process.argv[2] ?? "serve";
|
|
108
|
+
if (verb !== "serve") {
|
|
109
|
+
err(`REFUSED UNKNOWN_VERB: ${verb} (only: serve)`);
|
|
110
|
+
process.exitCode = 2;
|
|
111
|
+
} else {
|
|
112
|
+
await serveHosted(process.env);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// observa — the customer installer/orchestrator.
|
|
3
|
+
//
|
|
4
|
+
// install verify a signed manifest and install exact approved artifacts
|
|
5
|
+
// status non-secret installation facts
|
|
6
|
+
// diagnose integrity/signature/config-presence checks
|
|
7
|
+
// start start the Observa-owned local service
|
|
8
|
+
// stop stop it (never the customer's n8n or Odoo)
|
|
9
|
+
// update install a NEWER signed manifest (retains rollback set)
|
|
10
|
+
// rollback return to the retained previous signed manifest
|
|
11
|
+
// uninstall remove Observa-managed state (config survives by default)
|
|
12
|
+
// config set a LOCAL config value (never printed back)
|
|
13
|
+
// pair redeem one pairing code; store hosted delivery config locally
|
|
14
|
+
// unpair stop delivery and clear only the hosted pairing config
|
|
15
|
+
//
|
|
16
|
+
// There is no verb that approves, allows, denies, enforces, or decides.
|
|
17
|
+
// The signed manifest authorizes WHICH approved components install; it makes
|
|
18
|
+
// no runtime governance decision, and neither does this tool.
|
|
19
|
+
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { readFileSync, statSync } from "node:fs";
|
|
22
|
+
import { join, resolve } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { CliRefusal } from "../src/errors.mjs";
|
|
25
|
+
import { loadTrustedKeys } from "../src/manifest-verify.mjs";
|
|
26
|
+
import { installFromManifestFile, rollbackToPrevious, uninstall } from "../src/install.mjs";
|
|
27
|
+
import { serviceIsRunning, startService, stopService } from "../src/service.mjs";
|
|
28
|
+
import { collectStatus, runDiagnose } from "../src/status.mjs";
|
|
29
|
+
import { isPairingOwnedConfigKey, isSecretConfigKey, writeLocalConfigValue } from "../src/config.mjs";
|
|
30
|
+
import { pairHosted } from "../src/pair.mjs";
|
|
31
|
+
import {
|
|
32
|
+
pairingRecoveryAvailable, rePairHosted, unpairHosted,
|
|
33
|
+
} from "../src/re-pair.mjs";
|
|
34
|
+
import { readState } from "../src/state.mjs";
|
|
35
|
+
import { prepareN8nHook } from "../src/n8n-hook.mjs";
|
|
36
|
+
import { CLI_ARTIFACT_ID, CLI_AUTHORITY, CLI_VERSION } from "../src/vocabulary.mjs";
|
|
37
|
+
|
|
38
|
+
const out = (line) => process.stdout.write(`${line}\n`);
|
|
39
|
+
const err = (line) => process.stderr.write(`${line}\n`);
|
|
40
|
+
|
|
41
|
+
function defaultHome(env) {
|
|
42
|
+
return env.OBSERVA_HOME ? resolve(env.OBSERVA_HOME) : join(homedir(), ".observa", "installation");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultTrustDir(env) {
|
|
46
|
+
if (env.OBSERVA_TRUST_DIR) return resolve(env.OBSERVA_TRUST_DIR);
|
|
47
|
+
return fileURLToPath(new URL("../keys/", import.meta.url));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// When this CLI is delivered as the Observa Local Node npm distribution, the
|
|
51
|
+
// approved component artifacts ship beside it under <package>/artifacts. That
|
|
52
|
+
// directory is a CONVENIENCE default for `install`'s artifact drop dir — every
|
|
53
|
+
// artifact is still resolved by manifest-pinned sha256, so a wrong or
|
|
54
|
+
// tampered directory changes nothing about what can install.
|
|
55
|
+
function packagedArtifactsDir() {
|
|
56
|
+
const candidate = fileURLToPath(new URL("../../../artifacts/", import.meta.url));
|
|
57
|
+
try {
|
|
58
|
+
return statSync(candidate).isDirectory() ? candidate : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseFlags(argv) {
|
|
65
|
+
const flags = {};
|
|
66
|
+
const positional = [];
|
|
67
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
68
|
+
const arg = argv[i];
|
|
69
|
+
if (arg.startsWith("--")) {
|
|
70
|
+
const key = arg.slice(2);
|
|
71
|
+
if (["yes", "purge-config", "code-stdin"].includes(key)) {
|
|
72
|
+
flags[key] = true;
|
|
73
|
+
} else {
|
|
74
|
+
i += 1;
|
|
75
|
+
if (i >= argv.length) throw new CliRefusal("FLAG_VALUE_MISSING", key);
|
|
76
|
+
flags[key] = argv[i];
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
positional.push(arg);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { flags, positional };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function banner() {
|
|
86
|
+
out(`${CLI_ARTIFACT_ID} ${CLI_VERSION}`);
|
|
87
|
+
out(`authority ${CLI_AUTHORITY} · installer/orchestrator only · SHADOW_ONLY vocabulary`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function usage() {
|
|
91
|
+
banner();
|
|
92
|
+
out("");
|
|
93
|
+
out("usage: observa <install|status|diagnose|start|stop|update|rollback|uninstall|config|pair|unpair|n8n-setup|version>");
|
|
94
|
+
out("");
|
|
95
|
+
out(" install --manifest <signed.json> [--artifacts <dir>] [--expect-installation <id>]");
|
|
96
|
+
out(" update --manifest <signed.json> [--artifacts <dir>]");
|
|
97
|
+
out(" rollback");
|
|
98
|
+
out(" status | diagnose | start | stop | version");
|
|
99
|
+
out(" uninstall --yes [--purge-config]");
|
|
100
|
+
out(" config set <key> <value>");
|
|
101
|
+
out(" pair initial: --base-url <https://host> (--code-stdin | --code-file <owner-only-file>) [--deployment <id>] [--name <label>]");
|
|
102
|
+
out(" after unpair: no flags; reuses the installed identity");
|
|
103
|
+
out(" unpair revoke Hosted delivery; retain the installed identity for re-pair");
|
|
104
|
+
out(" n8n-setup write an owner-only environment for n8n's global observation hook");
|
|
105
|
+
out("");
|
|
106
|
+
out(" --help print this usage; --version print the version banner");
|
|
107
|
+
out("");
|
|
108
|
+
out("environment: OBSERVA_HOME (installation home), OBSERVA_TRUST_DIR (verification keys)");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function pairingCodeFromInput(flags) {
|
|
112
|
+
if (flags.code !== undefined) {
|
|
113
|
+
throw new CliRefusal("PAIR_CODE_ARGV_REFUSED", "use --code-stdin or --code-file");
|
|
114
|
+
}
|
|
115
|
+
const stdinSelected = flags["code-stdin"] === true;
|
|
116
|
+
const fileSelected = typeof flags["code-file"] === "string";
|
|
117
|
+
if (stdinSelected === fileSelected) {
|
|
118
|
+
throw new CliRefusal("PAIR_CODE_INPUT_REQUIRED", "choose exactly one of --code-stdin or --code-file");
|
|
119
|
+
}
|
|
120
|
+
if (fileSelected) {
|
|
121
|
+
const path = resolve(flags["code-file"]);
|
|
122
|
+
let stat;
|
|
123
|
+
try {
|
|
124
|
+
stat = statSync(path);
|
|
125
|
+
} catch {
|
|
126
|
+
throw new CliRefusal("PAIR_CODE_FILE_UNAVAILABLE");
|
|
127
|
+
}
|
|
128
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0) {
|
|
129
|
+
throw new CliRefusal("PAIR_CODE_FILE_PERMISSIONS_REFUSED", "pairing-code file must be owner-only");
|
|
130
|
+
}
|
|
131
|
+
const text = readFileSync(path, "utf8");
|
|
132
|
+
if (Buffer.byteLength(text) > 1024) throw new CliRefusal("PAIR_CODE_INPUT_REFUSED");
|
|
133
|
+
const code = text.trim();
|
|
134
|
+
if (code.length === 0) throw new CliRefusal("PAIR_CODE_INPUT_REFUSED");
|
|
135
|
+
return code;
|
|
136
|
+
}
|
|
137
|
+
const chunks = [];
|
|
138
|
+
let size = 0;
|
|
139
|
+
for await (const chunk of process.stdin) {
|
|
140
|
+
size += chunk.length;
|
|
141
|
+
if (size > 1024) throw new CliRefusal("PAIR_CODE_INPUT_REFUSED");
|
|
142
|
+
chunks.push(chunk);
|
|
143
|
+
}
|
|
144
|
+
const code = Buffer.concat(chunks).toString("utf8").trim();
|
|
145
|
+
if (code.length === 0) throw new CliRefusal("PAIR_CODE_INPUT_REFUSED");
|
|
146
|
+
return code;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function printStatus(status) {
|
|
150
|
+
banner();
|
|
151
|
+
out("");
|
|
152
|
+
if (!status.installed && !status.state_problem) {
|
|
153
|
+
out("installed no");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (status.state_problem) out(`state problem ${status.state_problem}`);
|
|
157
|
+
out(`installed ${status.installed ? "yes" : "no"}`);
|
|
158
|
+
if (status.installation_id) {
|
|
159
|
+
out(`installation ${status.installation_id} (org ${status.organization_id}, ws ${status.workspace_id})`);
|
|
160
|
+
out(`manifest ${status.manifest_id}`);
|
|
161
|
+
out(`previous ${status.previous_manifest_id ?? "none"}`);
|
|
162
|
+
out(`mode ${status.mode}`);
|
|
163
|
+
out(`service ${status.service}`);
|
|
164
|
+
out(`hosted ${status.hosted_configured ? "configured" : "not configured"}`);
|
|
165
|
+
if (status.manifest_problem) out(`manifest problem ${status.manifest_problem}`);
|
|
166
|
+
if (status.components) {
|
|
167
|
+
out("components:");
|
|
168
|
+
for (const c of status.components) {
|
|
169
|
+
out(` ${c.package_name}@${c.package_version} (${c.role})`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function main() {
|
|
176
|
+
const [, , command, ...rest] = process.argv;
|
|
177
|
+
const env = process.env;
|
|
178
|
+
const home = defaultHome(env);
|
|
179
|
+
const { flags, positional } = parseFlags(rest);
|
|
180
|
+
const trustedKeys = () => loadTrustedKeys(defaultTrustDir(env));
|
|
181
|
+
|
|
182
|
+
switch (command) {
|
|
183
|
+
case "install":
|
|
184
|
+
case "update": {
|
|
185
|
+
if (typeof flags.manifest !== "string") throw new CliRefusal("MANIFEST_FLAG_REQUIRED");
|
|
186
|
+
if (command === "update" && readState(home) === null) {
|
|
187
|
+
throw new CliRefusal("NOT_INSTALLED", "update requires an existing installation");
|
|
188
|
+
}
|
|
189
|
+
const result = installFromManifestFile({
|
|
190
|
+
home,
|
|
191
|
+
manifestFile: resolve(flags.manifest),
|
|
192
|
+
artifactDir: flags.artifacts ? resolve(flags.artifacts) : packagedArtifactsDir(),
|
|
193
|
+
trustedKeys: trustedKeys(),
|
|
194
|
+
expectedInstallationId: flags["expect-installation"] ?? null,
|
|
195
|
+
});
|
|
196
|
+
banner();
|
|
197
|
+
out("");
|
|
198
|
+
out(`${result.transition} ok`);
|
|
199
|
+
out(`manifest ${result.manifest.manifest_id}`);
|
|
200
|
+
out(`mode ${result.manifest.mode}`);
|
|
201
|
+
out(`components ${result.manifest.components.length}`);
|
|
202
|
+
out("status installed");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
case "rollback": {
|
|
206
|
+
const result = rollbackToPrevious({ home, trustedKeys: trustedKeys() });
|
|
207
|
+
banner();
|
|
208
|
+
out("");
|
|
209
|
+
out(`rollback ok → ${result.manifest.manifest_id}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
case "status": {
|
|
213
|
+
printStatus(collectStatus(home, { trustedKeys: trustedKeys() }));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
case "diagnose": {
|
|
217
|
+
const report = runDiagnose(home, { trustedKeys: trustedKeys() });
|
|
218
|
+
banner();
|
|
219
|
+
out("");
|
|
220
|
+
for (const check of report.checks) {
|
|
221
|
+
out(`${check.status.padEnd(4)} ${check.check}${check.detail ? ` — ${check.detail}` : ""}`);
|
|
222
|
+
}
|
|
223
|
+
out("");
|
|
224
|
+
out(report.ok ? "DIAGNOSE PASS" : "DIAGNOSE FAIL");
|
|
225
|
+
if (!report.ok) process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
case "start": {
|
|
229
|
+
const result = startService(home, { port: flags.port ? Number(flags.port) : null });
|
|
230
|
+
out(result.alreadyRunning ? "already running" : `started (pid ${result.pid})`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
case "stop": {
|
|
234
|
+
const result = stopService(home);
|
|
235
|
+
out(result.wasRunning ? "stopped" : "was not running");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
case "uninstall": {
|
|
239
|
+
if (flags.yes !== true) throw new CliRefusal("UNINSTALL_REQUIRES_CONFIRMATION", "pass --yes");
|
|
240
|
+
const result = uninstall({ home, purgeConfig: flags["purge-config"] === true });
|
|
241
|
+
out(`uninstalled (customer config ${result.configRetained ? "retained" : "purged"})`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
case "config": {
|
|
245
|
+
if (positional[0] !== "set" || positional.length !== 3) {
|
|
246
|
+
throw new CliRefusal("CONFIG_USAGE", "observa config set <key> <value>");
|
|
247
|
+
}
|
|
248
|
+
if (isPairingOwnedConfigKey(positional[1])) {
|
|
249
|
+
throw new CliRefusal("HOSTED_CONFIG_PAIRING_OWNED", "use `observa pair`");
|
|
250
|
+
}
|
|
251
|
+
writeLocalConfigValue(home, positional[1], positional[2]);
|
|
252
|
+
// Never echo the value; secret keys are not even named back verbatim.
|
|
253
|
+
out(`config ${isSecretConfigKey(positional[1]) ? "secret " : ""}key set: ${positional[1]}`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case "pair": {
|
|
257
|
+
let result;
|
|
258
|
+
if (pairingRecoveryAvailable(home)) {
|
|
259
|
+
if (Object.keys(flags).length !== 0 || positional.length !== 0) {
|
|
260
|
+
throw new CliRefusal("PAIR_RECOVERY_FLAGS_REFUSED", "run `observa pair` with no flags");
|
|
261
|
+
}
|
|
262
|
+
result = await rePairHosted({ home });
|
|
263
|
+
} else {
|
|
264
|
+
if (typeof flags["base-url"] !== "string") throw new CliRefusal("PAIR_BASE_URL_FLAG_REQUIRED");
|
|
265
|
+
const pairingCode = await pairingCodeFromInput(flags);
|
|
266
|
+
result = await pairHosted({
|
|
267
|
+
home,
|
|
268
|
+
baseUrl: flags["base-url"],
|
|
269
|
+
pairingCode,
|
|
270
|
+
...(typeof flags.deployment === "string" ? { deploymentId: flags.deployment } : {}),
|
|
271
|
+
...(typeof flags.name === "string" ? { installationName: flags.name } : {}),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
banner();
|
|
275
|
+
out("");
|
|
276
|
+
out(result.repaired ? "re-paired ok (installed identity preserved)" : "paired ok");
|
|
277
|
+
out(`installation ${result.installation_id}`);
|
|
278
|
+
if (result.credential_id) out(`credential id ${result.credential_id}`);
|
|
279
|
+
if (result.fingerprint) out(`fingerprint ${result.fingerprint}`);
|
|
280
|
+
out(`deployment ${result.deployment_id}`);
|
|
281
|
+
out(`hosted url ${result.hosted_base_url}`);
|
|
282
|
+
// The credential itself was stored owner-only and is never displayed.
|
|
283
|
+
out("credential stored locally (never displayed)");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
case "unpair": {
|
|
287
|
+
const wasRunning = serviceIsRunning(home);
|
|
288
|
+
if (wasRunning) stopService(home);
|
|
289
|
+
const result = await unpairHosted({ home });
|
|
290
|
+
out(`unpaired (hosted service ${wasRunning ? "stopped" : "was not running"}; ${result.revoked_credentials} Hosted credential(s) revoked; installed identity retained)`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
case "n8n-setup": {
|
|
294
|
+
if (Object.keys(flags).length !== 0 || positional.length !== 0) {
|
|
295
|
+
throw new CliRefusal("N8N_SETUP_FLAGS_REFUSED");
|
|
296
|
+
}
|
|
297
|
+
const result = prepareN8nHook(home);
|
|
298
|
+
out("n8n automatic observation configured");
|
|
299
|
+
out(`environment ${result.env_path}`);
|
|
300
|
+
out("workflow edits none");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
case "version":
|
|
304
|
+
case "--version": {
|
|
305
|
+
banner();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
case "help":
|
|
309
|
+
case "--help":
|
|
310
|
+
case "-h": {
|
|
311
|
+
// Asking for help is a successful invocation (exit 0); only an
|
|
312
|
+
// unrecognized command is an error. Reconciles the founder clean-install
|
|
313
|
+
// rehearsal finding of 2026-08-16 (`observa --help` once exited 2).
|
|
314
|
+
usage();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
default:
|
|
318
|
+
usage();
|
|
319
|
+
process.exitCode = command === undefined ? 0 : 2;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
main().catch((error) => {
|
|
324
|
+
if (error instanceof CliRefusal) {
|
|
325
|
+
err(`REFUSED ${error.code}${error.detail ? `: ${error.detail}` : ""}`);
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
err(`ERROR ${error.code ?? error.message}`);
|
|
330
|
+
process.exitCode = 1;
|
|
331
|
+
});
|