@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The CLI's closed vocabulary. This file is the structural statement of what
|
|
2
|
+
// the installer IS and IS NOT: an orchestrator of exact approved artifacts,
|
|
3
|
+
// with no policy, verification, enforcement, or decision surface. There is no
|
|
4
|
+
// ALLOW, no DENY, no APPROVE anywhere in this package's executable source —
|
|
5
|
+
// the authority scan in the test suite holds that closed.
|
|
6
|
+
|
|
7
|
+
export const CLI_ARTIFACT_ID = "observa-cli";
|
|
8
|
+
export const CLI_VERSION = "0.1.3";
|
|
9
|
+
export const CLI_AUTHORITY = "NONE";
|
|
10
|
+
export const CLI_POLICY_AUTHORITY = "NONE";
|
|
11
|
+
export const CLI_VERIFICATION_AUTHORITY = "NONE";
|
|
12
|
+
export const CLI_EXECUTION_AUTHORITY_OVER_CUSTOMER_APPS = "NONE";
|
|
13
|
+
|
|
14
|
+
// The only mode this CLI version knows how to install. Enforcement modes are
|
|
15
|
+
// not merely "off by default" — the word is not in the accepted vocabulary,
|
|
16
|
+
// so a manifest requesting one fails closed at schema validation.
|
|
17
|
+
export const SUPPORTED_MODES = Object.freeze(["SHADOW_ONLY"]);
|
|
18
|
+
|
|
19
|
+
export const SUPPORTED_MANIFEST_SCHEMA_VERSIONS = Object.freeze([1]);
|
|
20
|
+
export const SUPPORTED_RELEASE_CHANNELS = Object.freeze(["beta", "development"]);
|
|
21
|
+
|
|
22
|
+
export const MANIFEST_SCHEMA_ID = "observa.release.installation-manifest";
|
|
23
|
+
export const SIGNED_MANIFEST_SCHEMA_ID = "observa.release.signed-manifest";
|
|
24
|
+
export const REVOCATION_SCHEMA_ID = "observa.release.revocation";
|
|
25
|
+
export const SIGNED_REVOCATION_SCHEMA_ID = "observa.release.signed-revocation";
|
|
26
|
+
export const CATALOG_SCHEMA_ID = "observa.release.catalog";
|
|
27
|
+
export const STATE_SCHEMA_ID = "observa.cli.installation-state";
|
|
28
|
+
export const RELEASE_RECORD_SCHEMA_ID = "observa.cli.release-record";
|
|
29
|
+
|
|
30
|
+
export const COMPONENT_ROLES = Object.freeze([
|
|
31
|
+
"verification",
|
|
32
|
+
"runtime-adapter",
|
|
33
|
+
"integration-binding",
|
|
34
|
+
"transport-client",
|
|
35
|
+
"system-profile",
|
|
36
|
+
"domain-contract",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const HEALTHCARE_PACKAGE_NAME = "@mcphersonai/observa-domain-healthcare-h2";
|
|
40
|
+
export const HEALTHCARE_PROFILE_PREFIX = "healthcare-dev-";
|
|
41
|
+
export const GENERIC_DOMAIN_PACKAGE_NAME = "@mcphersonai/observa-domain-generic";
|
|
42
|
+
|
|
43
|
+
export const INSTALL_STATES = Object.freeze(["installed", "not_installed"]);
|
|
44
|
+
export const SERVICE_STATES = Object.freeze(["running", "stopped"]);
|
|
45
|
+
|
|
46
|
+
export const MAX_MANIFEST_COMPONENTS = 16;
|
|
47
|
+
export const MAX_SIGNED_DOCUMENT_BYTES = 256 * 1024;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"authority":{"governance_authority":"V7 (frozen) remains sole governance authority","installer_policy_authority":"NONE","installer_verification_authority":"NONE","npm_distribution_authority":"NONE"},"catalog_digest":"2d053df59e9acb1c8200ee5130e939920c9b6eb257af91f9656f86744521073b","catalog_generated_from_head":"a6c811cf5d3018a0f8b09af4592ec34649ce6aa0","cli_files":[{"git_blob":"9d306766ddb8f7d44dea49435fe4d3e08b740692","mode":"0644","path":"distribution/observa-cli/bin/observa-n8n-hosted.mjs","sha256":"70c520a6c4a2478a60b98bdc2018328c01d1c797c9048f664b507b4ee570632e"},{"git_blob":"d69a32fa5d699a33609503b3d882775504275976","mode":"0644","path":"distribution/observa-cli/bin/observa.mjs","sha256":"b2f67dcbf449e858933b5a19775adc8107bd66b6fc3ba2134e36b16eb892287a"},{"git_blob":"51f82379566f7ec1968f0fa44c70f2efa09e9230","mode":"0644","path":"distribution/observa-cli/integrations/n8n/observa-external-hook.cjs","sha256":"0caa044f718d6305d60d53a8fb76255b84c7d08ae70ecb7d285a8d1f37c7d3fd"},{"git_blob":"4f3d3658702916e155912240b31f9fa963c9e62b","mode":"0644","path":"distribution/observa-cli/keys/observa-beta-1.public.json","sha256":"e6f3353b323702df54e96210bd0f1363f89636d2c0b216057698b383ecf40c8c"},{"git_blob":"a515812743475a659f7372919159973cf4c7bc41","mode":"0644","path":"distribution/observa-cli/src/artifact.mjs","sha256":"31c5bf2495a237be149ab3b1bb72a0707d13f342bb438de1604dc06fe7faf71b"},{"git_blob":"fde8e88a13b35da13e02e22e195a80b816c0df88","mode":"0644","path":"distribution/observa-cli/src/config.mjs","sha256":"6935ff8d65887154f6e4527cb99e49dae88252f3f423dcc8e38a7a96b261af85"},{"git_blob":"8e35256ac3bc02fcebf6a79518fcaf7bf68f94ba","mode":"0644","path":"distribution/observa-cli/src/errors.mjs","sha256":"3ac787dccc3c6a09e7f800ba31e4e3f67fe21f160226a51ed36228d440503c7f"},{"git_blob":"7eca3575e16a8eb9cca4afd766df6b65341e7806","mode":"0644","path":"distribution/observa-cli/src/hosted-delivery.mjs","sha256":"8deccb19fca28ea4ebb84d2da43c346a216bf958444376a64b06a59336ec6138"},{"git_blob":"879bf71507eac6f56657f2f290fb103fc81911a7","mode":"0644","path":"distribution/observa-cli/src/index.mjs","sha256":"a89aeeeaa04f099949a40ea3b3cf678aa4114824042df661e1faa71d5b8ce97e"},{"git_blob":"f8fa583c57508f3aebe9964cc8bbbfe2be16c955","mode":"0644","path":"distribution/observa-cli/src/install.mjs","sha256":"2bccc8ca0bfd37382f00d032fa6dedce7ca2b6819019ab5d4c97e8995e58d1bc"},{"git_blob":"1c83ab09b41990dd7553bd9bd95d0757af4ea8db","mode":"0644","path":"distribution/observa-cli/src/lock.mjs","sha256":"0dffd6446a9428fc9f3e45e468988a9b13cfc99a5ad3179a1f1b01029bc5cfbc"},{"git_blob":"1f53fa37d4174bbd3840e80be3db5aa7ecfb760a","mode":"0644","path":"distribution/observa-cli/src/manifest-schema.mjs","sha256":"eccce81a4bd746805d31ee3a270dfd34083a40746293b1307f58288d0d8d79d0"},{"git_blob":"3657a1369a73b68954e248145ed1b38d1ecac69a","mode":"0644","path":"distribution/observa-cli/src/manifest-verify.mjs","sha256":"94dd7980032fe70636ef96e089cd96aba2ac1307a369ae7ed52aa3c04267f91b"},{"git_blob":"06d80e0dcb42db6bbce6758d58f941b782ee76f4","mode":"0644","path":"distribution/observa-cli/src/n8n-hook.mjs","sha256":"6ae519f0e59dccd14a6bc6655c65846b1b9084bf28f720b9a5c24bba563d05a0"},{"git_blob":"bcf8d30926565ae118aea3ac2adcce0420aa5371","mode":"0644","path":"distribution/observa-cli/src/pair.mjs","sha256":"ce2acc3d335467e7af2531a5950a34c3ed3dacdddb41601df20ff278efe7da63"},{"git_blob":"5f747b38dd5cf7d5b33659399fc684d73184a5b9","mode":"0644","path":"distribution/observa-cli/src/re-pair.mjs","sha256":"3ccc90a750ae941d9dffd7f8e19be9c0cc404016ee3f442fe5a8ee43abcf9664"},{"git_blob":"04df25dd55dc4bad932a736fb8201e6894dbfe1d","mode":"0644","path":"distribution/observa-cli/src/service.mjs","sha256":"01fc22e4fc893f58d5eef11d726b4044fe2f1ad7430d9acb322e964b60d246e1"},{"git_blob":"784a93ab8d037951fcf716e3afb11c6bac36189c","mode":"0644","path":"distribution/observa-cli/src/state.mjs","sha256":"07258afc26ec033d471af3a4bca60c1f2c74d898d55d295baa52a5eceaaf2073"},{"git_blob":"a46341fd73c2e657ec04065b569057df0857798c","mode":"0644","path":"distribution/observa-cli/src/status.mjs","sha256":"09dfc6c6e8668c8be4089c0bb6f85c49afdd54479489860c56b03198f011d03e"},{"git_blob":"f376d51a08765ce7d1d27945e6e2ed707ac112b2","mode":"0644","path":"distribution/observa-cli/src/vocabulary.mjs","sha256":"5077b323eef9aab9fcb71aae5dd3b5e8ca15ea9b71a489068c2df109da77c2f2"},{"git_blob":"851da53215dd32eaf2e7f0ebeaac6a9ec34d5b58","mode":"0644","path":"runtime-adapters/n8n/src/strict-json.mjs","sha256":"d3e88b83d1226878130e9fbfbd33fd29634d4cb0c49b4d6a2b6dbc9ebbdb63e1","source_commit":"39918d5ea57604375e27ce44b6303aaade9cb81a"},{"git_blob":"f0734ab1704ea0dfd306ca253b5f14772b47e7b3","mode":"0644","path":"sdk/contracts/canonical.mjs","sha256":"b28e8f6a53fe945ff1e2610a1f9d333aa9fa62ffe375e253aeadaed74a5eb72d","source_commit":"26e968240b7cb3c560064551213ca756ae89a2ca"},{"git_blob":"5cf63fb8ecd6d23037c5c93c6d484dc31f57ffd4","mode":"0644","path":"sdk/contracts/entry-boundary.mjs","sha256":"78aa76d66bf8606c2bcfe5e50004139fe8c2168fc3a9b653534fc41f2471a4e9","source_commit":"26e968240b7cb3c560064551213ca756ae89a2ca"},{"git_blob":"91463a257273ec73d66a25f46dc2348a85d0f452","mode":"0644","path":"sdk/contracts/errors.mjs","sha256":"d5aa1bbe72f3fa4ba9edf47ad7dc2113df3877fa852251221c8c0abee60545d7","source_commit":"26e968240b7cb3c560064551213ca756ae89a2ca"},{"git_blob":"07eb2a73377dd13b7fe61ae437c9706bd092674d","mode":"0644","path":"sdk/contracts/stable-primitives.mjs","sha256":"88da8b531a26fc8d1b7d68f622e1e135b0addbf75057fd161b773cb449044c87","source_commit":"26e968240b7cb3c560064551213ca756ae89a2ca"}],"cli_source":{"git_path":"distribution/observa-cli","tree":"f24591a0600055630847eda5a0f4c448abb8b1de"},"components":[{"artifact_filename":"mcphersonai-observa-node-0.1.0.tgz","artifact_sha256":"00e96d00c9ed9d8d3ca98cde4b9e14c8eecc9adc360174afb7ca95f03db61e3c","component_id":"observa-node-h1","content_digest":"ee13b4ca5e9cef4b544dd60c8085c645788d89e614b8fd742890564b64850171","package_name":"@mcphersonai/observa-node","package_version":"0.1.0","path":"artifacts/mcphersonai-observa-node-0.1.0.tgz","role":"verification","source_subtrees":[{"commit":"26e968240b7cb3c560064551213ca756ae89a2ca","git_path":"sdk/contracts","tree":"0a1e1b09b76772a0e8e1b265bfc577aef52259b5"},{"commit":"26e968240b7cb3c560064551213ca756ae89a2ca","git_path":"verification-node","tree":"9e1014c710826aee41ce75808b0079fc754318b3"}]},{"artifact_filename":"mcphersonai-observa-adapter-n8n-0.1.0.tgz","artifact_sha256":"daf644c3cc22f4b588af1f242dd02dd0946b6cfa869db1466da33dfb235871d0","component_id":"observa-adapter-n8n","content_digest":"8b0c33d62cd8a39cc962b03cae80e82ee08ef2daafeb676a5810da6427c1f63c","package_name":"@mcphersonai/observa-adapter-n8n","package_version":"0.1.0","path":"artifacts/mcphersonai-observa-adapter-n8n-0.1.0.tgz","role":"runtime-adapter","source_subtrees":[{"commit":"39918d5ea57604375e27ce44b6303aaade9cb81a","git_path":"runtime-adapters/n8n","tree":"a68bb0b5c062589effe907eb666ee45e0f484551"}]},{"artifact_filename":"mcphersonai-observa-n8n-h1-binding-0.1.0.tgz","artifact_sha256":"5f4bc40bf964255491a21c066d1578b9867b892689c609e4475c53210f7b6b72","component_id":"observa-n8n-h1-binding","content_digest":"d6fb3d97e9ca6d5f06feaa1abc12e0cc9ca904c8b4a00c304dbcadc6f315fec0","package_name":"@mcphersonai/observa-n8n-h1-binding","package_version":"0.1.0","path":"artifacts/mcphersonai-observa-n8n-h1-binding-0.1.0.tgz","role":"integration-binding","source_subtrees":[{"commit":"cb389b929e9f97fbfb47957615aaf58556db8ea8","git_path":"integrations/n8n-h1","tree":"569247d6084d6e343c4d177d10e7024864be425e"}]},{"artifact_filename":"mcphersonai-observa-domain-generic-0.1.0.tgz","artifact_sha256":"7f326e4b744f8eeaef7d12d781d82a6f8cf8cea5327ed4c9b22c180bd0ab5e04","component_id":"observa-domain-generic","content_digest":"9a839ee4ba687d6a7b531e1a2c318a52b91d4beea250d9d360e04df180a3944c","package_name":"@mcphersonai/observa-domain-generic","package_version":"0.1.0","path":"artifacts/mcphersonai-observa-domain-generic-0.1.0.tgz","role":"domain-contract","source_subtrees":[{"commit":"f73c53a26c9b77e151b4fffe89b657ae31ddd2cb","git_path":"domains/generic","tree":"92a014bb77e41812bc55a79a8a72837ec6d74537"}]},{"artifact_filename":"mcphersonai-observa-hosted-transport-0.1.0.tgz","artifact_sha256":"b6f2ccbbb0526aa08686c8725dd5d2e78aee58e607f15e380170c057a08881e8","component_id":"observa-hosted-transport-client","content_digest":"ca3b9cb0246a9f722138bd446ef1bfb7df42b1981065feb2a551cd42c534f007","package_name":"@mcphersonai/observa-hosted-transport","package_version":"0.1.0","path":"artifacts/mcphersonai-observa-hosted-transport-0.1.0.tgz","role":"transport-client","source_subtrees":[{"commit":"199dd6a87209de625915bad7cf63a359d6d2ff05","git_path":"transport/hosted","tree":"b01763f89bc1fa238fe238e1f47a471b94fb933c"}]}],"dist_tag":"beta","distribution":{"access":"public","dist_tag":"beta","prepared_only":true,"provenance_attestation":"UNAVAILABLE_PRIVATE_SOURCE","published":false,"registry":"https://registry.npmjs.org/","trusted_publishing":"PREPARED_NOT_ACTIVATED"},"files":[{"mode":"0644","path":"README.md","sha256":"757c317514d4082617a6d8be6a726e744561fc992ec7d57873c3a1029726f869","size_bytes":4458},{"mode":"0644","path":"artifacts/mcphersonai-observa-adapter-n8n-0.1.0.tgz","sha256":"daf644c3cc22f4b588af1f242dd02dd0946b6cfa869db1466da33dfb235871d0","size_bytes":72591},{"mode":"0644","path":"artifacts/mcphersonai-observa-domain-generic-0.1.0.tgz","sha256":"7f326e4b744f8eeaef7d12d781d82a6f8cf8cea5327ed4c9b22c180bd0ab5e04","size_bytes":7558},{"mode":"0644","path":"artifacts/mcphersonai-observa-hosted-transport-0.1.0.tgz","sha256":"b6f2ccbbb0526aa08686c8725dd5d2e78aee58e607f15e380170c057a08881e8","size_bytes":14008},{"mode":"0644","path":"artifacts/mcphersonai-observa-n8n-h1-binding-0.1.0.tgz","sha256":"5f4bc40bf964255491a21c066d1578b9867b892689c609e4475c53210f7b6b72","size_bytes":10441},{"mode":"0644","path":"artifacts/mcphersonai-observa-node-0.1.0.tgz","sha256":"00e96d00c9ed9d8d3ca98cde4b9e14c8eecc9adc360174afb7ca95f03db61e3c","size_bytes":106145},{"mode":"0644","path":"distribution/observa-cli/bin/observa-n8n-hosted.mjs","sha256":"70c520a6c4a2478a60b98bdc2018328c01d1c797c9048f664b507b4ee570632e","size_bytes":5170},{"mode":"0644","path":"distribution/observa-cli/bin/observa.mjs","sha256":"b2f67dcbf449e858933b5a19775adc8107bd66b6fc3ba2134e36b16eb892287a","size_bytes":13217},{"mode":"0644","path":"distribution/observa-cli/integrations/n8n/observa-external-hook.cjs","sha256":"0caa044f718d6305d60d53a8fb76255b84c7d08ae70ecb7d285a8d1f37c7d3fd","size_bytes":5374},{"mode":"0644","path":"distribution/observa-cli/keys/observa-beta-1.public.json","sha256":"e6f3353b323702df54e96210bd0f1363f89636d2c0b216057698b383ecf40c8c","size_bytes":224},{"mode":"0644","path":"distribution/observa-cli/src/artifact.mjs","sha256":"31c5bf2495a237be149ab3b1bb72a0707d13f342bb438de1604dc06fe7faf71b","size_bytes":5890},{"mode":"0644","path":"distribution/observa-cli/src/config.mjs","sha256":"6935ff8d65887154f6e4527cb99e49dae88252f3f423dcc8e38a7a96b261af85","size_bytes":6279},{"mode":"0644","path":"distribution/observa-cli/src/errors.mjs","sha256":"3ac787dccc3c6a09e7f800ba31e4e3f67fe21f160226a51ed36228d440503c7f","size_bytes":738},{"mode":"0644","path":"distribution/observa-cli/src/hosted-delivery.mjs","sha256":"8deccb19fca28ea4ebb84d2da43c346a216bf958444376a64b06a59336ec6138","size_bytes":6433},{"mode":"0644","path":"distribution/observa-cli/src/index.mjs","sha256":"a89aeeeaa04f099949a40ea3b3cf678aa4114824042df661e1faa71d5b8ce97e","size_bytes":1445},{"mode":"0644","path":"distribution/observa-cli/src/install.mjs","sha256":"2bccc8ca0bfd37382f00d032fa6dedce7ca2b6819019ab5d4c97e8995e58d1bc","size_bytes":18270},{"mode":"0644","path":"distribution/observa-cli/src/lock.mjs","sha256":"0dffd6446a9428fc9f3e45e468988a9b13cfc99a5ad3179a1f1b01029bc5cfbc","size_bytes":2070},{"mode":"0644","path":"distribution/observa-cli/src/manifest-schema.mjs","sha256":"eccce81a4bd746805d31ee3a270dfd34083a40746293b1307f58288d0d8d79d0","size_bytes":10092},{"mode":"0644","path":"distribution/observa-cli/src/manifest-verify.mjs","sha256":"94dd7980032fe70636ef96e089cd96aba2ac1307a369ae7ed52aa3c04267f91b","size_bytes":5034},{"mode":"0644","path":"distribution/observa-cli/src/n8n-hook.mjs","sha256":"6ae519f0e59dccd14a6bc6655c65846b1b9084bf28f720b9a5c24bba563d05a0","size_bytes":2916},{"mode":"0644","path":"distribution/observa-cli/src/pair.mjs","sha256":"ce2acc3d335467e7af2531a5950a34c3ed3dacdddb41601df20ff278efe7da63","size_bytes":6266},{"mode":"0644","path":"distribution/observa-cli/src/re-pair.mjs","sha256":"3ccc90a750ae941d9dffd7f8e19be9c0cc404016ee3f442fe5a8ee43abcf9664","size_bytes":6349},{"mode":"0644","path":"distribution/observa-cli/src/service.mjs","sha256":"01fc22e4fc893f58d5eef11d726b4044fe2f1ad7430d9acb322e964b60d246e1","size_bytes":9623},{"mode":"0644","path":"distribution/observa-cli/src/state.mjs","sha256":"07258afc26ec033d471af3a4bca60c1f2c74d898d55d295baa52a5eceaaf2073","size_bytes":3573},{"mode":"0644","path":"distribution/observa-cli/src/status.mjs","sha256":"09dfc6c6e8668c8be4089c0bb6f85c49afdd54479489860c56b03198f011d03e","size_bytes":7928},{"mode":"0644","path":"distribution/observa-cli/src/vocabulary.mjs","sha256":"5077b323eef9aab9fcb71aae5dd3b5e8ca15ea9b71a489068c2df109da77c2f2","size_bytes":2236},{"mode":"0644","path":"package.json","sha256":"2022669eda7348bcccbbfc69025cadf99df0f5a97c7b8a756499bbbf53d3cd26","size_bytes":979},{"mode":"0644","path":"runtime-adapters/n8n/src/strict-json.mjs","sha256":"d3e88b83d1226878130e9fbfbd33fd29634d4cb0c49b4d6a2b6dbc9ebbdb63e1","size_bytes":5590},{"mode":"0644","path":"sdk/contracts/canonical.mjs","sha256":"b28e8f6a53fe945ff1e2610a1f9d333aa9fa62ffe375e253aeadaed74a5eb72d","size_bytes":14109},{"mode":"0644","path":"sdk/contracts/entry-boundary.mjs","sha256":"78aa76d66bf8606c2bcfe5e50004139fe8c2168fc3a9b653534fc41f2471a4e9","size_bytes":22747},{"mode":"0644","path":"sdk/contracts/errors.mjs","sha256":"d5aa1bbe72f3fa4ba9edf47ad7dc2113df3877fa852251221c8c0abee60545d7","size_bytes":13626},{"mode":"0644","path":"sdk/contracts/stable-primitives.mjs","sha256":"88da8b531a26fc8d1b7d68f622e1e135b0addbf75057fd161b773cb449044c87","size_bytes":5753}],"installation_flow":"npm delivers bytes only; installation requires a signed installation manifest verified by the shipped CLI against the shipped trusted public keys","min_cli_version":"0.1.2","mode":"SHADOW_ONLY","package_name":"@mcpherson-ai/observa-local-node","package_version":"0.1.3","product":"Observa Local Node","profile_id":"observa-local-node-n8n-generic-shadow-beta","record_digest":"62024672c8db9dd8ebfd21f3b51a75d317575d31ae6cc8d3407ad7665db29e4e","release_channel":"beta","schema":"observa.release.npm-distribution","schema_version":1}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcpherson-ai/observa-local-node",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Observa Local Node — SHADOW-mode runtime observation and verification for supported workflow runtimes. Installer/orchestrator CLI plus the exact approved component artifacts it composes. Authority NONE; enforcement OFF; invite-only beta.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"exports": {},
|
|
11
|
+
"bin": {
|
|
12
|
+
"observa": "./distribution/observa-cli/bin/observa.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"README.md",
|
|
16
|
+
"SHA256SUMS",
|
|
17
|
+
"artifacts",
|
|
18
|
+
"distribution",
|
|
19
|
+
"npm-distribution-provenance.json",
|
|
20
|
+
"runtime-adapters",
|
|
21
|
+
"sdk"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org/",
|
|
26
|
+
"tag": "beta",
|
|
27
|
+
"provenance": false
|
|
28
|
+
},
|
|
29
|
+
"observa": {
|
|
30
|
+
"product": "Observa Local Node",
|
|
31
|
+
"authority": "NONE",
|
|
32
|
+
"enforcement": "OFF",
|
|
33
|
+
"mode": "SHADOW_ONLY",
|
|
34
|
+
"release_channel": "beta"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Strict JSON parsing — rejects duplicate object keys before decoding.
|
|
2
|
+
//
|
|
3
|
+
// Ported from the sealed platform's src/util/strict-json.mjs, which in turn
|
|
4
|
+
// notes its own origin in services/governance-api/strict-json.mjs (frozen v0.6
|
|
5
|
+
// base bbd3e563). Carried rather than imported for the reason the whole package
|
|
6
|
+
// is standalone: this artifact ships to a customer's n8n host and must not drag
|
|
7
|
+
// the governance platform along. The frozen copy is NOT modified by this file.
|
|
8
|
+
//
|
|
9
|
+
// WHY IT MATTERS HERE
|
|
10
|
+
// -------------------
|
|
11
|
+
// `JSON.parse` keeps the LAST value for a duplicated key, so
|
|
12
|
+
// `{"phase":"ATTEMPT","phase":"COMPLETION"}` parses to a completion — while
|
|
13
|
+
// anything that inspected the raw text, including a human reading a log, sees an
|
|
14
|
+
// attempt. Against a closed contract that is a way to smuggle a value past a
|
|
15
|
+
// reviewer's eye, and against the correlation ledger it is a way to make two
|
|
16
|
+
// different events hash the same. Refused before decoding rather than
|
|
17
|
+
// reconciled after.
|
|
18
|
+
//
|
|
19
|
+
// THE ESCAPE-DOMAIN DEFECT (Independent Review #1, N8N-IR1-04)
|
|
20
|
+
// -----------------------------------------------------------
|
|
21
|
+
// The ported scanner compared keys as they appeared in the SOURCE TEXT: it
|
|
22
|
+
// copied `\` plus the following character verbatim into the key it accumulated.
|
|
23
|
+
// `JSON.parse` compares them DECODED. Those are different domains, and the gap
|
|
24
|
+
// is exactly the smuggling channel the check exists to close:
|
|
25
|
+
//
|
|
26
|
+
// {"phase":"ATTEMPT","phase":"COMPLETION"}
|
|
27
|
+
//
|
|
28
|
+
// Two distinct source strings, one decoded key, no duplicate reported, and
|
|
29
|
+
// `JSON.parse` returns `phase: "COMPLETION"`. Same for `\"` vs `"`, for
|
|
30
|
+
// `\/` vs `/`, and for any pair of differently-escaped spellings.
|
|
31
|
+
//
|
|
32
|
+
// The fix is to compare in the domain the decision is made in. `readString`
|
|
33
|
+
// below DECODES — the full escape set including surrogate pairs — so the
|
|
34
|
+
// duplicate set holds exactly the keys `JSON.parse` will produce. A scanner that
|
|
35
|
+
// agrees with the parser on which keys exist is the only kind worth having.
|
|
36
|
+
|
|
37
|
+
/** Whitespace the JSON grammar permits between tokens. */
|
|
38
|
+
const WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
39
|
+
|
|
40
|
+
/** The single-character escapes JSON defines. */
|
|
41
|
+
const SIMPLE_ESCAPES = Object.freeze({
|
|
42
|
+
'"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export function assertNoDuplicateJsonKeys(text) {
|
|
46
|
+
const stack = [];
|
|
47
|
+
let i = 0;
|
|
48
|
+
const n = text.length;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read one JSON string starting at `text[i]`, returning its DECODED value.
|
|
52
|
+
*
|
|
53
|
+
* Malformed escapes throw rather than being passed through: a scanner that
|
|
54
|
+
* shrugged at bad input would disagree with `JSON.parse` about what the key
|
|
55
|
+
* is, which is the whole defect being repaired.
|
|
56
|
+
*/
|
|
57
|
+
const readString = () => {
|
|
58
|
+
let out = "";
|
|
59
|
+
i += 1; // opening quote
|
|
60
|
+
while (i < n) {
|
|
61
|
+
const c = text[i];
|
|
62
|
+
if (c === "\\") {
|
|
63
|
+
const escape = text[i + 1];
|
|
64
|
+
if (escape === undefined) throw new SyntaxError("unterminated_escape");
|
|
65
|
+
if (escape === "u") {
|
|
66
|
+
const hex = text.slice(i + 2, i + 6);
|
|
67
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new SyntaxError("invalid_unicode_escape");
|
|
68
|
+
const code = Number.parseInt(hex, 16);
|
|
69
|
+
i += 6;
|
|
70
|
+
// Surrogate pairs must be joined here, or "😀" would decode
|
|
71
|
+
// to two lone surrogates while JSON.parse produces one astral
|
|
72
|
+
// character — a third way for two spellings to look distinct.
|
|
73
|
+
if (code >= 0xd800 && code <= 0xdbff && text[i] === "\\" && text[i + 1] === "u") {
|
|
74
|
+
const lowHex = text.slice(i + 2, i + 6);
|
|
75
|
+
if (/^[0-9a-fA-F]{4}$/.test(lowHex)) {
|
|
76
|
+
const low = Number.parseInt(lowHex, 16);
|
|
77
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
78
|
+
out += String.fromCharCode(code, low);
|
|
79
|
+
i += 6;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
out += String.fromCharCode(code);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!Object.hasOwn(SIMPLE_ESCAPES, escape)) throw new SyntaxError("invalid_escape");
|
|
88
|
+
out += SIMPLE_ESCAPES[escape];
|
|
89
|
+
i += 2;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (c === '"') {
|
|
93
|
+
i += 1;
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
out += c;
|
|
97
|
+
i += 1;
|
|
98
|
+
}
|
|
99
|
+
throw new SyntaxError("unterminated_string");
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
while (i < n) {
|
|
103
|
+
const c = text[i];
|
|
104
|
+
if (WHITESPACE.has(c)) { i += 1; continue; }
|
|
105
|
+
if (c === "{") { stack.push({ keys: new Set(), expectKey: true }); i += 1; continue; }
|
|
106
|
+
if (c === "[") { stack.push({ array: true }); i += 1; continue; }
|
|
107
|
+
if (c === "}" || c === "]") { stack.pop(); i += 1; continue; }
|
|
108
|
+
if (c === '"') {
|
|
109
|
+
const top = stack[stack.length - 1];
|
|
110
|
+
if (top && !top.array && top.expectKey) {
|
|
111
|
+
const key = readString();
|
|
112
|
+
// The key is compared decoded, and NOT included in the thrown message:
|
|
113
|
+
// a duplicate key is caller-controlled text, and an exception message is
|
|
114
|
+
// the shortest path from caller input to somebody's log file.
|
|
115
|
+
if (top.keys.has(key)) throw new SyntaxError("duplicate_key");
|
|
116
|
+
top.keys.add(key);
|
|
117
|
+
top.expectKey = false; // next expect ':'
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
readString();
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (c === ":") { i += 1; continue; }
|
|
124
|
+
if (c === ",") {
|
|
125
|
+
const top = stack[stack.length - 1];
|
|
126
|
+
if (top && !top.array) top.expectKey = true;
|
|
127
|
+
i += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
i += 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function parseStrictJson(text) {
|
|
135
|
+
if (typeof text !== "string") throw new SyntaxError("not_a_string");
|
|
136
|
+
assertNoDuplicateJsonKeys(text);
|
|
137
|
+
return JSON.parse(text);
|
|
138
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
// ROUND-2 REVIEW #1, H1R2-01. Every reflection primitive in this file runs
|
|
3
|
+
// CALLER CODE. Wrapping the walk in `refuseOnRawThrow` closed the raw-throw
|
|
4
|
+
// half only: a trap raising a publicly constructed `SdkContractError` was
|
|
5
|
+
// rethrown unchanged, carrying the caller's text out of `canonicalDigest`,
|
|
6
|
+
// `cloneAndFreeze`, `snapshotRecord` and therefore nearly every validator here.
|
|
7
|
+
// Each trap-invoking call now runs inside `runUntrusted`; the refusals this
|
|
8
|
+
// file raises deliberately sit outside those regions and keep their codes.
|
|
9
|
+
import { entryArray } from "./entry-boundary.mjs";
|
|
10
|
+
import { refuse, runUntrusted } from "./errors.mjs";
|
|
11
|
+
import { stableSortedStringCopy } from "./stable-primitives.mjs";
|
|
12
|
+
|
|
13
|
+
const arrayIsArray = Array.isArray;
|
|
14
|
+
const objectFreeze = Object.freeze;
|
|
15
|
+
const objectKeys = Object.keys;
|
|
16
|
+
|
|
17
|
+
// Bounds exist so a hostile oversized structure is refused before it is
|
|
18
|
+
// walked, stored, or hashed. They are contract limits, not tuning knobs.
|
|
19
|
+
const MAX_DEPTH = 32;
|
|
20
|
+
const MAX_NODES = 50_000;
|
|
21
|
+
const MAX_STRING_LENGTH = 65_536;
|
|
22
|
+
|
|
23
|
+
// Validates a record AND RETURNS THE ENTRIES IT VALIDATED.
|
|
24
|
+
//
|
|
25
|
+
// ROUND 12B — THE VALIDATOR AND THE COPY READ THE SAME OBJECT TWICE.
|
|
26
|
+
// `assertDataRecord` read `Reflect.ownKeys(value)`, and then `normalize` read
|
|
27
|
+
// `Object.keys(value)` again to copy. Two reads of a caller-controlled key set,
|
|
28
|
+
// which is the exact validate-then-store desync this whole file exists to
|
|
29
|
+
// prevent — and a Proxy with a STATEFUL `ownKeys` trap walked straight through
|
|
30
|
+
// it: the validator was shown `["safe"]`, the copy loop was shown
|
|
31
|
+
// `["safe", "constructor"]`, and `snapshotRecord` returned a frozen clone
|
|
32
|
+
// carrying an own `constructor` key the unsafe-key guard had never seen.
|
|
33
|
+
//
|
|
34
|
+
// The same applied one level down: the validator inspected each property's
|
|
35
|
+
// DESCRIPTOR and the copy loop then read `value[key]`, so a `get` trap could
|
|
36
|
+
// answer the check with one value and the store with another.
|
|
37
|
+
//
|
|
38
|
+
// Returning the entries closes both. Everything downstream consumes what was
|
|
39
|
+
// validated, and the caller's object is never consulted again. Sorted here for
|
|
40
|
+
// the same reason the copy loop sorted: canonical form must not depend on
|
|
41
|
+
// insertion order.
|
|
42
|
+
//
|
|
43
|
+
// FINAL CLOSURE, INDEPENDENT REVIEW #1 — THE BOUND MUST PRECEDE THE SORT.
|
|
44
|
+
//
|
|
45
|
+
// This file's header states: "Bounds exist so a hostile oversized structure is
|
|
46
|
+
// refused before it is walked, stored, or hashed." It was walked and sorted
|
|
47
|
+
// first. `MAX_NODES` is consumed one node per `normalize()` call, and
|
|
48
|
+
// `normalize` calls this function BEFORE recursing into any child — so the
|
|
49
|
+
// O(n^2) insertion sort below ran to completion on an object of ANY width, and
|
|
50
|
+
// only then did the child loop refuse `VALUE_TOO_LARGE`.
|
|
51
|
+
//
|
|
52
|
+
// Measured on this worktree: 20 000 own keys accepted in 418 ms, 50 000 refused
|
|
53
|
+
// after 3.4 s, 100 000 refused after 15.9 s — all synchronous, on a
|
|
54
|
+
// single-threaded node, for input that is otherwise entirely legal (under
|
|
55
|
+
// MAX_DEPTH, under MAX_NODES, under MAX_STRING_LENGTH) and reachable straight
|
|
56
|
+
// through `recordReadback({ observedFacts })`. The refusal was correct; paying
|
|
57
|
+
// quadratic time to reach it was not.
|
|
58
|
+
//
|
|
59
|
+
// Bounding breadth against the REMAINING budget before the sort makes the
|
|
60
|
+
// header's claim true. It is conservative and sound: every own key of a plain
|
|
61
|
+
// record becomes a child that would consume one node anyway, so the pre-check
|
|
62
|
+
// can never refuse a structure the node budget would have admitted.
|
|
63
|
+
//
|
|
64
|
+
// The bound belongs HERE and not on the array path: an array's own keys carry a
|
|
65
|
+
// `length` that is not an element, so the same arithmetic would refuse an array
|
|
66
|
+
// sitting exactly ON the budget — and arrays are walked linearly anyway, with no
|
|
67
|
+
// sort to pay for. The quadratic cost this closes only ever existed here.
|
|
68
|
+
// A stable, allocation-explicit merge sort over [key, value] pairs, ordering by
|
|
69
|
+
// key with the same `>` comparison the previous insertion sort used.
|
|
70
|
+
function sortEntriesByKey(entries) {
|
|
71
|
+
const length = entries.length;
|
|
72
|
+
if (length < 2) return entries;
|
|
73
|
+
let source = entries;
|
|
74
|
+
let target = new Array(length);
|
|
75
|
+
for (let width = 1; width < length; width *= 2) {
|
|
76
|
+
for (let start = 0; start < length; start += width * 2) {
|
|
77
|
+
let left = start;
|
|
78
|
+
const leftEnd = Math.min(start + width, length);
|
|
79
|
+
let right = leftEnd;
|
|
80
|
+
const rightEnd = Math.min(start + width * 2, length);
|
|
81
|
+
let cursor = start;
|
|
82
|
+
while (left < leftEnd && right < rightEnd) {
|
|
83
|
+
target[cursor] = source[right][0] < source[left][0] ? source[right] : source[left];
|
|
84
|
+
if (source[right][0] < source[left][0]) right += 1; else left += 1;
|
|
85
|
+
cursor += 1;
|
|
86
|
+
}
|
|
87
|
+
while (left < leftEnd) { target[cursor] = source[left]; left += 1; cursor += 1; }
|
|
88
|
+
while (right < rightEnd) { target[cursor] = source[right]; right += 1; cursor += 1; }
|
|
89
|
+
}
|
|
90
|
+
const swap = source; source = target; target = swap;
|
|
91
|
+
}
|
|
92
|
+
return source;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function dataRecordEntries(value, budget) {
|
|
96
|
+
const prototype = runUntrusted("VALUE_ACCESS_REFUSED", () => Object.getPrototypeOf(value));
|
|
97
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
98
|
+
refuse("VALUE_NOT_PLAIN");
|
|
99
|
+
}
|
|
100
|
+
const entries = [];
|
|
101
|
+
const keys = runUntrusted("VALUE_ACCESS_REFUSED", () => Reflect.ownKeys(value));
|
|
102
|
+
if (budget.nodes + keys.length > MAX_NODES) refuse("VALUE_TOO_LARGE");
|
|
103
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
104
|
+
const key = keys[index];
|
|
105
|
+
if (typeof key !== "string" || key === "__proto__" || key === "prototype" || key === "constructor") {
|
|
106
|
+
refuse("VALUE_UNSAFE_KEY");
|
|
107
|
+
}
|
|
108
|
+
const descriptor = runUntrusted(
|
|
109
|
+
"VALUE_ACCESS_REFUSED",
|
|
110
|
+
() => Object.getOwnPropertyDescriptor(value, key),
|
|
111
|
+
);
|
|
112
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) {
|
|
113
|
+
refuse("VALUE_ACCESSOR_REFUSED");
|
|
114
|
+
}
|
|
115
|
+
entries[entries.length] = [key, descriptor.value];
|
|
116
|
+
}
|
|
117
|
+
// `Array#sort` is inherited mutable ambient behavior. These pairs are
|
|
118
|
+
// contract-owned, so they are sorted directly instead of dispatched.
|
|
119
|
+
//
|
|
120
|
+
// FINAL CLOSURE: this was an insertion sort, which is O(n^2). Own keys are
|
|
121
|
+
// unique, so the ordering is a total order and merge sort produces the
|
|
122
|
+
// BYTE-IDENTICAL canonical form at O(n log n) — the breadth bound above makes
|
|
123
|
+
// an oversized record cheap to refuse, and this makes an in-bounds wide record
|
|
124
|
+
// cheap to accept. Still no dispatch through a mutable prototype.
|
|
125
|
+
return sortEntriesByKey(entries);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// An array's own keys must be plain indices holding data properties. A
|
|
129
|
+
// caller-supplied `map` (or any other own method) would otherwise be
|
|
130
|
+
// dispatched by normalization itself, returning a value that was never
|
|
131
|
+
// normalized, bounds-checked, or key-checked.
|
|
132
|
+
// Same discipline for arrays: validate and CAPTURE in one pass.
|
|
133
|
+
//
|
|
134
|
+
// The copy loop used to read `value.length` and then `value[index]` after
|
|
135
|
+
// validation — two more reads of a caller-controlled object, so a Proxy could
|
|
136
|
+
// grow, shrink or re-answer its elements between the check and the copy.
|
|
137
|
+
// Elements are returned in index order, which is the only order an array has.
|
|
138
|
+
function arrayElements(value) {
|
|
139
|
+
// Plain arrays only. A subclass could otherwise carry a prototype method
|
|
140
|
+
// that normalization might dispatch through.
|
|
141
|
+
if (runUntrusted("VALUE_ACCESS_REFUSED", () => Object.getPrototypeOf(value)) !== Array.prototype) {
|
|
142
|
+
refuse("VALUE_NOT_PLAIN");
|
|
143
|
+
}
|
|
144
|
+
const byIndex = new Map();
|
|
145
|
+
const keys = runUntrusted("VALUE_ACCESS_REFUSED", () => Reflect.ownKeys(value));
|
|
146
|
+
for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
|
|
147
|
+
const key = keys[keyIndex];
|
|
148
|
+
if (key === "length") continue;
|
|
149
|
+
if (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key)) {
|
|
150
|
+
refuse("VALUE_UNSAFE_KEY");
|
|
151
|
+
}
|
|
152
|
+
const descriptor = runUntrusted(
|
|
153
|
+
"VALUE_ACCESS_REFUSED",
|
|
154
|
+
() => Object.getOwnPropertyDescriptor(value, key),
|
|
155
|
+
);
|
|
156
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) {
|
|
157
|
+
refuse("VALUE_ACCESSOR_REFUSED");
|
|
158
|
+
}
|
|
159
|
+
byIndex.set(Number(key), descriptor.value);
|
|
160
|
+
}
|
|
161
|
+
// A sparse array is refused rather than silently filled with nulls. That was
|
|
162
|
+
// already the rule — a hole reads as `undefined`, which is not JSON
|
|
163
|
+
// compatible — and it keeps its established code now that the check is made
|
|
164
|
+
// against the captured keys instead of a second read of `length`.
|
|
165
|
+
const elements = new Array(byIndex.size);
|
|
166
|
+
for (let index = 0; index < byIndex.size; index += 1) {
|
|
167
|
+
if (!byIndex.has(index)) refuse("VALUE_NOT_JSON_COMPATIBLE");
|
|
168
|
+
elements[index] = byIndex.get(index);
|
|
169
|
+
}
|
|
170
|
+
return elements;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalize(value, seen, depth, budget) {
|
|
174
|
+
if (depth > MAX_DEPTH) refuse("VALUE_TOO_DEEP");
|
|
175
|
+
budget.nodes += 1;
|
|
176
|
+
if (budget.nodes > MAX_NODES) refuse("VALUE_TOO_LARGE");
|
|
177
|
+
if (value === null || typeof value === "boolean") return value;
|
|
178
|
+
if (typeof value === "string") {
|
|
179
|
+
if (value.length > MAX_STRING_LENGTH) refuse("VALUE_TOO_LARGE");
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
if (typeof value === "number") {
|
|
183
|
+
if (!Number.isFinite(value)) refuse("VALUE_NOT_JSON_COMPATIBLE");
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
if (typeof value !== "object") refuse("VALUE_NOT_JSON_COMPATIBLE");
|
|
187
|
+
if (seen.has(value)) refuse("VALUE_CYCLE");
|
|
188
|
+
seen.add(value);
|
|
189
|
+
let result;
|
|
190
|
+
// Both branches normalize the CAPTURED entries. The caller's object is not
|
|
191
|
+
// consulted again after validation, so there is no second read for a trap to
|
|
192
|
+
// answer differently.
|
|
193
|
+
if (runUntrusted("VALUE_ACCESS_REFUSED", () => arrayIsArray(value))) {
|
|
194
|
+
const elements = arrayElements(value);
|
|
195
|
+
result = new Array(elements.length);
|
|
196
|
+
for (let index = 0; index < elements.length; index += 1) {
|
|
197
|
+
result[index] = normalize(elements[index], seen, depth + 1, budget);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
result = Object.create(null);
|
|
201
|
+
const entries = dataRecordEntries(value, budget);
|
|
202
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
203
|
+
const [key, entry] = entries[index];
|
|
204
|
+
result[key] = normalize(entry, seen, depth + 1, budget);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
seen.delete(value);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// REVIEW #1, MAJOR-1 — THE SECOND SYSTEMIC LEVER.
|
|
212
|
+
//
|
|
213
|
+
// Normalization is where this package walks caller-controlled STRUCTURE, and
|
|
214
|
+
// walking structure runs traps: `Reflect.ownKeys`, `getOwnPropertyDescriptor`
|
|
215
|
+
// and `getPrototypeOf` are all interceptable, and on a REVOKED Proxy every one
|
|
216
|
+
// of them throws before any check in this file can run. That raw `TypeError`
|
|
217
|
+
// escaped every public export reachable from here — `canonicalDigest`,
|
|
218
|
+
// `cloneAndFreeze`, `snapshotRecord`, and therefore nearly every validator in
|
|
219
|
+
// the SDK — carrying whatever message the caller's trap chose to throw.
|
|
220
|
+
//
|
|
221
|
+
// Wrapping the walk closes all of them in one place. A refusal this file raised
|
|
222
|
+
// on purpose keeps its own `VALUE_*` code; anything else becomes
|
|
223
|
+
// `VALUE_ACCESS_REFUSED` and the original is dropped, so no caller text
|
|
224
|
+
// survives. `entry-boundary.mjs` states the invariant this serves.
|
|
225
|
+
export function canonicalJson(value) {
|
|
226
|
+
return JSON.stringify(normalize(value, new Set(), 0, { nodes: 0 }));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// INTEGRITY, NOT AUTHENTICITY. An unkeyed SHA-256 over a canonical encoding of
|
|
230
|
+
// public fields, and a public export of this SDK — so it proves that a record
|
|
231
|
+
// is the record that was constructed, and proves nothing whatever about WHO
|
|
232
|
+
// constructed it. Anyone who can fabricate a record can compute its digest.
|
|
233
|
+
//
|
|
234
|
+
// FOUNDER DECISION (2026-08-12): that is the intended scope. H1 adds no key
|
|
235
|
+
// management, and no caller may read a digest match as evidence of origin.
|
|
236
|
+
// Forgery is bounded instead by identity: ids are minted here, never supplied,
|
|
237
|
+
// from 32 random bytes (`identity.mjs`).
|
|
238
|
+
export function canonicalDigest(value) {
|
|
239
|
+
return createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function cloneAndFreeze(value) {
|
|
243
|
+
const clone = JSON.parse(canonicalJson(value));
|
|
244
|
+
const freeze = (item) => {
|
|
245
|
+
if (item && typeof item === "object") {
|
|
246
|
+
if (arrayIsArray(item)) {
|
|
247
|
+
for (let index = 0; index < item.length; index += 1) freeze(item[index]);
|
|
248
|
+
} else {
|
|
249
|
+
const keys = objectKeys(item);
|
|
250
|
+
for (let index = 0; index < keys.length; index += 1) freeze(item[keys[index]]);
|
|
251
|
+
}
|
|
252
|
+
objectFreeze(item);
|
|
253
|
+
}
|
|
254
|
+
return item;
|
|
255
|
+
};
|
|
256
|
+
return freeze(clone);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Snapshot a caller-supplied record BEFORE validating it. Every later read —
|
|
260
|
+
// by a validator or by the code that stores the value — comes from a frozen
|
|
261
|
+
// plain clone, so an enumerable getter cannot return the declared value to
|
|
262
|
+
// the validator and a different one to the store. Accessors, symbol keys,
|
|
263
|
+
// unsafe keys, cycles, and oversized structures are refused here, at the
|
|
264
|
+
// boundary, before any field is inspected.
|
|
265
|
+
// WRAPPED AS A WHOLE, not just around the walk. `Array.isArray` looks like a
|
|
266
|
+
// pure predicate and is not: on a REVOKED Proxy it throws, and it runs BEFORE
|
|
267
|
+
// `cloneAndFreeze` — so wrapping only the canonicalization left this line as a
|
|
268
|
+
// raw-throw seam in the single most-called validator in the SDK. Found by the
|
|
269
|
+
// mechanical entry-point sweep in `execution-seams.test.mjs`, which is the
|
|
270
|
+
// argument for enumerating a surface rather than sampling it.
|
|
271
|
+
export function snapshotRecord(value, code) {
|
|
272
|
+
if (value === null || typeof value !== "object") refuse(code);
|
|
273
|
+
if (runUntrusted(code, () => arrayIsArray(value))) refuse(code);
|
|
274
|
+
return cloneAndFreeze(value);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// `Object.keys` is an own-keys walk, so it runs the same traps `normalize` does
|
|
278
|
+
// and throws on a revoked Proxy. This is a public export and its callers pass
|
|
279
|
+
// their own code, so the raw throw becomes THAT code rather than a generic one.
|
|
280
|
+
export function exactKeys(value, expected, code) {
|
|
281
|
+
if (!value || typeof value !== "object") refuse(code);
|
|
282
|
+
if (runUntrusted(code, () => arrayIsArray(value))) refuse(code);
|
|
283
|
+
const actual = stableSortedStringCopy(runUntrusted(code, () => Object.keys(value)));
|
|
284
|
+
const wanted = stableSortedStringCopy(entryArray(expected, code, code));
|
|
285
|
+
if (actual.length !== wanted.length) refuse(code);
|
|
286
|
+
for (let index = 0; index < actual.length; index += 1) {
|
|
287
|
+
if (actual[index] !== wanted[index]) refuse(code);
|
|
288
|
+
}
|
|
289
|
+
}
|