diff --git a/package.json b/package.json index 840ad15..2383118 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,13 @@ "supabase:dev": "supabase start --ignore-health-check" }, "dependencies": { + "@assembly-js/node-sdk": "^4.2.2", "@sentry/nextjs": "^10", "@trigger.dev/sdk": "4.0.6", "bottleneck": "^2.19.5", "camelcase-keys": "^10.0.1", "clsx": "^2.1.1", "copilot-design-system": "^2.2.6", - "copilot-node-sdk": "^3.16.0", "dayjs": "^1.11.19", "dotenv": "^17.2.1", "drizzle-orm": "^0.44.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74b1ccc..3ddb472 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@assembly-js/node-sdk': + specifier: ^4.2.2 + version: 4.2.2 '@sentry/nextjs': specifier: ^10 version: 10.25.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@15.5.18(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.7(react@19.1.7))(react@19.1.7))(react@19.1.7)(webpack@5.103.0(esbuild@0.25.9)) @@ -26,9 +29,6 @@ importers: copilot-design-system: specifier: ^2.2.6 version: 2.2.6(@types/react-dom@19.1.7(@types/react@19.1.11))(@types/react@19.1.11)(react-dom@19.1.7(react@19.1.7))(react@19.1.7)(typescript@5.9.2) - copilot-node-sdk: - specifier: ^3.16.0 - version: 3.16.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.7(react@19.1.7))(react@19.1.7) dayjs: specifier: ^1.11.19 version: 1.11.19 @@ -193,6 +193,10 @@ packages: '@apm-js-collab/tracing-hooks@0.3.1': resolution: {integrity: sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==} + '@assembly-js/node-sdk@4.2.2': + resolution: {integrity: sha512-zK7T9DuENmP4D7qyt9Xpblvo0d3WcBvh3rc3WQWimOoyvEOGyX/3IZ+oiCiTjmpqNisqYYa3MkZkbm1Ilr6fOA==} + engines: {node: '>=18'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -999,108 +1003,51 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@14.2.32': - resolution: {integrity: sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==} - '@next/env@15.5.18': resolution: {integrity: sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==} - '@next/swc-darwin-arm64@14.2.32': - resolution: {integrity: sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - '@next/swc-darwin-arm64@15.5.18': resolution: {integrity: sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.32': - resolution: {integrity: sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - '@next/swc-darwin-x64@15.5.18': resolution: {integrity: sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.32': - resolution: {integrity: sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - '@next/swc-linux-arm64-gnu@15.5.18': resolution: {integrity: sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.32': - resolution: {integrity: sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - '@next/swc-linux-arm64-musl@15.5.18': resolution: {integrity: sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.32': - resolution: {integrity: sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@next/swc-linux-x64-gnu@15.5.18': resolution: {integrity: sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.32': - resolution: {integrity: sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - '@next/swc-linux-x64-musl@15.5.18': resolution: {integrity: sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.32': - resolution: {integrity: sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - '@next/swc-win32-arm64-msvc@15.5.18': resolution: {integrity: sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.32': - resolution: {integrity: sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@next/swc-win32-x64-msvc@14.2.32': - resolution: {integrity: sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@next/swc-win32-x64-msvc@15.5.18': resolution: {integrity: sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==} engines: {node: '>= 10'} @@ -2030,15 +1977,9 @@ packages: '@supabase/supabase-js@2.56.1': resolution: {integrity: sha512-cb/kS0d6G/qbcmUFItkqVrQbxQHWXzfRZuoiSDv/QiU6RbGNTn73XjjvmbBCZ4MMHs+5teihjhpEVluqbXISEg==} - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@swc/helpers@0.5.5': - resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} - '@tailwindcss/node@4.1.12': resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==} @@ -2552,10 +2493,6 @@ packages: resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} engines: {node: '>=10.0.0'} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - byline@5.0.0: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} @@ -2576,9 +2513,6 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001737: - resolution: {integrity: sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==} - caniuse-lite@1.0.30001756: resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==} @@ -2709,9 +2643,6 @@ packages: react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 - copilot-node-sdk@3.16.0: - resolution: {integrity: sha512-wqqeJuW+uge39nC1/+cqMWSSgCCNThtCR+JykPYFB2PTj0d0tfROCXofwtqZZccUUzk/xXXkJF53QfweVxmMLg==} - copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -3431,9 +3362,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-fetch@3.0.0: - resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -3835,25 +3763,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@14.2.32: - resolution: {integrity: sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg==} - engines: {node: '>=18.17.0'} - deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - sass: - optional: true - next@15.5.18: resolution: {integrity: sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -4393,10 +4302,6 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - streamx@2.28.0: resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} @@ -4447,19 +4352,6 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - styled-jsx@5.1.1: - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -4857,9 +4749,6 @@ packages: webpack-cli: optional: true - whatwg-fetch@3.6.20: - resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4993,6 +4882,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@assembly-js/node-sdk@4.2.2': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -5723,58 +5614,29 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@14.2.32': {} - '@next/env@15.5.18': {} - '@next/swc-darwin-arm64@14.2.32': - optional: true - '@next/swc-darwin-arm64@15.5.18': optional: true - '@next/swc-darwin-x64@14.2.32': - optional: true - '@next/swc-darwin-x64@15.5.18': optional: true - '@next/swc-linux-arm64-gnu@14.2.32': - optional: true - '@next/swc-linux-arm64-gnu@15.5.18': optional: true - '@next/swc-linux-arm64-musl@14.2.32': - optional: true - '@next/swc-linux-arm64-musl@15.5.18': optional: true - '@next/swc-linux-x64-gnu@14.2.32': - optional: true - '@next/swc-linux-x64-gnu@15.5.18': optional: true - '@next/swc-linux-x64-musl@14.2.32': - optional: true - '@next/swc-linux-x64-musl@15.5.18': optional: true - '@next/swc-win32-arm64-msvc@14.2.32': - optional: true - '@next/swc-win32-arm64-msvc@15.5.18': optional: true - '@next/swc-win32-ia32-msvc@14.2.32': - optional: true - - '@next/swc-win32-x64-msvc@14.2.32': - optional: true - '@next/swc-win32-x64-msvc@15.5.18': optional: true @@ -6738,17 +6600,10 @@ snapshots: - bufferutil - utf-8-validate - '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 - '@swc/helpers@0.5.5': - dependencies: - '@swc/counter': 0.1.3 - tslib: 2.8.1 - '@tailwindcss/node@4.1.12': dependencies: '@jridgewell/remapping': 2.3.5 @@ -7361,10 +7216,6 @@ snapshots: buildcheck@0.0.7: optional: true - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - byline@5.0.0: {} call-bind-apply-helpers@1.0.2: @@ -7383,8 +7234,6 @@ snapshots: camelcase@8.0.0: {} - caniuse-lite@1.0.30001737: {} - caniuse-lite@1.0.30001756: {} chai@6.2.2: {} @@ -7519,20 +7368,6 @@ snapshots: - supports-color - typescript - copilot-node-sdk@3.16.0(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.7(react@19.1.7))(react@19.1.7): - dependencies: - isomorphic-fetch: 3.0.0 - next: 14.2.32(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.7(react@19.1.7))(react@19.1.7) - transitivePeerDependencies: - - '@babel/core' - - '@opentelemetry/api' - - '@playwright/test' - - babel-plugin-macros - - encoding - - react - - react-dom - - sass - copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -8202,13 +8037,6 @@ snapshots: isexe@2.0.0: {} - isomorphic-fetch@3.0.0: - dependencies: - node-fetch: 2.7.0 - whatwg-fetch: 3.6.20 - transitivePeerDependencies: - - encoding - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -8545,32 +8373,6 @@ snapshots: neo-async@2.6.2: {} - next@14.2.32(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.7(react@19.1.7))(react@19.1.7): - dependencies: - '@next/env': 14.2.32 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 - caniuse-lite: 1.0.30001737 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 19.1.7 - react-dom: 19.1.7(react@19.1.7) - styled-jsx: 5.1.1(@babel/core@7.28.5)(react@19.1.7) - optionalDependencies: - '@next/swc-darwin-arm64': 14.2.32 - '@next/swc-darwin-x64': 14.2.32 - '@next/swc-linux-arm64-gnu': 14.2.32 - '@next/swc-linux-arm64-musl': 14.2.32 - '@next/swc-linux-x64-gnu': 14.2.32 - '@next/swc-linux-x64-musl': 14.2.32 - '@next/swc-win32-arm64-msvc': 14.2.32 - '@next/swc-win32-ia32-msvc': 14.2.32 - '@next/swc-win32-x64-msvc': 14.2.32 - '@opentelemetry/api': 1.9.0 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - next@15.5.18(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.7(react@19.1.7))(react@19.1.7): dependencies: '@next/env': 15.5.18 @@ -9201,8 +9003,6 @@ snapshots: std-env@4.1.0: {} - streamsearch@1.1.0: {} - streamx@2.28.0: dependencies: events-universal: 1.0.1 @@ -9262,13 +9062,6 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.1(@babel/core@7.28.5)(react@19.1.7): - dependencies: - client-only: 0.0.1 - react: 19.1.7 - optionalDependencies: - '@babel/core': 7.28.5 - styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.1.7): dependencies: client-only: 0.0.1 @@ -9643,8 +9436,6 @@ snapshots: - esbuild - uglify-js - whatwg-fetch@3.6.20: {} - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/src/config/server.env.ts b/src/config/server.env.ts index 9db5940..6f947c5 100644 --- a/src/config/server.env.ts +++ b/src/config/server.env.ts @@ -3,17 +3,34 @@ import 'server-only' import { z } from 'zod' -const ServerEnvSchema = z.object({ - COPILOT_API_KEY: z.string().min(1), - DATABASE_URL: z.url(), - DROPBOX_APP_KEY: z.string().min(1), - DROPBOX_APP_SECRET: z.string().min(1), - DROPBOX_REDIRECT_URI: z.url(), - DROPBOX_SCOPES: z.string().min(1), - DROPBOX_API_URL: z.url(), - TRIGGER_MACHINE: TriggerMachineSchema, - WEBHOOK_CATCHUP_CRON: z.string().min(1), -}) +// Env modes where the SDK authorizes with the workspace-scoped key (no token). +const ASSEMBLY_KEYLESS_ENV_MODES = ['local', '__SECRET_STAGING__'] + +const ServerEnvSchema = z + .object({ + COPILOT_API_KEY: z.string().min(1), + DATABASE_URL: z.url(), + DROPBOX_APP_KEY: z.string().min(1), + DROPBOX_APP_SECRET: z.string().min(1), + DROPBOX_REDIRECT_URI: z.url(), + DROPBOX_SCOPES: z.string().min(1), + DROPBOX_API_URL: z.url(), + TRIGGER_MACHINE: TriggerMachineSchema, + WEBHOOK_CATCHUP_CRON: z.string().min(1), + ASSEMBLY_ENV: z.string().optional(), + COPILOT_ENV: z.string().optional(), + }) + .superRefine((val, ctx) => { + const envMode = val.ASSEMBLY_ENV ?? val.COPILOT_ENV + if (!envMode || !ASSEMBLY_KEYLESS_ENV_MODES.includes(envMode)) { + ctx.addIssue({ + code: 'custom', + path: ['COPILOT_ENV'], + message: + 'COPILOT_ENV (or ASSEMBLY_ENV) must be "local". Set it in every runtime (Vercel, Trigger.dev).', + }) + } + }) const env = ServerEnvSchema.parse(process.env) export default env diff --git a/src/features/sync/lib/Sync.service.ts b/src/features/sync/lib/Sync.service.ts index 34b0f75..7599f43 100644 --- a/src/features/sync/lib/Sync.service.ts +++ b/src/features/sync/lib/Sync.service.ts @@ -25,7 +25,7 @@ import type { WhereClause, } from '@/features/sync/types' import { copilotBottleneck } from '@/lib/copilot/bottleneck' -import { CopilotAPI, isCopilotApiError } from '@/lib/copilot/CopilotAPI' +import { type CopilotAPI, isCopilotApiError } from '@/lib/copilot/CopilotAPI' import type User from '@/lib/copilot/models/User.model' import type { CopilotFileRetrieve } from '@/lib/copilot/types' import AuthenticatedDropboxService from '@/lib/dropbox/AuthenticatedDropbox.service' @@ -478,7 +478,7 @@ export class SyncService extends AuthenticatedDropboxService { return } - const copilotApi = new CopilotAPI(this.user.token) + const copilotApi = this.copilot const fileCreateResponse = await copilotApi.createFile( assemblyCreatePath, assemblyChannelId, @@ -573,7 +573,7 @@ export class SyncService extends AuthenticatedDropboxService { entry, } = params const target = assemblyCreatePath.toLowerCase() - const copilotApi = new CopilotAPI(this.user.token) + const copilotApi = this.copilot let nextToken: string | undefined do { @@ -634,7 +634,7 @@ export class SyncService extends AuthenticatedDropboxService { assemblyCreatePath, assemblyPathOverride, } = params - const copilotApi = new CopilotAPI(this.user.token) + const copilotApi = this.copilot const fileCreateResponse = await copilotApi.createFile( assemblyPathOverride ?? assemblyCreatePath, @@ -1014,7 +1014,7 @@ export class SyncService extends AuthenticatedDropboxService { } private async deleteAssemblyFileQuietly(assemblyFileId: string) { - const copilotApi = new CopilotAPI(this.user.token) + const copilotApi = this.copilot try { await copilotApi.deleteFile(assemblyFileId) } catch (error) { diff --git a/src/features/workers/resync-failed-files/helper/resync-failed-files.helper.ts b/src/features/workers/resync-failed-files/helper/resync-failed-files.helper.ts index 2cad7cc..12b01e4 100644 --- a/src/features/workers/resync-failed-files/helper/resync-failed-files.helper.ts +++ b/src/features/workers/resync-failed-files/helper/resync-failed-files.helper.ts @@ -349,7 +349,7 @@ const initializeSyncDependencies = async ( }) const user = await User.authenticate(token) - const copilotApi = new CopilotAPI(token) + const copilotApi = new CopilotAPI(portalId) const dbxClient = new DropboxClient(refreshToken, rootNamespaceId) const connectionToken = { refreshToken, accountId, rootNamespaceId } const syncService = new SyncService(user, connectionToken) diff --git a/src/lib/copilot/CopilotAPI.ts b/src/lib/copilot/CopilotAPI.ts index 7250f70..5a5626c 100644 --- a/src/lib/copilot/CopilotAPI.ts +++ b/src/lib/copilot/CopilotAPI.ts @@ -1,21 +1,16 @@ import 'server-only' -import type { CopilotAPI as SDK } from 'copilot-node-sdk' -import { copilotApi } from 'copilot-node-sdk' +import { assemblyApi, type AssemblyAPI as SDK } from '@assembly-js/node-sdk' import fetch from 'node-fetch' -import z from 'zod' import env from '@/config/server.env' -import { MAX_FETCH_COPILOT_RESOURCES } from '@/constants/limits' import type { ObjectTypeValue } from '@/db/constants' import { MAX_FILES_LIMIT } from '@/features/sync/constant' import { - type ClientRequest, type ClientResponse, ClientResponseSchema, ClientsResponseSchema, type CompaniesResponse, CompaniesResponseSchema, - type CompanyCreateRequest, type CompanyResponse, CompanyResponseSchema, CopilotFileChannelListSchema, @@ -26,20 +21,7 @@ import { type CopilotFileRetrieve, CopilotFileRetrieveSchema, type CopilotListArgs, - type CopilotPrice, - CopilotPriceSchema, - type CopilotProduct, - CopilotProductSchema, type CreateFileType, - type InternalUser, - InternalUserSchema, - type InternalUsersResponse, - InternalUsersResponseSchema, - type NotificationCreatedResponse, - NotificationCreatedResponseSchema, - type NotificationRequestBody, - type Token, - TokenSchema, type WorkspaceResponse, WorkspaceResponseSchema, } from '@/lib/copilot/types' @@ -47,7 +29,7 @@ import logger from '@/lib/logger' import { withRetry } from '@/lib/withRetry' // Structural shape of the SDK's `ApiError` (declared in -// `copilot-node-sdk/dist/codegen/api/core/ApiError`). The class itself is not +// `@assembly-js/node-sdk/dist/codegen/api/core/ApiError`). The class itself is not // re-exported from the package's public entry point, so we identify it by // shape instead of `instanceof` — that avoids reaching into `node_modules` // internals, which would break on any minor SDK update. We check `url` + @@ -61,6 +43,9 @@ export type CopilotApiError = Error & { body: { message?: string } & Record } +// SDK's file type enum. Our ObjectType has the same string values, so we cast. +type AssemblyCreateFileType = Parameters[0]['requestBody']['type'] + export function isCopilotApiError(error: unknown): error is CopilotApiError { if (!(error instanceof Error)) return false const e = error as { @@ -79,142 +64,47 @@ export function isCopilotApiError(error: unknown): error is CopilotApiError { } export class CopilotAPI { - readonly copilot: SDK + readonly assemblySdk: Promise - constructor( - private readonly token: string, - readonly customApiKey?: string, - ) { - this.copilot = copilotApi({ - apiKey: customApiKey ?? env.COPILOT_API_KEY, - token, - }) + constructor(private readonly workspaceId: string) { + this.assemblySdk = assemblyApi({ apiKey: `${this.workspaceId}/${env.COPILOT_API_KEY}` }) + // Swallow here so an unused instance can't throw an unhandled rejection. + void this.assemblySdk.catch(() => undefined) } // NOTE: Any method prefixed with _ is a API method that doesn't implement retry & delay // NOTE: Any normal API method name implements `withRetry` with default config - // Get Token Payload from copilot request token - async _getTokenPayload(): Promise { - const getTokenPayload = this.copilot.getTokenPayload - if (!getTokenPayload) { - logger.error( - `CopilotAPI#getTokenPayload | Could not parse token payload for token ${this.token}`, - ) - return null - } - - return TokenSchema.parse(await getTokenPayload()) - } - async _getWorkspace(): Promise { logger.info('CopilotAPI#_getWorkspace') - return WorkspaceResponseSchema.parse(await this.copilot.retrieveWorkspace()) - } - - async _createClient( - requestBody: ClientRequest, - sendInvite: boolean = false, - ): Promise { - logger.info('CopilotAPI#_createClient', requestBody, sendInvite) - return ClientResponseSchema.parse(await this.copilot.createClient({ sendInvite, requestBody })) + const sdk = await this.assemblySdk + return WorkspaceResponseSchema.parse(await sdk.retrieveWorkspace()) } async _getClient(id: string): Promise { logger.info('CopilotAPI#_getClient', id) - return ClientResponseSchema.parse(await this.copilot.retrieveClient({ id })) + const sdk = await this.assemblySdk + return ClientResponseSchema.parse(await sdk.retrieveClient({ id })) } async _getClients(args: CopilotListArgs & { companyId?: string } = {}) { logger.info('CopilotAPI#_getClients', args) - return ClientsResponseSchema.parse(await this.copilot.listClients(args)) - } - - async _updateClient(id: string, requestBody: ClientRequest): Promise { - logger.info('CopilotAPI#_updateClient', id) - return ClientResponseSchema.parse(await this.copilot.updateClient({ id, requestBody })) - } - - async _deleteClient(id: string) { - logger.info('CopilotAPI#_deleteClient', id) - return await this.copilot.deleteClient({ id }) - } - - async _createCompany(requestBody: CompanyCreateRequest) { - logger.info('CopilotAPI#_createCompany', requestBody) - return CompanyResponseSchema.parse(await this.copilot.createCompany({ requestBody })) + const sdk = await this.assemblySdk + return ClientsResponseSchema.parse(await sdk.listClients(args)) } async _getCompany(id: string): Promise { logger.info('CopilotAPI#_getCompany', id) - return CompanyResponseSchema.parse(await this.copilot.retrieveCompany({ id })) + const sdk = await this.assemblySdk + return CompanyResponseSchema.parse(await sdk.retrieveCompany({ id })) } async _getCompanies( args: CopilotListArgs & { isPlaceholder?: boolean } = {}, ): Promise { logger.info('CopilotAPI#_getCompanies', args) - return CompaniesResponseSchema.parse(await this.copilot.listCompanies(args)) - } - - async _getCompanyClients(companyId: string): Promise { - logger.info('CopilotAPI#_getCompanyClients', companyId) - return (await this.getClients({ limit: 10000, companyId })).data || [] - } - - async _getInternalUsers(args: CopilotListArgs = {}): Promise { - logger.info('CopilotAPI#_getInternalUsers', args) - return InternalUsersResponseSchema.parse(await this.copilot.listInternalUsers(args)) - } - - async _getInternalUser(id: string): Promise { - logger.info('CopilotAPI#_getInternalUser', id) - return InternalUserSchema.parse(await this.copilot.retrieveInternalUser({ id })) - } - - async _createNotification( - requestBody: NotificationRequestBody, - ): Promise { - logger.info('CopilotAPI#_createNotification', requestBody) - const notification = await this.copilot.createNotification({ requestBody }) - return NotificationCreatedResponseSchema.parse(notification) - } - - /** - * Returns an object with product ID as key and product as value - * @param productIds Products to get details for - */ - async _getProducts( - productIds: string[], - args: CopilotListArgs = { limit: MAX_FETCH_COPILOT_RESOURCES }, - ): Promise> { - const allProductsResponse = await this.copilot.listProducts(args) - const allProducts = z.array(CopilotProductSchema).parse(allProductsResponse.data) - - return allProducts.reduce>((acc, product) => { - if (productIds.includes(product.id)) { - acc[product.id] = product - } - return acc - }, {}) - } - - /** - * Returns an object with price ID as key and price as value - * @param priceIds Prices to get details for - */ - async _getPrices( - priceIds: string[], - args = { limit: '10_000' }, - ): Promise> { - const allPricesResponse = await this.copilot.listPrices(args) - const allPrices = z.array(CopilotPriceSchema).parse(allPricesResponse.data) - return allPrices.reduce>((acc, price) => { - if (priceIds.includes(price.id)) { - acc[price.id] = price - } - return acc - }, {}) + const sdk = await this.assemblySdk + return CompaniesResponseSchema.parse(await sdk.listCompanies(args)) } async _createFile( @@ -224,12 +114,13 @@ export class CopilotAPI { ): Promise { // Names are validated upstream (SyncService); path passes through unchanged. console.info(`CopilotAPI#_createFile. Path: ${path}`) - - const createFileResponse = await this.copilot.createFile({ + const sdk = await this.assemblySdk + const createFileResponse = await sdk.createFile({ fileType, requestBody: { path, - channelId, + channelID: channelId, + type: fileType as unknown as AssemblyCreateFileType, }, }) return CopilotFileCreateSchema.parse(createFileResponse) @@ -251,7 +142,8 @@ export class CopilotAPI { } async _deleteFile(id: string) { - return await this.copilot.deleteFile({ id }) + const sdk = await this.assemblySdk + return await sdk.deleteFile({ id }) } async _listFiles( @@ -259,7 +151,8 @@ export class CopilotAPI { nextToken?: string, customLimit?: number, ): Promise { - const list = await this.copilot.listFiles({ + const sdk = await this.assemblySdk + const list = await sdk.listFiles({ channelId, nextToken, limit: customLimit || MAX_FILES_LIMIT, @@ -268,17 +161,20 @@ export class CopilotAPI { } async _retrieveFile(id: string): Promise { - const file = await this.copilot.retrieveFile({ id }) + const sdk = await this.assemblySdk + const file = await sdk.retrieveFile({ id }) return CopilotFileRetrieveSchema.parse(file) } async _retrieveFileChannel(id: string) { - const fileChannel = await this.copilot.retrieveFileChannel({ id }) + const sdk = await this.assemblySdk + const fileChannel = await sdk.retrieveFileChannel({ id }) return CopilotFileChannelRetrieveSchema.parse(fileChannel) } async _listFileChannels(args: CopilotListArgs & { companyId?: string; clientId?: string } = {}) { - const list = await this.copilot.listFileChannels(args) + const sdk = await this.assemblySdk + const list = await sdk.listFileChannels(args) return CopilotFileChannelListSchema.parse(list.data) } @@ -289,22 +185,11 @@ export class CopilotAPI { } // Methods wrapped with retry - getTokenPayload = this.wrapWithRetry(this._getTokenPayload) getWorkspace = this.wrapWithRetry(this._getWorkspace) - createClient = this.wrapWithRetry(this._createClient) getClient = this.wrapWithRetry(this._getClient) getClients = this.wrapWithRetry(this._getClients) - updateClient = this.wrapWithRetry(this._updateClient) - deleteClient = this.wrapWithRetry(this._deleteClient) - createCompany = this.wrapWithRetry(this._createCompany) getCompany = this.wrapWithRetry(this._getCompany) getCompanies = this.wrapWithRetry(this._getCompanies) - getCompanyClients = this.wrapWithRetry(this._getCompanyClients) - getInternalUsers = this.wrapWithRetry(this._getInternalUsers) - getInternalUser = this.wrapWithRetry(this._getInternalUser) - createNotification = this.wrapWithRetry(this._createNotification) - getProductsById = this.wrapWithRetry(this._getProducts) - getPricesById = this.wrapWithRetry(this._getPrices) createFile = this.wrapWithRetry(this._createFile) uploadFile = this.wrapWithRetry(this._uploadFile) deleteFile = this.wrapWithRetry(this._deleteFile) diff --git a/src/lib/copilot/models/User.model.ts b/src/lib/copilot/models/User.model.ts index 3a32c7d..1d10beb 100644 --- a/src/lib/copilot/models/User.model.ts +++ b/src/lib/copilot/models/User.model.ts @@ -2,8 +2,8 @@ import httpStatus from 'http-status' import { z } from 'zod' import APIError from '@/errors/APIError' import { CopilotAPI } from '@/lib/copilot/CopilotAPI' -import CopilotConnectionError from '@/lib/copilot/errors/CopilotConnectionError' import type { Token } from '@/lib/copilot/types' +import { getAssemblyTokenPayload } from '@/lib/copilot/utils' import logger from '@/lib/logger' class User { @@ -18,14 +18,14 @@ class User { ) { this.internalUserId = tokenPayload.internalUserId this.portalId = tokenPayload.workspaceId - this.copilot = copilot || new CopilotAPI(token) + this.copilot = copilot || new CopilotAPI(tokenPayload.workspaceId) } /** * Authenticates a Copilot user by token * @param token * @returns User instance modeled from the token payload - * @throws CopilotConnectionError when unable to connect to Copilot API + * @throws APIError when the token is missing or cannot be decoded */ static async authenticate(token?: unknown): Promise { logger.info('User#authenticate :: Authenticating user', token) @@ -41,26 +41,13 @@ class User { throw new APIError('Token parse error', httpStatus.UNAUTHORIZED) } - let copilot: CopilotAPI - try { - copilot = new CopilotAPI(tokenParsed.data) - } catch (err) { - if (err instanceof Error && err.message.includes('Unable to authorize Copilot SDK')) { - throw new APIError( - 'Unable to authorize Copilot with provided token', - httpStatus.UNAUTHORIZED, - ) - } - logger.error('User#authenticate :: Error while initializing Copilot client', err) - throw new CopilotConnectionError() - } - - const tokenPayload = await copilot.getTokenPayload() + const tokenPayload = await getAssemblyTokenPayload(tokenParsed.data) if (!tokenPayload) { throw new APIError('Unable to decode Copilot token payload', httpStatus.UNAUTHORIZED) } - return new User(tokenParsed.data, tokenPayload, copilot) + // SDK is built lazily, so auth errors surface on the first API call. + return new User(tokenParsed.data, tokenPayload) } } diff --git a/src/lib/copilot/services/base.service.ts b/src/lib/copilot/services/base.service.ts index 4fdf37f..87187ad 100644 --- a/src/lib/copilot/services/base.service.ts +++ b/src/lib/copilot/services/base.service.ts @@ -4,7 +4,7 @@ import type User from '@/lib/copilot/models/User.model' class BaseService { protected readonly copilot: CopilotAPI constructor(protected readonly user: User) { - this.copilot = new CopilotAPI(user.token) + this.copilot = new CopilotAPI(user.portalId) } } diff --git a/src/lib/copilot/types.ts b/src/lib/copilot/types.ts index a731b8c..577d119 100644 --- a/src/lib/copilot/types.ts +++ b/src/lib/copilot/types.ts @@ -105,60 +105,6 @@ export const InternalUsersResponseSchema = z.object({ }) export type InternalUsersResponse = z.infer -/** - * `senderType` field for notification payload in Copilot API - */ -export const NotificationSenderSchema = z.enum(['internalUser', 'client']) -export type NotificationSender = z.infer - -/** - * Notification RequestBody schema - accepted by SDK#createNotification - */ -export const NotificationRequestBodySchema = z.object({ - senderId: z.string(), - // New notification body schema for copilot to accomodate for multiple companies - senderType: NotificationSenderSchema, - senderCompanyId: z.string().optional(), - recipientInternalUserId: z.string().optional(), - recipientClientId: z.string().optional(), - recipientCompanyId: z.string().optional(), - deliveryTargets: z - .object({ - inProduct: z - .object({ - title: z.string(), - body: z.string().optional(), - }) - .optional(), - email: z - .object({ - subject: z.string().optional(), - header: z.string().optional(), - title: z.string().optional(), - body: z.string().optional(), - }) - .optional(), - }) - .optional(), -}) -export type NotificationRequestBody = z.infer - -export const NotificationCreatedResponseSchema = z.object({ - id: z.string(), - appId: z.uuid().optional(), - createdAt: z.iso.datetime(), - event: z.string().optional(), - object: z.string().optional(), - companyId: z.string().optional(), - recipientInternalUserId: z.string().optional(), - recipientClientId: z.string().optional(), - recipientCompanyId: z.string().optional(), - resourceId: z.string().optional(), - senderId: z.string().optional(), - senderType: z.string().optional(), -}) -export type NotificationCreatedResponse = z.infer - export const CopilotProductSchema = z.object({ id: z.string(), name: z.string(), diff --git a/src/lib/copilot/utils.ts b/src/lib/copilot/utils.ts index 7aa00ce..ab5f25d 100644 --- a/src/lib/copilot/utils.ts +++ b/src/lib/copilot/utils.ts @@ -1,2 +1,17 @@ +import { assemblyApi } from '@assembly-js/node-sdk' +import env from '@/config/server.env' +import { TokenSchema } from '@/lib/copilot/types' + export const buildClientName = (client: { givenName: string; familyName: string }) => `${client.givenName} ${client.familyName}` + +// Decodes a request token into its payload using the token-scoped SDK. +export async function getAssemblyTokenPayload(token: string) { + const sdk = await assemblyApi({ apiKey: env.COPILOT_API_KEY, token }) + if (!sdk.getTokenPayload) { + // Never log the raw token — it is a credential. + console.error('getAssemblyTokenPayload | cannot decode token') + return null + } + return TokenSchema.parse(await sdk.getTokenPayload()) +} diff --git a/src/trigger/processFileSync.ts b/src/trigger/processFileSync.ts index ad7b6bc..3589ad9 100644 --- a/src/trigger/processFileSync.ts +++ b/src/trigger/processFileSync.ts @@ -276,7 +276,7 @@ export const initiateAssemblyToDropboxSync = task({ const dbxAuth = new DropboxAuthClient() // 1. get al the files from the assembly - const copilotApi = new CopilotAPI(payload.user.token) + const copilotApi = new CopilotAPI(payload.user.portalId) let files = await copilotApi.listFiles(payload.assemblyChannelId) while (files.data.length) { diff --git a/src/utils/__tests__/workspace.test.md b/src/utils/__tests__/workspace.test.md new file mode 100644 index 0000000..ea0187b --- /dev/null +++ b/src/utils/__tests__/workspace.test.md @@ -0,0 +1,73 @@ +# `workspace.test.ts` — Test Documentation + +Tests for `src/utils/workspace.ts`, which provides two exports: a pure label-lookup helper and an async function to fetch workspace data from the Copilot API. + +--- + +## `getWorkspaceLabel` + +A synchronous function that resolves a human-readable label from a `WorkspaceResponse` object. + +```mermaid +flowchart LR + A["getWorkspaceLabel(workspace, key)"] --> B{"labels?"} + B -->|Yes| C{"key in labels?"} + B -->|No| D[Return default] + C -->|Yes| E["Return labels[key].toLowerCase()"] + C -->|No| D +``` + +### Lowercasing (4 test cases) + +Each of the four label keys is lowercased before being returned: + +| Key | Input value | Output | +|---|---|---| +| `individualTerm` | `"Patient"` | `"patient"` | +| `individualTermPlural` | `"Patients"` | `"patients"` | +| `groupTerm` | `"Org"` | `"org"` | +| `groupTermPlural` | `"Orgs"` | `"orgs"` | + +### Fallback defaults (4 test cases) + +When a label is absent (either `labels` is `undefined` or the specific key is `undefined`), the function falls back to a hardcoded default. Both scenarios exercise the same code path (`??` / `||` chain), so a single parameterized test covers them: + +| Key | Default | +|---|---| +| `individualTerm` | `"client"` | +| `individualTermPlural` | `"clients"` | +| `groupTerm` | `"company"` | +| `groupTermPlural` | `"companies"` | + +### Idempotency (1 test case) + +Input that is already lowercase passes through unchanged — ensures the function doesn't double-lowercase or corrupt the value. + +--- + +## `getWorkspace` + +An async function that decodes the request token into its payload, instantiates `CopilotAPI` with the resolved `workspaceId`, and calls `getWorkspace()`. + +```mermaid +flowchart LR + A[getWorkspace
token] --> B[getAssemblyTokenPayload
token] + B --> C{payload?} + C -->|No| E[throw APIError] + C -->|Yes| D[new CopilotAPI
workspaceId] + D --> F[.getWorkspace] + F --> G[WorkspaceResponse] +``` + +### `CopilotAPI` and `getAssemblyTokenPayload` are mocked + +- `@/lib/copilot/CopilotAPI` is replaced with a `vi.fn()` that returns a stub instance whose `getWorkspace` resolves with a known `WorkspaceResponse` (all four labels: `Patient`/`Patients`/`Org`/`Orgs`). +- `@/lib/copilot/utils` is mocked so `getAssemblyTokenPayload` resolves to `{ workspaceId: 'ws_1' }`. This keeps the real Assembly SDK out of the test — importing it under vitest crashes on an unsupported ESM directory import. + +### Tests + +1. **Returns the result from `copilot.getWorkspace()`** — asserts the returned object has the expected shape (`id`, `brandName`, `portalUrl`). +2. **Constructs `CopilotAPI` with the decoded `workspaceId`, not the raw token** — verifies the token is passed to `getAssemblyTokenPayload` and the resulting `workspaceId` (`ws_1`) is passed to the `CopilotAPI` constructor. +3. **Throws when the token payload cannot be decoded** — when `getAssemblyTokenPayload` returns `null`, `getWorkspace` rejects with `Unable to decode Copilot token payload`. + +Together these confirm `getWorkspace` decodes the token, guards the null case, and delegates construction and data fetching with no extra transformation. diff --git a/src/utils/__tests__/workspace.test.ts b/src/utils/__tests__/workspace.test.ts new file mode 100644 index 0000000..c55a7db --- /dev/null +++ b/src/utils/__tests__/workspace.test.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CopilotAPI } from '@/lib/copilot/CopilotAPI' +import type { WorkspaceResponse } from '@/lib/copilot/types' +import { getAssemblyTokenPayload } from '@/lib/copilot/utils' +import { getWorkspace, getWorkspaceLabel } from '@/utils/workspace' + +vi.mock('@/lib/copilot/CopilotAPI', () => { + const workspace: WorkspaceResponse = { + id: 'ws_1', + brandName: 'Test', + portalUrl: 'https://example.com', + labels: { + individualTerm: 'Patient', + individualTermPlural: 'Patients', + groupTerm: 'Org', + groupTermPlural: 'Orgs', + }, + } + + return { + CopilotAPI: vi.fn(function Mock() { + return { getWorkspace: vi.fn().mockResolvedValue(workspace) } + }), + } +}) + +// Mock the decode so the test never loads the real Assembly SDK. +vi.mock('@/lib/copilot/utils', () => ({ + getAssemblyTokenPayload: vi.fn().mockResolvedValue({ workspaceId: 'ws_1' }), +})) + +function makeWorkspace(overrides?: Partial): WorkspaceResponse { + return { id: 'ws_1', portalUrl: '', ...overrides } +} + +describe('getWorkspaceLabel', () => { + const labels = { + individualTerm: 'Patient', + individualTermPlural: 'Patients', + groupTerm: 'Org', + groupTermPlural: 'Orgs', + } as const + + it.each([ + { key: 'individualTerm', expected: 'patient' }, + { key: 'individualTermPlural', expected: 'patients' }, + { key: 'groupTerm', expected: 'org' }, + { key: 'groupTermPlural', expected: 'orgs' }, + ] as const)('lowercases $key', ({ key, expected }) => { + expect(getWorkspaceLabel(makeWorkspace({ labels }), key)).toBe(expected) + }) + + it.each([ + { key: 'individualTerm', expected: 'client' }, + { key: 'individualTermPlural', expected: 'clients' }, + { key: 'groupTerm', expected: 'company' }, + { key: 'groupTermPlural', expected: 'companies' }, + ] as const)('falls back to "$expected" when $key is missing', ({ key, expected }) => { + expect(getWorkspaceLabel(makeWorkspace(), key)).toBe(expected) + }) + + it('passes already-lowercase input through unchanged', () => { + expect( + getWorkspaceLabel(makeWorkspace({ labels: { groupTerm: 'company' } }), 'groupTerm'), + ).toBe('company') + }) +}) + +describe('getWorkspace', () => { + beforeEach(() => { + vi.mocked(getAssemblyTokenPayload).mockResolvedValue({ workspaceId: 'ws_1' }) + }) + + it('returns the result from copilot.getWorkspace()', async () => { + const result = await getWorkspace('token-123') + expect(result.id).toBe('ws_1') + expect(result.brandName).toBe('Test') + expect(result.portalUrl).toBe('https://example.com') + }) + + it('constructs CopilotAPI with the decoded workspaceId, not the raw token', async () => { + await getWorkspace('token-xyz') + expect(getAssemblyTokenPayload).toHaveBeenCalledWith('token-xyz') + expect(CopilotAPI).toHaveBeenCalledWith('ws_1') + }) + + it('throws when the token payload cannot be decoded', async () => { + vi.mocked(getAssemblyTokenPayload).mockResolvedValueOnce(null) + await expect(getWorkspace('bad-token')).rejects.toThrow( + 'Unable to decode Copilot token payload', + ) + }) +}) diff --git a/src/utils/workspace.ts b/src/utils/workspace.ts index 0fcfe63..1cd8655 100644 --- a/src/utils/workspace.ts +++ b/src/utils/workspace.ts @@ -1,5 +1,6 @@ import { CopilotAPI } from '@/lib/copilot/CopilotAPI' import type { WorkspaceResponse } from '@/lib/copilot/types' +import { getAssemblyTokenPayload } from '@/lib/copilot/utils' export const getWorkspaceLabel = ( workspace: WorkspaceResponse, @@ -14,6 +15,11 @@ export const getWorkspaceLabel = ( } export async function getWorkspace(token: string): Promise { - const copilot = new CopilotAPI(token) + const tokenPayload = await getAssemblyTokenPayload(token) + if (!tokenPayload) { + throw new Error('Unable to decode Copilot token payload') + } + + const copilot = new CopilotAPI(tokenPayload.workspaceId) return await copilot.getWorkspace() } diff --git a/test/msw/hosts.ts b/test/msw/hosts.ts index 8780a7a..0035880 100644 --- a/test/msw/hosts.ts +++ b/test/msw/hosts.ts @@ -1,4 +1,5 @@ // Dependency-free so other files can import it without an import cycle via server.ts. export const DROPBOX_RPC_HOST = 'https://api.dropboxapi.com' export const DROPBOX_CONTENT_HOST = 'https://content.dropboxapi.com' -export const COPILOT_HOST = 'https://api.copilot.app' +// Matches the Assembly SDK's default base URL when COPILOT_ENV is 'local'. +export const COPILOT_HOST = 'https://api.assembly.com' diff --git a/test/msw/msw-harness.integration.test.ts b/test/msw/msw-harness.integration.test.ts index 7d09cc2..6d6eefe 100644 --- a/test/msw/msw-harness.integration.test.ts +++ b/test/msw/msw-harness.integration.test.ts @@ -1,4 +1,4 @@ -import { copilotApi } from 'copilot-node-sdk' +import { assemblyApi } from '@assembly-js/node-sdk' import { DropboxResponseError } from 'dropbox' import { HttpResponse } from 'msw' import { describe, expect, it } from 'vitest' @@ -31,7 +31,7 @@ describe('MSW harness — base handlers + hosts', () => { }) it('serves an empty Copilot file page to the real SDK (undici fetch)', async () => { - const page = await new CopilotAPI('token')._listFiles('ch_1') + const page = await new CopilotAPI('ws_1')._listFiles('ch_1') expect(page.data).toEqual([]) }) @@ -56,7 +56,7 @@ describe('MSW harness — base handlers + hosts', () => { describe('MSW harness — Copilot error shapes', () => { it('reproduces isCopilotApiError 400 "Folder already exists"', async () => { mockCopilot('/v1/files', () => copilotFolderExists()) - const client = copilotApi({ apiKey: 'k', token: 't' }) + const client = await assemblyApi({ apiKey: 'k', token: 't' }) try { await client.listFiles({ channelId: 'ch' }) expect.unreachable('listFiles should have thrown') @@ -71,7 +71,7 @@ describe('MSW harness — Copilot error shapes', () => { it('reproduces isCopilotApiError 404', async () => { mockCopilot('/v1/files', () => copilotNotFound()) - const client = copilotApi({ apiKey: 'k', token: 't' }) + const client = await assemblyApi({ apiKey: 'k', token: 't' }) try { await client.listFiles({ channelId: 'ch' }) expect.unreachable('listFiles should have thrown') @@ -136,7 +136,7 @@ describe('MSW harness — pagination', () => { path: `/f${i}`, })) server.use(paginateCopilotListFiles(items, { pageSize: 100 })) - const api = new CopilotAPI('token') + const api = new CopilotAPI('ws_1') const all: unknown[] = [] let nextToken: string | undefined do { diff --git a/test/msw/write.integration.test.ts b/test/msw/write.integration.test.ts index c466bfd..618bf6a 100644 --- a/test/msw/write.integration.test.ts +++ b/test/msw/write.integration.test.ts @@ -79,13 +79,13 @@ describe('Dropbox content handlers', () => { describe('Copilot create-file handler', () => { it('createFile(folder) returns folder metadata (no upload URL needed)', async () => { mockCopilotCreateFile() - const res = await new CopilotAPI('token').createFile('/folder', 'ch-1', ObjectType.FOLDER) + const res = await new CopilotAPI('ws_1').createFile('/folder', 'ch-1', ObjectType.FOLDER) expect(res.object).toBe(ObjectType.FOLDER) }) it('createFile(file) returns an uploadUrl that accepts a PUT', async () => { mockCopilotCreateFile() - const api = new CopilotAPI('token') + const api = new CopilotAPI('ws_1') const created = await api.createFile('/a.txt', 'ch-1', ObjectType.FILE) expect(created.uploadUrl).toBeTruthy() const put = await api.uploadFile(created.uploadUrl as string, '5', null) @@ -103,14 +103,14 @@ describe('delta handlers (webhook flow)', () => { it('retrieveFile returns the file for a known id and 404s an unknown one', async () => { const file = copilotFileFactory.build() mockCopilotRetrieveFile({ [file.id]: file }) - const api = new CopilotAPI('token') + const api = new CopilotAPI('ws_1') expect((await api.retrieveFile(file.id)).id).toBe(file.id) await expect(api.retrieveFile('missing-id')).rejects.toMatchObject({ status: 404 }) }) it('deleteFile hits the Copilot DELETE handler and captures the id', async () => { const { deletedIds } = mockCopilotDeleteFile() - await expect(new CopilotAPI('token').deleteFile('file-id')).resolves.toBeDefined() + await expect(new CopilotAPI('ws_1').deleteFile('file-id')).resolves.toBeDefined() expect(deletedIds).toEqual(['file-id']) }) diff --git a/test/msw/write.ts b/test/msw/write.ts index 3177100..f756867 100644 --- a/test/msw/write.ts +++ b/test/msw/write.ts @@ -120,12 +120,13 @@ export function mockCopilotCreateFile(opts: { uploadUrl?: string } = {}): void { mockCopilot( '/v1/files/:fileType', async ({ request, params }) => { - const { path, channelId } = (await request.json()) as { path: string; channelId: string } + // The SDK sends `channelID` (capital ID); the response echoes `channelId`. + const { path, channelID } = (await request.json()) as { path: string; channelID: string } const fileType = params.fileType as string return HttpResponse.json({ // Real Copilot returns a UUID; callers persist it into a uuid() column. id: randomUUID(), - channelId, + channelId: channelID, name: nameOf(path), object: fileType, path, diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts index 2b1b388..831e03c 100644 --- a/vitest.integration.config.ts +++ b/vitest.integration.config.ts @@ -19,12 +19,9 @@ export default defineConfig({ globalSetup: ['./test/integration/global-setup.ts'], setupFiles: ['./test/integration/setup.ts'], server: { - // `copilot-node-sdk` is a pure-ESM package whose dist does an internal - // directory import (`../codegen/api`, resolved to its `index.js` only by - // bundler-style resolution). Node's native ESM loader can't resolve that - // when Vite externalizes the package, so force it through Vite's own - // resolver instead. - deps: { inline: ['copilot-node-sdk'] }, + // `@assembly-js/node-sdk` uses an internal directory import that Node's + // ESM loader can't resolve, so force it through Vite's resolver. + deps: { inline: ['@assembly-js/node-sdk'] }, }, // One container, one shared DB — run files serially so truncate-between-tests // isolation is safe.