{"_id":"@ekanos/harness","_rev":"8-dd130a28397c604c9cdaed6972533dce","name":"@ekanos/harness","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@ekanos/harness","version":"0.1.0","license":"MIT","_id":"@ekanos/harness@0.1.0","maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"bugs":{"email":"npm@govastly.com"},"dist":{"shasum":"1cd13b811111bc536223cf8470d38f5ef318bc25","tarball":"https://registry.npmjs.org/@ekanos/harness/-/harness-0.1.0.tgz","fileCount":72,"integrity":"sha512-xYBZ2K8HzPGX3pmVwyuVnLzipcidsqHzFc6JDBQWpdwQMAtbKAfJwHlEGHAy527PADWJcD8rgd2lur1vRJeU8Q==","signatures":[{"sig":"MEYCIQCQXDNfGx+NK38pG/8PfZBY5T9KeL879vgm953j+O5btQIhAIjyDpYwo3ZrxyFr1/wKDDhH/lptlvcAPVaAShBSQS2M","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":230648},"type":"module","ekanos":{"shellContract":1},"exports":{"./app":{"types":"./dist/app.d.ts","default":"./dist/app.js"},"./config":{"types":"./dist/config.d.ts","default":"./dist/config.js"},"./routes":{"types":"./dist/routes.d.ts","default":"./dist/routes.js"},"./registry":{"types":"./dist/registry.d.ts","default":"./dist/registry.js"},"./styles.css":"./dist/styles.css","./mocks/team-account-workspace":{"types":"./dist/mocks/team-account-workspace.d.ts","default":"./dist/mocks/team-account-workspace.js"}},"scripts":{"lint":"eslint .","test":"vitest run --config vitest.config.ts","build":"tsup && tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs && node scripts/build-css.mjs","clean":"git clean -xdf .turbo node_modules dist","format":"prettier --check \"**/*.{ts,tsx,mjs}\"","pack:test":"node scripts/pack-test.mjs","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ekanos-bot","email":"npm@govastly.com"},"prettier":"@kit/prettier-config","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase, auth or network.","directories":{},"sideEffects":["**/*.css","**/i18n.js"],"_nodeVersion":"24.11.1","dependencies":{"i18next":"25.10.10","next-themes":"0.4.6","react-i18next":"^16.6.6","tw-animate-css":"1.4.0","@fortawesome/fontawesome-free":"^7.3.1"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"*":["dist/*"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"3.25.76","next":"16.3.3","tsup":"8.5.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","@ekanos/ui":"0.1.2","typescript":"^5.9.3","@ekanos/sdk":"0.1.2","@types/node":"25.0.1","tailwindcss":"4.3.3","@types/react":"19.2.18","@kit/tsconfig":"0.1.0","@types/react-dom":"19.2.5","@kit/eslint-config":"0.2.0","@kit/prettier-config":"0.1.0","@tailwindcss/postcss":"4.3.3","@tanstack/react-query":"5.102.8"},"peerDependencies":{"next":"^16.0.0","react":"^19.2.8","react-dom":"^19.2.8","@ekanos/ui":"^0.1.2","@ekanos/sdk":"^0.1.2","tailwindcss":"^4.0.0","@tanstack/react-query":"^5.101.4"},"_npmOperationalInternal":{"tmp":"tmp/harness_0.1.0_1788826961778_0.8255082016360376","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@ekanos/harness","version":"0.1.1","license":"MIT","_id":"@ekanos/harness@0.1.1","maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"bugs":{"email":"npm@govastly.com"},"dist":{"shasum":"bcdd267fc5325afc342a4a469637b4da92535da7","tarball":"https://registry.npmjs.org/@ekanos/harness/-/harness-0.1.1.tgz","fileCount":72,"integrity":"sha512-xqIL2ljX1PkAQYTjxGOKAsUtlRFt6NVYdPSr6CGWL47HL8MheX3yvN3JYGQjoBWmYuWTwFTfnE+OMBNuWjLz4Q==","signatures":[{"sig":"MEUCIQCGYzwnnMzoYqt01oIV/QL+XcXDwuWzexdsCJCAbP/VvgIgH3rzer9OyTJ7cxX0TeG/+L1aBmxlzkSGlj0r5oZilZw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":230649},"type":"module","ekanos":{"shellContract":1},"exports":{"./app":{"types":"./dist/app.d.ts","default":"./dist/app.js"},"./config":{"types":"./dist/config.d.ts","default":"./dist/config.js"},"./routes":{"types":"./dist/routes.d.ts","default":"./dist/routes.js"},"./registry":{"types":"./dist/registry.d.ts","default":"./dist/registry.js"},"./styles.css":"./dist/styles.css","./mocks/team-account-workspace":{"types":"./dist/mocks/team-account-workspace.d.ts","default":"./dist/mocks/team-account-workspace.js"}},"scripts":{"lint":"eslint .","test":"vitest run --config vitest.config.ts","build":"tsup && tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs && node scripts/build-css.mjs","clean":"git clean -xdf .turbo node_modules dist","format":"prettier --check \"**/*.{ts,tsx,mjs}\"","pack:test":"node scripts/pack-test.mjs","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ekanos-bot","email":"npm@govastly.com"},"prettier":"@kit/prettier-config","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase, auth or network.","directories":{},"sideEffects":["**/*.css","**/i18n.js"],"_nodeVersion":"24.11.1","dependencies":{"i18next":"^25.10.10","next-themes":"0.4.6","react-i18next":"^16.6.6","tw-animate-css":"1.4.0","@fortawesome/fontawesome-free":"^7.3.1"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"*":["dist/*"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"3.25.76","next":"16.3.3","tsup":"8.5.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","@ekanos/ui":"0.1.3","typescript":"^5.9.3","@ekanos/sdk":"0.1.3","@types/node":"25.0.1","tailwindcss":"4.3.3","@types/react":"19.2.18","@kit/tsconfig":"0.1.0","@types/react-dom":"19.2.5","@kit/eslint-config":"0.2.0","@kit/prettier-config":"0.1.0","@tailwindcss/postcss":"4.3.3","@tanstack/react-query":"5.102.8"},"peerDependencies":{"next":"^16.0.0","react":"^19.2.8","react-dom":"^19.2.8","@ekanos/ui":"^0.1.2","@ekanos/sdk":"^0.1.2","tailwindcss":"^4.0.0","@tanstack/react-query":"^5.101.4"},"_npmOperationalInternal":{"tmp":"tmp/harness_0.1.1_1788889021961_0.9043396230692156","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@ekanos/harness","version":"0.1.2","license":"MIT","_id":"@ekanos/harness@0.1.2","maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"bugs":{"email":"npm@govastly.com"},"dist":{"shasum":"546f0900f912239293db805b3c0ed560bc9e0a67","tarball":"https://registry.npmjs.org/@ekanos/harness/-/harness-0.1.2.tgz","fileCount":76,"integrity":"sha512-4+f4uHPLU2j3Ifj63uxZAKL+6EHab8im4OrrUOD9/6yOIVBi7AJMvoPGS2i+3hCv+4CKC+6PIPQ9BoD6Dbfwlg==","signatures":[{"sig":"MEUCIQCdAFfOhw72ufR6DealJ3ff85B2Jsw0dICu+oSuhNxizwIgI9nnJxUErf8M3bfn5AbuheWN9sIwJgWkYjKP2VcU0d4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":238038},"type":"module","ekanos":{"shellContract":1},"exports":{"./app":{"types":"./dist/app.d.ts","default":"./dist/app.js"},"./config":{"types":"./dist/config.d.ts","default":"./dist/config.js"},"./routes":{"types":"./dist/routes.d.ts","default":"./dist/routes.js"},"./registry":{"types":"./dist/registry.d.ts","default":"./dist/registry.js"},"./styles.css":"./dist/styles.css","./mocks/team-account-workspace":{"types":"./dist/mocks/team-account-workspace.d.ts","default":"./dist/mocks/team-account-workspace.js"}},"scripts":{"lint":"eslint .","test":"vitest run --config vitest.config.ts","build":"tsup && tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs && node scripts/build-css.mjs","clean":"git clean -xdf .turbo node_modules dist","format":"prettier --check \"**/*.{ts,tsx,mjs}\"","pack:test":"node scripts/pack-test.mjs","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ekanos-bot","email":"npm@govastly.com"},"prettier":"@kit/prettier-config","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase, auth or network.","directories":{},"sideEffects":["**/*.css","**/i18n.js"],"_nodeVersion":"24.11.1","dependencies":{"i18next":"^25.10.10","next-themes":"0.4.6","react-i18next":"^16.6.6","tw-animate-css":"1.4.0","@fortawesome/fontawesome-free":"^7.3.1"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"*":["dist/*"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"3.25.76","next":"16.3.3","tsup":"8.5.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","@ekanos/ui":"0.1.4","typescript":"^5.9.3","@ekanos/sdk":"0.1.4","@types/node":"25.0.1","tailwindcss":"4.3.3","@types/react":"19.2.18","@kit/tsconfig":"0.1.0","@types/react-dom":"19.2.5","@kit/eslint-config":"0.2.0","@kit/prettier-config":"0.1.0","@tailwindcss/postcss":"4.3.3","@tanstack/react-query":"5.102.8"},"peerDependencies":{"next":"^16.0.0","react":"^19.2.8","react-dom":"^19.2.8","@ekanos/ui":"^0.1.4","@ekanos/sdk":"^0.1.4","tailwindcss":"^4.0.0","@tanstack/react-query":"^5.101.4"},"_npmOperationalInternal":{"tmp":"tmp/harness_0.1.2_1788902449743_0.30408408831529576","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@ekanos/harness","version":"0.1.3","license":"MIT","_id":"@ekanos/harness@0.1.3","maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"bugs":{"email":"npm@govastly.com"},"dist":{"shasum":"501a1c5ff84f648bacc6b46aab9bc75c44bead9c","tarball":"https://registry.npmjs.org/@ekanos/harness/-/harness-0.1.3.tgz","fileCount":84,"integrity":"sha512-oLoIJsNFf5PvAmqmagfCkCQ03CKt/gRFvW6r1AjAsifdFe5wNKOXVQZCCM+73ZSTW0ZblB90G4UJisPvmH695w==","signatures":[{"sig":"MEUCIQDsSgc1MEUiUWcSPaUcpo/Hmn6mTyOA/BZynO4zj3fbBAIgXPyRRgAgvUkwSZF+6AgiMax5N8mmUrtGURREAr3MolU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDzdrXK7+7Ejb509s89GAO1HYCZUcRts0SYjpdDTySTigIhAJ3jt1qhKnuUUjpkdcLge35ufxu/MuJvELdGwnYm7B1b","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":257090},"type":"module","ekanos":{"shellContract":1},"exports":{"./app":{"types":"./dist/app.d.ts","default":"./dist/app.js"},"./hooks":{"types":"./dist/hooks.d.ts","default":"./dist/hooks.js"},"./config":{"types":"./dist/config.d.ts","default":"./dist/config.js"},"./routes":{"types":"./dist/routes.d.ts","default":"./dist/routes.js"},"./registry":{"types":"./dist/registry.d.ts","default":"./dist/registry.js"},"./styles.css":"./dist/styles.css","./mocks/team-account-workspace":{"types":"./dist/mocks/team-account-workspace.d.ts","default":"./dist/mocks/team-account-workspace.js"}},"scripts":{"lint":"eslint .","test":"vitest run --config vitest.config.ts","build":"tsup && tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs && node scripts/build-css.mjs","clean":"git clean -xdf .turbo node_modules dist","format":"prettier --check \"**/*.{ts,tsx,mjs}\"","pack:test":"node scripts/pack-test.mjs","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ekanos-bot","email":"npm@govastly.com"},"prettier":"@kit/prettier-config","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase, auth or network.","directories":{},"sideEffects":["**/*.css","**/i18n.js"],"dependencies":{"i18next":"^25.10.10","next-themes":"0.4.6","react-i18next":"^16.6.6","tw-animate-css":"1.4.0","@fortawesome/fontawesome-free":"^7.3.1"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"*":["dist/*"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"3.25.76","next":"16.3.3","tsup":"8.5.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","@ekanos/ui":"0.1.5","typescript":"^5.9.3","@ekanos/sdk":"0.1.5","@types/node":"25.0.1","tailwindcss":"4.3.3","@types/react":"19.2.18","@kit/tsconfig":"0.1.0","@types/react-dom":"19.2.5","@kit/eslint-config":"0.2.0","@kit/prettier-config":"0.1.0","@tailwindcss/postcss":"4.3.3","@tanstack/react-query":"5.102.8"},"peerDependencies":{"next":"^16.0.0","react":"^19.2.8","react-dom":"^19.2.8","@ekanos/ui":"^0.1.4","@ekanos/sdk":"^0.1.4","tailwindcss":"^4.0.0","@tanstack/react-query":"^5.101.4"},"_npmOperationalInternal":{"tmp":"tmp/harness_0.1.3_1789121793149_0.9768958100772951","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@ekanos/harness","version":"0.1.4","license":"MIT","_id":"@ekanos/harness@0.1.4","maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"bugs":{"email":"npm@govastly.com"},"dist":{"shasum":"54c0082d4198a4f84f871d9975a60b65d9689f00","tarball":"https://registry.npmjs.org/@ekanos/harness/-/harness-0.1.4.tgz","fileCount":84,"integrity":"sha512-R6BbRAl2cPPBIahDNtn6ZJGM+GrKbOGz4glspZ3ow0XTSpsooIm2JMUZe+27U0lDexHD0+Mf3eCmaLrYcS+GsQ==","signatures":[{"sig":"MEYCIQClOgxTfVrfJtR+mhz69+XIX1KSdZuXCiGSLZr/AzaOZAIhAMcdFpd15Zuvk929e8nh96GP4WNoG291/AemrGweAQRP","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIENzI+kkJseoyHuaMZJfx3UyMhqnLPV/T+8K6LZ2i6TwAiEAoWeA60v8S86WNSFYO2zsDO7on4pprgkKjiJ7ircHybA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":257698},"type":"module","ekanos":{"shellContract":1},"exports":{"./app":{"types":"./dist/app.d.ts","default":"./dist/app.js"},"./hooks":{"types":"./dist/hooks.d.ts","default":"./dist/hooks.js"},"./config":{"types":"./dist/config.d.ts","default":"./dist/config.js"},"./routes":{"types":"./dist/routes.d.ts","default":"./dist/routes.js"},"./registry":{"types":"./dist/registry.d.ts","default":"./dist/registry.js"},"./styles.css":"./dist/styles.css","./mocks/team-account-workspace":{"types":"./dist/mocks/team-account-workspace.d.ts","default":"./dist/mocks/team-account-workspace.js"}},"private":false,"scripts":{"lint":"eslint .","test":"vitest run --config vitest.config.ts","build":"tsup && tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs && node scripts/build-css.mjs","clean":"git clean -xdf .turbo node_modules dist","format":"prettier --check \"**/*.{ts,tsx,mjs}\"","pack:test":"node scripts/pack-test.mjs","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ekanos-bot","email":"npm@govastly.com"},"prettier":"@kit/prettier-config","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase, auth or network.","directories":{},"sideEffects":["**/*.css","**/i18n.js"],"dependencies":{"i18next":"^25.10.10","next-themes":"0.4.6","react-i18next":"^16.6.6","tw-animate-css":"1.4.0","@fortawesome/fontawesome-free":"^7.3.1"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"*":["dist/*"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"3.25.76","next":"16.3.3","tsup":"8.5.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","@ekanos/ui":"0.1.6","typescript":"^5.9.3","@ekanos/sdk":"0.1.6","@types/node":"25.0.1","tailwindcss":"4.3.3","@types/react":"19.2.18","@kit/tsconfig":"0.1.0","@types/react-dom":"19.2.5","@kit/eslint-config":"0.2.0","@kit/prettier-config":"0.1.0","@tailwindcss/postcss":"4.3.3","@tanstack/react-query":"5.102.8"},"peerDependencies":{"next":"^16.0.0","react":"^19.2.8","react-dom":"^19.2.8","@ekanos/ui":"^0.1.6","@ekanos/sdk":"^0.1.6","tailwindcss":"^4.0.0","@tanstack/react-query":"^5.101.4"},"_npmOperationalInternal":{"tmp":"tmp/harness_0.1.4_1789758273121_0.7477113487199614","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@ekanos/harness","version":"0.1.5","license":"MIT","_id":"@ekanos/harness@0.1.5","maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"bugs":{"email":"npm@govastly.com"},"dist":{"shasum":"380d1afb2ff9ce83a576791f310f104000748960","tarball":"https://registry.npmjs.org/@ekanos/harness/-/harness-0.1.5.tgz","fileCount":84,"integrity":"sha512-WH0L3yZ9SOQYsWbW0Jkk8pHD7pFUBFFJn+GOKU8ux4cBCfMBixWkHJSbQkfofSTWcSDSF7VvRwoPJJ37M9NIvA==","signatures":[{"sig":"MEUCIGHPxil5Y6tqtLmHOJ2LIzwA4L11SDuj8Q/jtXdxgwFtAiEA+WafSz/SqoObv9Jmeg0WEn5QmhBONORtkB5+dxVifN4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDF3y06nUNo06+2Q8bsCYxqFTwShCRL+tIrZdVlNHUUOwIgEccstZNjLvJegZsKdWRn0ZY1+WKAzJQphEZPzgd4/Tg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":262226},"type":"module","ekanos":{"shellContract":1},"exports":{"./app":{"types":"./dist/app.d.ts","default":"./dist/app.js"},"./hooks":{"types":"./dist/hooks.d.ts","default":"./dist/hooks.js"},"./config":{"types":"./dist/config.d.ts","default":"./dist/config.js"},"./routes":{"types":"./dist/routes.d.ts","default":"./dist/routes.js"},"./registry":{"types":"./dist/registry.d.ts","default":"./dist/registry.js"},"./styles.css":"./dist/styles.css","./mocks/team-account-workspace":{"types":"./dist/mocks/team-account-workspace.d.ts","default":"./dist/mocks/team-account-workspace.js"}},"scripts":{"lint":"eslint .","test":"vitest run --config vitest.config.ts","build":"tsup && tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs && node scripts/build-css.mjs","clean":"git clean -xdf .turbo node_modules dist","format":"prettier --check \"**/*.{ts,tsx,mjs}\"","pack:test":"node scripts/pack-test.mjs","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ekanos-bot","email":"npm@govastly.com"},"prettier":"@kit/prettier-config","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase, auth or network.","directories":{},"sideEffects":["**/*.css","**/i18n.js"],"dependencies":{"i18next":"^25.10.10","next-themes":"0.4.6","react-i18next":"^16.6.6","tw-animate-css":"1.4.0","@fortawesome/fontawesome-free":"^7.3.1"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"*":["dist/*"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"3.25.76","next":"16.3.3","tsup":"8.5.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","@ekanos/ui":"0.2.0","typescript":"^5.9.3","@ekanos/sdk":"0.2.0","@types/node":"25.0.1","tailwindcss":"4.3.3","@types/react":"19.2.18","@kit/tsconfig":"0.1.0","@types/react-dom":"19.2.5","@kit/eslint-config":"0.2.0","@kit/prettier-config":"0.1.0","@tailwindcss/postcss":"4.3.3","@tanstack/react-query":"5.102.8"},"peerDependencies":{"next":"^16.0.0","react":"^19.2.8","react-dom":"^19.2.8","@ekanos/ui":"^0.2.0","@ekanos/sdk":"^0.2.0","tailwindcss":"^4.0.0","@tanstack/react-query":"^5.101.4"},"_npmOperationalInternal":{"tmp":"tmp/harness_0.1.5_1789777479579_0.7925750462018752","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@ekanos/harness","version":"0.2.0","license":"MIT","_id":"@ekanos/harness@0.2.0","maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"bugs":{"email":"npm@govastly.com"},"dist":{"shasum":"c236e3e5e56561680683c1235cba8238f45b8919","tarball":"https://registry.npmjs.org/@ekanos/harness/-/harness-0.2.0.tgz","fileCount":112,"integrity":"sha512-niSNFz0NrXxZg9LxRd4AI6GrAofZMewSQ37bD9NWDXsz6KA5ZIHdGm9vokCVbbMmMHmWcGuT5WTFY9sEo9HoHA==","signatures":[{"sig":"MEUCIQDxprO1nMC7n/cSEyKQO9BqXz9nVXrUOsX7YTj9z4N7DgIgMYK1MXMYNU4ECtzYZ07usxgAQaFNbH0uUrKCcpQuIbM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIFVihimT6kx59DqFPfxoTY8QKHINiW3iG5TAMECeY35ZAiEA95glNyiysstjR+H1uZ6QpyAMyNzJuWvT37Vrh9JjYkM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":371196},"type":"module","ekanos":{"shellContract":2},"exports":{"./api":{"types":"./dist/api.d.ts","default":"./dist/api.js"},"./app":{"types":"./dist/app.d.ts","default":"./dist/app.js"},"./hooks":{"types":"./dist/hooks.d.ts","default":"./dist/hooks.js"},"./config":{"types":"./dist/config.d.ts","default":"./dist/config.js"},"./routes":{"types":"./dist/routes.d.ts","default":"./dist/routes.js"},"./registry":{"types":"./dist/registry.d.ts","default":"./dist/registry.js"},"./styles.css":"./dist/styles.css","./mocks/team-account-workspace":{"types":"./dist/mocks/team-account-workspace.d.ts","default":"./dist/mocks/team-account-workspace.js"}},"private":false,"scripts":{"lint":"eslint .","test":"vitest run --config vitest.config.ts","build":"tsup && tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs && node scripts/build-css.mjs","clean":"git clean -xdf .turbo node_modules dist","format":"prettier --check \"**/*.{ts,tsx,mjs}\"","pack:test":"node scripts/pack-test.mjs","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ekanos-bot","email":"npm@govastly.com"},"prettier":"@kit/prettier-config","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase or auth; and, in live mode, a real localhost OAuth loop over your own dev-app credentials.","directories":{},"sideEffects":["**/*.css","**/i18n.js"],"dependencies":{"i18next":"^25.10.10","next-themes":"0.4.6","react-i18next":"^16.6.6","tw-animate-css":"1.4.0","@fortawesome/fontawesome-free":"^7.3.1"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"*":["dist/*"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"3.25.76","next":"16.3.3","tsup":"8.5.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","@ekanos/ui":"0.2.0","typescript":"^5.9.3","@ekanos/sdk":"0.2.0","@types/node":"25.0.1","tailwindcss":"4.3.3","@types/react":"19.2.18","@kit/tsconfig":"0.1.0","react-hook-form":"^7.87.0","@types/react-dom":"19.2.5","@kit/eslint-config":"0.2.0","@kit/prettier-config":"0.1.0","@tailwindcss/postcss":"4.3.3","@tanstack/react-query":"5.102.8"},"peerDependencies":{"next":"^16.0.0","react":"^19.2.8","react-dom":"^19.2.8","@ekanos/ui":"^0.2.0","@ekanos/sdk":"^0.2.0","tailwindcss":"^4.0.0","react-hook-form":"^7.68.0","@tanstack/react-query":"^5.101.4"},"_npmOperationalInternal":{"tmp":"tmp/harness_0.2.0_1790093622870_0.3535896145638331","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"_id":"@ekanos/harness@0.3.0","bugs":{"email":"npm@govastly.com"},"dist":{"shasum":"7eb636895abdf3b4ba10f826651c01fb4a201a14","tarball":"https://registry.npmjs.org/@ekanos/harness/-/harness-0.3.0.tgz","fileCount":116,"integrity":"sha512-H/5ngWqjGJ2jNEcThHLhRBuFac0FLZoygyNLP4Wt6oL3j2jx4aaFRF55Mpe0Bja9Y+nMr81XdOcYTsczrYud4A==","signatures":[{"sig":"MEQCIEX4dweKepGtPqJDjxfuxk/25Fw+WuVusXiZYK0VJtBtAiB9xRKlIlEeV8keSyqJRU3t87REBm9/wQcEqruNBgxN4g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBMf9C+rciW1wY/yUtTMMsgTF1Ff4TkkvknbyOSsqXTvAiA4D6fklEQXGL2zuwbES75Gh6uoVYWxMAeCuKfTwviZaQ=="}],"unpackedSize":391270},"name":"@ekanos/harness","type":"module","ekanos":{"shellContract":2},"exports":{"./api":{"types":"./dist/api.d.ts","default":"./dist/api.js"},"./app":{"types":"./dist/app.d.ts","default":"./dist/app.js"},"./hooks":{"types":"./dist/hooks.d.ts","default":"./dist/hooks.js"},"./config":{"types":"./dist/config.d.ts","default":"./dist/config.js"},"./routes":{"types":"./dist/routes.d.ts","default":"./dist/routes.js"},"./registry":{"types":"./dist/registry.d.ts","default":"./dist/registry.js"},"./styles.css":"./dist/styles.css","./mocks/team-account-workspace":{"types":"./dist/mocks/team-account-workspace.d.ts","default":"./dist/mocks/team-account-workspace.js"}},"license":"MIT","scripts":{"lint":"eslint .","test":"vitest run --config vitest.config.ts","build":"tsup && tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs && node scripts/build-css.mjs","clean":"git clean -xdf .turbo node_modules dist","format":"prettier --check \"**/*.{ts,tsx,mjs}\"","pack:test":"node scripts/pack-test.mjs","typecheck":"tsc --noEmit"},"version":"0.3.0","_npmUser":{"name":"ekanos-bot","email":"npm@govastly.com"},"prettier":"@kit/prettier-config","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase or auth; and, in live mode, a real localhost OAuth loop over your own dev-app credentials.","directories":{},"maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"sideEffects":["**/*.css","**/i18n.js"],"dependencies":{"i18next":"^25.10.10","next-themes":"0.4.6","react-i18next":"^16.6.6","tw-animate-css":"1.4.0","@fortawesome/fontawesome-free":"^7.3.1"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"*":["dist/*"]}},"_hasShrinkwrap":false,"devDependencies":{"zod":"3.25.76","next":"16.3.3","tsup":"8.5.1","react":"19.2.8","vitest":"4.1.10","react-dom":"19.2.8","@ekanos/ui":"0.3.0","typescript":"^5.9.3","@ekanos/sdk":"0.3.0","@types/node":"25.0.1","tailwindcss":"4.3.3","@types/react":"19.2.18","@kit/tsconfig":"0.1.0","react-hook-form":"^7.87.0","@types/react-dom":"19.2.5","@kit/eslint-config":"0.2.0","@kit/prettier-config":"0.1.0","@tailwindcss/postcss":"4.3.3","@tanstack/react-query":"5.102.8"},"peerDependencies":{"next":"^16.0.0","react":"^19.2.8","react-dom":"^19.2.8","@ekanos/ui":"^0.3.0","@ekanos/sdk":"^0.3.0","tailwindcss":"^4.0.0","react-hook-form":"^7.68.0","@tanstack/react-query":"^5.101.4"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/harness_0.3.0_1790096715554_0.6452361716283519"}}},"time":{"created":"2026-09-08T00:22:41.585Z","modified":"2026-09-22T17:05:15.941Z","0.1.0":"2026-09-08T00:22:41.926Z","0.1.1":"2026-09-08T17:37:02.111Z","0.1.2":"2026-09-08T21:20:49.933Z","0.1.3":"2026-09-11T10:16:33.246Z","0.1.4":"2026-09-18T19:04:33.213Z","0.1.5":"2026-09-19T00:24:39.684Z","0.2.0":"2026-09-22T16:13:42.968Z","0.3.0":"2026-09-22T17:05:15.674Z"},"bugs":{"email":"npm@govastly.com"},"license":"MIT","description":"The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase or auth; and, in live mode, a real localhost OAuth loop over your own dev-app credentials.","maintainers":[{"name":"ekanos-bot","email":"npm@govastly.com"}],"readme":"# @ekanos/harness\n\nThe Ekanos integration dev harness: every surface of an integration rendered in\nreal Fusion chrome — the actual dashboard grid, the actual `Widget.*` compound\ncomponents, the actual design tokens — from fixtures, with no Supabase, no auth\nand no network.\n\n**New here? Start with the `@ekanos/sdk` README** — it is the authoring\nreference for everything you declare (widgets, tools, webhooks, schedules,\nOAuth, storage, egress). This document covers only the local dev harness that\nrenders those surfaces.\n\n> **Not published yet.** `@ekanos/harness` and `@ekanos/cli` are not on npm, so\n> the commands below do not resolve today. `@ekanos/sdk`, `@ekanos/ui` and\n> `@ekanos/integration-schema` are (`0.1.2`), and everything in the SDK README\n> works without this package — `defineIntegration()` validates at import and\n> the testing helpers need only vitest. What you cannot do until this ships is\n> _see_ your surfaces rendered. This document describes what will ship.\n\nYou do not normally install this by hand. `ekanos dev` scaffolds a shell around\nit and adds it to your `package.json`.\n\n```bash\npnpm --filter <your project> exec ekanos dev\n```\n\n- [Why it is a package](#why-it-is-a-package)\n- [The registry is injected, never imported](#the-registry-is-injected-never-imported)\n- [The registry — `harness.config.ts`](#the-registry--harnessconfigts)\n- [Fixtures](#fixtures)\n  - [`HttpFixture` — the request line](#httpfixture--the-request-line)\n  - [When there is no fixture for a request](#when-there-is-no-fixture-for-a-request)\n  - [`seeds` — the react-query fast path](#seeds--the-react-query-fast-path)\n- [Live mode — real requests to your own API](#live-mode--real-requests-to-your-own-api)\n  - [`FetchProvider` — the part you have to write yourself](#fetchprovider--the-part-you-have-to-write-yourself)\n- [Triggers — running webhooks, schedules and OAuth locally](#triggers--running-webhooks-schedules-and-oauth-locally)\n  - [`triggerMocks`](#triggermocks)\n- [What ends up in the browser bundle](#what-ends-up-in-the-browser-bundle)\n- [Exports](#exports)\n- [The shell contract](#the-shell-contract)\n\nThe surfaces this renders — widgets, the tile, the activation form, and the\nwebhook / schedule / OAuth declarations behind Triggers — are documented in the\n`@ekanos/sdk` README. This one covers the harness's own contract.\n\n## Why it is a package\n\nSo that a harness bugfix reaches you as a version bump you pick up with\n`pnpm up`, with zero edits to any file you own. The harness is a dependency,\nnot code you copied.\n\n| Path                                                            | Who owns it                                                |\n| --------------------------------------------------------------- | ---------------------------------------------------------- |\n| `harness.config.ts`                                             | **you** — the registry, and the only file you have to edit |\n| your integration source                                         | **you**                                                    |\n| the generated `app/**`, `styles/globals.css`, `next.config.mjs` | the CLI; regenerated on upgrade                            |\n| `@ekanos/harness`                                               | us                                                         |\n\n## The registry is injected, never imported\n\nA package inside `node_modules` cannot reach a file in your project, so nothing\nhere imports your config. The shell passes your array down instead:\n\n```tsx\n// app/harness-shell.tsx — generated\n'use client';\n\nimport { RootLayout } from '@ekanos/harness/app';\n\nimport { harnessIntegrations } from '~/harness.config';\n\nexport default function HarnessShell(props: { children: React.ReactNode }) {\n  return <RootLayout integrations={harnessIntegrations} {...props} />;\n}\n```\n\n`RootLayout` mounts a context provider; every route reads the registry back out\nof it and takes its own `[slug]` from `useParams()`. That is why the generated\nroute files are one line each:\n\n```tsx\nexport { WidgetsPage as default } from '@ekanos/harness/routes';\n```\n\n`RootLayout` is the client boundary of the whole harness, and has to be: a\n`HarnessIntegration` holds live React component references and MCP `run`\nfunctions, which cannot be serialized across a server/client edge.\n\n## The registry — `harness.config.ts`\n\nThis is the only file you edit. It exports an array of `HarnessIntegration`,\none entry per integration, and each entry lights up four tabbed surfaces at\n`/<slug>` — `widgets`, `tile`, `activation` and `triggers` — plus a\nsingle-widget page at `/<slug>/widgets/<widgetId>`, which the widget grid links\ninto for isolating one widget.\n\n```ts\nimport { defineHarnessConfig } from '@ekanos/harness/config';\n\nimport { acmePayments } from './harness/acme-payments';\n\nexport const harnessIntegrations = defineHarnessConfig([acmePayments]);\n```\n\n`defineHarnessConfig()` is an identity function. It exists for the inference —\nnothing else.\n\n### `HarnessIntegration`\n\n| Field                 | Required | What it does                                                                                                              |\n| --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------- |\n| `slug`                | yes      | URL segment, and the id you would ship as your product slug                                                               |\n| `name`, `description` | yes      | Header chrome and the index page                                                                                          |\n| `widgets`             | yes      | `HarnessWidget[]` — may be empty                                                                                          |\n| `fixtures`            | no       | `Partial<Record<FixtureVariant, HttpFixture[]>>` — the recorded HTTP exchanges, per variant. **This is how data gets in** |\n| `tile`                | no       | `ComponentType<MarketplaceTileProps>` — the marketplace card                                                              |\n| `activationForm`      | no       | `ComponentType<ActivationFormProps>`, rendered with `inline` so it does not open a dialog                                 |\n| `definition`          | no       | Your validated `defineIntegration()` output. Powers Triggers                                                              |\n| `triggerMocks`        | no       | Seeds for the Triggers surface's mock context                                                                             |\n| `live`                | no       | Opt in to real third-party requests                                                                                       |\n\n### `HarnessWidget`\n\n| Field             | Required | What it does                                                                                                                                                       |\n| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `id`              | yes      | Stable id; the URL segment on `/<slug>/widgets/<widgetId>`                                                                                                         |\n| `title`           | yes      | Rendered in the widget header chrome                                                                                                                               |\n| `component`       | yes      | `ComponentType<IntegrationComponentProps>`                                                                                                                         |\n| `width`           | no       | `'half'` (default) or `'full'`. Runs of `half` balance across two columns                                                                                          |\n| `isCollapsible`   | no       | Renders the collapse button. Off unless set                                                                                                                        |\n| `isPinnable`      | no       | Reaches `WidgetContext`; nothing draws a pin — see below                                                                                                           |\n| `aiFooterEnabled` | no       | Renders the \"Ask about this\" AI footer bar. Off unless set                                                                                                         |\n| `seeds`           | no       | `Partial<Record<FixtureVariant, FixtureSeed[]>>` — react-query cache seeds for this widget, per variant. The fast path, not the default; see [Fixtures](#fixtures) |\n\nAll three chrome flags are opt-in and behave as the type reads: omit one and\nyou get nothing. The harness applies the host's own default (`?? false`) in its\ncopy of the host's grid, so what renders here is what renders in production.\n\nOne of them, though, renders nothing even when you do set it:\n\n**`isPinnable` is plumbed end to end with no consumer.** It reaches\n`WidgetContext` as `meta.isPinnable`, the current value is `state.pinned`, and\n`actions.togglePinned()` works — but no shipped `Widget.*` component draws a\npin control.\n\n**This is faithful, not a harness gap.** The real dashboard does not render a\npin in widget chrome either; it drives pinning from its own loader and\ncustomize panel, outside the widget. So if you want a pin _inside_ your widget,\nit is yours to build, in the harness and in production alike:\n\n```tsx\nconst ctx = use(WidgetContext);\n{\n  ctx?.meta.isPinnable && (\n    <button onClick={ctx.actions.togglePinned}>\n      {ctx.state.pinned ? 'Unpin' : 'Pin'}\n    </button>\n  );\n}\n```\n\n### Derive the widget list — don't write it twice\n\n`HarnessWidget` repeats `id`, `component`, `isCollapsible`, `isPinnable` and\n`aiFooterEnabled` from `components.widgets[]` in your `defineIntegration()`\noutput, renaming `name` to `title`. Writing both by hand is two declarations of\nthe same thing with nothing detecting drift, so don't:\n\n```ts HarnessIntegration\nimport { harnessWidgetsFromDefinition } from '@ekanos/harness/registry';\n\nwidgets: harnessWidgetsFromDefinition(acmeDefinition, {\n  'acme-payments-summary': { width: 'full' },\n}),\n```\n\nThe definition supplies id, title, component and the chrome flags. The\noverrides map supplies what is genuinely harness-only — `width`, and per-widget\n`seeds` if you use them — and is **keyed by widget id**, so `id` is not a field\ninside the override value. An override wins over the definition where both\nspeak.\n\nYour HTTP fixtures are not in here. They sit on the integration\n(`HarnessIntegration.fixtures`), one level up, because a URL is not owned by\none widget.\n\nPass your definition straight in, generic and all — unlike the `definition:`\nfield on the same entry, this does **not** need `asHarnessDefinition()` (the\ngeneric-erasing wrapper the Triggers section below explains). That\nasymmetry is real and easy to trip on: the field stores the definition in a\nheterogeneous array and so needs the generic erased, while the helper only\nreads it.\n\nTwo properties worth knowing, both deliberate:\n\n- **An override key that matches no declared widget throws**, naming the key\n  and listing the ids that do exist. A typo'd id would otherwise produce a\n  widget rendering with none of its overrides and no complaint — the same\n  silent-drift class the helper exists to kill.\n- **A chrome flag your definition does not declare is omitted, not defaulted**,\n  so it still lands on the host's default rather than one the helper invented.\n\n**Call this unless you have a reason not to.** The manual form is fully\nsupported and `widgets` is still an ordinary `HarnessWidget[]` — the helper\njust returns one, so you can spread it and add entries. That is the reason to\nreach for the manual form: a scratch widget or a variant you are trying out\nthat is deliberately _not_ in your definition yet. The harness is exactly where\nyou should be able to try something before declaring it.\n\n```ts HarnessIntegration\nwidgets: [\n  ...harnessWidgetsFromDefinition(acmeDefinition, { /* … */ }),\n  { id: 'scratch-experiment', title: 'Scratch', component: Experiment },\n],\n```\n\n**Why any of this is needed: the harness never reads\n`definition.components.widgets`.** It touches your definition in four places —\nthe egress cross-check, twice in the Triggers surface, and once on the\nactivation page for `capabilities`/`permissions`. Widgets are not among them,\nso on this surface the registry is the _only_ declaration and the definition's\nwidget flags are inert. The helper is how you make the definition the source\nanyway.\n\nOne thing the helper does **not** unify, because the two are genuinely\ndifferent: **widget sizing is two unrelated systems.**\n`components.widgets[].layouts` (`lg`/`md`/`sm` grid rectangles, in the\ndefinition) is what the real dashboard reads. `HarnessWidget.width`\n(`'half' | 'full'`, in the registry) is what the harness reads. Setting\n`layouts` changes nothing here and setting `width` changes nothing in\nproduction, so set both and expect neither to validate the other.\n\nYour widgets are rendered with `HARNESS_ACCOUNT_ID`, `HARNESS_SOURCE_ID` and\n`HARNESS_ACCOUNT_SLUG`, all exported from `@ekanos/harness/registry`. They are\nplausible UUIDs rather than sentinels like `'preview-mode'`, so a widget that\nvalidates the shape of its `accountId` is happy. Nothing reads them — there is\nno database.\n\n## Fixtures\n\n**In fixtures mode the harness answers every request from recorded HTTP\nexchanges and refuses the network.** It patches `globalThis.fetch` itself, so\nthis holds however your widgets fetch — react-query, SWR, a bare `useEffect`, a\npromise started in render and read with `use()`. There is no data layer to opt\ninto and nothing to wire.\n\nYou declare those exchanges on the **integration**, keyed by variant:\n\n```ts\nimport type { HarnessIntegration } from '@ekanos/harness/registry';\n\nimport { TidepoolForecast, TidepoolNow, TidepoolNowEmpty } from './fixtures';\nimport { ForecastWidget, NowWidget } from './widgets';\n\nconst API = 'https://api.tidepool.example.com';\n\nexport const tidepool: HarnessIntegration = {\n  slug: 'tidepool',\n  name: 'Tidepool',\n  description: 'Tide readings and forecast.',\n\n  fixtures: {\n    default: [\n      { request: `GET ${API}/v1/tides/current`, response: TidepoolNow },\n      { request: `GET ${API}/v1/tides/forecast`, response: TidepoolForecast },\n    ],\n    empty: [\n      { request: `GET ${API}/v1/tides/current`, response: TidepoolNowEmpty },\n      {\n        request: `GET ${API}/v1/tides/forecast`,\n        response: { station: '9414290', entries: [] },\n      },\n    ],\n    error: [\n      { request: `GET ${API}/v1/tides/current`, status: 503 },\n      { request: `GET ${API}/v1/tides/forecast`, status: 503 },\n    ],\n  },\n\n  widgets: [\n    { id: 'tidepool-now', title: 'Tide Now', component: NowWidget },\n    {\n      id: 'tidepool-forecast',\n      title: 'Tide Forecast',\n      component: ForecastWidget,\n    },\n  ],\n};\n```\n\n**They sit on the integration, not the widget, because a URL is not owned by\none.** The same `GET /v1/payouts` may answer one widget, a second widget, and a\nwebhook handler on the Triggers surface — and it should be written once. The\nTriggers surface reads this same list, so `ctx.fetch` inside a webhook or\nschedule handler resolves against the fixtures your widgets already use.\n\nThere are exactly three variants, switched from the dev toolbar:\n\n| Variant   | For                                                              |\n| --------- | ---------------------------------------------------------------- |\n| `default` | the populated happy path — declare this one                      |\n| `empty`   | the zero-rows state                                              |\n| `error`   | the failure state, as ordinary responses with a failure `status` |\n\n`empty` and `error` are optional, and an omitted variant is **not** a fallback\nto `default`: the list for that variant is empty, so every request is refused\nand each widget shows the refusal. That is deliberate — a variant that silently\nserved the happy path would look like your empty state working.\n\n**Switching the variant only reaches a widget that re-fetches.** The harness\ntears down and rebuilds the whole subtree on a switch, so a widget fetching in\nan effect or through react-query asks again and gets the new recording. A widget\nthat starts its promise during render and caches it at MODULE scope — the\nobvious way to stop `use()` re-firing on every render — does not: the module\noutlives the remount, so it keeps serving the first variant's answer while the\ntoolbar says `error`. Measured on our own tidepool example, whose forecast\nwidget is exactly that shape: three switches, one `forecast` request, happy-path\ndata showing under every variant.\n\n**The fix is where the promise is created, not how.** Moving the cache into the\ncomponent that calls `use()` — `const [p] = useState(() => fetch(…))` right above\nthe `use(p)` — looks right and is worse: a render that suspends is discarded\nbefore it commits, so the state it created is thrown away and the initializer\nruns again on every retry. Measured on the same three switches, that turns one\nrequest into twenty-six, and the widget still displays correctly the whole time.\n\nCreate the promise in the non-suspending **parent** and pass it across the\nSuspense boundary:\n\n```tsx\nfunction ForecastWidget() {\n  const [request] = useState(() => fetchForecast(FORECAST_URL));\n\n  return (\n    <Suspense fallback={<Loading />}>\n      <ForecastBody request={request} />\n    </Suspense>\n  );\n}\n```\n\nThe parent commits, so its state survives; the child suspends and retries\nagainst the same promise. It still starts during render, so nothing about the\narchetype changes.\n\nThis is a widget-side trap rather than a fixtures one — the recording was\nswapped correctly; nothing asked for it — but it looks exactly like a fixture\nthat did not take, so it is worth recognising. Note what does NOT distinguish\nthe three states: the rendered output. Stuck, storming and correct all look\nidentical on screen, and only a request count tells them apart.\n\n### `HttpFixture` — the request line\n\n```ts-mirror HttpFixture\ninterface HttpFixture {\n  request: string;                     // '<METHOD> <url-or-path>'\n  response?: unknown;                  // body; JSON unless already a string\n  status?: number;                     // default 200 with a body, 204 without\n  headers?: Record<string, string>;\n}\n```\n\n`request` is a method and a target separated by a space. The method is\ncase-insensitive and must be one of `GET`, `HEAD`, `POST`, `PUT`, `PATCH`,\n`DELETE`, `OPTIONS`. The target is one of three forms:\n\n| Form         | Example                               | Resolves against                            |\n| ------------ | ------------------------------------- | ------------------------------------------- |\n| Absolute URL | `GET https://api.acme.com/v1/payouts` | itself — always unambiguous                 |\n| Path only    | `GET /v1/payouts`                     | your `defineIntegration({ egress })` origin |\n| Host route   | `GET /api/integrations/acme/summary`  | the harness's own origin                    |\n\nThe path-only form exists so you do not maintain a second `baseUrl` beside your\negress declaration, and it is allowed **only when your definition declares\nexactly one origin**. With none or with two, the harness cannot make a\ndefensible guess, so `compileFixture` throws while the surface renders — naming\nyour fixture and printing the absolute form to write instead. It never guesses\nand it never skips a fixture it cannot parse.\n\n`/api/…` is the exception to both rules: it is the host-route namespace. A\nfirst-party integration's widgets call `/api/integrations/<slug>/…` rather than\nthe vendor directly, because that is where the credential lives. The harness\nserves no backend, so those would 404; a fixture is the only thing that can\nanswer one, and it is answered like any other.\n\nMatching, in the order the rules apply:\n\n- **Method and origin must be equal.**\n- **Path segments must agree in count.** `:param` matches exactly one segment,\n  `*` matches the rest (including nothing). So `/v1/payouts` does _not_ match\n  `/v1/payouts/42`, but `/v1/payouts/:id` and `/v1/payouts/*` both do.\n- **Query is a subset match, or ignored entirely.** If your request line has a\n  `?`, every param in it must be present on the real request with the same\n  value — so you can pin the two that matter and ignore the other nine. If it\n  has no `?`, query is not consulted at all. That is what makes\n  `GET https://api.acme.com/v1/payouts` answer a request carrying a page\n  cursor, a locale and an API key.\n- **First declaration wins.** Write the specific fixture above the general one.\n\nThe response side has defaults chosen so the common cases are one line.\n`{ request: 'DELETE /v1/thing/:id' }` is a complete fixture: no body, so 204. A\n`response` that is a string is sent as-is with `text/plain`; anything else is\nJSON-serialized with `application/json`. `status` overrides the default, which\nis how an error variant is written — `{ request: 'GET /v1/payouts', status: 503 }`\nis an ordinary recorded response rather than a special code path.\n\nOne constraint comes from the shell rather than from fixtures:\n`harness.config.ts` is imported from both a server component and a client\ncomponent, so **everything reachable from it must be importable in both\ngraphs.** A fixture module carrying a `'use client'` directive compiles and then\nfails at request time with `Attempted to call … from the server`. Keep fixture\ndata and URL constants in a plain module with no directive; put hooks and\ncomponents in the client ones.\n\n### When there is no fixture for a request\n\nThe request is **refused, not sent** — that is the promise fixtures mode makes,\nand it is unconditional. Three things happen, and they are worth being able to\nrecognise:\n\n1. **The console gets a line naming the URL**, from the harness rather than\n   from your code:\n\n   ```\n   [harness:tidepool] refused GET https://api.tidepool.example.com/v1/tides/current?…\n   ```\n\n   Every third-party call is logged this way — `fixture`, `network` (a vendor\n   request in live mode) or `refused` — so the console is the fastest place to\n   see what a widget actually asked for versus what you recorded. A same-origin\n   `/api/…` request logs `fixture` or `refused` in EITHER toolbar mode: it is\n   answered from your registry entry whether the switch says Fixtures or Live\n   API, because the harness serves no backend either way. Only genuinely\n   third-party traffic can log `network`, and only in live mode.\n\n2. **The `fetch` call rejects with `NoRecordedResponseError`**, whose message\n   quotes the fixture to paste:\n\n   ```\n   No recorded response for GET https://api.tidepool.example.com/v1/tides/current?….\n\n   Fixtures mode answers every request from your registry entry and\n   never leaves the machine, so this call was refused rather than sent.\n   Record it:\n\n     fixtures: {\n       default: [{ request: 'GET https://api.tidepool.example.com/v1/tides/current', response: /* … */ }],\n     }\n\n   To exercise the failure branch instead, give it a status:\n\n     { request: 'GET https://api.tidepool.example.com/v1/tides/current', status: 403 }\n\n   The query string is hidden above because it can carry\n   credentials. It is left off the line to paste on purpose: a\n   request with no `?` matches whatever query the widget sends.\n   Add `?key=value` only to pin params you want matched.\n\n   If this URL looks unfamiliar, log it from the code that builds it —\n   the path and any pinned query params must match exactly.\n   ```\n\n3. **Where you see it depends on your own error handling**, because it is an\n   ordinary rejected `fetch`. A react-query widget lands in its error state\n   (the harness sets `retry: false`, so immediately rather than after three\n   backoffs). A widget that starts a promise in render and reads it with\n   `use()` throws, and if it does not catch it itself the harness's per-widget\n   error boundary renders the whole message in the card — one broken widget,\n   not a broken page.\n\n**Credentials are redacted from what is printed.** Any userinfo in the URL is\nstripped and the entire query string is replaced with `?…`, in the console line\nand in the error alike, because an API key in a query param is exactly the thing\nthat ends up in a screenshot. The quoted `request:` line therefore has no query\non it — which is correct as written, since a fixture with no `?` matches\nwhatever query your widget sends. Add `?key=value` back only to pin a param you\nwant matched.\n\nWhen the refused request was a **host route**, the quoted line is the path form\n(`GET /api/integrations/acme/summary`) even though the URL above it is absolute.\nThat is deliberate, not an inconsistency: `/api/…` resolves against whatever port\nthis harness happens to run on, and quoting that back would pin your fixture to\na port that is ours and not yours. Paste it as printed.\n\n**A URL you do not recognise is usually the real answer.** Log it from the code\nthat builds it rather than guessing: the path and any pinned query params have\nto match exactly, and a trailing slash or an extra segment is enough to miss.\n\nOne thing is deliberately **not** intercepted: same-origin traffic outside\n`/api/`. That is `/_next/static`, the document itself and the `?_rsc=` payloads\nclient navigation fetches — the framework's own plumbing, none of it your\nintegration talking to an API.\n\n**The server render is a partial pass, not an exempt one.** It matters because a\nwidget that starts its fetch during render and reads it with `use()` runs there\ntoo. Every fixture is compiled for that pass **except the `/api/` host-route\nshape**, which is the only one that resolves against the harness's own origin —\nand a server render has none. Absolute and path-only fixtures need no origin, so\nthey answer on the server exactly as they do in the browser.\n\nA host-route request during a server render is therefore refused, with a message\nthat says so and tells you there is nothing to change in your registry entry —\nthe client render compiles that fixture and answers it, so what you see after\nhydration is the real result. If you need it answered on the server too, write\nthe fixture and the widget's URL both absolute, at the cost of hardcoding the\ndev server's origin.\n\nSame-origin traffic **outside** `/api/` gets a third message, and it is the one\nworth reading carefully: there is no fixture to write for it. The client passes\nthat traffic straight through as framework plumbing, so it is never refused in\nthe browser — only the server pass has nowhere to send it. If the request was\nmeant to reach a vendor, give it an absolute URL and it becomes an ordinary\nfixture in both passes.\n\n### `seeds` — the react-query fast path\n\n`HarnessWidget.seeds` writes straight into the react-query cache, so a widget\npaints populated with **no request at all**:\n\n```ts HarnessIntegration\nwidgets: [\n  {\n    id: 'acme-payments-summary',\n    title: 'Payments Summary',\n    component: AcmeSummaryWidget,\n    seeds: {\n      default: [{ queryKey: summaryKey, data: acmeSummaryFixture }],\n      empty: [{ queryKey: summaryKey, data: acmeSummaryEmptyFixture }],\n      error: [{ queryKey: summaryKey, error: new Error('Acme returned 503') }],\n    },\n  },\n],\n```\n\nA `FixtureSeed` is `{ queryKey, data }` or `{ queryKey, error }`. Set both and\n`error` wins.\n\n**This is the fastest seam and the narrowest.** It only works if your data layer\nis `@tanstack/react-query`, and it addresses data by query key rather than by\nwhat your vendor returns — so the same payload has to be written again, in a\ndifferent shape, for anything that is not a widget. Reach for it when you want a\nspecific widget to skip the request entirely; otherwise record the exchange.\n\nTwo rules if you do use it:\n\n- **Derive the key from the same factory your hook calls.** Retyping a key array\n  gives you a widget that silently renders empty when someone renames a key;\n  importing the factory makes it a compile error.\n- **A seeded key never fetches, so an HTTP fixture behind it is unreachable.**\n  Seeding short-circuits the request. Use one or the other for a given key, not\n  both.\n\nThere is one error a widget can hit that no HTTP fixture can answer:\n`MissingFixtureError`, thrown when a query has neither a seed nor a `queryFn` of\nits own. Nothing makes a request in that case, so there is nothing to record —\nseed the key.\n\nFinally, `HarnessWidget.seeds` was called `fixtures` before HTTP fixtures\nexisted. It was renamed rather than overloaded: two fields called `fixtures` one\nlevel apart, meaning different things, is a trap worth spending a rename to\navoid.\n\n### Why the format is HTTP and not query keys\n\nBoth of the problems it fixes are ones you would otherwise hit on your first\nintegration:\n\n- **You authored the same vendor data twice, in two shapes, with nothing\n  relating them.** A seed takes `data` in the shape your hook _returns_ —\n  domain-shaped, post-parse. A `triggerMocks.fetchHandlers` entry returns a\n  `Response`, so it is wire-shaped. The shipped Acme example used to carry both:\n  one export feeding the handler's JSON envelope, separate exports feeding the\n  widget seeds. Change one and the other went stale silently, and then the\n  widget and the webhook handler disagreed about the same account.\n- **Only react-query users could express a fixture at all.** `queryKey` was the\n  only way to address one, so a partner fetching with SWR, in a server component\n  with plain `fetch`, or from a `useEffect` had an inert fixtures surface — the\n  harness mocking a cache they did not have rather than the network they did.\n  That was also the population whose requests reached the real internet, because\n  the interception was mounted on the same seams the fixtures were.\n\nRecording the HTTP boundary fixes both at once: one declaration in one shape,\nserving the widget and the handlers alike, with no opinion about how your\ncomponents fetch. `@tanstack/react-query` is still a hard peer of `@ekanos/sdk`\n— `useActivateIntegration` and `useOAuthConnectionStatus` are built on it, and\nthe harness mounts `QueryClientProvider` unconditionally so the activation\nsurface works — so you install it either way. You just no longer have to build\non it to write a fixture.\n\n## Live mode — real requests to your own API\n\nThe harness mocks Fusion, not your vendor. Declare a `live` block and the\ntoolbar grows a fixtures ⇄ live switch: in live mode the harness stops seeding\nyour data keys, your widgets' own query functions run, and their requests reach\nthe real API. There is still no Supabase, no auth and no server actions in\neither mode — but live mode is no longer browser-only. The generated shell\nserves `/api/integrations/<slug>/…` itself (`@ekanos/harness/api`) over one\n**server-held context** per integration: the real OAuth loop, the storage\nbridge your widgets read, and a trigger route the Triggers and Activation\nsurfaces run handlers through. See \"Local OAuth\" below.\n\n```ts HarnessIntegration\nlive: {\n  // No `egress` here: the harness reads it off `definition`.\n  seeds: [{ queryKey: settingsKey, data: acmeSettingsFixture }],\n},\n```\n\n**No partner wiring required.** As of harness 0.1.5, `HarnessProviders` mounts\n`@ekanos/sdk/hooks`'s `IntegrationFetchProvider`/`IntegrationActivationDataProvider`\nunconditionally. A widget that calls `useIntegrationFetch()` gets back a fetch\nthat is already correctly gated for whichever mode the toolbar is in — the\nfixtures-mode refusal or the live-mode allowlist — with nothing declared\nbeyond the `live` block above. The hand-written `FetchProvider` field still\nworks (see \"The legacy seam\" below), but a new integration does not need to\nwrite one.\n\n**The guarantee: live mode enforces the egress your definition declares — not\na second list you maintain beside it.** A request to an origin your definition\ndid not declare throws `EgressDeniedError` on your laptop, carrying the message\na reviewer would have seen at promotion. Flipping the toolbar to live is\ntherefore a real test of the declaration under review, not of a convenience\ncopy that happens to sit next to it. **This is a harness convenience, not a\npreview of production enforcement:** the check runs one layer below\n`useIntegrationFetch()`, at a patched `globalThis.fetch` that can verify an\norigin and a redirect target the way a browser genuinely can — which is not\nthe same as what `ctx.fetch` does on the server (DNS pinning, real redirect\nre-vetting), and production runs no egress check on the client path, at any\nlayer, today. See the \"Network model\" note in a scaffolded project's\n`AGENTS.md` for the fuller statement partners see.\n\n**So when you set `definition`, do not set `live.egress`.** Supplying both is a\nhard error naming your slug, not a silent preference — a silent preference is\nexactly how the two lists became able to disagree. The harness resolves the\nallowlist from `definition.egress` centrally (`resolveEgress`), so there is only\none list and it is the one a reviewer reads.\n\n`live.egress` survives for the one case with nothing to read: an entry with no\n`definition` linked. There it is the only source, and `findEgressMismatch` —\nwhich reports a `live.egress` wider than the definition's — is what still has\nwork to do. A definition declaring more than live mode exercises is fine; the\nreverse is the fault, and while it stands the harness forces fixtures mode and\nsays so on the surface.\n\n**`seeds` are the keys that stay mocked in live mode.** Fusion-side state your\nwidgets read before they can call anything: a saved location, an account\npreference, whatever your own host route would have returned. Everything not\nlisted runs its real query function. That one line is the whole boundary the\ndesign draws: Fusion mocked, vendor real.\n\nAn integration with no `live` block is fixtures-only; the toolbar control is\ndisabled for it and says why. One WITH a `live` block still opens in fixtures\nmode unless it declares `live: { default: 'live' }` — reserve that for an\nintegration whose widgets only talk to the harness's own routes, so opening\nit fires nothing at a vendor.\n\n### Knowing the mode, and the activation-data channel\n\nTwo things a live-mode widget routinely needs: **which mode is actually\nrunning**, and **a credential to authenticate with**.\n\nFor the credential, call `useActivationData()` from `@ekanos/sdk/hooks`. The\nharness mounts `IntegrationActivationDataProvider` unconditionally, so this\nreturns whatever was last submitted to this integration's activation form —\n`null` before any submission or after Disconnect — with no provider of your\nown required. It is held **in memory only**: never written to `localStorage`,\na fixture, or the registry, and cleared on reload and on disconnect. It is a\ndev-only convenience standing in for `ctx.secrets`, which is what production\nactually hands your server-side handlers.\n\nFor the mode itself, call `useHarnessDataMode()` from `@ekanos/harness/hooks`.\nThis one has no SDK equivalent and stays harness-only — production runs\nexactly one mode, so there is nothing for a host-side hook to report. It\nreturns the EFFECTIVE mode (`'fixtures' | 'live'`), already accounting for an\nintegration with no `live` block or a `live.egress`/definition mismatch (both\nforce `'fixtures'`, whatever the toolbar says):\n\n```tsx\nimport { useHarnessDataMode } from '@ekanos/harness/hooks';\nimport { useActivationData, useIntegrationFetch } from '@ekanos/sdk/hooks';\n\nfunction useAcmeSummary(accountId: string) {\n  const fetch = useIntegrationFetch();\n  const mode = useHarnessDataMode();\n  const activationData = useActivationData<{ apiToken?: string }>();\n\n  // ...\n}\n```\n\nA hand-written `FetchProvider` — where one already exists — receives all\nthree (`fetch`, `mode`, `activationData`) as props too, no extra plumbing\nrequired; see \"The legacy seam\" below for when writing one is still the\nbetter call over the hooks above (and for the same token-threading example,\nwritten the `FetchProvider` way).\n\nReach for `useHarnessDataMode()`/`useActivationData()` directly (as above)\nrather than the props on a `FetchProvider` wherever you can — they need no\n`FetchProvider` of your own, and `useIntegrationFetch()`/`useActivationData()`\nbehave identically whether or not `@ekanos/harness` is even installed at the\ncall site (a plain fetch and `null`, outside the harness, until a host mounts\nits own provider). `useHarnessDataMode()` is the one exception with no SDK\nequivalent — call it only from code that runs nowhere but inside the dev\nharness.\n\n### `FetchProvider` — the legacy seam, still supported\n\n> **No longer the default for a new integration.** `useIntegrationFetch()`\n> from `@ekanos/sdk/hooks` already returns a fetch gated correctly for\n> whichever mode the harness toolbar is in, with nothing to write — see\n> \"No partner wiring required\" above. Reach for a hand-written\n> `FetchProvider` only for the one case it still earns its keep: composing an\n> authenticated fetch ONCE, at a single seam (e.g. attaching an\n> `Authorization` header derived from `activationData` to every request your\n> query functions make), instead of repeating that composition in every query\n> function. If that is not a problem you have, skip this section.\n>\n> The harness patches `globalThis.fetch` underneath either path, so a\n> `FetchProvider` is not an escape hatch from the harness's live-mode egress\n> check — see \"Network model\" in a scaffolded project's `AGENTS.md` for what\n> that check does and does not mean once you leave the harness.\n\nThe provider receives `fetch`, `mode`, and `activationData` as props, no extra\nplumbing required — this is, in miniature, the same ~twenty-line pattern\n`@ekanos/sdk/hooks` now ships as `IntegrationFetchProvider`/\n`useIntegrationFetch()`:\n\n```tsx\n'use client';\n\nimport { type ReactNode, createContext, use } from 'react';\n\nimport type { DataMode } from '@ekanos/harness/registry';\nimport type { IntegrationFetch } from '@ekanos/sdk';\n\nconst browserFetch: IntegrationFetch = (input, init) =>\n  globalThis.fetch(input as RequestInfo, init);\n\nconst AcmeFetchContext = createContext<IntegrationFetch>(browserFetch);\n\nexport function AcmeFetchProvider({\n  fetch,\n  children,\n}: {\n  fetch: IntegrationFetch;\n  mode: DataMode;\n  activationData: unknown;\n  children: ReactNode;\n}) {\n  return <AcmeFetchContext value={fetch}>{children}</AcmeFetchContext>;\n}\n\n/** The fetch this integration's query functions should use. */\nexport function useAcmeFetch(): IntegrationFetch {\n  return use(AcmeFetchContext);\n}\n```\n\nComposing the token onto every request, the one case this seam is still for:\n\n```tsx\nexport function AcmeFetchProvider({\n  fetch,\n  activationData,\n  children,\n}: {\n  fetch: IntegrationFetch;\n  mode: DataMode;\n  activationData: unknown;\n  children: ReactNode;\n}) {\n  const token = (activationData as { apiToken?: string } | null)?.apiToken;\n\n  const authedFetch: IntegrationFetch = (input, init) =>\n    fetch(input, {\n      ...init,\n      headers: {\n        ...(init?.headers as Record<string, string> | undefined),\n        ...(token ? { Authorization: `Bearer ${token}` } : {}),\n      },\n    });\n\n  return <AcmeFetchContext value={authedFetch}>{children}</AcmeFetchContext>;\n}\n```\n\nEither way — the SDK's hook or a hand-rolled one — have your query functions\n**take an `IntegrationFetch` as an argument** rather than reaching for a\nglobal:\n\n```ts\nimport type { IntegrationFetch } from '@ekanos/sdk';\n\nexport async function fetchAcmeSummary(\n  fetch: IntegrationFetch,\n  accountId: string,\n) {\n  const response = await fetch(\n    `https://api.acme.example/v1/summary?a=${accountId}`,\n  );\n  if (!response.ok) throw new Error(`Acme returned ${response.status}.`);\n  return response.json();\n}\n\n// in the hook\nconst fetch = useIntegrationFetch(); // or useAcmeFetch() for the legacy seam\nuseQuery({\n  queryKey: acmeKeys.summary(accountId),\n  queryFn: () => fetchAcmeSummary(fetch, accountId),\n});\n```\n\nThat is the value of the pattern, beyond the harness: the _same_ code path\nserves `ctx.fetch` on the server and this hook's fetch on the client — gated\nby the harness's own check in dev, and a plain, unmediated browser fetch in\nproduction today (see \"Network model\" in `AGENTS.md`).\n\nThe provider's props are fixed by the type — `{ fetch: IntegrationFetch; mode:\nDataMode; activationData: unknown; children: ReactNode }` — because the harness\nmounts it, alongside (not instead of) the SDK's own providers, whenever an\nentry declares one. Omit `FetchProvider` entirely if your widgets fetch only\nthrough your own host routes: those are answered from fixtures (record them\nunder `/api/…`) in EVERY toolbar mode, live included — there is no Supabase\nand no `ctx` to execute a route with, so there is nothing \"live\" to run for\nthat traffic either way.\n\n## Triggers — running webhooks, schedules and OAuth locally\n\nSet `definition` on the registry entry and the Triggers surface reads your\ndeclared webhooks, schedules and OAuth straight off it. Omit it and the surface\nexplains what to add.\n\n```ts HarnessIntegration\nimport { asHarnessDefinition } from '@ekanos/harness/registry';\n\nimport { integration as acmeDefinition } from '@you/acme-payments/integration';\n\ndefinition: asHarnessDefinition(acmeDefinition),\n```\n\n`asHarnessDefinition()` erases the storage generic so the definition can sit in\nthe heterogeneous registry array — the same erasure the host performs when it\nregisters you. Nothing rests on the generic surviving: the mock context is built\nFROM the definition's own `storage` schemas and validates every read and write\nagainst them at runtime.\n\nWhat the surface then does:\n\n- **Webhooks** get a payload editor, seeded from `examplePayload`. Delivering\n  runs your REAL handler. The payload is validated against `payloadSchema`\n  first; an invalid one never reaches the handler. Signature verification is\n  logged as skipped — it is the transport's job in every environment.\n- **Schedules** get a \"Run now\" button, which invokes the handler with\n  `trigger: 'manual'`.\n- **OAuth** gets the declaration readout, an egress-coverage check on both\n  endpoint origins, and the **Local OAuth** panel — in live mode, the real\n  authorize → callback → `onTokens` loop runs through the shell against your\n  own developer app. See the next section.\n\nIn **fixtures** mode every invocation runs in the browser against **one**\n`createMockContext()` per visit, seeded from `triggerMocks`, so state\naccumulates across invocations the way it would in a real account and leaving\nthe page resets it. In **live** mode the same buttons post to\n`/api/integrations/<slug>/trigger` and the handlers run on the **server-held\nlive context** — the one the OAuth loop stored tokens in, whose `ctx.fetch`\nreaches your vendor under the declared egress. Same result panel either way.\n\n**`onActivate` runs on the Activation surface, not here.** Submitting the\nactivation form there runs your real `onActivate` hook (if declared) against\nthe same context the mode above selects — the browser mock in fixtures, the\nlive context in live — and shows the outcome — ran / skipped (not declared) /\nthrew with the message — right under the connect dialog.\n\n### Local OAuth — the loop runs here, in live mode\n\nFor an integration whose definition declares `oauth`:\n\n1. Switch the toolbar's **Data** dropdown to **Live API** (the entry needs a\n   `live` block). If your widgets never call the vendor from the browser — the\n   OAuth shape, where every vendor call is server-side behind `ctx.fetch` —\n   declare `live: { default: 'live' }` and the harness starts there; a choice\n   made at the toolbar still wins and is remembered.\n2. The **Local OAuth** panel on the Activation and Triggers surfaces opens\n   with the part only a human can do: a checklist for your provider's developer\n   console — create the app, add the redirect URI\n   `http://localhost:3100/api/integrations/<slug>/oauth/callback` (Copy button),\n   enable the declared scopes, allow the code grant (with PKCE when declared),\n   copy the client id and secret. Everything there is derived from your\n   declaration; add `oauth.setup` to the harness entry for the two things it\n   cannot know — where the console is and what the provider calls each key:\n\n   ```ts HarnessIntegration\n   oauth: {\n     setup: {\n       consoleUrl: 'https://admindemo.docusign.com/apps-and-keys',\n       consoleLabel: 'DocuSign Admin → Apps and Keys',\n       notes: ['Integration Key is the client id; add a Secret Key — shown once.'],\n     },\n   },\n   ```\n\n   Below the checklist is a form with one field per secret name your\n   definition declares\n   (`oauth.credentials.clientIdSecretName`, `clientSecretSecretName`, and every\n   signed webhook's `secretName`). Paste the values and **Save credentials**.\n   The shell writes them to **`.ekanos/local-secrets.json`** at your project\n   root (gitignored, never packed by `ekanos publish`, read on every request)\n   and never echoes them back — a saved key shows as `filled` beside an empty\n   field; a blank field keeps what is saved. The panel works in fixtures mode\n   too, so you can set up before switching.\n3. Click **Connect** in the activation dialog. The shell's `oauth/connect`\n   route builds the authorize URL (PKCE when declared) and sends the browser\n   to your provider; `oauth/callback` verifies the single-use `state`,\n   exchanges the code through `ctx.fetch` — an undeclared token-endpoint origin\n   fails with `EgressDeniedError` exactly as on the host — and awaits your\n   `onTokens` on the live context. `oauth/status` then answers connected, the\n   SDK's `OAuthActivationForm` auto-activates, and `onActivate` runs on that\n   same context.\n4. Your widgets read what those handlers wrote through the ordinary\n   `fetchIntegrationStorage` route, unchanged; an empty read runs your declared\n   schedules once first, as the host does.\n\nThe connection is **memory-only**: restarting the dev server forgets it and you\nConnect again. Editing a value in the secrets file rebuilds the context and\nforgets it too. The API routes refuse any request whose `Host` is not loopback\nunless `EKANOS_HARNESS_EXPOSED=1` — `ekanos dev --host` sets that, after its\nwarning.\n\nIn **fixtures** mode none of this runs. The harness answers `oauth/status`\nfrom the entry's `oauth.connected` — connected by default, so an OAuth widget\nrenders with zero traffic and you do not hand-write that fixture:\n\n```ts HarnessIntegration\noauth: { connected: { empty: false } }, // show the Connect prompt on `empty`\n```\n\n### `triggerMocks`\n\nThe mock context is derived from the definition — slug, storage schemas, egress\n— and from your `fixtures`, which answer `ctx.fetch` here exactly as they answer\na widget. `triggerMocks` supplies the rest: whatever Fusion-side state your\nhandlers need to run a happy path.\n\n**So do not re-author your vendor payloads as `fetchHandlers`.** That was the\ndouble-authoring HTTP fixtures exist to kill — the same data in a domain shape\nfor widgets and a wire shape for handlers, with nothing relating them. Reach for\n`fetchHandlers` only when a response has to be dynamic or stateful; an explicit\nhandler is tried first and wins for the URLs it matches, so you keep fixtures\nfor everything else.\n\nOne behaviour worth knowing: a handler calling an endpoint nothing answers gets\nthe same named `NoRecordedResponseError` a widget gets. `createMockContext()`'s\nown fallback is a plausible empty `200` — the trigger-side twin of a silent\nfixture miss — and the harness replaces it.\n\n```ts HarnessIntegration\ntriggerMocks: {\n  // Storage rows per scope, validated against your declared schemas.\n  storage: {\n    account: { config: { merchantId: 'mrc_4820193', environment: 'sandbox' } },\n    user: {},\n  },\n  // Secrets by tier. `account` is writable via ctx.secrets.set(); `admin`\n  // stands in for admin-issued credentials and rejects writes.\n  secrets: {\n    account: { acme_api_key: 'acme_test_9f2c41ab' },\n  },\n  // ONLY for a response that has to be dynamic or stateful — your `fixtures`\n  // already answer ctx.fetch. A string `match` matches URLs that start with\n  // it; a RegExp is tested against the full URL. Tried before the fixtures.\n  fetchHandlers: [\n    {\n      match: 'https://api.acme.example',\n      respond: () => Response.json({ payments: [], totalCount: 0 }),\n    },\n  ],\n},\n```\n\nThere is no Supabase and no network in here either way. Keep the values fake —\nsee the bundle note below.\n\n## Actions — invoking declared server handlers from a widget\n\n`useIntegrationAction()` (`@ekanos/sdk/hooks`) is the write counterpart to\n`useIntegrationFetch()`/`fetchIntegrationStorage()`: a widget's Save button\nposts the user's own typed values straight to a declared `actions[name]`\nhandler — deterministic, no assistant, no natural-language round trip.\nUnlike a widget's read path, there is no client-side mock to short-circuit —\nthe hook always makes a real `fetch()` — so the harness answers\n`POST /api/integrations/<slug>/actions/<actionName>` itself, in BOTH modes,\nover the same local API router that already serves storage, OAuth, webhooks\nand schedules (`createHarnessApiHandlers`). What differs between modes is\nwhich context resolves the call, exactly as it does for Triggers above:\n\n- **Fixtures (default).** The route validates the posted input against the\n  action's declared `input` schema exactly like production, then runs\n  `handler(ctx, input)` against one `createMockContext()` — the identical\n  mechanism `invokeWebhook`/`invokeSchedule` already use. An action is\n  executed the same way a trigger is; it's just invoked from a rendered\n  widget's own Save button instead of the Triggers page's \"Run now\".\n- **Live (opt-in).** The route runs the handler against the same\n  **server-held live context** per integration that OAuth and live schedules\n  already use — its `ctx.fetch` reaches your real vendor under the\n  definition's declared `egress`, with credentials from\n  `.ekanos/local-secrets.json` or whatever `onTokens`/the activation form\n  stored. Requires the entry's `live` block, same as every other live\n  surface — an integration with no `live` block stays on fixtures for its\n  actions exactly as it does for triggers.\n\n### The session confirm\n\nAn action is a write, and unlike Triggers' dedicated \"Run now\" button, it\nfires from **ordinary widget interaction** — a partner debugging an\nunrelated CSS issue on the same widget while the toolbar happens to be on\nLive could otherwise trigger a real vendor write without meaning to touch\ndata at all. So the first `useIntegrationAction()` call in a dev-server\nsession while the toolbar's **Data** mode is Live raises a confirmation\n(\"This will run against real `<vendor>` data\") before it reaches the live\ncontext; acknowledging it (\"allow for this session\") lets every subsequent\naction call that session proceed without asking again. Reloading the page or\nrestarting the dev server resets it, same as the live connection itself.\n\n**This is a harness convenience for accidental clicks during iteration, not\na production security boundary.** Production runs exactly one mode — there\nis no fixtures/live toggle and no per-session confirmation to bypass — and\nenforces the real thing through `ctx.fetch`'s egress allowlist regardless of\nanything the toolbar or this confirm ever did. Do not describe the\nper-session confirm to a partner as protection that carries over once the\nintegration is published.\n\n### Pinned error codes\n\nThe route answers the same fixed `code`s the production host route does, so\na widget's error handling written against the harness behaves identically\nonce it ships for real: `action_not_found` (404, unknown action name),\n`invalid_input` (400, with the partner's own zod `issues`), `rate_limited`\n(429), and `handler_error` (500 — a handler threw, or its result failed the\noptional `output` schema; opaque by default). A handler that throws\n`PublicActionError` (`@ekanos/sdk/integration`) gets its own `message` shown\nverbatim under that same `handler_error` code — the one escape hatch from\nthe generic message. `useIntegrationAction()`'s rejected mutation always\nsurfaces these as a typed `IntegrationActionError` (`status`, `code`,\n`issues?`), plus two transport-only codes the hook assigns itself —\n`network_error` / `invalid_response` — for a request that never reached a\nvalid envelope at all. The harness does not itself enforce the production\nrate limiter (there is exactly one developer hitting this route locally);\nit recognizes and can answer `rate_limited` the same way, but nothing here\nexercises it under normal use.\n\n## What ends up in the browser bundle\n\n**Everything in your registry does, including `triggerMocks.secrets`.**\n\nThe shell's injection module is a client module — it has to be, because the\nregistry holds live React component references — so the whole of\n`harness.config.ts` is compiled into the browser bundle and written into the\n`.next` build output on disk. There is no boundary that can prevent this while\nthe registry still reaches the routes, so the harness warns at startup rather\nthan pretending otherwise:\n\n```\n[harness] acme-widgets seed triggerMocks.secrets, and those literals are\ncompiled into the browser bundle and written into the .next build output …\n```\n\nIn practice this is your own mock values on your own machine, which is why it\nis a warning and not a refusal. Two things follow from it:\n\n- **Keep trigger mocks fake.** They are seeds for a local mock context, not\n  credentials — nothing in the harness ever authenticates.\n- **Do not commit or deploy the generated build directory.** `.ekanos/` is\n  gitignored for you; the `.next` output inside it is a build artifact.\n\nRelated, and deliberate: the activation surface hides credential-shaped fields\nin its payload readout behind a **Reveal values** toggle, and the fixture\nactivation actions log a redacted copy. That readout is the one place a real\nAPI token predictably appears on screen.\n\n## Exports\n\n| Subpath                                        | Contents                                                                                                                                                                                                                                                                   |\n| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `@ekanos/harness/registry`                     | `HarnessIntegration`, `HarnessWidget`, `HttpFixture`, `FixtureSeed`, `FixtureVariant`, `HarnessLiveMode`, `HarnessTriggerMocks`, the `HARNESS_*` ids and the pure helpers. Server-safe; this is the type surface you author against.                                       |\n| `@ekanos/harness/config`                       | `defineHarnessConfig()` — an identity function that exists for the inference.                                                                                                                                                                                              |\n| `@ekanos/harness/app`                          | `RootLayout`, `IndexPage`, `harnessMetadata`.                                                                                                                                                                                                                              |\n| `@ekanos/harness/routes`                       | `IntegrationLayout`, `WidgetsPage`, `SingleWidgetPage`, `TilePage`, `ActivationPage`, `TriggersPage`.                                                                                                                                                                      |\n| `@ekanos/harness/hooks`                        | `useHarnessDataMode()`, `useLiveActivationData()` — see \"Knowing the mode, and the activation-data channel\" above. Prefer `FetchProvider`'s own `mode`/`activationData` props where you can; reach for these only from code that runs nowhere but inside the dev harness.  |\n| `@ekanos/harness/api`                          | `createHarnessApiHandlers(registry)` — the SERVER entry the shell's one API route injects your registry into. Serves `oauth/{connect,callback,status}`, `storage`, `trigger` and `local-setup` per integration in live mode. Never import it from a `'use client'` module. |\n| `@ekanos/harness/styles.css`                   | The harness's Tailwind layer: the Tailwind entry, the `@ekanos/ui` token preset, the Font Awesome repairs, and the `@source` globs covering everything the chrome renders.                                                                                                 |\n| `@ekanos/harness/mocks/team-account-workspace` | The stand-in for the host's `useTeamAccountWorkspace()`. The generated `next.config.mjs` aliases `@kit/team-accounts/hooks/use-team-account-workspace` onto it, so unmodified widget source runs unchanged.                                                                |\n\n## The shell contract\n\n`package.json` declares `ekanos.shellContract`. `@ekanos/cli` reads it before\nscaffolding and refuses if its templates generate a different one, naming which\nside to upgrade.\n\nBump it when the SHELL this package requires changes — an export renamed or\nmoved between `/app` and `/routes`, the props of `RootLayout` or `IndexPage`,\nthe server/client split, the alias specifier, the stylesheet's ownership of the\nTailwind import, or the set of files the shell must contain. A release that only\nfixes a bug inside the package leaves it alone. History: **1** the original\nten-file shell; **2** (0.2.0) adds `app/api/integrations/[slug]/[...path]/route.ts`\ninjecting the registry into `@ekanos/harness/api`.\n\n**A shell change also needs a `@ekanos/cli` release.** The templates live there;\nonly the code they import lives here. `ekanos dev` rewrites a generated file\nonly when its bytes change, so bumping the harness alone regenerates nothing —\nthe old CLI's templates are exactly what the old CLI intends. The contract\ninteger is what turns that silent mismatch into a refusal.\n\n**What it does NOT cover is your `harness.config.ts`.** The contract is checked\nbefore the CLI writes the generated shell, so it guards `.ekanos/harness/**` —\nour files. Yours is bootstrapped once and never touched again, deliberately,\nbecause your fixtures live in it. So if we rename something in the registry\ntypes you write against, it reaches you as a TypeScript error in a file no tool\nwill migrate for you. That is the right trade — we are not editing your file —\nbut it means **the release notes are the migration path**, and you should read\nthem on a minor bump rather than only on a major. The `HarnessWidget.fixtures`\n→ `seeds` rename — made when `HarnessIntegration.fixtures` took the name — is\nexactly this shape.\n\n## Building it\n\n```bash\npnpm --filter @ekanos/harness build\n```\n\n`@ekanos/ui` must be built first — `styles.css` imports its token preset.\n\nThe build compiles **per file** rather than bundling (`bundle: false` in\n`tsup.config.ts`), and that is load-bearing in three separate ways: the\nserver/client boundary runs _through_ the `/app` entry rather than around it,\n`next/font/google` requires its call to survive as a `const` (esbuild's bundler\nrewrites top-level `const` to `var`, which Next rejects outright), and there is\nnothing to inline in the first place. Each of those is a bug we have already\nshipped once; the build config in the source tree carries the long version.\n\n`pnpm --filter @ekanos/harness pack:test` re-checks all of that on the actual\npublished tarball, in a clean room outside the workspace.\n\n## Licence\n\nMIT. Font Awesome is the consuming app's responsibility: this package ships no\nicon-font data, and the generated shell loads Font Awesome **Free**, because\nFusion's own Pro webfonts are commercially licensed and cannot be redistributed.\n","readmeFilename":""}