@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.
Files changed (34) hide show
  1. package/README.md +103 -0
  2. package/SHA256SUMS +33 -0
  3. package/artifacts/mcphersonai-observa-adapter-n8n-0.1.0.tgz +0 -0
  4. package/artifacts/mcphersonai-observa-domain-generic-0.1.0.tgz +0 -0
  5. package/artifacts/mcphersonai-observa-hosted-transport-0.1.0.tgz +0 -0
  6. package/artifacts/mcphersonai-observa-n8n-h1-binding-0.1.0.tgz +0 -0
  7. package/artifacts/mcphersonai-observa-node-0.1.0.tgz +0 -0
  8. package/distribution/observa-cli/bin/observa-n8n-hosted.mjs +114 -0
  9. package/distribution/observa-cli/bin/observa.mjs +331 -0
  10. package/distribution/observa-cli/integrations/n8n/observa-external-hook.cjs +132 -0
  11. package/distribution/observa-cli/keys/observa-beta-1.public.json +7 -0
  12. package/distribution/observa-cli/src/artifact.mjs +143 -0
  13. package/distribution/observa-cli/src/config.mjs +162 -0
  14. package/distribution/observa-cli/src/errors.mjs +21 -0
  15. package/distribution/observa-cli/src/hosted-delivery.mjs +148 -0
  16. package/distribution/observa-cli/src/index.mjs +39 -0
  17. package/distribution/observa-cli/src/install.mjs +443 -0
  18. package/distribution/observa-cli/src/lock.mjs +57 -0
  19. package/distribution/observa-cli/src/manifest-schema.mjs +208 -0
  20. package/distribution/observa-cli/src/manifest-verify.mjs +122 -0
  21. package/distribution/observa-cli/src/n8n-hook.mjs +70 -0
  22. package/distribution/observa-cli/src/pair.mjs +149 -0
  23. package/distribution/observa-cli/src/re-pair.mjs +171 -0
  24. package/distribution/observa-cli/src/service.mjs +218 -0
  25. package/distribution/observa-cli/src/state.mjs +87 -0
  26. package/distribution/observa-cli/src/status.mjs +191 -0
  27. package/distribution/observa-cli/src/vocabulary.mjs +47 -0
  28. package/npm-distribution-provenance.json +1 -0
  29. package/package.json +36 -0
  30. package/runtime-adapters/n8n/src/strict-json.mjs +138 -0
  31. package/sdk/contracts/canonical.mjs +289 -0
  32. package/sdk/contracts/entry-boundary.mjs +417 -0
  33. package/sdk/contracts/errors.mjs +334 -0
  34. 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
+ }