From b348035b7030187e999d59c16442bca7133ae908 Mon Sep 17 00:00:00 2001 From: pdparchitect Date: Thu, 3 Sep 2026 11:20:41 +0000 Subject: [PATCH] First public release This is the the first public release of the CBK platform. --- .devcontainer/devcontainer.json | 15 + .dockerignore | 47 + .gitattributes | 36 + .github/ISSUE_TEMPLATE/bug_report.yml | 52 + .github/ISSUE_TEMPLATE/config.yml | 7 + .github/ISSUE_TEMPLATE/feature_request.yml | 28 + .github/PULL_REQUEST_TEMPLATE.md | 26 + .github/workflows/_verify.yaml | 142 + .github/workflows/publish-ghcr-platform.yaml | 415 + .github/workflows/pull-request.yaml | 52 + .gitignore | 69 + .pnpmfile.cjs | 146 + .vscode/extensions.json | 10 + .vscode/launch.json | 13 + .vscode/settings.json | 16 + CODEOWNERS | 14 + CODE_OF_CONDUCT.md | 30 + CONTRIBUTING.md | 121 + LICENSE | 201 + LICENSING.md | 19 + NOTICE | 4 + README.md | 100 +- SECURITY.md | 18 + SUPPORT.md | 24 + docker-compose.yml | 267 + docker/Dockerfile | 206 + docker/distro/AGENTS.md | 60 + docker/distro/community/compose.yml | 231 + docker/entrypoint.sh | 171 + docker/garage/garage.toml | 36 + docker/garage/init.mjs | 160 + docs/README.md | 32 + docs/architecture.md | 343 + docs/configuration.md | 489 + docs/deployment.md | 258 + docs/getting-started.md | 168 + docs/module-defaults.md | 80 + eslint.config.mjs | 22 + package.json | 39 + packages/AGENTS.md | 261 + packages/README.md | 11 + packages/auxiliary-google-calendar/README.md | 1 + .../auxiliary-google-calendar/jest.config.js | 5 + .../auxiliary-google-calendar/package.json | 31 + .../src/index.test.ts | 5 + .../auxiliary-google-calendar/src/index.ts | 1 + .../auxiliary-google-calendar/tsconfig.json | 23 + packages/auxiliary-google-docs/README.md | 1 + packages/auxiliary-google-docs/jest.config.js | 5 + packages/auxiliary-google-docs/package.json | 31 + .../auxiliary-google-docs/src/index.test.ts | 5 + packages/auxiliary-google-docs/src/index.ts | 1 + packages/auxiliary-google-docs/tsconfig.json | 23 + packages/auxiliary-google-mail/README.md | 1 + packages/auxiliary-google-mail/jest.config.js | 5 + packages/auxiliary-google-mail/package.json | 31 + .../auxiliary-google-mail/src/index.test.ts | 5 + packages/auxiliary-google-mail/src/index.ts | 1 + packages/auxiliary-google-mail/tsconfig.json | 23 + packages/auxiliary-google-meet/README.md | 1 + packages/auxiliary-google-meet/jest.config.js | 5 + packages/auxiliary-google-meet/package.json | 31 + .../auxiliary-google-meet/src/index.test.ts | 5 + packages/auxiliary-google-meet/src/index.ts | 1 + packages/auxiliary-google-meet/tsconfig.json | 23 + packages/auxiliary-graphql/README.md | 1 + packages/auxiliary-graphql/jest.config.js | 5 + packages/auxiliary-graphql/package.json | 45 + packages/auxiliary-graphql/src/builder.ts | 24 + packages/auxiliary-graphql/src/http-field.ts | 107 + packages/auxiliary-graphql/src/index.test.ts | 41 + packages/auxiliary-graphql/src/index.ts | 12 + packages/auxiliary-graphql/src/notion.ts | 97 + packages/auxiliary-graphql/src/schema.ts | 48 + packages/auxiliary-graphql/src/slack.ts | 183 + packages/auxiliary-graphql/tsconfig.json | 23 + packages/auxiliary-microsoft-drive/README.md | 1 + .../auxiliary-microsoft-drive/jest.config.js | 5 + .../auxiliary-microsoft-drive/package.json | 35 + .../src/index.test.ts | 5 + .../auxiliary-microsoft-drive/src/index.ts | 1 + .../auxiliary-microsoft-drive/tsconfig.json | 23 + .../auxiliary-microsoft-sharepoint/README.md | 1 + .../jest.config.js | 5 + .../package.json | 35 + .../src/index.test.ts | 5 + .../src/index.ts | 1 + .../tsconfig.json | 23 + packages/auxiliary-notion/README.md | 1 + packages/auxiliary-notion/jest.config.js | 5 + packages/auxiliary-notion/package.json | 55 + packages/auxiliary-notion/src/client.test.ts | 19 + packages/auxiliary-notion/src/client.ts | 28 + packages/auxiliary-notion/src/contents.ts | 26 + .../auxiliary-notion/src/database.test.ts | 577 + packages/auxiliary-notion/src/database.ts | 766 + packages/auxiliary-notion/src/errors.test.ts | 23 + packages/auxiliary-notion/src/errors.ts | 16 + packages/auxiliary-notion/src/handler.ts | 847 + packages/auxiliary-notion/src/index.ts | 6 + packages/auxiliary-notion/src/page.test.ts | 319 + packages/auxiliary-notion/src/page.ts | 128 + packages/auxiliary-notion/src/sql.test.ts | 397 + packages/auxiliary-notion/src/sql.ts | 221 + packages/auxiliary-notion/tsconfig.json | 34 + packages/batch-spec/package.json | 34 + packages/batch-spec/src/index.ts | 209 + packages/batch-spec/tsconfig.json | 35 + packages/batch/jest.config.js | 11 + packages/batch/package.json | 41 + packages/batch/src/index.test.js | 58 + packages/batch/src/index.ts | 126 + packages/batch/tsconfig.json | 37 + packages/billing-spec/package.json | 34 + packages/billing-spec/src/index.ts | 148 + packages/billing-spec/tsconfig.json | 35 + packages/billing/package.json | 45 + packages/billing/src/assert.ts | 8 + packages/billing/src/config.ts | 15 + packages/billing/src/gates.ts | 22 + packages/billing/src/index.ts | 10 + packages/billing/src/model.ts | 49 + packages/billing/src/provider.ts | 135 + packages/billing/src/trial.ts | 27 + packages/billing/tsconfig.json | 35 + packages/blacklists/jest.config.js | 5 + packages/blacklists/package.json | 45 + packages/blacklists/src/domains.ts | 87 + packages/blacklists/src/index.test.ts | 29 + packages/blacklists/src/index.ts | 18 + packages/blacklists/tsconfig.json | 38 + packages/buffer/README.md | 12 + packages/buffer/jest.config.js | 5 + packages/buffer/package.json | 40 + packages/buffer/src/index.test.ts | 135 + packages/buffer/src/index.ts | 111 + packages/buffer/tsconfig.json | 23 + packages/cloak/LICENSE.md | 22 + packages/cloak/README.md | 12 + packages/cloak/jest.config.js | 5 + packages/cloak/package.json | 39 + packages/cloak/src/ciphers/aes-gcm.ts | 63 + packages/cloak/src/index.test.ts | 197 + packages/cloak/src/index.ts | 10 + packages/cloak/src/key.test.ts | 32 + packages/cloak/src/key.ts | 84 + packages/cloak/src/keychain.ts | 104 + packages/cloak/src/message.ts | 145 + packages/cloak/tsconfig.json | 23 + packages/db-spec/README.md | 35 + packages/db-spec/jest.config.js | 16 + packages/db-spec/package.json | 40 + packages/db-spec/prisma/schema.prisma | 3500 ++ ...talContactsWithConversationsOverPeriod.sql | 13 + .../breakdownTotalConversationsOverPeriod.sql | 13 + ...breakdownTotalMessagesOfTypeOverPeriod.sql | 16 + .../sql/breakdownTotalMessagesOverPeriod.sql | 14 + .../sql/breakdownTotalRatingsOverPeriod.sql | 14 + .../breakdownTotalUsageTokensOverPeriod.sql | 13 + .../getAverageMessagesOfTypeOverPeriod.sql | 15 + .../sql/getBotConversationCountByDay.sql | 12 + ...etBotConversationsWithUserMessageCount.sql | 16 + .../prisma/sql/getBotMessageCountByDay.sql | 13 + .../sql/getBotSingleTurnConversationCount.sql | 17 + .../db-spec/prisma/sql/getBotUsageStats.sql | 22 + .../prisma/sql/getConversationUsageStats.sql | 18 + .../sql/getDailyNegativeRatingCount.sql | 11 + .../sql/getEventMetricSeriesOverPeriod.sql | 12 + .../prisma/sql/getTotalAbilitiesForUser.sql | 7 + .../prisma/sql/getTotalBotsForUser.sql | 6 + .../db-spec/prisma/sql/getTotalContacts.sql | 4 + ...talContactsWithConversationsOverPeriod.sql | 13 + .../sql/getTotalConversationsOverPeriod.sql | 8 + ...rsationsWithNumberedMessagesOverPeriod.sql | 15 + .../prisma/sql/getTotalDatasetsForUser.sql | 6 + .../prisma/sql/getTotalFilesForUser.sql | 6 + .../sql/getTotalMessagesOfTypeOverPeriod.sql | 11 + .../prisma/sql/getTotalMessagesOverPeriod.sql | 9 + .../prisma/sql/getTotalPoliciesForUser.sql | 6 + .../prisma/sql/getTotalPortalsForUser.sql | 6 + .../prisma/sql/getTotalRatingsOverPeriod.sql | 8 + .../prisma/sql/getTotalSkillsetsForUser.sql | 6 + .../prisma/sql/getTotalTeamMembersForUser.sql | 7 + .../prisma/sql/getTotalTeamsForUser.sql | 6 + .../sql/getTotalThumbsDownOverPeriod.sql | 9 + .../prisma/sql/getTotalThumbsUpOverPeriod.sql | 9 + .../sql/getTotalUsageTokensOverPeriod.sql | 10 + .../prisma/sql/getTotalUsersForUser.sql | 4 + .../sql/getUsageCountByTypeOverPeriod.sql | 14 + .../prisma/sql/getUsageCountByTypeSince.sql | 12 + packages/db-spec/prisma/sql/listContacts.sql | 7 + ...istContactsWithConversationsOverPeriod.sql | 23 + .../listContactsWithMessagesOverPeriod.sql | 24 + .../sql/listContactsWithRatingsOverPeriod.sql | 26 + .../sql/listConversationsOverPeriod.sql | 25 + ...rsationsWithNumberedMessagesOverPeriod.sql | 24 + ...gsOfTypeActionsGroupedByTypeOverPeriod.sql | 18 + .../sql/listTopBotsByTokenUsageOverPeriod.sql | 19 + .../listTopContactsByTokenUsageOverPeriod.sql | 19 + .../sql/listTopDownvotersOverPeriod.sql | 24 + .../prisma/sql/listTopUpvotersOverPeriod.sql | 24 + .../db-spec/prisma/zod-generator.config.json | 24 + packages/db-spec/scripts/post-generate-zod.js | 145 + packages/db-spec/src/derive.js | 157 + packages/db-spec/src/types.test.js | 296 + packages/db-spec/src/types.ts | 271 + packages/db-spec/tsconfig.json | 38 + packages/db/.gitignore | 3 + packages/db/README.md | 21 + packages/db/package.json | 68 + packages/db/prisma.config.ts | 19 + packages/db/prisma/schema.prisma | 3500 ++ ...talContactsWithConversationsOverPeriod.sql | 13 + .../breakdownTotalConversationsOverPeriod.sql | 13 + ...breakdownTotalMessagesOfTypeOverPeriod.sql | 16 + .../sql/breakdownTotalMessagesOverPeriod.sql | 14 + .../sql/breakdownTotalRatingsOverPeriod.sql | 14 + .../breakdownTotalUsageTokensOverPeriod.sql | 13 + .../getAverageMessagesOfTypeOverPeriod.sql | 15 + .../sql/getBotConversationCountByDay.sql | 12 + ...etBotConversationsWithUserMessageCount.sql | 16 + .../db/prisma/sql/getBotMessageCountByDay.sql | 13 + .../sql/getBotSingleTurnConversationCount.sql | 17 + packages/db/prisma/sql/getBotUsageStats.sql | 22 + .../prisma/sql/getConversationUsageStats.sql | 18 + .../sql/getDailyNegativeRatingCount.sql | 11 + .../sql/getEventMetricSeriesOverPeriod.sql | 12 + .../prisma/sql/getTotalAbilitiesForUser.sql | 7 + .../db/prisma/sql/getTotalBotsForUser.sql | 6 + packages/db/prisma/sql/getTotalContacts.sql | 4 + ...talContactsWithConversationsOverPeriod.sql | 13 + .../sql/getTotalConversationsOverPeriod.sql | 8 + ...rsationsWithNumberedMessagesOverPeriod.sql | 15 + .../db/prisma/sql/getTotalDatasetsForUser.sql | 6 + .../db/prisma/sql/getTotalFilesForUser.sql | 6 + .../sql/getTotalMessagesOfTypeOverPeriod.sql | 11 + .../prisma/sql/getTotalMessagesOverPeriod.sql | 9 + .../db/prisma/sql/getTotalPoliciesForUser.sql | 6 + .../db/prisma/sql/getTotalPortalsForUser.sql | 6 + .../prisma/sql/getTotalRatingsOverPeriod.sql | 8 + .../prisma/sql/getTotalSkillsetsForUser.sql | 6 + .../prisma/sql/getTotalTeamMembersForUser.sql | 7 + .../db/prisma/sql/getTotalTeamsForUser.sql | 6 + .../sql/getTotalThumbsDownOverPeriod.sql | 9 + .../prisma/sql/getTotalThumbsUpOverPeriod.sql | 9 + .../sql/getTotalUsageTokensOverPeriod.sql | 10 + .../db/prisma/sql/getTotalUsersForUser.sql | 4 + .../sql/getUsageCountByTypeOverPeriod.sql | 14 + .../prisma/sql/getUsageCountByTypeSince.sql | 12 + packages/db/prisma/sql/listContacts.sql | 7 + ...istContactsWithConversationsOverPeriod.sql | 23 + .../listContactsWithMessagesOverPeriod.sql | 24 + .../sql/listContactsWithRatingsOverPeriod.sql | 26 + .../sql/listConversationsOverPeriod.sql | 25 + ...rsationsWithNumberedMessagesOverPeriod.sql | 24 + ...gsOfTypeActionsGroupedByTypeOverPeriod.sql | 18 + .../sql/listTopBotsByTokenUsageOverPeriod.sql | 19 + .../listTopContactsByTokenUsageOverPeriod.sql | 19 + .../sql/listTopDownvotersOverPeriod.sql | 24 + .../prisma/sql/listTopUpvotersOverPeriod.sql | 24 + packages/db/prisma/zod-generator.config.json | 24 + packages/db/scripts/derive.js | 16 + packages/db/scripts/generate.js | 51 + packages/db/src/constraints.ts | 48 + packages/db/src/index.ts | 86 + packages/db/tsconfig.json | 37 + packages/debug/README.md | 11 + packages/debug/jest.config.js | 19 + packages/debug/package.json | 42 + packages/debug/src/index.test.js | 187 + packages/debug/src/index.ts | 635 + packages/debug/src/redact.test.js | 244 + packages/debug/tsconfig.json | 38 + packages/email-spec/README.md | 28 + packages/email-spec/package.json | 34 + packages/email-spec/src/index.ts | 220 + packages/email-spec/tsconfig.json | 29 + packages/email/README.md | 23 + packages/email/jest.config.js | 5 + packages/email/package.json | 42 + packages/email/src/index.test.js | 67 + packages/email/src/index.ts | 112 + packages/email/tsconfig.json | 35 + packages/encoding/README.md | 1 + packages/encoding/jest.config.js | 5 + packages/encoding/package.json | 35 + packages/encoding/src/index.test.ts | 27 + packages/encoding/src/index.ts | 200 + packages/encoding/tsconfig.json | 23 + packages/env/README.md | 8 + packages/env/jest.config.js | 5 + packages/env/package.json | 38 + packages/env/src/index.test.js | 94 + packages/env/src/index.ts | 27 + packages/env/tsconfig.json | 38 + packages/errors/README.md | 12 + packages/errors/jest.config.js | 19 + packages/errors/package.json | 42 + packages/errors/src/index.test.js | 904 + packages/errors/src/index.ts | 714 + packages/errors/tsconfig.json | 38 + packages/eslint-config/index.js | 202 + packages/eslint-config/package.json | 36 + packages/fetch/README.md | 11 + packages/fetch/jest.config.js | 19 + packages/fetch/package.json | 43 + packages/fetch/src/index.test.js | 2009 + packages/fetch/src/index.ts | 1176 + packages/fetch/tsconfig.json | 38 + packages/file-csv/README.md | 1 + packages/file-csv/data/test01.csv | 2 + packages/file-csv/jest.config.js | 5 + packages/file-csv/package.json | 42 + packages/file-csv/src/index.ts | 17 + packages/file-csv/src/parse.test.ts | 21 + packages/file-csv/src/parse.ts | 26 + packages/file-csv/tsconfig.json | 23 + packages/file-docx/README.md | 1 + packages/file-docx/data/test01.docx | 3 + packages/file-docx/jest.config.js | 5 + packages/file-docx/package.json | 43 + packages/file-docx/src/index.ts | 41 + packages/file-docx/src/parse.test.ts | 13 + packages/file-docx/src/parse.ts | 7 + packages/file-docx/tsconfig.json | 23 + packages/file-html/README.md | 1 + packages/file-html/jest.config.js | 5 + packages/file-html/package.json | 44 + packages/file-html/src/index.ts | 41 + packages/file-html/src/parse.test.ts | 132 + packages/file-html/src/parse.ts | 196 + packages/file-html/tsconfig.json | 23 + packages/file-json/README.md | 1 + packages/file-json/jest.config.js | 5 + packages/file-json/package.json | 39 + packages/file-json/src/index.test.ts | 172 + packages/file-json/src/index.ts | 108 + packages/file-json/tsconfig.json | 23 + packages/file-jsonl/README.md | 1 + packages/file-jsonl/jest.config.js | 5 + packages/file-jsonl/package.json | 38 + packages/file-jsonl/src/index.test.ts | 101 + packages/file-jsonl/src/index.ts | 32 + packages/file-jsonl/tsconfig.json | 23 + packages/file-md/README.md | 1 + packages/file-md/jest.config.js | 5 + packages/file-md/package.json | 39 + packages/file-md/src/index.test.ts | 5 + packages/file-md/src/index.ts | 37 + packages/file-md/tsconfig.json | 23 + packages/file-pdf/README.md | 1 + packages/file-pdf/data/test01.pdf | Bin 0 -> 130 bytes packages/file-pdf/jest.config.js | 5 + packages/file-pdf/package.json | 44 + packages/file-pdf/src/index.ts | 41 + packages/file-pdf/src/parse.test.ts | 13 + packages/file-pdf/src/parse.ts | 15 + packages/file-pdf/tsconfig.json | 23 + packages/file-pptx/README.md | 1 + packages/file-pptx/data/test01.pptx | 3 + packages/file-pptx/jest.config.js | 5 + packages/file-pptx/package.json | 43 + packages/file-pptx/src/index.ts | 41 + packages/file-pptx/src/parse.test.ts | 13 + packages/file-pptx/src/parse.ts | 7 + packages/file-pptx/tsconfig.json | 23 + packages/file-txt/README.md | 1 + packages/file-txt/jest.config.js | 5 + packages/file-txt/package.json | 39 + packages/file-txt/src/index.test.ts | 5 + packages/file-txt/src/index.ts | 37 + packages/file-txt/tsconfig.json | 23 + packages/file-xlsx/README.md | 1 + packages/file-xlsx/data/test01.xlsx | 3 + packages/file-xlsx/jest.config.js | 5 + packages/file-xlsx/package.json | 43 + packages/file-xlsx/src/index.ts | 41 + packages/file-xlsx/src/parse.test.ts | 15 + packages/file-xlsx/src/parse.ts | 7 + packages/file-xlsx/tsconfig.json | 23 + packages/file-yaml/README.md | 1 + packages/file-yaml/jest.config.js | 5 + packages/file-yaml/package.json | 40 + packages/file-yaml/src/index.test.ts | 82 + packages/file-yaml/src/index.ts | 85 + packages/file-yaml/tsconfig.json | 23 + packages/file/README.md | 1 + packages/file/jest.config.js | 5 + packages/file/package.json | 55 + packages/file/src/index.test.ts | 35 + packages/file/src/index.ts | 109 + packages/file/src/index2.test.ts | 44 + packages/file/src/index2.ts | 111 + packages/file/src/support.ts | 36 + packages/file/tsconfig.json | 23 + packages/gpt/README.md | 1 + packages/gpt/jest.config.js | 5 + packages/gpt/package.json | 42 + packages/gpt/src/index.test.ts | 519 + packages/gpt/src/index.ts | 484 + packages/gpt/src/text-splitter.test.ts | 1195 + packages/gpt/src/text-splitter.ts | 339 + packages/gpt/tsconfig.json | 23 + packages/http-codes/README.md | 13 + packages/http-codes/package.json | 34 + packages/http-codes/src/index.ts | 174 + packages/http-codes/tsconfig.json | 35 + packages/it/README.md | 7 + packages/it/jest.config.js | 23 + packages/it/package.json | 38 + packages/it/src/index.test.js | 1025 + packages/it/src/index.ts | 646 + packages/it/tsconfig.json | 38 + packages/jest-jsdom/README.md | 1 + packages/jest-jsdom/jest.config.js | 5 + packages/jest-jsdom/package.json | 35 + packages/jest-jsdom/src/index.js | 231 + packages/jest-jsdom/src/index.test.js | 73 + packages/jest-jsdom/tsconfig.json | 23 + packages/json/README.md | 8 + packages/json/jest.config.js | 5 + packages/json/package.json | 41 + packages/json/src/index.test.js | 459 + packages/json/src/index.ts | 84 + packages/json/tsconfig.json | 38 + packages/math/README.md | 6 + packages/math/jest.config.js | 5 + packages/math/package.json | 41 + packages/math/src/index.test.js | 15 + packages/math/src/index.ts | 20 + packages/math/tsconfig.json | 38 + .../.github/workflows/backmerge.yaml | 41 + packages/mcp-widgets/.gitignore | 142 + packages/mcp-widgets/.prettierrc | 7 + packages/mcp-widgets/.storybook/main.ts | 23 + packages/mcp-widgets/.storybook/preview.tsx | 30 + packages/mcp-widgets/LICENSE | 21 + packages/mcp-widgets/README.md | 235 + packages/mcp-widgets/eslint.config.mjs | 45 + packages/mcp-widgets/package.json | 124 + packages/mcp-widgets/scripts/build-cdn.js | 298 + packages/mcp-widgets/scripts/validate-cdn.js | 325 + packages/mcp-widgets/src/index.ts | 21 + packages/mcp-widgets/src/loaders/chatgpt.ts | 168 + packages/mcp-widgets/src/schemas/index.ts | 18 + .../mcp-widgets/src/schemas/manifest.test.ts | 162 + packages/mcp-widgets/src/schemas/manifest.ts | 150 + packages/mcp-widgets/src/styles.d.ts | 1 + packages/mcp-widgets/src/styles/base.css | 7 + packages/mcp-widgets/src/types/index.ts | 7 + packages/mcp-widgets/src/types/manifest.ts | 170 + packages/mcp-widgets/src/types/openai.ts | 53 + packages/mcp-widgets/src/types/widget.ts | 25 + packages/mcp-widgets/src/utils/index.ts | 6 + packages/mcp-widgets/src/utils/openai.ts | 78 + packages/mcp-widgets/src/utils/register.ts | 119 + .../widgets/data-card/DataCard.stories.tsx | 153 + .../src/widgets/data-card/DataCard.test.ts | 55 + .../src/widgets/data-card/DataCard.ts | 201 + .../mcp-widgets/src/widgets/data-card/cdn.ts | 11 + .../src/widgets/data-card/index.ts | 1 + .../src/widgets/data-card/manifest.ts | 45 + packages/mcp-widgets/src/widgets/index.ts | 17 + .../rich-data-card/RichDataCard.stories.tsx | 181 + .../rich-data-card/RichDataCard.test.tsx | 62 + .../widgets/rich-data-card/RichDataCard.tsx | 135 + .../src/widgets/rich-data-card/cdn.tsx | 71 + .../src/widgets/rich-data-card/index.ts | 6 + .../src/widgets/rich-data-card/manifest.ts | 79 + .../src/widgets/rich-data-card/styles.css | 114 + packages/mcp-widgets/tailwind.config.ts | 5 + packages/mcp-widgets/tsconfig.build.json | 19 + packages/mcp-widgets/tsconfig.json | 34 + packages/mcp-widgets/vite.config.ts | 29 + packages/mcp-widgets/vitest.config.ts | 17 + packages/mcp-widgets/vitest.setup.ts | 1 + packages/md/README.md | 19 + packages/md/jest.config.js | 61 + packages/md/package.json | 78 + packages/md/src/chat.test.js | 479 + packages/md/src/chat.ts | 505 + packages/md/src/convert.test.js | 44 + packages/md/src/convert.ts | 39 + packages/md/src/extract.test.js | 166 + packages/md/src/extract.ts | 122 + packages/md/src/frontmatter.test.js | 409 + packages/md/src/frontmatter.ts | 59 + packages/md/src/index.ts | 11 + packages/md/src/linkify.test.js | 126 + packages/md/src/linkify.ts | 69 + packages/md/src/split.test.js | 309 + packages/md/src/split.ts | 309 + packages/md/src/table.test.js | 134 + packages/md/src/table.ts | 22 + packages/md/tsconfig.json | 38 + packages/memcache-spec/package.json | 34 + packages/memcache-spec/src/index.ts | 329 + packages/memcache-spec/tsconfig.json | 35 + packages/memcache/README.md | 58 + packages/memcache/jest.config.js | 5 + packages/memcache/package.json | 44 + packages/memcache/src/index.browser.ts | 17 + packages/memcache/src/index.test.js | 671 + packages/memcache/src/index.ts | 287 + packages/memcache/src/memory.ts | 1094 + packages/memcache/src/redis.test.js | 327 + packages/memcache/src/redis.ts | 780 + packages/memcache/tsconfig.json | 37 + packages/metascraper-font/README.md | 1 + packages/metascraper-font/jest.config.js | 5 + packages/metascraper-font/package.json | 39 + packages/metascraper-font/src/index.test.ts | 5 + packages/metascraper-font/src/index.ts | 66 + packages/metascraper-font/tsconfig.json | 23 + packages/observability-spec/README.md | 25 + packages/observability-spec/package.json | 38 + packages/observability-spec/src/index.ts | 75 + packages/observability-spec/src/next.ts | 8 + packages/observability-spec/tsconfig.json | 29 + packages/observability/README.md | 10 + packages/observability/jest.config.js | 5 + packages/observability/package.json | 54 + packages/observability/src/index.test.js | 82 + packages/observability/src/index.ts | 102 + packages/observability/src/next.test.js | 17 + packages/observability/src/next/client.ts | 5 + packages/observability/src/next/config.js | 8 + packages/observability/src/next/server.ts | 9 + packages/observability/tsconfig.json | 34 + packages/openapi-types-generator/README.md | 85 + packages/openapi-types-generator/bin/cli.ts | 83 + .../openapi-types-generator/jest.config.js | 5 + packages/openapi-types-generator/package.json | 44 + .../openapi-types-generator/src/generator.ts | 179 + .../openapi-types-generator/src/index.test.js | 86 + packages/openapi-types-generator/src/index.ts | 58 + .../openapi-types-generator/src/naming.ts | 120 + .../openapi-types-generator/src/parser.ts | 221 + .../src/swagger-client.d.ts | 7 + .../openapi-types-generator/tsconfig.json | 19 + packages/partners-spec/README.md | 6 + packages/partners-spec/package.json | 34 + packages/partners-spec/src/index.ts | 157 + packages/partners-spec/tsconfig.json | 35 + packages/partners/README.md | 35 + packages/partners/package.json | 42 + packages/partners/src/assert.d.ts | 1 + packages/partners/src/assert.js | 15 + packages/partners/src/index.d.ts | 11 + packages/partners/src/index.js | 26 + packages/partners/tsconfig.json | 36 + packages/path/jest.config.js | 5 + packages/path/package.json | 37 + packages/path/src/index.test.js | 478 + packages/path/src/index.ts | 99 + packages/path/tsconfig.json | 38 + packages/pii-spec/README.md | 34 + packages/pii-spec/package.json | 34 + packages/pii-spec/src/index.ts | 155 + packages/pii-spec/tsconfig.json | 35 + packages/pii/README.md | 30 + packages/pii/jest.config.js | 5 + packages/pii/package.json | 41 + packages/pii/src/index.test.js | 114 + packages/pii/src/index.ts | 98 + packages/pii/tsconfig.json | 37 + packages/queue-spec/package.json | 34 + packages/queue-spec/src/index.ts | 210 + packages/queue-spec/tsconfig.json | 35 + packages/queue/jest.config.js | 11 + packages/queue/package.json | 41 + packages/queue/src/index.test.js | 159 + packages/queue/src/index.ts | 185 + packages/queue/tsconfig.json | 37 + .../.github/workflows/backmerge.yaml | 41 + packages/react-prompt-kit/.gitignore | 143 + packages/react-prompt-kit/.npmignore | 36 + packages/react-prompt-kit/CHANGELOG.md | 15 + packages/react-prompt-kit/LICENSE | 21 + packages/react-prompt-kit/README.md | 774 + packages/react-prompt-kit/examples/demo.tsx | 54 + packages/react-prompt-kit/jest.config.js | 5 + packages/react-prompt-kit/package.json | 58 + .../react-prompt-kit/src/components.test.tsx | 356 + packages/react-prompt-kit/src/components.tsx | 233 + packages/react-prompt-kit/src/index.ts | 16 + packages/react-prompt-kit/src/prompt.test.tsx | 535 + packages/react-prompt-kit/src/prompt.ts | 404 + packages/react-prompt-kit/tsconfig.build.json | 14 + packages/react-prompt-kit/tsconfig.json | 26 + packages/regex/README.md | 5 + packages/regex/jest.config.js | 5 + packages/regex/package.json | 38 + packages/regex/src/index.test.js | 409 + packages/regex/src/index.ts | 40 + packages/regex/tsconfig.json | 38 + packages/relay-spec/package.json | 34 + packages/relay-spec/src/index.ts | 107 + packages/relay-spec/tsconfig.json | 35 + packages/relay/jest.config.js | 11 + packages/relay/package.json | 41 + packages/relay/src/error.ts | 33 + packages/relay/src/index.test.js | 57 + packages/relay/src/index.ts | 77 + packages/relay/tsconfig.json | 37 + packages/respond-spec/package.json | 34 + packages/respond-spec/src/index.ts | 85 + packages/respond-spec/tsconfig.json | 35 + packages/respond/jest.config.js | 11 + packages/respond/package.json | 41 + packages/respond/src/error.ts | 36 + packages/respond/src/index.test.js | 47 + packages/respond/src/index.ts | 64 + packages/respond/tsconfig.json | 37 + packages/sandbox-spec/package.json | 37 + packages/sandbox-spec/src/index.ts | 319 + packages/sandbox-spec/tsconfig.json | 35 + packages/sandbox/README.md | 112 + packages/sandbox/jest.config.js | 11 + packages/sandbox/package.json | 43 + .../sandbox/scripts/verify-interpreters.js | 85 + packages/sandbox/src/index.test.js | 268 + packages/sandbox/src/index.ts | 440 + packages/sandbox/tsconfig.json | 37 + packages/screenshot-spec/package.json | 34 + packages/screenshot-spec/src/index.ts | 206 + packages/screenshot-spec/tsconfig.json | 35 + packages/screenshot/jest.config.js | 11 + packages/screenshot/package.json | 41 + packages/screenshot/src/error.ts | 36 + packages/screenshot/src/index.test.js | 53 + packages/screenshot/src/index.ts | 90 + packages/screenshot/tsconfig.json | 37 + packages/script/README.md | 32 + packages/script/jest.config.js | 19 + packages/script/package.json | 42 + packages/script/src/index.test.js | 411 + packages/script/src/index.ts | 463 + packages/script/src/inquirer.d.ts | 12 + packages/script/tsconfig.json | 36 + packages/searchengine-spec/README.md | 43 + packages/searchengine-spec/package.json | 34 + packages/searchengine-spec/src/index.ts | 101 + packages/searchengine-spec/tsconfig.json | 35 + packages/searchengine/README.md | 35 + packages/searchengine/jest.config.js | 5 + packages/searchengine/package.json | 41 + packages/searchengine/src/index.test.js | 27 + packages/searchengine/src/index.ts | 55 + packages/searchengine/tsconfig.json | 37 + packages/secrets-platform-spec/README.md | 30 + packages/secrets-platform-spec/package.json | 34 + packages/secrets-platform-spec/src/index.ts | 116 + packages/secrets-platform-spec/tsconfig.json | 29 + packages/secrets-platform/README.md | 46 + packages/secrets-platform/jest.config.js | 5 + packages/secrets-platform/package.json | 41 + packages/secrets-platform/src/index.test.js | 11 + packages/secrets-platform/src/index.ts | 32 + packages/secrets-platform/tsconfig.json | 31 + packages/sql/README.md | 1 + packages/sql/jest.config.js | 5 + packages/sql/package.json | 44 + packages/sql/src/driver.test.ts | 467 + packages/sql/src/driver.ts | 230 + packages/sql/src/index.ts | 2 + packages/sql/src/parse.test.ts | 463 + packages/sql/src/parse.ts | 723 + packages/sql/tsconfig.json | 23 + packages/storage-spec/package.json | 34 + packages/storage-spec/src/index.ts | 240 + packages/storage-spec/tsconfig.json | 35 + packages/storage/README.md | 42 + packages/storage/jest.config.js | 10 + packages/storage/package.json | 48 + packages/storage/src/adapter.test.js | 427 + packages/storage/src/index.test.js | 212 + packages/storage/src/index.ts | 698 + packages/storage/src/mounts.test.js | 213 + packages/storage/src/polyfill.test.js | 117 + packages/storage/src/sts.ts | 112 + packages/storage/tsconfig.json | 37 + packages/string/README.md | 6 + packages/string/jest.config.js | 5 + packages/string/package.json | 41 + packages/string/src/index.test.js | 1090 + packages/string/src/index.ts | 735 + packages/string/tsconfig.json | 38 + packages/struct/README.md | 8 + packages/struct/jest.config.js | 5 + packages/struct/package.json | 38 + packages/struct/src/index.test.js | 272 + packages/struct/src/index.ts | 182 + packages/struct/tsconfig.json | 38 + packages/template/README.md | 54 + packages/template/jest.config.js | 5 + packages/template/package.json | 36 + packages/template/src/index.test.ts | 398 + packages/template/src/index.ts | 265 + packages/template/tsconfig.json | 23 + packages/time/README.md | 12 + packages/time/jest.config.js | 5 + packages/time/package.json | 36 + packages/time/src/index.test.ts | 548 + packages/time/src/index.ts | 567 + packages/time/tsconfig.json | 23 + packages/typescript-utils/README.md | 1 + packages/typescript-utils/jest.config.js | 5 + packages/typescript-utils/package.json | 65 + packages/typescript-utils/src/args.ts | 12 + packages/typescript-utils/src/assert.ts | 45 + packages/typescript-utils/src/exact.ts | 19 + packages/typescript-utils/src/function.ts | 35 + packages/typescript-utils/src/index.test.ts | 5 + packages/typescript-utils/src/index.ts | 10 + packages/typescript-utils/src/number.ts | 17 + packages/typescript-utils/src/object.ts | 12 + packages/typescript-utils/src/promise.ts | 1 + packages/typescript-utils/src/record.ts | 3 + packages/typescript-utils/src/string.ts | 15 + packages/typescript-utils/src/unreachable.ts | 13 + packages/typescript-utils/tsconfig.json | 23 + packages/vector-spec/package.json | 34 + packages/vector-spec/src/index.ts | 279 + packages/vector-spec/tsconfig.json | 35 + packages/vector/jest.config.js | 11 + packages/vector/package.json | 42 + packages/vector/src/embed.ts | 166 + packages/vector/src/error.ts | 49 + packages/vector/src/files.ts | 219 + packages/vector/src/index.test.js | 470 + packages/vector/src/index.ts | 109 + packages/vector/src/local.ts | 358 + packages/vector/src/qdrant.test.js | 380 + packages/vector/src/qdrant.ts | 632 + packages/vector/tsconfig.json | 37 + packages/yaml/README.md | 7 + packages/yaml/jest.config.js | 19 + packages/yaml/package.json | 42 + packages/yaml/src/index.test.js | 1015 + packages/yaml/src/index.ts | 85 + packages/yaml/tsconfig.json | 38 + patches/README.md | 5 + patches/next-auth@4.24.15.patch | 60 + patches/react-markdown@8.0.7.patch | 8 + platform/.env.example | 308 + platform/.gitignore | 37 + platform/.storybook/main.ts | 214 + platform/.storybook/main.utest.js | 47 + platform/.storybook/mocks/async_hooks.ts | 1 + platform/.storybook/mocks/empty.ts | 5 + platform/.storybook/mocks/prisma-client.ts | 17 + platform/.storybook/mocks/prisma-generated.js | 26 + platform/.storybook/preview.tsx | 30 + platform/.vercelignore | 2 + platform/README.md | 102 + platform/app/apps/(adhoc)/30fc6ef2/README.md | 76 + .../app/apps/(adhoc)/30fc6ef2/app.manifest | 9 + .../app/apps/(adhoc)/30fc6ef2/components.jsx | 536 + platform/app/apps/(adhoc)/30fc6ef2/config.ts | 47 + platform/app/apps/(adhoc)/30fc6ef2/const.ts | 5 + platform/app/apps/(adhoc)/30fc6ef2/layout.jsx | 43 + platform/app/apps/(adhoc)/30fc6ef2/page.tsx | 9 + .../app/apps/(adhoc)/30fc6ef2/prompt.d.ts | 9 + .../app/apps/(adhoc)/30fc6ef2/prompt.yaml | 29 + platform/app/apps/(adhoc)/30fc6ef2/server.ts | 242 + platform/app/apps/(adhoc)/3e882a70/README.md | 27 + .../(adhoc)/3e882a70/[datasetId]/page.tsx | 54 + .../app/apps/(adhoc)/3e882a70/app.manifest | 9 + .../app/apps/(adhoc)/3e882a70/components.jsx | 373 + platform/app/apps/(adhoc)/3e882a70/config.ts | 15 + platform/app/apps/(adhoc)/3e882a70/const.ts | 3 + platform/app/apps/(adhoc)/3e882a70/layout.jsx | 39 + platform/app/apps/(adhoc)/3e882a70/page.tsx | 35 + platform/app/apps/(adhoc)/3e882a70/server.ts | 182 + platform/app/apps/(adhoc)/41f203dc/README.md | 27 + .../app/apps/(adhoc)/41f203dc/app.manifest | 8 + .../app/apps/(adhoc)/41f203dc/components.jsx | 374 + platform/app/apps/(adhoc)/41f203dc/config.ts | 16 + platform/app/apps/(adhoc)/41f203dc/const.ts | 3 + platform/app/apps/(adhoc)/41f203dc/layout.jsx | 47 + platform/app/apps/(adhoc)/41f203dc/page.tsx | 52 + platform/app/apps/(adhoc)/41f203dc/server.ts | 837 + platform/app/apps/(adhoc)/5c0a7a11/README.md | 9 + .../app/apps/(adhoc)/5c0a7a11/app.manifest | 9 + .../app/apps/(adhoc)/5c0a7a11/components.jsx | 1116 + platform/app/apps/(adhoc)/5c0a7a11/config.ts | 17 + platform/app/apps/(adhoc)/5c0a7a11/const.ts | 4 + platform/app/apps/(adhoc)/5c0a7a11/layout.jsx | 47 + platform/app/apps/(adhoc)/5c0a7a11/page.tsx | 42 + platform/app/apps/(adhoc)/5c0a7a11/server.ts | 663 + .../app/apps/(adhoc)/5c0a7a11/server.utest.js | 354 + platform/app/apps/(adhoc)/6c4a7b9e/README.md | 40 + .../app/apps/(adhoc)/6c4a7b9e/app.manifest | 9 + .../app/apps/(adhoc)/6c4a7b9e/components.jsx | 129 + .../(adhoc)/6c4a7b9e/components.utest.jsx | 47 + platform/app/apps/(adhoc)/6c4a7b9e/config.ts | 15 + platform/app/apps/(adhoc)/6c4a7b9e/const.ts | 3 + platform/app/apps/(adhoc)/6c4a7b9e/layout.jsx | 47 + platform/app/apps/(adhoc)/6c4a7b9e/page.tsx | 5 + platform/app/apps/(adhoc)/6e3b7f2a/README.md | 29 + .../app/apps/(adhoc)/6e3b7f2a/app.manifest | 9 + .../app/apps/(adhoc)/6e3b7f2a/components.jsx | 852 + platform/app/apps/(adhoc)/6e3b7f2a/config.ts | 15 + platform/app/apps/(adhoc)/6e3b7f2a/const.ts | 3 + platform/app/apps/(adhoc)/6e3b7f2a/layout.jsx | 47 + .../app/apps/(adhoc)/6e3b7f2a/normalize.ts | 119 + platform/app/apps/(adhoc)/6e3b7f2a/page.tsx | 24 + platform/app/apps/(adhoc)/6e3b7f2a/server.ts | 176 + .../app/apps/(adhoc)/6e3b7f2a/server.utest.js | 127 + platform/app/apps/(adhoc)/7cb29ccc/README.md | 27 + .../app/apps/(adhoc)/7cb29ccc/app.manifest | 9 + .../app/apps/(adhoc)/7cb29ccc/components.jsx | 215 + platform/app/apps/(adhoc)/7cb29ccc/config.ts | 15 + platform/app/apps/(adhoc)/7cb29ccc/const.ts | 3 + platform/app/apps/(adhoc)/7cb29ccc/layout.jsx | 39 + platform/app/apps/(adhoc)/7cb29ccc/page.jsx | 39 + platform/app/apps/(adhoc)/7cb29ccc/server.ts | 40 + platform/app/apps/(adhoc)/7f1fb51c/README.md | 27 + .../app/apps/(adhoc)/7f1fb51c/app.manifest | 9 + .../app/apps/(adhoc)/7f1fb51c/components.jsx | 28 + platform/app/apps/(adhoc)/7f1fb51c/config.ts | 15 + platform/app/apps/(adhoc)/7f1fb51c/const.ts | 3 + platform/app/apps/(adhoc)/7f1fb51c/layout.jsx | 46 + platform/app/apps/(adhoc)/7f1fb51c/page.tsx | 9 + platform/app/apps/(adhoc)/81fdc94a/README.md | 27 + .../app/apps/(adhoc)/81fdc94a/app.manifest | 9 + .../app/apps/(adhoc)/81fdc94a/components.jsx | 192 + platform/app/apps/(adhoc)/81fdc94a/config.ts | 15 + platform/app/apps/(adhoc)/81fdc94a/const.ts | 3 + platform/app/apps/(adhoc)/81fdc94a/layout.jsx | 39 + platform/app/apps/(adhoc)/81fdc94a/page.tsx | 35 + platform/app/apps/(adhoc)/81fdc94a/server.ts | 139 + platform/app/apps/(adhoc)/8734d1ad/README.md | 85 + .../app/apps/(adhoc)/8734d1ad/app.manifest | 9 + .../app/apps/(adhoc)/8734d1ad/components.jsx | 117 + platform/app/apps/(adhoc)/8734d1ad/config.ts | 26 + platform/app/apps/(adhoc)/8734d1ad/const.ts | 3 + platform/app/apps/(adhoc)/8734d1ad/layout.jsx | 39 + platform/app/apps/(adhoc)/8734d1ad/page.tsx | 35 + platform/app/apps/(adhoc)/8734d1ad/server.ts | 335 + platform/app/apps/(adhoc)/8df57107/README.md | 151 + .../8df57107/[blueprintId]/components.jsx | 203 + .../8df57107/[blueprintId]/context.jsx | 15 + .../[blueprintId]/files/components.jsx | 264 + .../8df57107/[blueprintId]/files/page.tsx | 62 + .../(adhoc)/8df57107/[blueprintId]/layout.jsx | 121 + .../(adhoc)/8df57107/[blueprintId]/page.tsx | 9 + .../[blueprintId]/sitemaps/components.jsx | 253 + .../8df57107/[blueprintId]/sitemaps/page.tsx | 58 + .../app/apps/(adhoc)/8df57107/app.manifest | 9 + .../app/apps/(adhoc)/8df57107/components.jsx | 275 + platform/app/apps/(adhoc)/8df57107/config.ts | 15 + platform/app/apps/(adhoc)/8df57107/const.ts | 3 + platform/app/apps/(adhoc)/8df57107/layout.jsx | 39 + platform/app/apps/(adhoc)/8df57107/page.tsx | 35 + platform/app/apps/(adhoc)/8df57107/server.ts | 703 + platform/app/apps/(adhoc)/8ea0112f/README.md | 27 + .../app/apps/(adhoc)/8ea0112f/app.manifest | 9 + .../app/apps/(adhoc)/8ea0112f/components.jsx | 284 + platform/app/apps/(adhoc)/8ea0112f/config.ts | 15 + platform/app/apps/(adhoc)/8ea0112f/const.ts | 3 + platform/app/apps/(adhoc)/8ea0112f/layout.jsx | 41 + platform/app/apps/(adhoc)/8ea0112f/page.tsx | 35 + platform/app/apps/(adhoc)/8ea0112f/server.ts | 88 + platform/app/apps/(adhoc)/90dff690/README.md | 27 + .../app/apps/(adhoc)/90dff690/app.manifest | 9 + .../app/apps/(adhoc)/90dff690/components.jsx | 315 + platform/app/apps/(adhoc)/90dff690/config.ts | 15 + platform/app/apps/(adhoc)/90dff690/const.ts | 3 + platform/app/apps/(adhoc)/90dff690/layout.jsx | 41 + platform/app/apps/(adhoc)/90dff690/page.tsx | 35 + platform/app/apps/(adhoc)/90dff690/server.ts | 148 + platform/app/apps/(adhoc)/95ca8b4c/README.md | 27 + .../(adhoc)/95ca8b4c/[[...args]]/page.tsx | 23 + .../app/apps/(adhoc)/95ca8b4c/app.manifest | 9 + .../app/apps/(adhoc)/95ca8b4c/components.jsx | 952 + platform/app/apps/(adhoc)/95ca8b4c/config.ts | 19 + platform/app/apps/(adhoc)/95ca8b4c/const.ts | 3 + platform/app/apps/(adhoc)/95ca8b4c/layout.jsx | 70 + platform/app/apps/(adhoc)/95ca8b4c/server.ts | 344 + platform/app/apps/(adhoc)/9f3b5e2a/README.md | 27 + .../(adhoc)/9f3b5e2a/[spaceId]/components.jsx | 226 + .../apps/(adhoc)/9f3b5e2a/[spaceId]/page.tsx | 55 + .../apps/(adhoc)/9f3b5e2a/[spaceId]/server.ts | 157 + .../app/apps/(adhoc)/9f3b5e2a/app.manifest | 9 + .../app/apps/(adhoc)/9f3b5e2a/components.jsx | 327 + platform/app/apps/(adhoc)/9f3b5e2a/config.ts | 15 + platform/app/apps/(adhoc)/9f3b5e2a/const.ts | 3 + platform/app/apps/(adhoc)/9f3b5e2a/layout.jsx | 41 + platform/app/apps/(adhoc)/9f3b5e2a/page.tsx | 35 + platform/app/apps/(adhoc)/9f3b5e2a/server.ts | 114 + platform/app/apps/(adhoc)/b4d0c8f2/README.md | 27 + .../app/apps/(adhoc)/b4d0c8f2/app.manifest | 8 + .../app/apps/(adhoc)/b4d0c8f2/components.jsx | 969 + platform/app/apps/(adhoc)/b4d0c8f2/config.ts | 15 + platform/app/apps/(adhoc)/b4d0c8f2/const.ts | 3 + platform/app/apps/(adhoc)/b4d0c8f2/layout.jsx | 47 + platform/app/apps/(adhoc)/b4d0c8f2/page.tsx | 53 + platform/app/apps/(adhoc)/b4d0c8f2/server.ts | 348 + platform/app/apps/(adhoc)/b7f4c2de/README.md | 19 + .../b7f4c2de/[blueprintId]/components.jsx | 32 + .../(adhoc)/b7f4c2de/[blueprintId]/page.tsx | 43 + .../(adhoc)/b7f4c2de/[blueprintId]/viewer.jsx | 67 + .../app/apps/(adhoc)/b7f4c2de/app.manifest | 9 + .../app/apps/(adhoc)/b7f4c2de/components.jsx | 32 + platform/app/apps/(adhoc)/b7f4c2de/config.ts | 18 + platform/app/apps/(adhoc)/b7f4c2de/const.ts | 3 + platform/app/apps/(adhoc)/b7f4c2de/layout.jsx | 46 + platform/app/apps/(adhoc)/b7f4c2de/page.tsx | 35 + platform/app/apps/(adhoc)/b7f4c2de/server.ts | 263 + platform/app/apps/(adhoc)/c0de9a7f/README.md | 60 + .../(adhoc)/c0de9a7f/[factory]/components.jsx | 644 + .../(adhoc)/c0de9a7f/[factory]/layout.jsx | 74 + .../apps/(adhoc)/c0de9a7f/[factory]/page.tsx | 41 + .../[factory]/playbooks/components.jsx | 328 + .../c0de9a7f/[factory]/playbooks/page.tsx | 17 + .../[factory]/settings/components.jsx | 478 + .../c0de9a7f/[factory]/settings/page.tsx | 43 + .../app/apps/(adhoc)/c0de9a7f/app.manifest | 10 + .../app/apps/(adhoc)/c0de9a7f/components.jsx | 310 + platform/app/apps/(adhoc)/c0de9a7f/config.ts | 21 + platform/app/apps/(adhoc)/c0de9a7f/const.ts | 60 + .../apps/(adhoc)/c0de9a7f/factory-template.ts | 297 + platform/app/apps/(adhoc)/c0de9a7f/factory.ts | 305 + platform/app/apps/(adhoc)/c0de9a7f/layout.jsx | 46 + platform/app/apps/(adhoc)/c0de9a7f/page.tsx | 40 + platform/app/apps/(adhoc)/c0de9a7f/server.ts | 967 + platform/app/apps/(adhoc)/c19e4a72/README.md | 17 + .../app/apps/(adhoc)/c19e4a72/app.manifest | 8 + .../app/apps/(adhoc)/c19e4a72/components.jsx | 774 + platform/app/apps/(adhoc)/c19e4a72/config.ts | 27 + platform/app/apps/(adhoc)/c19e4a72/const.ts | 3 + platform/app/apps/(adhoc)/c19e4a72/layout.jsx | 47 + platform/app/apps/(adhoc)/c19e4a72/page.tsx | 16 + platform/app/apps/(adhoc)/c19e4a72/server.ts | 59 + platform/app/apps/(adhoc)/d6d4b7eb/README.md | 27 + .../app/apps/(adhoc)/d6d4b7eb/app.manifest | 9 + .../app/apps/(adhoc)/d6d4b7eb/components.jsx | 146 + platform/app/apps/(adhoc)/d6d4b7eb/config.ts | 15 + platform/app/apps/(adhoc)/d6d4b7eb/const.ts | 3 + platform/app/apps/(adhoc)/d6d4b7eb/layout.jsx | 41 + platform/app/apps/(adhoc)/d6d4b7eb/page.jsx | 33 + platform/app/apps/(adhoc)/d6d4b7eb/server.ts | 45 + platform/app/apps/(adhoc)/e083ca0f/README.md | 27 + .../app/apps/(adhoc)/e083ca0f/app.manifest | 9 + .../app/apps/(adhoc)/e083ca0f/components.jsx | 60 + platform/app/apps/(adhoc)/e083ca0f/config.ts | 15 + platform/app/apps/(adhoc)/e083ca0f/const.ts | 3 + platform/app/apps/(adhoc)/e083ca0f/layout.jsx | 39 + platform/app/apps/(adhoc)/e083ca0f/page.jsx | 9 + .../apps/(adhoc)/f2a9c7d4/(home)/layout.jsx | 20 + .../app/apps/(adhoc)/f2a9c7d4/(home)/page.tsx | 35 + .../(project)/[projectId]/components.jsx | 1375 + .../f2a9c7d4/(project)/[projectId]/layout.jsx | 31 + .../f2a9c7d4/(project)/[projectId]/page.tsx | 35 + .../f2a9c7d4/(project)/[projectId]/server.ts | 364 + platform/app/apps/(adhoc)/f2a9c7d4/README.md | 27 + .../app/apps/(adhoc)/f2a9c7d4/app.manifest | 10 + .../app/apps/(adhoc)/f2a9c7d4/components.jsx | 312 + platform/app/apps/(adhoc)/f2a9c7d4/config.ts | 16 + platform/app/apps/(adhoc)/f2a9c7d4/const.ts | 12 + platform/app/apps/(adhoc)/f2a9c7d4/layout.jsx | 35 + platform/app/apps/(adhoc)/f2a9c7d4/lib.ts | 132 + platform/app/apps/(adhoc)/f2a9c7d4/server.ts | 142 + platform/app/apps/(adhoc)/f2a9c7d4/space.ts | 147 + platform/app/apps/(adhoc)/f49c75da/README.md | 27 + .../app/apps/(adhoc)/f49c75da/app.manifest | 9 + .../app/apps/(adhoc)/f49c75da/components.jsx | 277 + platform/app/apps/(adhoc)/f49c75da/config.ts | 15 + platform/app/apps/(adhoc)/f49c75da/const.ts | 3 + platform/app/apps/(adhoc)/f49c75da/layout.jsx | 39 + platform/app/apps/(adhoc)/f49c75da/page.jsx | 35 + platform/app/apps/(adhoc)/f49c75da/server.ts | 138 + .../app/apps/(index)/app.webmanifest/route.ts | 59 + platform/app/apps/(index)/components.jsx | 267 + platform/app/apps/(index)/layout.jsx | 93 + platform/app/apps/(index)/layout.utest.js | 67 + platform/app/apps/(index)/page.jsx | 39 + platform/app/apps/(public)/404/page.jsx | 32 + platform/app/apps/(public)/layout.jsx | 29 + platform/app/apps/(public)/overview/page.jsx | 5 + platform/app/apps/(public)/signin/page.jsx | 38 + .../app/apps/(public)/signin/verify/page.jsx | 39 + platform/app/apps/[...path]/route.ts | 30 + platform/app/apps/_components/Toolbar.jsx | 137 + platform/app/apps/app.manifest.d.ts | 81 + platform/app/apps/auditlog/README.md | 36 + platform/app/apps/auditlog/app.manifest | 9 + platform/app/apps/auditlog/components.jsx | 541 + platform/app/apps/auditlog/config.ts | 26 + platform/app/apps/auditlog/const.ts | 3 + platform/app/apps/auditlog/layout.jsx | 90 + platform/app/apps/auditlog/layout.utest.jsx | 49 + platform/app/apps/auditlog/page.tsx | 39 + platform/app/apps/auditlog/server.ts | 141 + platform/app/apps/chat/README.md | 35 + platform/app/apps/chat/[[...args]]/page.jsx | 34 + .../InputMentionsAnimation.stories.jsx | 27 + .../animations/InputMentionsAnimation.tsx | 478 + .../TextSelectionAnimation.stories.jsx | 27 + .../animations/TextSelectionAnimation.tsx | 380 + platform/app/apps/chat/app.manifest | 40 + .../app/apps/chat/components/ChatArea.jsx | 791 + .../apps/chat/components/ChatArea.stories.jsx | 385 + .../components/ChatExtraFeaturesContext.jsx | 46 + .../app/apps/chat/components/ChatMessages.jsx | 1994 + .../apps/chat/components/ChatReasoning.jsx | 143 + .../chat/components/ChatReasoning.stories.jsx | 92 + .../components/ChatTextSelectionTools.jsx | 107 + .../chat/components/ConversationContext.jsx | 21 + platform/app/apps/chat/components/Form.jsx | 231 + platform/app/apps/chat/components/Main.jsx | 2257 + .../app/apps/chat/components/Selector.jsx | 1016 + platform/app/apps/chat/components/Tips.jsx | 39 + platform/app/apps/chat/config.ts | 188 + platform/app/apps/chat/const.ts | 3 + platform/app/apps/chat/hooks/useDebugMode.tsx | 7 + platform/app/apps/chat/layout.jsx | 106 + platform/app/apps/chat/lib.ts | 85 + platform/app/apps/chat/server.tsx | 2933 ++ platform/app/apps/code/README.md | 31 + platform/app/apps/code/app.manifest | 9 + platform/app/apps/code/components.jsx | 260 + platform/app/apps/code/config.ts | 15 + platform/app/apps/code/const.ts | 15 + platform/app/apps/code/layout.jsx | 39 + platform/app/apps/code/page.tsx | 35 + platform/app/apps/code/server.ts | 145 + platform/app/apps/connect/README.md | 30 + platform/app/apps/connect/app.manifest | 9 + platform/app/apps/connect/components.jsx | 190 + platform/app/apps/connect/config.ts | 24 + platform/app/apps/connect/const.ts | 3 + platform/app/apps/connect/layout.jsx | 61 + platform/app/apps/connect/page.tsx | 35 + platform/app/apps/connect/server.ts | 149 + platform/app/apps/const.ts | 4 + platform/app/apps/error.tsx | 110 + platform/app/apps/eventlog/README.md | 84 + platform/app/apps/eventlog/app.manifest | 9 + platform/app/apps/eventlog/components.jsx | 606 + .../app/apps/eventlog/components.utest.jsx | 150 + platform/app/apps/eventlog/config.ts | 24 + platform/app/apps/eventlog/const.ts | 3 + platform/app/apps/eventlog/layout.jsx | 91 + platform/app/apps/eventlog/layout.utest.jsx | 49 + platform/app/apps/eventlog/page.tsx | 39 + platform/app/apps/eventlog/server.ts | 152 + platform/app/apps/eventlog/server.utest.js | 151 + platform/app/apps/inbox/README.md | 33 + .../inbox/[conversationId]/components.jsx | 248 + .../app/apps/inbox/[conversationId]/lib.ts | 32 + .../apps/inbox/[conversationId]/loading.jsx | 9 + .../app/apps/inbox/[conversationId]/page.jsx | 57 + .../app/apps/inbox/[conversationId]/server.ts | 153 + platform/app/apps/inbox/app.manifest | 10 + platform/app/apps/inbox/components.jsx | 178 + platform/app/apps/inbox/config.ts | 46 + platform/app/apps/inbox/const.ts | 3 + platform/app/apps/inbox/layout.jsx | 119 + platform/app/apps/inbox/page.tsx | 3 + platform/app/apps/inbox/server.ts | 61 + platform/app/apps/layout.jsx | 104 + platform/app/apps/layout.utest.js | 25 + platform/app/apps/static/README.md | 27 + platform/app/apps/static/[[...path]]/route.ts | 17 + platform/app/apps/static/app.manifest | 10 + platform/app/apps/static/config.ts | 37 + platform/app/apps/static/const.ts | 4 + platform/app/apps/static/lib.ts | 648 + platform/app/apps/static/lib.utest.ts | 303 + platform/app/apps/task/README.md | 30 + platform/app/apps/task/app.manifest | 9 + platform/app/apps/task/bot.policy.ts | 26 + platform/app/apps/task/bot.policy.utest.js | 39 + platform/app/apps/task/components.jsx | 706 + platform/app/apps/task/config.ts | 35 + platform/app/apps/task/const.ts | 3 + platform/app/apps/task/layout.jsx | 61 + platform/app/apps/task/page.tsx | 35 + platform/app/apps/task/server.ts | 386 + platform/app/apps/trace/README.md | 30 + platform/app/apps/trace/app.manifest | 9 + platform/app/apps/trace/components.jsx | 334 + platform/app/apps/trace/config.ts | 24 + platform/app/apps/trace/const.ts | 3 + platform/app/apps/trace/layout.jsx | 66 + platform/app/apps/trace/layout.utest.jsx | 49 + platform/app/apps/trace/page.tsx | 13 + platform/app/apps/usage/README.md | 42 + platform/app/apps/usage/app.manifest | 9 + platform/app/apps/usage/components.jsx | 189 + platform/app/apps/usage/config.ts | 24 + platform/app/apps/usage/const.ts | 3 + platform/app/apps/usage/layout.jsx | 61 + platform/app/apps/usage/page.tsx | 29 + platform/app/apps/usage/server.ts | 191 + platform/app/apps/usage/server.utest.js | 144 + platform/app/apps/usagelog/README.md | 37 + platform/app/apps/usagelog/app.manifest | 9 + platform/app/apps/usagelog/components.jsx | 508 + platform/app/apps/usagelog/config.ts | 26 + platform/app/apps/usagelog/const.ts | 3 + platform/app/apps/usagelog/layout.jsx | 88 + platform/app/apps/usagelog/layout.utest.jsx | 49 + platform/app/apps/usagelog/page.tsx | 39 + platform/app/apps/usagelog/server.ts | 117 + platform/app/global-error.tsx | 82 + platform/app/layout.tsx | 83 + platform/codegen.ts | 24 + platform/components/AbilityCreateDialog.jsx | 415 + .../components/AbilityTemplateBrowser.jsx | 315 + platform/components/AdvancedAutoTextarea.jsx | 61 + .../components/AdvancedAutoTextarea.utest.js | 179 + platform/components/AgentsScreen.jsx | 193 + platform/components/AttachmentsArea.jsx | 87 + platform/components/AttachmentsArea.utest.jsx | 197 + platform/components/AudioPlayer.jsx | 135 + platform/components/AudioPlayer.utest.js | 179 + platform/components/AuditLog.jsx | 115 + platform/components/AuditLog.utest.js | 66 + platform/components/Auth.jsx | 497 + platform/components/Auth.utest.js | 138 + platform/components/AutoScrollArea.jsx | 264 + platform/components/AutoScrollArea.utest.js | 272 + platform/components/AutoTextarea.jsx | 25 + platform/components/AutoTextarea.utest.js | 265 + platform/components/AutoTextarea.utest.jsx | 225 + platform/components/BackButton.jsx | 31 + platform/components/BackButton.stories.tsx | 55 + platform/components/BackButton.utest.js | 233 + platform/components/BackButton.utest.jsx | 137 + platform/components/BackLink.jsx | 30 + platform/components/BackLink.utest.js | 280 + platform/components/BackLink.utest.jsx | 185 + platform/components/BackstoryInput.jsx | 365 + .../components/BackstoryInput.stories.jsx | 372 + platform/components/BillingButtons.jsx | 96 + platform/components/BillingButtons.utest.jsx | 430 + platform/components/BlueprintCodeBlock.jsx | 43 + .../components/BlueprintCodeBlock.utest.jsx | 58 + platform/components/BlueprintList.jsx | 95 + platform/components/BlueprintList.utest.js | 143 + platform/components/BlueprintList.utest.jsx | 225 + platform/components/BotBlockStatus.jsx | 80 + platform/components/BotBlockStatus.utest.jsx | 109 + platform/components/BotBlockTag.jsx | 41 + platform/components/BotInsights.jsx | 522 + platform/components/BotList.jsx | 112 + platform/components/BotList.utest.jsx | 236 + platform/components/BotSelect.jsx | 464 + platform/components/Box.jsx | 10 + platform/components/Box.utest.js | 67 + platform/components/Box.utest.jsx | 36 + platform/components/CalendarInput.jsx | 233 + platform/components/CalendarInput.utest.jsx | 87 + platform/components/Carousel.jsx | 150 + platform/components/Carousel.utest.jsx | 364 + platform/components/CategoryGrid.jsx | 65 + platform/components/CategoryGrid.utest.js | 388 + platform/components/ChatInput.jsx | 59 + platform/components/ChatInput.utest.js | 342 + platform/components/Children.tsx | 19 + platform/components/Children.utest.js | 158 + platform/components/Children.utest.jsx | 183 + platform/components/ChunkErrorListener.tsx | 75 + .../components/ChunkErrorListener.utest.js | 123 + platform/components/CodeAction.tsx | 209 + platform/components/CodeAction.utest.js | 304 + platform/components/CodeBlock.jsx | 184 + platform/components/CodeBlock.stories.jsx | 112 + platform/components/CodeBlock.utest.jsx | 573 + platform/components/Collapsible.jsx | 56 + platform/components/Collapsible.stories.jsx | 459 + platform/components/Collapsible.utest.js | 222 + platform/components/ColorInput.jsx | 102 + platform/components/ColorInput.stories.jsx | 377 + platform/components/ColorInput.utest.jsx | 306 + platform/components/CommaListSelect.jsx | 262 + .../components/CommaListSelect.stories.jsx | 452 + platform/components/CommaListSelect.utest.js | 344 + platform/components/Component.jsx | 12 + platform/components/Component.utest.js | 399 + platform/components/Confirm.jsx | 366 + platform/components/Confirm.stories.jsx | 543 + platform/components/Confirm.utest.jsx | 177 + platform/components/ConfirmButton.jsx | 107 + platform/components/ConfirmButton.utest.jsx | 442 + platform/components/Consent.jsx | 223 + platform/components/Consent.utest.js | 103 + platform/components/Console.jsx | 141 + platform/components/Console.utest.js | 180 + platform/components/ContactList.jsx | 103 + platform/components/ContactList.utest.js | 220 + platform/components/ContactSelect.jsx | 59 + platform/components/ContactSelect.utest.jsx | 297 + .../ContextInput.ReactFlow.stories.jsx | 354 + platform/components/ContextInput.jsx | 1651 + .../ContextInput.setValueIdentity.utest.jsx | 119 + .../ContextInput.staleContextLag.utest.jsx | 114 + platform/components/ContextInput.stories.jsx | 489 + platform/components/ContextInput.utest.js | 1323 + platform/components/Conversation.jsx | 1405 + platform/components/Conversation.utest.js | 183 + .../components/ConversationAttachmentList.jsx | 72 + .../ConversationAttachmentList.utest.js | 95 + platform/components/ConversationInsights.jsx | 95 + .../components/ConversationInsights.utest.jsx | 82 + platform/components/ConversationList.jsx | 239 + platform/components/ConversationList.utest.js | 552 + platform/components/ConversationManager.jsx | 119 + .../components/ConversationManager.utest.jsx | 121 + platform/components/ConversationMonitor.jsx | 160 + platform/components/ConversationsButton.jsx | 61 + platform/components/CopyButton.tsx | 53 + platform/components/CopyButton.utest.js | 244 + platform/components/DailyChart.tsx | 137 + platform/components/DailyChart.utest.js | 319 + platform/components/DarkModeSwitch.jsx | 96 + platform/components/DarkModeSwitch.utest.jsx | 282 + .../components/DashboardCommandPalette.jsx | 290 + .../DashboardCommandPalette.utest.jsx | 138 + platform/components/DatasetFiles.jsx | 361 + platform/components/DatasetFiles.utest.jsx | 348 + .../components/DatasetImportJobFinishURLs.jsx | 213 + .../DatasetImportJobFinishURLs.utest.jsx | 139 + platform/components/DatasetList.jsx | 112 + platform/components/DatasetList.utest.jsx | 240 + platform/components/DatasetRecordList.jsx | 288 + .../components/DatasetRecordList.utest.jsx | 126 + platform/components/DatasetSelect.jsx | 382 + platform/components/DaysSelect.jsx | 16 + platform/components/DaysSelect.utest.js | 334 + platform/components/DbTextInput.jsx | 74 + platform/components/DbTextInput.utest.js | 227 + platform/components/DescriptionInput.jsx | 128 + platform/components/DescriptionInput.utest.js | 378 + platform/components/Diagram.jsx | 138 + platform/components/Diagram.utest.jsx | 93 + platform/components/DocsLink.jsx | 51 + platform/components/DocsLink.utest.js | 189 + platform/components/DotsLoader.jsx | 18 + platform/components/DotsLoader.utest.jsx | 224 + platform/components/DurationSelect.jsx | 123 + platform/components/DurationSelect.utest.js | 523 + platform/components/DynamicIcon.jsx | 686 + platform/components/DynamicIcon.utest.jsx | 37 + platform/components/DynamicImage.tsx | 9 + platform/components/DynamicImage.utest.js | 22 + platform/components/DynamicLogo.tsx | 9 + platform/components/DynamicLogo.utest.js | 22 + platform/components/Emoji.jsx | 58 + platform/components/Emoji.utest.jsx | 386 + platform/components/EventLog.jsx | 143 + platform/components/EventLog.utest.js | 69 + platform/components/Expando.jsx | 64 + platform/components/Expando.utest.jsx | 344 + platform/components/ExpiresAtInput.jsx | 176 + platform/components/ExpiresAtInput.utest.jsx | 43 + platform/components/ExportLink.jsx | 106 + platform/components/ExportLink.utest.js | 399 + platform/components/ExportLink.utest.jsx | 276 + .../components/ExtractIntegrationItemList.jsx | 81 + .../ExtractIntegrationItemList.utest.jsx | 272 + .../components/ExtractSchemaCheatsheet.jsx | 97 + .../ExtractSchemaCheatsheet.utest.jsx | 248 + platform/components/ExtractSchemaInput.jsx | 514 + platform/components/FAQ.jsx | 148 + platform/components/FAQ.utest.jsx | 93 + platform/components/FAQ3.jsx | 25 + platform/components/FAQ3.utest.jsx | 123 + platform/components/FAQStructuredData.jsx | 22 + .../components/FAQStructuredData.utest.js | 274 + platform/components/FOC.tsx | 101 + platform/components/FOC.utest.js | 277 + platform/components/FOC.utest.jsx | 170 + platform/components/FadeInIframe.jsx | 29 + platform/components/FadeInIframe.utest.jsx | 82 + platform/components/FancyLink.jsx | 77 + platform/components/FancyLink.stories.jsx | 478 + platform/components/FancyLink.utest.js | 243 + platform/components/FancyLink.utest.jsx | 213 + platform/components/FileDownloadButton.jsx | 45 + .../components/FileDownloadButton.utest.jsx | 84 + platform/components/FileDrop.jsx | 44 + platform/components/FileDrop.utest.jsx | 196 + platform/components/FileEditButton.jsx | 161 + platform/components/FileEditButton.utest.js | 284 + platform/components/FileIcon.jsx | 67 + platform/components/FileIcon.utest.js | 81 + platform/components/FileIcon.utest.jsx | 220 + platform/components/FileList.jsx | 161 + platform/components/FileList.utest.jsx | 416 + platform/components/FileManager.jsx | 71 + platform/components/FileManager.utest.jsx | 387 + platform/components/FileSelect.jsx | 59 + platform/components/FileSelect.utest.jsx | 242 + platform/components/FileUploadButton.jsx | 80 + .../components/FileUploadButton.utest.jsx | 197 + platform/components/FloatingBox.jsx | 135 + platform/components/FloatingBox.stories.jsx | 512 + platform/components/FloatingBox.utest.js | 112 + platform/components/ForwardButton.tsx | 30 + platform/components/ForwardButton.utest.jsx | 203 + platform/components/ForwardLink.jsx | 14 + platform/components/ForwardLink.utest.js | 120 + platform/components/GTag.jsx | 69 + platform/components/GTag.utest.jsx | 384 + platform/components/GeneralBasicOptions.jsx | 54 + .../components/GeneralBasicOptions.utest.jsx | 169 + platform/components/GlobalRoot.jsx | 23 + platform/components/GlobalRoot.utest.js | 150 + platform/components/GlobalRoot.utest.jsx | 95 + platform/components/GravatarIcon.jsx | 31 + platform/components/GravatarIcon.utest.js | 257 + platform/components/Headline.jsx | 17 + platform/components/Headline.utest.jsx | 260 + platform/components/Hero.jsx | 187 + platform/components/Hero.utest.js | 74 + platform/components/HubOptions.jsx | 203 + platform/components/HubOptions.utest.jsx | 153 + platform/components/IPhoneFrame.jsx | 17 + platform/components/IPhoneFrame.utest.js | 312 + platform/components/IconSelect.jsx | 104 + platform/components/IconSelect.utest.js | 277 + platform/components/Image.tsx | 35 + platform/components/Image.utest.js | 256 + platform/components/ImageBlock.jsx | 85 + platform/components/ImageBlock.utest.js | 163 + platform/components/ImageModelSelect.jsx | 467 + .../components/ImageModelSelect.stories.jsx | 166 + platform/components/ImageModelSelect.utest.js | 252 + platform/components/Initials.jsx | 18 + platform/components/Initials.utest.js | 101 + platform/components/Initials.utest.jsx | 33 + platform/components/InputArea.jsx | 17 + platform/components/InputArea.utest.js | 56 + platform/components/InstructionCheatsheet.jsx | 175 + .../InstructionCheatsheet.utest.jsx | 40 + platform/components/InstructionInput.jsx | 810 + .../components/InstructionInput.stories.jsx | 306 + .../components/IntegrationInstallButton.jsx | 183 + .../IntegrationInstallButton.utest.jsx | 262 + platform/components/IntegrationList.jsx | 420 + platform/components/IntegrationList.utest.jsx | 616 + platform/components/ItemsDisplay.jsx | 108 + platform/components/ItemsDisplay.utest.jsx | 306 + platform/components/KeyCombo.tsx | 22 + platform/components/KeyCombo.utest.js | 290 + platform/components/LanguageModelSelect.jsx | 1035 + .../LanguageModelSelect.stories.jsx | 380 + .../components/LanguageModelSelect.utest.js | 741 + platform/components/LimitsCheatsheet.jsx | 48 + .../components/LimitsCheatsheet.utest.jsx | 238 + platform/components/LimitsConfigInput.jsx | 392 + .../components/LimitsConfigInput.utest.js | 313 + platform/components/Link.jsx | 297 + platform/components/Link.utest.jsx | 116 + platform/components/List.jsx | 373 + platform/components/List.stories.jsx | 370 + platform/components/List.utest.jsx | 86 + platform/components/LoadMoreButton.jsx | 145 + platform/components/LoadMoreButton.utest.js | 129 + platform/components/LoadMoreButton.utest.jsx | 533 + platform/components/LoadingIframe.jsx | 123 + platform/components/LoadingIframe.utest.jsx | 70 + platform/components/MainNavbar.jsx | 384 + platform/components/ManualLink.jsx | 42 + platform/components/ManualLink.utest.js | 54 + platform/components/MarkdownCheatsheet.jsx | 116 + .../components/MarkdownCheatsheet.utest.jsx | 190 + platform/components/MarkdownInput.jsx | 148 + platform/components/MarkdownInput.utest.jsx | 109 + platform/components/MemoryList.jsx | 182 + platform/components/MemoryList.utest.jsx | 86 + platform/components/MenuButton.jsx | 40 + platform/components/MenuButton.stories.jsx | 582 + platform/components/MenuButton.utest.js | 325 + platform/components/Meta.jsx | 265 + platform/components/Meta.utest.js | 221 + platform/components/MetaArea.jsx | 30 + platform/components/MetaArea.utest.js | 271 + platform/components/MetaInput.jsx | 185 + platform/components/MetaInput.utest.jsx | 254 + platform/components/MovingScreen.jsx | 254 + platform/components/MovingScreen.utest.jsx | 117 + platform/components/MultiLanguageSelect.jsx | 10 + .../components/MultiLanguageSelect.utest.js | 292 + platform/components/NameInput.jsx | 43 + platform/components/NameInput.utest.js | 326 + platform/components/NameInput.utest.jsx | 341 + platform/components/NavHeader.jsx | 36 + platform/components/NavHeader.utest.js | 255 + platform/components/NavbarAccount.jsx | 66 + platform/components/Nbsp.tsx | 5 + platform/components/Nbsp.utest.js | 30 + platform/components/NestedAccordion.jsx | 606 + platform/components/NestedAccordion.utest.jsx | 355 + platform/components/NoRubberBand.jsx | 10 + platform/components/NoRubberBand.utest.jsx | 21 + platform/components/NoSsr.jsx | 29 + platform/components/NoSsr.utest.js | 208 + platform/components/NoSsr.utest.jsx | 29 + platform/components/Notifications.jsx | 61 + platform/components/Notifications.utest.jsx | 111 + platform/components/NotificationsButton.jsx | 191 + .../components/NotificationsButton.utest.js | 266 + platform/components/ObjectInput.jsx | 461 + platform/components/ObjectInput.stories.jsx | 740 + platform/components/ObjectInput.utest.js | 303 + platform/components/ObjectView.jsx | 50 + platform/components/ObjectView.utest.js | 249 + platform/components/PagePlaceholder.jsx | 119 + platform/components/PagePlaceholder.utest.jsx | 70 + platform/components/PageSections.jsx | 267 + platform/components/PageSections.utest.jsx | 304 + platform/components/Pagedown.jsx | 242 + platform/components/Pagedown.utest.jsx | 178 + platform/components/PartnerBanner.jsx | 17 + platform/components/PartnerBanner.utest.jsx | 45 + platform/components/PinInput.jsx | 70 + platform/components/PinInput.stories.jsx | 462 + platform/components/PinInput.utest.js | 310 + platform/components/Ping.tsx | 21 + platform/components/Ping.utest.js | 119 + .../components/PlatformExperienceOnly.jsx | 18 + platform/components/PolicyBlockStatus.jsx | 94 + .../components/PolicyBlockStatus.utest.jsx | 130 + platform/components/PolicyConfigInput.jsx | 446 + .../components/PolicyConfigInput.utest.js | 318 + platform/components/PolicyList.jsx | 124 + platform/components/PolicyList.utest.jsx | 295 + platform/components/PopButton.jsx | 196 + platform/components/PopButton.stories.jsx | 501 + platform/components/PopButton.utest.jsx | 96 + platform/components/Portal.jsx | 17 + platform/components/Portal.utest.js | 381 + platform/components/PortalConfigInput.jsx | 1263 + .../components/PortalConfigInput.utest.js | 502 + platform/components/PortalList.jsx | 130 + platform/components/PortalList.utest.jsx | 267 + platform/components/PortalTarget.jsx | 49 + platform/components/PortalTarget.utest.js | 202 + platform/components/ProfileBar.jsx | 53 + platform/components/ProfileBar.utest.jsx | 356 + platform/components/ProfileDropdown.jsx | 343 + platform/components/ProfileDropdown.utest.jsx | 87 + platform/components/Progress.jsx | 61 + platform/components/Progress.utest.jsx | 249 + platform/components/ProgressBar.jsx | 36 + platform/components/ProgressBar.utest.jsx | 78 + platform/components/ProjectScopeSelector.jsx | 157 + .../components/ProjectScopeSelector.utest.jsx | 107 + platform/components/Pullout.jsx | 395 + platform/components/Pullout.utest.jsx | 104 + platform/components/RatingList.jsx | 222 + platform/components/RatingList.utest.jsx | 167 + platform/components/ReadyFrame.jsx | 46 + platform/components/ReadyFrame.utest.jsx | 288 + platform/components/ReadyImage.jsx | 96 + platform/components/ReadyImage.utest.jsx | 344 + platform/components/RecordInput.jsx | 93 + platform/components/RecordInput.utest.js | 308 + platform/components/RefreshTimer.jsx | 134 + platform/components/RefreshTimer.stories.jsx | 263 + platform/components/RefreshTimer.utest.js | 267 + .../components/ReloadingPageErrorBoundary.jsx | 72 + .../ReloadingPageErrorBoundary.utest.js | 158 + platform/components/RerankerModelSelect.jsx | 484 + .../components/RerankerModelSelect.utest.js | 252 + platform/components/ResourceFilterButton.jsx | 228 + .../components/ResourceFilterButton.utest.jsx | 199 + platform/components/ResourceList.jsx | 914 + platform/components/ResourceList.utest.js | 697 + platform/components/ResponsiveIframe.jsx | 35 + platform/components/ResponsiveIframe.utest.js | 225 + .../components/ResponsiveIframe.utest.jsx | 318 + platform/components/RevealTextarea.jsx | 51 + platform/components/RevealTextarea.utest.jsx | 69 + platform/components/RevealToken.jsx | 57 + platform/components/RevealToken.utest.js | 209 + platform/components/RewardsButton.jsx | 95 + platform/components/RewardsButton.utest.js | 112 + platform/components/Safedown.jsx | 142 + platform/components/Safedown.stories.jsx | 512 + platform/components/Safedown.utest.jsx | 182 + platform/components/SaveButton.jsx | 37 + platform/components/SaveButton.utest.js | 218 + platform/components/SaveButton.utest.jsx | 237 + platform/components/ScheduleSelect.jsx | 226 + platform/components/ScheduleSelect.utest.js | 468 + platform/components/SchemaPanel.jsx | 288 + platform/components/SchemaPanel.utest.jsx | 487 + platform/components/SecretConfigInput.jsx | 61 + .../components/SecretConfigInput.utest.jsx | 131 + .../components/SecretConnectionStatus.jsx | 34 + platform/components/SecretList.jsx | 161 + platform/components/SecretList.utest.js | 255 + platform/components/SecretSelect.jsx | 362 + platform/components/SendInstructions.jsx | 20 + platform/components/SendInstructions.utest.js | 71 + platform/components/Session.jsx | 11 + platform/components/Session.utest.jsx | 41 + platform/components/SessionContext.jsx | 524 + platform/components/SessionContext.utest.jsx | 149 + platform/components/SideWidget.jsx | 375 + .../components/SilencingErrorBoundary.jsx | 36 + .../SilencingErrorBoundary.utest.js | 218 + platform/components/SimpleTabs.jsx | 90 + platform/components/SimpleTabs.utest.js | 326 + platform/components/Skeleton.jsx | 14 + platform/components/Skeleton.utest.js | 68 + platform/components/SkillBrowser.jsx | 333 + platform/components/SkillBrowser.utest.js | 234 + platform/components/SkillsetAbilityList.jsx | 269 + .../components/SkillsetAbilityList.utest.jsx | 459 + platform/components/SkillsetAbilityTester.jsx | 254 + .../components/SkillsetAbilityTester.utest.js | 263 + .../components/SkillsetConnectionList.jsx | 184 + platform/components/SkillsetList.jsx | 113 + platform/components/SkillsetList.utest.jsx | 240 + platform/components/SkillsetSelect.jsx | 376 + platform/components/SpaceList.jsx | 158 + platform/components/SpaceList.utest.jsx | 175 + platform/components/SpaceSavingDiv.jsx | 67 + platform/components/SpaceSavingDiv.utest.js | 328 + platform/components/SpaceSelect.jsx | 69 + platform/components/SpaceSelect.utest.jsx | 274 + platform/components/SpaceSiteList.jsx | 312 + platform/components/SpaceStorageList.jsx | 1067 + platform/components/SpaceStorageList.utest.js | 699 + platform/components/Spinner.jsx | 23 + platform/components/Spinner.utest.js | 172 + platform/components/Spinner.utest.jsx | 144 + platform/components/StructuredData.jsx | 43 + platform/components/StructuredData.utest.js | 361 + platform/components/SuperTools.jsx | 385 + platform/components/SuperTools.utest.jsx | 343 + platform/components/SwitchBar.jsx | 62 + platform/components/SwitchBar.utest.jsx | 304 + platform/components/TaskExecutionList.jsx | 136 + .../components/TaskExecutionList.utest.jsx | 155 + platform/components/TaskList.jsx | 177 + platform/components/TaskList.utest.js | 327 + platform/components/TeamList.jsx | 99 + platform/components/TeamList.utest.js | 114 + platform/components/TextSelectionTools.jsx | 148 + .../components/TextSelectionTools.stories.jsx | 399 + .../components/TextSelectionTools.utest.jsx | 140 + platform/components/TextareaHighlighter.jsx | 236 + .../TextareaHighlighter.stories.jsx | 391 + .../components/TextareaHighlighter.utest.js | 173 + .../components/TextareaQuickEditTools.jsx | 372 + .../TextareaQuickEditTools.utest.js | 302 + platform/components/Theme.jsx | 35 + platform/components/Theme.utest.js | 319 + platform/components/ThemeBuilder.jsx | 82 + platform/components/ThemeBuilder.utest.jsx | 93 + platform/components/ThemeCanvas.jsx | 215 + platform/components/ThemeCanvas.utest.jsx | 95 + platform/components/ThemeColor.jsx | 29 + platform/components/ThemeColor.utest.js | 206 + platform/components/ThemeDesigner.jsx | 213 + platform/components/ThemeDesigner.utest.jsx | 81 + platform/components/ThemePanel.jsx | 1191 + platform/components/ThemePanel.utest.jsx | 291 + platform/components/ThisSolution.jsx | 819 + platform/components/ThisSolution.utest.jsx | 90 + platform/components/TimeAgo.jsx | 30 + platform/components/TimeAgo.utest.js | 288 + platform/components/TimeAgo.utest.jsx | 167 + platform/components/TimezoneSelect.jsx | 299 + platform/components/TimezoneSelect.utest.jsx | 148 + platform/components/Toggle.jsx | 93 + platform/components/Toggle.stories.jsx | 210 + platform/components/Toggle.utest.jsx | 89 + platform/components/TokenAutoTextarea.jsx | 103 + .../components/TokenAutoTextarea.stories.jsx | 518 + .../components/TokenAutoTextarea.utest.js | 107 + platform/components/TokenConfigInput.jsx | 386 + platform/components/TokenConfigInput.utest.js | 292 + platform/components/TokenList.jsx | 30 + platform/components/TokenList.utest.jsx | 175 + platform/components/TooltipButton.jsx | 181 + platform/components/TooltipButton.stories.jsx | 355 + platform/components/TooltipButton.utest.jsx | 450 + platform/components/TriggerSelect.jsx | 51 + platform/components/TriggerSelect.utest.jsx | 219 + platform/components/UpgradePlans.jsx | 394 + platform/components/UpgradePlans.utest.jsx | 164 + platform/components/UsageList.jsx | 126 + platform/components/UsageList.utest.js | 112 + platform/components/UsageView.jsx | 279 + platform/components/UsageView.utest.js | 224 + platform/components/UsageView.utest.jsx | 60 + platform/components/UserList.jsx | 153 + platform/components/UserList.utest.jsx | 99 + platform/components/VideoModelSelect.jsx | 462 + platform/components/VideoModelSelect.utest.js | 252 + platform/components/WebhookList.jsx | 30 + platform/components/WebhookList.utest.jsx | 169 + platform/components/WebhookSetupSection.jsx | 185 + .../WebhookSetupSection.stories.jsx | 172 + .../components/WebhookSetupSection.utest.js | 231 + platform/components/Widget.jsx | 677 + platform/components/WidgetPluginsSelect.jsx | 10 + .../components/WidgetPluginsSelect.utest.jsx | 33 + platform/components/WidgetPreview.jsx | 62 + platform/components/WidgetPreview.utest.jsx | 255 + platform/components/WidgetScript.jsx | 13 + platform/components/WidgetScript.utest.js | 23 + platform/components/WidgetsScreen.jsx | 46 + platform/components/WidgetsScreen.utest.jsx | 55 + platform/components/Wrapper.jsx | 43 + platform/components/Wrapper.utest.jsx | 221 + platform/components/YoutubePlayer.jsx | 192 + platform/components/YoutubePlayer.utest.jsx | 266 + platform/components/ZoomableArea.jsx | 111 + platform/components/ZoomableArea.utest.jsx | 74 + platform/config/abilities.ts | 13 + platform/config/admins.ts | 35 + platform/config/apexes.js | 39 + platform/config/apps.ts | 258 + platform/config/bots.ts | 22 + platform/config/conversations.ts | 3 + platform/config/cookie.ts | 11 + platform/config/datasets.ts | 40 + platform/config/debug.ts | 225 + platform/config/execution.ts | 89 + platform/config/experiments.ts | 22 + platform/config/headers.js | 28 + platform/config/hosts.js | 119 + platform/config/hub.ts | 13 + platform/config/limits.ts | 581 + platform/config/messages.ts | 14 + platform/config/models.ts | 8118 ++++ platform/config/navigation.ts | 7 + platform/config/oauth.ts | 6 + platform/config/origins.js | 58 + platform/config/platform.ts | 71 + platform/config/queue.ts | 20 + platform/config/records.ts | 17 + platform/config/server.ts | 6 + platform/config/site.js | 76 + platform/config/skillsets.ts | 23 + platform/config/structures.ts | 9 + platform/config/widget.ts | 26 + platform/content/faqs/platform-actions.yaml | 10 + platform/content/faqs/platform-alerts.yaml | 31 + platform/content/faqs/platform-analytics.yaml | 1 + .../content/faqs/platform-api-v1-spec.yaml | 6 + platform/content/faqs/platform-audit.yaml | 8 + .../faqs/platform-blueprint-instance.yaml | 1 + .../content/faqs/platform-blueprints.yaml | 14 + .../content/faqs/platform-bot-instance.yaml | 8 + platform/content/faqs/platform-bots.yaml | 12 + .../faqs/platform-contact-instance.yaml | 1 + platform/content/faqs/platform-contacts.yaml | 1 + .../faqs/platform-conversation-instance.yaml | 6 + .../content/faqs/platform-conversations.yaml | 10 + .../faqs/platform-dataset-instance.yaml | 10 + .../platform-dataset-record-instance.yaml | 16 + platform/content/faqs/platform-datasets.yaml | 10 + platform/content/faqs/platform-events.yaml | 6 + .../content/faqs/platform-file-instance.yaml | 1 + platform/content/faqs/platform-files.yaml | 1 + .../faqs/platform-integrations-discord.yaml | 14 + .../faqs/platform-integrations-email.yaml | 1 + .../faqs/platform-integrations-extract.yaml | 1 + .../platform-integrations-googlechat.yaml | 47 + .../faqs/platform-integrations-instagram.yaml | 1 + .../faqs/platform-integrations-mcpserver.yaml | 1 + .../faqs/platform-integrations-messenger.yaml | 1 + .../platform-integrations-microsoftteams.yaml | 32 + .../faqs/platform-integrations-notion.yaml | 12 + .../faqs/platform-integrations-sitemap.yaml | 17 + .../platform-integrations-skillserver.yaml | 1 + .../faqs/platform-integrations-slack.yaml | 14 + .../faqs/platform-integrations-support.yaml | 20 + .../faqs/platform-integrations-telegram.yaml | 1 + .../faqs/platform-integrations-trigger.yaml | 1 + .../faqs/platform-integrations-twilio.yaml | 1 + .../faqs/platform-integrations-whatsapp.yaml | 4 + .../faqs/platform-integrations-widget.yaml | 18 + .../content/faqs/platform-integrations.yaml | 6 + platform/content/faqs/platform-memories.yaml | 1 + .../faqs/platform-memory-instance.yaml | 1 + platform/content/faqs/platform-models.yaml | 1 + platform/content/faqs/platform-overview.yaml | 1 + platform/content/faqs/platform-policies.yaml | 6 + .../faqs/platform-policy-instance.yaml | 1 + .../faqs/platform-portal-instance.yaml | 1 + platform/content/faqs/platform-portals.yaml | 1 + platform/content/faqs/platform-ratings.yaml | 1 + .../faqs/platform-secret-instance.yaml | 1 + platform/content/faqs/platform-secrets.yaml | 1 + .../platform-skillset-ability-instance.yaml | 12 + .../faqs/platform-skillset-instance.yaml | 13 + platform/content/faqs/platform-skillsets.yaml | 10 + .../content/faqs/platform-space-instance.yaml | 1 + platform/content/faqs/platform-spaces.yaml | 1 + .../content/faqs/platform-task-instance.yaml | 1 + platform/content/faqs/platform-tasks.yaml | 1 + .../content/faqs/platform-team-instance.yaml | 20 + platform/content/faqs/platform-teams.yaml | 14 + .../content/faqs/platform-token-instance.yaml | 1 + platform/content/faqs/platform-tokens.yaml | 16 + platform/content/faqs/platform-usage.yaml | 17 + .../content/faqs/platform-user-instance.yaml | 1 + platform/content/faqs/platform-users.yaml | 1 + platform/content/faqs/platform-webhooks.yaml | 1 + platform/content/faqs/types.d.ts | 10 + platform/content/faqs/website-examples.yaml | 17 + .../faqs/website-playground-ability.yaml | 1 + .../content/faqs/website-playground-api.yaml | 1 + .../faqs/website-playground-backstory.yaml | 8 + .../faqs/website-playground-conversation.yaml | 6 + .../faqs/website-playground-entity.yaml | 6 + .../faqs/website-playground-graphql.yaml | 1 + .../content/faqs/website-playground-html.yaml | 1 + .../faqs/website-playground-image.yaml | 8 + .../faqs/website-playground-jmespath.yaml | 1 + .../faqs/website-playground-jsonpath.yaml | 1 + .../faqs/website-playground-message.yaml | 6 + .../content/faqs/website-playground-pdf.yaml | 1 + .../faqs/website-playground-record.yaml | 1 + .../faqs/website-playground-widget.yaml | 15 + platform/content/faqs/website-playground.yaml | 6 + platform/content/other/connections.d.ts | 14 + platform/content/other/connections.utest.js | 45 + platform/content/other/connections.yaml | 2026 + platform/data/abilities/all.ts | 436 + platform/data/abilities/all.utest.ts | 401 + platform/data/abilities/auth.utest.js | 30 + platform/data/abilities/catalogue.d.ts | 25 + platform/data/abilities/catalogue/README.md | 72 + platform/data/abilities/catalogue/ably.ts | 189 + .../data/abilities/catalogue/ably.utest.js | 111 + .../abilities/catalogue/abstractapi.api.ts | 35 + .../catalogue/abstractapi.email.openapi.yaml | 99 + .../abilities/catalogue/abstractapi.email.ts | 34 + .../catalogue/abstractapi.email.utest.js | 102 + .../abstractapi.holidays.openapi.yaml | 76 + .../catalogue/abstractapi.holidays.ts | 44 + .../catalogue/abstractapi.holidays.utest.js | 102 + .../catalogue/abstractapi.ip.openapi.yaml | 113 + .../abilities/catalogue/abstractapi.ip.ts | 27 + .../catalogue/abstractapi.ip.utest.js | 102 + .../catalogue/abstractapi.phone.openapi.yaml | 63 + .../abilities/catalogue/abstractapi.phone.ts | 27 + .../catalogue/abstractapi.phone.utest.js | 102 + .../catalogue/abstractapi.vat.openapi.yaml | 55 + .../abilities/catalogue/abstractapi.vat.ts | 27 + .../catalogue/abstractapi.vat.utest.js | 102 + .../catalogue/accuweather.openapi.yaml | 328 + .../data/abilities/catalogue/accuweather.ts | 223 + .../abilities/catalogue/accuweather.utest.js | 127 + .../catalogue/activecampaign.openapi.yaml | 758 + .../abilities/catalogue/activecampaign.ts | 913 + .../catalogue/activecampaign.utest.js | 170 + .../abilities/catalogue/ahrefs.openapi.yaml | 150 + platform/data/abilities/catalogue/ahrefs.ts | 168 + .../data/abilities/catalogue/ahrefs.utest.js | 114 + .../abilities/catalogue/airtable.openapi.yaml | 550 + platform/data/abilities/catalogue/airtable.ts | 846 + .../abilities/catalogue/airtable.utest.js | 169 + platform/data/abilities/catalogue/alpaca.ts | 85 + .../catalogue/alphavantage.openapi.yaml | 81 + .../data/abilities/catalogue/alphavantage.ts | 56 + .../abilities/catalogue/alphavantage.utest.js | 113 + .../catalogue/amplitude.openapi.yaml | 139 + .../data/abilities/catalogue/amplitude.ts | 293 + .../abilities/catalogue/amplitude.utest.js | 121 + .../data/abilities/catalogue/anthropic.yaml | 2 + platform/data/abilities/catalogue/apollo.ts | 365 + platform/data/abilities/catalogue/asana.ts | 494 + .../data/abilities/catalogue/asana.utest.js | 109 + .../data/abilities/catalogue/atlassian.ts | 744 + platform/data/abilities/catalogue/attio.ts | 876 + .../data/abilities/catalogue/attio.utest.js | 208 + .../abilities/catalogue/bamboohr.openapi.yaml | 417 + platform/data/abilities/catalogue/bamboohr.ts | 428 + .../abilities/catalogue/bamboohr.utest.js | 148 + .../catalogue/barcodelookup.openapi.yaml | 225 + .../data/abilities/catalogue/barcodelookup.ts | 114 + .../catalogue/barcodelookup.utest.js | 106 + platform/data/abilities/catalogue/basecamp.ts | 331 + platform/data/abilities/catalogue/beehiiv.ts | 106 + .../data/abilities/catalogue/beehiiv.utest.js | 114 + .../catalogue/betterstack.clickhouse.ts | 178 + .../catalogue/betterstack.clickhouse.utest.js | 145 + .../catalogue/bigcommerce.openapi.yaml | 119 + .../data/abilities/catalogue/bigcommerce.ts | 137 + .../abilities/catalogue/bigcommerce.utest.js | 120 + .../catalogue/brandfetch.openapi.yaml | 131 + .../data/abilities/catalogue/brandfetch.ts | 131 + .../abilities/catalogue/brandfetch.utest.js | 115 + .../abilities/catalogue/brave.openapi.yaml | 92 + platform/data/abilities/catalogue/brave.ts | 58 + .../data/abilities/catalogue/brave.utest.js | 113 + platform/data/abilities/catalogue/buffer.ts | 487 + .../data/abilities/catalogue/cal.openapi.yaml | 109 + platform/data/abilities/catalogue/cal.ts | 138 + .../data/abilities/catalogue/cal.utest.js | 110 + platform/data/abilities/catalogue/calendly.ts | 233 + .../abilities/catalogue/calendly.utest.js | 112 + .../data/abilities/catalogue/cbk.abort.ts | 55 + .../data/abilities/catalogue/cbk.agent.ts | 131 + platform/data/abilities/catalogue/cbk.anam.ts | 35 + .../abilities/catalogue/cbk.attachment.yaml | 25 + .../data/abilities/catalogue/cbk.avatar.ts | 35 + .../data/abilities/catalogue/cbk.blueprint.ts | 115 + platform/data/abilities/catalogue/cbk.bot.ts | 425 + .../abilities/catalogue/cbk.conversation.ts | 165 + .../data/abilities/catalogue/cbk.dataset.ts | 11 + .../data/abilities/catalogue/cbk.dataset.yaml | 77 + .../data/abilities/catalogue/cbk.discord.ts | 61 + .../data/abilities/catalogue/cbk.email.ts | 65 + .../data/abilities/catalogue/cbk.email.yaml | 16 + .../data/abilities/catalogue/cbk.fetch.ts | 195 + platform/data/abilities/catalogue/cbk.file.ts | 479 + platform/data/abilities/catalogue/cbk.git.ts | 164 + .../data/abilities/catalogue/cbk.github.ts | 295 + .../abilities/catalogue/cbk.googlechat.ts | 60 + .../data/abilities/catalogue/cbk.graphql.ts | 75 + .../data/abilities/catalogue/cbk.image.ts | 385 + .../data/abilities/catalogue/cbk.instagram.ts | 65 + platform/data/abilities/catalogue/cbk.list.ts | 101 + .../data/abilities/catalogue/cbk.listen.yaml | 13 + .../data/abilities/catalogue/cbk.math.yaml | 20 + platform/data/abilities/catalogue/cbk.mcp.ts | 44 + .../data/abilities/catalogue/cbk.memory.ts | 304 + .../data/abilities/catalogue/cbk.messenger.ts | 64 + .../abilities/catalogue/cbk.microsoftteams.ts | 66 + .../data/abilities/catalogue/cbk.mock.yaml | 268 + .../data/abilities/catalogue/cbk.rating.ts | 593 + .../data/abilities/catalogue/cbk.recall.ts | 58 + .../data/abilities/catalogue/cbk.research.ts | 24 + .../data/abilities/catalogue/cbk.search.yaml | 55 + .../data/abilities/catalogue/cbk.shell.ts | 330 + .../data/abilities/catalogue/cbk.skills.ts | 391 + .../data/abilities/catalogue/cbk.skillset.ts | 40 + .../data/abilities/catalogue/cbk.slack.ts | 62 + .../data/abilities/catalogue/cbk.space.ts | 831 + platform/data/abilities/catalogue/cbk.task.ts | 855 + .../data/abilities/catalogue/cbk.telegram.ts | 62 + .../data/abilities/catalogue/cbk.text.yaml | 101 + platform/data/abilities/catalogue/cbk.time.ts | 36 + platform/data/abilities/catalogue/cbk.todo.ts | 106 + .../data/abilities/catalogue/cbk.twilio.ts | 136 + platform/data/abilities/catalogue/cbk.url.ts | 109 + .../data/abilities/catalogue/cbk.view.yaml | 14 + .../data/abilities/catalogue/cbk.whatsapp.ts | 62 + .../data/abilities/catalogue/chargebee.ts | 318 + .../abilities/catalogue/clearbit.openapi.yaml | 488 + platform/data/abilities/catalogue/clearbit.ts | 254 + .../abilities/catalogue/clearbit.utest.js | 113 + .../data/abilities/catalogue/clickhouse.ts | 70 + platform/data/abilities/catalogue/clickup.ts | 361 + .../abilities/catalogue/clickup.utest.js.bck | 108 + platform/data/abilities/catalogue/clockify.ts | 347 + .../abilities/catalogue/clockify.utest.js | 119 + .../abilities/catalogue/cloudflare.sandbox.ts | 545 + .../data/abilities/catalogue/cloudinary.ts | 166 + .../abilities/catalogue/cloudinary.utest.js | 126 + platform/data/abilities/catalogue/coda.ts | 382 + .../data/abilities/catalogue/coda.utest.js | 111 + .../abilities/catalogue/codeqr.openapi.yaml | 363 + platform/data/abilities/catalogue/codeqr.ts | 326 + .../data/abilities/catalogue/codeqr.utest.js | 115 + .../abilities/catalogue/coinapi.openapi.yaml | 109 + platform/data/abilities/catalogue/coinapi.ts | 59 + .../data/abilities/catalogue/coinapi.utest.js | 103 + .../abilities/catalogue/devto.openapi.yaml | 2907 ++ platform/data/abilities/catalogue/devto.ts | 164 + .../data/abilities/catalogue/devto.utest.js | 142 + .../catalogue/dictionaryapi.openapi.yaml | 122 + .../data/abilities/catalogue/dictionaryapi.ts | 25 + .../catalogue/dictionaryapi.utest.js | 105 + .../abilities/catalogue/diffbot.openapi.yaml | 256 + platform/data/abilities/catalogue/diffbot.ts | 144 + .../data/abilities/catalogue/diffbot.utest.js | 121 + platform/data/abilities/catalogue/discord.ts | 452 + .../data/abilities/catalogue/discord.utest.js | 151 + .../data/abilities/catalogue/docusign.api.ts | 35 + .../catalogue/docusign.esignature.ts | 479 + .../catalogue/docusign.esignature.utest.js | 136 + .../abilities/catalogue/dropbox.openapi.yaml | 157 + platform/data/abilities/catalogue/dropbox.ts | 163 + .../data/abilities/catalogue/dropbox.utest.js | 118 + .../data/abilities/catalogue/dropbox.yaml | 21 + .../abilities/catalogue/easypost.openapi.yaml | 102 + platform/data/abilities/catalogue/easypost.ts | 62 + .../abilities/catalogue/easypost.utest.js | 121 + .../catalogue/elevenlabs.openapi.yaml | 128 + .../data/abilities/catalogue/elevenlabs.ts | 189 + .../abilities/catalogue/elevenlabs.utest.js | 169 + .../catalogue/facebook.ads.openapi.yaml | 185 + .../data/abilities/catalogue/facebook.ads.ts | 1210 + .../abilities/catalogue/facebook.ads.utest.js | 114 + .../catalogue/facebook.pages.openapi.yaml | 77 + .../abilities/catalogue/facebook.pages.ts | 88 + .../catalogue/facebook.pages.utest.js | 112 + platform/data/abilities/catalogue/figma.ts | 327 + .../data/abilities/catalogue/figma.utest.js | 121 + .../catalogue/financialmodelingprep.yaml | 61 + .../data/abilities/catalogue/firecrawl.ts | 596 + .../abilities/catalogue/firecrawl.utest.js | 114 + .../data/abilities/catalogue/freshdesk.yaml | 75 + platform/data/abilities/catalogue/geocodio.ts | 87 + .../abilities/catalogue/geocodio.utest.js | 101 + platform/data/abilities/catalogue/giphy.ts | 221 + .../data/abilities/catalogue/giphy.utest.js | 111 + platform/data/abilities/catalogue/github.ts | 56 + .../data/abilities/catalogue/github.utest.js | 6 + platform/data/abilities/catalogue/github.yaml | 1114 + .../abilities/catalogue/glimpse.openapi.yaml | 154 + platform/data/abilities/catalogue/glimpse.ts | 131 + .../data/abilities/catalogue/glimpse.utest.js | 115 + platform/data/abilities/catalogue/godaddy.ts | 57 + .../data/abilities/catalogue/godaddy.utest.js | 131 + .../data/abilities/catalogue/gohighlevel.ts | 32 + .../data/abilities/catalogue/google.ads.ts | 397 + .../abilities/catalogue/google.calendar.ts | 343 + .../data/abilities/catalogue/google.docs.ts | 384 + .../data/abilities/catalogue/google.drive.ts | 234 + .../data/abilities/catalogue/google.mail.ts | 695 + .../data/abilities/catalogue/google.meet.ts | 81 + .../catalogue/google.searchconsole.ts | 244 + .../catalogue/google.searchconsole.utest.js | 202 + .../data/abilities/catalogue/google.sheets.ts | 319 + .../data/abilities/catalogue/google.tasks.ts | 435 + .../abilities/catalogue/gumroad.openapi.yaml | 249 + platform/data/abilities/catalogue/gumroad.ts | 249 + .../data/abilities/catalogue/gumroad.utest.js | 111 + platform/data/abilities/catalogue/harvest.ts | 372 + .../data/abilities/catalogue/harvest.utest.js | 111 + platform/data/abilities/catalogue/hubspot.ts | 985 + .../abilities/catalogue/hunter.openapi.yaml | 536 + platform/data/abilities/catalogue/hunter.ts | 187 + .../data/abilities/catalogue/hunter.utest.js | 114 + .../data/abilities/catalogue/hyperproof.yaml | 558 + .../data/abilities/catalogue/instantly.yaml | 256 + platform/data/abilities/catalogue/intercom.ts | 368 + .../abilities/catalogue/intercom.utest.js | 110 + .../catalogue/lemonsqueezy.openapi.yaml | 352 + .../data/abilities/catalogue/lemonsqueezy.ts | 298 + .../abilities/catalogue/lemonsqueezy.utest.js | 111 + .../abilities/catalogue/linear.openapi.yaml | 214 + platform/data/abilities/catalogue/linear.ts | 707 + .../data/abilities/catalogue/linear.utest.js | 127 + .../abilities/catalogue/linkedin.openapi.yaml | 441 + platform/data/abilities/catalogue/linkedin.ts | 576 + .../abilities/catalogue/linkedin.utest.js | 111 + .../abilities/catalogue/linkupso.openapi.yaml | 117 + platform/data/abilities/catalogue/linkupso.ts | 125 + .../abilities/catalogue/linkupso.utest.js | 116 + .../data/abilities/catalogue/listennotes.ts | 181 + .../abilities/catalogue/listennotes.utest.js | 116 + .../data/abilities/catalogue/magento.yaml | 44 + .../abilities/catalogue/mailchimp.utest.js | 129 + .../data/abilities/catalogue/mailchimp.yaml | 169 + .../data/abilities/catalogue/mailgun.api.ts | 35 + .../data/abilities/catalogue/mailgun.send.ts | 146 + .../abilities/catalogue/mailgun.send.utest.js | 122 + .../abilities/catalogue/mailgun.validate.ts | 28 + .../catalogue/mailgun.validate.utest.js | 109 + platform/data/abilities/catalogue/make.yaml | 20 + platform/data/abilities/catalogue/manychat.ts | 511 + .../abilities/catalogue/manychat.utest.js | 122 + .../abilities/catalogue/mapbox.openapi.yaml | 154 + platform/data/abilities/catalogue/mapbox.ts | 75 + .../data/abilities/catalogue/mapbox.utest.js | 116 + .../catalogue/matillion.openapi.yaml | 1027 + .../data/abilities/catalogue/matillion.ts | 1111 + .../abilities/catalogue/matillion.utest.js | 133 + platform/data/abilities/catalogue/mcp.yaml | 2088 + .../catalogue/microsoft.graph.api.ts | 35 + .../catalogue/microsoft.graph.calendar.ts | 166 + .../microsoft.graph.calendar.utest.js | 114 + .../catalogue/microsoft.graph.contact.ts | 138 + .../microsoft.graph.contact.utest.js | 117 + .../catalogue/microsoft.graph.file.ts | 282 + .../catalogue/microsoft.graph.file.utest.js | 120 + .../catalogue/microsoft.graph.message.ts | 192 + .../microsoft.graph.message.utest.js | 117 + .../catalogue/microsoft.graph.openapi.yaml | 1421 + .../catalogue/microsoft.graph.team.ts | 427 + .../catalogue/microsoft.graph.team.utest.js | 117 + .../catalogue/microsoft.graph.user.ts | 377 + .../catalogue/microsoft.graph.user.utest.js | 114 + platform/data/abilities/catalogue/miro.ts | 126 + .../data/abilities/catalogue/miro.utest.js | 153 + .../data/abilities/catalogue/mixpanel.yaml | 39 + .../catalogue/modelcontextprotocol.ts | 101 + .../catalogue/modelcontextprotocol.utest.js | 106 + platform/data/abilities/catalogue/moltbook.ts | 18 + platform/data/abilities/catalogue/monday.ts | 326 + .../abilities/catalogue/newsapi.openapi.yaml | 180 + platform/data/abilities/catalogue/newsapi.ts | 55 + .../data/abilities/catalogue/newsapi.utest.js | 104 + platform/data/abilities/catalogue/notion.yaml | 440 + platform/data/abilities/catalogue/nubela.yaml | 23 + platform/data/abilities/catalogue/okta.ts | 755 + .../data/abilities/catalogue/okta.utest.js | 125 + .../catalogue/openai.ads.openapi.yaml | 439 + .../data/abilities/catalogue/openai.ads.ts | 998 + .../abilities/catalogue/openai.ads.utest.js | 106 + .../abilities/catalogue/openstreetmap.yaml | 56 + .../catalogue/openweathermap.openapi.yaml | 440 + .../abilities/catalogue/openweathermap.ts | 245 + .../catalogue/openweathermap.utest.js | 113 + .../data/abilities/catalogue/pagerduty.ts | 267 + .../abilities/catalogue/pagerduty.utest.js | 167 + platform/data/abilities/catalogue/paypal.yaml | 50 + .../abilities/catalogue/peopledatalabs.yaml | 191 + .../data/abilities/catalogue/perplexity.yaml | 79 + .../abilities/catalogue/pexels.openapi.yaml | 181 + platform/data/abilities/catalogue/pexels.ts | 150 + .../data/abilities/catalogue/pexels.utest.js | 113 + .../data/abilities/catalogue/pipedream.yaml | 135 + .../data/abilities/catalogue/pipedrive.ts | 401 + .../abilities/catalogue/pipedrive.utest.js | 110 + .../data/abilities/catalogue/planetscale.ts | 848 + .../abilities/catalogue/planetscale.utest.js | 115 + .../data/abilities/catalogue/polymarket.ts | 718 + .../abilities/catalogue/polymarket.utest.js | 309 + .../data/abilities/catalogue/postgrest.ts | 68 + .../data/abilities/catalogue/productboard.ts | 286 + .../abilities/catalogue/raindrop.openapi.yaml | 402 + platform/data/abilities/catalogue/raindrop.ts | 417 + .../abilities/catalogue/raindrop.utest.js | 115 + .../abilities/catalogue/reddit.openapi.yaml | 362 + platform/data/abilities/catalogue/reddit.ts | 647 + .../data/abilities/catalogue/reddit.utest.js | 249 + .../data/abilities/catalogue/replicate.ts | 178 + .../abilities/catalogue/replicate.utest.js | 107 + platform/data/abilities/catalogue/resend.ts | 182 + .../data/abilities/catalogue/resend.utest.js | 120 + .../catalogue/revenuecat.openapi.yaml | 166 + .../data/abilities/catalogue/revenuecat.ts | 197 + .../abilities/catalogue/revenuecat.utest.js | 103 + platform/data/abilities/catalogue/sendgrid.ts | 88 + .../abilities/catalogue/sendgrid.utest.js | 120 + platform/data/abilities/catalogue/sentry.ts | 435 + .../data/abilities/catalogue/sentry.utest.js | 131 + platform/data/abilities/catalogue/serpapi.ts | 345 + platform/data/abilities/catalogue/serper.ts | 243 + .../data/abilities/catalogue/serper.utest.js | 122 + .../data/abilities/catalogue/shopify.yaml | 175 + platform/data/abilities/catalogue/slack.ts | 1167 + .../data/abilities/catalogue/slack.utest.js | 142 + platform/data/abilities/catalogue/slack.yaml | 102 + .../data/abilities/catalogue/snowflake.ts | 178 + .../abilities/catalogue/snowflake.utest.js | 230 + platform/data/abilities/catalogue/sprites.ts | 1130 + platform/data/abilities/catalogue/stripe.ts | 1057 + .../data/abilities/catalogue/stripe.utest.js | 135 + platform/data/abilities/catalogue/stripe.yaml | 40 + .../data/abilities/catalogue/supabase.yaml | 48 + platform/data/abilities/catalogue/tally.ts | 460 + .../data/abilities/catalogue/tally.utest.js | 190 + .../abilities/catalogue/tavily.openapi.yaml | 162 + platform/data/abilities/catalogue/tavily.ts | 159 + .../data/abilities/catalogue/tavily.utest.js | 116 + platform/data/abilities/catalogue/tavily.yaml | 45 + .../abilities/catalogue/taxjar.openapi.yaml | 985 + platform/data/abilities/catalogue/taxjar.ts | 277 + .../data/abilities/catalogue/taxjar.utest.js | 110 + platform/data/abilities/catalogue/telegram.ts | 321 + .../abilities/catalogue/telegram.utest.js | 148 + platform/data/abilities/catalogue/todoist.ts | 289 + .../data/abilities/catalogue/todoist.utest.js | 134 + platform/data/abilities/catalogue/trello.ts | 895 + .../data/abilities/catalogue/twilio.api.ts | 436 + .../abilities/catalogue/twilio.api.utest.js | 176 + .../data/abilities/catalogue/twilio.lookup.ts | 39 + .../catalogue/twilio.lookup.utest.js | 124 + .../data/abilities/catalogue/twilio.verify.ts | 84 + .../catalogue/twilio.verify.utest.js | 157 + .../abilities/catalogue/twitter.openapi.yaml | 37 + platform/data/abilities/catalogue/twitter.ts | 318 + .../data/abilities/catalogue/twitter.utest.js | 115 + .../abilities/catalogue/typeform.openapi.yaml | 213 + platform/data/abilities/catalogue/typeform.ts | 197 + .../abilities/catalogue/typeform.utest.js | 110 + .../abilities/catalogue/unsplash.openapi.yaml | 316 + platform/data/abilities/catalogue/unsplash.ts | 215 + .../abilities/catalogue/unsplash.utest.js | 115 + .../abilities/catalogue/uplead.openapi.yaml | 188 + platform/data/abilities/catalogue/uplead.ts | 114 + .../data/abilities/catalogue/uplead.utest.js | 113 + .../data/abilities/catalogue/usefind.yaml | 47 + platform/data/abilities/catalogue/vanta.yaml | 560 + platform/data/abilities/catalogue/vapi.ts | 200 + .../data/abilities/catalogue/vapi.utest.js | 186 + platform/data/abilities/catalogue/vonage.ts | 89 + .../data/abilities/catalogue/vonage.utest.js | 124 + platform/data/abilities/catalogue/vonage.yaml | 2 + .../data/abilities/catalogue/weatherbit.ts | 213 + .../abilities/catalogue/weatherbit.utest.js | 156 + .../catalogue/wikipedia.openapi.json | 1 + .../data/abilities/catalogue/wikipedia.ts | 24 + .../abilities/catalogue/wikipedia.utest.js | 114 + .../data/abilities/catalogue/woocommerce.yaml | 89 + .../abilities/catalogue/xero.accounting.ts | 838 + .../catalogue/xero.accounting.utest.js | 130 + platform/data/abilities/catalogue/xero.api.ts | 35 + platform/data/abilities/catalogue/zapier.yaml | 182 + platform/data/abilities/catalogue/zendesk.ts | 542 + .../data/abilities/catalogue/zendesk.utest.js | 204 + .../data/abilities/catalogue/zoom.utest.js | 103 + platform/data/abilities/catalogue/zoom.yaml | 155 + platform/data/abilities/visible.ts | 12 + platform/data/abilities/visible.utest.js | 9 + platform/data/demos.d.ts | 18 + platform/data/demos.yaml | 173 + platform/data/files.d.ts | 11 + platform/data/files.yaml | 12 + platform/data/secrets/all.ts | 18 + platform/data/secrets/all.utest.js | 5 + platform/data/secrets/catalogue/standard.d.ts | 104 + .../data/secrets/catalogue/standard.utest.ts | 276 + platform/data/secrets/catalogue/standard.yaml | 2967 ++ platform/data/secrets/visible.ts | 12 + platform/data/secrets/visible.utest.ts | 9 + platform/data/spaces.d.ts | 11 + platform/data/spaces.yaml | 12 + platform/emails/ContentAbuseDetected.jsx | 35 + platform/emails/DatasetSyncCompleted.jsx | 37 + platform/emails/EmailAction.tsx | 29 + platform/emails/EmailAction.utest.js | 92 + platform/emails/EmailLogin.tsx | 58 + platform/emails/EmailLogin.utest.js | 48 + platform/emails/ExceededAccountLimits.jsx | 56 + platform/emails/ExceededDatabaseLimits.jsx | 57 + platform/emails/ExceededRateLimits.jsx | 57 + platform/emails/InvoicePaymentFailed.jsx | 45 + platform/emails/InvoicePaymentSucceeded.jsx | 41 + .../emails/NearlyExceededAccountLimits.jsx | 57 + .../emails/NearlyExceededDatabaseLimits.jsx | 57 + platform/emails/SubscriptionDeleted.jsx | 49 + platform/emails/TeamInvitation.tsx | 94 + platform/emails/TeamInvitation.utest.js | 119 + platform/emails/TrialStart.jsx | 42 + platform/emails/TrialStart.utest.js | 90 + .../TrialStartDuplicateCardDetected.jsx | 44 + platform/emails/UsagePolicyTriggered.jsx | 59 + platform/emails/UserDeleted.jsx | 33 + platform/embeds/mcp/v1.ts | 50 + .../widget/plugins/analytics-consent.js | 81 + platform/embeds/widget/v1.ts | 584 + platform/embeds/widget/v2.ts | 2690 ++ platform/eslint.config.mjs | 206 + platform/eslint/README.md | 1 + .../eslint/custom-rules/directive-first.js | 55 + .../custom-rules/directive-first.utest.js | 68 + platform/eslint/custom-rules/index.js | 18 + .../eslint/custom-rules/lib/client-files.js | 46 + .../eslint/custom-rules/lib/global-fetch.js | 36 + .../custom-rules/lib/transpiled-packages.js | 331 + .../no-direct-documentation-links.js | 144 + .../no-direct-documentation-links.utest.js | 107 + .../custom-rules/no-direct-process-env.js | 232 + .../no-direct-process-env.utest.js | 142 + .../eslint/custom-rules/no-global-fetch.js | 66 + .../custom-rules/no-global-fetch.utest.js | 101 + .../custom-rules/no-plain-fetch-in-routes.js | 85 + .../no-plain-fetch-in-routes.utest.js | 101 + .../no-restricted-client-imports.js | 82 + .../no-restricted-client-imports.utest.js | 80 + platform/eslint/custom-rules/package.json | 7 + .../custom-rules/require-custom-use-router.js | 97 + .../require-dispose-for-factory-result.js | 190 + .../custom-rules/require-make-json-safe.js | 318 + .../require-safe-prisma-delete.js | 251 + .../require-transpiled-package.js | 149 + .../require-transpiled-package.utest.js | 230 + .../eslint/custom-rules/require-typed-sql.js | 63 + platform/eslint/custom-rules/todo-by.js | 89 + platform/eslint/custom-rules/todo-by.utest.js | 92 + platform/examples/catalogue.utest.ts | 164 + platform/examples/catalogue/blueprints.yaml | 35033 ++++++++++++++++ platform/examples/catalogue/demos.yaml | 1057 + platform/examples/catalogue/hub.yaml | 113 + platform/examples/catalogue/projects.yaml | 1542 + platform/examples/catalogue/widgets.yaml | 5855 +++ platform/examples/index.ts | 11 + platform/examples/index.utest.js | 1067 + platform/examples/types.d.ts | 149 + platform/graphql/v1/client/graphql.ts | 7241 ++++ platform/graphql/v1/client/index.ts | 56 + platform/graphql/v1/pothos.utest.js | 12 + .../v1/queries/apps/availableSecrets.graphql | 27 + .../apps/availableSharedSecrets.graphql | 19 + .../v1/queries/apps/availableSources.graphql | 57 + .../apps/blueprintAgentResources.graphql | 55 + .../v1/queries/apps/configuredBots.graphql | 29 + .../apps/listContactConversations.graphql | 44 + .../v1/queries/apps/listContactTasks.graphql | 42 + .../v1/queries/blueprints/portalUrl.graphql | 14 + .../blueprints/templateBlueprints.graphql | 14 + .../blueprints/templateSecrets.graphql | 14 + .../v1/queries/platform/templates.graphql | 59 + platform/graphql/v1/schema.mutations.utest.ts | 149 + platform/graphql/v1/schema.ts | 14342 +++++++ platform/hooks/useAbilityTemplates.js | 45 + platform/hooks/useAbilityTemplates.utest.js | 138 + platform/hooks/useAborter.jsx | 68 + platform/hooks/useAborter.utest.js | 424 + platform/hooks/useAborter.utest.jsx | 248 + platform/hooks/useAggressiveScrollHeight.jsx | 55 + .../useAggressiveScrollHeight.stories.jsx | 466 + .../hooks/useAggressiveScrollHeight.utest.js | 295 + platform/hooks/useAutoRevert.tsx | 29 + platform/hooks/useAutoRevert.utest.js | 436 + platform/hooks/useAutoRevert.utest.jsx | 267 + platform/hooks/useAvailableModels.ts | 98 + platform/hooks/useAvailableModels.utest.jsx | 117 + platform/hooks/useAvatarInputAudioStream.ts | 64 + .../hooks/useAvatarInputAudioStream.utest.js | 164 + platform/hooks/useAvatarTransport.ts | 183 + platform/hooks/useAvatarTransport.utest.js | 259 + platform/hooks/useAwaitableComponent.tsx | 64 + platform/hooks/useAwaitableComponent.utest.js | 514 + .../hooks/useAwaitableComponent.utest.jsx | 467 + platform/hooks/useBotBlock.jsx | 153 + platform/hooks/useBotBlock.utest.js | 130 + platform/hooks/useBroadcastChannel.tsx | 21 + platform/hooks/useBroadcastChannel.utest.js | 139 + platform/hooks/useBroadcastChannel.utest.jsx | 172 + platform/hooks/useBroadcastChannelState.ts | 63 + .../hooks/useBroadcastChannelState.utest.js | 364 + platform/hooks/useBuilderExperience.jsx | 56 + platform/hooks/useBuilderExperience.utest.jsx | 135 + platform/hooks/useBus.tsx | 43 + platform/hooks/useBus.utest.js | 80 + platform/hooks/useBus.utest.jsx | 146 + platform/hooks/useCache.tsx | 289 + platform/hooks/useCache.utest.js | 918 + platform/hooks/useClassNameOnNewElements.jsx | 77 + .../hooks/useClassNameOnNewElements.utest.js | 422 + platform/hooks/useClickOutside.tsx | 48 + platform/hooks/useClickOutside.utest.js | 174 + platform/hooks/useClipboardContainer.tsx | 48 + platform/hooks/useClipboardContainer.utest.js | 319 + platform/hooks/useCodeAction.tsx | 35 + platform/hooks/useCodeAction.utest.js | 239 + platform/hooks/useComboKeyBinding.tsx | 45 + platform/hooks/useComboKeyBinding.utest.js | 503 + platform/hooks/useControllableInput.tsx | 72 + platform/hooks/useControllableInput.utest.js | 276 + platform/hooks/useControlledState.stories.jsx | 311 + platform/hooks/useControlledState.tsx | 73 + platform/hooks/useControlledState.utest.js | 397 + platform/hooks/useConversationManager.jsx | 2033 + .../hooks/useConversationManager.utest.js | 2422 ++ platform/hooks/useConversationManager2.jsx | 197 + .../hooks/useConversationManager2.utest.js | 435 + .../hooks/useConversationManagerFetch.jsx | 139 + .../useConversationManagerFetch.utest.js | 610 + .../hooks/useConversationManagerState.jsx | 1080 + .../useConversationManagerState.utest.js | 1528 + platform/hooks/useConversationMonitor.tsx | 294 + .../hooks/useConversationMonitor.utest.js | 167 + platform/hooks/useCookie.tsx | 60 + platform/hooks/useCookie.utest.js | 160 + platform/hooks/useCopyWebsiteTheme.tsx | 62 + platform/hooks/useCopyWebsiteTheme.utest.js | 209 + platform/hooks/useDOMQuerySelector.jsx | 68 + .../hooks/useDOMQuerySelector.stories.jsx | 452 + platform/hooks/useDOMQuerySelector.utest.js | 414 + platform/hooks/useDOMQuerySelector.utest.jsx | 395 + platform/hooks/useDashboardWidgetSend.tsx | 45 + .../hooks/useDashboardWidgetSend.utest.js | 161 + platform/hooks/useDebounce.ts | 32 + platform/hooks/useDebounce.utest.js | 298 + platform/hooks/useDebouncedInput.ts | 107 + platform/hooks/useDebouncedInput.utest.js | 402 + platform/hooks/useDeps.ts | 16 + platform/hooks/useDeps.utest.js | 280 + platform/hooks/useDropzone.tsx | 20 + platform/hooks/useDropzone.utest.js | 237 + platform/hooks/useEntryAnimation.tsx | 61 + platform/hooks/useEntryAnimation.utest.js | 506 + platform/hooks/useEventChannel.ts | 36 + platform/hooks/useEventChannel.utest.js | 147 + platform/hooks/useExternalAPIURL.ts | 18 + platform/hooks/useExternalAPIURL.utest.js | 55 + platform/hooks/useExternalFrontendURL.ts | 18 + .../hooks/useExternalFrontendURL.utest.jsx | 29 + platform/hooks/useExtractIntegrationSeries.js | 108 + .../useExtractIntegrationSeries.utest.js | 161 + platform/hooks/useFetch.jsx | 473 + platform/hooks/useFetch.utest.js | 1126 + platform/hooks/useFirst.tsx | 31 + platform/hooks/useFirst.utest.js | 170 + platform/hooks/useFormElementAutoTools.jsx | 116 + .../hooks/useFormElementAutoTools.utest.js | 472 + platform/hooks/useFramePopup.jsx | 129 + platform/hooks/useFramePopup.utest.js | 122 + platform/hooks/useFunctionDispatch.tsx | 41 + platform/hooks/useFunctionDispatch.utest.js | 363 + platform/hooks/useFunctionPacks.ts | 136 + platform/hooks/useFunctionPacks.utest.js | 277 + platform/hooks/useFuzzySearch.ts | 73 + platform/hooks/useFuzzySearch.utest.js | 275 + platform/hooks/useFuzzySearchFunction.ts | 101 + .../hooks/useFuzzySearchFunction.utest.js | 352 + .../hooks/useGraphQLConnectionListRoute.jsx | 90 + .../useGraphQLConnectionListRoute.utest.js | 131 + platform/hooks/useHasChildren.tsx | 30 + platform/hooks/useHasChildren.utest.js | 476 + platform/hooks/useHistory.jsx | 94 + platform/hooks/useHistory.utest.js | 283 + platform/hooks/useHostname.tsx | 224 + platform/hooks/useHostname.utest.js | 641 + platform/hooks/useHydrated.tsx | 16 + .../hooks/useHydrationSafeLayoutEffect.ts | 6 + platform/hooks/useImageColorPalette.tsx | 62 + platform/hooks/useImageColorPalette.utest.js | 424 + platform/hooks/useImageColorPalette.utest.jsx | 188 + .../useIncrementIndexAfterDelay.stories.js | 172 + ...eIncrementIndexAfterDelay.stories.utest.js | 66 + platform/hooks/useIncrementIndexAfterDelay.ts | 82 + .../useIncrementIndexAfterDelay.utest.js | 82 + platform/hooks/useInitial.tsx | 25 + platform/hooks/useInitial.utest.js | 175 + platform/hooks/useInstallIntent.ts | 61 + platform/hooks/useInstallIntent.utest.jsx | 82 + platform/hooks/useIsAppRouter.tsx | 7 + platform/hooks/useIsAppRouter.utest.js | 186 + platform/hooks/useIsContainerScrolled.jsx | 120 + .../hooks/useIsContainerScrolled.stories.jsx | 127 + .../useIsContainerScrolled.stories.utest.js | 54 + .../hooks/useIsContainerScrolled.utest.js | 494 + platform/hooks/useIsMounted.tsx | 11 + platform/hooks/useIsMounted.utest.js | 66 + platform/hooks/useIsScrolled.tsx | 32 + platform/hooks/useIsScrolled.utest.js | 243 + platform/hooks/useIsTop.tsx | 13 + platform/hooks/useIsTop.utest.js | 61 + platform/hooks/useLocalStorage.tsx | 111 + platform/hooks/useLocalStorage.utest.js | 346 + platform/hooks/useMagicDialog.jsx | 151 + platform/hooks/useMagicDialog.utest.js | 497 + platform/hooks/useMediaQuery.tsx | 56 + platform/hooks/useMediaQuery.utest.js | 104 + platform/hooks/useMintedClient.ts | 101 + platform/hooks/useMintedClient.utest.js | 109 + platform/hooks/useMouseOutsideWindow.tsx | 29 + platform/hooks/useMouseOutsideWindow.utest.js | 199 + platform/hooks/usePartner.jsx | 80 + platform/hooks/usePartner.utest.js | 87 + platform/hooks/usePartner.utest.jsx | 181 + platform/hooks/usePlatformExperience.jsx | 11 + platform/hooks/usePopup.jsx | 441 + platform/hooks/usePopup.utest.js | 660 + platform/hooks/usePopupJob.jsx | 104 + platform/hooks/usePopupJob.utest.js | 115 + platform/hooks/usePopupJob.utest.jsx | 90 + platform/hooks/usePostMessageHandler.jsx | 54 + platform/hooks/usePostMessageHandler.utest.js | 372 + platform/hooks/usePreventLeave.tsx | 27 + platform/hooks/usePreventLeave.utest.js | 181 + platform/hooks/usePreventLeave.utest.jsx | 169 + platform/hooks/usePrevious.ts | 14 + platform/hooks/usePrevious.utest.js | 268 + platform/hooks/useProjectScope.jsx | 438 + platform/hooks/useProjectScope.utest.jsx | 494 + platform/hooks/useQuerySessionOption.tsx | 35 + platform/hooks/useQuerySessionOption.utest.js | 85 + .../hooks/useQuerySessionOption.utest.jsx | 93 + platform/hooks/useReadyNotification.tsx | 54 + platform/hooks/useReadyNotification.utest.js | 371 + platform/hooks/useRouter.jsx | 530 + platform/hooks/useRouter.utest.jsx | 202 + platform/hooks/useScopedCreateData.jsx | 20 + platform/hooks/useScopedCreateData.utest.jsx | 33 + .../hooks/useScopedQuerySessionOption.tsx | 47 + .../useScopedQuerySessionOption.utest.js | 68 + .../useScopedQuerySessionOption.utest.jsx | 44 + platform/hooks/useScreenshot.jsx | 138 + platform/hooks/useScreenshot.utest.js | 5 + platform/hooks/useScrollHeight.tsx | 51 + platform/hooks/useScrollHeight.utest.js | 261 + platform/hooks/useScrollSaveRestore.jsx | 91 + platform/hooks/useScrollSaveRestore.utest.js | 379 + platform/hooks/useSearchParam.tsx | 7 + platform/hooks/useSearchParam.utest.js | 46 + platform/hooks/useSearchParams.tsx | 10 + platform/hooks/useSearchParams.utest.js | 34 + platform/hooks/useSecretAuthenticate.js | 60 + platform/hooks/useSecretTemplates.js | 45 + platform/hooks/useSecretTemplates.utest.js | 154 + platform/hooks/useSession.tsx | 7 + platform/hooks/useSession.utest.js | 91 + platform/hooks/useSignin.ts | 73 + platform/hooks/useSignin.utest.js | 121 + platform/hooks/useSignout.ts | 67 + platform/hooks/useSignout.utest.js | 93 + platform/hooks/useSkillsetSecrets.js | 143 + platform/hooks/useSkillsetSecrets.utest.js | 405 + platform/hooks/useTabIndent.jsx | 60 + platform/hooks/useTabIndent.utest.js | 439 + platform/hooks/useTeamSwitch.tsx | 57 + platform/hooks/useTeamSwitch.utest.js | 117 + platform/hooks/useTextAnimation.jsx | 86 + platform/hooks/useTextAnimation.utest.js | 496 + platform/hooks/useTextCompletion.tsx | 32 + platform/hooks/useTextCompletion.utest.js | 372 + platform/hooks/useTextSelection.stories.tsx | 271 + platform/hooks/useTextSelection.tsx | 183 + platform/hooks/useTextSelection.utest.js | 323 + platform/hooks/useTextSelection.utest.tsx | 213 + platform/hooks/useTextareaSelection.tsx | 221 + platform/hooks/useTextareaSelection.utest.js | 206 + platform/hooks/useTheme.ts | 13 + platform/hooks/useTheme.utest.js | 148 + platform/hooks/useThrottle.tsx | 28 + platform/hooks/useThrottle.utest.js | 235 + platform/hooks/useThrottle.utest.jsx | 366 + platform/hooks/useTimeout.tsx | 32 + platform/hooks/useTimeout.utest.js | 280 + platform/hooks/useTokenCount.ts | 78 + platform/hooks/useTokenCount.utest.js | 102 + platform/hooks/useTrace.jsx | 68 + platform/hooks/useTrace.utest.js | 295 + platform/hooks/useTrace.utest.jsx | 341 + platform/hooks/useTraceUpdate.tsx | 48 + platform/hooks/useTraceUpdate.utest.js | 324 + platform/hooks/useUrl.tsx | 24 + platform/hooks/useUrl.utest.js | 267 + platform/hooks/useUrl.utest.jsx | 274 + platform/hooks/useUserSwitch.tsx | 64 + platform/hooks/useUserSwitch.utest.js | 128 + platform/hooks/useWebMCP.tsx | 85 + platform/hooks/useWebMCP.utest.js | 170 + platform/hooks/useWindowFocus.tsx | 26 + platform/hooks/useWindowFocus.utest.js | 119 + platform/i18n.config.js | 9 + platform/icons/avatar.svg | 23 + platform/icons/brands/anam.svg | 6 + platform/icons/brands/discord.svg | 524 + platform/icons/brands/mcp.svg | 20 + platform/icons/brands/messenger.svg | 23 + platform/icons/brands/recall.svg | 6 + platform/icons/brands/slack.svg | 28 + platform/icons/brands/telegram.svg | 32 + platform/icons/brands/twitter.svg | 40 + platform/icons/brands/whatsapp.svg | 56 + platform/icons/cbkai.svg | 8 + platform/icons/social/discord.svg | 6 + platform/icons/social/github.svg | 6 + platform/icons/social/instagram.svg | 6 + platform/icons/social/linkedin.svg | 6 + platform/icons/social/tiktok.svg | 6 + platform/icons/social/youtube.svg | 6 + platform/icons/widget.svg | 8 + platform/instrumentation-client.ts | 4 + platform/instrumentation.ts | 36 + platform/jest.itest.config.js | 264 + platform/jest.utest.config.js | 456 + platform/jest/resolver.js | 60 + platform/jest/utils/ability.js | 548 + platform/jest/utils/ability.utest.js | 257 + platform/jest/utils/openai.js | 57 + platform/jest/workspace.patterns.js | 32 + platform/layouts/Admin.jsx | 46 + platform/layouts/App.jsx | 1592 + platform/layouts/Dashboard.jsx | 888 + platform/layouts/Dashboard.utest.jsx | 312 + platform/layouts/Demo.jsx | 204 + platform/layouts/Email.tsx | 577 + platform/layouts/Email.utest.js | 804 + platform/layouts/Errata.jsx | 113 + platform/layouts/Explore.jsx | 128 + platform/layouts/Explore.utest.js | 38 + platform/layouts/Hub.jsx | 199 + platform/layouts/Main.jsx | 92 + platform/layouts/Platform.jsx | 65 + platform/layouts/Wizard.jsx | 487 + platform/lib/ability.catalogue.ts | 93 + platform/lib/ability.catalogue.utest.js | 665 + platform/lib/ability.fields.ts | 35 + platform/lib/ability.fields.utest.js | 177 + platform/lib/ability.fields.utest.ts | 154 + platform/lib/ability.function.ts | 626 + platform/lib/ability.function.utest.js | 4361 ++ platform/lib/ability.icon.ts | 130 + platform/lib/ability.icon.utest.js | 169 + platform/lib/ability.icon.utest.ts | 412 + platform/lib/ability.instruction.ts | 70 + platform/lib/ability.instruction.utest.js | 320 + platform/lib/ability.provider.ts | 101 + platform/lib/ability.provider.utest.js | 402 + platform/lib/ability.secret.ts | 39 + platform/lib/ability.secret.utest.js | 208 + platform/lib/ability.template.ts | 3617 ++ platform/lib/ability.template.utest.js | 1889 + platform/lib/abort.ts | 99 + platform/lib/abort.utest.js | 200 + platform/lib/abort.utest.ts | 218 + platform/lib/action.config.ts | 433 + platform/lib/action.config.utest.js | 1764 + platform/lib/action.definition.ts | 133 + platform/lib/action.definition.utest.js | 339 + platform/lib/action.definition.utest.ts | 176 + platform/lib/action.exec.abort.ts | 63 + platform/lib/action.exec.abort.utest.js | 339 + platform/lib/action.exec.agent.ts | 374 + platform/lib/action.exec.agent.utest.js | 796 + platform/lib/action.exec.all.ts | 240 + platform/lib/action.exec.all.utest.js | 755 + platform/lib/action.exec.attachment.ts | 173 + platform/lib/action.exec.attachment.utest.js | 682 + platform/lib/action.exec.blueprint.ts | 926 + platform/lib/action.exec.blueprint.utest.js | 964 + platform/lib/action.exec.bot.ts | 1674 + platform/lib/action.exec.bot.utest.js | 1603 + platform/lib/action.exec.conversation.ts | 355 + .../lib/action.exec.conversation.utest.js | 560 + platform/lib/action.exec.dataset.ts | 523 + platform/lib/action.exec.dataset.utest.js | 717 + platform/lib/action.exec.echo.ts | 51 + platform/lib/action.exec.echo.utest.js | 342 + platform/lib/action.exec.email.tsx | 164 + platform/lib/action.exec.email.utest.js | 497 + platform/lib/action.exec.fetch.ts | 1173 + platform/lib/action.exec.fetch.utest.js | 4624 ++ platform/lib/action.exec.file.ts | 1006 + platform/lib/action.exec.file.utest.js | 1117 + platform/lib/action.exec.form.ts | 100 + platform/lib/action.exec.form.utest.js | 330 + platform/lib/action.exec.image.ts | 498 + platform/lib/action.exec.image.utest.js | 1007 + platform/lib/action.exec.list.ts | 363 + platform/lib/action.exec.list.utest.js | 334 + platform/lib/action.exec.listen.ts | 180 + platform/lib/action.exec.listen.utest.js | 800 + platform/lib/action.exec.mcp.ts | 368 + platform/lib/action.exec.mcp.utest.js | 1028 + platform/lib/action.exec.memory.ts | 603 + platform/lib/action.exec.memory.utest.js | 627 + platform/lib/action.exec.pack.ts | 388 + platform/lib/action.exec.pack.utest.js | 671 + platform/lib/action.exec.rating.ts | 542 + platform/lib/action.exec.rating.utest.js | 608 + platform/lib/action.exec.search.ts | 221 + platform/lib/action.exec.search.utest.js | 505 + platform/lib/action.exec.shell.ts | 2197 + platform/lib/action.exec.shell.utest.js | 1708 + platform/lib/action.exec.skillset.ts | 316 + platform/lib/action.exec.skillset.utest.js | 696 + platform/lib/action.exec.space.ts | 1787 + platform/lib/action.exec.space.utest.js | 1249 + platform/lib/action.exec.task.ts | 1350 + platform/lib/action.exec.task.utest.js | 1779 + platform/lib/action.exec.text.ts | 131 + platform/lib/action.exec.text.utest.js | 912 + platform/lib/action.exec.time.ts | 210 + platform/lib/action.exec.time.utest.js | 205 + platform/lib/action.exec.todo.ts | 310 + platform/lib/action.exec.todo.utest.js | 481 + platform/lib/action.exec.view.ts | 192 + platform/lib/action.exec.view.utest.js | 651 + platform/lib/action.filter.ts | 151 + platform/lib/action.filter.utest.js | 335 + platform/lib/action.label.ts | 22 + platform/lib/action.label.utest.js | 51 + platform/lib/action.name.ts | 58 + platform/lib/action.name.utest.js | 195 + platform/lib/action.parse.ts | 244 + platform/lib/action.parse.utest.js | 1529 + platform/lib/action.tags.concat.utest.ts | 78 + platform/lib/action.tags.ts | 5847 +++ platform/lib/action.tags.utest.js | 2647 ++ platform/lib/activity.js | 739 + platform/lib/activity.utest.js | 1075 + platform/lib/admin.ts | 41 + platform/lib/admin.utest.js | 313 + platform/lib/analytics.ts | 24 + platform/lib/analytics.utest.js | 194 + platform/lib/anam.access.ts | 55 + platform/lib/anam.access.utest.js | 153 + platform/lib/anam.session.ts | 45 + platform/lib/anam.session.utest.js | 72 + platform/lib/app.action.ts | 155 + platform/lib/app.action.utest.js | 403 + platform/lib/app.cache.ts | 68 + platform/lib/app.cache.utest.js | 315 + platform/lib/app.config.bot.ts | 194 + platform/lib/app.config.bot.utest.js | 1094 + platform/lib/app.config.helpers.ts | 227 + platform/lib/app.config.helpers.utest.js | 1473 + platform/lib/app.config.schemas.ts | 133 + platform/lib/app.config.schemas.utest.js | 73 + platform/lib/app.contact.ts | 134 + platform/lib/app.contact.utest.js | 440 + platform/lib/app.context.ts | 80 + platform/lib/app.context.utest.js | 365 + platform/lib/app.helpers.ts | 156 + platform/lib/app.helpers.utest.js | 286 + platform/lib/app.method.ts | 129 + platform/lib/app.method.utest.js | 418 + platform/lib/app.route.ts | 114 + platform/lib/app.route.utest.js | 264 + platform/lib/app.router.app.config.ts | 608 + platform/lib/app.router.app.config.utest.js | 867 + platform/lib/app.router.app.manifest.ts | 22 + platform/lib/app.router.app.manifest.utest.js | 139 + platform/lib/app.router.context.ts | 33 + platform/lib/app.router.context.utest.js | 56 + platform/lib/app.router.pages.config.ts | 5 + platform/lib/app.router.pages.config.utest.js | 18 + platform/lib/app.session.ts | 58 + platform/lib/app.session.utest.js | 254 + platform/lib/array.ts | 70 + platform/lib/array.utest.js | 68 + platform/lib/audience.consts.js | 20 + platform/lib/audience.helpers.ts | 64 + platform/lib/audience.helpers.utest.js | 169 + platform/lib/auth.adapter.ts | 104 + platform/lib/auth.adapter.utest.js | 219 + platform/lib/auth.callbacks.js | 302 + platform/lib/auth.callbacks.utest.js | 393 + platform/lib/auth.options.ts | 55 + platform/lib/auth.options.utest.js | 256 + platform/lib/auth.providers.ts | 136 + platform/lib/auth.providers.utest.js | 248 + platform/lib/auth.rate.ts | 186 + platform/lib/auth.rate.utest.js | 167 + platform/lib/auth.signin.ts | 26 + platform/lib/auth.signin.utest.js | 212 + platform/lib/auxiliary.duckdb.ts | 103 + platform/lib/auxiliary.duckdb.utest.js | 410 + .../lib/auxiliary.handler.boundary.utest.js | 151 + .../lib/auxiliary.handler.conformity.utest.js | 105 + platform/lib/auxiliary.handler.ts | 309 + platform/lib/auxiliary.handler.utest.js | 457 + platform/lib/auxiliary.sql.ts | 225 + platform/lib/auxiliary.sql.utest.js | 484 + platform/lib/avatar.access.ts | 55 + platform/lib/avatar.access.utest.js | 161 + platform/lib/avatar.session.ts | 43 + platform/lib/avatar.session.utest.js | 43 + platform/lib/b64.ts | 18 + platform/lib/b64.utest.js | 480 + platform/lib/backstory.js | 966 + platform/lib/backstory.utest.js | 1201 + platform/lib/banner.js | 10 + platform/lib/batch.ts | 188 + platform/lib/batch.utest.js | 159 + platform/lib/billing.core.ts | 47 + platform/lib/billing.handler.ts | 83 + platform/lib/billing.handler.utest.js | 305 + platform/lib/billing.provider.ts | 14 + platform/lib/binary.ts | 64 + platform/lib/binary.utest.js | 113 + platform/lib/blueprint.access.ts | 32 + platform/lib/blueprint.access.utest.js | 388 + platform/lib/blueprint.bulletin.ts | 220 + platform/lib/blueprint.bulletin.utest.js | 234 + .../lib/blueprint.delete.coverage.utest.js | 251 + platform/lib/blueprint.delete.ts | 137 + platform/lib/blueprint.delete.utest.js | 283 + platform/lib/blueprint.export.ts | 173 + platform/lib/blueprint.export.utest.js | 498 + platform/lib/blueprint.export.utest.ts | 247 + platform/lib/blueprint.fields.ts | 97 + platform/lib/blueprint.fields.utest.ts | 233 + platform/lib/blueprint.import.ts | 1840 + platform/lib/blueprint.import.utest.ts | 1982 + platform/lib/blueprint.resources.ts | 204 + platform/lib/blueprint.resources.utest.js | 620 + platform/lib/blueprint.terraform.js | 660 + platform/lib/blueprint.terraform.utest.js | 757 + platform/lib/bool.ts | 28 + platform/lib/bool.utest.js | 251 + platform/lib/bot.access.ts | 56 + platform/lib/bot.access.utest.js | 74 + platform/lib/bot.block.ts | 209 + platform/lib/bot.block.utest.js | 223 + platform/lib/bot.conversation.ts | 185 + platform/lib/bot.conversation.utest.js | 289 + platform/lib/bot.delete.ts | 43 + platform/lib/bot.delete.utest.js | 331 + platform/lib/bot.kind.ts | 9 + platform/lib/bot.kind.utest.js | 411 + platform/lib/browserstorage.ts | 384 + platform/lib/browserstorage.utest.js | 1004 + platform/lib/cache.ts | 153 + platform/lib/cache.utest.js | 594 + platform/lib/call.ts | 220 + platform/lib/call.utest.js | 811 + platform/lib/card.response.ts | 58 + platform/lib/card.response.utest.js | 415 + platform/lib/card.tsx | 165 + platform/lib/card.utest.js | 269 + platform/lib/cbk.graphql.ts | 107 + platform/lib/cbk.graphql.utest.js | 46 + platform/lib/cbk.sdk.ts | 142 + platform/lib/cbk.sdk.utest.js | 950 + platform/lib/cdn.ts | 198 + platform/lib/cdn.utest.js | 309 + platform/lib/channel.core.ts | 510 + platform/lib/channel.core.utest.js | 468 + platform/lib/channel.session.ts | 102 + platform/lib/channel.session.utest.js | 294 + platform/lib/channel.user.ts | 75 + platform/lib/channel.user.utest.js | 177 + platform/lib/chunk.ts | 163 + platform/lib/chunk.utest.js | 138 + platform/lib/cloak.ts | 100 + platform/lib/cloak.utest.js | 100 + platform/lib/code.schema.ts | 159 + platform/lib/code.schema.utest.js | 493 + platform/lib/code.schema.utest.ts | 116 + platform/lib/color.filter.ts | 8 + platform/lib/color.filter.utest.js | 269 + platform/lib/color.ts | 49 + platform/lib/color.utest.js | 15 + platform/lib/color2.ts | 105 + platform/lib/color2.utest.js | 317 + platform/lib/concurrency.ts | 39 + platform/lib/concurrency.utest.js | 80 + platform/lib/contact.access.ts | 8 + platform/lib/contact.access.utest.js | 97 + platform/lib/contact.context.ts | 29 + platform/lib/contact.context.utest.js | 46 + platform/lib/contact.create.ts | 140 + platform/lib/contact.create.utest.js | 383 + platform/lib/context.setup.ts | 195 + platform/lib/context.setup.utest.js | 439 + platform/lib/context.store.js | 530 + platform/lib/context.store.utest.js | 638 + platform/lib/conv.iteration.ts | 35 + platform/lib/conv.iteration.utest.js | 96 + platform/lib/conv.ts | 359 + platform/lib/conv.utest.js | 81 + platform/lib/conversation.app.ts | 25 + platform/lib/conversation.app.utest.js | 37 + platform/lib/conversation.attachment.js | 469 + platform/lib/conversation.attachment.utest.js | 775 + platform/lib/conversation.compact.ts | 36 + platform/lib/conversation.compact.utest.js | 105 + platform/lib/conversation.compaction.ts | 407 + platform/lib/conversation.compaction.utest.js | 524 + platform/lib/conversation.create.js | 507 + platform/lib/conversation.create.utest.js | 408 + platform/lib/conversation.delete.ts | 22 + platform/lib/conversation.delete.utest.js | 248 + platform/lib/conversation.engine.js | 7640 ++++ .../lib/conversation.engine.stateful.utest.js | 1588 + .../conversation.engine.stateless.utest.js | 182 + platform/lib/conversation.engine.utest.js | 8958 ++++ platform/lib/conversation.features.ts | 403 + platform/lib/conversation.features.utest.js | 651 + platform/lib/conversation.find.ts | 13 + platform/lib/conversation.find.utest.js | 117 + platform/lib/conversation.group.ts | 223 + platform/lib/conversation.group.utest.js | 1019 + platform/lib/conversation.idle.ts | 61 + platform/lib/conversation.idle.utest.js | 306 + platform/lib/conversation.monitor.channel.ts | 261 + .../lib/conversation.monitor.channel.utest.js | 286 + .../lib/conversation.monitor.channel.utest.ts | 183 + platform/lib/conversation.tag.ts | 562 + platform/lib/conversation.tag.utest.js | 314 + platform/lib/cookie.ts | 22 + platform/lib/cookie.utest.js | 193 + platform/lib/credential.digest.ts | 41 + platform/lib/credential.digest.utest.js | 91 + platform/lib/credential.mask.ts | 141 + platform/lib/credential.mask.utest.js | 86 + platform/lib/credential.policy.ts | 216 + platform/lib/credential.policy.utest.js | 141 + platform/lib/creds.basic.parse.ts | 191 + platform/lib/creds.basic.parse.utest.js | 206 + platform/lib/cron.ts | 63 + platform/lib/cron.utest.js | 366 + platform/lib/csrf.ts | 74 + platform/lib/csrf.utest.js | 213 + platform/lib/css.support.ts | 13 + platform/lib/css.support.utest.js | 117 + platform/lib/css.var.ts | 22 + platform/lib/css.var.utest.js | 145 + platform/lib/cuid.ts | 49 + platform/lib/cuid.utest.js | 145 + platform/lib/dataset.access.ts | 52 + platform/lib/dataset.access.utest.js | 86 + platform/lib/dataset.apply.ts | 378 + platform/lib/dataset.apply.utest.js | 481 + platform/lib/dataset.delete.ts | 52 + platform/lib/dataset.delete.utest.js | 269 + platform/lib/dataset.filter.ts | 15 + platform/lib/dataset.filter.utest.js | 221 + platform/lib/dataset.search.ts | 126 + platform/lib/dataset.search.utest.js | 399 + platform/lib/dataurl.blob.ts | 12 + platform/lib/dataurl.blob.utest.js | 356 + platform/lib/dataurl.fetch.ts | 14 + platform/lib/dataurl.fetch.utest.js | 228 + platform/lib/dataurl.parse.ts | 50 + platform/lib/dataurl.parse.utest.js | 43 + platform/lib/dataurl.response.ts | 13 + platform/lib/dataurl.response.utest.js | 336 + platform/lib/db.string.ts | 41 + platform/lib/db.string.utest.js | 246 + platform/lib/debounce.ts | 83 + platform/lib/debounce.utest.js | 147 + platform/lib/debug.ts | 30 + platform/lib/defer.ts | 197 + platform/lib/defer.utest.js | 781 + platform/lib/description.parse.ts | 125 + platform/lib/description.parse.utest.js | 179 + platform/lib/discord.api.ts | 72 + platform/lib/discord.api.utest.js | 397 + platform/lib/discord.markdown.ts | 35 + platform/lib/discord.markdown.utest.js | 245 + platform/lib/discord.validation.ts | 121 + platform/lib/discord.validation.utest.js | 183 + platform/lib/domain.ts | 102 + platform/lib/domain.utest.js | 441 + platform/lib/dsd.ts | 75 + platform/lib/dsd.utest.js | 263 + platform/lib/dsd2.ts | 315 + platform/lib/dsd2.utest.js | 511 + platform/lib/edit.ts | 503 + platform/lib/edit.utest.js | 569 + platform/lib/egress.call.ts | 12 + platform/lib/egress.call.utest.js | 46 + platform/lib/egress.core.ts | 181 + platform/lib/egress.core.utest.js | 252 + platform/lib/egress.fetch.ts | 23 + platform/lib/egress.fetch.utest.js | 44 + platform/lib/email.message.ts | 10 + platform/lib/email.message.utest.js | 19 + .../lib/email.validation.blacklist.utest.js | 93 + platform/lib/email.validation.ts | 368 + platform/lib/email.validation.utest.js | 456 + platform/lib/embed.ts | 59 + platform/lib/embed.utest.js | 40 + platform/lib/emoji.ts | 86 + platform/lib/emoji.utest.js | 303 + platform/lib/emoji2.ts | 15 + platform/lib/emoji2.utest.js | 138 + platform/lib/entity.ts | 62 + platform/lib/entity.utest.js | 513 + platform/lib/env.ts | 8 + platform/lib/error.js | 8 + platform/lib/event.ts | 1358 + platform/lib/example.fetch.ts | 200 + platform/lib/example.fetch.utest.js | 261 + platform/lib/example.search.ts | 60 + platform/lib/example.search.utest.js | 61 + platform/lib/experience.ts | 63 + platform/lib/experience.utest.js | 264 + platform/lib/experiment.ts | 104 + platform/lib/experiment.utest.js | 138 + platform/lib/extract.contact.ts | 271 + platform/lib/extract.contact.utest.js | 331 + platform/lib/extract.data.ts | 340 + platform/lib/extract.data.utest.js | 612 + platform/lib/extract.input.ts | 134 + platform/lib/extract.input.utest.js | 523 + platform/lib/extract.references.ts | 100 + platform/lib/extract.references.utest.js | 379 + platform/lib/fetch.js | 10 + platform/lib/field.js | 649 + platform/lib/field.utest.js | 2022 + platform/lib/file.access.ts | 15 + platform/lib/file.access.utest.js | 229 + platform/lib/file.delete.ts | 21 + platform/lib/file.delete.utest.js | 110 + platform/lib/file.helpers.ts | 41 + platform/lib/file.helpers.utest.js | 276 + platform/lib/file.icon.ts | 159 + platform/lib/file.storage.ts | 156 + platform/lib/file.storage.utest.js | 434 + platform/lib/file3.utest.js | 48 + platform/lib/filter.ts | 381 + platform/lib/filter.utest.js | 1904 + platform/lib/form.ts | 199 + platform/lib/form.utest.js | 1195 + platform/lib/fs.cache.ts | 168 + platform/lib/fs.cache.utest.js | 244 + platform/lib/github.app.ts | 566 + platform/lib/github.app.utest.js | 271 + platform/lib/github.signature.ts | 76 + platform/lib/github.signature.utest.js | 611 + platform/lib/github.validation.ts | 204 + platform/lib/github.validation.utest.js | 244 + platform/lib/glob.ts | 32 + platform/lib/glob.utest.js | 35 + platform/lib/googlechat.api.ts | 368 + platform/lib/googlechat.api.utest.js | 511 + platform/lib/googlechat.auth.ts | 316 + platform/lib/googlechat.auth.utest.js | 341 + platform/lib/googlechat.event.ts | 118 + platform/lib/googlechat.event.utest.js | 213 + platform/lib/googlechat.markdown.ts | 385 + platform/lib/googlechat.markdown.utest.js | 398 + platform/lib/googlechat.validation.ts | 86 + platform/lib/googlechat.validation.utest.js | 118 + platform/lib/header.assertion.ts | 276 + platform/lib/header.assertion.utest.js | 230 + platform/lib/header.ts | 315 + platform/lib/header.utest.js | 2206 + platform/lib/helpers.ts | 6 + platform/lib/helpers.utest.js | 78 + platform/lib/highlighter.ts | 210 + platform/lib/highlighter.utest.js | 418 + platform/lib/host.ts | 300 + platform/lib/host.utest.js | 634 + platform/lib/http.ts | 550 + platform/lib/http.utest.js | 843 + platform/lib/hub.ts | 128 + platform/lib/hub.utest.js | 476 + platform/lib/icon.theme.ts | 22 + platform/lib/icon.theme.utest.js | 22 + platform/lib/image.transform.ts | 275 + platform/lib/image.transform.utest.js | 342 + platform/lib/image.ts | 394 + platform/lib/image.utest.js | 866 + platform/lib/instagram.markdown.ts | 338 + platform/lib/instagram.markdown.utest.js | 523 + platform/lib/instruction.convert.ts | 147 + platform/lib/instruction.convert.utest.js | 590 + platform/lib/instruction.extract.automatic.ts | 60 + .../instruction.extract.automatic.utest.js | 45 + platform/lib/instruction.extract.complex.ts | 164 + .../lib/instruction.extract.complex.utest.js | 288 + platform/lib/instruction.extract.simple.ts | 185 + .../lib/instruction.extract.simple.utest.js | 603 + .../lib/instruction.extract.structured.ts | 197 + .../instruction.extract.structured.utest.js | 910 + platform/lib/instruction.extract.template.ts | 283 + .../lib/instruction.extract.template.utest.js | 1194 + platform/lib/instruction.field.ts | 200 + platform/lib/instruction.field.utest.js | 432 + platform/lib/instruction.template.parse.ts | 122 + .../lib/instruction.template.parse.utest.js | 785 + platform/lib/instruction.template.unpack.ts | 20 + .../lib/instruction.template.unpack.utest.js | 84 + .../lib/instruction.transform.automatic.ts | 26 + .../instruction.transform.automatic.utest.js | 17 + platform/lib/instruction.transform.complex.ts | 393 + .../instruction.transform.complex.utest.js | 215 + platform/lib/instruction.transform.simple.js | 524 + .../lib/instruction.transform.simple.utest.js | 2194 + .../lib/instruction.transform.structured.ts | 451 + .../instruction.transform.structured.utest.js | 1242 + .../lib/instruction.transform.template.js | 502 + .../instruction.transform.template.utest.js | 1012 + platform/lib/instruction.transform.types.ts | 49 + .../lib/instruction.transform.types.utest.js | 130 + platform/lib/instruction.type.ts | 253 + platform/lib/instruction.type.utest.js | 463 + platform/lib/integration.api.error.js | 68 + platform/lib/integration.api.error.utest.js | 95 + platform/lib/integration.context.ts | 46 + platform/lib/integration.context.utest.js | 127 + platform/lib/integration.items.js | 302 + platform/lib/integration.items.utest.js | 53 + platform/lib/integration.session.ts | 106 + platform/lib/integration.session.utest.js | 129 + platform/lib/integration.verification.ts | 119 + .../lib/integration.verification.utest.ts | 208 + platform/lib/intent.ts | 492 + platform/lib/intent.utest.js | 383 + platform/lib/intl.ts | 263 + platform/lib/intl.utest.js | 50 + platform/lib/ip.ts | 221 + platform/lib/ip.utest.js | 113 + platform/lib/it.ts | 8 + platform/lib/jmespath.ts | 13 + platform/lib/jmespath.utest.js | 869 + platform/lib/job.ts | 188 + platform/lib/job.utest.js | 875 + platform/lib/joi.handler.ts | 45 + platform/lib/joi.handler.utest.js | 280 + platform/lib/joi.schema.ts | 225 + platform/lib/joi.schema.utest.js | 180 + platform/lib/json.repair.ts | 33 + platform/lib/json.repair.utest.js | 319 + platform/lib/json.ts | 9 + platform/lib/jsonpath.ts | 13 + platform/lib/jsonpath.utest.js | 574 + platform/lib/jsonschema.ts | 128 + platform/lib/jsonschema.utest.js | 153 + platform/lib/jwt.ts | 151 + platform/lib/jwt.utest.js | 166 + platform/lib/keyvalue.ts | 334 + platform/lib/keyvalue.utest.js | 262 + platform/lib/latex.ts | 29 + platform/lib/latex.utest.js | 570 + platform/lib/limit.core.display.utest.js | 347 + platform/lib/limit.core.planless.utest.js | 141 + platform/lib/limit.core.ts | 1156 + platform/lib/limit.core.utest.js | 1949 + platform/lib/limit.estimate.ts | 227 + platform/lib/limit.estimate.utest.js | 498 + platform/lib/limit.handler.ts | 139 + platform/lib/limit.handler.utest.js | 409 + platform/lib/limit.platform.ts | 82 + platform/lib/limit.platform.utest.js | 120 + platform/lib/localhost.ts | 68 + platform/lib/localhost.utest.js | 273 + platform/lib/log.cleanup.ts | 188 + platform/lib/log.cleanup.utest.js | 390 + platform/lib/log.ts | 386 + platform/lib/log.utest.js | 401 + platform/lib/magic.js | 44 + platform/lib/markup.ts | 31 + platform/lib/markup.utest.js | 49 + platform/lib/mcp.direct.ts | 209 + platform/lib/mcp.direct.utest.js | 324 + platform/lib/mcp.edge.ts | 173 + platform/lib/mcp.edge.utest.js | 642 + platform/lib/mcp.error.ts | 32 + platform/lib/mcp.error.utest.js | 90 + platform/lib/mcp.oauth.constants.ts | 3 + platform/lib/mcp.oauth.constants.utest.js | 11 + platform/lib/mcp.oauth.idp.ts | 83 + platform/lib/mcp.oauth.idp.utest.js | 113 + platform/lib/mcp.oauth.ts | 387 + platform/lib/mcp.oauth.utest.js | 451 + platform/lib/mcp.session.ts | 93 + platform/lib/mcp.session.utest.js | 256 + platform/lib/mcp.types.ts | 39 + platform/lib/mcp.types.utest.js | 202 + platform/lib/mcp.widget.ts | 269 + platform/lib/mcp.widget.utest.js | 411 + platform/lib/md.chat.ts | 6 + platform/lib/md.convert.ts | 6 + platform/lib/md.extract.ts | 6 + platform/lib/md.frontmatter.ts | 6 + platform/lib/md.split.ts | 6 + platform/lib/md.table.ts | 6 + platform/lib/memcache.ts | 15 + platform/lib/memcache.utest.js | 54 + platform/lib/memory.search.ts | 122 + platform/lib/memory.search.utest.js | 303 + platform/lib/message.ts | 204 + platform/lib/message.utest.js | 593 + platform/lib/messaging.supersede.ts | 205 + platform/lib/messaging.supersede.utest.js | 196 + platform/lib/messaging.thinking.ts | 99 + platform/lib/messaging.thinking.utest.ts | 98 + platform/lib/messenger.config.js | 1 + platform/lib/messenger.config.utest.js | 18 + platform/lib/messenger.markdown.ts | 488 + platform/lib/messenger.markdown.utest.js | 866 + platform/lib/meta.signature.ts | 33 + platform/lib/meta.signature.utest.js | 53 + platform/lib/meta.ts | 204 + platform/lib/meta.user.ts | 46 + platform/lib/meta.user.utest.js | 128 + platform/lib/meta.utest.js | 90 + platform/lib/method.ts | 687 + platform/lib/method.utest.js | 1533 + platform/lib/metric.ts | 49 + platform/lib/metric.utest.js | 147 + platform/lib/microsoftteams.api.ts | 180 + platform/lib/microsoftteams.api.utest.js | 107 + platform/lib/microsoftteams.auth.ts | 223 + platform/lib/microsoftteams.auth.utest.js | 503 + platform/lib/microsoftteams.markdown.ts | 95 + platform/lib/microsoftteams.markdown.utest.js | 56 + platform/lib/microsoftteams.validation.ts | 76 + .../lib/microsoftteams.validation.utest.js | 135 + platform/lib/mime.ts | 338 + platform/lib/mime.utest.js | 468 + platform/lib/mime2.ts | 139 + platform/lib/mime2.utest.js | 158 + platform/lib/model.access.ts | 49 + platform/lib/model.access.utest.js | 101 + platform/lib/model.context.ts | 83 + platform/lib/model.context.utest.js | 289 + platform/lib/model.credentials.ts | 112 + platform/lib/model.credentials.utest.js | 220 + platform/lib/model.pricing.ts | 122 + platform/lib/model.pricing.utest.js | 276 + .../lib/model.provider.bedrock.adaptor.ts | 72 + .../model.provider.bedrock.adaptor.utest.js | 130 + platform/lib/model.provider.bedrock.conv.ts | 37 + .../lib/model.provider.bedrock.conv.utest.js | 78 + platform/lib/model.provider.bedrock.ts | 82 + platform/lib/model.provider.bedrock.utest.js | 111 + .../lib/model.provider.cloudflare.adaptor.ts | 261 + ...model.provider.cloudflare.adaptor.utest.js | 130 + .../lib/model.provider.cloudflare.conv.ts | 30 + .../model.provider.cloudflare.conv.utest.js | 60 + platform/lib/model.provider.cloudflare.ts | 596 + .../lib/model.provider.cloudflare.utest.js | 828 + .../lib/model.provider.deepseek.adaptor.ts | 72 + .../model.provider.deepseek.adaptor.utest.js | 266 + platform/lib/model.provider.deepseek.conv.ts | 37 + .../lib/model.provider.deepseek.conv.utest.js | 64 + platform/lib/model.provider.deepseek.ts | 49 + platform/lib/model.provider.deepseek.utest.js | 117 + platform/lib/model.provider.groq.adaptor.ts | 72 + .../lib/model.provider.groq.adaptor.utest.js | 200 + platform/lib/model.provider.groq.conv.ts | 134 + .../lib/model.provider.groq.conv.utest.js | 72 + platform/lib/model.provider.groq.ts | 67 + platform/lib/model.provider.groq.utest.js | 119 + .../lib/model.provider.mistral.adaptor.ts | 72 + .../model.provider.mistral.adaptor.utest.js | 270 + platform/lib/model.provider.mistral.conv.ts | 37 + .../lib/model.provider.mistral.conv.utest.js | 61 + platform/lib/model.provider.mistral.ts | 71 + platform/lib/model.provider.mistral.utest.js | 119 + .../lib/model.provider.moonshot.adaptor.ts | 72 + .../model.provider.moonshot.adaptor.utest.js | 176 + platform/lib/model.provider.moonshot.conv.ts | 37 + platform/lib/model.provider.moonshot.ts | 54 + platform/lib/model.provider.moonshot.utest.js | 63 + platform/lib/model.provider.openai.adaptor.ts | 241 + .../model.provider.openai.adaptor.utest.js | 438 + ...l.provider.openai.conv.attachment.utest.js | 92 + .../model.provider.openai.conv.retry.utest.js | 162 + ...rovider.openai.conv.timeout-abort.utest.js | 270 + platform/lib/model.provider.openai.conv.ts | 8265 ++++ .../lib/model.provider.openai.conv.utest.js | 11356 +++++ .../lib/model.provider.openai.image.utest.js | 139 + ...odel.provider.openai.stream-retry.utest.js | 269 + .../lib/model.provider.openai.stream.utest.js | 886 + platform/lib/model.provider.openai.ts | 4460 ++ platform/lib/model.provider.openai.utest.js | 2152 + .../lib/model.provider.openrouter.adaptor.ts | 293 + ...model.provider.openrouter.adaptor.utest.js | 314 + .../lib/model.provider.openrouter.conv.ts | 34 + .../model.provider.openrouter.conv.utest.js | 76 + platform/lib/model.provider.openrouter.ts | 375 + .../lib/model.provider.openrouter.utest.js | 248 + .../lib/model.provider.perplexity.adaptor.ts | 233 + ...model.provider.perplexity.adaptor.utest.js | 340 + .../lib/model.provider.perplexity.conv.ts | 131 + .../model.provider.perplexity.conv.utest.js | 65 + platform/lib/model.provider.perplexity.ts | 49 + .../lib/model.provider.perplexity.utest.js | 123 + platform/lib/model.provider.qwen.adaptor.ts | 72 + .../lib/model.provider.qwen.adaptor.utest.js | 170 + platform/lib/model.provider.qwen.conv.ts | 37 + platform/lib/model.provider.qwen.ts | 54 + platform/lib/model.provider.qwen.utest.js | 59 + platform/lib/model.provider.vercel.adaptor.ts | 530 + .../model.provider.vercel.adaptor.utest.js | 774 + platform/lib/model.provider.vercel.conv.ts | 37 + .../lib/model.provider.vercel.conv.utest.js | 64 + .../lib/model.provider.vercel.image.utest.js | 349 + .../lib/model.provider.vercel.rerank.utest.js | 173 + platform/lib/model.provider.vercel.ts | 1062 + platform/lib/model.provider.vercel.utest.js | 1182 + .../lib/model.provider.vercel.video.utest.js | 202 + platform/lib/model.provider.vertex.adaptor.ts | 229 + .../model.provider.vertex.adaptor.utest.js | 288 + platform/lib/model.provider.vertex.conv.ts | 37 + .../lib/model.provider.vertex.conv.utest.js | 64 + platform/lib/model.provider.vertex.ts | 67 + platform/lib/model.provider.vertex.utest.js | 119 + platform/lib/model.provider.zai.adaptor.ts | 72 + .../lib/model.provider.zai.adaptor.utest.js | 170 + platform/lib/model.provider.zai.conv.ts | 37 + platform/lib/model.provider.zai.ts | 54 + platform/lib/model.provider.zai.utest.js | 59 + platform/lib/model.retry.ts | 145 + platform/lib/model.retry.utest.js | 123 + platform/lib/model.types.ts | 603 + platform/lib/model.utils.js | 2289 + platform/lib/model.utils.utest.js | 1726 + platform/lib/moderation.ts | 5 + platform/lib/moderation.utest.js | 131 + platform/lib/name.icon.ts | 95 + platform/lib/name.icon.utest.js | 124 + platform/lib/namespace.attachment.js | 335 + platform/lib/namespace.attachment.utest.js | 559 + platform/lib/namespace.safe.ts | 17 + platform/lib/namespace.safe.utest.js | 299 + platform/lib/nextjs.config.rewrites.js | 110 + platform/lib/nextjs.config.rewrites.utest.js | 180 + platform/lib/notify.tsx | 489 + platform/lib/notify.utest.js | 347 + platform/lib/number.ts | 86 + platform/lib/number.utest.js | 246 + platform/lib/oauth.authorization.ts | 603 + platform/lib/oauth.authorization.utest.js | 1097 + platform/lib/oauth.connection.idp.ts | 256 + platform/lib/oauth.connection.idp.utest.js | 157 + platform/lib/oauth.discovery.ts | 541 + platform/lib/oauth.discovery.utest.js | 1003 + platform/lib/oauth.jwt.ts | 1213 + platform/lib/oauth.jwt.utest.js | 927 + platform/lib/oauth.pkce.ts | 191 + platform/lib/oauth.pkce.utest.js | 241 + platform/lib/oauth.registration.ts | 278 + platform/lib/oauth.registration.utest.js | 523 + platform/lib/oauth.revoke.ts | 51 + platform/lib/oauth.revoke.utest.js | 304 + platform/lib/oauth.server.js | 658 + platform/lib/oauth.server.utest.js | 1332 + platform/lib/oauth.token.js | 487 + platform/lib/oauth.token.utest.js | 456 + platform/lib/object.js | 526 + platform/lib/object.utest.js | 1045 + platform/lib/og.ts | 14 + platform/lib/og.utest.js | 114 + platform/lib/partner.auth.ts | 420 + platform/lib/partner.auth.utest.js | 938 + platform/lib/partner.helpers.ts | 101 + platform/lib/partner.helpers.utest.js | 421 + platform/lib/path.ts | 8 + platform/lib/pii.ts | 10 + platform/lib/plan.compare.ts | 270 + platform/lib/plan.compare.utest.js | 289 + platform/lib/plan.label.ts | 29 + platform/lib/plan.label.utest.js | 22 + platform/lib/policy.config.ts | 37 + platform/lib/policy.config.utest.js | 127 + platform/lib/policy.text.ts | 112 + platform/lib/policy.text.utest.js | 272 + platform/lib/policy.text.utest.ts | 102 + platform/lib/portal.auth.ts | 580 + platform/lib/portal.auth.utest.js | 766 + platform/lib/portal.config.ts | 45 + platform/lib/portal.config.utest.js | 188 + platform/lib/portal.hostname.ts | 63 + platform/lib/portal.hostname.utest.js | 286 + platform/lib/portal.slug.ts | 36 + platform/lib/portal.slug.utest.js | 202 + platform/lib/promise.ts | 132 + platform/lib/promise.utest.js | 305 + platform/lib/prompt.ts | 371 + platform/lib/prompt.utest.js | 552 + platform/lib/query.get.ts | 105 + platform/lib/query.get.utest.js | 635 + platform/lib/query.helpers.ts | 12 + platform/lib/query.helpers.utest.js | 270 + platform/lib/query.ts | 29 + platform/lib/query.utest.js | 276 + platform/lib/queue.ts | 224 + platform/lib/queue.utest.js | 196 + platform/lib/queue2.ts | 295 + platform/lib/queue2.utest.js | 673 + platform/lib/random.ts | 19 + platform/lib/random.utest.js | 176 + platform/lib/rate.ts | 107 + platform/lib/rate.utest.js | 265 + platform/lib/ratelimit.ts | 25 + platform/lib/ratelimit.utest.js | 63 + platform/lib/rating.ts | 30 + platform/lib/rating.utest.js | 440 + platform/lib/react.ts | 52 + platform/lib/react.utest.js | 183 + platform/lib/react.utest.jsx | 269 + platform/lib/realtime.client.ts | 656 + platform/lib/realtime.client.utest.js | 491 + platform/lib/realtime.client.utest.ts | 388 + platform/lib/realtime.session.ts | 37 + platform/lib/realtime.session.utest.js | 72 + platform/lib/recall.bot.ts | 539 + platform/lib/recall.bot.utest.js | 466 + platform/lib/recall.constants.ts | 22 + platform/lib/recall.constants.utest.js | 35 + platform/lib/recall.schemas.ts | 384 + platform/lib/recall.schemas.utest.js | 644 + platform/lib/recall.screenshare.ts | 103 + platform/lib/recall.screenshare.utest.js | 237 + platform/lib/recall.session.ts | 120 + platform/lib/recall.session.utest.js | 169 + platform/lib/recall.signature.ts | 75 + platform/lib/recall.signature.utest.js | 153 + platform/lib/record.ts | 231 + platform/lib/record.utest.js | 386 + platform/lib/redact.entropy.ts | 426 + platform/lib/redact.entropy.utest.js | 656 + platform/lib/redact.secrets.ts | 93 + platform/lib/redact.secrets.utest.js | 80 + platform/lib/regex.ts | 8 + platform/lib/rehype.plugins.ts | 135 + platform/lib/rehype.plugins.utest.js | 578 + platform/lib/report.ts | 5403 +++ platform/lib/report.utest.js | 1054 + platform/lib/request.ts | 26 + platform/lib/request.utest.js | 433 + platform/lib/rerank.ts | 90 + platform/lib/rerank.utest.js | 68 + platform/lib/response.js | 789 + platform/lib/response.utest.js | 1149 + platform/lib/result.ts | 17 + platform/lib/result.utest.js | 266 + platform/lib/rss.ts | 73 + platform/lib/rss.utest.js | 331 + platform/lib/runas.ts | 45 + platform/lib/runas.utest.js | 268 + platform/lib/sandbox.shell.ts | 510 + platform/lib/sandbox.shell.utest.js | 414 + platform/lib/satori.ts | 49 + platform/lib/satori.utest.js | 319 + platform/lib/save.ts | 162 + platform/lib/save.utest.js | 396 + platform/lib/schedule.ts | 49 + platform/lib/schedule.utest.js | 165 + platform/lib/scope.client.ts | 8 + platform/lib/scope.client.utest.js | 5 + platform/lib/scope.server.ts | 8 + platform/lib/scope.server.utest.js | 5 + platform/lib/script.ts | 3 + platform/lib/scroll.manager.ts | 523 + platform/lib/scroll.manager.utest.js | 93 + platform/lib/secret.access.ts | 70 + platform/lib/secret.access.utest.js | 122 + platform/lib/secret.authenticate.ts | 82 + platform/lib/secret.authenticate.utest.ts | 96 + platform/lib/secret.authorize.ts | 74 + platform/lib/secret.authorize.utest.js | 123 + platform/lib/secret.constants.ts | 35 + platform/lib/secret.constants.utest.js | 115 + platform/lib/secret.extract.ts | 25 + platform/lib/secret.extract.utest.js | 80 + platform/lib/secret.icon.ts | 96 + platform/lib/secret.icon.utest.js | 86 + platform/lib/secret.manager.ts | 486 + platform/lib/secret.manager.utest.js | 758 + platform/lib/secret.mint.ts | 151 + platform/lib/secret.mint.utest.js | 188 + platform/lib/secret.name.ts | 25 + platform/lib/secret.name.utest.js | 194 + platform/lib/secret.oauth.ts | 643 + platform/lib/secret.oauth.utest.js | 2078 + platform/lib/secret.proxy.ts | 279 + platform/lib/secret.proxy.utest.js | 344 + platform/lib/secret.reference.ts | 112 + platform/lib/secret.reference.utest.js | 1022 + platform/lib/secret.template.ts | 225 + platform/lib/secret.template.utest.js | 1406 + platform/lib/secret.value.ts | 890 + platform/lib/secret.value.utest.js | 3820 ++ platform/lib/security.headers.js | 358 + platform/lib/security.headers.utest.js | 229 + platform/lib/selector.ts | 43 + platform/lib/selector.utest.js | 19 + platform/lib/session.context.ts | 119 + platform/lib/session.context.utest.js | 237 + platform/lib/session.duration.ts | 47 + platform/lib/session.duration.utest.js | 47 + platform/lib/session.file.ts | 224 + platform/lib/session.file.utest.js | 365 + platform/lib/session.get.js | 1114 + platform/lib/session.get.utest.js | 2700 ++ platform/lib/session.handler.ts | 106 + platform/lib/session.handler.utest.js | 429 + platform/lib/session.temp.ts | 223 + platform/lib/session.temp.utest.js | 1152 + platform/lib/short.ts | 102 + platform/lib/short.utest.js | 240 + platform/lib/signature.url.ts | 188 + platform/lib/signature.url.utest.js | 442 + platform/lib/skillserver.ts | 192 + platform/lib/skillserver.utest.js | 339 + platform/lib/skillset.abilities.ts | 36 + platform/lib/skillset.abilities.utest.js | 49 + platform/lib/skillset.access.ts | 52 + platform/lib/skillset.access.utest.js | 93 + platform/lib/skillset.apply.js | 681 + platform/lib/skillset.apply.utest.js | 2081 + platform/lib/skillset.chunk.ts | 221 + platform/lib/skillset.chunk.utest.js | 423 + platform/lib/skillset.delete.ts | 38 + platform/lib/skillset.delete.utest.js | 284 + platform/lib/slack.channel.ts | 401 + platform/lib/slack.channel.utest.js | 1023 + platform/lib/slack.manifest.ts | 195 + platform/lib/slack.manifest.utest.js | 997 + platform/lib/slack.markdown.ts | 1176 + platform/lib/slack.markdown.utest.js | 2046 + platform/lib/slack.references.ts | 150 + platform/lib/slack.references.utest.js | 448 + platform/lib/slack.signature.ts | 88 + platform/lib/slack.signature.utest.js | 338 + platform/lib/slack.types.ts | 34 + platform/lib/slack.types.utest.js | 7 + platform/lib/slack.user.ts | 112 + platform/lib/slack.user.utest.js | 595 + platform/lib/slack.validation.ts | 165 + platform/lib/slack.validation.utest.js | 254 + platform/lib/slug.js | 742 + platform/lib/slug.utest.js | 61 + platform/lib/solution.js | 811 + platform/lib/solution.utest.js | 418 + platform/lib/sort.ts | 101 + platform/lib/sort.utest.js | 180 + platform/lib/source.ts | 41 + platform/lib/source.utest.js | 374 + platform/lib/space.access.ts | 8 + platform/lib/space.access.utest.js | 97 + platform/lib/space.delete.ts | 42 + platform/lib/space.delete.utest.js | 213 + platform/lib/space.site.serve.ts | 531 + platform/lib/space.site.serve.utest.ts | 263 + platform/lib/space.site.ts | 104 + platform/lib/space.site.utest.js | 68 + platform/lib/space.storage.async.utest.js | 264 + platform/lib/space.storage.ts | 881 + platform/lib/space.storage.utest.js | 1350 + platform/lib/stale.ts | 185 + platform/lib/stale.utest.js | 106 + platform/lib/static.ts | 247 + platform/lib/static.utest.js | 371 + platform/lib/storage.ts | 10 + platform/lib/store.filter.ts | 42 + platform/lib/store.filter.utest.js | 379 + platform/lib/store.memory.ts | 368 + platform/lib/store.memory.utest.js | 617 + platform/lib/store.types.ts | 306 + platform/lib/store.types.utest.js | 123 + platform/lib/store.vector.ts | 312 + platform/lib/store.vector.utest.js | 329 + platform/lib/stream.ts | 885 + platform/lib/stream.utest.js | 319 + platform/lib/string.ts | 8 + platform/lib/struct.ts | 8 + platform/lib/structstr.ts | 123 + platform/lib/structstr.utest.js | 295 + platform/lib/swagger.ts | 110 + platform/lib/swagger.utest.js | 325 + platform/lib/system.metrics.behavior.utest.js | 35 + platform/lib/system.metrics.ts | 17 + platform/lib/system.metrics.utest.js | 138 + platform/lib/task.access.ts | 8 + platform/lib/task.access.utest.js | 98 + platform/lib/task.schedule.ts | 102 + platform/lib/task.schedule.utest.js | 188 + platform/lib/task.validation.ts | 46 + platform/lib/task.validation.utest.js | 108 + platform/lib/task.workflow.channel.ts | 145 + platform/lib/task.workflow.channel.utest.js | 350 + platform/lib/team.get.ts | 103 + platform/lib/team.get.utest.js | 181 + platform/lib/telegram.config.js | 1 + platform/lib/telegram.config.utest.js | 18 + platform/lib/telegram.markdown.ts | 566 + platform/lib/telegram.markdown.utest.js | 335 + platform/lib/telegram.signature.ts | 40 + platform/lib/telegram.signature.utest.js | 46 + platform/lib/telegram.validation.ts | 134 + platform/lib/telegram.validation.utest.js | 159 + platform/lib/telegram.webhook.ts | 12 + platform/lib/telegram.webhook.utest.js | 115 + platform/lib/temp.file.ts | 291 + platform/lib/temp.file.utest.js | 546 + platform/lib/template.ts | 38 + platform/lib/template.utest.js | 67 + platform/lib/text.search.ts | 127 + platform/lib/text.search.utest.js | 35 + platform/lib/theme.ts | 364 + platform/lib/theme.utest.js | 304 + platform/lib/thread.ts | 1238 + platform/lib/thread.utest.js | 3214 ++ platform/lib/timeout.monitor.ts | 145 + platform/lib/timeout.monitor.utest.js | 562 + platform/lib/toast.ts | 7 + platform/lib/toast.utest.js | 5 + platform/lib/token.ts | 354 + platform/lib/token.utest.js | 755 + platform/lib/tool.environment.ts | 505 + platform/lib/tool.environment.utest.js | 1162 + platform/lib/transform.ts | 1495 + platform/lib/transform.utest.js | 1808 + platform/lib/twilio.api.ts | 354 + platform/lib/twilio.api.utest.js | 661 + platform/lib/twilio.markdown.ts | 308 + platform/lib/twilio.markdown.utest.js | 114 + platform/lib/twilio.phone.ts | 70 + platform/lib/twilio.phone.utest.js | 49 + platform/lib/twilio.phone.utest.ts | 66 + platform/lib/twilio.signature.ts | 45 + platform/lib/twilio.signature.utest.js | 77 + platform/lib/twilio.twiml.ts | 82 + platform/lib/twilio.twiml.utest.js | 76 + platform/lib/twilio.validation.ts | 84 + platform/lib/twilio.validation.utest.js | 64 + platform/lib/twilio.voice.ts | 23 + platform/lib/twilio.voice.utest.js | 45 + platform/lib/twilio.webhook.ts | 21 + platform/lib/twilio.webhook.utest.js | 198 + platform/lib/ua.ts | 7 + platform/lib/ua.utest.js | 49 + platform/lib/unfurl.fetch.ts | 48 + platform/lib/unfurl.fetch.utest.js | 478 + platform/lib/unfurl.url.ts | 43 + platform/lib/unfurl.url.utest.js | 279 + platform/lib/upload.ts | 28 + platform/lib/upload.utest.js | 201 + platform/lib/url.js | 410 + platform/lib/url.utest.js | 149 + platform/lib/url3.ts | 38 + platform/lib/url3.utest.js | 371 + platform/lib/usage.cleanup.ts | 67 + platform/lib/usage.cleanup.utest.js | 279 + platform/lib/usage.get.ts | 331 + platform/lib/usage.get.utest.js | 426 + platform/lib/usage.model.ts | 377 + platform/lib/usage.model.utest.js | 1084 + platform/lib/usage.period.ts | 76 + platform/lib/usage.period.utest.js | 403 + platform/lib/usage.policy.ts | 380 + platform/lib/usage.policy.utest.js | 654 + platform/lib/usage.record.ts | 991 + platform/lib/usage.record.utest.js | 1665 + platform/lib/usage.types.ts | 38 + platform/lib/usage.types.utest.js | 59 + platform/lib/user.delete.ts | 242 + platform/lib/user.delete.utest.js | 638 + platform/lib/user.find.js | 580 + platform/lib/user.find.utest.js | 378 + platform/lib/user.get.ts | 249 + platform/lib/user.get.utest.js | 761 + platform/lib/user.handler.ts | 38 + platform/lib/user.handler.utest.js | 410 + platform/lib/user.identity.ts | 29 + platform/lib/user.identity.utest.js | 24 + platform/lib/user.limits.ts | 63 + platform/lib/user.limits.utest.js | 190 + platform/lib/user.plan.ts | 76 + platform/lib/user.plan.utest.js | 362 + platform/lib/user.relation.ts | 40 + platform/lib/user.relation.utest.js | 378 + platform/lib/user.session.ts | 47 + platform/lib/user.session.utest.js | 148 + platform/lib/user.type.ts | 124 + platform/lib/user.type.utest.js | 338 + platform/lib/util.ts | 7 + platform/lib/util.utest.js | 280 + platform/lib/uuid.ts | 17 + platform/lib/uuid.utest.js | 75 + platform/lib/video.ts | 386 + platform/lib/video.utest.js | 165 + platform/lib/webcrypto.ts | 226 + platform/lib/webcrypto.utest.js | 329 + platform/lib/webshot.ts | 55 + platform/lib/webshot.utest.js | 87 + platform/lib/whatsapp.config.js | 1 + platform/lib/whatsapp.config.utest.js | 18 + platform/lib/whatsapp.markdown.ts | 476 + platform/lib/whatsapp.markdown.utest.js | 1166 + platform/lib/whatsapp.signature.ts | 57 + platform/lib/whatsapp.signature.utest.js | 96 + platform/lib/whatsapp.validation.ts | 94 + platform/lib/whatsapp.validation.utest.js | 160 + platform/lib/widget.ts | 50 + platform/lib/widget.utest.js | 141 + platform/lib/workflow.ts | 858 + platform/lib/workflow.utest.js | 2375 ++ platform/lib/yaml.repair.ts | 33 + platform/lib/yaml.repair.utest.js | 889 + platform/lib/yaml.ts | 8 + platform/lib/youtube.ts | 92 + platform/lib/youtube.utest.js | 372 + platform/lib/zlib.ts | 1 + platform/lib/zlib.utest.js | 157 + platform/lib/zod.error.ts | 8 + platform/lib/zod.error.utest.js | 277 + platform/lib/zod.error.utest.ts | 141 + platform/lib/zod.handler.ts | 58 + platform/lib/zod.handler.utest.js | 391 + platform/lib/zod.jsonschema.ts | 87 + platform/lib/zod.jsonschema.utest.js | 191 + platform/lib/zod.schema.ts | 222 + platform/lib/zod.schema.utest.js | 408 + platform/lib/zod.schema.utest.ts | 1518 + platform/lib/zod.test.ts | 14 + platform/lib/zod.test.utest.js | 120 + platform/lib/zod.types.ts | 16 + platform/lib/zod.types.utest.js | 222 + platform/lib/zod.types.utest.ts | 463 + platform/next-sitemap.config.js | 153 + platform/next.config.d/actions.config.js | 40 + .../next.config.d/actions.config.utest.js | 49 + platform/next.config.d/api.config.js | 170 + platform/next.config.d/api.config.utest.js | 257 + platform/next.config.d/apps.config.js | 343 + platform/next.config.d/base.config.js | 7 + platform/next.config.d/bundling.config.js | 58 + platform/next.config.d/content.config.js | 11 + .../next.config.d/content.config.utest.js | 15 + platform/next.config.d/cq.config.js | 8 + platform/next.config.d/discovery.config.js | 22 + platform/next.config.d/embeds.config.js | 98 + platform/next.config.d/env.config.js | 52 + platform/next.config.d/host.config.js | 25 + platform/next.config.d/i18n.config.js | 24 + platform/next.config.d/images.config.js | 21 + platform/next.config.d/llms.config.js | 118 + platform/next.config.d/modularize.config.js | 41 + platform/next.config.d/oauth.config.js | 60 + platform/next.config.d/output.config.js | 32 + platform/next.config.d/partner.config.js | 174 + platform/next.config.d/portals.config.js | 87 + platform/next.config.d/profiling.config.js | 19 + platform/next.config.d/security.config.js | 152 + .../next.config.d/security.config.utest.js | 55 + platform/next.config.d/seo.config.js | 51 + platform/next.config.d/short.config.js | 23 + platform/next.config.d/spaces.config.js | 61 + platform/next.config.d/static.config.js | 75 + platform/next.config.d/static.config.utest.js | 123 + platform/next.config.d/transpile.config.js | 266 + platform/next.config.d/zone.config.js | 350 + platform/next.config.d/zone.config.utest.js | 79 + platform/next.config.js | 535 + platform/package.json | 456 + platform/pages/404.jsx | 89 + platform/pages/500.jsx | 53 + platform/pages/_app.jsx | 5 + platform/pages/_document.jsx | 142 + platform/pages/_error.jsx | 18 + .../pages/abilities/[abilityId]/index.jsx | 51 + platform/pages/abilities/index.jsx | 25 + platform/pages/admin/index.jsx | 34 + platform/pages/admin/system/index.jsx | 100 + .../admin/users/[userId]/_index.utest.js | 189 + platform/pages/admin/users/[userId]/index.jsx | 301 + platform/pages/admin/users/index.jsx | 202 + platform/pages/alerts/index.jsx | 229 + platform/pages/analytics/index.jsx | 1345 + .../api/.well-known/_api-catalog.utest.js | 49 + .../_microsoft-identity-association.utest.js | 47 + platform/pages/api/.well-known/api-catalog.ts | 52 + .../microsoft-identity-association.json.ts | 26 + platform/pages/api/404.js | 7 + platform/pages/api/admin/debug/header.ts | 31 + platform/pages/api/admin/debug/throw.ts | 8 + .../api/admin/user/[userId]/_delete.utest.js | 138 + .../api/admin/user/[userId]/_switch.utest.js | 324 + .../pages/api/admin/user/[userId]/delete.js | 51 + .../pages/api/admin/user/[userId]/switch.js | 66 + .../api/admin/user/[userId]/usage/adjust.js | 58 + platform/pages/api/auth/[...nextauth].js | 350 + platform/pages/api/auth/_nextauth.utest.js | 201 + platform/pages/api/auxiliary/README.md | 59 + .../api/auxiliary/dataset/_chunk.utest.js | 428 + platform/pages/api/auxiliary/dataset/chunk.js | 191 + .../pages/api/auxiliary/playground/ability.ts | 47 + .../oauth/pipedream/_authorize.utest.js | 837 + .../secret/oauth/pipedream/_revoke.utest.js | 286 + .../secret/oauth/pipedream/_token.utest.js | 508 + .../secret/oauth/pipedream/_validate.utest.js | 456 + .../secret/oauth/pipedream/authorize.ts | 752 + .../secret/oauth/pipedream/revoke.ts | 189 + .../auxiliary/secret/oauth/pipedream/token.ts | 368 + .../secret/oauth/pipedream/validate.ts | 215 + .../skillset/ability/apollo/handler.ts | 174 + .../skillset/ability/atlassian/proxy.ts | 161 + .../skillset/ability/attio/_sql.utest.js | 621 + .../auxiliary/skillset/ability/attio/sql.ts | 454 + .../chatbotkit/dataset/_search.utest.js | 128 + .../ability/chatbotkit/dataset/create.ts | 29 + .../chatbotkit/dataset/file/_attach.utest.js | 387 + .../ability/chatbotkit/dataset/file/attach.ts | 84 + .../dataset/record/_create.utest.js | 112 + .../dataset/record/_delete.utest.js | 114 + .../dataset/record/_update.utest.js | 115 + .../chatbotkit/dataset/record/create.ts | 33 + .../chatbotkit/dataset/record/delete.ts | 31 + .../chatbotkit/dataset/record/update.ts | 34 + .../ability/chatbotkit/dataset/search.ts | 31 + .../ability/chatbotkit/file/_sql.utest.js | 71 + .../skillset/ability/chatbotkit/file/sql.ts | 244 + .../chatbotkit/integration/_anam.utest.js | 185 + .../chatbotkit/integration/_avatar.utest.js | 185 + .../chatbotkit/integration/_discord.utest.js | 236 + .../chatbotkit/integration/_email.utest.js | 202 + .../chatbotkit/integration/_recall.utest.js | 376 + .../chatbotkit/integration/_slack.utest.js | 272 + .../chatbotkit/integration/_telegram.utest.js | 236 + .../chatbotkit/integration/_twilio.utest.js | 71 + .../chatbotkit/integration/_whatsapp.utest.js | 270 + .../ability/chatbotkit/integration/anam.ts | 87 + .../ability/chatbotkit/integration/avatar.ts | 89 + .../ability/chatbotkit/integration/discord.ts | 121 + .../ability/chatbotkit/integration/email.ts | 102 + .../ability/chatbotkit/integration/github.ts | 463 + .../chatbotkit/integration/googlechat.ts | 115 + .../chatbotkit/integration/instagram.ts | 123 + .../chatbotkit/integration/messenger.ts | 118 + .../chatbotkit/integration/microsoftteams.ts | 124 + .../ability/chatbotkit/integration/recall.ts | 123 + .../ability/chatbotkit/integration/slack.ts | 117 + .../chatbotkit/integration/telegram.ts | 123 + .../ability/chatbotkit/integration/twilio.ts | 393 + .../chatbotkit/integration/whatsapp.ts | 132 + .../chatbotkit/math/_evaluate.utest.js | 169 + .../ability/chatbotkit/math/evaluate.ts | 28 + .../chatbotkit/mcp/tool/_call.utest.js | 294 + .../chatbotkit/mcp/tool/_install.utest.js | 296 + .../ability/chatbotkit/mcp/tool/call.ts | 89 + .../ability/chatbotkit/mcp/tool/install.ts | 113 + .../ability/chatbotkit/url/_git.utest.js | 1455 + .../ability/chatbotkit/url/_sql.utest.js | 131 + .../ability/chatbotkit/url/_unfurl.utest.js | 111 + .../skillset/ability/chatbotkit/url/git.ts | 807 + .../ability/chatbotkit/url/screenshot.ts | 96 + .../skillset/ability/chatbotkit/url/sql.ts | 245 + .../skillset/ability/chatbotkit/url/unfurl.ts | 29 + .../skillset/ability/clawhub/handler.ts | 205 + .../ability/docusign/esignature/sql.ts | 373 + .../ability/financialmodelingprep/proxy.ts | 62 + .../skillset/ability/github/tools.ts | 128 + .../ability/gohighlevel/_sql.utest.js | 255 + .../skillset/ability/gohighlevel/sql.ts | 119 + .../skillset/ability/google/_ads.utest.js | 373 + .../ability/google/_calendar.utest.js | 193 + .../skillset/ability/google/_drive.utest.js | 329 + .../skillset/ability/google/_mail.utest.js | 2513 ++ .../skillset/ability/google/_sheets.utest.js | 800 + .../auxiliary/skillset/ability/google/ads.ts | 458 + .../skillset/ability/google/calendar.ts | 730 + .../auxiliary/skillset/ability/google/docs.ts | 809 + .../skillset/ability/google/drive.ts | 843 + .../auxiliary/skillset/ability/google/mail.ts | 1603 + .../auxiliary/skillset/ability/google/meet.ts | 258 + .../skillset/ability/google/sheets.ts | 530 + .../skillset/ability/hubspot/_sql.utest.js | 418 + .../auxiliary/skillset/ability/hubspot/sql.ts | 251 + .../skillset/ability/instantly/_sql.utest.js | 440 + .../skillset/ability/instantly/proxy.ts | 62 + .../skillset/ability/instantly/sql.ts | 439 + .../skillset/ability/matillion/_sql.utest.js | 460 + .../skillset/ability/matillion/sql.ts | 452 + .../skillset/ability/notion/_search.utest.js | 230 + .../ability/notion/database/item/create.ts | 62 + .../ability/notion/database/item/delete.ts | 50 + .../ability/notion/database/item/fetch.ts | 52 + .../ability/notion/database/item/list.ts | 58 + .../ability/notion/database/item/update.ts | 68 + .../skillset/ability/notion/database/list.ts | 52 + .../notion/database/property/introspect.ts | 44 + .../notion/database/property/search.ts | 77 + .../skillset/ability/notion/page/fetch.ts | 46 + .../skillset/ability/notion/page/list.ts | 52 + .../skillset/ability/notion/search.ts | 54 + .../skillset/ability/nubela/proxy.ts | 64 + .../overpass/amenity/around/_list.utest.js | 232 + .../overpass/amenity/around/list.ts | 92 + .../skillset/ability/postgrest/rpc.ts | 55 + .../skillset/ability/postgrest/sql.ts | 60 + .../skillset/ability/reddit/_post.utest.js | 456 + .../auxiliary/skillset/ability/reddit/post.ts | 393 + .../skillset/ability/serpapi/proxy.ts | 64 + .../skillset/ability/skillssh/handler.ts | 83 + .../skillset/ability/space/_skill.utest.js | 184 + .../auxiliary/skillset/ability/space/skill.ts | 456 + .../skillset/ability/supabase/rpc.ts | 53 + .../skillset/ability/supabase/sql.ts | 58 + .../skillset/ability/twitter/_sql.utest.js | 300 + .../auxiliary/skillset/ability/twitter/sql.ts | 345 + .../skillset/ability/usefind/proxy.ts | 114 + .../ability/xero/accounting/_sql.utest.js | 423 + .../skillset/ability/xero/accounting/sql.ts | 936 + .../skillset/ability/zendesk/_sql.utest.js | 370 + .../auxiliary/skillset/ability/zendesk/sql.ts | 246 + platform/pages/api/billing/_checkout.utest.js | 246 + platform/pages/api/billing/_event.utest.js | 427 + platform/pages/api/billing/checkout.js | 128 + platform/pages/api/billing/event.js | 176 + platform/pages/api/billing/session.js | 73 + platform/pages/api/billing/skip.js | 72 + platform/pages/api/index.js | 7 + .../api/me/team/[teamId]/_switch.utest.js | 181 + platform/pages/api/me/team/[teamId]/switch.ts | 102 + platform/pages/api/me/team/list.ts | 44 + platform/pages/api/me/team/unswitch.ts | 40 + .../api/me/user/[userId]/_switch.utest.js | 164 + platform/pages/api/me/user/[userId]/switch.ts | 84 + platform/pages/api/me/user/list.ts | 32 + platform/pages/api/me/user/unswitch.ts | 27 + platform/pages/api/oauth/_authorize.utest.js | 326 + platform/pages/api/oauth/_revoke.utest.js | 140 + platform/pages/api/oauth/_token.utest.js | 109 + platform/pages/api/oauth/authorize.js | 121 + .../api/oauth/connection/_callback.utest.js | 499 + .../pages/api/oauth/connection/callback.ts | 430 + platform/pages/api/oauth/refresh.js | 73 + platform/pages/api/oauth/revoke.js | 41 + .../pages/api/oauth/secret/_callback.utest.js | 124 + platform/pages/api/oauth/secret/callback.js | 18 + platform/pages/api/oauth/token.js | 71 + platform/pages/api/session/_queue.utest.js | 122 + platform/pages/api/session/queue.js | 67 + .../api/short/[shortId]/_redirect.utest.js | 96 + .../pages/api/short/[shortId]/redirect.js | 17 + .../pages/api/system/clock/_queue.utest.js | 136 + platform/pages/api/system/clock/queue.js | 420 + .../api/system/queue/callback/_post.utest.js | 149 + .../queue/callback/failure/_post.utest.js | 229 + .../api/system/queue/callback/failure/post.js | 76 + .../pages/api/system/queue/callback/post.js | 26 + platform/pages/api/system/status/fetch.js | 13 + platform/pages/api/system/status/ping.js | 7 + .../pages/api/user/[userId]/_queue.utest.js | 316 + platform/pages/api/user/[userId]/queue.js | 410 + platform/pages/api/v1/_index.utest.js | 29 + platform/pages/api/v1/_spec.utest.js | 86 + .../v1/ability/[abilityId]/_delete.utest.js | 172 + .../v1/ability/[abilityId]/_fetch.utest.js | 252 + .../v1/ability/[abilityId]/_update.utest.js | 215 + .../api/v1/ability/[abilityId]/delete.js | 40 + .../pages/api/v1/ability/[abilityId]/fetch.js | 78 + .../api/v1/ability/[abilityId]/update.js | 143 + .../pages/api/v1/ability/_create.utest.js | 201 + .../pages/api/v1/ability/_export.utest.js | 171 + platform/pages/api/v1/ability/_list.utest.js | 298 + platform/pages/api/v1/ability/create.js | 134 + platform/pages/api/v1/ability/export.js | 102 + platform/pages/api/v1/ability/list.js | 85 + .../pages/api/v1/audit/log/_list.utest.js | 265 + .../pages/api/v1/audit/log/_queue.utest.js | 210 + platform/pages/api/v1/audit/log/export.js | 393 + platform/pages/api/v1/audit/log/list.js | 293 + platform/pages/api/v1/audit/log/queue.js | 58 + .../blueprint/[blueprintId]/_clone.utest.js | 484 + .../blueprint/[blueprintId]/_delete.utest.js | 132 + .../blueprint/[blueprintId]/_fetch.utest.js | 123 + .../blueprint/[blueprintId]/_update.utest.js | 161 + .../[blueprintId]/bulletin/_create.utest.js | 124 + .../[blueprintId]/bulletin/_list.utest.js | 146 + .../[blueprintId]/bulletin/create.js | 157 + .../blueprint/[blueprintId]/bulletin/list.js | 180 + .../api/v1/blueprint/[blueprintId]/clone.js | 335 + .../api/v1/blueprint/[blueprintId]/delete.js | 148 + .../api/v1/blueprint/[blueprintId]/fetch.js | 123 + .../[blueprintId]/resource/_export.utest.js | 592 + .../[blueprintId]/resource/_import.utest.js | 1632 + .../[blueprintId]/resource/_list.utest.js | 258 + .../[blueprintId]/resource/export.js | 216 + .../[blueprintId]/resource/import.js | 312 + .../blueprint/[blueprintId]/resource/list.js | 246 + .../api/v1/blueprint/[blueprintId]/update.js | 171 + .../pages/api/v1/blueprint/_create.utest.js | 262 + .../pages/api/v1/blueprint/_list.utest.js | 286 + platform/pages/api/v1/blueprint/create.js | 155 + platform/pages/api/v1/blueprint/list.js | 173 + .../pages/api/v1/bot/[botId]/_clone.utest.js | 197 + .../pages/api/v1/bot/[botId]/_delete.utest.js | 152 + .../api/v1/bot/[botId]/_downvote.utest.js | 243 + .../pages/api/v1/bot/[botId]/_fetch.utest.js | 281 + .../pages/api/v1/bot/[botId]/_update.utest.js | 376 + .../pages/api/v1/bot/[botId]/_upvote.utest.js | 248 + .../api/v1/bot/[botId]/access/_fetch.utest.js | 98 + .../v1/bot/[botId]/access/_unblock.utest.js | 85 + .../pages/api/v1/bot/[botId]/access/fetch.js | 40 + .../api/v1/bot/[botId]/access/unblock.js | 41 + platform/pages/api/v1/bot/[botId]/clone.js | 149 + platform/pages/api/v1/bot/[botId]/delete.js | 120 + platform/pages/api/v1/bot/[botId]/downvote.js | 155 + platform/pages/api/v1/bot/[botId]/fetch.js | 148 + .../v1/bot/[botId]/memory/_search.utest.js | 402 + .../pages/api/v1/bot/[botId]/memory/search.js | 167 + .../realtime/websocket/_create.utest.js | 252 + .../bot/[botId]/realtime/websocket/create.js | 305 + .../v1/bot/[botId]/session/_create.utest.js | 363 + .../api/v1/bot/[botId]/session/create.js | 437 + platform/pages/api/v1/bot/[botId]/update.js | 220 + platform/pages/api/v1/bot/[botId]/upvote.js | 166 + .../api/v1/bot/[botId]/usage/_fetch.utest.js | 364 + .../pages/api/v1/bot/[botId]/usage/fetch.js | 331 + platform/pages/api/v1/bot/_create.utest.js | 528 + platform/pages/api/v1/bot/_list.utest.js | 496 + platform/pages/api/v1/bot/create.js | 210 + platform/pages/api/v1/bot/list.js | 203 + .../v1/channel/[channelId]/_publish.utest.js | 155 + .../channel/[channelId]/_subscribe.utest.js | 283 + .../api/v1/channel/[channelId]/publish.js | 176 + .../api/v1/channel/[channelId]/subscribe.js | 241 + .../v1/contact/[contactId]/_delete.utest.js | 224 + .../v1/contact/[contactId]/_fetch.utest.js | 206 + .../v1/contact/[contactId]/_update.utest.js | 290 + .../[contactId]/conversation/_list.utest.js | 244 + .../contact/[contactId]/conversation/list.js | 215 + .../api/v1/contact/[contactId]/delete.js | 139 + .../pages/api/v1/contact/[contactId]/fetch.js | 171 + .../contact/[contactId]/memory/_list.utest.js | 158 + .../[contactId]/memory/_search.utest.js | 224 + .../api/v1/contact/[contactId]/memory/list.js | 214 + .../v1/contact/[contactId]/memory/search.js | 231 + .../api/v1/contact/[contactId]/rating/list.js | 281 + .../secret/[secretId]/_authenticate.utest.js | 284 + .../secret/[secretId]/_mint.utest.js | 110 + .../secret/[secretId]/_proxy.utest.js | 123 + .../secret/[secretId]/_revoke.utest.js | 365 + .../secret/[secretId]/_verify.utest.js | 361 + .../secret/[secretId]/authenticate.js | 243 + .../[contactId]/secret/[secretId]/mint.js | 108 + .../[contactId]/secret/[secretId]/proxy.js | 141 + .../[contactId]/secret/[secretId]/revoke.js | 178 + .../[contactId]/secret/[secretId]/verify.js | 283 + .../contact/[contactId]/secret/_list.utest.js | 154 + .../api/v1/contact/[contactId]/secret/list.js | 258 + .../api/v1/contact/[contactId]/space/list.js | 288 + .../contact/[contactId]/task/_list.utest.js | 251 + .../api/v1/contact/[contactId]/task/list.js | 411 + .../api/v1/contact/[contactId]/update.js | 241 + .../pages/api/v1/contact/_create.utest.js | 147 + .../pages/api/v1/contact/_ensure.utest.js | 378 + platform/pages/api/v1/contact/_list.utest.js | 309 + platform/pages/api/v1/contact/create.js | 209 + platform/pages/api/v1/contact/ensure.js | 213 + platform/pages/api/v1/contact/export.js | 255 + platform/pages/api/v1/contact/list.js | 223 + .../[conversationId]/_apply.utest.js | 88 + .../[conversationId]/_callback.utest.js | 340 + .../[conversationId]/_compact.utest.js | 130 + .../[conversationId]/_complete.utest.js | 699 + .../[conversationId]/_delete.utest.js | 142 + .../[conversationId]/_dispatch.utest.js | 167 + .../[conversationId]/_downvote.utest.js | 334 + .../[conversationId]/_fetch.utest.js | 180 + .../[conversationId]/_initiate.utest.js | 621 + .../[conversationId]/_queue.utest.js | 1561 + .../[conversationId]/_receive.utest.js | 476 + .../[conversationId]/_send.utest.js | 433 + .../[conversationId]/_update.utest.js | 255 + .../[conversationId]/_upvote.utest.js | 328 + .../v1/conversation/[conversationId]/apply.js | 275 + .../[attachmentId]/_download.utest.js | 189 + .../attachment/[attachmentId]/download.js | 37 + .../attachment/_list.utest.js | 157 + .../attachment/_upload.utest.js | 512 + .../[conversationId]/attachment/list.js | 153 + .../[conversationId]/attachment/upload.js | 692 + .../conversation/[conversationId]/callback.js | 43 + .../[conversationId]/channel/subscribe.js | 214 + .../conversation/[conversationId]/compact.js | 163 + .../conversation/[conversationId]/complete.js | 663 + .../[conversationId]/contact/_upsert.utest.js | 273 + .../[conversationId]/contact/upsert.js | 353 + .../conversation/[conversationId]/delete.js | 155 + .../conversation/[conversationId]/dispatch.js | 225 + .../conversation/[conversationId]/downvote.js | 302 + .../v1/conversation/[conversationId]/fetch.js | 155 + .../conversation/[conversationId]/initiate.js | 441 + .../message/[messageId]/_delete.utest.js | 190 + .../message/[messageId]/_downvote.utest.js | 315 + .../message/[messageId]/_fetch.utest.js | 196 + .../message/[messageId]/_synthesize.utest.js | 395 + .../message/[messageId]/_update.utest.js | 346 + .../message/[messageId]/_upvote.utest.js | 297 + .../message/[messageId]/delete.js | 206 + .../message/[messageId]/downvote.js | 237 + .../message/[messageId]/fetch.js | 197 + .../message/[messageId]/synthesize.js | 365 + .../message/[messageId]/update.js | 283 + .../message/[messageId]/upvote.js | 240 + .../[conversationId]/message/_create.utest.js | 295 + .../[conversationId]/message/_list.utest.js | 118 + .../message/batch/_create.utest.js | 469 + .../[conversationId]/message/batch/create.js | 338 + .../[conversationId]/message/create.js | 264 + .../[conversationId]/message/list.js | 307 + .../v1/conversation/[conversationId]/queue.js | 1228 + .../realtime/websocket/_create.utest.js | 404 + .../realtime/websocket/create.js | 147 + .../conversation/[conversationId]/receive.js | 432 + .../v1/conversation/[conversationId]/send.js | 419 + .../[conversationId]/session/_create.utest.js | 230 + .../[conversationId]/session/create.js | 294 + .../conversation/[conversationId]/update.js | 304 + .../conversation/[conversationId]/upvote.js | 254 + .../[conversationId]/usage/_fetch.utest.js | 313 + .../[conversationId]/usage/fetch.js | 216 + .../pages/api/v1/conversation/_apply.utest.js | 137 + .../api/v1/conversation/_compact.utest.js | 181 + .../api/v1/conversation/_complete.utest.js | 799 + .../api/v1/conversation/_create.utest.js | 335 + .../api/v1/conversation/_dispatch.utest.js | 146 + .../api/v1/conversation/_export.utest.js | 360 + .../pages/api/v1/conversation/_list.utest.js | 366 + .../pages/api/v1/conversation/_queue.utest.js | 519 + platform/pages/api/v1/conversation/apply.js | 408 + platform/pages/api/v1/conversation/compact.js | 299 + .../pages/api/v1/conversation/complete.js | 1254 + platform/pages/api/v1/conversation/create.js | 344 + .../pages/api/v1/conversation/dispatch.js | 220 + platform/pages/api/v1/conversation/export.js | 407 + platform/pages/api/v1/conversation/list.js | 260 + platform/pages/api/v1/conversation/queue.js | 464 + .../v1/dataset/[datasetId]/_delete.utest.js | 94 + .../v1/dataset/[datasetId]/_fetch.utest.js | 201 + .../v1/dataset/[datasetId]/_import.utest.js | 113 + .../v1/dataset/[datasetId]/_queue.utest.js | 123 + .../v1/dataset/[datasetId]/_search.utest.js | 259 + .../v1/dataset/[datasetId]/_update.utest.js | 310 + .../api/v1/dataset/[datasetId]/delete.js | 126 + .../pages/api/v1/dataset/[datasetId]/fetch.js | 204 + .../file/[fileId]/_attach.utest.js | 203 + .../file/[fileId]/_detach.utest.js | 217 + .../[datasetId]/file/[fileId]/_sync.utest.js | 159 + .../[datasetId]/file/[fileId]/attach.js | 233 + .../[datasetId]/file/[fileId]/detach.js | 245 + .../dataset/[datasetId]/file/[fileId]/sync.js | 355 + .../dataset/[datasetId]/file/_list.utest.js | 280 + .../api/v1/dataset/[datasetId]/file/list.js | 279 + .../api/v1/dataset/[datasetId]/import.js | 57 + .../api/v1/dataset/[datasetId]/ingest.js | 65 + .../pages/api/v1/dataset/[datasetId]/queue.js | 750 + .../record/[recordId]/_delete.utest.js | 118 + .../record/[recordId]/_fetch.utest.js | 190 + .../record/[recordId]/_update.utest.js | 221 + .../[datasetId]/record/[recordId]/delete.js | 122 + .../[datasetId]/record/[recordId]/fetch.js | 175 + .../[datasetId]/record/[recordId]/update.js | 250 + .../[datasetId]/record/_create.utest.js | 214 + .../[datasetId]/record/_export.utest.js | 332 + .../dataset/[datasetId]/record/_list.utest.js | 282 + .../v1/dataset/[datasetId]/record/create.js | 174 + .../v1/dataset/[datasetId]/record/export.js | 206 + .../api/v1/dataset/[datasetId]/record/list.js | 178 + .../api/v1/dataset/[datasetId]/search.js | 180 + .../api/v1/dataset/[datasetId]/update.js | 306 + .../pages/api/v1/dataset/_create.utest.js | 254 + platform/pages/api/v1/dataset/_list.utest.js | 258 + platform/pages/api/v1/dataset/_queue.utest.js | 117 + platform/pages/api/v1/dataset/create.js | 292 + platform/pages/api/v1/dataset/list.js | 270 + platform/pages/api/v1/dataset/queue.js | 63 + .../pages/api/v1/entity/text/_redact.utest.js | 318 + platform/pages/api/v1/entity/text/redact.js | 30 + .../pages/api/v1/event/log/_export.utest.js | 299 + .../pages/api/v1/event/log/_list.utest.js | 363 + .../pages/api/v1/event/log/_queue.utest.js | 144 + .../api/v1/event/log/_subscribe.utest.js | 321 + platform/pages/api/v1/event/log/export.js | 478 + platform/pages/api/v1/event/log/list.js | 483 + platform/pages/api/v1/event/log/queue.js | 58 + platform/pages/api/v1/event/log/subscribe.js | 246 + .../api/v1/event/metric/_export.utest.js | 325 + .../pages/api/v1/event/metric/_list.utest.js | 351 + .../pages/api/v1/event/metric/_queue.utest.js | 80 + platform/pages/api/v1/event/metric/export.js | 270 + platform/pages/api/v1/event/metric/list.js | 335 + platform/pages/api/v1/event/metric/queue.js | 58 + .../v1/event/metric/series/_fetch.utest.js | 92 + .../pages/api/v1/event/metric/series/fetch.js | 173 + .../api/v1/file/[fileId]/_delete.utest.js | 107 + .../api/v1/file/[fileId]/_download.utest.js | 451 + .../api/v1/file/[fileId]/_fetch.utest.js | 116 + .../pages/api/v1/file/[fileId]/_sync.utest.js | 185 + .../api/v1/file/[fileId]/_update.utest.js | 223 + .../api/v1/file/[fileId]/_upload.utest.js | 184 + platform/pages/api/v1/file/[fileId]/delete.js | 143 + .../pages/api/v1/file/[fileId]/download.js | 222 + platform/pages/api/v1/file/[fileId]/fetch.js | 151 + .../file/[fileId]/portrait/_download.utest.js | 119 + .../api/v1/file/[fileId]/portrait/download.js | 140 + platform/pages/api/v1/file/[fileId]/sync.js | 159 + .../[fileId]/thumbnail/_download.utest.js | 119 + .../v1/file/[fileId]/thumbnail/download.js | 139 + platform/pages/api/v1/file/[fileId]/update.js | 201 + platform/pages/api/v1/file/[fileId]/upload.js | 706 + platform/pages/api/v1/file/_create.utest.js | 153 + platform/pages/api/v1/file/_list.utest.js | 136 + platform/pages/api/v1/file/create.js | 228 + platform/pages/api/v1/file/list.js | 218 + platform/pages/api/v1/graphql.ts | 292 + .../blueprint/[blueprintId]/_publish.utest.js | 215 + .../[blueprintId]/_unpublish.utest.js | 113 + .../v1/hub/blueprint/[blueprintId]/publish.js | 147 + .../hub/blueprint/[blueprintId]/unpublish.js | 41 + .../api/v1/hub/bot/[botId]/_publish.utest.js | 230 + .../v1/hub/bot/[botId]/_unpublish.utest.js | 109 + .../pages/api/v1/hub/bot/[botId]/publish.js | 135 + .../hub/bot/[botId]/session/_create.utest.js | 160 + .../api/v1/hub/bot/[botId]/session/create.js | 108 + .../pages/api/v1/hub/bot/[botId]/unpublish.js | 41 + .../hub/dataset/[datasetId]/_publish.utest.js | 141 + .../dataset/[datasetId]/_unpublish.utest.js | 113 + .../api/v1/hub/dataset/[datasetId]/publish.js | 135 + .../[datasetId]/session/_create.utest.js | 154 + .../hub/dataset/[datasetId]/session/create.js | 122 + .../v1/hub/dataset/[datasetId]/unpublish.js | 41 + .../skillset/[skillsetId]/_publish.utest.js | 204 + .../skillset/[skillsetId]/_unpublish.utest.js | 115 + .../v1/hub/skillset/[skillsetId]/publish.js | 135 + .../[skillsetId]/session/_create.utest.js | 157 + .../skillset/[skillsetId]/session/create.js | 122 + .../v1/hub/skillset/[skillsetId]/unpublish.js | 41 + .../hub/widget/[widgetId]/_publish.utest.js | 204 + .../hub/widget/[widgetId]/_unpublish.utest.js | 114 + .../api/v1/hub/widget/[widgetId]/publish.js | 135 + .../api/v1/hub/widget/[widgetId]/unpublish.js | 41 + .../api/v1/image/[imageId]/_download.utest.js | 124 + .../pages/api/v1/image/[imageId]/download.js | 28 + platform/pages/api/v1/image/_create.utest.js | 187 + platform/pages/api/v1/image/_edit.utest.js | 242 + platform/pages/api/v1/image/create.js | 72 + platform/pages/api/v1/image/edit.js | 110 + platform/pages/api/v1/index.js | 162 + .../pages/api/v1/integration/_queue.utest.js | 728 + .../anam/[anamIntegrationId]/_delete.utest.js | 126 + .../anam/[anamIntegrationId]/_fetch.utest.js | 130 + .../anam/[anamIntegrationId]/_update.utest.js | 153 + .../anam/[anamIntegrationId]/delete.js | 38 + .../anam/[anamIntegrationId]/fetch.js | 68 + .../session/_create.utest.js | 487 + .../[anamIntegrationId]/session/create.js | 234 + .../anam/[anamIntegrationId]/update.js | 108 + .../api/v1/integration/anam/_create.utest.js | 129 + .../api/v1/integration/anam/_list.utest.js | 161 + .../pages/api/v1/integration/anam/create.js | 95 + .../pages/api/v1/integration/anam/list.js | 73 + .../[avatarIntegrationId]/_delete.utest.js | 269 + .../[avatarIntegrationId]/_fetch.utest.js | 126 + .../[avatarIntegrationId]/_update.utest.js | 149 + .../avatar/[avatarIntegrationId]/delete.js | 38 + .../avatar/[avatarIntegrationId]/fetch.js | 63 + .../session/_create.utest.js | 321 + .../[avatarIntegrationId]/session/create.js | 163 + .../avatar/[avatarIntegrationId]/update.js | 99 + .../v1/integration/avatar/_create.utest.js | 125 + .../api/v1/integration/avatar/_list.utest.js | 156 + .../pages/api/v1/integration/avatar/create.js | 86 + .../pages/api/v1/integration/avatar/list.js | 71 + .../[discordIntegrationId]/_delete.utest.js | 218 + .../[discordIntegrationId]/_fetch.utest.js | 321 + .../[discordIntegrationId]/_initiate.utest.js | 229 + .../[discordIntegrationId]/_interact.utest.js | 497 + .../[discordIntegrationId]/_queue.utest.js | 1275 + .../[discordIntegrationId]/_setup.utest.js | 363 + .../discord/[discordIntegrationId]/delete.js | 144 + .../discord/[discordIntegrationId]/fetch.js | 192 + .../[discordIntegrationId]/initiate.ts | 148 + .../[discordIntegrationId]/interact.js | 282 + .../discord/[discordIntegrationId]/queue.js | 997 + .../discord/[discordIntegrationId]/setup.js | 232 + .../discord/[discordIntegrationId]/update.js | 288 + .../v1/integration/discord/_create.utest.js | 336 + .../api/v1/integration/discord/_list.utest.js | 424 + .../api/v1/integration/discord/create.js | 277 + .../pages/api/v1/integration/discord/list.js | 247 + .../[emailIntegrationId]/_delete.utest.js | 339 + .../[emailIntegrationId]/_fetch.utest.js | 425 + .../[emailIntegrationId]/_initiate.utest.js | 213 + .../[emailIntegrationId]/_queue.utest.js | 1056 + .../[emailIntegrationId]/_setup.utest.js | 269 + .../email/[emailIntegrationId]/delete.js | 153 + .../email/[emailIntegrationId]/fetch.js | 184 + .../email/[emailIntegrationId]/initiate.ts | 163 + .../email/[emailIntegrationId]/queue.js | 920 + .../email/[emailIntegrationId]/setup.js | 147 + .../email/[emailIntegrationId]/update.js | 280 + .../api/v1/integration/email/_create.utest.js | 513 + .../api/v1/integration/email/_parse.utest.js | 346 + .../pages/api/v1/integration/email/create.js | 266 + .../pages/api/v1/integration/email/list.js | 232 + .../pages/api/v1/integration/email/parse.js | 245 + .../[extractIntegrationId]/_delete.utest.js | 233 + .../[extractIntegrationId]/_fetch.utest.js | 133 + .../[extractIntegrationId]/_queue.utest.js | 776 + .../[extractIntegrationId]/_trigger.utest.js | 273 + .../[extractIntegrationId]/_update.utest.js | 154 + .../extract/[extractIntegrationId]/delete.js | 119 + .../extract/[extractIntegrationId]/fetch.js | 141 + .../item/_export.utest.js | 142 + .../item/_list.utest.js | 191 + .../[extractIntegrationId]/item/export.js | 214 + .../[extractIntegrationId]/item/list.js | 199 + .../extract/[extractIntegrationId]/queue.js | 539 + .../extract/[extractIntegrationId]/trigger.js | 251 + .../extract/[extractIntegrationId]/update.js | 242 + .../v1/integration/extract/_create.utest.js | 155 + .../api/v1/integration/extract/_list.utest.js | 140 + .../api/v1/integration/extract/create.js | 242 + .../pages/api/v1/integration/extract/list.js | 201 + .../[githubIntegrationId]/_event.utest.js | 529 + .../[githubIntegrationId]/_fetch.utest.js | 132 + .../[githubIntegrationId]/_queue.utest.js | 277 + .../[githubIntegrationId]/_update.utest.js | 132 + .../github/[githubIntegrationId]/delete.js | 97 + .../github/[githubIntegrationId]/event.js | 243 + .../github/[githubIntegrationId]/fetch.js | 143 + .../github/[githubIntegrationId]/queue.js | 697 + .../github/[githubIntegrationId]/setup.js | 153 + .../github/[githubIntegrationId]/update.js | 219 + .../v1/integration/github/_create.utest.js | 97 + .../api/v1/integration/github/_list.utest.js | 94 + .../pages/api/v1/integration/github/create.js | 278 + .../pages/api/v1/integration/github/list.js | 163 + .../_delete.utest.js | 124 + .../[googlechatIntegrationId]/_event.utest.js | 804 + .../[googlechatIntegrationId]/_fetch.utest.js | 167 + .../_initiate.utest.js | 279 + .../[googlechatIntegrationId]/_queue.utest.js | 1622 + .../[googlechatIntegrationId]/delete.js | 119 + .../[googlechatIntegrationId]/event.js | 644 + .../[googlechatIntegrationId]/fetch.js | 204 + .../[googlechatIntegrationId]/initiate.ts | 152 + .../[googlechatIntegrationId]/queue.js | 1385 + .../[googlechatIntegrationId]/setup.js | 308 + .../[googlechatIntegrationId]/update.js | 301 + .../api/v1/integration/googlechat/create.js | 253 + .../api/v1/integration/googlechat/list.js | 280 + .../_callback.utest.js | 1382 + .../[instagramIntegrationId]/_delete.utest.js | 83 + .../[instagramIntegrationId]/_fetch.utest.js | 151 + .../_initiate.utest.js | 211 + .../[instagramIntegrationId]/_queue.utest.js | 962 + .../[instagramIntegrationId]/_setup.utest.js | 305 + .../[instagramIntegrationId]/_update.utest.js | 424 + .../[instagramIntegrationId]/callback.js | 283 + .../[instagramIntegrationId]/delete.js | 179 + .../[instagramIntegrationId]/fetch.js | 219 + .../[instagramIntegrationId]/initiate.ts | 151 + .../[instagramIntegrationId]/queue.js | 1110 + .../[instagramIntegrationId]/setup.js | 244 + .../[instagramIntegrationId]/update.js | 279 + .../v1/integration/instagram/_create.utest.js | 335 + .../v1/integration/instagram/_list.utest.js | 166 + .../api/v1/integration/instagram/create.js | 273 + .../api/v1/integration/instagram/list.js | 273 + .../[mcpserverIntegrationId]/_delete.utest.js | 266 + .../[mcpserverIntegrationId]/_fetch.utest.js | 396 + .../[mcpserverIntegrationId]/_mcp.utest.js | 1438 + .../[mcpserverIntegrationId]/delete.js | 170 + .../[mcpserverIntegrationId]/fetch.js | 190 + .../mcpserver/[mcpserverIntegrationId]/mcp.ts | 1223 + .../oauth/_authorize.utest.js | 444 + .../oauth/_register.utest.js | 271 + .../oauth/_revoke.utest.js | 131 + .../oauth/_token.utest.js | 638 + .../oauth/authorize.ts | 373 + .../oauth/register.ts | 170 + .../[mcpserverIntegrationId]/oauth/revoke.ts | 72 + .../[mcpserverIntegrationId]/oauth/token.ts | 403 + .../well-known/_authorization-server.utest.js | 109 + .../well-known/_protected-resource.utest.js | 85 + .../oauth/well-known/authorization-server.ts | 70 + .../oauth/well-known/protected-resource.ts | 55 + .../[mcpserverIntegrationId]/update.js | 290 + .../v1/integration/mcpserver/_create.utest.js | 258 + .../api/v1/integration/mcpserver/create.js | 369 + .../api/v1/integration/mcpserver/list.js | 266 + .../_callback.utest.js | 1182 + .../[messengerIntegrationId]/_delete.utest.js | 229 + .../[messengerIntegrationId]/_fetch.utest.js | 400 + .../_initiate.utest.js | 211 + .../[messengerIntegrationId]/_queue.utest.js | 1429 + .../[messengerIntegrationId]/_setup.utest.js | 287 + .../[messengerIntegrationId]/_update.utest.js | 424 + .../[messengerIntegrationId]/callback.js | 283 + .../[messengerIntegrationId]/delete.js | 128 + .../[messengerIntegrationId]/fetch.js | 184 + .../[messengerIntegrationId]/initiate.ts | 151 + .../[messengerIntegrationId]/queue.js | 1329 + .../[messengerIntegrationId]/setup.js | 193 + .../[messengerIntegrationId]/update.js | 254 + .../v1/integration/messenger/_create.utest.js | 335 + .../v1/integration/messenger/_list.utest.js | 166 + .../api/v1/integration/messenger/create.js | 255 + .../api/v1/integration/messenger/list.js | 245 + .../_callback.utest.js | 408 + .../_delete.utest.js | 192 + .../_fetch.utest.js | 159 + .../_initiate.utest.js | 268 + .../_queue.utest.js | 1065 + .../_setup.utest.js | 155 + .../[microsoftteamsIntegrationId]/callback.js | 207 + .../[microsoftteamsIntegrationId]/delete.js | 134 + .../[microsoftteamsIntegrationId]/fetch.js | 184 + .../[microsoftteamsIntegrationId]/initiate.ts | 145 + .../[microsoftteamsIntegrationId]/queue.js | 1004 + .../[microsoftteamsIntegrationId]/setup.js | 206 + .../[microsoftteamsIntegrationId]/update.js | 264 + .../v1/integration/microsoftteams/create.js | 263 + .../api/v1/integration/microsoftteams/list.js | 259 + .../[notionIntegrationId]/_delete.utest.js | 91 + .../[notionIntegrationId]/_fetch.utest.js | 123 + .../[notionIntegrationId]/_queue.utest.js | 244 + .../[notionIntegrationId]/_sync.utest.js | 334 + .../notion/[notionIntegrationId]/delete.js | 145 + .../notion/[notionIntegrationId]/fetch.js | 176 + .../notion/[notionIntegrationId]/queue.js | 76 + .../notion/[notionIntegrationId]/sync.js | 317 + .../notion/[notionIntegrationId]/update.js | 259 + .../v1/integration/notion/_create.utest.js | 99 + .../api/v1/integration/notion/_list.utest.js | 161 + .../pages/api/v1/integration/notion/create.js | 209 + .../pages/api/v1/integration/notion/list.js | 233 + platform/pages/api/v1/integration/queue.ts | 592 + .../[recallIntegrationId]/_delete.utest.js | 126 + .../[recallIntegrationId]/_fetch.utest.js | 131 + .../[recallIntegrationId]/_initiate.utest.js | 563 + .../[recallIntegrationId]/_queue.utest.js | 354 + .../[recallIntegrationId]/_update.utest.js | 124 + .../[recallIntegrationId]/_webhook.utest.js | 422 + .../recall/[recallIntegrationId]/delete.js | 39 + .../recall/[recallIntegrationId]/fetch.js | 70 + .../recall/[recallIntegrationId]/initiate.ts | 225 + .../recall/[recallIntegrationId]/queue.js | 210 + .../session/[sessionId]/_leave.utest.js | 96 + .../session/[sessionId]/leave.ts | 37 + .../[sessionId]/message/_send.utest.js | 142 + .../session/[sessionId]/message/send.ts | 44 + .../[sessionId]/recording/_pause.utest.js | 96 + .../[sessionId]/recording/_resume.utest.js | 96 + .../session/[sessionId]/recording/pause.ts | 37 + .../session/[sessionId]/recording/resume.ts | 37 + .../[sessionId]/screenshare/_start.utest.js | 114 + .../[sessionId]/screenshare/_stop.utest.js | 98 + .../session/[sessionId]/screenshare/start.ts | 48 + .../session/[sessionId]/screenshare/stop.ts | 37 + .../session/_create.utest.js | 183 + .../[recallIntegrationId]/session/create.js | 150 + .../recall/[recallIntegrationId]/update.js | 113 + .../recall/[recallIntegrationId]/webhook.js | 194 + .../v1/integration/recall/_create.utest.js | 136 + .../api/v1/integration/recall/_list.utest.js | 151 + .../pages/api/v1/integration/recall/create.js | 100 + .../pages/api/v1/integration/recall/list.js | 71 + .../[sitemapIntegrationId]/_delete.utest.js | 93 + .../[sitemapIntegrationId]/_fetch.utest.js | 105 + .../[sitemapIntegrationId]/_queue.utest.js | 182 + .../[sitemapIntegrationId]/_sync.utest.js | 497 + .../[sitemapIntegrationId]/_update.utest.js | 187 + .../sitemap/[sitemapIntegrationId]/delete.js | 160 + .../sitemap/[sitemapIntegrationId]/fetch.js | 201 + .../sitemap/[sitemapIntegrationId]/queue.js | 81 + .../sitemap/[sitemapIntegrationId]/sync.js | 456 + .../sitemap/[sitemapIntegrationId]/update.js | 314 + .../v1/integration/sitemap/_create.utest.js | 134 + .../api/v1/integration/sitemap/create.js | 263 + .../pages/api/v1/integration/sitemap/list.js | 246 + .../_delete.utest.js | 285 + .../_invoke.utest.js | 656 + .../[skillserverIntegrationId]/delete.js | 95 + .../[skillserverIntegrationId]/fetch.js | 117 + .../[skillserverIntegrationId]/invoke.js | 342 + .../[skillserverIntegrationId]/update.js | 161 + .../integration/skillserver/_create.utest.js | 444 + .../api/v1/integration/skillserver/create.js | 190 + .../api/v1/integration/skillserver/list.js | 173 + .../[slackIntegrationId]/_command.utest.js | 194 + .../[slackIntegrationId]/_delete.utest.js | 217 + .../[slackIntegrationId]/_event.utest.js | 1417 + .../[slackIntegrationId]/_fetch.utest.js | 492 + .../[slackIntegrationId]/_initiate.utest.js | 228 + .../_interaction.utest.js | 1064 + .../[slackIntegrationId]/_queue.utest.js | 5036 +++ .../[slackIntegrationId]/_setup.utest.js | 274 + .../slack/[slackIntegrationId]/command.js | 247 + .../slack/[slackIntegrationId]/delete.js | 171 + .../slack/[slackIntegrationId]/event.js | 729 + .../slack/[slackIntegrationId]/fetch.js | 257 + .../slack/[slackIntegrationId]/initiate.ts | 161 + .../slack/[slackIntegrationId]/interaction.js | 824 + .../slack/[slackIntegrationId]/queue.js | 3702 ++ .../slack/[slackIntegrationId]/setup.js | 323 + .../slack/[slackIntegrationId]/update.js | 387 + .../api/v1/integration/slack/_create.utest.js | 221 + .../api/v1/integration/slack/_list.utest.js | 566 + .../pages/api/v1/integration/slack/create.js | 393 + .../pages/api/v1/integration/slack/list.js | 298 + .../[supportIntegrationId]/_delete.utest.js | 285 + .../[supportIntegrationId]/_fetch.utest.js | 473 + .../[supportIntegrationId]/_queue.utest.js | 591 + .../[supportIntegrationId]/_trigger.utest.js | 230 + .../[supportIntegrationId]/_update.utest.js | 115 + .../support/[supportIntegrationId]/delete.js | 128 + .../support/[supportIntegrationId]/fetch.js | 135 + .../support/[supportIntegrationId]/queue.js | 314 + .../support/[supportIntegrationId]/trigger.js | 235 + .../support/[supportIntegrationId]/update.js | 200 + .../v1/integration/support/_create.utest.js | 101 + .../api/v1/integration/support/_list.utest.js | 135 + .../api/v1/integration/support/create.js | 193 + .../pages/api/v1/integration/support/list.js | 199 + .../[telegramIntegrationId]/_delete.utest.js | 151 + .../[telegramIntegrationId]/_fetch.utest.js | 359 + .../_initiate.utest.js | 238 + .../[telegramIntegrationId]/_queue.utest.js | 1647 + .../[telegramIntegrationId]/_setup.utest.js | 418 + .../[telegramIntegrationId]/_update.utest.js | 427 + .../[telegramIntegrationId]/_webhook.utest.js | 1002 + .../[telegramIntegrationId]/delete.js | 142 + .../telegram/[telegramIntegrationId]/fetch.js | 176 + .../[telegramIntegrationId]/initiate.ts | 159 + .../telegram/[telegramIntegrationId]/queue.js | 1575 + .../telegram/[telegramIntegrationId]/setup.js | 260 + .../[telegramIntegrationId]/update.js | 250 + .../[telegramIntegrationId]/webhook.js | 423 + .../v1/integration/telegram/_list.utest.js | 146 + .../api/v1/integration/telegram/create.js | 246 + .../pages/api/v1/integration/telegram/list.js | 218 + .../[triggerIntegrationId]/_delete.utest.js | 222 + .../[triggerIntegrationId]/_event.utest.js | 395 + .../[triggerIntegrationId]/_fetch.utest.js | 519 + .../[triggerIntegrationId]/_invoke.utest.js | 234 + .../[triggerIntegrationId]/_queue.utest.js | 585 + .../[triggerIntegrationId]/_setup.utest.js | 260 + .../[triggerIntegrationId]/_update.utest.js | 515 + .../trigger/[triggerIntegrationId]/delete.js | 135 + .../trigger/[triggerIntegrationId]/event.js | 321 + .../trigger/[triggerIntegrationId]/fetch.js | 196 + .../trigger/[triggerIntegrationId]/invoke.js | 147 + .../trigger/[triggerIntegrationId]/queue.js | 509 + .../trigger/[triggerIntegrationId]/setup.js | 139 + .../trigger/[triggerIntegrationId]/update.js | 298 + .../v1/integration/trigger/_create.utest.js | 549 + .../api/v1/integration/trigger/_list.utest.js | 312 + .../api/v1/integration/trigger/create.js | 244 + .../pages/api/v1/integration/trigger/list.js | 236 + .../[twilioIntegrationId]/_delete.utest.js | 91 + .../[twilioIntegrationId]/_fetch.utest.js | 103 + .../[twilioIntegrationId]/_initiate.utest.js | 325 + .../[twilioIntegrationId]/_queue.utest.js | 1690 + .../[twilioIntegrationId]/_setup.utest.js | 132 + .../[twilioIntegrationId]/_webhook.utest.js | 792 + .../twilio/[twilioIntegrationId]/delete.js | 155 + .../twilio/[twilioIntegrationId]/fetch.js | 206 + .../twilio/[twilioIntegrationId]/initiate.ts | 202 + .../twilio/[twilioIntegrationId]/queue.js | 1390 + .../twilio/[twilioIntegrationId]/setup.js | 168 + .../twilio/[twilioIntegrationId]/update.js | 328 + .../twilio/[twilioIntegrationId]/webhook.js | 494 + .../pages/api/v1/integration/twilio/create.js | 381 + .../pages/api/v1/integration/twilio/list.js | 297 + .../_callback.utest.js | 518 + .../[whatsappIntegrationId]/_delete.utest.js | 91 + .../[whatsappIntegrationId]/_fetch.utest.js | 132 + .../_initiate.utest.js | 291 + .../[whatsappIntegrationId]/_queue.utest.js | 2278 + .../[whatsappIntegrationId]/_setup.utest.js | 45 + .../[whatsappIntegrationId]/callback.js | 281 + .../[whatsappIntegrationId]/delete.js | 179 + .../whatsapp/[whatsappIntegrationId]/fetch.js | 231 + .../[whatsappIntegrationId]/initiate.ts | 183 + .../whatsapp/[whatsappIntegrationId]/queue.js | 1893 + .../whatsapp/[whatsappIntegrationId]/setup.js | 217 + .../[whatsappIntegrationId]/update.js | 302 + .../v1/integration/whatsapp/_create.utest.js | 325 + .../api/v1/integration/whatsapp/create.js | 300 + .../pages/api/v1/integration/whatsapp/list.js | 285 + .../[widgetIntegrationId]/_clone.utest.js | 277 + .../[widgetIntegrationId]/_delete.utest.js | 83 + .../[widgetIntegrationId]/_setup.utest.js | 364 + .../[widgetIntegrationId]/_update.utest.js | 176 + .../widget/[widgetIntegrationId]/clone.js | 228 + .../widget/[widgetIntegrationId]/delete.js | 152 + .../widget/[widgetIntegrationId]/fetch.js | 302 + .../file/[fileId]/_attach.utest.js | 448 + .../file/[fileId]/_detach.utest.js | 223 + .../file/[fileId]/attach.js | 203 + .../file/[fileId]/detach.js | 144 + .../session/_create.utest.js | 353 + .../[widgetIntegrationId]/session/create.js | 526 + .../widget/[widgetIntegrationId]/setup.js | 245 + .../widget/[widgetIntegrationId]/update.js | 475 + .../v1/integration/widget/_create.utest.js | 238 + .../api/v1/integration/widget/_list.utest.js | 247 + .../pages/api/v1/integration/widget/create.js | 517 + .../pages/api/v1/integration/widget/list.js | 345 + .../v1/magic/[promptId]/_generate.utest.js | 385 + .../pages/api/v1/magic/[promptId]/generate.js | 156 + platform/pages/api/v1/magic/_list.utest.js | 67 + platform/pages/api/v1/magic/list.js | 116 + platform/pages/api/v1/me/_fetch.utest.js | 241 + platform/pages/api/v1/me/_update.utest.js | 284 + platform/pages/api/v1/me/fetch.js | 28 + platform/pages/api/v1/me/update.js | 52 + .../api/v1/memory/[memoryId]/_delete.utest.js | 90 + .../api/v1/memory/[memoryId]/_fetch.utest.js | 116 + .../api/v1/memory/[memoryId]/_update.utest.js | 164 + .../pages/api/v1/memory/[memoryId]/delete.js | 129 + .../pages/api/v1/memory/[memoryId]/fetch.js | 142 + .../pages/api/v1/memory/[memoryId]/update.js | 212 + platform/pages/api/v1/memory/_create.utest.js | 69 + platform/pages/api/v1/memory/_export.utest.js | 148 + platform/pages/api/v1/memory/_list.utest.js | 139 + platform/pages/api/v1/memory/_queue.utest.js | 213 + platform/pages/api/v1/memory/_search.utest.js | 181 + platform/pages/api/v1/memory/create.js | 193 + platform/pages/api/v1/memory/export.js | 241 + platform/pages/api/v1/memory/list.js | 208 + platform/pages/api/v1/memory/queue.js | 94 + platform/pages/api/v1/memory/search.js | 153 + .../application/[oAuthApplicationId]/.gitkeep | 0 .../api/v1/oauth/application/_queue.utest.js | 64 + .../pages/api/v1/oauth/application/queue.js | 70 + .../[oAuthConnectionId]/_delete.utest.js | 95 + .../[oAuthConnectionId]/_fetch.utest.js | 160 + .../[oAuthConnectionId]/_update.utest.js | 180 + .../connection/[oAuthConnectionId]/delete.js | 38 + .../connection/[oAuthConnectionId]/fetch.js | 69 + .../connection/[oAuthConnectionId]/update.js | 111 + .../api/v1/oauth/connection/_create.utest.js | 129 + .../api/v1/oauth/connection/_list.utest.js | 106 + .../pages/api/v1/oauth/connection/create.js | 91 + .../pages/api/v1/oauth/connection/list.js | 73 + .../api/v1/openai/chat/_completions.utest.js | 803 + .../pages/api/v1/openai/chat/completions.ts | 1012 + .../api/v1/platform/ability/_list.utest.js | 169 + .../api/v1/platform/ability/_search.utest.js | 103 + .../pages/api/v1/platform/ability/list.js | 326 + .../pages/api/v1/platform/ability/search.js | 316 + .../api/v1/platform/action/_list.utest.js | 78 + platform/pages/api/v1/platform/action/list.js | 212 + .../example/[exampleId]/_clone.utest.js | 583 + .../example/[exampleId]/_fetch.utest.js | 306 + .../v1/platform/example/[exampleId]/clone.js | 589 + .../v1/platform/example/[exampleId]/fetch.js | 329 + .../api/v1/platform/example/_list.utest.js | 182 + .../api/v1/platform/example/_search.utest.js | 124 + .../pages/api/v1/platform/example/list.js | 231 + .../pages/api/v1/platform/example/search.js | 224 + .../api/v1/platform/limit/_list.utest.js | 142 + platform/pages/api/v1/platform/limit/list.js | 11 + .../api/v1/platform/model/_list.utest.js | 75 + platform/pages/api/v1/platform/model/list.js | 298 + .../report/[reportId]/_generate.utest.js | 310 + .../v1/platform/report/[reportId]/generate.ts | 200 + .../api/v1/platform/report/_generate.utest.js | 323 + .../api/v1/platform/report/_list.utest.js | 71 + .../pages/api/v1/platform/report/generate.ts | 263 + platform/pages/api/v1/platform/report/list.ts | 151 + .../api/v1/platform/secret/_list.utest.js | 250 + .../api/v1/platform/secret/_search.utest.js | 105 + platform/pages/api/v1/platform/secret/list.js | 266 + .../pages/api/v1/platform/secret/search.js | 281 + .../v1/platform/subscription/_list.utest.js | 32 + .../api/v1/platform/subscription/list.js | 19 + .../api/v1/policy/[policyId]/_delete.utest.js | 125 + .../api/v1/policy/[policyId]/_fetch.utest.js | 161 + .../api/v1/policy/[policyId]/_update.utest.js | 278 + .../policy/[policyId]/block/_clear.utest.js | 136 + .../v1/policy/[policyId]/block/_list.utest.js | 142 + .../api/v1/policy/[policyId]/block/clear.js | 64 + .../api/v1/policy/[policyId]/block/list.js | 61 + .../pages/api/v1/policy/[policyId]/delete.js | 146 + .../pages/api/v1/policy/[policyId]/fetch.js | 178 + .../pages/api/v1/policy/[policyId]/update.js | 257 + platform/pages/api/v1/policy/_create.utest.js | 136 + platform/pages/api/v1/policy/_list.utest.js | 137 + platform/pages/api/v1/policy/create.js | 238 + platform/pages/api/v1/policy/list.js | 319 + .../api/v1/portal/[portalId]/_delete.utest.js | 95 + .../api/v1/portal/[portalId]/_fetch.utest.js | 118 + .../api/v1/portal/[portalId]/_update.utest.js | 162 + .../pages/api/v1/portal/[portalId]/delete.js | 129 + .../pages/api/v1/portal/[portalId]/fetch.js | 146 + .../pages/api/v1/portal/[portalId]/update.js | 207 + platform/pages/api/v1/portal/_create.utest.js | 212 + platform/pages/api/v1/portal/_list.utest.js | 218 + platform/pages/api/v1/portal/create.js | 295 + platform/pages/api/v1/portal/list.js | 200 + .../api/v1/rating/[ratingId]/_delete.utest.js | 229 + .../api/v1/rating/[ratingId]/_fetch.utest.js | 412 + .../api/v1/rating/[ratingId]/_update.utest.js | 485 + .../pages/api/v1/rating/[ratingId]/delete.js | 218 + .../pages/api/v1/rating/[ratingId]/fetch.js | 213 + .../pages/api/v1/rating/[ratingId]/update.js | 332 + platform/pages/api/v1/rating/_create.utest.js | 337 + platform/pages/api/v1/rating/_export.utest.js | 564 + platform/pages/api/v1/rating/_list.utest.js | 398 + platform/pages/api/v1/rating/create.js | 283 + platform/pages/api/v1/rating/export.js | 231 + platform/pages/api/v1/rating/list.js | 259 + .../secret/[secretId]/_authenticate.utest.js | 196 + .../api/v1/secret/[secretId]/_delete.utest.js | 222 + .../api/v1/secret/[secretId]/_fetch.utest.js | 193 + .../api/v1/secret/[secretId]/_mint.utest.js | 85 + .../api/v1/secret/[secretId]/_proxy.utest.js | 96 + .../api/v1/secret/[secretId]/_revoke.utest.js | 261 + .../api/v1/secret/[secretId]/_update.utest.js | 301 + .../api/v1/secret/[secretId]/_verify.utest.js | 264 + .../api/v1/secret/[secretId]/authenticate.js | 157 + .../pages/api/v1/secret/[secretId]/delete.js | 135 + .../pages/api/v1/secret/[secretId]/fetch.js | 157 + .../pages/api/v1/secret/[secretId]/mint.js | 111 + .../pages/api/v1/secret/[secretId]/proxy.js | 139 + .../pages/api/v1/secret/[secretId]/revoke.js | 148 + .../pages/api/v1/secret/[secretId]/update.js | 237 + .../pages/api/v1/secret/[secretId]/verify.js | 218 + platform/pages/api/v1/secret/_create.utest.js | 186 + platform/pages/api/v1/secret/_list.utest.js | 262 + platform/pages/api/v1/secret/create.js | 217 + platform/pages/api/v1/secret/list.js | 207 + .../api/v1/session/file/_upload.utest.js | 131 + platform/pages/api/v1/session/file/upload.js | 592 + .../v1/skillset/[skillsetId]/_delete.utest.js | 94 + .../v1/skillset/[skillsetId]/_fetch.utest.js | 108 + .../v1/skillset/[skillsetId]/_update.utest.js | 146 + .../ability/[abilityId]/_delete.utest.js | 119 + .../ability/[abilityId]/_execute.utest.js | 477 + .../ability/[abilityId]/_fetch.utest.js | 135 + .../ability/[abilityId]/_update.utest.js | 217 + .../ability/[abilityId]/delete.js | 182 + .../ability/[abilityId]/execute.js | 427 + .../[skillsetId]/ability/[abilityId]/fetch.js | 229 + .../ability/[abilityId]/update.js | 340 + .../[skillsetId]/ability/_create.utest.js | 209 + .../[skillsetId]/ability/_export.utest.js | 294 + .../[skillsetId]/ability/_list.utest.js | 317 + .../skillset/[skillsetId]/ability/create.js | 324 + .../skillset/[skillsetId]/ability/export.js | 410 + .../v1/skillset/[skillsetId]/ability/list.js | 276 + .../api/v1/skillset/[skillsetId]/delete.js | 114 + .../api/v1/skillset/[skillsetId]/fetch.js | 140 + .../api/v1/skillset/[skillsetId]/update.js | 198 + .../pages/api/v1/skillset/_create.utest.js | 145 + platform/pages/api/v1/skillset/_list.utest.js | 153 + platform/pages/api/v1/skillset/create.js | 212 + platform/pages/api/v1/skillset/list.js | 202 + .../api/v1/space/[spaceId]/_delete.utest.js | 90 + .../api/v1/space/[spaceId]/_fetch.utest.js | 116 + .../api/v1/space/[spaceId]/_update.utest.js | 142 + .../pages/api/v1/space/[spaceId]/delete.js | 131 + .../pages/api/v1/space/[spaceId]/fetch.js | 145 + .../[spaceId]/site/[siteId]/_delete.utest.js | 87 + .../[spaceId]/site/[siteId]/_fetch.utest.js | 101 + .../[spaceId]/site/[siteId]/_update.utest.js | 126 + .../space/[spaceId]/site/[siteId]/delete.js | 152 + .../v1/space/[spaceId]/site/[siteId]/fetch.js | 188 + .../space/[spaceId]/site/[siteId]/update.js | 250 + .../v1/space/[spaceId]/site/_create.utest.js | 162 + .../v1/space/[spaceId]/site/_list.utest.js | 111 + .../api/v1/space/[spaceId]/site/create.js | 229 + .../pages/api/v1/space/[spaceId]/site/list.js | 245 + .../[spaceId]/storage/copy/[[...path]].js | 164 + .../[spaceId]/storage/copy/_path.utest.js | 175 + .../[spaceId]/storage/delete/[[...path]].js | 181 + .../[spaceId]/storage/delete/_path.utest.js | 213 + .../[spaceId]/storage/download/[[...path]].js | 177 + .../[spaceId]/storage/download/_path.utest.js | 249 + .../[spaceId]/storage/list/[[...path]].js | 183 + .../[spaceId]/storage/list/_path.utest.js | 249 + .../[spaceId]/storage/move/[[...path]].js | 189 + .../[spaceId]/storage/move/_path.utest.js | 175 + .../[spaceId]/storage/upload/[[...path]].js | 593 + .../[spaceId]/storage/upload/_path.utest.js | 633 + .../pages/api/v1/space/[spaceId]/update.js | 191 + platform/pages/api/v1/space/_create.utest.js | 284 + platform/pages/api/v1/space/_export.utest.js | 145 + platform/pages/api/v1/space/_list.utest.js | 207 + platform/pages/api/v1/space/create.js | 248 + platform/pages/api/v1/space/export.js | 245 + platform/pages/api/v1/space/list.js | 204 + .../api/v1/space/system/site/[[...path]].js | 28 + platform/pages/api/v1/spec.ts | 41 + platform/pages/api/v1/status/_ping.utest.js | 93 + platform/pages/api/v1/status/ping.js | 7 + .../api/v1/task/[taskId]/_cancel.utest.js | 278 + .../api/v1/task/[taskId]/_delete.utest.js | 136 + .../api/v1/task/[taskId]/_fetch.utest.js | 198 + .../api/v1/task/[taskId]/_subscribe.utest.js | 184 + .../api/v1/task/[taskId]/_trigger.utest.js | 207 + .../api/v1/task/[taskId]/_update.utest.js | 473 + .../api/v1/task/[taskId]/_workflow.utest.js | 3565 ++ platform/pages/api/v1/task/[taskId]/cancel.js | 171 + platform/pages/api/v1/task/[taskId]/delete.js | 126 + .../[taskExecutionId]/_cancel.utest.js | 457 + .../execution/[taskExecutionId]/cancel.js | 206 + .../v1/task/[taskId]/execution/_list.utest.js | 273 + .../api/v1/task/[taskId]/execution/list.js | 253 + platform/pages/api/v1/task/[taskId]/fetch.js | 190 + .../pages/api/v1/task/[taskId]/subscribe.ts | 237 + .../pages/api/v1/task/[taskId]/trigger.js | 137 + platform/pages/api/v1/task/[taskId]/update.js | 312 + .../pages/api/v1/task/[taskId]/workflow.ts | 2125 + platform/pages/api/v1/task/_create.utest.js | 489 + platform/pages/api/v1/task/_export.utest.js | 270 + platform/pages/api/v1/task/_list.utest.js | 415 + platform/pages/api/v1/task/_queue.utest.js | 1100 + platform/pages/api/v1/task/create.js | 493 + platform/pages/api/v1/task/export.js | 245 + platform/pages/api/v1/task/list.js | 295 + platform/pages/api/v1/task/queue.ts | 595 + .../api/v1/team/[teamId]/_delete.utest.js | 270 + .../api/v1/team/[teamId]/_fetch.utest.js | 101 + .../api/v1/team/[teamId]/_update.utest.js | 135 + platform/pages/api/v1/team/[teamId]/delete.js | 48 + platform/pages/api/v1/team/[teamId]/fetch.js | 81 + .../[teamMembershipId]/_delete.utest.js | 145 + .../[teamMembershipId]/_fetch.utest.js | 130 + .../[teamMembershipId]/_update.utest.js | 151 + .../membership/[teamMembershipId]/delete.js | 53 + .../membership/[teamMembershipId]/fetch.js | 103 + .../invite/_resend.utest.js | 268 + .../[teamMembershipId]/invite/resend.ts | 86 + .../membership/[teamMembershipId]/update.js | 85 + .../team/[teamId]/membership/_create.utest.js | 161 + .../team/[teamId]/membership/_list.utest.js | 142 + .../api/v1/team/[teamId]/membership/create.js | 136 + .../api/v1/team/[teamId]/membership/list.js | 152 + platform/pages/api/v1/team/[teamId]/update.js | 67 + platform/pages/api/v1/team/_create.utest.js | 354 + platform/pages/api/v1/team/_list.utest.js | 103 + platform/pages/api/v1/team/create.js | 67 + platform/pages/api/v1/team/list.js | 204 + platform/pages/api/v1/text/_complete.utest.js | 241 + platform/pages/api/v1/text/_insert.utest.js | 253 + platform/pages/api/v1/text/complete.js | 95 + platform/pages/api/v1/text/insert.js | 97 + .../api/v1/token/[tokenId]/_delete.utest.js | 264 + .../api/v1/token/[tokenId]/_fetch.utest.js | 312 + .../api/v1/token/[tokenId]/_update.utest.js | 146 + .../pages/api/v1/token/[tokenId]/delete.js | 40 + .../pages/api/v1/token/[tokenId]/fetch.js | 58 + .../pages/api/v1/token/[tokenId]/update.js | 77 + platform/pages/api/v1/token/_create.utest.js | 107 + platform/pages/api/v1/token/_list.utest.js | 115 + platform/pages/api/v1/token/create.js | 73 + platform/pages/api/v1/token/list.js | 64 + platform/pages/api/v1/url/_fetch.utest.js | 151 + platform/pages/api/v1/url/_sign.utest.js | 342 + platform/pages/api/v1/url/_unfurl.utest.js | 139 + platform/pages/api/v1/url/fetch.js | 66 + platform/pages/api/v1/url/sign.js | 27 + platform/pages/api/v1/url/unfurl.js | 81 + platform/pages/api/v1/usage/_export.utest.js | 245 + platform/pages/api/v1/usage/_fetch.utest.js | 254 + platform/pages/api/v1/usage/_list.utest.js | 163 + platform/pages/api/v1/usage/_queue.utest.js | 226 + platform/pages/api/v1/usage/export.js | 153 + platform/pages/api/v1/usage/fetch.js | 228 + platform/pages/api/v1/usage/list.js | 256 + platform/pages/api/v1/usage/queue.js | 132 + .../pages/api/v1/usage/series/_fetch.utest.js | 159 + platform/pages/api/v1/usage/series/fetch.js | 239 + .../api/v1/user/[userId]/_delete.utest.js | 163 + .../api/v1/user/[userId]/_fetch.utest.js | 195 + .../api/v1/user/[userId]/_update.utest.js | 201 + .../context/[contextId]/_delete.utest.js | 125 + .../context/[contextId]/_fetch.utest.js | 175 + .../context/[contextId]/_update.utest.js | 220 + .../[userId]/context/[contextId]/delete.js | 115 + .../[userId]/context/[contextId]/fetch.js | 135 + .../[userId]/context/[contextId]/update.js | 185 + .../v1/user/[userId]/context/_create.utest.js | 204 + .../v1/user/[userId]/context/_list.utest.js | 146 + .../api/v1/user/[userId]/context/create.js | 222 + .../api/v1/user/[userId]/context/list.js | 214 + platform/pages/api/v1/user/[userId]/delete.js | 123 + platform/pages/api/v1/user/[userId]/fetch.js | 136 + .../v1/user/[userId]/session/_create.utest.js | 101 + .../api/v1/user/[userId]/session/create.js | 173 + .../[userId]/token/[tokenId]/_delete.utest.js | 102 + .../[userId]/token/[tokenId]/_update.utest.js | 124 + .../user/[userId]/token/[tokenId]/delete.js | 131 + .../user/[userId]/token/[tokenId]/update.js | 175 + .../v1/user/[userId]/token/_create.utest.js | 124 + .../api/v1/user/[userId]/token/_list.utest.js | 196 + .../api/v1/user/[userId]/token/create.js | 201 + .../pages/api/v1/user/[userId]/token/list.js | 165 + platform/pages/api/v1/user/[userId]/update.js | 207 + platform/pages/api/v1/user/_create.utest.js | 246 + platform/pages/api/v1/user/_list.utest.js | 158 + platform/pages/api/v1/user/create.js | 297 + platform/pages/api/v1/user/list.js | 201 + .../api/v1/video/[videoId]/_download.utest.js | 70 + .../pages/api/v1/video/[videoId]/download.js | 26 + platform/pages/api/v1/video/_create.utest.js | 184 + platform/pages/api/v1/video/_edit.utest.js | 258 + platform/pages/api/v1/video/create.js | 92 + platform/pages/api/v1/video/edit.js | 101 + .../v1/webhook/[webhookId]/_delete.utest.js | 223 + .../v1/webhook/[webhookId]/_fetch.utest.js | 100 + .../v1/webhook/[webhookId]/_queue.utest.js | 512 + .../v1/webhook/[webhookId]/_update.utest.js | 146 + .../api/v1/webhook/[webhookId]/delete.js | 40 + .../pages/api/v1/webhook/[webhookId]/fetch.js | 68 + .../pages/api/v1/webhook/[webhookId]/queue.js | 179 + .../api/v1/webhook/[webhookId]/update.js | 85 + .../pages/api/v1/webhook/_create.utest.js | 90 + platform/pages/api/v1/webhook/_list.utest.js | 110 + platform/pages/api/v1/webhook/create.js | 74 + platform/pages/api/v1/webhook/list.js | 76 + platform/pages/audit/index.jsx | 92 + platform/pages/auto/widget/frame.jsx | 489 + platform/pages/billing/checkout.jsx | 124 + platform/pages/billing/index.jsx | 95 + platform/pages/billing/upgrade.jsx | 112 + .../_designer.applyBlueprintFromText.utest.js | 670 + .../_designer.buildSecretResources.utest.js | 77 + .../_designer.computeAutoConnections.utest.js | 418 + .../_designer.contextInput.utest.jsx | 213 + .../_designer.genericConfigurator.utest.jsx | 218 + .../_designer.getAbilityWarnings.utest.js | 222 + .../_designer.getAllowFromWarnings.utest.js | 129 + .../_designer.getBlueprintGraph.utest.js | 303 + .../_designer.getDatasetWarnings.utest.js | 102 + ...gner.getFilteredDanglingResources.utest.js | 86 + ...igner.getResourceReferenceEntries.utest.js | 95 + ..._designer.getSecretTooltipPreface.utest.js | 42 + .../_designer.getSecretWarnings.utest.js | 250 + .../[blueprintId]/_designer.group.utest.js | 81 + ...designer.hasBlueprintGraphChanges.utest.js | 1882 + .../_designer.keyboardShortcuts.utest.js | 120 + .../_designer.noteSurfaceClasses.utest.js | 81 + ...signer.resourceIdConnectionStatus.utest.js | 50 + .../_designer.secretConfigurator.utest.jsx | 200 + ...designer.serializeBlueprintToText.utest.js | 204 + ...ner.summarizeBlueprintLintResults.utest.js | 166 + .../_designer.tagTruncation.utest.jsx | 140 + ...designer.toolbarFacets.production.utest.js | 110 + .../_designer.toolbarFacets.utest.js | 241 + .../_designer.validateResourceData.utest.js | 208 + .../blueprints/[blueprintId]/designer.jsx | 20342 +++++++++ .../pages/blueprints/[blueprintId]/index.jsx | 384 + .../blueprints/[blueprintId]/preview.jsx | 267 + platform/pages/blueprints/index.jsx | 95 + platform/pages/bots/[botId]/_index.utest.jsx | 288 + platform/pages/bots/[botId]/index.jsx | 854 + platform/pages/bots/index.jsx | 90 + platform/pages/card.jsx | 15 + platform/pages/connections/[slug]/card.jsx | 25 + platform/pages/connections/[slug]/index.jsx | 477 + platform/pages/connections/[slug]/mcp.jsx | 273 + platform/pages/connections/index.jsx | 92 + .../pages/connections/sitemap.xml/index.jsx | 33 + platform/pages/contacts/[contactId]/index.jsx | 425 + platform/pages/contacts/index.jsx | 93 + .../conversations/[conversationId]/index.jsx | 922 + platform/pages/conversations/index.jsx | 168 + .../datasets/[datasetId]/_index.utest.jsx | 324 + platform/pages/datasets/[datasetId]/index.jsx | 1219 + .../[datasetId]/records/[recordId]/index.jsx | 833 + platform/pages/datasets/index.jsx | 95 + platform/pages/events/index.jsx | 92 + platform/pages/examples/[slug]/card.jsx | 28 + platform/pages/examples/[slug]/designer.jsx | 244 + platform/pages/examples/[slug]/editor.jsx | 132 + .../examples/[slug]/files.json/index.jsx | 181 + platform/pages/examples/[slug]/frame.jsx | 192 + platform/pages/examples/[slug]/index.jsx | 976 + platform/pages/examples/[slug]/preview.jsx | 94 + .../examples/airline-booking/demo/index.jsx | 1580 + .../examples/call-analyzer/demo/index.jsx | 79 + .../client-side-functions/demo/index.jsx | 239 + .../examples/custom-components/demo/index.jsx | 98 + .../email-template-generator/demo/index.jsx | 238 + .../examples/form-autofill/demo/index.jsx | 185 + platform/pages/examples/index.jsx | 601 + .../examples/message-sending/demo/index.jsx | 128 + .../examples/mini-game-maker/demo/index.jsx | 247 + .../examples/patch-assistant/demo/index.jsx | 428 + .../physics-playground/demo/index.jsx | 1494 + platform/pages/examples/rss.xml/index.jsx | 102 + platform/pages/examples/sitemap.xml/index.jsx | 25 + .../theme-color-builder/demo/index.jsx | 199 + .../pages/examples/world-map/demo/index.jsx | 348 + platform/pages/files/[fileId]/index.jsx | 358 + platform/pages/files/index.jsx | 93 + .../hub/blueprints/[blueprintId]/card.jsx | 41 + .../conversations/[conversationId]/index.jsx | 361 + .../conversations/rss.xml/index.jsx | 177 + .../hub/blueprints/[blueprintId]/designer.jsx | 271 + .../hub/blueprints/[blueprintId]/index.jsx | 580 + platform/pages/hub/blueprints/index.jsx | 159 + .../pages/hub/blueprints/latest/index.jsx | 65 + .../hub/blueprints/latest/rss.xml/index.jsx | 130 + .../blueprints/latest/sitemap.xml/index.jsx | 65 + platform/pages/hub/bots/[botId]/card.jsx | 39 + platform/pages/hub/bots/[botId]/index.jsx | 734 + platform/pages/hub/bots/index.jsx | 163 + platform/pages/hub/bots/latest/index.jsx | 65 + .../pages/hub/bots/latest/rss.xml/index.jsx | 130 + .../hub/bots/latest/sitemap.xml/index.jsx | 65 + platform/pages/hub/card.jsx | 15 + .../pages/hub/datasets/[datasetId]/card.jsx | 42 + .../pages/hub/datasets/[datasetId]/index.jsx | 520 + platform/pages/hub/datasets/index.jsx | 159 + platform/pages/hub/datasets/latest/index.jsx | 65 + .../hub/datasets/latest/rss.xml/index.jsx | 130 + .../hub/datasets/latest/sitemap.xml/index.jsx | 65 + platform/pages/hub/index.jsx | 101 + platform/pages/hub/rss.xml/index.jsx | 100 + .../pages/hub/skillsets/[skillsetId]/card.jsx | 42 + .../hub/skillsets/[skillsetId]/index.jsx | 524 + platform/pages/hub/skillsets/index.jsx | 165 + platform/pages/hub/skillsets/latest/index.jsx | 65 + .../hub/skillsets/latest/rss.xml/index.jsx | 130 + .../skillsets/latest/sitemap.xml/index.jsx | 65 + platform/pages/hub/users/[userId]/card.jsx | 37 + platform/pages/hub/users/[userId]/index.jsx | 351 + .../pages/hub/widgets/[widgetId]/card.jsx | 39 + .../pages/hub/widgets/[widgetId]/index.jsx | 459 + platform/pages/hub/widgets/index.jsx | 157 + platform/pages/hub/widgets/latest/index.jsx | 65 + .../hub/widgets/latest/rss.xml/index.jsx | 130 + .../hub/widgets/latest/sitemap.xml/index.jsx | 65 + platform/pages/index.jsx | 12 + platform/pages/integrations/_index.utest.js | 261 + .../anam/[anamIntegrationId]/404.jsx | 54 + .../_frame.contract.utest.js | 186 + .../anam/[anamIntegrationId]/_frame.utest.js | 466 + .../anam/[anamIntegrationId]/frame.jsx | 933 + .../anam/[anamIntegrationId]/index.jsx | 483 + .../anam/[anamIntegrationId]/test.jsx | 56 + .../avatar/[avatarIntegrationId]/404.jsx | 54 + .../_frame.contract.utest.js | 178 + .../[avatarIntegrationId]/_frame.utest.js | 399 + .../avatar/[avatarIntegrationId]/frame.jsx | 608 + .../avatar/[avatarIntegrationId]/index.jsx | 355 + .../avatar/[avatarIntegrationId]/test.jsx | 55 + .../discord/[discordIntegrationId]/index.jsx | 780 + .../email/[emailIntegrationId]/index.jsx | 678 + .../extract/[extractIntegrationId]/index.jsx | 597 + .../github/[githubIntegrationId]/index.jsx | 592 + .../[googlechatIntegrationId]/index.jsx | 848 + platform/pages/integrations/index.jsx | 83 + .../[instagramIntegrationId]/index.jsx | 725 + .../[mcpserverIntegrationId]/frame.jsx | 45 + .../[mcpserverIntegrationId]/index.jsx | 621 + .../[messengerIntegrationId]/index.jsx | 746 + .../[microsoftteamsIntegrationId]/index.jsx | 728 + .../notion/[notionIntegrationId]/index.jsx | 482 + .../[recallIntegrationId]/_camera.utest.js | 2889 ++ .../recall/[recallIntegrationId]/camera.tsx | 2459 ++ .../recall/[recallIntegrationId]/index.jsx | 521 + .../[recallIntegrationId]/screenshare.tsx | 85 + .../sitemap/[sitemapIntegrationId]/index.jsx | 673 + .../[skillserverIntegrationId]/index.jsx | 486 + .../slack/[slackIntegrationId]/index.jsx | 925 + .../support/[supportIntegrationId]/index.jsx | 474 + .../[telegramIntegrationId]/index.jsx | 690 + .../trigger/[triggerIntegrationId]/index.jsx | 677 + .../twilio/[twilioIntegrationId]/index.jsx | 1022 + .../[whatsappIntegrationId]/index.jsx | 772 + .../widget/[widgetIntegrationId]/404.jsx | 74 + .../_frame.Messages.stories.jsx | 635 + .../_frame.Micro.stories.jsx | 211 + .../_frame.ThemeWrapper.utest.js | 135 + .../_frame.isSafeMessageEvent.utest.js | 71 + .../_frame.postToParent.utest.js | 77 + .../_frame.resolvePoweredByColors.utest.js | 77 + .../_frame.sessionItems.utest.js | 303 + .../_frame.useSessionChannel.utest.js | 168 + .../_frame.useSessionCleanup.utest.js | 378 + .../_frame.useSessionSaveRestoreSync.utest.js | 557 + .../widget/[widgetIntegrationId]/frame.jsx | 9275 ++++ .../widget/[widgetIntegrationId]/index.jsx | 1940 + .../widget/[widgetIntegrationId]/test.jsx | 190 + platform/pages/memories/[memoryId]/index.jsx | 348 + platform/pages/memories/index.jsx | 147 + platform/pages/new/_example.utest.jsx | 153 + platform/pages/new/_examples.utest.jsx | 296 + platform/pages/new/_index.utest.js | 32 + platform/pages/new/_success.utest.js | 316 + platform/pages/new/channel.jsx | 125 + platform/pages/new/details.jsx | 86 + platform/pages/new/docs.jsx | 76 + platform/pages/new/example.jsx | 198 + platform/pages/new/examples.jsx | 261 + platform/pages/new/files.jsx | 68 + platform/pages/new/goal.jsx | 100 + platform/pages/new/hub.jsx | 334 + platform/pages/new/index.jsx | 328 + platform/pages/new/integrations.jsx | 125 + platform/pages/new/intent.jsx | 149 + platform/pages/new/messaging.jsx | 109 + platform/pages/new/success.jsx | 136 + platform/pages/new/user.jsx | 210 + platform/pages/new/website.jsx | 62 + .../pages/oauth/applications/tokens/index.jsx | 220 + platform/pages/oauth/consent.jsx | 80 + platform/pages/oauth/mcp/callback.jsx | 86 + platform/pages/overview/_index.utest.js | 317 + platform/pages/overview/index.jsx | 2751 ++ .../partner/signin/[partnerId]/index.jsx | 52 + .../partner/signin/[partnerId]/verify.jsx | 52 + platform/pages/platform/actions/index.jsx | 116 + platform/pages/platform/index.jsx | 12 + platform/pages/platform/limits/index.jsx | 90 + .../pages/platform/models/[modelId]/card.jsx | 25 + .../pages/platform/models/[modelId]/index.jsx | 115 + platform/pages/platform/models/index.jsx | 199 + .../platform/models/sitemap.xml/index.jsx | 25 + platform/pages/platform/reports/index.jsx | 97 + platform/pages/playground/[slug]/card.jsx | 29 + platform/pages/playground/ability/index.jsx | 122 + platform/pages/playground/api/index.jsx | 482 + platform/pages/playground/backstory/index.jsx | 63 + .../playground/blueprint/embed/preview.jsx | 277 + .../pages/playground/conversation/index.jsx | 206 + platform/pages/playground/entity/index.jsx | 138 + platform/pages/playground/graphql/card.jsx | 17 + platform/pages/playground/graphql/index.jsx | 809 + platform/pages/playground/html/index.jsx | 152 + platform/pages/playground/image/index.jsx | 529 + platform/pages/playground/index.jsx | 191 + platform/pages/playground/jmespath/index.jsx | 117 + platform/pages/playground/jsonpath/index.jsx | 152 + platform/pages/playground/message/index.jsx | 252 + platform/pages/playground/pdf/index.jsx | 110 + platform/pages/playground/record/index.jsx | 54 + platform/pages/playground/situation/index.jsx | 606 + platform/pages/playground/video/index.jsx | 644 + .../playground/widget/embed/designer.jsx | 116 + .../pages/playground/widget/embed/preview.jsx | 97 + platform/pages/playground/widget/index.jsx | 95 + platform/pages/policies/[policyId]/index.jsx | 399 + platform/pages/policies/index.jsx | 90 + platform/pages/portals/[portalId]/index.jsx | 481 + platform/pages/portals/index.jsx | 272 + platform/pages/ratings/[ratingId]/index.jsx | 307 + platform/pages/ratings/index.jsx | 231 + platform/pages/redirect.jsx | 43 + .../pages/secrets/[secretId]/_index.utest.js | 258 + platform/pages/secrets/[secretId]/index.jsx | 906 + .../[secretId]/manager/authenticate.tsx | 307 + .../[secretId]/manager/oauth/callback.tsx | 161 + platform/pages/secrets/index.jsx | 369 + platform/pages/secrets/oauth/callback.tsx | 197 + platform/pages/signin/index.jsx | 47 + platform/pages/signin/verify.jsx | 36 + .../skillsets/[skillsetId]/_index.utest.jsx | 321 + .../abilities/[abilityId]/index.jsx | 619 + .../pages/skillsets/[skillsetId]/index.jsx | 938 + platform/pages/skillsets/index.jsx | 161 + platform/pages/spaces/[spaceId]/index.jsx | 344 + platform/pages/spaces/index.jsx | 93 + platform/pages/support/_index.utest.js | 46 + platform/pages/support/index.jsx | 119 + platform/pages/tasks/[taskId]/_index.utest.js | 190 + platform/pages/tasks/[taskId]/index.jsx | 625 + platform/pages/tasks/index.jsx | 214 + platform/pages/teams/[teamId]/index.jsx | 398 + platform/pages/teams/index.jsx | 96 + .../pages/tokens/[tokenId]/_index.utest.js | 142 + platform/pages/tokens/[tokenId]/index.jsx | 655 + platform/pages/tokens/index.jsx | 223 + platform/pages/usage/index.jsx | 254 + platform/pages/users/[userId]/index.jsx | 385 + platform/pages/users/index.jsx | 101 + platform/pages/webhooks/[webhookId]/index.jsx | 459 + platform/pages/webhooks/index.jsx | 93 + platform/pages/welcome/_index.utest.js | 126 + platform/pages/welcome/index.jsx | 64 + platform/pages/widgets/preview/[...url].jsx | 1077 + platform/pages/widgets/preview/index.jsx | 123 + platform/polyfills/async-hooks.js | 59 + platform/polyfills/client.js | 53 + platform/polyfills/server.js | 1 + platform/postcss.config.cjs | 68 + platform/prisma/audit.ts | 418 + platform/prisma/audit.utest.js | 481 + platform/prisma/cache.ts | 357 + platform/prisma/cache.utest.js | 428 + platform/prisma/client.ts | 63 + platform/prisma/constraints.ts | 9 + platform/prisma/encryption.ts | 616 + platform/prisma/encryption.utest.js | 1583 + platform/prisma/enums.ts | 104 + platform/prisma/json.d.ts | 18 + platform/prisma/methods.ts | 483 + platform/prisma/methods.utest.js | 473 + platform/prisma/methods.utest.ts | 450 + platform/prisma/models.ts | 7 + platform/prisma/nulls.ts | 5 + platform/prisma/retry.ts | 255 + platform/prisma/retry.utest.js | 283 + platform/prisma/sql.ts | 1 + platform/prisma/types.ts | 3 + platform/prisma/utils.ts | 8 + platform/prisma/zod.ts | 8 + platform/prompts/answer_v1.yaml | 13 + platform/prompts/audio_generator_v1.yaml | 15 + platform/prompts/auth_generator_v1.yaml | 48 + platform/prompts/auto_agent_v1.yaml | 28 + platform/prompts/batch_v1.yaml | 26 + platform/prompts/blueprint_assistant_v1.yaml | 245 + platform/prompts/buttons_generator_v1.yaml | 18 + platform/prompts/canvas_generator_v1.yaml | 36 + platform/prompts/carousel_generator_v1.yaml | 20 + platform/prompts/dashboard_assistant_v1.yaml | 84 + platform/prompts/detect_intent_v1.yaml | 43 + platform/prompts/detect_intent_v2.yaml | 43 + platform/prompts/detect_intent_v3.yaml | 43 + platform/prompts/detect_intent_v4.yaml | 43 + platform/prompts/detect_search_intent_v1.yaml | 11 + platform/prompts/detect_search_intent_v2.yaml | 31 + platform/prompts/detect_search_intent_v3.yaml | 31 + platform/prompts/detect_search_intent_v4.yaml | 40 + platform/prompts/diligence_v1.yaml | 42 + .../prompts/extract_contact_details_v1.yaml | 40 + .../prompts/extract_contact_details_v2.yaml | 43 + platform/prompts/footnotes_generator_v1.yaml | 50 + platform/prompts/form_generator_v1.yaml | 37 + .../generate_backstory_from_text_v1.yaml | 27 + .../generate_backstory_from_text_v2.yaml | 31 + .../generate_backstory_from_text_v3.yaml | 32 + .../generate_backstory_from_text_v4.yaml | 32 + .../generate_backstory_from_text_v5.yaml | 189 + .../prompts/generate_cron_schedule_v1.yaml | 52 + platform/prompts/generate_description_v1.yaml | 21 + platform/prompts/generate_description_v2.yaml | 24 + platform/prompts/generate_description_v3.yaml | 27 + .../prompts/generate_faq_from_record_v1.yaml | 14 + platform/prompts/generate_form_v1.yaml | 35 + .../generate_instruction_from_text_v1.yaml | 53 + .../generate_instruction_from_text_v2.yaml | 116 + .../generate_instruction_from_text_v3.yaml | 173 + platform/prompts/generate_json_v1.yaml | 6 + platform/prompts/generate_text_v1.yaml | 6 + platform/prompts/initiate_message_v1.yaml | 39 + platform/prompts/language_selection_v1.yaml | 9 + platform/prompts/markdown_generator_v1.yaml | 13 + platform/prompts/math_generator_v1.yaml | 45 + platform/prompts/mermaid_generator_v1.yaml | 41 + platform/prompts/personalization_v1.yaml | 18 + platform/prompts/references_generator_v1.yaml | 50 + platform/prompts/reprogramming_v1.yaml | 21 + platform/prompts/silent_v1.yaml | 17 + platform/prompts/summarize_text_v1.yaml | 11 + platform/prompts/timeout_marks_v1.yaml | 13 + .../prompts/transform_instruction_v1.yaml | 38 + .../prompts/transform_instruction_v2.yaml | 38 + .../prompts/transform_instruction_v3.yaml | 62 + platform/prompts/translate_text_v1.yaml | 10 + platform/prompts/types.d.ts | 14 + platform/prompts/website_assistant_v1.yaml | 45 + platform/public/404.txt | 0 platform/public/android-chrome-192x192.png | 3 + platform/public/android-chrome-512x512.png | 3 + platform/public/apple-touch-icon.png | 3 + platform/public/apps/ab501ab5/banner.png | 3 + platform/public/apps/c0de9a7f/banner.png | 3 + platform/public/apps/chat/banner.png | 3 + platform/public/apps/f2a9c7d4/banner.png | 3 + platform/public/apps/inbox/banner.png | 3 + platform/public/avatars/friend.mp4 | 3 + .../public/examples/ai-answers/chatgpt.png | 3 + .../examples/airline-booking/banner.png | 3 + .../airline-booking/destinations/bali.jpg | 3 + .../airline-booking/destinations/dubai.jpg | 3 + .../airline-booking/destinations/newyork.jpg | 3 + .../airline-booking/destinations/paris.jpg | 3 + .../airline-booking/destinations/rome.jpg | 3 + .../airline-booking/destinations/tokyo.jpg | 3 + .../public/examples/airline-booking/icon.png | 3 + .../discord-ai-image-generator-bot/logo.png | 3 + .../discord-community-bot/discord.png | 3 + .../examples/discord-community-bot/user.png | 3 + .../hotel-booking-bot/deluxe-suite.jpg | 3 + .../hotel-booking-bot/double-queen-room.jpg | 3 + .../examples/hotel-booking-bot/room-view.jpg | 3 + .../public/examples/image-bot/logo.old.png | 3 + platform/public/examples/image-bot/logo.png | 3 + .../examples/liquid-glass/background.jpg | 3 + .../examples/magento-assistant/targus.png | 3 + .../movie-recommendation-chatbot/banner.png | 3 + .../examples/personal-advisor/banner.png | 3 + .../examples/product-finder/iphone-11.jpg | 3 + .../product-finder/iphone-12-mini.jpg | 3 + .../examples/product-finder/iphone-se.jpg | 3 + .../examples/retro-gaming-bot/amiga.png | 3 + .../examples/shopify-assistant/burton.png | 3 + .../slack-corporate-ai-bot/avatar.png | 3 + .../examples/slack-corporate-ai-bot/slack.png | 3 + .../public/examples/super-clippy/clippy.png | 3 + .../whatsapp-support-ai-bot/whatsapp.png | 3 + platform/public/favicon-dark.ico | 3 + platform/public/favicon-light.ico | 3 + platform/public/favicon.ico | 3 + platform/public/fonts/inter/400.ttf | 3 + platform/public/fonts/inter/700.ttf | 3 + platform/public/fonts/inter/900.ttf | 3 + platform/public/fonts/twemoji/twemoji.woff2 | 3 + platform/public/icon-dark.svg | 8 + platform/public/icon-light.svg | 8 + platform/public/icon.png | 3 + platform/public/icon.svg | 8 + platform/public/llms.txt | 164 + platform/public/logo.png | 3 + platform/public/logo.svg | 26 + .../052dc948-8c4c-419f-b8eb-d4e31fd2983a.mp4 | 3 + .../09d1e92b-a469-42f2-803f-7530e9fa9c81.png | 3 + .../0ed3a64a-b5e6-44fe-b6cc-5266d63b6f4e.png | 3 + .../32aae0f1-9a8b-49c0-86ed-9f090339a037.png | 3 + .../37f42535-449b-4478-9a9e-8a5b651fdc91.png | 3 + .../385f0bc2-f9cc-425c-9b76-91641ebb32bf.png | 3 + .../402ce413-1afc-4978-b908-a3bf815714ea.png | 3 + .../44f544b1-08ac-466b-a535-2b633af7eb72.png | 3 + .../57685a09-e010-4596-a1cc-bc58aac2bdcd.png | 3 + .../5e48b323-26c5-4f0e-8425-b6a5eec4dd34.png | 3 + .../7923e23c-1008-47b2-ba9f-d2651dde5f8a.png | 3 + .../7cbc9f4d-11b2-457b-bcea-743784fbcf08.png | 3 + .../96ed5f99-9495-42bd-a26d-cab4b659be6c.png | 3 + .../99e54aaa-1fa8-4108-9bc5-0b3334fb4a6b.png | 3 + .../a4f64366-9d08-4e2d-80d4-70d463e22ca8.mp4 | 3 + .../abbd424d-f2b1-4070-9930-f868ee1e9f18.png | 3 + .../c2ced854-aecd-4f45-99f9-ba1b2ddc67ca.png | 3 + .../d7375c5d-a92d-45ca-ba08-f74d7140abb2.png | 3 + .../d82629ef-36ad-474a-ba70-6bb2a9932239.png | 3 + .../d8895253-04f5-4325-9064-c9c947f488ad.png | 3 + .../d8a98b56-2106-46c3-bc4e-ddabcd79686c.png | 3 + .../d921e502-3f3e-47e6-9aa8-1064a8a7d8b7.png | 3 + .../dac1ed5e-42cb-47be-8ec8-946ae82b9434.png | 3 + .../dde0daeb-2699-4111-b7b7-a7cb2c8edd37.png | 3 + .../e82a8649-8c59-452e-aacc-ae5ff059b4aa.png | 3 + .../ea2084b0-39c0-476f-a244-da827b3fe902.mp4 | 3 + .../ea648b32-84ef-4991-8960-f10053465647.png | 3 + .../ef2470a8-b8ed-4822-92cf-b8adea32a231.png | 3 + .../fb69bc0e-9274-4bb8-916e-d03e5b0efbe2.png | 3 + .../fda04eba-f291-4a67-88f8-aed4b8fafc6b.mp4 | 3 + platform/public/site.webmanifest | 19 + .../_triggerIntegrationSchedule.utest.js | 106 + platform/schemas/abilityDescription.js | 6 + platform/schemas/abilityDescription.utest.js | 135 + platform/schemas/abilityInstruction.js | 19 + platform/schemas/abilityInstruction.utest.js | 190 + platform/schemas/abilityName.js | 6 + platform/schemas/abilityName.utest.js | 110 + platform/schemas/alias.js | 150 + platform/schemas/alias.utest.js | 104 + platform/schemas/api/v1/ability.ts | 40 + platform/schemas/api/v1/anamIntegration.ts | 40 + platform/schemas/api/v1/avatarIntegration.ts | 40 + platform/schemas/api/v1/bot.ts | 40 + platform/schemas/api/v1/dataset.ts | 47 + platform/schemas/api/v1/discordIntegration.ts | 42 + platform/schemas/api/v1/emailIntegration.ts | 42 + platform/schemas/api/v1/extractIntegration.ts | 40 + platform/schemas/api/v1/file.ts | 40 + platform/schemas/api/v1/githubIntegration.ts | 40 + .../schemas/api/v1/googlechatIntegration.ts | 42 + .../schemas/api/v1/instagramIntegration.ts | 43 + .../schemas/api/v1/mcpserverIntegration.ts | 41 + .../schemas/api/v1/messengerIntegration.ts | 43 + .../api/v1/microsoftteamsIntegration.ts | 42 + platform/schemas/api/v1/notionIntegration.ts | 42 + platform/schemas/api/v1/oAuthConnection.ts | 39 + platform/schemas/api/v1/policy.ts | 40 + platform/schemas/api/v1/portal.ts | 40 + platform/schemas/api/v1/recallIntegration.ts | 40 + platform/schemas/api/v1/secret.ts | 39 + platform/schemas/api/v1/sitemapIntegration.ts | 42 + .../schemas/api/v1/skillserverIntegration.ts | 41 + platform/schemas/api/v1/skillset.ts | 40 + platform/schemas/api/v1/slackIntegration.ts | 42 + platform/schemas/api/v1/space.ts | 40 + platform/schemas/api/v1/supportIntegration.ts | 40 + platform/schemas/api/v1/task.ts | 54 + .../schemas/api/v1/telegramIntegration.ts | 42 + platform/schemas/api/v1/triggerIntegration.ts | 51 + platform/schemas/api/v1/twilioIntegration.ts | 40 + .../schemas/api/v1/whatsappIntegration.ts | 45 + platform/schemas/api/v1/widgetIntegration.ts | 84 + platform/schemas/backstory.js | 9 + platform/schemas/backstory.utest.js | 82 + platform/schemas/blueprintConfig.js | 79 + platform/schemas/blueprintId.js | 66 + platform/schemas/blueprintId.utest.js | 28 + platform/schemas/botConfig.js | 21 + platform/schemas/botId.js | 61 + platform/schemas/botId.utest.js | 196 + platform/schemas/contactFingerprint.js | 40 + platform/schemas/contactFingerprint.utest.js | 474 + platform/schemas/contactId.js | 518 + platform/schemas/contactId.utest.js | 423 + platform/schemas/conversationId.js | 62 + platform/schemas/conversationId.utest.js | 28 + platform/schemas/datasetId.js | 64 + platform/schemas/datasetId.utest.js | 215 + platform/schemas/dbMediumText.js | 9 + platform/schemas/dbString.js | 9 + platform/schemas/dbString.utest.js | 107 + platform/schemas/dbText.js | 9 + platform/schemas/description.js | 9 + platform/schemas/description.utest.js | 87 + platform/schemas/description2.js | 24 + platform/schemas/description2.utest.js | 28 + platform/schemas/events.ts | 17 + platform/schemas/events.utest.js | 39 + platform/schemas/expiresAt.js | 8 + platform/schemas/externalUrl.js | 36 + platform/schemas/externalUrl.utest.js | 67 + platform/schemas/features.js | 14 + platform/schemas/features.utest.js | 232 + platform/schemas/fileId.js | 61 + platform/schemas/functionsSchema.js | 40 + platform/schemas/functionsSchema.utest.js | 220 + platform/schemas/icon.js | 16 + platform/schemas/icon.utest.js | 92 + platform/schemas/imageModel.js | 17 + platform/schemas/inlineExtensions.ts | 137 + platform/schemas/inlineExtensions.utest.js | 200 + platform/schemas/jsonSchema.js | 205 + platform/schemas/jsonSchema.utest.js | 647 + platform/schemas/languageModel.js | 116 + platform/schemas/languageModel.utest.js | 316 + platform/schemas/messageId.js | 72 + platform/schemas/messageId.utest.js | 28 + platform/schemas/messageText.js | 6 + platform/schemas/messageText.utest.js | 97 + platform/schemas/messageType.js | 6 + platform/schemas/messageType.utest.js | 106 + platform/schemas/messages.js | 111 + platform/schemas/messages.utest.js | 65 + platform/schemas/meta.js | 4 + platform/schemas/meta.utest.js | 67 + platform/schemas/name.js | 9 + platform/schemas/name.utest.js | 78 + platform/schemas/name2.js | 24 + platform/schemas/name2.utest.js | 28 + platform/schemas/namespace.js | 25 + platform/schemas/namespace.utest.js | 155 + platform/schemas/notUsed.js | 5 + platform/schemas/notUsed.utest.js | 23 + platform/schemas/oAuthConnectionId.js | 50 + platform/schemas/recordText.js | 6 + platform/schemas/recordText.utest.js | 107 + platform/schemas/request.js | 14 + platform/schemas/request.utest.js | 129 + platform/schemas/reranker.js | 14 + platform/schemas/reranker.utest.js | 81 + platform/schemas/schedule.js | 20 + platform/schemas/schedule.utest.js | 20 + platform/schemas/secretId.js | 61 + platform/schemas/secretId.utest.js | 28 + platform/schemas/skillsetId.js | 64 + platform/schemas/skillsetId.utest.js | 215 + platform/schemas/slug.js | 70 + platform/schemas/slug.utest.js | 133 + platform/schemas/source.js | 4 + platform/schemas/source.utest.js | 114 + platform/schemas/spaceId.js | 52 + platform/schemas/spaceSiteSlug.js | 14 + platform/schemas/spaceSiteSlug.utest.js | 29 + platform/schemas/state.js | 20 + platform/schemas/structstr.js | 14 + platform/schemas/structstr.utest.js | 82 + platform/schemas/taskId.js | 52 + platform/schemas/taskId.utest.js | 28 + platform/schemas/taskSchedule.js | 14 + platform/schemas/taskSchedule.utest.js | 103 + platform/schemas/theme.js | 37 + platform/schemas/theme.utest.js | 147 + platform/schemas/timezone.js | 16 + platform/schemas/trigger.js | 9 + platform/schemas/trigger.utest.js | 41 + .../schemas/triggerIntegrationSchedule.js | 14 + platform/schemas/videoModel.js | 14 + platform/scripts/README.md | 15 + platform/scripts/adjust-record-usage.js | 153 + platform/scripts/adjust-usage.js | 93 + .../scripts/backfill-database-encryption.js | 220 + platform/scripts/build-api-spec.ts | 1107 + platform/scripts/build-templates.js | 17 + platform/scripts/cleanup-conversations.js | 337 + .../scripts/cleanup-expired-conversations.js | 73 + platform/scripts/cleanup-old-audit-logs.js | 45 + platform/scripts/cleanup-old-event-logs.js | 45 + platform/scripts/cleanup-old-event-metrics.js | 45 + platform/scripts/cleanup-old-usage-records.js | 45 + platform/scripts/count-tokens.js | 63 + platform/scripts/create-team.js | 65 + platform/scripts/db-push.js | 55 + platform/scripts/decrypt.js | 30 + platform/scripts/delete-bot.js | 71 + platform/scripts/delete-dataset.js | 71 + platform/scripts/delete-skillset.js | 71 + platform/scripts/delete-user.js | 63 + platform/scripts/delete-users-by-domain.js | 110 + platform/scripts/emfile-guard.cjs | 44 + platform/scripts/encrypt.js | 30 + platform/scripts/explore-sandbox.js | 354 + platform/scripts/export-dataset-to-csv.js | 103 + platform/scripts/export-dataset-to-dataset.js | 120 + platform/scripts/export-dataset-to-json.js | 100 + platform/scripts/fetch-sitemap-urls.js | 279 + platform/scripts/fetch-url-content.js | 128 + platform/scripts/find-ability.js | 57 + platform/scripts/find-empty-basic-info.js | 152 + platform/scripts/find-empty-references.js | 105 + platform/scripts/find-expired-tasks.js | 46 + platform/scripts/find-session.js | 45 + platform/scripts/find-unscheduled-tasks.js | 72 + platform/scripts/find-user.js | 51 + platform/scripts/generate-api-types.js | 225 + platform/scripts/generate-encryption-key.js | 25 + platform/scripts/get-contact-secret.js | 51 + platform/scripts/get-idle-conversations.js | 28 + platform/scripts/get-usage.js | 67 + platform/scripts/inspect-user-usage.js | 68 + platform/scripts/introspect-db.js | 27 + platform/scripts/list-recent-messages.js | 63 + platform/scripts/list-recent-usage.js | 59 + platform/scripts/list-space-storage.js | 175 + platform/scripts/move-bot.js | 379 + platform/scripts/move-dataset.js | 167 + platform/scripts/nuke-account.js | 1301 + platform/scripts/print-env.js | 104 + platform/scripts/print-system.js | 29 + platform/scripts/prisma-query.js | 128 + platform/scripts/rename-user-email.js | 104 + platform/scripts/reset-usage.js | 62 + platform/scripts/run-db-placeholder.js | 36 + platform/scripts/run-proxy.js | 133 + platform/scripts/run-with-environment.js | 200 + platform/scripts/seed-db.js | 9 + platform/scripts/setup-prisma-client.js | 38 + platform/scripts/show-conversation.js | 103 + platform/scripts/simulate-conversation.js | 153 + platform/scripts/smoke-selfhost.js | 347 + platform/scripts/test-mcp-server.js | 66 + platform/scripts/trigger-clock-10.js | 35 + platform/scripts/trigger-conversation-idle.js | 59 + platform/scripts/trigger-dataset-cleanup.js | 35 + platform/scripts/trigger-task-schedule.js | 52 + platform/scripts/trigger-task-stalled.js | 52 + platform/scripts/trigger-task-trigger.js | 52 + platform/scripts/verify-bundle-modules.js | 112 + platform/scripts/yaml-esm-hooks.mjs | 32 + platform/scripts/yaml-esm-register.mjs | 18 + platform/stories/Introduction.mdx | 13 + platform/stories/Welcome.stories.tsx | 47 + platform/stories/assets/.gitkeep | 0 platform/styles/globals.css | 1690 + platform/tailwind.config.js | 309 + platform/templates/agent.js | 74 + platform/templates/channels.js | 157 + platform/templates/channels.utest.js | 103 + platform/templates/example.js | 108 + platform/templates/example.utest.js | 72 + platform/templates/hub.js | 295 + platform/templates/hub.utest.js | 354 + platform/templates/index.js | 68 + platform/templates/messaging.js | 232 + platform/templates/onboarding.js | 116 + platform/templates/onboarding.utest.js | 166 + platform/templates/original.js | 453 + platform/templates/widget.js | 174 + platform/templates/widget.utest.js | 115 + platform/tests/config/apps.utest.js | 12 + platform/tests/config/env.utest.ts | 75 + platform/tests/config/events.utest.js | 34 + platform/tests/config/headers.utest.js | 20 + .../tests/config/host-configuration.utest.js | 128 + platform/tests/config/limits.utest.ts | 415 + platform/tests/config/models.utest.js | 102 + platform/tests/config/platform.utest.js | 67 + platform/tests/config/providers.utest.js | 146 + platform/tests/config/site.utest.js | 29 + platform/tests/config/site.utest.ts | 113 + .../integration/api-security-headers.itest.js | 79 + .../tests/integration/conversation.itest.js | 176 + platform/tests/integration/dataset.itest.js | 96 + platform/tests/integration/file.itest.js | 35 + platform/tests/integration/openai.itest.js | 127 + platform/tests/integration/skillset.itest.js | 97 + platform/tests/integration/status.itest.js | 17 + platform/tests/integration/widget.itest.js | 41 + platform/tests/patch/next-auth.utest.js | 7 + platform/tests/setup/msw.utest.js | 53 + platform/tsconfig.json | 81 + platform/types/global.d.ts | 5 + platform/types/next-auth.d.ts | 41 + platform/vercel.json | 7 + platform/webpack/README.md | 1 + platform/webpack/loaders/json-gz.cjs | 41 + platform/webpack/loaders/json-gz.utest.js | 283 + platform/webpack/loaders/md-frontmatter.js | 66 + .../webpack/loaders/md-frontmatter.utest.js | 139 + platform/webpack/loaders/yaml.js | 70 + platform/webpack/loaders/yaml.utest.js | 584 + .../webpack/plugins/validate-sourcemaps.js | 189 + .../plugins/validate-sourcemaps.utest.js | 575 + platform/webpack/stubs/empty-page.js | 8 + platform/workers/file.worker.js | 111 + platform/workers/gpt.worker.js | 85 + platform/workers/highlighter.worker.js | 63 + platform/workers/highlighter.worker.utest.js | 36 + pnpm-lock.yaml | 34958 +++++++++++++++ pnpm-workspace.yaml | 431 + stubs/LICENSE | 15 + stubs/README.md | 6 + stubs/canvas/README.md | 33 + stubs/canvas/index.js | 0 stubs/canvas/package.json | 12 + stubs/napi-rs-canvas/README.md | 37 + stubs/napi-rs-canvas/index.d.ts | 84 + stubs/napi-rs-canvas/index.js | 18 + stubs/napi-rs-canvas/package.json | 13 + stubs/nodemailer/README.md | 29 + stubs/nodemailer/index.js | 0 stubs/nodemailer/package.json | 12 + stubs/pdfjs-dist/README.md | 36 + stubs/pdfjs-dist/index.js | 19 + stubs/pdfjs-dist/package.json | 19 + stubs/re2/README.md | 44 + stubs/re2/index.js | 0 stubs/re2/package.json | 12 + turbo.json | 18 + 5497 files changed, 1374509 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/_verify.yaml create mode 100644 .github/workflows/publish-ghcr-platform.yaml create mode 100644 .github/workflows/pull-request.yaml create mode 100644 .gitignore create mode 100644 .pnpmfile.cjs create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 LICENSING.md create mode 100644 NOTICE create mode 100644 SECURITY.md create mode 100644 SUPPORT.md create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile create mode 100644 docker/distro/AGENTS.md create mode 100644 docker/distro/community/compose.yml create mode 100644 docker/entrypoint.sh create mode 100644 docker/garage/garage.toml create mode 100644 docker/garage/init.mjs create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/configuration.md create mode 100644 docs/deployment.md create mode 100644 docs/getting-started.md create mode 100644 docs/module-defaults.md create mode 100644 eslint.config.mjs create mode 100644 package.json create mode 100644 packages/AGENTS.md create mode 100644 packages/README.md create mode 100644 packages/auxiliary-google-calendar/README.md create mode 100644 packages/auxiliary-google-calendar/jest.config.js create mode 100644 packages/auxiliary-google-calendar/package.json create mode 100644 packages/auxiliary-google-calendar/src/index.test.ts create mode 100644 packages/auxiliary-google-calendar/src/index.ts create mode 100644 packages/auxiliary-google-calendar/tsconfig.json create mode 100644 packages/auxiliary-google-docs/README.md create mode 100644 packages/auxiliary-google-docs/jest.config.js create mode 100644 packages/auxiliary-google-docs/package.json create mode 100644 packages/auxiliary-google-docs/src/index.test.ts create mode 100644 packages/auxiliary-google-docs/src/index.ts create mode 100644 packages/auxiliary-google-docs/tsconfig.json create mode 100644 packages/auxiliary-google-mail/README.md create mode 100644 packages/auxiliary-google-mail/jest.config.js create mode 100644 packages/auxiliary-google-mail/package.json create mode 100644 packages/auxiliary-google-mail/src/index.test.ts create mode 100644 packages/auxiliary-google-mail/src/index.ts create mode 100644 packages/auxiliary-google-mail/tsconfig.json create mode 100644 packages/auxiliary-google-meet/README.md create mode 100644 packages/auxiliary-google-meet/jest.config.js create mode 100644 packages/auxiliary-google-meet/package.json create mode 100644 packages/auxiliary-google-meet/src/index.test.ts create mode 100644 packages/auxiliary-google-meet/src/index.ts create mode 100644 packages/auxiliary-google-meet/tsconfig.json create mode 100644 packages/auxiliary-graphql/README.md create mode 100644 packages/auxiliary-graphql/jest.config.js create mode 100644 packages/auxiliary-graphql/package.json create mode 100644 packages/auxiliary-graphql/src/builder.ts create mode 100644 packages/auxiliary-graphql/src/http-field.ts create mode 100644 packages/auxiliary-graphql/src/index.test.ts create mode 100644 packages/auxiliary-graphql/src/index.ts create mode 100644 packages/auxiliary-graphql/src/notion.ts create mode 100644 packages/auxiliary-graphql/src/schema.ts create mode 100644 packages/auxiliary-graphql/src/slack.ts create mode 100644 packages/auxiliary-graphql/tsconfig.json create mode 100644 packages/auxiliary-microsoft-drive/README.md create mode 100644 packages/auxiliary-microsoft-drive/jest.config.js create mode 100644 packages/auxiliary-microsoft-drive/package.json create mode 100644 packages/auxiliary-microsoft-drive/src/index.test.ts create mode 100644 packages/auxiliary-microsoft-drive/src/index.ts create mode 100644 packages/auxiliary-microsoft-drive/tsconfig.json create mode 100644 packages/auxiliary-microsoft-sharepoint/README.md create mode 100644 packages/auxiliary-microsoft-sharepoint/jest.config.js create mode 100644 packages/auxiliary-microsoft-sharepoint/package.json create mode 100644 packages/auxiliary-microsoft-sharepoint/src/index.test.ts create mode 100644 packages/auxiliary-microsoft-sharepoint/src/index.ts create mode 100644 packages/auxiliary-microsoft-sharepoint/tsconfig.json create mode 100644 packages/auxiliary-notion/README.md create mode 100644 packages/auxiliary-notion/jest.config.js create mode 100644 packages/auxiliary-notion/package.json create mode 100644 packages/auxiliary-notion/src/client.test.ts create mode 100644 packages/auxiliary-notion/src/client.ts create mode 100644 packages/auxiliary-notion/src/contents.ts create mode 100644 packages/auxiliary-notion/src/database.test.ts create mode 100644 packages/auxiliary-notion/src/database.ts create mode 100644 packages/auxiliary-notion/src/errors.test.ts create mode 100644 packages/auxiliary-notion/src/errors.ts create mode 100644 packages/auxiliary-notion/src/handler.ts create mode 100644 packages/auxiliary-notion/src/index.ts create mode 100644 packages/auxiliary-notion/src/page.test.ts create mode 100644 packages/auxiliary-notion/src/page.ts create mode 100644 packages/auxiliary-notion/src/sql.test.ts create mode 100644 packages/auxiliary-notion/src/sql.ts create mode 100644 packages/auxiliary-notion/tsconfig.json create mode 100644 packages/batch-spec/package.json create mode 100644 packages/batch-spec/src/index.ts create mode 100644 packages/batch-spec/tsconfig.json create mode 100644 packages/batch/jest.config.js create mode 100644 packages/batch/package.json create mode 100644 packages/batch/src/index.test.js create mode 100644 packages/batch/src/index.ts create mode 100644 packages/batch/tsconfig.json create mode 100644 packages/billing-spec/package.json create mode 100644 packages/billing-spec/src/index.ts create mode 100644 packages/billing-spec/tsconfig.json create mode 100644 packages/billing/package.json create mode 100644 packages/billing/src/assert.ts create mode 100644 packages/billing/src/config.ts create mode 100644 packages/billing/src/gates.ts create mode 100644 packages/billing/src/index.ts create mode 100644 packages/billing/src/model.ts create mode 100644 packages/billing/src/provider.ts create mode 100644 packages/billing/src/trial.ts create mode 100644 packages/billing/tsconfig.json create mode 100644 packages/blacklists/jest.config.js create mode 100644 packages/blacklists/package.json create mode 100644 packages/blacklists/src/domains.ts create mode 100644 packages/blacklists/src/index.test.ts create mode 100644 packages/blacklists/src/index.ts create mode 100644 packages/blacklists/tsconfig.json create mode 100644 packages/buffer/README.md create mode 100644 packages/buffer/jest.config.js create mode 100644 packages/buffer/package.json create mode 100644 packages/buffer/src/index.test.ts create mode 100644 packages/buffer/src/index.ts create mode 100644 packages/buffer/tsconfig.json create mode 100644 packages/cloak/LICENSE.md create mode 100644 packages/cloak/README.md create mode 100644 packages/cloak/jest.config.js create mode 100644 packages/cloak/package.json create mode 100644 packages/cloak/src/ciphers/aes-gcm.ts create mode 100644 packages/cloak/src/index.test.ts create mode 100644 packages/cloak/src/index.ts create mode 100644 packages/cloak/src/key.test.ts create mode 100644 packages/cloak/src/key.ts create mode 100644 packages/cloak/src/keychain.ts create mode 100644 packages/cloak/src/message.ts create mode 100644 packages/cloak/tsconfig.json create mode 100644 packages/db-spec/README.md create mode 100644 packages/db-spec/jest.config.js create mode 100644 packages/db-spec/package.json create mode 100644 packages/db-spec/prisma/schema.prisma create mode 100644 packages/db-spec/prisma/sql/breakdownTotalContactsWithConversationsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/breakdownTotalConversationsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/breakdownTotalMessagesOfTypeOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/breakdownTotalMessagesOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/breakdownTotalRatingsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/breakdownTotalUsageTokensOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getAverageMessagesOfTypeOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getBotConversationCountByDay.sql create mode 100644 packages/db-spec/prisma/sql/getBotConversationsWithUserMessageCount.sql create mode 100644 packages/db-spec/prisma/sql/getBotMessageCountByDay.sql create mode 100644 packages/db-spec/prisma/sql/getBotSingleTurnConversationCount.sql create mode 100644 packages/db-spec/prisma/sql/getBotUsageStats.sql create mode 100644 packages/db-spec/prisma/sql/getConversationUsageStats.sql create mode 100644 packages/db-spec/prisma/sql/getDailyNegativeRatingCount.sql create mode 100644 packages/db-spec/prisma/sql/getEventMetricSeriesOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalAbilitiesForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalBotsForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalContacts.sql create mode 100644 packages/db-spec/prisma/sql/getTotalContactsWithConversationsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalConversationsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalConversationsWithNumberedMessagesOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalDatasetsForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalFilesForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalMessagesOfTypeOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalMessagesOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalPoliciesForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalPortalsForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalRatingsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalSkillsetsForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalTeamMembersForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalTeamsForUser.sql create mode 100644 packages/db-spec/prisma/sql/getTotalThumbsDownOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalThumbsUpOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalUsageTokensOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getTotalUsersForUser.sql create mode 100644 packages/db-spec/prisma/sql/getUsageCountByTypeOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/getUsageCountByTypeSince.sql create mode 100644 packages/db-spec/prisma/sql/listContacts.sql create mode 100644 packages/db-spec/prisma/sql/listContactsWithConversationsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listContactsWithMessagesOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listContactsWithRatingsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listConversationsOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listConversationsWithNumberedMessagesOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listEventLogsOfTypeActionsGroupedByTypeOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listTopBotsByTokenUsageOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listTopContactsByTokenUsageOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listTopDownvotersOverPeriod.sql create mode 100644 packages/db-spec/prisma/sql/listTopUpvotersOverPeriod.sql create mode 100644 packages/db-spec/prisma/zod-generator.config.json create mode 100644 packages/db-spec/scripts/post-generate-zod.js create mode 100644 packages/db-spec/src/derive.js create mode 100644 packages/db-spec/src/types.test.js create mode 100644 packages/db-spec/src/types.ts create mode 100644 packages/db-spec/tsconfig.json create mode 100644 packages/db/.gitignore create mode 100644 packages/db/README.md create mode 100644 packages/db/package.json create mode 100644 packages/db/prisma.config.ts create mode 100644 packages/db/prisma/schema.prisma create mode 100644 packages/db/prisma/sql/breakdownTotalContactsWithConversationsOverPeriod.sql create mode 100644 packages/db/prisma/sql/breakdownTotalConversationsOverPeriod.sql create mode 100644 packages/db/prisma/sql/breakdownTotalMessagesOfTypeOverPeriod.sql create mode 100644 packages/db/prisma/sql/breakdownTotalMessagesOverPeriod.sql create mode 100644 packages/db/prisma/sql/breakdownTotalRatingsOverPeriod.sql create mode 100644 packages/db/prisma/sql/breakdownTotalUsageTokensOverPeriod.sql create mode 100644 packages/db/prisma/sql/getAverageMessagesOfTypeOverPeriod.sql create mode 100644 packages/db/prisma/sql/getBotConversationCountByDay.sql create mode 100644 packages/db/prisma/sql/getBotConversationsWithUserMessageCount.sql create mode 100644 packages/db/prisma/sql/getBotMessageCountByDay.sql create mode 100644 packages/db/prisma/sql/getBotSingleTurnConversationCount.sql create mode 100644 packages/db/prisma/sql/getBotUsageStats.sql create mode 100644 packages/db/prisma/sql/getConversationUsageStats.sql create mode 100644 packages/db/prisma/sql/getDailyNegativeRatingCount.sql create mode 100644 packages/db/prisma/sql/getEventMetricSeriesOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalAbilitiesForUser.sql create mode 100644 packages/db/prisma/sql/getTotalBotsForUser.sql create mode 100644 packages/db/prisma/sql/getTotalContacts.sql create mode 100644 packages/db/prisma/sql/getTotalContactsWithConversationsOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalConversationsOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalConversationsWithNumberedMessagesOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalDatasetsForUser.sql create mode 100644 packages/db/prisma/sql/getTotalFilesForUser.sql create mode 100644 packages/db/prisma/sql/getTotalMessagesOfTypeOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalMessagesOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalPoliciesForUser.sql create mode 100644 packages/db/prisma/sql/getTotalPortalsForUser.sql create mode 100644 packages/db/prisma/sql/getTotalRatingsOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalSkillsetsForUser.sql create mode 100644 packages/db/prisma/sql/getTotalTeamMembersForUser.sql create mode 100644 packages/db/prisma/sql/getTotalTeamsForUser.sql create mode 100644 packages/db/prisma/sql/getTotalThumbsDownOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalThumbsUpOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalUsageTokensOverPeriod.sql create mode 100644 packages/db/prisma/sql/getTotalUsersForUser.sql create mode 100644 packages/db/prisma/sql/getUsageCountByTypeOverPeriod.sql create mode 100644 packages/db/prisma/sql/getUsageCountByTypeSince.sql create mode 100644 packages/db/prisma/sql/listContacts.sql create mode 100644 packages/db/prisma/sql/listContactsWithConversationsOverPeriod.sql create mode 100644 packages/db/prisma/sql/listContactsWithMessagesOverPeriod.sql create mode 100644 packages/db/prisma/sql/listContactsWithRatingsOverPeriod.sql create mode 100644 packages/db/prisma/sql/listConversationsOverPeriod.sql create mode 100644 packages/db/prisma/sql/listConversationsWithNumberedMessagesOverPeriod.sql create mode 100644 packages/db/prisma/sql/listEventLogsOfTypeActionsGroupedByTypeOverPeriod.sql create mode 100644 packages/db/prisma/sql/listTopBotsByTokenUsageOverPeriod.sql create mode 100644 packages/db/prisma/sql/listTopContactsByTokenUsageOverPeriod.sql create mode 100644 packages/db/prisma/sql/listTopDownvotersOverPeriod.sql create mode 100644 packages/db/prisma/sql/listTopUpvotersOverPeriod.sql create mode 100644 packages/db/prisma/zod-generator.config.json create mode 100644 packages/db/scripts/derive.js create mode 100644 packages/db/scripts/generate.js create mode 100644 packages/db/src/constraints.ts create mode 100644 packages/db/src/index.ts create mode 100644 packages/db/tsconfig.json create mode 100644 packages/debug/README.md create mode 100644 packages/debug/jest.config.js create mode 100644 packages/debug/package.json create mode 100644 packages/debug/src/index.test.js create mode 100644 packages/debug/src/index.ts create mode 100644 packages/debug/src/redact.test.js create mode 100644 packages/debug/tsconfig.json create mode 100644 packages/email-spec/README.md create mode 100644 packages/email-spec/package.json create mode 100644 packages/email-spec/src/index.ts create mode 100644 packages/email-spec/tsconfig.json create mode 100644 packages/email/README.md create mode 100644 packages/email/jest.config.js create mode 100644 packages/email/package.json create mode 100644 packages/email/src/index.test.js create mode 100644 packages/email/src/index.ts create mode 100644 packages/email/tsconfig.json create mode 100644 packages/encoding/README.md create mode 100644 packages/encoding/jest.config.js create mode 100644 packages/encoding/package.json create mode 100644 packages/encoding/src/index.test.ts create mode 100644 packages/encoding/src/index.ts create mode 100644 packages/encoding/tsconfig.json create mode 100644 packages/env/README.md create mode 100644 packages/env/jest.config.js create mode 100644 packages/env/package.json create mode 100644 packages/env/src/index.test.js create mode 100644 packages/env/src/index.ts create mode 100644 packages/env/tsconfig.json create mode 100644 packages/errors/README.md create mode 100644 packages/errors/jest.config.js create mode 100644 packages/errors/package.json create mode 100644 packages/errors/src/index.test.js create mode 100644 packages/errors/src/index.ts create mode 100644 packages/errors/tsconfig.json create mode 100644 packages/eslint-config/index.js create mode 100644 packages/eslint-config/package.json create mode 100644 packages/fetch/README.md create mode 100644 packages/fetch/jest.config.js create mode 100644 packages/fetch/package.json create mode 100644 packages/fetch/src/index.test.js create mode 100644 packages/fetch/src/index.ts create mode 100644 packages/fetch/tsconfig.json create mode 100644 packages/file-csv/README.md create mode 100644 packages/file-csv/data/test01.csv create mode 100644 packages/file-csv/jest.config.js create mode 100644 packages/file-csv/package.json create mode 100644 packages/file-csv/src/index.ts create mode 100644 packages/file-csv/src/parse.test.ts create mode 100644 packages/file-csv/src/parse.ts create mode 100644 packages/file-csv/tsconfig.json create mode 100644 packages/file-docx/README.md create mode 100644 packages/file-docx/data/test01.docx create mode 100644 packages/file-docx/jest.config.js create mode 100644 packages/file-docx/package.json create mode 100644 packages/file-docx/src/index.ts create mode 100644 packages/file-docx/src/parse.test.ts create mode 100644 packages/file-docx/src/parse.ts create mode 100644 packages/file-docx/tsconfig.json create mode 100644 packages/file-html/README.md create mode 100644 packages/file-html/jest.config.js create mode 100644 packages/file-html/package.json create mode 100644 packages/file-html/src/index.ts create mode 100644 packages/file-html/src/parse.test.ts create mode 100644 packages/file-html/src/parse.ts create mode 100644 packages/file-html/tsconfig.json create mode 100644 packages/file-json/README.md create mode 100644 packages/file-json/jest.config.js create mode 100644 packages/file-json/package.json create mode 100644 packages/file-json/src/index.test.ts create mode 100644 packages/file-json/src/index.ts create mode 100644 packages/file-json/tsconfig.json create mode 100644 packages/file-jsonl/README.md create mode 100644 packages/file-jsonl/jest.config.js create mode 100644 packages/file-jsonl/package.json create mode 100644 packages/file-jsonl/src/index.test.ts create mode 100644 packages/file-jsonl/src/index.ts create mode 100644 packages/file-jsonl/tsconfig.json create mode 100644 packages/file-md/README.md create mode 100644 packages/file-md/jest.config.js create mode 100644 packages/file-md/package.json create mode 100644 packages/file-md/src/index.test.ts create mode 100644 packages/file-md/src/index.ts create mode 100644 packages/file-md/tsconfig.json create mode 100644 packages/file-pdf/README.md create mode 100644 packages/file-pdf/data/test01.pdf create mode 100644 packages/file-pdf/jest.config.js create mode 100644 packages/file-pdf/package.json create mode 100644 packages/file-pdf/src/index.ts create mode 100644 packages/file-pdf/src/parse.test.ts create mode 100644 packages/file-pdf/src/parse.ts create mode 100644 packages/file-pdf/tsconfig.json create mode 100644 packages/file-pptx/README.md create mode 100644 packages/file-pptx/data/test01.pptx create mode 100644 packages/file-pptx/jest.config.js create mode 100644 packages/file-pptx/package.json create mode 100644 packages/file-pptx/src/index.ts create mode 100644 packages/file-pptx/src/parse.test.ts create mode 100644 packages/file-pptx/src/parse.ts create mode 100644 packages/file-pptx/tsconfig.json create mode 100644 packages/file-txt/README.md create mode 100644 packages/file-txt/jest.config.js create mode 100644 packages/file-txt/package.json create mode 100644 packages/file-txt/src/index.test.ts create mode 100644 packages/file-txt/src/index.ts create mode 100644 packages/file-txt/tsconfig.json create mode 100644 packages/file-xlsx/README.md create mode 100644 packages/file-xlsx/data/test01.xlsx create mode 100644 packages/file-xlsx/jest.config.js create mode 100644 packages/file-xlsx/package.json create mode 100644 packages/file-xlsx/src/index.ts create mode 100644 packages/file-xlsx/src/parse.test.ts create mode 100644 packages/file-xlsx/src/parse.ts create mode 100644 packages/file-xlsx/tsconfig.json create mode 100644 packages/file-yaml/README.md create mode 100644 packages/file-yaml/jest.config.js create mode 100644 packages/file-yaml/package.json create mode 100644 packages/file-yaml/src/index.test.ts create mode 100644 packages/file-yaml/src/index.ts create mode 100644 packages/file-yaml/tsconfig.json create mode 100644 packages/file/README.md create mode 100644 packages/file/jest.config.js create mode 100644 packages/file/package.json create mode 100644 packages/file/src/index.test.ts create mode 100644 packages/file/src/index.ts create mode 100644 packages/file/src/index2.test.ts create mode 100644 packages/file/src/index2.ts create mode 100644 packages/file/src/support.ts create mode 100644 packages/file/tsconfig.json create mode 100644 packages/gpt/README.md create mode 100644 packages/gpt/jest.config.js create mode 100644 packages/gpt/package.json create mode 100644 packages/gpt/src/index.test.ts create mode 100644 packages/gpt/src/index.ts create mode 100644 packages/gpt/src/text-splitter.test.ts create mode 100644 packages/gpt/src/text-splitter.ts create mode 100644 packages/gpt/tsconfig.json create mode 100644 packages/http-codes/README.md create mode 100644 packages/http-codes/package.json create mode 100644 packages/http-codes/src/index.ts create mode 100644 packages/http-codes/tsconfig.json create mode 100644 packages/it/README.md create mode 100644 packages/it/jest.config.js create mode 100644 packages/it/package.json create mode 100644 packages/it/src/index.test.js create mode 100644 packages/it/src/index.ts create mode 100644 packages/it/tsconfig.json create mode 100644 packages/jest-jsdom/README.md create mode 100644 packages/jest-jsdom/jest.config.js create mode 100644 packages/jest-jsdom/package.json create mode 100644 packages/jest-jsdom/src/index.js create mode 100644 packages/jest-jsdom/src/index.test.js create mode 100644 packages/jest-jsdom/tsconfig.json create mode 100644 packages/json/README.md create mode 100644 packages/json/jest.config.js create mode 100644 packages/json/package.json create mode 100644 packages/json/src/index.test.js create mode 100644 packages/json/src/index.ts create mode 100644 packages/json/tsconfig.json create mode 100644 packages/math/README.md create mode 100644 packages/math/jest.config.js create mode 100644 packages/math/package.json create mode 100644 packages/math/src/index.test.js create mode 100644 packages/math/src/index.ts create mode 100644 packages/math/tsconfig.json create mode 100644 packages/mcp-widgets/.github/workflows/backmerge.yaml create mode 100644 packages/mcp-widgets/.gitignore create mode 100644 packages/mcp-widgets/.prettierrc create mode 100644 packages/mcp-widgets/.storybook/main.ts create mode 100644 packages/mcp-widgets/.storybook/preview.tsx create mode 100644 packages/mcp-widgets/LICENSE create mode 100644 packages/mcp-widgets/README.md create mode 100644 packages/mcp-widgets/eslint.config.mjs create mode 100644 packages/mcp-widgets/package.json create mode 100644 packages/mcp-widgets/scripts/build-cdn.js create mode 100644 packages/mcp-widgets/scripts/validate-cdn.js create mode 100644 packages/mcp-widgets/src/index.ts create mode 100644 packages/mcp-widgets/src/loaders/chatgpt.ts create mode 100644 packages/mcp-widgets/src/schemas/index.ts create mode 100644 packages/mcp-widgets/src/schemas/manifest.test.ts create mode 100644 packages/mcp-widgets/src/schemas/manifest.ts create mode 100644 packages/mcp-widgets/src/styles.d.ts create mode 100644 packages/mcp-widgets/src/styles/base.css create mode 100644 packages/mcp-widgets/src/types/index.ts create mode 100644 packages/mcp-widgets/src/types/manifest.ts create mode 100644 packages/mcp-widgets/src/types/openai.ts create mode 100644 packages/mcp-widgets/src/types/widget.ts create mode 100644 packages/mcp-widgets/src/utils/index.ts create mode 100644 packages/mcp-widgets/src/utils/openai.ts create mode 100644 packages/mcp-widgets/src/utils/register.ts create mode 100644 packages/mcp-widgets/src/widgets/data-card/DataCard.stories.tsx create mode 100644 packages/mcp-widgets/src/widgets/data-card/DataCard.test.ts create mode 100644 packages/mcp-widgets/src/widgets/data-card/DataCard.ts create mode 100644 packages/mcp-widgets/src/widgets/data-card/cdn.ts create mode 100644 packages/mcp-widgets/src/widgets/data-card/index.ts create mode 100644 packages/mcp-widgets/src/widgets/data-card/manifest.ts create mode 100644 packages/mcp-widgets/src/widgets/index.ts create mode 100644 packages/mcp-widgets/src/widgets/rich-data-card/RichDataCard.stories.tsx create mode 100644 packages/mcp-widgets/src/widgets/rich-data-card/RichDataCard.test.tsx create mode 100644 packages/mcp-widgets/src/widgets/rich-data-card/RichDataCard.tsx create mode 100644 packages/mcp-widgets/src/widgets/rich-data-card/cdn.tsx create mode 100644 packages/mcp-widgets/src/widgets/rich-data-card/index.ts create mode 100644 packages/mcp-widgets/src/widgets/rich-data-card/manifest.ts create mode 100644 packages/mcp-widgets/src/widgets/rich-data-card/styles.css create mode 100644 packages/mcp-widgets/tailwind.config.ts create mode 100644 packages/mcp-widgets/tsconfig.build.json create mode 100644 packages/mcp-widgets/tsconfig.json create mode 100644 packages/mcp-widgets/vite.config.ts create mode 100644 packages/mcp-widgets/vitest.config.ts create mode 100644 packages/mcp-widgets/vitest.setup.ts create mode 100644 packages/md/README.md create mode 100644 packages/md/jest.config.js create mode 100644 packages/md/package.json create mode 100644 packages/md/src/chat.test.js create mode 100644 packages/md/src/chat.ts create mode 100644 packages/md/src/convert.test.js create mode 100644 packages/md/src/convert.ts create mode 100644 packages/md/src/extract.test.js create mode 100644 packages/md/src/extract.ts create mode 100644 packages/md/src/frontmatter.test.js create mode 100644 packages/md/src/frontmatter.ts create mode 100644 packages/md/src/index.ts create mode 100644 packages/md/src/linkify.test.js create mode 100644 packages/md/src/linkify.ts create mode 100644 packages/md/src/split.test.js create mode 100644 packages/md/src/split.ts create mode 100644 packages/md/src/table.test.js create mode 100644 packages/md/src/table.ts create mode 100644 packages/md/tsconfig.json create mode 100644 packages/memcache-spec/package.json create mode 100644 packages/memcache-spec/src/index.ts create mode 100644 packages/memcache-spec/tsconfig.json create mode 100644 packages/memcache/README.md create mode 100644 packages/memcache/jest.config.js create mode 100644 packages/memcache/package.json create mode 100644 packages/memcache/src/index.browser.ts create mode 100644 packages/memcache/src/index.test.js create mode 100644 packages/memcache/src/index.ts create mode 100644 packages/memcache/src/memory.ts create mode 100644 packages/memcache/src/redis.test.js create mode 100644 packages/memcache/src/redis.ts create mode 100644 packages/memcache/tsconfig.json create mode 100644 packages/metascraper-font/README.md create mode 100644 packages/metascraper-font/jest.config.js create mode 100644 packages/metascraper-font/package.json create mode 100644 packages/metascraper-font/src/index.test.ts create mode 100644 packages/metascraper-font/src/index.ts create mode 100644 packages/metascraper-font/tsconfig.json create mode 100644 packages/observability-spec/README.md create mode 100644 packages/observability-spec/package.json create mode 100644 packages/observability-spec/src/index.ts create mode 100644 packages/observability-spec/src/next.ts create mode 100644 packages/observability-spec/tsconfig.json create mode 100644 packages/observability/README.md create mode 100644 packages/observability/jest.config.js create mode 100644 packages/observability/package.json create mode 100644 packages/observability/src/index.test.js create mode 100644 packages/observability/src/index.ts create mode 100644 packages/observability/src/next.test.js create mode 100644 packages/observability/src/next/client.ts create mode 100644 packages/observability/src/next/config.js create mode 100644 packages/observability/src/next/server.ts create mode 100644 packages/observability/tsconfig.json create mode 100644 packages/openapi-types-generator/README.md create mode 100755 packages/openapi-types-generator/bin/cli.ts create mode 100644 packages/openapi-types-generator/jest.config.js create mode 100644 packages/openapi-types-generator/package.json create mode 100644 packages/openapi-types-generator/src/generator.ts create mode 100644 packages/openapi-types-generator/src/index.test.js create mode 100644 packages/openapi-types-generator/src/index.ts create mode 100644 packages/openapi-types-generator/src/naming.ts create mode 100644 packages/openapi-types-generator/src/parser.ts create mode 100644 packages/openapi-types-generator/src/swagger-client.d.ts create mode 100644 packages/openapi-types-generator/tsconfig.json create mode 100644 packages/partners-spec/README.md create mode 100644 packages/partners-spec/package.json create mode 100644 packages/partners-spec/src/index.ts create mode 100644 packages/partners-spec/tsconfig.json create mode 100644 packages/partners/README.md create mode 100644 packages/partners/package.json create mode 100644 packages/partners/src/assert.d.ts create mode 100644 packages/partners/src/assert.js create mode 100644 packages/partners/src/index.d.ts create mode 100644 packages/partners/src/index.js create mode 100644 packages/partners/tsconfig.json create mode 100644 packages/path/jest.config.js create mode 100644 packages/path/package.json create mode 100644 packages/path/src/index.test.js create mode 100644 packages/path/src/index.ts create mode 100644 packages/path/tsconfig.json create mode 100644 packages/pii-spec/README.md create mode 100644 packages/pii-spec/package.json create mode 100644 packages/pii-spec/src/index.ts create mode 100644 packages/pii-spec/tsconfig.json create mode 100644 packages/pii/README.md create mode 100644 packages/pii/jest.config.js create mode 100644 packages/pii/package.json create mode 100644 packages/pii/src/index.test.js create mode 100644 packages/pii/src/index.ts create mode 100644 packages/pii/tsconfig.json create mode 100644 packages/queue-spec/package.json create mode 100644 packages/queue-spec/src/index.ts create mode 100644 packages/queue-spec/tsconfig.json create mode 100644 packages/queue/jest.config.js create mode 100644 packages/queue/package.json create mode 100644 packages/queue/src/index.test.js create mode 100644 packages/queue/src/index.ts create mode 100644 packages/queue/tsconfig.json create mode 100644 packages/react-prompt-kit/.github/workflows/backmerge.yaml create mode 100644 packages/react-prompt-kit/.gitignore create mode 100644 packages/react-prompt-kit/.npmignore create mode 100644 packages/react-prompt-kit/CHANGELOG.md create mode 100644 packages/react-prompt-kit/LICENSE create mode 100644 packages/react-prompt-kit/README.md create mode 100644 packages/react-prompt-kit/examples/demo.tsx create mode 100644 packages/react-prompt-kit/jest.config.js create mode 100644 packages/react-prompt-kit/package.json create mode 100644 packages/react-prompt-kit/src/components.test.tsx create mode 100644 packages/react-prompt-kit/src/components.tsx create mode 100644 packages/react-prompt-kit/src/index.ts create mode 100644 packages/react-prompt-kit/src/prompt.test.tsx create mode 100644 packages/react-prompt-kit/src/prompt.ts create mode 100644 packages/react-prompt-kit/tsconfig.build.json create mode 100644 packages/react-prompt-kit/tsconfig.json create mode 100644 packages/regex/README.md create mode 100644 packages/regex/jest.config.js create mode 100644 packages/regex/package.json create mode 100644 packages/regex/src/index.test.js create mode 100644 packages/regex/src/index.ts create mode 100644 packages/regex/tsconfig.json create mode 100644 packages/relay-spec/package.json create mode 100644 packages/relay-spec/src/index.ts create mode 100644 packages/relay-spec/tsconfig.json create mode 100644 packages/relay/jest.config.js create mode 100644 packages/relay/package.json create mode 100644 packages/relay/src/error.ts create mode 100644 packages/relay/src/index.test.js create mode 100644 packages/relay/src/index.ts create mode 100644 packages/relay/tsconfig.json create mode 100644 packages/respond-spec/package.json create mode 100644 packages/respond-spec/src/index.ts create mode 100644 packages/respond-spec/tsconfig.json create mode 100644 packages/respond/jest.config.js create mode 100644 packages/respond/package.json create mode 100644 packages/respond/src/error.ts create mode 100644 packages/respond/src/index.test.js create mode 100644 packages/respond/src/index.ts create mode 100644 packages/respond/tsconfig.json create mode 100644 packages/sandbox-spec/package.json create mode 100644 packages/sandbox-spec/src/index.ts create mode 100644 packages/sandbox-spec/tsconfig.json create mode 100644 packages/sandbox/README.md create mode 100644 packages/sandbox/jest.config.js create mode 100644 packages/sandbox/package.json create mode 100644 packages/sandbox/scripts/verify-interpreters.js create mode 100644 packages/sandbox/src/index.test.js create mode 100644 packages/sandbox/src/index.ts create mode 100644 packages/sandbox/tsconfig.json create mode 100644 packages/screenshot-spec/package.json create mode 100644 packages/screenshot-spec/src/index.ts create mode 100644 packages/screenshot-spec/tsconfig.json create mode 100644 packages/screenshot/jest.config.js create mode 100644 packages/screenshot/package.json create mode 100644 packages/screenshot/src/error.ts create mode 100644 packages/screenshot/src/index.test.js create mode 100644 packages/screenshot/src/index.ts create mode 100644 packages/screenshot/tsconfig.json create mode 100644 packages/script/README.md create mode 100644 packages/script/jest.config.js create mode 100644 packages/script/package.json create mode 100644 packages/script/src/index.test.js create mode 100644 packages/script/src/index.ts create mode 100644 packages/script/src/inquirer.d.ts create mode 100644 packages/script/tsconfig.json create mode 100644 packages/searchengine-spec/README.md create mode 100644 packages/searchengine-spec/package.json create mode 100644 packages/searchengine-spec/src/index.ts create mode 100644 packages/searchengine-spec/tsconfig.json create mode 100644 packages/searchengine/README.md create mode 100644 packages/searchengine/jest.config.js create mode 100644 packages/searchengine/package.json create mode 100644 packages/searchengine/src/index.test.js create mode 100644 packages/searchengine/src/index.ts create mode 100644 packages/searchengine/tsconfig.json create mode 100644 packages/secrets-platform-spec/README.md create mode 100644 packages/secrets-platform-spec/package.json create mode 100644 packages/secrets-platform-spec/src/index.ts create mode 100644 packages/secrets-platform-spec/tsconfig.json create mode 100644 packages/secrets-platform/README.md create mode 100644 packages/secrets-platform/jest.config.js create mode 100644 packages/secrets-platform/package.json create mode 100644 packages/secrets-platform/src/index.test.js create mode 100644 packages/secrets-platform/src/index.ts create mode 100644 packages/secrets-platform/tsconfig.json create mode 100644 packages/sql/README.md create mode 100644 packages/sql/jest.config.js create mode 100644 packages/sql/package.json create mode 100644 packages/sql/src/driver.test.ts create mode 100644 packages/sql/src/driver.ts create mode 100644 packages/sql/src/index.ts create mode 100644 packages/sql/src/parse.test.ts create mode 100644 packages/sql/src/parse.ts create mode 100644 packages/sql/tsconfig.json create mode 100644 packages/storage-spec/package.json create mode 100644 packages/storage-spec/src/index.ts create mode 100644 packages/storage-spec/tsconfig.json create mode 100644 packages/storage/README.md create mode 100644 packages/storage/jest.config.js create mode 100644 packages/storage/package.json create mode 100644 packages/storage/src/adapter.test.js create mode 100644 packages/storage/src/index.test.js create mode 100644 packages/storage/src/index.ts create mode 100644 packages/storage/src/mounts.test.js create mode 100644 packages/storage/src/polyfill.test.js create mode 100644 packages/storage/src/sts.ts create mode 100644 packages/storage/tsconfig.json create mode 100644 packages/string/README.md create mode 100644 packages/string/jest.config.js create mode 100644 packages/string/package.json create mode 100644 packages/string/src/index.test.js create mode 100644 packages/string/src/index.ts create mode 100644 packages/string/tsconfig.json create mode 100644 packages/struct/README.md create mode 100644 packages/struct/jest.config.js create mode 100644 packages/struct/package.json create mode 100644 packages/struct/src/index.test.js create mode 100644 packages/struct/src/index.ts create mode 100644 packages/struct/tsconfig.json create mode 100644 packages/template/README.md create mode 100644 packages/template/jest.config.js create mode 100644 packages/template/package.json create mode 100644 packages/template/src/index.test.ts create mode 100644 packages/template/src/index.ts create mode 100644 packages/template/tsconfig.json create mode 100644 packages/time/README.md create mode 100644 packages/time/jest.config.js create mode 100644 packages/time/package.json create mode 100644 packages/time/src/index.test.ts create mode 100644 packages/time/src/index.ts create mode 100644 packages/time/tsconfig.json create mode 100644 packages/typescript-utils/README.md create mode 100644 packages/typescript-utils/jest.config.js create mode 100644 packages/typescript-utils/package.json create mode 100644 packages/typescript-utils/src/args.ts create mode 100644 packages/typescript-utils/src/assert.ts create mode 100644 packages/typescript-utils/src/exact.ts create mode 100644 packages/typescript-utils/src/function.ts create mode 100644 packages/typescript-utils/src/index.test.ts create mode 100644 packages/typescript-utils/src/index.ts create mode 100644 packages/typescript-utils/src/number.ts create mode 100644 packages/typescript-utils/src/object.ts create mode 100644 packages/typescript-utils/src/promise.ts create mode 100644 packages/typescript-utils/src/record.ts create mode 100644 packages/typescript-utils/src/string.ts create mode 100644 packages/typescript-utils/src/unreachable.ts create mode 100644 packages/typescript-utils/tsconfig.json create mode 100644 packages/vector-spec/package.json create mode 100644 packages/vector-spec/src/index.ts create mode 100644 packages/vector-spec/tsconfig.json create mode 100644 packages/vector/jest.config.js create mode 100644 packages/vector/package.json create mode 100644 packages/vector/src/embed.ts create mode 100644 packages/vector/src/error.ts create mode 100644 packages/vector/src/files.ts create mode 100644 packages/vector/src/index.test.js create mode 100644 packages/vector/src/index.ts create mode 100644 packages/vector/src/local.ts create mode 100644 packages/vector/src/qdrant.test.js create mode 100644 packages/vector/src/qdrant.ts create mode 100644 packages/vector/tsconfig.json create mode 100644 packages/yaml/README.md create mode 100644 packages/yaml/jest.config.js create mode 100644 packages/yaml/package.json create mode 100644 packages/yaml/src/index.test.js create mode 100644 packages/yaml/src/index.ts create mode 100644 packages/yaml/tsconfig.json create mode 100644 patches/README.md create mode 100644 patches/next-auth@4.24.15.patch create mode 100644 patches/react-markdown@8.0.7.patch create mode 100644 platform/.env.example create mode 100644 platform/.gitignore create mode 100644 platform/.storybook/main.ts create mode 100644 platform/.storybook/main.utest.js create mode 100644 platform/.storybook/mocks/async_hooks.ts create mode 100644 platform/.storybook/mocks/empty.ts create mode 100644 platform/.storybook/mocks/prisma-client.ts create mode 100644 platform/.storybook/mocks/prisma-generated.js create mode 100644 platform/.storybook/preview.tsx create mode 100644 platform/.vercelignore create mode 100644 platform/README.md create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/README.md create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/app.manifest create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/components.jsx create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/config.ts create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/const.ts create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/layout.jsx create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/page.tsx create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/prompt.d.ts create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/prompt.yaml create mode 100644 platform/app/apps/(adhoc)/30fc6ef2/server.ts create mode 100644 platform/app/apps/(adhoc)/3e882a70/README.md create mode 100644 platform/app/apps/(adhoc)/3e882a70/[datasetId]/page.tsx create mode 100644 platform/app/apps/(adhoc)/3e882a70/app.manifest create mode 100644 platform/app/apps/(adhoc)/3e882a70/components.jsx create mode 100644 platform/app/apps/(adhoc)/3e882a70/config.ts create mode 100644 platform/app/apps/(adhoc)/3e882a70/const.ts create mode 100644 platform/app/apps/(adhoc)/3e882a70/layout.jsx create mode 100644 platform/app/apps/(adhoc)/3e882a70/page.tsx create mode 100644 platform/app/apps/(adhoc)/3e882a70/server.ts create mode 100644 platform/app/apps/(adhoc)/41f203dc/README.md create mode 100644 platform/app/apps/(adhoc)/41f203dc/app.manifest create mode 100644 platform/app/apps/(adhoc)/41f203dc/components.jsx create mode 100644 platform/app/apps/(adhoc)/41f203dc/config.ts create mode 100644 platform/app/apps/(adhoc)/41f203dc/const.ts create mode 100644 platform/app/apps/(adhoc)/41f203dc/layout.jsx create mode 100644 platform/app/apps/(adhoc)/41f203dc/page.tsx create mode 100644 platform/app/apps/(adhoc)/41f203dc/server.ts create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/README.md create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/app.manifest create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/components.jsx create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/config.ts create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/const.ts create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/layout.jsx create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/page.tsx create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/server.ts create mode 100644 platform/app/apps/(adhoc)/5c0a7a11/server.utest.js create mode 100644 platform/app/apps/(adhoc)/6c4a7b9e/README.md create mode 100644 platform/app/apps/(adhoc)/6c4a7b9e/app.manifest create mode 100644 platform/app/apps/(adhoc)/6c4a7b9e/components.jsx create mode 100644 platform/app/apps/(adhoc)/6c4a7b9e/components.utest.jsx create mode 100644 platform/app/apps/(adhoc)/6c4a7b9e/config.ts create mode 100644 platform/app/apps/(adhoc)/6c4a7b9e/const.ts create mode 100644 platform/app/apps/(adhoc)/6c4a7b9e/layout.jsx create mode 100644 platform/app/apps/(adhoc)/6c4a7b9e/page.tsx create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/README.md create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/app.manifest create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/components.jsx create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/config.ts create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/const.ts create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/layout.jsx create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/normalize.ts create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/page.tsx create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/server.ts create mode 100644 platform/app/apps/(adhoc)/6e3b7f2a/server.utest.js create mode 100644 platform/app/apps/(adhoc)/7cb29ccc/README.md create mode 100644 platform/app/apps/(adhoc)/7cb29ccc/app.manifest create mode 100644 platform/app/apps/(adhoc)/7cb29ccc/components.jsx create mode 100644 platform/app/apps/(adhoc)/7cb29ccc/config.ts create mode 100644 platform/app/apps/(adhoc)/7cb29ccc/const.ts create mode 100644 platform/app/apps/(adhoc)/7cb29ccc/layout.jsx create mode 100644 platform/app/apps/(adhoc)/7cb29ccc/page.jsx create mode 100644 platform/app/apps/(adhoc)/7cb29ccc/server.ts create mode 100644 platform/app/apps/(adhoc)/7f1fb51c/README.md create mode 100644 platform/app/apps/(adhoc)/7f1fb51c/app.manifest create mode 100644 platform/app/apps/(adhoc)/7f1fb51c/components.jsx create mode 100644 platform/app/apps/(adhoc)/7f1fb51c/config.ts create mode 100644 platform/app/apps/(adhoc)/7f1fb51c/const.ts create mode 100644 platform/app/apps/(adhoc)/7f1fb51c/layout.jsx create mode 100644 platform/app/apps/(adhoc)/7f1fb51c/page.tsx create mode 100644 platform/app/apps/(adhoc)/81fdc94a/README.md create mode 100644 platform/app/apps/(adhoc)/81fdc94a/app.manifest create mode 100644 platform/app/apps/(adhoc)/81fdc94a/components.jsx create mode 100644 platform/app/apps/(adhoc)/81fdc94a/config.ts create mode 100644 platform/app/apps/(adhoc)/81fdc94a/const.ts create mode 100644 platform/app/apps/(adhoc)/81fdc94a/layout.jsx create mode 100644 platform/app/apps/(adhoc)/81fdc94a/page.tsx create mode 100644 platform/app/apps/(adhoc)/81fdc94a/server.ts create mode 100644 platform/app/apps/(adhoc)/8734d1ad/README.md create mode 100644 platform/app/apps/(adhoc)/8734d1ad/app.manifest create mode 100644 platform/app/apps/(adhoc)/8734d1ad/components.jsx create mode 100644 platform/app/apps/(adhoc)/8734d1ad/config.ts create mode 100644 platform/app/apps/(adhoc)/8734d1ad/const.ts create mode 100644 platform/app/apps/(adhoc)/8734d1ad/layout.jsx create mode 100644 platform/app/apps/(adhoc)/8734d1ad/page.tsx create mode 100644 platform/app/apps/(adhoc)/8734d1ad/server.ts create mode 100644 platform/app/apps/(adhoc)/8df57107/README.md create mode 100644 platform/app/apps/(adhoc)/8df57107/[blueprintId]/components.jsx create mode 100644 platform/app/apps/(adhoc)/8df57107/[blueprintId]/context.jsx create mode 100644 platform/app/apps/(adhoc)/8df57107/[blueprintId]/files/components.jsx create mode 100644 platform/app/apps/(adhoc)/8df57107/[blueprintId]/files/page.tsx create mode 100644 platform/app/apps/(adhoc)/8df57107/[blueprintId]/layout.jsx create mode 100644 platform/app/apps/(adhoc)/8df57107/[blueprintId]/page.tsx create mode 100644 platform/app/apps/(adhoc)/8df57107/[blueprintId]/sitemaps/components.jsx create mode 100644 platform/app/apps/(adhoc)/8df57107/[blueprintId]/sitemaps/page.tsx create mode 100644 platform/app/apps/(adhoc)/8df57107/app.manifest create mode 100644 platform/app/apps/(adhoc)/8df57107/components.jsx create mode 100644 platform/app/apps/(adhoc)/8df57107/config.ts create mode 100644 platform/app/apps/(adhoc)/8df57107/const.ts create mode 100644 platform/app/apps/(adhoc)/8df57107/layout.jsx create mode 100644 platform/app/apps/(adhoc)/8df57107/page.tsx create mode 100644 platform/app/apps/(adhoc)/8df57107/server.ts create mode 100644 platform/app/apps/(adhoc)/8ea0112f/README.md create mode 100644 platform/app/apps/(adhoc)/8ea0112f/app.manifest create mode 100644 platform/app/apps/(adhoc)/8ea0112f/components.jsx create mode 100644 platform/app/apps/(adhoc)/8ea0112f/config.ts create mode 100644 platform/app/apps/(adhoc)/8ea0112f/const.ts create mode 100644 platform/app/apps/(adhoc)/8ea0112f/layout.jsx create mode 100644 platform/app/apps/(adhoc)/8ea0112f/page.tsx create mode 100644 platform/app/apps/(adhoc)/8ea0112f/server.ts create mode 100644 platform/app/apps/(adhoc)/90dff690/README.md create mode 100644 platform/app/apps/(adhoc)/90dff690/app.manifest create mode 100644 platform/app/apps/(adhoc)/90dff690/components.jsx create mode 100644 platform/app/apps/(adhoc)/90dff690/config.ts create mode 100644 platform/app/apps/(adhoc)/90dff690/const.ts create mode 100644 platform/app/apps/(adhoc)/90dff690/layout.jsx create mode 100644 platform/app/apps/(adhoc)/90dff690/page.tsx create mode 100644 platform/app/apps/(adhoc)/90dff690/server.ts create mode 100644 platform/app/apps/(adhoc)/95ca8b4c/README.md create mode 100644 platform/app/apps/(adhoc)/95ca8b4c/[[...args]]/page.tsx create mode 100644 platform/app/apps/(adhoc)/95ca8b4c/app.manifest create mode 100644 platform/app/apps/(adhoc)/95ca8b4c/components.jsx create mode 100644 platform/app/apps/(adhoc)/95ca8b4c/config.ts create mode 100644 platform/app/apps/(adhoc)/95ca8b4c/const.ts create mode 100644 platform/app/apps/(adhoc)/95ca8b4c/layout.jsx create mode 100644 platform/app/apps/(adhoc)/95ca8b4c/server.ts create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/README.md create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/[spaceId]/components.jsx create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/[spaceId]/page.tsx create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/[spaceId]/server.ts create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/app.manifest create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/components.jsx create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/config.ts create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/const.ts create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/layout.jsx create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/page.tsx create mode 100644 platform/app/apps/(adhoc)/9f3b5e2a/server.ts create mode 100644 platform/app/apps/(adhoc)/b4d0c8f2/README.md create mode 100644 platform/app/apps/(adhoc)/b4d0c8f2/app.manifest create mode 100644 platform/app/apps/(adhoc)/b4d0c8f2/components.jsx create mode 100644 platform/app/apps/(adhoc)/b4d0c8f2/config.ts create mode 100644 platform/app/apps/(adhoc)/b4d0c8f2/const.ts create mode 100644 platform/app/apps/(adhoc)/b4d0c8f2/layout.jsx create mode 100644 platform/app/apps/(adhoc)/b4d0c8f2/page.tsx create mode 100644 platform/app/apps/(adhoc)/b4d0c8f2/server.ts create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/README.md create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/[blueprintId]/components.jsx create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/[blueprintId]/page.tsx create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/[blueprintId]/viewer.jsx create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/app.manifest create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/components.jsx create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/config.ts create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/const.ts create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/layout.jsx create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/page.tsx create mode 100644 platform/app/apps/(adhoc)/b7f4c2de/server.ts create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/README.md create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/[factory]/components.jsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/[factory]/layout.jsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/[factory]/page.tsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/[factory]/playbooks/components.jsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/[factory]/playbooks/page.tsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/[factory]/settings/components.jsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/[factory]/settings/page.tsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/app.manifest create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/components.jsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/config.ts create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/const.ts create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/factory-template.ts create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/factory.ts create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/layout.jsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/page.tsx create mode 100644 platform/app/apps/(adhoc)/c0de9a7f/server.ts create mode 100644 platform/app/apps/(adhoc)/c19e4a72/README.md create mode 100644 platform/app/apps/(adhoc)/c19e4a72/app.manifest create mode 100644 platform/app/apps/(adhoc)/c19e4a72/components.jsx create mode 100644 platform/app/apps/(adhoc)/c19e4a72/config.ts create mode 100644 platform/app/apps/(adhoc)/c19e4a72/const.ts create mode 100644 platform/app/apps/(adhoc)/c19e4a72/layout.jsx create mode 100644 platform/app/apps/(adhoc)/c19e4a72/page.tsx create mode 100644 platform/app/apps/(adhoc)/c19e4a72/server.ts create mode 100644 platform/app/apps/(adhoc)/d6d4b7eb/README.md create mode 100644 platform/app/apps/(adhoc)/d6d4b7eb/app.manifest create mode 100644 platform/app/apps/(adhoc)/d6d4b7eb/components.jsx create mode 100644 platform/app/apps/(adhoc)/d6d4b7eb/config.ts create mode 100644 platform/app/apps/(adhoc)/d6d4b7eb/const.ts create mode 100644 platform/app/apps/(adhoc)/d6d4b7eb/layout.jsx create mode 100644 platform/app/apps/(adhoc)/d6d4b7eb/page.jsx create mode 100644 platform/app/apps/(adhoc)/d6d4b7eb/server.ts create mode 100644 platform/app/apps/(adhoc)/e083ca0f/README.md create mode 100644 platform/app/apps/(adhoc)/e083ca0f/app.manifest create mode 100644 platform/app/apps/(adhoc)/e083ca0f/components.jsx create mode 100644 platform/app/apps/(adhoc)/e083ca0f/config.ts create mode 100644 platform/app/apps/(adhoc)/e083ca0f/const.ts create mode 100644 platform/app/apps/(adhoc)/e083ca0f/layout.jsx create mode 100644 platform/app/apps/(adhoc)/e083ca0f/page.jsx create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/(home)/layout.jsx create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/(home)/page.tsx create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/(project)/[projectId]/components.jsx create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/(project)/[projectId]/layout.jsx create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/(project)/[projectId]/page.tsx create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/(project)/[projectId]/server.ts create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/README.md create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/app.manifest create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/components.jsx create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/config.ts create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/const.ts create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/layout.jsx create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/lib.ts create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/server.ts create mode 100644 platform/app/apps/(adhoc)/f2a9c7d4/space.ts create mode 100644 platform/app/apps/(adhoc)/f49c75da/README.md create mode 100644 platform/app/apps/(adhoc)/f49c75da/app.manifest create mode 100644 platform/app/apps/(adhoc)/f49c75da/components.jsx create mode 100644 platform/app/apps/(adhoc)/f49c75da/config.ts create mode 100644 platform/app/apps/(adhoc)/f49c75da/const.ts create mode 100644 platform/app/apps/(adhoc)/f49c75da/layout.jsx create mode 100644 platform/app/apps/(adhoc)/f49c75da/page.jsx create mode 100644 platform/app/apps/(adhoc)/f49c75da/server.ts create mode 100644 platform/app/apps/(index)/app.webmanifest/route.ts create mode 100644 platform/app/apps/(index)/components.jsx create mode 100644 platform/app/apps/(index)/layout.jsx create mode 100644 platform/app/apps/(index)/layout.utest.js create mode 100644 platform/app/apps/(index)/page.jsx create mode 100644 platform/app/apps/(public)/404/page.jsx create mode 100644 platform/app/apps/(public)/layout.jsx create mode 100644 platform/app/apps/(public)/overview/page.jsx create mode 100644 platform/app/apps/(public)/signin/page.jsx create mode 100644 platform/app/apps/(public)/signin/verify/page.jsx create mode 100644 platform/app/apps/[...path]/route.ts create mode 100644 platform/app/apps/_components/Toolbar.jsx create mode 100644 platform/app/apps/app.manifest.d.ts create mode 100644 platform/app/apps/auditlog/README.md create mode 100644 platform/app/apps/auditlog/app.manifest create mode 100644 platform/app/apps/auditlog/components.jsx create mode 100644 platform/app/apps/auditlog/config.ts create mode 100644 platform/app/apps/auditlog/const.ts create mode 100644 platform/app/apps/auditlog/layout.jsx create mode 100644 platform/app/apps/auditlog/layout.utest.jsx create mode 100644 platform/app/apps/auditlog/page.tsx create mode 100644 platform/app/apps/auditlog/server.ts create mode 100644 platform/app/apps/chat/README.md create mode 100644 platform/app/apps/chat/[[...args]]/page.jsx create mode 100644 platform/app/apps/chat/animations/InputMentionsAnimation.stories.jsx create mode 100644 platform/app/apps/chat/animations/InputMentionsAnimation.tsx create mode 100644 platform/app/apps/chat/animations/TextSelectionAnimation.stories.jsx create mode 100644 platform/app/apps/chat/animations/TextSelectionAnimation.tsx create mode 100644 platform/app/apps/chat/app.manifest create mode 100644 platform/app/apps/chat/components/ChatArea.jsx create mode 100644 platform/app/apps/chat/components/ChatArea.stories.jsx create mode 100644 platform/app/apps/chat/components/ChatExtraFeaturesContext.jsx create mode 100644 platform/app/apps/chat/components/ChatMessages.jsx create mode 100644 platform/app/apps/chat/components/ChatReasoning.jsx create mode 100644 platform/app/apps/chat/components/ChatReasoning.stories.jsx create mode 100644 platform/app/apps/chat/components/ChatTextSelectionTools.jsx create mode 100644 platform/app/apps/chat/components/ConversationContext.jsx create mode 100644 platform/app/apps/chat/components/Form.jsx create mode 100644 platform/app/apps/chat/components/Main.jsx create mode 100644 platform/app/apps/chat/components/Selector.jsx create mode 100644 platform/app/apps/chat/components/Tips.jsx create mode 100644 platform/app/apps/chat/config.ts create mode 100644 platform/app/apps/chat/const.ts create mode 100644 platform/app/apps/chat/hooks/useDebugMode.tsx create mode 100644 platform/app/apps/chat/layout.jsx create mode 100644 platform/app/apps/chat/lib.ts create mode 100644 platform/app/apps/chat/server.tsx create mode 100644 platform/app/apps/code/README.md create mode 100644 platform/app/apps/code/app.manifest create mode 100644 platform/app/apps/code/components.jsx create mode 100644 platform/app/apps/code/config.ts create mode 100644 platform/app/apps/code/const.ts create mode 100644 platform/app/apps/code/layout.jsx create mode 100644 platform/app/apps/code/page.tsx create mode 100644 platform/app/apps/code/server.ts create mode 100644 platform/app/apps/connect/README.md create mode 100644 platform/app/apps/connect/app.manifest create mode 100644 platform/app/apps/connect/components.jsx create mode 100644 platform/app/apps/connect/config.ts create mode 100644 platform/app/apps/connect/const.ts create mode 100644 platform/app/apps/connect/layout.jsx create mode 100644 platform/app/apps/connect/page.tsx create mode 100644 platform/app/apps/connect/server.ts create mode 100644 platform/app/apps/const.ts create mode 100644 platform/app/apps/error.tsx create mode 100644 platform/app/apps/eventlog/README.md create mode 100644 platform/app/apps/eventlog/app.manifest create mode 100644 platform/app/apps/eventlog/components.jsx create mode 100644 platform/app/apps/eventlog/components.utest.jsx create mode 100644 platform/app/apps/eventlog/config.ts create mode 100644 platform/app/apps/eventlog/const.ts create mode 100644 platform/app/apps/eventlog/layout.jsx create mode 100644 platform/app/apps/eventlog/layout.utest.jsx create mode 100644 platform/app/apps/eventlog/page.tsx create mode 100644 platform/app/apps/eventlog/server.ts create mode 100644 platform/app/apps/eventlog/server.utest.js create mode 100644 platform/app/apps/inbox/README.md create mode 100644 platform/app/apps/inbox/[conversationId]/components.jsx create mode 100644 platform/app/apps/inbox/[conversationId]/lib.ts create mode 100644 platform/app/apps/inbox/[conversationId]/loading.jsx create mode 100644 platform/app/apps/inbox/[conversationId]/page.jsx create mode 100644 platform/app/apps/inbox/[conversationId]/server.ts create mode 100644 platform/app/apps/inbox/app.manifest create mode 100644 platform/app/apps/inbox/components.jsx create mode 100644 platform/app/apps/inbox/config.ts create mode 100644 platform/app/apps/inbox/const.ts create mode 100644 platform/app/apps/inbox/layout.jsx create mode 100644 platform/app/apps/inbox/page.tsx create mode 100644 platform/app/apps/inbox/server.ts create mode 100644 platform/app/apps/layout.jsx create mode 100644 platform/app/apps/layout.utest.js create mode 100644 platform/app/apps/static/README.md create mode 100644 platform/app/apps/static/[[...path]]/route.ts create mode 100644 platform/app/apps/static/app.manifest create mode 100644 platform/app/apps/static/config.ts create mode 100644 platform/app/apps/static/const.ts create mode 100644 platform/app/apps/static/lib.ts create mode 100644 platform/app/apps/static/lib.utest.ts create mode 100644 platform/app/apps/task/README.md create mode 100644 platform/app/apps/task/app.manifest create mode 100644 platform/app/apps/task/bot.policy.ts create mode 100644 platform/app/apps/task/bot.policy.utest.js create mode 100644 platform/app/apps/task/components.jsx create mode 100644 platform/app/apps/task/config.ts create mode 100644 platform/app/apps/task/const.ts create mode 100644 platform/app/apps/task/layout.jsx create mode 100644 platform/app/apps/task/page.tsx create mode 100644 platform/app/apps/task/server.ts create mode 100644 platform/app/apps/trace/README.md create mode 100644 platform/app/apps/trace/app.manifest create mode 100644 platform/app/apps/trace/components.jsx create mode 100644 platform/app/apps/trace/config.ts create mode 100644 platform/app/apps/trace/const.ts create mode 100644 platform/app/apps/trace/layout.jsx create mode 100644 platform/app/apps/trace/layout.utest.jsx create mode 100644 platform/app/apps/trace/page.tsx create mode 100644 platform/app/apps/usage/README.md create mode 100644 platform/app/apps/usage/app.manifest create mode 100644 platform/app/apps/usage/components.jsx create mode 100644 platform/app/apps/usage/config.ts create mode 100644 platform/app/apps/usage/const.ts create mode 100644 platform/app/apps/usage/layout.jsx create mode 100644 platform/app/apps/usage/page.tsx create mode 100644 platform/app/apps/usage/server.ts create mode 100644 platform/app/apps/usage/server.utest.js create mode 100644 platform/app/apps/usagelog/README.md create mode 100644 platform/app/apps/usagelog/app.manifest create mode 100644 platform/app/apps/usagelog/components.jsx create mode 100644 platform/app/apps/usagelog/config.ts create mode 100644 platform/app/apps/usagelog/const.ts create mode 100644 platform/app/apps/usagelog/layout.jsx create mode 100644 platform/app/apps/usagelog/layout.utest.jsx create mode 100644 platform/app/apps/usagelog/page.tsx create mode 100644 platform/app/apps/usagelog/server.ts create mode 100644 platform/app/global-error.tsx create mode 100644 platform/app/layout.tsx create mode 100644 platform/codegen.ts create mode 100644 platform/components/AbilityCreateDialog.jsx create mode 100644 platform/components/AbilityTemplateBrowser.jsx create mode 100644 platform/components/AdvancedAutoTextarea.jsx create mode 100644 platform/components/AdvancedAutoTextarea.utest.js create mode 100644 platform/components/AgentsScreen.jsx create mode 100644 platform/components/AttachmentsArea.jsx create mode 100644 platform/components/AttachmentsArea.utest.jsx create mode 100644 platform/components/AudioPlayer.jsx create mode 100644 platform/components/AudioPlayer.utest.js create mode 100644 platform/components/AuditLog.jsx create mode 100644 platform/components/AuditLog.utest.js create mode 100644 platform/components/Auth.jsx create mode 100644 platform/components/Auth.utest.js create mode 100644 platform/components/AutoScrollArea.jsx create mode 100644 platform/components/AutoScrollArea.utest.js create mode 100644 platform/components/AutoTextarea.jsx create mode 100644 platform/components/AutoTextarea.utest.js create mode 100644 platform/components/AutoTextarea.utest.jsx create mode 100644 platform/components/BackButton.jsx create mode 100644 platform/components/BackButton.stories.tsx create mode 100644 platform/components/BackButton.utest.js create mode 100644 platform/components/BackButton.utest.jsx create mode 100644 platform/components/BackLink.jsx create mode 100644 platform/components/BackLink.utest.js create mode 100644 platform/components/BackLink.utest.jsx create mode 100644 platform/components/BackstoryInput.jsx create mode 100644 platform/components/BackstoryInput.stories.jsx create mode 100644 platform/components/BillingButtons.jsx create mode 100644 platform/components/BillingButtons.utest.jsx create mode 100644 platform/components/BlueprintCodeBlock.jsx create mode 100644 platform/components/BlueprintCodeBlock.utest.jsx create mode 100644 platform/components/BlueprintList.jsx create mode 100644 platform/components/BlueprintList.utest.js create mode 100644 platform/components/BlueprintList.utest.jsx create mode 100644 platform/components/BotBlockStatus.jsx create mode 100644 platform/components/BotBlockStatus.utest.jsx create mode 100644 platform/components/BotBlockTag.jsx create mode 100644 platform/components/BotInsights.jsx create mode 100644 platform/components/BotList.jsx create mode 100644 platform/components/BotList.utest.jsx create mode 100644 platform/components/BotSelect.jsx create mode 100644 platform/components/Box.jsx create mode 100644 platform/components/Box.utest.js create mode 100644 platform/components/Box.utest.jsx create mode 100644 platform/components/CalendarInput.jsx create mode 100644 platform/components/CalendarInput.utest.jsx create mode 100644 platform/components/Carousel.jsx create mode 100644 platform/components/Carousel.utest.jsx create mode 100644 platform/components/CategoryGrid.jsx create mode 100644 platform/components/CategoryGrid.utest.js create mode 100644 platform/components/ChatInput.jsx create mode 100644 platform/components/ChatInput.utest.js create mode 100644 platform/components/Children.tsx create mode 100644 platform/components/Children.utest.js create mode 100644 platform/components/Children.utest.jsx create mode 100644 platform/components/ChunkErrorListener.tsx create mode 100644 platform/components/ChunkErrorListener.utest.js create mode 100644 platform/components/CodeAction.tsx create mode 100644 platform/components/CodeAction.utest.js create mode 100644 platform/components/CodeBlock.jsx create mode 100644 platform/components/CodeBlock.stories.jsx create mode 100644 platform/components/CodeBlock.utest.jsx create mode 100644 platform/components/Collapsible.jsx create mode 100644 platform/components/Collapsible.stories.jsx create mode 100644 platform/components/Collapsible.utest.js create mode 100644 platform/components/ColorInput.jsx create mode 100644 platform/components/ColorInput.stories.jsx create mode 100644 platform/components/ColorInput.utest.jsx create mode 100644 platform/components/CommaListSelect.jsx create mode 100644 platform/components/CommaListSelect.stories.jsx create mode 100644 platform/components/CommaListSelect.utest.js create mode 100644 platform/components/Component.jsx create mode 100644 platform/components/Component.utest.js create mode 100644 platform/components/Confirm.jsx create mode 100644 platform/components/Confirm.stories.jsx create mode 100644 platform/components/Confirm.utest.jsx create mode 100644 platform/components/ConfirmButton.jsx create mode 100644 platform/components/ConfirmButton.utest.jsx create mode 100644 platform/components/Consent.jsx create mode 100644 platform/components/Consent.utest.js create mode 100644 platform/components/Console.jsx create mode 100644 platform/components/Console.utest.js create mode 100644 platform/components/ContactList.jsx create mode 100644 platform/components/ContactList.utest.js create mode 100644 platform/components/ContactSelect.jsx create mode 100644 platform/components/ContactSelect.utest.jsx create mode 100644 platform/components/ContextInput.ReactFlow.stories.jsx create mode 100644 platform/components/ContextInput.jsx create mode 100644 platform/components/ContextInput.setValueIdentity.utest.jsx create mode 100644 platform/components/ContextInput.staleContextLag.utest.jsx create mode 100644 platform/components/ContextInput.stories.jsx create mode 100644 platform/components/ContextInput.utest.js create mode 100644 platform/components/Conversation.jsx create mode 100644 platform/components/Conversation.utest.js create mode 100644 platform/components/ConversationAttachmentList.jsx create mode 100644 platform/components/ConversationAttachmentList.utest.js create mode 100644 platform/components/ConversationInsights.jsx create mode 100644 platform/components/ConversationInsights.utest.jsx create mode 100644 platform/components/ConversationList.jsx create mode 100644 platform/components/ConversationList.utest.js create mode 100644 platform/components/ConversationManager.jsx create mode 100644 platform/components/ConversationManager.utest.jsx create mode 100644 platform/components/ConversationMonitor.jsx create mode 100644 platform/components/ConversationsButton.jsx create mode 100644 platform/components/CopyButton.tsx create mode 100644 platform/components/CopyButton.utest.js create mode 100644 platform/components/DailyChart.tsx create mode 100644 platform/components/DailyChart.utest.js create mode 100644 platform/components/DarkModeSwitch.jsx create mode 100644 platform/components/DarkModeSwitch.utest.jsx create mode 100644 platform/components/DashboardCommandPalette.jsx create mode 100644 platform/components/DashboardCommandPalette.utest.jsx create mode 100644 platform/components/DatasetFiles.jsx create mode 100644 platform/components/DatasetFiles.utest.jsx create mode 100644 platform/components/DatasetImportJobFinishURLs.jsx create mode 100644 platform/components/DatasetImportJobFinishURLs.utest.jsx create mode 100644 platform/components/DatasetList.jsx create mode 100644 platform/components/DatasetList.utest.jsx create mode 100644 platform/components/DatasetRecordList.jsx create mode 100644 platform/components/DatasetRecordList.utest.jsx create mode 100644 platform/components/DatasetSelect.jsx create mode 100644 platform/components/DaysSelect.jsx create mode 100644 platform/components/DaysSelect.utest.js create mode 100644 platform/components/DbTextInput.jsx create mode 100644 platform/components/DbTextInput.utest.js create mode 100644 platform/components/DescriptionInput.jsx create mode 100644 platform/components/DescriptionInput.utest.js create mode 100644 platform/components/Diagram.jsx create mode 100644 platform/components/Diagram.utest.jsx create mode 100644 platform/components/DocsLink.jsx create mode 100644 platform/components/DocsLink.utest.js create mode 100644 platform/components/DotsLoader.jsx create mode 100644 platform/components/DotsLoader.utest.jsx create mode 100644 platform/components/DurationSelect.jsx create mode 100644 platform/components/DurationSelect.utest.js create mode 100644 platform/components/DynamicIcon.jsx create mode 100644 platform/components/DynamicIcon.utest.jsx create mode 100644 platform/components/DynamicImage.tsx create mode 100644 platform/components/DynamicImage.utest.js create mode 100644 platform/components/DynamicLogo.tsx create mode 100644 platform/components/DynamicLogo.utest.js create mode 100644 platform/components/Emoji.jsx create mode 100644 platform/components/Emoji.utest.jsx create mode 100644 platform/components/EventLog.jsx create mode 100644 platform/components/EventLog.utest.js create mode 100644 platform/components/Expando.jsx create mode 100644 platform/components/Expando.utest.jsx create mode 100644 platform/components/ExpiresAtInput.jsx create mode 100644 platform/components/ExpiresAtInput.utest.jsx create mode 100644 platform/components/ExportLink.jsx create mode 100644 platform/components/ExportLink.utest.js create mode 100644 platform/components/ExportLink.utest.jsx create mode 100644 platform/components/ExtractIntegrationItemList.jsx create mode 100644 platform/components/ExtractIntegrationItemList.utest.jsx create mode 100644 platform/components/ExtractSchemaCheatsheet.jsx create mode 100644 platform/components/ExtractSchemaCheatsheet.utest.jsx create mode 100644 platform/components/ExtractSchemaInput.jsx create mode 100644 platform/components/FAQ.jsx create mode 100644 platform/components/FAQ.utest.jsx create mode 100644 platform/components/FAQ3.jsx create mode 100644 platform/components/FAQ3.utest.jsx create mode 100644 platform/components/FAQStructuredData.jsx create mode 100644 platform/components/FAQStructuredData.utest.js create mode 100644 platform/components/FOC.tsx create mode 100644 platform/components/FOC.utest.js create mode 100644 platform/components/FOC.utest.jsx create mode 100644 platform/components/FadeInIframe.jsx create mode 100644 platform/components/FadeInIframe.utest.jsx create mode 100644 platform/components/FancyLink.jsx create mode 100644 platform/components/FancyLink.stories.jsx create mode 100644 platform/components/FancyLink.utest.js create mode 100644 platform/components/FancyLink.utest.jsx create mode 100644 platform/components/FileDownloadButton.jsx create mode 100644 platform/components/FileDownloadButton.utest.jsx create mode 100644 platform/components/FileDrop.jsx create mode 100644 platform/components/FileDrop.utest.jsx create mode 100644 platform/components/FileEditButton.jsx create mode 100644 platform/components/FileEditButton.utest.js create mode 100644 platform/components/FileIcon.jsx create mode 100644 platform/components/FileIcon.utest.js create mode 100644 platform/components/FileIcon.utest.jsx create mode 100644 platform/components/FileList.jsx create mode 100644 platform/components/FileList.utest.jsx create mode 100644 platform/components/FileManager.jsx create mode 100644 platform/components/FileManager.utest.jsx create mode 100644 platform/components/FileSelect.jsx create mode 100644 platform/components/FileSelect.utest.jsx create mode 100644 platform/components/FileUploadButton.jsx create mode 100644 platform/components/FileUploadButton.utest.jsx create mode 100644 platform/components/FloatingBox.jsx create mode 100644 platform/components/FloatingBox.stories.jsx create mode 100644 platform/components/FloatingBox.utest.js create mode 100644 platform/components/ForwardButton.tsx create mode 100644 platform/components/ForwardButton.utest.jsx create mode 100644 platform/components/ForwardLink.jsx create mode 100644 platform/components/ForwardLink.utest.js create mode 100644 platform/components/GTag.jsx create mode 100644 platform/components/GTag.utest.jsx create mode 100644 platform/components/GeneralBasicOptions.jsx create mode 100644 platform/components/GeneralBasicOptions.utest.jsx create mode 100644 platform/components/GlobalRoot.jsx create mode 100644 platform/components/GlobalRoot.utest.js create mode 100644 platform/components/GlobalRoot.utest.jsx create mode 100644 platform/components/GravatarIcon.jsx create mode 100644 platform/components/GravatarIcon.utest.js create mode 100644 platform/components/Headline.jsx create mode 100644 platform/components/Headline.utest.jsx create mode 100644 platform/components/Hero.jsx create mode 100644 platform/components/Hero.utest.js create mode 100644 platform/components/HubOptions.jsx create mode 100644 platform/components/HubOptions.utest.jsx create mode 100644 platform/components/IPhoneFrame.jsx create mode 100644 platform/components/IPhoneFrame.utest.js create mode 100644 platform/components/IconSelect.jsx create mode 100644 platform/components/IconSelect.utest.js create mode 100644 platform/components/Image.tsx create mode 100644 platform/components/Image.utest.js create mode 100644 platform/components/ImageBlock.jsx create mode 100644 platform/components/ImageBlock.utest.js create mode 100644 platform/components/ImageModelSelect.jsx create mode 100644 platform/components/ImageModelSelect.stories.jsx create mode 100644 platform/components/ImageModelSelect.utest.js create mode 100644 platform/components/Initials.jsx create mode 100644 platform/components/Initials.utest.js create mode 100644 platform/components/Initials.utest.jsx create mode 100644 platform/components/InputArea.jsx create mode 100644 platform/components/InputArea.utest.js create mode 100644 platform/components/InstructionCheatsheet.jsx create mode 100644 platform/components/InstructionCheatsheet.utest.jsx create mode 100644 platform/components/InstructionInput.jsx create mode 100644 platform/components/InstructionInput.stories.jsx create mode 100644 platform/components/IntegrationInstallButton.jsx create mode 100644 platform/components/IntegrationInstallButton.utest.jsx create mode 100644 platform/components/IntegrationList.jsx create mode 100644 platform/components/IntegrationList.utest.jsx create mode 100644 platform/components/ItemsDisplay.jsx create mode 100644 platform/components/ItemsDisplay.utest.jsx create mode 100644 platform/components/KeyCombo.tsx create mode 100644 platform/components/KeyCombo.utest.js create mode 100644 platform/components/LanguageModelSelect.jsx create mode 100644 platform/components/LanguageModelSelect.stories.jsx create mode 100644 platform/components/LanguageModelSelect.utest.js create mode 100644 platform/components/LimitsCheatsheet.jsx create mode 100644 platform/components/LimitsCheatsheet.utest.jsx create mode 100644 platform/components/LimitsConfigInput.jsx create mode 100644 platform/components/LimitsConfigInput.utest.js create mode 100644 platform/components/Link.jsx create mode 100644 platform/components/Link.utest.jsx create mode 100644 platform/components/List.jsx create mode 100644 platform/components/List.stories.jsx create mode 100644 platform/components/List.utest.jsx create mode 100644 platform/components/LoadMoreButton.jsx create mode 100644 platform/components/LoadMoreButton.utest.js create mode 100644 platform/components/LoadMoreButton.utest.jsx create mode 100644 platform/components/LoadingIframe.jsx create mode 100644 platform/components/LoadingIframe.utest.jsx create mode 100644 platform/components/MainNavbar.jsx create mode 100644 platform/components/ManualLink.jsx create mode 100644 platform/components/ManualLink.utest.js create mode 100644 platform/components/MarkdownCheatsheet.jsx create mode 100644 platform/components/MarkdownCheatsheet.utest.jsx create mode 100644 platform/components/MarkdownInput.jsx create mode 100644 platform/components/MarkdownInput.utest.jsx create mode 100644 platform/components/MemoryList.jsx create mode 100644 platform/components/MemoryList.utest.jsx create mode 100644 platform/components/MenuButton.jsx create mode 100644 platform/components/MenuButton.stories.jsx create mode 100644 platform/components/MenuButton.utest.js create mode 100644 platform/components/Meta.jsx create mode 100644 platform/components/Meta.utest.js create mode 100644 platform/components/MetaArea.jsx create mode 100644 platform/components/MetaArea.utest.js create mode 100644 platform/components/MetaInput.jsx create mode 100644 platform/components/MetaInput.utest.jsx create mode 100644 platform/components/MovingScreen.jsx create mode 100644 platform/components/MovingScreen.utest.jsx create mode 100644 platform/components/MultiLanguageSelect.jsx create mode 100644 platform/components/MultiLanguageSelect.utest.js create mode 100644 platform/components/NameInput.jsx create mode 100644 platform/components/NameInput.utest.js create mode 100644 platform/components/NameInput.utest.jsx create mode 100644 platform/components/NavHeader.jsx create mode 100644 platform/components/NavHeader.utest.js create mode 100644 platform/components/NavbarAccount.jsx create mode 100644 platform/components/Nbsp.tsx create mode 100644 platform/components/Nbsp.utest.js create mode 100644 platform/components/NestedAccordion.jsx create mode 100644 platform/components/NestedAccordion.utest.jsx create mode 100644 platform/components/NoRubberBand.jsx create mode 100644 platform/components/NoRubberBand.utest.jsx create mode 100644 platform/components/NoSsr.jsx create mode 100644 platform/components/NoSsr.utest.js create mode 100644 platform/components/NoSsr.utest.jsx create mode 100644 platform/components/Notifications.jsx create mode 100644 platform/components/Notifications.utest.jsx create mode 100644 platform/components/NotificationsButton.jsx create mode 100644 platform/components/NotificationsButton.utest.js create mode 100644 platform/components/ObjectInput.jsx create mode 100644 platform/components/ObjectInput.stories.jsx create mode 100644 platform/components/ObjectInput.utest.js create mode 100644 platform/components/ObjectView.jsx create mode 100644 platform/components/ObjectView.utest.js create mode 100644 platform/components/PagePlaceholder.jsx create mode 100644 platform/components/PagePlaceholder.utest.jsx create mode 100644 platform/components/PageSections.jsx create mode 100644 platform/components/PageSections.utest.jsx create mode 100644 platform/components/Pagedown.jsx create mode 100644 platform/components/Pagedown.utest.jsx create mode 100644 platform/components/PartnerBanner.jsx create mode 100644 platform/components/PartnerBanner.utest.jsx create mode 100644 platform/components/PinInput.jsx create mode 100644 platform/components/PinInput.stories.jsx create mode 100644 platform/components/PinInput.utest.js create mode 100644 platform/components/Ping.tsx create mode 100644 platform/components/Ping.utest.js create mode 100644 platform/components/PlatformExperienceOnly.jsx create mode 100644 platform/components/PolicyBlockStatus.jsx create mode 100644 platform/components/PolicyBlockStatus.utest.jsx create mode 100644 platform/components/PolicyConfigInput.jsx create mode 100644 platform/components/PolicyConfigInput.utest.js create mode 100644 platform/components/PolicyList.jsx create mode 100644 platform/components/PolicyList.utest.jsx create mode 100644 platform/components/PopButton.jsx create mode 100644 platform/components/PopButton.stories.jsx create mode 100644 platform/components/PopButton.utest.jsx create mode 100644 platform/components/Portal.jsx create mode 100644 platform/components/Portal.utest.js create mode 100644 platform/components/PortalConfigInput.jsx create mode 100644 platform/components/PortalConfigInput.utest.js create mode 100644 platform/components/PortalList.jsx create mode 100644 platform/components/PortalList.utest.jsx create mode 100644 platform/components/PortalTarget.jsx create mode 100644 platform/components/PortalTarget.utest.js create mode 100644 platform/components/ProfileBar.jsx create mode 100644 platform/components/ProfileBar.utest.jsx create mode 100644 platform/components/ProfileDropdown.jsx create mode 100644 platform/components/ProfileDropdown.utest.jsx create mode 100644 platform/components/Progress.jsx create mode 100644 platform/components/Progress.utest.jsx create mode 100644 platform/components/ProgressBar.jsx create mode 100644 platform/components/ProgressBar.utest.jsx create mode 100644 platform/components/ProjectScopeSelector.jsx create mode 100644 platform/components/ProjectScopeSelector.utest.jsx create mode 100644 platform/components/Pullout.jsx create mode 100644 platform/components/Pullout.utest.jsx create mode 100644 platform/components/RatingList.jsx create mode 100644 platform/components/RatingList.utest.jsx create mode 100644 platform/components/ReadyFrame.jsx create mode 100644 platform/components/ReadyFrame.utest.jsx create mode 100644 platform/components/ReadyImage.jsx create mode 100644 platform/components/ReadyImage.utest.jsx create mode 100644 platform/components/RecordInput.jsx create mode 100644 platform/components/RecordInput.utest.js create mode 100644 platform/components/RefreshTimer.jsx create mode 100644 platform/components/RefreshTimer.stories.jsx create mode 100644 platform/components/RefreshTimer.utest.js create mode 100644 platform/components/ReloadingPageErrorBoundary.jsx create mode 100644 platform/components/ReloadingPageErrorBoundary.utest.js create mode 100644 platform/components/RerankerModelSelect.jsx create mode 100644 platform/components/RerankerModelSelect.utest.js create mode 100644 platform/components/ResourceFilterButton.jsx create mode 100644 platform/components/ResourceFilterButton.utest.jsx create mode 100644 platform/components/ResourceList.jsx create mode 100644 platform/components/ResourceList.utest.js create mode 100644 platform/components/ResponsiveIframe.jsx create mode 100644 platform/components/ResponsiveIframe.utest.js create mode 100644 platform/components/ResponsiveIframe.utest.jsx create mode 100644 platform/components/RevealTextarea.jsx create mode 100644 platform/components/RevealTextarea.utest.jsx create mode 100644 platform/components/RevealToken.jsx create mode 100644 platform/components/RevealToken.utest.js create mode 100644 platform/components/RewardsButton.jsx create mode 100644 platform/components/RewardsButton.utest.js create mode 100644 platform/components/Safedown.jsx create mode 100644 platform/components/Safedown.stories.jsx create mode 100644 platform/components/Safedown.utest.jsx create mode 100644 platform/components/SaveButton.jsx create mode 100644 platform/components/SaveButton.utest.js create mode 100644 platform/components/SaveButton.utest.jsx create mode 100644 platform/components/ScheduleSelect.jsx create mode 100644 platform/components/ScheduleSelect.utest.js create mode 100644 platform/components/SchemaPanel.jsx create mode 100644 platform/components/SchemaPanel.utest.jsx create mode 100644 platform/components/SecretConfigInput.jsx create mode 100644 platform/components/SecretConfigInput.utest.jsx create mode 100644 platform/components/SecretConnectionStatus.jsx create mode 100644 platform/components/SecretList.jsx create mode 100644 platform/components/SecretList.utest.js create mode 100644 platform/components/SecretSelect.jsx create mode 100644 platform/components/SendInstructions.jsx create mode 100644 platform/components/SendInstructions.utest.js create mode 100644 platform/components/Session.jsx create mode 100644 platform/components/Session.utest.jsx create mode 100644 platform/components/SessionContext.jsx create mode 100644 platform/components/SessionContext.utest.jsx create mode 100644 platform/components/SideWidget.jsx create mode 100644 platform/components/SilencingErrorBoundary.jsx create mode 100644 platform/components/SilencingErrorBoundary.utest.js create mode 100644 platform/components/SimpleTabs.jsx create mode 100644 platform/components/SimpleTabs.utest.js create mode 100644 platform/components/Skeleton.jsx create mode 100644 platform/components/Skeleton.utest.js create mode 100644 platform/components/SkillBrowser.jsx create mode 100644 platform/components/SkillBrowser.utest.js create mode 100644 platform/components/SkillsetAbilityList.jsx create mode 100644 platform/components/SkillsetAbilityList.utest.jsx create mode 100644 platform/components/SkillsetAbilityTester.jsx create mode 100644 platform/components/SkillsetAbilityTester.utest.js create mode 100644 platform/components/SkillsetConnectionList.jsx create mode 100644 platform/components/SkillsetList.jsx create mode 100644 platform/components/SkillsetList.utest.jsx create mode 100644 platform/components/SkillsetSelect.jsx create mode 100644 platform/components/SpaceList.jsx create mode 100644 platform/components/SpaceList.utest.jsx create mode 100644 platform/components/SpaceSavingDiv.jsx create mode 100644 platform/components/SpaceSavingDiv.utest.js create mode 100644 platform/components/SpaceSelect.jsx create mode 100644 platform/components/SpaceSelect.utest.jsx create mode 100644 platform/components/SpaceSiteList.jsx create mode 100644 platform/components/SpaceStorageList.jsx create mode 100644 platform/components/SpaceStorageList.utest.js create mode 100644 platform/components/Spinner.jsx create mode 100644 platform/components/Spinner.utest.js create mode 100644 platform/components/Spinner.utest.jsx create mode 100644 platform/components/StructuredData.jsx create mode 100644 platform/components/StructuredData.utest.js create mode 100644 platform/components/SuperTools.jsx create mode 100644 platform/components/SuperTools.utest.jsx create mode 100644 platform/components/SwitchBar.jsx create mode 100644 platform/components/SwitchBar.utest.jsx create mode 100644 platform/components/TaskExecutionList.jsx create mode 100644 platform/components/TaskExecutionList.utest.jsx create mode 100644 platform/components/TaskList.jsx create mode 100644 platform/components/TaskList.utest.js create mode 100644 platform/components/TeamList.jsx create mode 100644 platform/components/TeamList.utest.js create mode 100644 platform/components/TextSelectionTools.jsx create mode 100644 platform/components/TextSelectionTools.stories.jsx create mode 100644 platform/components/TextSelectionTools.utest.jsx create mode 100644 platform/components/TextareaHighlighter.jsx create mode 100644 platform/components/TextareaHighlighter.stories.jsx create mode 100644 platform/components/TextareaHighlighter.utest.js create mode 100644 platform/components/TextareaQuickEditTools.jsx create mode 100644 platform/components/TextareaQuickEditTools.utest.js create mode 100644 platform/components/Theme.jsx create mode 100644 platform/components/Theme.utest.js create mode 100644 platform/components/ThemeBuilder.jsx create mode 100644 platform/components/ThemeBuilder.utest.jsx create mode 100644 platform/components/ThemeCanvas.jsx create mode 100644 platform/components/ThemeCanvas.utest.jsx create mode 100644 platform/components/ThemeColor.jsx create mode 100644 platform/components/ThemeColor.utest.js create mode 100644 platform/components/ThemeDesigner.jsx create mode 100644 platform/components/ThemeDesigner.utest.jsx create mode 100644 platform/components/ThemePanel.jsx create mode 100644 platform/components/ThemePanel.utest.jsx create mode 100644 platform/components/ThisSolution.jsx create mode 100644 platform/components/ThisSolution.utest.jsx create mode 100644 platform/components/TimeAgo.jsx create mode 100644 platform/components/TimeAgo.utest.js create mode 100644 platform/components/TimeAgo.utest.jsx create mode 100644 platform/components/TimezoneSelect.jsx create mode 100644 platform/components/TimezoneSelect.utest.jsx create mode 100644 platform/components/Toggle.jsx create mode 100644 platform/components/Toggle.stories.jsx create mode 100644 platform/components/Toggle.utest.jsx create mode 100644 platform/components/TokenAutoTextarea.jsx create mode 100644 platform/components/TokenAutoTextarea.stories.jsx create mode 100644 platform/components/TokenAutoTextarea.utest.js create mode 100644 platform/components/TokenConfigInput.jsx create mode 100644 platform/components/TokenConfigInput.utest.js create mode 100644 platform/components/TokenList.jsx create mode 100644 platform/components/TokenList.utest.jsx create mode 100644 platform/components/TooltipButton.jsx create mode 100644 platform/components/TooltipButton.stories.jsx create mode 100644 platform/components/TooltipButton.utest.jsx create mode 100644 platform/components/TriggerSelect.jsx create mode 100644 platform/components/TriggerSelect.utest.jsx create mode 100644 platform/components/UpgradePlans.jsx create mode 100644 platform/components/UpgradePlans.utest.jsx create mode 100644 platform/components/UsageList.jsx create mode 100644 platform/components/UsageList.utest.js create mode 100644 platform/components/UsageView.jsx create mode 100644 platform/components/UsageView.utest.js create mode 100644 platform/components/UsageView.utest.jsx create mode 100644 platform/components/UserList.jsx create mode 100644 platform/components/UserList.utest.jsx create mode 100644 platform/components/VideoModelSelect.jsx create mode 100644 platform/components/VideoModelSelect.utest.js create mode 100644 platform/components/WebhookList.jsx create mode 100644 platform/components/WebhookList.utest.jsx create mode 100644 platform/components/WebhookSetupSection.jsx create mode 100644 platform/components/WebhookSetupSection.stories.jsx create mode 100644 platform/components/WebhookSetupSection.utest.js create mode 100644 platform/components/Widget.jsx create mode 100644 platform/components/WidgetPluginsSelect.jsx create mode 100644 platform/components/WidgetPluginsSelect.utest.jsx create mode 100644 platform/components/WidgetPreview.jsx create mode 100644 platform/components/WidgetPreview.utest.jsx create mode 100644 platform/components/WidgetScript.jsx create mode 100644 platform/components/WidgetScript.utest.js create mode 100644 platform/components/WidgetsScreen.jsx create mode 100644 platform/components/WidgetsScreen.utest.jsx create mode 100644 platform/components/Wrapper.jsx create mode 100644 platform/components/Wrapper.utest.jsx create mode 100644 platform/components/YoutubePlayer.jsx create mode 100644 platform/components/YoutubePlayer.utest.jsx create mode 100644 platform/components/ZoomableArea.jsx create mode 100644 platform/components/ZoomableArea.utest.jsx create mode 100644 platform/config/abilities.ts create mode 100644 platform/config/admins.ts create mode 100644 platform/config/apexes.js create mode 100644 platform/config/apps.ts create mode 100644 platform/config/bots.ts create mode 100644 platform/config/conversations.ts create mode 100644 platform/config/cookie.ts create mode 100644 platform/config/datasets.ts create mode 100644 platform/config/debug.ts create mode 100644 platform/config/execution.ts create mode 100644 platform/config/experiments.ts create mode 100644 platform/config/headers.js create mode 100644 platform/config/hosts.js create mode 100644 platform/config/hub.ts create mode 100644 platform/config/limits.ts create mode 100644 platform/config/messages.ts create mode 100644 platform/config/models.ts create mode 100644 platform/config/navigation.ts create mode 100644 platform/config/oauth.ts create mode 100644 platform/config/origins.js create mode 100644 platform/config/platform.ts create mode 100644 platform/config/queue.ts create mode 100644 platform/config/records.ts create mode 100644 platform/config/server.ts create mode 100644 platform/config/site.js create mode 100644 platform/config/skillsets.ts create mode 100644 platform/config/structures.ts create mode 100644 platform/config/widget.ts create mode 100644 platform/content/faqs/platform-actions.yaml create mode 100644 platform/content/faqs/platform-alerts.yaml create mode 100644 platform/content/faqs/platform-analytics.yaml create mode 100644 platform/content/faqs/platform-api-v1-spec.yaml create mode 100644 platform/content/faqs/platform-audit.yaml create mode 100644 platform/content/faqs/platform-blueprint-instance.yaml create mode 100644 platform/content/faqs/platform-blueprints.yaml create mode 100644 platform/content/faqs/platform-bot-instance.yaml create mode 100644 platform/content/faqs/platform-bots.yaml create mode 100644 platform/content/faqs/platform-contact-instance.yaml create mode 100644 platform/content/faqs/platform-contacts.yaml create mode 100644 platform/content/faqs/platform-conversation-instance.yaml create mode 100644 platform/content/faqs/platform-conversations.yaml create mode 100644 platform/content/faqs/platform-dataset-instance.yaml create mode 100644 platform/content/faqs/platform-dataset-record-instance.yaml create mode 100644 platform/content/faqs/platform-datasets.yaml create mode 100644 platform/content/faqs/platform-events.yaml create mode 100644 platform/content/faqs/platform-file-instance.yaml create mode 100644 platform/content/faqs/platform-files.yaml create mode 100644 platform/content/faqs/platform-integrations-discord.yaml create mode 100644 platform/content/faqs/platform-integrations-email.yaml create mode 100644 platform/content/faqs/platform-integrations-extract.yaml create mode 100644 platform/content/faqs/platform-integrations-googlechat.yaml create mode 100644 platform/content/faqs/platform-integrations-instagram.yaml create mode 100644 platform/content/faqs/platform-integrations-mcpserver.yaml create mode 100644 platform/content/faqs/platform-integrations-messenger.yaml create mode 100644 platform/content/faqs/platform-integrations-microsoftteams.yaml create mode 100644 platform/content/faqs/platform-integrations-notion.yaml create mode 100644 platform/content/faqs/platform-integrations-sitemap.yaml create mode 100644 platform/content/faqs/platform-integrations-skillserver.yaml create mode 100644 platform/content/faqs/platform-integrations-slack.yaml create mode 100644 platform/content/faqs/platform-integrations-support.yaml create mode 100644 platform/content/faqs/platform-integrations-telegram.yaml create mode 100644 platform/content/faqs/platform-integrations-trigger.yaml create mode 100644 platform/content/faqs/platform-integrations-twilio.yaml create mode 100644 platform/content/faqs/platform-integrations-whatsapp.yaml create mode 100644 platform/content/faqs/platform-integrations-widget.yaml create mode 100644 platform/content/faqs/platform-integrations.yaml create mode 100644 platform/content/faqs/platform-memories.yaml create mode 100644 platform/content/faqs/platform-memory-instance.yaml create mode 100644 platform/content/faqs/platform-models.yaml create mode 100644 platform/content/faqs/platform-overview.yaml create mode 100644 platform/content/faqs/platform-policies.yaml create mode 100644 platform/content/faqs/platform-policy-instance.yaml create mode 100644 platform/content/faqs/platform-portal-instance.yaml create mode 100644 platform/content/faqs/platform-portals.yaml create mode 100644 platform/content/faqs/platform-ratings.yaml create mode 100644 platform/content/faqs/platform-secret-instance.yaml create mode 100644 platform/content/faqs/platform-secrets.yaml create mode 100644 platform/content/faqs/platform-skillset-ability-instance.yaml create mode 100644 platform/content/faqs/platform-skillset-instance.yaml create mode 100644 platform/content/faqs/platform-skillsets.yaml create mode 100644 platform/content/faqs/platform-space-instance.yaml create mode 100644 platform/content/faqs/platform-spaces.yaml create mode 100644 platform/content/faqs/platform-task-instance.yaml create mode 100644 platform/content/faqs/platform-tasks.yaml create mode 100644 platform/content/faqs/platform-team-instance.yaml create mode 100644 platform/content/faqs/platform-teams.yaml create mode 100644 platform/content/faqs/platform-token-instance.yaml create mode 100644 platform/content/faqs/platform-tokens.yaml create mode 100644 platform/content/faqs/platform-usage.yaml create mode 100644 platform/content/faqs/platform-user-instance.yaml create mode 100644 platform/content/faqs/platform-users.yaml create mode 100644 platform/content/faqs/platform-webhooks.yaml create mode 100644 platform/content/faqs/types.d.ts create mode 100644 platform/content/faqs/website-examples.yaml create mode 100644 platform/content/faqs/website-playground-ability.yaml create mode 100644 platform/content/faqs/website-playground-api.yaml create mode 100644 platform/content/faqs/website-playground-backstory.yaml create mode 100644 platform/content/faqs/website-playground-conversation.yaml create mode 100644 platform/content/faqs/website-playground-entity.yaml create mode 100644 platform/content/faqs/website-playground-graphql.yaml create mode 100644 platform/content/faqs/website-playground-html.yaml create mode 100644 platform/content/faqs/website-playground-image.yaml create mode 100644 platform/content/faqs/website-playground-jmespath.yaml create mode 100644 platform/content/faqs/website-playground-jsonpath.yaml create mode 100644 platform/content/faqs/website-playground-message.yaml create mode 100644 platform/content/faqs/website-playground-pdf.yaml create mode 100644 platform/content/faqs/website-playground-record.yaml create mode 100644 platform/content/faqs/website-playground-widget.yaml create mode 100644 platform/content/faqs/website-playground.yaml create mode 100644 platform/content/other/connections.d.ts create mode 100644 platform/content/other/connections.utest.js create mode 100644 platform/content/other/connections.yaml create mode 100644 platform/data/abilities/all.ts create mode 100644 platform/data/abilities/all.utest.ts create mode 100644 platform/data/abilities/auth.utest.js create mode 100644 platform/data/abilities/catalogue.d.ts create mode 100644 platform/data/abilities/catalogue/README.md create mode 100644 platform/data/abilities/catalogue/ably.ts create mode 100644 platform/data/abilities/catalogue/ably.utest.js create mode 100644 platform/data/abilities/catalogue/abstractapi.api.ts create mode 100644 platform/data/abilities/catalogue/abstractapi.email.openapi.yaml create mode 100644 platform/data/abilities/catalogue/abstractapi.email.ts create mode 100644 platform/data/abilities/catalogue/abstractapi.email.utest.js create mode 100644 platform/data/abilities/catalogue/abstractapi.holidays.openapi.yaml create mode 100644 platform/data/abilities/catalogue/abstractapi.holidays.ts create mode 100644 platform/data/abilities/catalogue/abstractapi.holidays.utest.js create mode 100644 platform/data/abilities/catalogue/abstractapi.ip.openapi.yaml create mode 100644 platform/data/abilities/catalogue/abstractapi.ip.ts create mode 100644 platform/data/abilities/catalogue/abstractapi.ip.utest.js create mode 100644 platform/data/abilities/catalogue/abstractapi.phone.openapi.yaml create mode 100644 platform/data/abilities/catalogue/abstractapi.phone.ts create mode 100644 platform/data/abilities/catalogue/abstractapi.phone.utest.js create mode 100644 platform/data/abilities/catalogue/abstractapi.vat.openapi.yaml create mode 100644 platform/data/abilities/catalogue/abstractapi.vat.ts create mode 100644 platform/data/abilities/catalogue/abstractapi.vat.utest.js create mode 100644 platform/data/abilities/catalogue/accuweather.openapi.yaml create mode 100644 platform/data/abilities/catalogue/accuweather.ts create mode 100644 platform/data/abilities/catalogue/accuweather.utest.js create mode 100644 platform/data/abilities/catalogue/activecampaign.openapi.yaml create mode 100644 platform/data/abilities/catalogue/activecampaign.ts create mode 100644 platform/data/abilities/catalogue/activecampaign.utest.js create mode 100644 platform/data/abilities/catalogue/ahrefs.openapi.yaml create mode 100644 platform/data/abilities/catalogue/ahrefs.ts create mode 100644 platform/data/abilities/catalogue/ahrefs.utest.js create mode 100644 platform/data/abilities/catalogue/airtable.openapi.yaml create mode 100644 platform/data/abilities/catalogue/airtable.ts create mode 100644 platform/data/abilities/catalogue/airtable.utest.js create mode 100644 platform/data/abilities/catalogue/alpaca.ts create mode 100644 platform/data/abilities/catalogue/alphavantage.openapi.yaml create mode 100644 platform/data/abilities/catalogue/alphavantage.ts create mode 100644 platform/data/abilities/catalogue/alphavantage.utest.js create mode 100644 platform/data/abilities/catalogue/amplitude.openapi.yaml create mode 100644 platform/data/abilities/catalogue/amplitude.ts create mode 100644 platform/data/abilities/catalogue/amplitude.utest.js create mode 100644 platform/data/abilities/catalogue/anthropic.yaml create mode 100644 platform/data/abilities/catalogue/apollo.ts create mode 100644 platform/data/abilities/catalogue/asana.ts create mode 100644 platform/data/abilities/catalogue/asana.utest.js create mode 100644 platform/data/abilities/catalogue/atlassian.ts create mode 100644 platform/data/abilities/catalogue/attio.ts create mode 100644 platform/data/abilities/catalogue/attio.utest.js create mode 100644 platform/data/abilities/catalogue/bamboohr.openapi.yaml create mode 100644 platform/data/abilities/catalogue/bamboohr.ts create mode 100644 platform/data/abilities/catalogue/bamboohr.utest.js create mode 100644 platform/data/abilities/catalogue/barcodelookup.openapi.yaml create mode 100644 platform/data/abilities/catalogue/barcodelookup.ts create mode 100644 platform/data/abilities/catalogue/barcodelookup.utest.js create mode 100644 platform/data/abilities/catalogue/basecamp.ts create mode 100644 platform/data/abilities/catalogue/beehiiv.ts create mode 100644 platform/data/abilities/catalogue/beehiiv.utest.js create mode 100644 platform/data/abilities/catalogue/betterstack.clickhouse.ts create mode 100644 platform/data/abilities/catalogue/betterstack.clickhouse.utest.js create mode 100644 platform/data/abilities/catalogue/bigcommerce.openapi.yaml create mode 100644 platform/data/abilities/catalogue/bigcommerce.ts create mode 100644 platform/data/abilities/catalogue/bigcommerce.utest.js create mode 100644 platform/data/abilities/catalogue/brandfetch.openapi.yaml create mode 100644 platform/data/abilities/catalogue/brandfetch.ts create mode 100644 platform/data/abilities/catalogue/brandfetch.utest.js create mode 100644 platform/data/abilities/catalogue/brave.openapi.yaml create mode 100644 platform/data/abilities/catalogue/brave.ts create mode 100644 platform/data/abilities/catalogue/brave.utest.js create mode 100644 platform/data/abilities/catalogue/buffer.ts create mode 100644 platform/data/abilities/catalogue/cal.openapi.yaml create mode 100644 platform/data/abilities/catalogue/cal.ts create mode 100644 platform/data/abilities/catalogue/cal.utest.js create mode 100644 platform/data/abilities/catalogue/calendly.ts create mode 100644 platform/data/abilities/catalogue/calendly.utest.js create mode 100644 platform/data/abilities/catalogue/cbk.abort.ts create mode 100644 platform/data/abilities/catalogue/cbk.agent.ts create mode 100644 platform/data/abilities/catalogue/cbk.anam.ts create mode 100644 platform/data/abilities/catalogue/cbk.attachment.yaml create mode 100644 platform/data/abilities/catalogue/cbk.avatar.ts create mode 100644 platform/data/abilities/catalogue/cbk.blueprint.ts create mode 100644 platform/data/abilities/catalogue/cbk.bot.ts create mode 100644 platform/data/abilities/catalogue/cbk.conversation.ts create mode 100644 platform/data/abilities/catalogue/cbk.dataset.ts create mode 100644 platform/data/abilities/catalogue/cbk.dataset.yaml create mode 100644 platform/data/abilities/catalogue/cbk.discord.ts create mode 100644 platform/data/abilities/catalogue/cbk.email.ts create mode 100644 platform/data/abilities/catalogue/cbk.email.yaml create mode 100644 platform/data/abilities/catalogue/cbk.fetch.ts create mode 100644 platform/data/abilities/catalogue/cbk.file.ts create mode 100644 platform/data/abilities/catalogue/cbk.git.ts create mode 100644 platform/data/abilities/catalogue/cbk.github.ts create mode 100644 platform/data/abilities/catalogue/cbk.googlechat.ts create mode 100644 platform/data/abilities/catalogue/cbk.graphql.ts create mode 100644 platform/data/abilities/catalogue/cbk.image.ts create mode 100644 platform/data/abilities/catalogue/cbk.instagram.ts create mode 100644 platform/data/abilities/catalogue/cbk.list.ts create mode 100644 platform/data/abilities/catalogue/cbk.listen.yaml create mode 100644 platform/data/abilities/catalogue/cbk.math.yaml create mode 100644 platform/data/abilities/catalogue/cbk.mcp.ts create mode 100644 platform/data/abilities/catalogue/cbk.memory.ts create mode 100644 platform/data/abilities/catalogue/cbk.messenger.ts create mode 100644 platform/data/abilities/catalogue/cbk.microsoftteams.ts create mode 100644 platform/data/abilities/catalogue/cbk.mock.yaml create mode 100644 platform/data/abilities/catalogue/cbk.rating.ts create mode 100644 platform/data/abilities/catalogue/cbk.recall.ts create mode 100644 platform/data/abilities/catalogue/cbk.research.ts create mode 100644 platform/data/abilities/catalogue/cbk.search.yaml create mode 100644 platform/data/abilities/catalogue/cbk.shell.ts create mode 100644 platform/data/abilities/catalogue/cbk.skills.ts create mode 100644 platform/data/abilities/catalogue/cbk.skillset.ts create mode 100644 platform/data/abilities/catalogue/cbk.slack.ts create mode 100644 platform/data/abilities/catalogue/cbk.space.ts create mode 100644 platform/data/abilities/catalogue/cbk.task.ts create mode 100644 platform/data/abilities/catalogue/cbk.telegram.ts create mode 100644 platform/data/abilities/catalogue/cbk.text.yaml create mode 100644 platform/data/abilities/catalogue/cbk.time.ts create mode 100644 platform/data/abilities/catalogue/cbk.todo.ts create mode 100644 platform/data/abilities/catalogue/cbk.twilio.ts create mode 100644 platform/data/abilities/catalogue/cbk.url.ts create mode 100644 platform/data/abilities/catalogue/cbk.view.yaml create mode 100644 platform/data/abilities/catalogue/cbk.whatsapp.ts create mode 100644 platform/data/abilities/catalogue/chargebee.ts create mode 100644 platform/data/abilities/catalogue/clearbit.openapi.yaml create mode 100644 platform/data/abilities/catalogue/clearbit.ts create mode 100644 platform/data/abilities/catalogue/clearbit.utest.js create mode 100644 platform/data/abilities/catalogue/clickhouse.ts create mode 100644 platform/data/abilities/catalogue/clickup.ts create mode 100644 platform/data/abilities/catalogue/clickup.utest.js.bck create mode 100644 platform/data/abilities/catalogue/clockify.ts create mode 100644 platform/data/abilities/catalogue/clockify.utest.js create mode 100644 platform/data/abilities/catalogue/cloudflare.sandbox.ts create mode 100644 platform/data/abilities/catalogue/cloudinary.ts create mode 100644 platform/data/abilities/catalogue/cloudinary.utest.js create mode 100644 platform/data/abilities/catalogue/coda.ts create mode 100644 platform/data/abilities/catalogue/coda.utest.js create mode 100644 platform/data/abilities/catalogue/codeqr.openapi.yaml create mode 100644 platform/data/abilities/catalogue/codeqr.ts create mode 100644 platform/data/abilities/catalogue/codeqr.utest.js create mode 100644 platform/data/abilities/catalogue/coinapi.openapi.yaml create mode 100644 platform/data/abilities/catalogue/coinapi.ts create mode 100644 platform/data/abilities/catalogue/coinapi.utest.js create mode 100644 platform/data/abilities/catalogue/devto.openapi.yaml create mode 100644 platform/data/abilities/catalogue/devto.ts create mode 100644 platform/data/abilities/catalogue/devto.utest.js create mode 100644 platform/data/abilities/catalogue/dictionaryapi.openapi.yaml create mode 100644 platform/data/abilities/catalogue/dictionaryapi.ts create mode 100644 platform/data/abilities/catalogue/dictionaryapi.utest.js create mode 100644 platform/data/abilities/catalogue/diffbot.openapi.yaml create mode 100644 platform/data/abilities/catalogue/diffbot.ts create mode 100644 platform/data/abilities/catalogue/diffbot.utest.js create mode 100644 platform/data/abilities/catalogue/discord.ts create mode 100644 platform/data/abilities/catalogue/discord.utest.js create mode 100644 platform/data/abilities/catalogue/docusign.api.ts create mode 100644 platform/data/abilities/catalogue/docusign.esignature.ts create mode 100644 platform/data/abilities/catalogue/docusign.esignature.utest.js create mode 100644 platform/data/abilities/catalogue/dropbox.openapi.yaml create mode 100644 platform/data/abilities/catalogue/dropbox.ts create mode 100644 platform/data/abilities/catalogue/dropbox.utest.js create mode 100644 platform/data/abilities/catalogue/dropbox.yaml create mode 100644 platform/data/abilities/catalogue/easypost.openapi.yaml create mode 100644 platform/data/abilities/catalogue/easypost.ts create mode 100644 platform/data/abilities/catalogue/easypost.utest.js create mode 100644 platform/data/abilities/catalogue/elevenlabs.openapi.yaml create mode 100644 platform/data/abilities/catalogue/elevenlabs.ts create mode 100644 platform/data/abilities/catalogue/elevenlabs.utest.js create mode 100644 platform/data/abilities/catalogue/facebook.ads.openapi.yaml create mode 100644 platform/data/abilities/catalogue/facebook.ads.ts create mode 100644 platform/data/abilities/catalogue/facebook.ads.utest.js create mode 100644 platform/data/abilities/catalogue/facebook.pages.openapi.yaml create mode 100644 platform/data/abilities/catalogue/facebook.pages.ts create mode 100644 platform/data/abilities/catalogue/facebook.pages.utest.js create mode 100644 platform/data/abilities/catalogue/figma.ts create mode 100644 platform/data/abilities/catalogue/figma.utest.js create mode 100644 platform/data/abilities/catalogue/financialmodelingprep.yaml create mode 100644 platform/data/abilities/catalogue/firecrawl.ts create mode 100644 platform/data/abilities/catalogue/firecrawl.utest.js create mode 100644 platform/data/abilities/catalogue/freshdesk.yaml create mode 100644 platform/data/abilities/catalogue/geocodio.ts create mode 100644 platform/data/abilities/catalogue/geocodio.utest.js create mode 100644 platform/data/abilities/catalogue/giphy.ts create mode 100644 platform/data/abilities/catalogue/giphy.utest.js create mode 100644 platform/data/abilities/catalogue/github.ts create mode 100644 platform/data/abilities/catalogue/github.utest.js create mode 100644 platform/data/abilities/catalogue/github.yaml create mode 100644 platform/data/abilities/catalogue/glimpse.openapi.yaml create mode 100644 platform/data/abilities/catalogue/glimpse.ts create mode 100644 platform/data/abilities/catalogue/glimpse.utest.js create mode 100644 platform/data/abilities/catalogue/godaddy.ts create mode 100644 platform/data/abilities/catalogue/godaddy.utest.js create mode 100644 platform/data/abilities/catalogue/gohighlevel.ts create mode 100644 platform/data/abilities/catalogue/google.ads.ts create mode 100644 platform/data/abilities/catalogue/google.calendar.ts create mode 100644 platform/data/abilities/catalogue/google.docs.ts create mode 100644 platform/data/abilities/catalogue/google.drive.ts create mode 100644 platform/data/abilities/catalogue/google.mail.ts create mode 100644 platform/data/abilities/catalogue/google.meet.ts create mode 100644 platform/data/abilities/catalogue/google.searchconsole.ts create mode 100644 platform/data/abilities/catalogue/google.searchconsole.utest.js create mode 100644 platform/data/abilities/catalogue/google.sheets.ts create mode 100644 platform/data/abilities/catalogue/google.tasks.ts create mode 100644 platform/data/abilities/catalogue/gumroad.openapi.yaml create mode 100644 platform/data/abilities/catalogue/gumroad.ts create mode 100644 platform/data/abilities/catalogue/gumroad.utest.js create mode 100644 platform/data/abilities/catalogue/harvest.ts create mode 100644 platform/data/abilities/catalogue/harvest.utest.js create mode 100644 platform/data/abilities/catalogue/hubspot.ts create mode 100644 platform/data/abilities/catalogue/hunter.openapi.yaml create mode 100644 platform/data/abilities/catalogue/hunter.ts create mode 100644 platform/data/abilities/catalogue/hunter.utest.js create mode 100644 platform/data/abilities/catalogue/hyperproof.yaml create mode 100644 platform/data/abilities/catalogue/instantly.yaml create mode 100644 platform/data/abilities/catalogue/intercom.ts create mode 100644 platform/data/abilities/catalogue/intercom.utest.js create mode 100644 platform/data/abilities/catalogue/lemonsqueezy.openapi.yaml create mode 100644 platform/data/abilities/catalogue/lemonsqueezy.ts create mode 100644 platform/data/abilities/catalogue/lemonsqueezy.utest.js create mode 100644 platform/data/abilities/catalogue/linear.openapi.yaml create mode 100644 platform/data/abilities/catalogue/linear.ts create mode 100644 platform/data/abilities/catalogue/linear.utest.js create mode 100644 platform/data/abilities/catalogue/linkedin.openapi.yaml create mode 100644 platform/data/abilities/catalogue/linkedin.ts create mode 100644 platform/data/abilities/catalogue/linkedin.utest.js create mode 100644 platform/data/abilities/catalogue/linkupso.openapi.yaml create mode 100644 platform/data/abilities/catalogue/linkupso.ts create mode 100644 platform/data/abilities/catalogue/linkupso.utest.js create mode 100644 platform/data/abilities/catalogue/listennotes.ts create mode 100644 platform/data/abilities/catalogue/listennotes.utest.js create mode 100644 platform/data/abilities/catalogue/magento.yaml create mode 100644 platform/data/abilities/catalogue/mailchimp.utest.js create mode 100644 platform/data/abilities/catalogue/mailchimp.yaml create mode 100644 platform/data/abilities/catalogue/mailgun.api.ts create mode 100644 platform/data/abilities/catalogue/mailgun.send.ts create mode 100644 platform/data/abilities/catalogue/mailgun.send.utest.js create mode 100644 platform/data/abilities/catalogue/mailgun.validate.ts create mode 100644 platform/data/abilities/catalogue/mailgun.validate.utest.js create mode 100644 platform/data/abilities/catalogue/make.yaml create mode 100644 platform/data/abilities/catalogue/manychat.ts create mode 100644 platform/data/abilities/catalogue/manychat.utest.js create mode 100644 platform/data/abilities/catalogue/mapbox.openapi.yaml create mode 100644 platform/data/abilities/catalogue/mapbox.ts create mode 100644 platform/data/abilities/catalogue/mapbox.utest.js create mode 100644 platform/data/abilities/catalogue/matillion.openapi.yaml create mode 100644 platform/data/abilities/catalogue/matillion.ts create mode 100644 platform/data/abilities/catalogue/matillion.utest.js create mode 100644 platform/data/abilities/catalogue/mcp.yaml create mode 100644 platform/data/abilities/catalogue/microsoft.graph.api.ts create mode 100644 platform/data/abilities/catalogue/microsoft.graph.calendar.ts create mode 100644 platform/data/abilities/catalogue/microsoft.graph.calendar.utest.js create mode 100644 platform/data/abilities/catalogue/microsoft.graph.contact.ts create mode 100644 platform/data/abilities/catalogue/microsoft.graph.contact.utest.js create mode 100644 platform/data/abilities/catalogue/microsoft.graph.file.ts create mode 100644 platform/data/abilities/catalogue/microsoft.graph.file.utest.js create mode 100644 platform/data/abilities/catalogue/microsoft.graph.message.ts create mode 100644 platform/data/abilities/catalogue/microsoft.graph.message.utest.js create mode 100644 platform/data/abilities/catalogue/microsoft.graph.openapi.yaml create mode 100644 platform/data/abilities/catalogue/microsoft.graph.team.ts create mode 100644 platform/data/abilities/catalogue/microsoft.graph.team.utest.js create mode 100644 platform/data/abilities/catalogue/microsoft.graph.user.ts create mode 100644 platform/data/abilities/catalogue/microsoft.graph.user.utest.js create mode 100644 platform/data/abilities/catalogue/miro.ts create mode 100644 platform/data/abilities/catalogue/miro.utest.js create mode 100644 platform/data/abilities/catalogue/mixpanel.yaml create mode 100644 platform/data/abilities/catalogue/modelcontextprotocol.ts create mode 100644 platform/data/abilities/catalogue/modelcontextprotocol.utest.js create mode 100644 platform/data/abilities/catalogue/moltbook.ts create mode 100644 platform/data/abilities/catalogue/monday.ts create mode 100644 platform/data/abilities/catalogue/newsapi.openapi.yaml create mode 100644 platform/data/abilities/catalogue/newsapi.ts create mode 100644 platform/data/abilities/catalogue/newsapi.utest.js create mode 100644 platform/data/abilities/catalogue/notion.yaml create mode 100644 platform/data/abilities/catalogue/nubela.yaml create mode 100644 platform/data/abilities/catalogue/okta.ts create mode 100644 platform/data/abilities/catalogue/okta.utest.js create mode 100644 platform/data/abilities/catalogue/openai.ads.openapi.yaml create mode 100644 platform/data/abilities/catalogue/openai.ads.ts create mode 100644 platform/data/abilities/catalogue/openai.ads.utest.js create mode 100644 platform/data/abilities/catalogue/openstreetmap.yaml create mode 100644 platform/data/abilities/catalogue/openweathermap.openapi.yaml create mode 100644 platform/data/abilities/catalogue/openweathermap.ts create mode 100644 platform/data/abilities/catalogue/openweathermap.utest.js create mode 100644 platform/data/abilities/catalogue/pagerduty.ts create mode 100644 platform/data/abilities/catalogue/pagerduty.utest.js create mode 100644 platform/data/abilities/catalogue/paypal.yaml create mode 100644 platform/data/abilities/catalogue/peopledatalabs.yaml create mode 100644 platform/data/abilities/catalogue/perplexity.yaml create mode 100644 platform/data/abilities/catalogue/pexels.openapi.yaml create mode 100644 platform/data/abilities/catalogue/pexels.ts create mode 100644 platform/data/abilities/catalogue/pexels.utest.js create mode 100644 platform/data/abilities/catalogue/pipedream.yaml create mode 100644 platform/data/abilities/catalogue/pipedrive.ts create mode 100644 platform/data/abilities/catalogue/pipedrive.utest.js create mode 100644 platform/data/abilities/catalogue/planetscale.ts create mode 100644 platform/data/abilities/catalogue/planetscale.utest.js create mode 100644 platform/data/abilities/catalogue/polymarket.ts create mode 100644 platform/data/abilities/catalogue/polymarket.utest.js create mode 100644 platform/data/abilities/catalogue/postgrest.ts create mode 100644 platform/data/abilities/catalogue/productboard.ts create mode 100644 platform/data/abilities/catalogue/raindrop.openapi.yaml create mode 100644 platform/data/abilities/catalogue/raindrop.ts create mode 100644 platform/data/abilities/catalogue/raindrop.utest.js create mode 100644 platform/data/abilities/catalogue/reddit.openapi.yaml create mode 100644 platform/data/abilities/catalogue/reddit.ts create mode 100644 platform/data/abilities/catalogue/reddit.utest.js create mode 100644 platform/data/abilities/catalogue/replicate.ts create mode 100644 platform/data/abilities/catalogue/replicate.utest.js create mode 100644 platform/data/abilities/catalogue/resend.ts create mode 100644 platform/data/abilities/catalogue/resend.utest.js create mode 100644 platform/data/abilities/catalogue/revenuecat.openapi.yaml create mode 100644 platform/data/abilities/catalogue/revenuecat.ts create mode 100644 platform/data/abilities/catalogue/revenuecat.utest.js create mode 100644 platform/data/abilities/catalogue/sendgrid.ts create mode 100644 platform/data/abilities/catalogue/sendgrid.utest.js create mode 100644 platform/data/abilities/catalogue/sentry.ts create mode 100644 platform/data/abilities/catalogue/sentry.utest.js create mode 100644 platform/data/abilities/catalogue/serpapi.ts create mode 100644 platform/data/abilities/catalogue/serper.ts create mode 100644 platform/data/abilities/catalogue/serper.utest.js create mode 100644 platform/data/abilities/catalogue/shopify.yaml create mode 100644 platform/data/abilities/catalogue/slack.ts create mode 100644 platform/data/abilities/catalogue/slack.utest.js create mode 100644 platform/data/abilities/catalogue/slack.yaml create mode 100644 platform/data/abilities/catalogue/snowflake.ts create mode 100644 platform/data/abilities/catalogue/snowflake.utest.js create mode 100644 platform/data/abilities/catalogue/sprites.ts create mode 100644 platform/data/abilities/catalogue/stripe.ts create mode 100644 platform/data/abilities/catalogue/stripe.utest.js create mode 100644 platform/data/abilities/catalogue/stripe.yaml create mode 100644 platform/data/abilities/catalogue/supabase.yaml create mode 100644 platform/data/abilities/catalogue/tally.ts create mode 100644 platform/data/abilities/catalogue/tally.utest.js create mode 100644 platform/data/abilities/catalogue/tavily.openapi.yaml create mode 100644 platform/data/abilities/catalogue/tavily.ts create mode 100644 platform/data/abilities/catalogue/tavily.utest.js create mode 100644 platform/data/abilities/catalogue/tavily.yaml create mode 100644 platform/data/abilities/catalogue/taxjar.openapi.yaml create mode 100644 platform/data/abilities/catalogue/taxjar.ts create mode 100644 platform/data/abilities/catalogue/taxjar.utest.js create mode 100644 platform/data/abilities/catalogue/telegram.ts create mode 100644 platform/data/abilities/catalogue/telegram.utest.js create mode 100644 platform/data/abilities/catalogue/todoist.ts create mode 100644 platform/data/abilities/catalogue/todoist.utest.js create mode 100644 platform/data/abilities/catalogue/trello.ts create mode 100644 platform/data/abilities/catalogue/twilio.api.ts create mode 100644 platform/data/abilities/catalogue/twilio.api.utest.js create mode 100644 platform/data/abilities/catalogue/twilio.lookup.ts create mode 100644 platform/data/abilities/catalogue/twilio.lookup.utest.js create mode 100644 platform/data/abilities/catalogue/twilio.verify.ts create mode 100644 platform/data/abilities/catalogue/twilio.verify.utest.js create mode 100644 platform/data/abilities/catalogue/twitter.openapi.yaml create mode 100644 platform/data/abilities/catalogue/twitter.ts create mode 100644 platform/data/abilities/catalogue/twitter.utest.js create mode 100644 platform/data/abilities/catalogue/typeform.openapi.yaml create mode 100644 platform/data/abilities/catalogue/typeform.ts create mode 100644 platform/data/abilities/catalogue/typeform.utest.js create mode 100644 platform/data/abilities/catalogue/unsplash.openapi.yaml create mode 100644 platform/data/abilities/catalogue/unsplash.ts create mode 100644 platform/data/abilities/catalogue/unsplash.utest.js create mode 100644 platform/data/abilities/catalogue/uplead.openapi.yaml create mode 100644 platform/data/abilities/catalogue/uplead.ts create mode 100644 platform/data/abilities/catalogue/uplead.utest.js create mode 100644 platform/data/abilities/catalogue/usefind.yaml create mode 100644 platform/data/abilities/catalogue/vanta.yaml create mode 100644 platform/data/abilities/catalogue/vapi.ts create mode 100644 platform/data/abilities/catalogue/vapi.utest.js create mode 100644 platform/data/abilities/catalogue/vonage.ts create mode 100644 platform/data/abilities/catalogue/vonage.utest.js create mode 100644 platform/data/abilities/catalogue/vonage.yaml create mode 100644 platform/data/abilities/catalogue/weatherbit.ts create mode 100644 platform/data/abilities/catalogue/weatherbit.utest.js create mode 100644 platform/data/abilities/catalogue/wikipedia.openapi.json create mode 100644 platform/data/abilities/catalogue/wikipedia.ts create mode 100644 platform/data/abilities/catalogue/wikipedia.utest.js create mode 100644 platform/data/abilities/catalogue/woocommerce.yaml create mode 100644 platform/data/abilities/catalogue/xero.accounting.ts create mode 100644 platform/data/abilities/catalogue/xero.accounting.utest.js create mode 100644 platform/data/abilities/catalogue/xero.api.ts create mode 100644 platform/data/abilities/catalogue/zapier.yaml create mode 100644 platform/data/abilities/catalogue/zendesk.ts create mode 100644 platform/data/abilities/catalogue/zendesk.utest.js create mode 100644 platform/data/abilities/catalogue/zoom.utest.js create mode 100644 platform/data/abilities/catalogue/zoom.yaml create mode 100644 platform/data/abilities/visible.ts create mode 100644 platform/data/abilities/visible.utest.js create mode 100644 platform/data/demos.d.ts create mode 100644 platform/data/demos.yaml create mode 100644 platform/data/files.d.ts create mode 100644 platform/data/files.yaml create mode 100644 platform/data/secrets/all.ts create mode 100644 platform/data/secrets/all.utest.js create mode 100644 platform/data/secrets/catalogue/standard.d.ts create mode 100644 platform/data/secrets/catalogue/standard.utest.ts create mode 100644 platform/data/secrets/catalogue/standard.yaml create mode 100644 platform/data/secrets/visible.ts create mode 100644 platform/data/secrets/visible.utest.ts create mode 100644 platform/data/spaces.d.ts create mode 100644 platform/data/spaces.yaml create mode 100644 platform/emails/ContentAbuseDetected.jsx create mode 100644 platform/emails/DatasetSyncCompleted.jsx create mode 100644 platform/emails/EmailAction.tsx create mode 100644 platform/emails/EmailAction.utest.js create mode 100644 platform/emails/EmailLogin.tsx create mode 100644 platform/emails/EmailLogin.utest.js create mode 100644 platform/emails/ExceededAccountLimits.jsx create mode 100644 platform/emails/ExceededDatabaseLimits.jsx create mode 100644 platform/emails/ExceededRateLimits.jsx create mode 100644 platform/emails/InvoicePaymentFailed.jsx create mode 100644 platform/emails/InvoicePaymentSucceeded.jsx create mode 100644 platform/emails/NearlyExceededAccountLimits.jsx create mode 100644 platform/emails/NearlyExceededDatabaseLimits.jsx create mode 100644 platform/emails/SubscriptionDeleted.jsx create mode 100644 platform/emails/TeamInvitation.tsx create mode 100644 platform/emails/TeamInvitation.utest.js create mode 100644 platform/emails/TrialStart.jsx create mode 100644 platform/emails/TrialStart.utest.js create mode 100644 platform/emails/TrialStartDuplicateCardDetected.jsx create mode 100644 platform/emails/UsagePolicyTriggered.jsx create mode 100644 platform/emails/UserDeleted.jsx create mode 100644 platform/embeds/mcp/v1.ts create mode 100644 platform/embeds/widget/plugins/analytics-consent.js create mode 100644 platform/embeds/widget/v1.ts create mode 100644 platform/embeds/widget/v2.ts create mode 100644 platform/eslint.config.mjs create mode 100644 platform/eslint/README.md create mode 100644 platform/eslint/custom-rules/directive-first.js create mode 100644 platform/eslint/custom-rules/directive-first.utest.js create mode 100644 platform/eslint/custom-rules/index.js create mode 100644 platform/eslint/custom-rules/lib/client-files.js create mode 100644 platform/eslint/custom-rules/lib/global-fetch.js create mode 100644 platform/eslint/custom-rules/lib/transpiled-packages.js create mode 100644 platform/eslint/custom-rules/no-direct-documentation-links.js create mode 100644 platform/eslint/custom-rules/no-direct-documentation-links.utest.js create mode 100644 platform/eslint/custom-rules/no-direct-process-env.js create mode 100644 platform/eslint/custom-rules/no-direct-process-env.utest.js create mode 100644 platform/eslint/custom-rules/no-global-fetch.js create mode 100644 platform/eslint/custom-rules/no-global-fetch.utest.js create mode 100644 platform/eslint/custom-rules/no-plain-fetch-in-routes.js create mode 100644 platform/eslint/custom-rules/no-plain-fetch-in-routes.utest.js create mode 100644 platform/eslint/custom-rules/no-restricted-client-imports.js create mode 100644 platform/eslint/custom-rules/no-restricted-client-imports.utest.js create mode 100644 platform/eslint/custom-rules/package.json create mode 100644 platform/eslint/custom-rules/require-custom-use-router.js create mode 100644 platform/eslint/custom-rules/require-dispose-for-factory-result.js create mode 100644 platform/eslint/custom-rules/require-make-json-safe.js create mode 100644 platform/eslint/custom-rules/require-safe-prisma-delete.js create mode 100644 platform/eslint/custom-rules/require-transpiled-package.js create mode 100644 platform/eslint/custom-rules/require-transpiled-package.utest.js create mode 100644 platform/eslint/custom-rules/require-typed-sql.js create mode 100644 platform/eslint/custom-rules/todo-by.js create mode 100644 platform/eslint/custom-rules/todo-by.utest.js create mode 100644 platform/examples/catalogue.utest.ts create mode 100644 platform/examples/catalogue/blueprints.yaml create mode 100644 platform/examples/catalogue/demos.yaml create mode 100644 platform/examples/catalogue/hub.yaml create mode 100644 platform/examples/catalogue/projects.yaml create mode 100644 platform/examples/catalogue/widgets.yaml create mode 100644 platform/examples/index.ts create mode 100644 platform/examples/index.utest.js create mode 100644 platform/examples/types.d.ts create mode 100644 platform/graphql/v1/client/graphql.ts create mode 100644 platform/graphql/v1/client/index.ts create mode 100644 platform/graphql/v1/pothos.utest.js create mode 100644 platform/graphql/v1/queries/apps/availableSecrets.graphql create mode 100644 platform/graphql/v1/queries/apps/availableSharedSecrets.graphql create mode 100644 platform/graphql/v1/queries/apps/availableSources.graphql create mode 100644 platform/graphql/v1/queries/apps/blueprintAgentResources.graphql create mode 100644 platform/graphql/v1/queries/apps/configuredBots.graphql create mode 100644 platform/graphql/v1/queries/apps/listContactConversations.graphql create mode 100644 platform/graphql/v1/queries/apps/listContactTasks.graphql create mode 100644 platform/graphql/v1/queries/blueprints/portalUrl.graphql create mode 100644 platform/graphql/v1/queries/blueprints/templateBlueprints.graphql create mode 100644 platform/graphql/v1/queries/blueprints/templateSecrets.graphql create mode 100644 platform/graphql/v1/queries/platform/templates.graphql create mode 100644 platform/graphql/v1/schema.mutations.utest.ts create mode 100644 platform/graphql/v1/schema.ts create mode 100644 platform/hooks/useAbilityTemplates.js create mode 100644 platform/hooks/useAbilityTemplates.utest.js create mode 100644 platform/hooks/useAborter.jsx create mode 100644 platform/hooks/useAborter.utest.js create mode 100644 platform/hooks/useAborter.utest.jsx create mode 100644 platform/hooks/useAggressiveScrollHeight.jsx create mode 100644 platform/hooks/useAggressiveScrollHeight.stories.jsx create mode 100644 platform/hooks/useAggressiveScrollHeight.utest.js create mode 100644 platform/hooks/useAutoRevert.tsx create mode 100644 platform/hooks/useAutoRevert.utest.js create mode 100644 platform/hooks/useAutoRevert.utest.jsx create mode 100644 platform/hooks/useAvailableModels.ts create mode 100644 platform/hooks/useAvailableModels.utest.jsx create mode 100644 platform/hooks/useAvatarInputAudioStream.ts create mode 100644 platform/hooks/useAvatarInputAudioStream.utest.js create mode 100644 platform/hooks/useAvatarTransport.ts create mode 100644 platform/hooks/useAvatarTransport.utest.js create mode 100644 platform/hooks/useAwaitableComponent.tsx create mode 100644 platform/hooks/useAwaitableComponent.utest.js create mode 100644 platform/hooks/useAwaitableComponent.utest.jsx create mode 100644 platform/hooks/useBotBlock.jsx create mode 100644 platform/hooks/useBotBlock.utest.js create mode 100644 platform/hooks/useBroadcastChannel.tsx create mode 100644 platform/hooks/useBroadcastChannel.utest.js create mode 100644 platform/hooks/useBroadcastChannel.utest.jsx create mode 100644 platform/hooks/useBroadcastChannelState.ts create mode 100644 platform/hooks/useBroadcastChannelState.utest.js create mode 100644 platform/hooks/useBuilderExperience.jsx create mode 100644 platform/hooks/useBuilderExperience.utest.jsx create mode 100644 platform/hooks/useBus.tsx create mode 100644 platform/hooks/useBus.utest.js create mode 100644 platform/hooks/useBus.utest.jsx create mode 100644 platform/hooks/useCache.tsx create mode 100644 platform/hooks/useCache.utest.js create mode 100644 platform/hooks/useClassNameOnNewElements.jsx create mode 100644 platform/hooks/useClassNameOnNewElements.utest.js create mode 100644 platform/hooks/useClickOutside.tsx create mode 100644 platform/hooks/useClickOutside.utest.js create mode 100644 platform/hooks/useClipboardContainer.tsx create mode 100644 platform/hooks/useClipboardContainer.utest.js create mode 100644 platform/hooks/useCodeAction.tsx create mode 100644 platform/hooks/useCodeAction.utest.js create mode 100644 platform/hooks/useComboKeyBinding.tsx create mode 100644 platform/hooks/useComboKeyBinding.utest.js create mode 100644 platform/hooks/useControllableInput.tsx create mode 100644 platform/hooks/useControllableInput.utest.js create mode 100644 platform/hooks/useControlledState.stories.jsx create mode 100644 platform/hooks/useControlledState.tsx create mode 100644 platform/hooks/useControlledState.utest.js create mode 100644 platform/hooks/useConversationManager.jsx create mode 100644 platform/hooks/useConversationManager.utest.js create mode 100644 platform/hooks/useConversationManager2.jsx create mode 100644 platform/hooks/useConversationManager2.utest.js create mode 100644 platform/hooks/useConversationManagerFetch.jsx create mode 100644 platform/hooks/useConversationManagerFetch.utest.js create mode 100644 platform/hooks/useConversationManagerState.jsx create mode 100644 platform/hooks/useConversationManagerState.utest.js create mode 100644 platform/hooks/useConversationMonitor.tsx create mode 100644 platform/hooks/useConversationMonitor.utest.js create mode 100644 platform/hooks/useCookie.tsx create mode 100644 platform/hooks/useCookie.utest.js create mode 100644 platform/hooks/useCopyWebsiteTheme.tsx create mode 100644 platform/hooks/useCopyWebsiteTheme.utest.js create mode 100644 platform/hooks/useDOMQuerySelector.jsx create mode 100644 platform/hooks/useDOMQuerySelector.stories.jsx create mode 100644 platform/hooks/useDOMQuerySelector.utest.js create mode 100644 platform/hooks/useDOMQuerySelector.utest.jsx create mode 100644 platform/hooks/useDashboardWidgetSend.tsx create mode 100644 platform/hooks/useDashboardWidgetSend.utest.js create mode 100644 platform/hooks/useDebounce.ts create mode 100644 platform/hooks/useDebounce.utest.js create mode 100644 platform/hooks/useDebouncedInput.ts create mode 100644 platform/hooks/useDebouncedInput.utest.js create mode 100644 platform/hooks/useDeps.ts create mode 100644 platform/hooks/useDeps.utest.js create mode 100644 platform/hooks/useDropzone.tsx create mode 100644 platform/hooks/useDropzone.utest.js create mode 100644 platform/hooks/useEntryAnimation.tsx create mode 100644 platform/hooks/useEntryAnimation.utest.js create mode 100644 platform/hooks/useEventChannel.ts create mode 100644 platform/hooks/useEventChannel.utest.js create mode 100644 platform/hooks/useExternalAPIURL.ts create mode 100644 platform/hooks/useExternalAPIURL.utest.js create mode 100644 platform/hooks/useExternalFrontendURL.ts create mode 100644 platform/hooks/useExternalFrontendURL.utest.jsx create mode 100644 platform/hooks/useExtractIntegrationSeries.js create mode 100644 platform/hooks/useExtractIntegrationSeries.utest.js create mode 100644 platform/hooks/useFetch.jsx create mode 100644 platform/hooks/useFetch.utest.js create mode 100644 platform/hooks/useFirst.tsx create mode 100644 platform/hooks/useFirst.utest.js create mode 100644 platform/hooks/useFormElementAutoTools.jsx create mode 100644 platform/hooks/useFormElementAutoTools.utest.js create mode 100644 platform/hooks/useFramePopup.jsx create mode 100644 platform/hooks/useFramePopup.utest.js create mode 100644 platform/hooks/useFunctionDispatch.tsx create mode 100644 platform/hooks/useFunctionDispatch.utest.js create mode 100644 platform/hooks/useFunctionPacks.ts create mode 100644 platform/hooks/useFunctionPacks.utest.js create mode 100644 platform/hooks/useFuzzySearch.ts create mode 100644 platform/hooks/useFuzzySearch.utest.js create mode 100644 platform/hooks/useFuzzySearchFunction.ts create mode 100644 platform/hooks/useFuzzySearchFunction.utest.js create mode 100644 platform/hooks/useGraphQLConnectionListRoute.jsx create mode 100644 platform/hooks/useGraphQLConnectionListRoute.utest.js create mode 100644 platform/hooks/useHasChildren.tsx create mode 100644 platform/hooks/useHasChildren.utest.js create mode 100644 platform/hooks/useHistory.jsx create mode 100644 platform/hooks/useHistory.utest.js create mode 100644 platform/hooks/useHostname.tsx create mode 100644 platform/hooks/useHostname.utest.js create mode 100644 platform/hooks/useHydrated.tsx create mode 100644 platform/hooks/useHydrationSafeLayoutEffect.ts create mode 100644 platform/hooks/useImageColorPalette.tsx create mode 100644 platform/hooks/useImageColorPalette.utest.js create mode 100644 platform/hooks/useImageColorPalette.utest.jsx create mode 100644 platform/hooks/useIncrementIndexAfterDelay.stories.js create mode 100644 platform/hooks/useIncrementIndexAfterDelay.stories.utest.js create mode 100644 platform/hooks/useIncrementIndexAfterDelay.ts create mode 100644 platform/hooks/useIncrementIndexAfterDelay.utest.js create mode 100644 platform/hooks/useInitial.tsx create mode 100644 platform/hooks/useInitial.utest.js create mode 100644 platform/hooks/useInstallIntent.ts create mode 100644 platform/hooks/useInstallIntent.utest.jsx create mode 100644 platform/hooks/useIsAppRouter.tsx create mode 100644 platform/hooks/useIsAppRouter.utest.js create mode 100644 platform/hooks/useIsContainerScrolled.jsx create mode 100644 platform/hooks/useIsContainerScrolled.stories.jsx create mode 100644 platform/hooks/useIsContainerScrolled.stories.utest.js create mode 100644 platform/hooks/useIsContainerScrolled.utest.js create mode 100644 platform/hooks/useIsMounted.tsx create mode 100644 platform/hooks/useIsMounted.utest.js create mode 100644 platform/hooks/useIsScrolled.tsx create mode 100644 platform/hooks/useIsScrolled.utest.js create mode 100644 platform/hooks/useIsTop.tsx create mode 100644 platform/hooks/useIsTop.utest.js create mode 100644 platform/hooks/useLocalStorage.tsx create mode 100644 platform/hooks/useLocalStorage.utest.js create mode 100644 platform/hooks/useMagicDialog.jsx create mode 100644 platform/hooks/useMagicDialog.utest.js create mode 100644 platform/hooks/useMediaQuery.tsx create mode 100644 platform/hooks/useMediaQuery.utest.js create mode 100644 platform/hooks/useMintedClient.ts create mode 100644 platform/hooks/useMintedClient.utest.js create mode 100644 platform/hooks/useMouseOutsideWindow.tsx create mode 100644 platform/hooks/useMouseOutsideWindow.utest.js create mode 100644 platform/hooks/usePartner.jsx create mode 100644 platform/hooks/usePartner.utest.js create mode 100644 platform/hooks/usePartner.utest.jsx create mode 100644 platform/hooks/usePlatformExperience.jsx create mode 100644 platform/hooks/usePopup.jsx create mode 100644 platform/hooks/usePopup.utest.js create mode 100644 platform/hooks/usePopupJob.jsx create mode 100644 platform/hooks/usePopupJob.utest.js create mode 100644 platform/hooks/usePopupJob.utest.jsx create mode 100644 platform/hooks/usePostMessageHandler.jsx create mode 100644 platform/hooks/usePostMessageHandler.utest.js create mode 100644 platform/hooks/usePreventLeave.tsx create mode 100644 platform/hooks/usePreventLeave.utest.js create mode 100644 platform/hooks/usePreventLeave.utest.jsx create mode 100644 platform/hooks/usePrevious.ts create mode 100644 platform/hooks/usePrevious.utest.js create mode 100644 platform/hooks/useProjectScope.jsx create mode 100644 platform/hooks/useProjectScope.utest.jsx create mode 100644 platform/hooks/useQuerySessionOption.tsx create mode 100644 platform/hooks/useQuerySessionOption.utest.js create mode 100644 platform/hooks/useQuerySessionOption.utest.jsx create mode 100644 platform/hooks/useReadyNotification.tsx create mode 100644 platform/hooks/useReadyNotification.utest.js create mode 100644 platform/hooks/useRouter.jsx create mode 100644 platform/hooks/useRouter.utest.jsx create mode 100644 platform/hooks/useScopedCreateData.jsx create mode 100644 platform/hooks/useScopedCreateData.utest.jsx create mode 100644 platform/hooks/useScopedQuerySessionOption.tsx create mode 100644 platform/hooks/useScopedQuerySessionOption.utest.js create mode 100644 platform/hooks/useScopedQuerySessionOption.utest.jsx create mode 100644 platform/hooks/useScreenshot.jsx create mode 100644 platform/hooks/useScreenshot.utest.js create mode 100644 platform/hooks/useScrollHeight.tsx create mode 100644 platform/hooks/useScrollHeight.utest.js create mode 100644 platform/hooks/useScrollSaveRestore.jsx create mode 100644 platform/hooks/useScrollSaveRestore.utest.js create mode 100644 platform/hooks/useSearchParam.tsx create mode 100644 platform/hooks/useSearchParam.utest.js create mode 100644 platform/hooks/useSearchParams.tsx create mode 100644 platform/hooks/useSearchParams.utest.js create mode 100644 platform/hooks/useSecretAuthenticate.js create mode 100644 platform/hooks/useSecretTemplates.js create mode 100644 platform/hooks/useSecretTemplates.utest.js create mode 100644 platform/hooks/useSession.tsx create mode 100644 platform/hooks/useSession.utest.js create mode 100644 platform/hooks/useSignin.ts create mode 100644 platform/hooks/useSignin.utest.js create mode 100644 platform/hooks/useSignout.ts create mode 100644 platform/hooks/useSignout.utest.js create mode 100644 platform/hooks/useSkillsetSecrets.js create mode 100644 platform/hooks/useSkillsetSecrets.utest.js create mode 100644 platform/hooks/useTabIndent.jsx create mode 100644 platform/hooks/useTabIndent.utest.js create mode 100644 platform/hooks/useTeamSwitch.tsx create mode 100644 platform/hooks/useTeamSwitch.utest.js create mode 100644 platform/hooks/useTextAnimation.jsx create mode 100644 platform/hooks/useTextAnimation.utest.js create mode 100644 platform/hooks/useTextCompletion.tsx create mode 100644 platform/hooks/useTextCompletion.utest.js create mode 100644 platform/hooks/useTextSelection.stories.tsx create mode 100644 platform/hooks/useTextSelection.tsx create mode 100644 platform/hooks/useTextSelection.utest.js create mode 100644 platform/hooks/useTextSelection.utest.tsx create mode 100644 platform/hooks/useTextareaSelection.tsx create mode 100644 platform/hooks/useTextareaSelection.utest.js create mode 100644 platform/hooks/useTheme.ts create mode 100644 platform/hooks/useTheme.utest.js create mode 100644 platform/hooks/useThrottle.tsx create mode 100644 platform/hooks/useThrottle.utest.js create mode 100644 platform/hooks/useThrottle.utest.jsx create mode 100644 platform/hooks/useTimeout.tsx create mode 100644 platform/hooks/useTimeout.utest.js create mode 100644 platform/hooks/useTokenCount.ts create mode 100644 platform/hooks/useTokenCount.utest.js create mode 100644 platform/hooks/useTrace.jsx create mode 100644 platform/hooks/useTrace.utest.js create mode 100644 platform/hooks/useTrace.utest.jsx create mode 100644 platform/hooks/useTraceUpdate.tsx create mode 100644 platform/hooks/useTraceUpdate.utest.js create mode 100644 platform/hooks/useUrl.tsx create mode 100644 platform/hooks/useUrl.utest.js create mode 100644 platform/hooks/useUrl.utest.jsx create mode 100644 platform/hooks/useUserSwitch.tsx create mode 100644 platform/hooks/useUserSwitch.utest.js create mode 100644 platform/hooks/useWebMCP.tsx create mode 100644 platform/hooks/useWebMCP.utest.js create mode 100644 platform/hooks/useWindowFocus.tsx create mode 100644 platform/hooks/useWindowFocus.utest.js create mode 100644 platform/i18n.config.js create mode 100644 platform/icons/avatar.svg create mode 100644 platform/icons/brands/anam.svg create mode 100644 platform/icons/brands/discord.svg create mode 100644 platform/icons/brands/mcp.svg create mode 100644 platform/icons/brands/messenger.svg create mode 100644 platform/icons/brands/recall.svg create mode 100644 platform/icons/brands/slack.svg create mode 100644 platform/icons/brands/telegram.svg create mode 100644 platform/icons/brands/twitter.svg create mode 100644 platform/icons/brands/whatsapp.svg create mode 100644 platform/icons/cbkai.svg create mode 100644 platform/icons/social/discord.svg create mode 100644 platform/icons/social/github.svg create mode 100644 platform/icons/social/instagram.svg create mode 100644 platform/icons/social/linkedin.svg create mode 100644 platform/icons/social/tiktok.svg create mode 100644 platform/icons/social/youtube.svg create mode 100644 platform/icons/widget.svg create mode 100644 platform/instrumentation-client.ts create mode 100644 platform/instrumentation.ts create mode 100644 platform/jest.itest.config.js create mode 100644 platform/jest.utest.config.js create mode 100644 platform/jest/resolver.js create mode 100644 platform/jest/utils/ability.js create mode 100644 platform/jest/utils/ability.utest.js create mode 100644 platform/jest/utils/openai.js create mode 100644 platform/jest/workspace.patterns.js create mode 100644 platform/layouts/Admin.jsx create mode 100644 platform/layouts/App.jsx create mode 100644 platform/layouts/Dashboard.jsx create mode 100644 platform/layouts/Dashboard.utest.jsx create mode 100644 platform/layouts/Demo.jsx create mode 100644 platform/layouts/Email.tsx create mode 100644 platform/layouts/Email.utest.js create mode 100644 platform/layouts/Errata.jsx create mode 100644 platform/layouts/Explore.jsx create mode 100644 platform/layouts/Explore.utest.js create mode 100644 platform/layouts/Hub.jsx create mode 100644 platform/layouts/Main.jsx create mode 100644 platform/layouts/Platform.jsx create mode 100644 platform/layouts/Wizard.jsx create mode 100644 platform/lib/ability.catalogue.ts create mode 100644 platform/lib/ability.catalogue.utest.js create mode 100644 platform/lib/ability.fields.ts create mode 100644 platform/lib/ability.fields.utest.js create mode 100644 platform/lib/ability.fields.utest.ts create mode 100644 platform/lib/ability.function.ts create mode 100644 platform/lib/ability.function.utest.js create mode 100644 platform/lib/ability.icon.ts create mode 100644 platform/lib/ability.icon.utest.js create mode 100644 platform/lib/ability.icon.utest.ts create mode 100644 platform/lib/ability.instruction.ts create mode 100644 platform/lib/ability.instruction.utest.js create mode 100644 platform/lib/ability.provider.ts create mode 100644 platform/lib/ability.provider.utest.js create mode 100644 platform/lib/ability.secret.ts create mode 100644 platform/lib/ability.secret.utest.js create mode 100644 platform/lib/ability.template.ts create mode 100644 platform/lib/ability.template.utest.js create mode 100644 platform/lib/abort.ts create mode 100644 platform/lib/abort.utest.js create mode 100644 platform/lib/abort.utest.ts create mode 100644 platform/lib/action.config.ts create mode 100644 platform/lib/action.config.utest.js create mode 100644 platform/lib/action.definition.ts create mode 100644 platform/lib/action.definition.utest.js create mode 100644 platform/lib/action.definition.utest.ts create mode 100644 platform/lib/action.exec.abort.ts create mode 100644 platform/lib/action.exec.abort.utest.js create mode 100644 platform/lib/action.exec.agent.ts create mode 100644 platform/lib/action.exec.agent.utest.js create mode 100644 platform/lib/action.exec.all.ts create mode 100644 platform/lib/action.exec.all.utest.js create mode 100644 platform/lib/action.exec.attachment.ts create mode 100644 platform/lib/action.exec.attachment.utest.js create mode 100644 platform/lib/action.exec.blueprint.ts create mode 100644 platform/lib/action.exec.blueprint.utest.js create mode 100644 platform/lib/action.exec.bot.ts create mode 100644 platform/lib/action.exec.bot.utest.js create mode 100644 platform/lib/action.exec.conversation.ts create mode 100644 platform/lib/action.exec.conversation.utest.js create mode 100644 platform/lib/action.exec.dataset.ts create mode 100644 platform/lib/action.exec.dataset.utest.js create mode 100644 platform/lib/action.exec.echo.ts create mode 100644 platform/lib/action.exec.echo.utest.js create mode 100644 platform/lib/action.exec.email.tsx create mode 100644 platform/lib/action.exec.email.utest.js create mode 100644 platform/lib/action.exec.fetch.ts create mode 100644 platform/lib/action.exec.fetch.utest.js create mode 100644 platform/lib/action.exec.file.ts create mode 100644 platform/lib/action.exec.file.utest.js create mode 100644 platform/lib/action.exec.form.ts create mode 100644 platform/lib/action.exec.form.utest.js create mode 100644 platform/lib/action.exec.image.ts create mode 100644 platform/lib/action.exec.image.utest.js create mode 100644 platform/lib/action.exec.list.ts create mode 100644 platform/lib/action.exec.list.utest.js create mode 100644 platform/lib/action.exec.listen.ts create mode 100644 platform/lib/action.exec.listen.utest.js create mode 100644 platform/lib/action.exec.mcp.ts create mode 100644 platform/lib/action.exec.mcp.utest.js create mode 100644 platform/lib/action.exec.memory.ts create mode 100644 platform/lib/action.exec.memory.utest.js create mode 100644 platform/lib/action.exec.pack.ts create mode 100644 platform/lib/action.exec.pack.utest.js create mode 100644 platform/lib/action.exec.rating.ts create mode 100644 platform/lib/action.exec.rating.utest.js create mode 100644 platform/lib/action.exec.search.ts create mode 100644 platform/lib/action.exec.search.utest.js create mode 100644 platform/lib/action.exec.shell.ts create mode 100644 platform/lib/action.exec.shell.utest.js create mode 100644 platform/lib/action.exec.skillset.ts create mode 100644 platform/lib/action.exec.skillset.utest.js create mode 100644 platform/lib/action.exec.space.ts create mode 100644 platform/lib/action.exec.space.utest.js create mode 100644 platform/lib/action.exec.task.ts create mode 100644 platform/lib/action.exec.task.utest.js create mode 100644 platform/lib/action.exec.text.ts create mode 100644 platform/lib/action.exec.text.utest.js create mode 100644 platform/lib/action.exec.time.ts create mode 100644 platform/lib/action.exec.time.utest.js create mode 100644 platform/lib/action.exec.todo.ts create mode 100644 platform/lib/action.exec.todo.utest.js create mode 100644 platform/lib/action.exec.view.ts create mode 100644 platform/lib/action.exec.view.utest.js create mode 100644 platform/lib/action.filter.ts create mode 100644 platform/lib/action.filter.utest.js create mode 100644 platform/lib/action.label.ts create mode 100644 platform/lib/action.label.utest.js create mode 100644 platform/lib/action.name.ts create mode 100644 platform/lib/action.name.utest.js create mode 100644 platform/lib/action.parse.ts create mode 100644 platform/lib/action.parse.utest.js create mode 100644 platform/lib/action.tags.concat.utest.ts create mode 100644 platform/lib/action.tags.ts create mode 100644 platform/lib/action.tags.utest.js create mode 100644 platform/lib/activity.js create mode 100644 platform/lib/activity.utest.js create mode 100644 platform/lib/admin.ts create mode 100644 platform/lib/admin.utest.js create mode 100644 platform/lib/analytics.ts create mode 100644 platform/lib/analytics.utest.js create mode 100644 platform/lib/anam.access.ts create mode 100644 platform/lib/anam.access.utest.js create mode 100644 platform/lib/anam.session.ts create mode 100644 platform/lib/anam.session.utest.js create mode 100644 platform/lib/app.action.ts create mode 100644 platform/lib/app.action.utest.js create mode 100644 platform/lib/app.cache.ts create mode 100644 platform/lib/app.cache.utest.js create mode 100644 platform/lib/app.config.bot.ts create mode 100644 platform/lib/app.config.bot.utest.js create mode 100644 platform/lib/app.config.helpers.ts create mode 100644 platform/lib/app.config.helpers.utest.js create mode 100644 platform/lib/app.config.schemas.ts create mode 100644 platform/lib/app.config.schemas.utest.js create mode 100644 platform/lib/app.contact.ts create mode 100644 platform/lib/app.contact.utest.js create mode 100644 platform/lib/app.context.ts create mode 100644 platform/lib/app.context.utest.js create mode 100644 platform/lib/app.helpers.ts create mode 100644 platform/lib/app.helpers.utest.js create mode 100644 platform/lib/app.method.ts create mode 100644 platform/lib/app.method.utest.js create mode 100644 platform/lib/app.route.ts create mode 100644 platform/lib/app.route.utest.js create mode 100644 platform/lib/app.router.app.config.ts create mode 100644 platform/lib/app.router.app.config.utest.js create mode 100644 platform/lib/app.router.app.manifest.ts create mode 100644 platform/lib/app.router.app.manifest.utest.js create mode 100644 platform/lib/app.router.context.ts create mode 100644 platform/lib/app.router.context.utest.js create mode 100644 platform/lib/app.router.pages.config.ts create mode 100644 platform/lib/app.router.pages.config.utest.js create mode 100644 platform/lib/app.session.ts create mode 100644 platform/lib/app.session.utest.js create mode 100644 platform/lib/array.ts create mode 100644 platform/lib/array.utest.js create mode 100644 platform/lib/audience.consts.js create mode 100644 platform/lib/audience.helpers.ts create mode 100644 platform/lib/audience.helpers.utest.js create mode 100644 platform/lib/auth.adapter.ts create mode 100644 platform/lib/auth.adapter.utest.js create mode 100644 platform/lib/auth.callbacks.js create mode 100644 platform/lib/auth.callbacks.utest.js create mode 100644 platform/lib/auth.options.ts create mode 100644 platform/lib/auth.options.utest.js create mode 100644 platform/lib/auth.providers.ts create mode 100644 platform/lib/auth.providers.utest.js create mode 100644 platform/lib/auth.rate.ts create mode 100644 platform/lib/auth.rate.utest.js create mode 100644 platform/lib/auth.signin.ts create mode 100644 platform/lib/auth.signin.utest.js create mode 100644 platform/lib/auxiliary.duckdb.ts create mode 100644 platform/lib/auxiliary.duckdb.utest.js create mode 100644 platform/lib/auxiliary.handler.boundary.utest.js create mode 100644 platform/lib/auxiliary.handler.conformity.utest.js create mode 100644 platform/lib/auxiliary.handler.ts create mode 100644 platform/lib/auxiliary.handler.utest.js create mode 100644 platform/lib/auxiliary.sql.ts create mode 100644 platform/lib/auxiliary.sql.utest.js create mode 100644 platform/lib/avatar.access.ts create mode 100644 platform/lib/avatar.access.utest.js create mode 100644 platform/lib/avatar.session.ts create mode 100644 platform/lib/avatar.session.utest.js create mode 100644 platform/lib/b64.ts create mode 100644 platform/lib/b64.utest.js create mode 100644 platform/lib/backstory.js create mode 100644 platform/lib/backstory.utest.js create mode 100644 platform/lib/banner.js create mode 100644 platform/lib/batch.ts create mode 100644 platform/lib/batch.utest.js create mode 100644 platform/lib/billing.core.ts create mode 100644 platform/lib/billing.handler.ts create mode 100644 platform/lib/billing.handler.utest.js create mode 100644 platform/lib/billing.provider.ts create mode 100644 platform/lib/binary.ts create mode 100644 platform/lib/binary.utest.js create mode 100644 platform/lib/blueprint.access.ts create mode 100644 platform/lib/blueprint.access.utest.js create mode 100644 platform/lib/blueprint.bulletin.ts create mode 100644 platform/lib/blueprint.bulletin.utest.js create mode 100644 platform/lib/blueprint.delete.coverage.utest.js create mode 100644 platform/lib/blueprint.delete.ts create mode 100644 platform/lib/blueprint.delete.utest.js create mode 100644 platform/lib/blueprint.export.ts create mode 100644 platform/lib/blueprint.export.utest.js create mode 100644 platform/lib/blueprint.export.utest.ts create mode 100644 platform/lib/blueprint.fields.ts create mode 100644 platform/lib/blueprint.fields.utest.ts create mode 100644 platform/lib/blueprint.import.ts create mode 100644 platform/lib/blueprint.import.utest.ts create mode 100644 platform/lib/blueprint.resources.ts create mode 100644 platform/lib/blueprint.resources.utest.js create mode 100644 platform/lib/blueprint.terraform.js create mode 100644 platform/lib/blueprint.terraform.utest.js create mode 100644 platform/lib/bool.ts create mode 100644 platform/lib/bool.utest.js create mode 100644 platform/lib/bot.access.ts create mode 100644 platform/lib/bot.access.utest.js create mode 100644 platform/lib/bot.block.ts create mode 100644 platform/lib/bot.block.utest.js create mode 100644 platform/lib/bot.conversation.ts create mode 100644 platform/lib/bot.conversation.utest.js create mode 100644 platform/lib/bot.delete.ts create mode 100644 platform/lib/bot.delete.utest.js create mode 100644 platform/lib/bot.kind.ts create mode 100644 platform/lib/bot.kind.utest.js create mode 100644 platform/lib/browserstorage.ts create mode 100644 platform/lib/browserstorage.utest.js create mode 100644 platform/lib/cache.ts create mode 100644 platform/lib/cache.utest.js create mode 100644 platform/lib/call.ts create mode 100644 platform/lib/call.utest.js create mode 100644 platform/lib/card.response.ts create mode 100644 platform/lib/card.response.utest.js create mode 100644 platform/lib/card.tsx create mode 100644 platform/lib/card.utest.js create mode 100644 platform/lib/cbk.graphql.ts create mode 100644 platform/lib/cbk.graphql.utest.js create mode 100644 platform/lib/cbk.sdk.ts create mode 100644 platform/lib/cbk.sdk.utest.js create mode 100644 platform/lib/cdn.ts create mode 100644 platform/lib/cdn.utest.js create mode 100644 platform/lib/channel.core.ts create mode 100644 platform/lib/channel.core.utest.js create mode 100644 platform/lib/channel.session.ts create mode 100644 platform/lib/channel.session.utest.js create mode 100644 platform/lib/channel.user.ts create mode 100644 platform/lib/channel.user.utest.js create mode 100644 platform/lib/chunk.ts create mode 100644 platform/lib/chunk.utest.js create mode 100644 platform/lib/cloak.ts create mode 100644 platform/lib/cloak.utest.js create mode 100644 platform/lib/code.schema.ts create mode 100644 platform/lib/code.schema.utest.js create mode 100644 platform/lib/code.schema.utest.ts create mode 100644 platform/lib/color.filter.ts create mode 100644 platform/lib/color.filter.utest.js create mode 100644 platform/lib/color.ts create mode 100644 platform/lib/color.utest.js create mode 100644 platform/lib/color2.ts create mode 100644 platform/lib/color2.utest.js create mode 100644 platform/lib/concurrency.ts create mode 100644 platform/lib/concurrency.utest.js create mode 100644 platform/lib/contact.access.ts create mode 100644 platform/lib/contact.access.utest.js create mode 100644 platform/lib/contact.context.ts create mode 100644 platform/lib/contact.context.utest.js create mode 100644 platform/lib/contact.create.ts create mode 100644 platform/lib/contact.create.utest.js create mode 100644 platform/lib/context.setup.ts create mode 100644 platform/lib/context.setup.utest.js create mode 100644 platform/lib/context.store.js create mode 100644 platform/lib/context.store.utest.js create mode 100644 platform/lib/conv.iteration.ts create mode 100644 platform/lib/conv.iteration.utest.js create mode 100644 platform/lib/conv.ts create mode 100644 platform/lib/conv.utest.js create mode 100644 platform/lib/conversation.app.ts create mode 100644 platform/lib/conversation.app.utest.js create mode 100644 platform/lib/conversation.attachment.js create mode 100644 platform/lib/conversation.attachment.utest.js create mode 100644 platform/lib/conversation.compact.ts create mode 100644 platform/lib/conversation.compact.utest.js create mode 100644 platform/lib/conversation.compaction.ts create mode 100644 platform/lib/conversation.compaction.utest.js create mode 100644 platform/lib/conversation.create.js create mode 100644 platform/lib/conversation.create.utest.js create mode 100644 platform/lib/conversation.delete.ts create mode 100644 platform/lib/conversation.delete.utest.js create mode 100644 platform/lib/conversation.engine.js create mode 100644 platform/lib/conversation.engine.stateful.utest.js create mode 100644 platform/lib/conversation.engine.stateless.utest.js create mode 100644 platform/lib/conversation.engine.utest.js create mode 100644 platform/lib/conversation.features.ts create mode 100644 platform/lib/conversation.features.utest.js create mode 100644 platform/lib/conversation.find.ts create mode 100644 platform/lib/conversation.find.utest.js create mode 100644 platform/lib/conversation.group.ts create mode 100644 platform/lib/conversation.group.utest.js create mode 100644 platform/lib/conversation.idle.ts create mode 100644 platform/lib/conversation.idle.utest.js create mode 100644 platform/lib/conversation.monitor.channel.ts create mode 100644 platform/lib/conversation.monitor.channel.utest.js create mode 100644 platform/lib/conversation.monitor.channel.utest.ts create mode 100644 platform/lib/conversation.tag.ts create mode 100644 platform/lib/conversation.tag.utest.js create mode 100644 platform/lib/cookie.ts create mode 100644 platform/lib/cookie.utest.js create mode 100644 platform/lib/credential.digest.ts create mode 100644 platform/lib/credential.digest.utest.js create mode 100644 platform/lib/credential.mask.ts create mode 100644 platform/lib/credential.mask.utest.js create mode 100644 platform/lib/credential.policy.ts create mode 100644 platform/lib/credential.policy.utest.js create mode 100644 platform/lib/creds.basic.parse.ts create mode 100644 platform/lib/creds.basic.parse.utest.js create mode 100644 platform/lib/cron.ts create mode 100644 platform/lib/cron.utest.js create mode 100644 platform/lib/csrf.ts create mode 100644 platform/lib/csrf.utest.js create mode 100644 platform/lib/css.support.ts create mode 100644 platform/lib/css.support.utest.js create mode 100644 platform/lib/css.var.ts create mode 100644 platform/lib/css.var.utest.js create mode 100644 platform/lib/cuid.ts create mode 100644 platform/lib/cuid.utest.js create mode 100644 platform/lib/dataset.access.ts create mode 100644 platform/lib/dataset.access.utest.js create mode 100644 platform/lib/dataset.apply.ts create mode 100644 platform/lib/dataset.apply.utest.js create mode 100644 platform/lib/dataset.delete.ts create mode 100644 platform/lib/dataset.delete.utest.js create mode 100644 platform/lib/dataset.filter.ts create mode 100644 platform/lib/dataset.filter.utest.js create mode 100644 platform/lib/dataset.search.ts create mode 100644 platform/lib/dataset.search.utest.js create mode 100644 platform/lib/dataurl.blob.ts create mode 100644 platform/lib/dataurl.blob.utest.js create mode 100644 platform/lib/dataurl.fetch.ts create mode 100644 platform/lib/dataurl.fetch.utest.js create mode 100644 platform/lib/dataurl.parse.ts create mode 100644 platform/lib/dataurl.parse.utest.js create mode 100644 platform/lib/dataurl.response.ts create mode 100644 platform/lib/dataurl.response.utest.js create mode 100644 platform/lib/db.string.ts create mode 100644 platform/lib/db.string.utest.js create mode 100644 platform/lib/debounce.ts create mode 100644 platform/lib/debounce.utest.js create mode 100644 platform/lib/debug.ts create mode 100644 platform/lib/defer.ts create mode 100644 platform/lib/defer.utest.js create mode 100644 platform/lib/description.parse.ts create mode 100644 platform/lib/description.parse.utest.js create mode 100644 platform/lib/discord.api.ts create mode 100644 platform/lib/discord.api.utest.js create mode 100644 platform/lib/discord.markdown.ts create mode 100644 platform/lib/discord.markdown.utest.js create mode 100644 platform/lib/discord.validation.ts create mode 100644 platform/lib/discord.validation.utest.js create mode 100644 platform/lib/domain.ts create mode 100644 platform/lib/domain.utest.js create mode 100644 platform/lib/dsd.ts create mode 100644 platform/lib/dsd.utest.js create mode 100644 platform/lib/dsd2.ts create mode 100644 platform/lib/dsd2.utest.js create mode 100644 platform/lib/edit.ts create mode 100644 platform/lib/edit.utest.js create mode 100644 platform/lib/egress.call.ts create mode 100644 platform/lib/egress.call.utest.js create mode 100644 platform/lib/egress.core.ts create mode 100644 platform/lib/egress.core.utest.js create mode 100644 platform/lib/egress.fetch.ts create mode 100644 platform/lib/egress.fetch.utest.js create mode 100644 platform/lib/email.message.ts create mode 100644 platform/lib/email.message.utest.js create mode 100644 platform/lib/email.validation.blacklist.utest.js create mode 100644 platform/lib/email.validation.ts create mode 100644 platform/lib/email.validation.utest.js create mode 100644 platform/lib/embed.ts create mode 100644 platform/lib/embed.utest.js create mode 100644 platform/lib/emoji.ts create mode 100644 platform/lib/emoji.utest.js create mode 100644 platform/lib/emoji2.ts create mode 100644 platform/lib/emoji2.utest.js create mode 100644 platform/lib/entity.ts create mode 100644 platform/lib/entity.utest.js create mode 100644 platform/lib/env.ts create mode 100644 platform/lib/error.js create mode 100644 platform/lib/event.ts create mode 100644 platform/lib/example.fetch.ts create mode 100644 platform/lib/example.fetch.utest.js create mode 100644 platform/lib/example.search.ts create mode 100644 platform/lib/example.search.utest.js create mode 100644 platform/lib/experience.ts create mode 100644 platform/lib/experience.utest.js create mode 100644 platform/lib/experiment.ts create mode 100644 platform/lib/experiment.utest.js create mode 100644 platform/lib/extract.contact.ts create mode 100644 platform/lib/extract.contact.utest.js create mode 100644 platform/lib/extract.data.ts create mode 100644 platform/lib/extract.data.utest.js create mode 100644 platform/lib/extract.input.ts create mode 100644 platform/lib/extract.input.utest.js create mode 100644 platform/lib/extract.references.ts create mode 100644 platform/lib/extract.references.utest.js create mode 100644 platform/lib/fetch.js create mode 100644 platform/lib/field.js create mode 100644 platform/lib/field.utest.js create mode 100644 platform/lib/file.access.ts create mode 100644 platform/lib/file.access.utest.js create mode 100644 platform/lib/file.delete.ts create mode 100644 platform/lib/file.delete.utest.js create mode 100644 platform/lib/file.helpers.ts create mode 100644 platform/lib/file.helpers.utest.js create mode 100644 platform/lib/file.icon.ts create mode 100644 platform/lib/file.storage.ts create mode 100644 platform/lib/file.storage.utest.js create mode 100644 platform/lib/file3.utest.js create mode 100644 platform/lib/filter.ts create mode 100644 platform/lib/filter.utest.js create mode 100644 platform/lib/form.ts create mode 100644 platform/lib/form.utest.js create mode 100644 platform/lib/fs.cache.ts create mode 100644 platform/lib/fs.cache.utest.js create mode 100644 platform/lib/github.app.ts create mode 100644 platform/lib/github.app.utest.js create mode 100644 platform/lib/github.signature.ts create mode 100644 platform/lib/github.signature.utest.js create mode 100644 platform/lib/github.validation.ts create mode 100644 platform/lib/github.validation.utest.js create mode 100644 platform/lib/glob.ts create mode 100644 platform/lib/glob.utest.js create mode 100644 platform/lib/googlechat.api.ts create mode 100644 platform/lib/googlechat.api.utest.js create mode 100644 platform/lib/googlechat.auth.ts create mode 100644 platform/lib/googlechat.auth.utest.js create mode 100644 platform/lib/googlechat.event.ts create mode 100644 platform/lib/googlechat.event.utest.js create mode 100644 platform/lib/googlechat.markdown.ts create mode 100644 platform/lib/googlechat.markdown.utest.js create mode 100644 platform/lib/googlechat.validation.ts create mode 100644 platform/lib/googlechat.validation.utest.js create mode 100644 platform/lib/header.assertion.ts create mode 100644 platform/lib/header.assertion.utest.js create mode 100644 platform/lib/header.ts create mode 100644 platform/lib/header.utest.js create mode 100644 platform/lib/helpers.ts create mode 100644 platform/lib/helpers.utest.js create mode 100644 platform/lib/highlighter.ts create mode 100644 platform/lib/highlighter.utest.js create mode 100644 platform/lib/host.ts create mode 100644 platform/lib/host.utest.js create mode 100644 platform/lib/http.ts create mode 100644 platform/lib/http.utest.js create mode 100644 platform/lib/hub.ts create mode 100644 platform/lib/hub.utest.js create mode 100644 platform/lib/icon.theme.ts create mode 100644 platform/lib/icon.theme.utest.js create mode 100644 platform/lib/image.transform.ts create mode 100644 platform/lib/image.transform.utest.js create mode 100644 platform/lib/image.ts create mode 100644 platform/lib/image.utest.js create mode 100644 platform/lib/instagram.markdown.ts create mode 100644 platform/lib/instagram.markdown.utest.js create mode 100644 platform/lib/instruction.convert.ts create mode 100644 platform/lib/instruction.convert.utest.js create mode 100644 platform/lib/instruction.extract.automatic.ts create mode 100644 platform/lib/instruction.extract.automatic.utest.js create mode 100644 platform/lib/instruction.extract.complex.ts create mode 100644 platform/lib/instruction.extract.complex.utest.js create mode 100644 platform/lib/instruction.extract.simple.ts create mode 100644 platform/lib/instruction.extract.simple.utest.js create mode 100644 platform/lib/instruction.extract.structured.ts create mode 100644 platform/lib/instruction.extract.structured.utest.js create mode 100644 platform/lib/instruction.extract.template.ts create mode 100644 platform/lib/instruction.extract.template.utest.js create mode 100644 platform/lib/instruction.field.ts create mode 100644 platform/lib/instruction.field.utest.js create mode 100644 platform/lib/instruction.template.parse.ts create mode 100644 platform/lib/instruction.template.parse.utest.js create mode 100644 platform/lib/instruction.template.unpack.ts create mode 100644 platform/lib/instruction.template.unpack.utest.js create mode 100644 platform/lib/instruction.transform.automatic.ts create mode 100644 platform/lib/instruction.transform.automatic.utest.js create mode 100644 platform/lib/instruction.transform.complex.ts create mode 100644 platform/lib/instruction.transform.complex.utest.js create mode 100644 platform/lib/instruction.transform.simple.js create mode 100644 platform/lib/instruction.transform.simple.utest.js create mode 100644 platform/lib/instruction.transform.structured.ts create mode 100644 platform/lib/instruction.transform.structured.utest.js create mode 100644 platform/lib/instruction.transform.template.js create mode 100644 platform/lib/instruction.transform.template.utest.js create mode 100644 platform/lib/instruction.transform.types.ts create mode 100644 platform/lib/instruction.transform.types.utest.js create mode 100644 platform/lib/instruction.type.ts create mode 100644 platform/lib/instruction.type.utest.js create mode 100644 platform/lib/integration.api.error.js create mode 100644 platform/lib/integration.api.error.utest.js create mode 100644 platform/lib/integration.context.ts create mode 100644 platform/lib/integration.context.utest.js create mode 100644 platform/lib/integration.items.js create mode 100644 platform/lib/integration.items.utest.js create mode 100644 platform/lib/integration.session.ts create mode 100644 platform/lib/integration.session.utest.js create mode 100644 platform/lib/integration.verification.ts create mode 100644 platform/lib/integration.verification.utest.ts create mode 100644 platform/lib/intent.ts create mode 100644 platform/lib/intent.utest.js create mode 100644 platform/lib/intl.ts create mode 100644 platform/lib/intl.utest.js create mode 100644 platform/lib/ip.ts create mode 100644 platform/lib/ip.utest.js create mode 100644 platform/lib/it.ts create mode 100644 platform/lib/jmespath.ts create mode 100644 platform/lib/jmespath.utest.js create mode 100644 platform/lib/job.ts create mode 100644 platform/lib/job.utest.js create mode 100644 platform/lib/joi.handler.ts create mode 100644 platform/lib/joi.handler.utest.js create mode 100644 platform/lib/joi.schema.ts create mode 100644 platform/lib/joi.schema.utest.js create mode 100644 platform/lib/json.repair.ts create mode 100644 platform/lib/json.repair.utest.js create mode 100644 platform/lib/json.ts create mode 100644 platform/lib/jsonpath.ts create mode 100644 platform/lib/jsonpath.utest.js create mode 100644 platform/lib/jsonschema.ts create mode 100644 platform/lib/jsonschema.utest.js create mode 100644 platform/lib/jwt.ts create mode 100644 platform/lib/jwt.utest.js create mode 100644 platform/lib/keyvalue.ts create mode 100644 platform/lib/keyvalue.utest.js create mode 100644 platform/lib/latex.ts create mode 100644 platform/lib/latex.utest.js create mode 100644 platform/lib/limit.core.display.utest.js create mode 100644 platform/lib/limit.core.planless.utest.js create mode 100644 platform/lib/limit.core.ts create mode 100644 platform/lib/limit.core.utest.js create mode 100644 platform/lib/limit.estimate.ts create mode 100644 platform/lib/limit.estimate.utest.js create mode 100644 platform/lib/limit.handler.ts create mode 100644 platform/lib/limit.handler.utest.js create mode 100644 platform/lib/limit.platform.ts create mode 100644 platform/lib/limit.platform.utest.js create mode 100644 platform/lib/localhost.ts create mode 100644 platform/lib/localhost.utest.js create mode 100644 platform/lib/log.cleanup.ts create mode 100644 platform/lib/log.cleanup.utest.js create mode 100644 platform/lib/log.ts create mode 100644 platform/lib/log.utest.js create mode 100644 platform/lib/magic.js create mode 100644 platform/lib/markup.ts create mode 100644 platform/lib/markup.utest.js create mode 100644 platform/lib/mcp.direct.ts create mode 100644 platform/lib/mcp.direct.utest.js create mode 100644 platform/lib/mcp.edge.ts create mode 100644 platform/lib/mcp.edge.utest.js create mode 100644 platform/lib/mcp.error.ts create mode 100644 platform/lib/mcp.error.utest.js create mode 100644 platform/lib/mcp.oauth.constants.ts create mode 100644 platform/lib/mcp.oauth.constants.utest.js create mode 100644 platform/lib/mcp.oauth.idp.ts create mode 100644 platform/lib/mcp.oauth.idp.utest.js create mode 100644 platform/lib/mcp.oauth.ts create mode 100644 platform/lib/mcp.oauth.utest.js create mode 100644 platform/lib/mcp.session.ts create mode 100644 platform/lib/mcp.session.utest.js create mode 100644 platform/lib/mcp.types.ts create mode 100644 platform/lib/mcp.types.utest.js create mode 100644 platform/lib/mcp.widget.ts create mode 100644 platform/lib/mcp.widget.utest.js create mode 100644 platform/lib/md.chat.ts create mode 100644 platform/lib/md.convert.ts create mode 100644 platform/lib/md.extract.ts create mode 100644 platform/lib/md.frontmatter.ts create mode 100644 platform/lib/md.split.ts create mode 100644 platform/lib/md.table.ts create mode 100644 platform/lib/memcache.ts create mode 100644 platform/lib/memcache.utest.js create mode 100644 platform/lib/memory.search.ts create mode 100644 platform/lib/memory.search.utest.js create mode 100644 platform/lib/message.ts create mode 100644 platform/lib/message.utest.js create mode 100644 platform/lib/messaging.supersede.ts create mode 100644 platform/lib/messaging.supersede.utest.js create mode 100644 platform/lib/messaging.thinking.ts create mode 100644 platform/lib/messaging.thinking.utest.ts create mode 100644 platform/lib/messenger.config.js create mode 100644 platform/lib/messenger.config.utest.js create mode 100644 platform/lib/messenger.markdown.ts create mode 100644 platform/lib/messenger.markdown.utest.js create mode 100644 platform/lib/meta.signature.ts create mode 100644 platform/lib/meta.signature.utest.js create mode 100644 platform/lib/meta.ts create mode 100644 platform/lib/meta.user.ts create mode 100644 platform/lib/meta.user.utest.js create mode 100644 platform/lib/meta.utest.js create mode 100644 platform/lib/method.ts create mode 100644 platform/lib/method.utest.js create mode 100644 platform/lib/metric.ts create mode 100644 platform/lib/metric.utest.js create mode 100644 platform/lib/microsoftteams.api.ts create mode 100644 platform/lib/microsoftteams.api.utest.js create mode 100644 platform/lib/microsoftteams.auth.ts create mode 100644 platform/lib/microsoftteams.auth.utest.js create mode 100644 platform/lib/microsoftteams.markdown.ts create mode 100644 platform/lib/microsoftteams.markdown.utest.js create mode 100644 platform/lib/microsoftteams.validation.ts create mode 100644 platform/lib/microsoftteams.validation.utest.js create mode 100644 platform/lib/mime.ts create mode 100644 platform/lib/mime.utest.js create mode 100644 platform/lib/mime2.ts create mode 100644 platform/lib/mime2.utest.js create mode 100644 platform/lib/model.access.ts create mode 100644 platform/lib/model.access.utest.js create mode 100644 platform/lib/model.context.ts create mode 100644 platform/lib/model.context.utest.js create mode 100644 platform/lib/model.credentials.ts create mode 100644 platform/lib/model.credentials.utest.js create mode 100644 platform/lib/model.pricing.ts create mode 100644 platform/lib/model.pricing.utest.js create mode 100644 platform/lib/model.provider.bedrock.adaptor.ts create mode 100644 platform/lib/model.provider.bedrock.adaptor.utest.js create mode 100644 platform/lib/model.provider.bedrock.conv.ts create mode 100644 platform/lib/model.provider.bedrock.conv.utest.js create mode 100644 platform/lib/model.provider.bedrock.ts create mode 100644 platform/lib/model.provider.bedrock.utest.js create mode 100644 platform/lib/model.provider.cloudflare.adaptor.ts create mode 100644 platform/lib/model.provider.cloudflare.adaptor.utest.js create mode 100644 platform/lib/model.provider.cloudflare.conv.ts create mode 100644 platform/lib/model.provider.cloudflare.conv.utest.js create mode 100644 platform/lib/model.provider.cloudflare.ts create mode 100644 platform/lib/model.provider.cloudflare.utest.js create mode 100644 platform/lib/model.provider.deepseek.adaptor.ts create mode 100644 platform/lib/model.provider.deepseek.adaptor.utest.js create mode 100644 platform/lib/model.provider.deepseek.conv.ts create mode 100644 platform/lib/model.provider.deepseek.conv.utest.js create mode 100644 platform/lib/model.provider.deepseek.ts create mode 100644 platform/lib/model.provider.deepseek.utest.js create mode 100644 platform/lib/model.provider.groq.adaptor.ts create mode 100644 platform/lib/model.provider.groq.adaptor.utest.js create mode 100644 platform/lib/model.provider.groq.conv.ts create mode 100644 platform/lib/model.provider.groq.conv.utest.js create mode 100644 platform/lib/model.provider.groq.ts create mode 100644 platform/lib/model.provider.groq.utest.js create mode 100644 platform/lib/model.provider.mistral.adaptor.ts create mode 100644 platform/lib/model.provider.mistral.adaptor.utest.js create mode 100644 platform/lib/model.provider.mistral.conv.ts create mode 100644 platform/lib/model.provider.mistral.conv.utest.js create mode 100644 platform/lib/model.provider.mistral.ts create mode 100644 platform/lib/model.provider.mistral.utest.js create mode 100644 platform/lib/model.provider.moonshot.adaptor.ts create mode 100644 platform/lib/model.provider.moonshot.adaptor.utest.js create mode 100644 platform/lib/model.provider.moonshot.conv.ts create mode 100644 platform/lib/model.provider.moonshot.ts create mode 100644 platform/lib/model.provider.moonshot.utest.js create mode 100644 platform/lib/model.provider.openai.adaptor.ts create mode 100644 platform/lib/model.provider.openai.adaptor.utest.js create mode 100644 platform/lib/model.provider.openai.conv.attachment.utest.js create mode 100644 platform/lib/model.provider.openai.conv.retry.utest.js create mode 100644 platform/lib/model.provider.openai.conv.timeout-abort.utest.js create mode 100644 platform/lib/model.provider.openai.conv.ts create mode 100644 platform/lib/model.provider.openai.conv.utest.js create mode 100644 platform/lib/model.provider.openai.image.utest.js create mode 100644 platform/lib/model.provider.openai.stream-retry.utest.js create mode 100644 platform/lib/model.provider.openai.stream.utest.js create mode 100644 platform/lib/model.provider.openai.ts create mode 100644 platform/lib/model.provider.openai.utest.js create mode 100644 platform/lib/model.provider.openrouter.adaptor.ts create mode 100644 platform/lib/model.provider.openrouter.adaptor.utest.js create mode 100644 platform/lib/model.provider.openrouter.conv.ts create mode 100644 platform/lib/model.provider.openrouter.conv.utest.js create mode 100644 platform/lib/model.provider.openrouter.ts create mode 100644 platform/lib/model.provider.openrouter.utest.js create mode 100644 platform/lib/model.provider.perplexity.adaptor.ts create mode 100644 platform/lib/model.provider.perplexity.adaptor.utest.js create mode 100644 platform/lib/model.provider.perplexity.conv.ts create mode 100644 platform/lib/model.provider.perplexity.conv.utest.js create mode 100644 platform/lib/model.provider.perplexity.ts create mode 100644 platform/lib/model.provider.perplexity.utest.js create mode 100644 platform/lib/model.provider.qwen.adaptor.ts create mode 100644 platform/lib/model.provider.qwen.adaptor.utest.js create mode 100644 platform/lib/model.provider.qwen.conv.ts create mode 100644 platform/lib/model.provider.qwen.ts create mode 100644 platform/lib/model.provider.qwen.utest.js create mode 100644 platform/lib/model.provider.vercel.adaptor.ts create mode 100644 platform/lib/model.provider.vercel.adaptor.utest.js create mode 100644 platform/lib/model.provider.vercel.conv.ts create mode 100644 platform/lib/model.provider.vercel.conv.utest.js create mode 100644 platform/lib/model.provider.vercel.image.utest.js create mode 100644 platform/lib/model.provider.vercel.rerank.utest.js create mode 100644 platform/lib/model.provider.vercel.ts create mode 100644 platform/lib/model.provider.vercel.utest.js create mode 100644 platform/lib/model.provider.vercel.video.utest.js create mode 100644 platform/lib/model.provider.vertex.adaptor.ts create mode 100644 platform/lib/model.provider.vertex.adaptor.utest.js create mode 100644 platform/lib/model.provider.vertex.conv.ts create mode 100644 platform/lib/model.provider.vertex.conv.utest.js create mode 100644 platform/lib/model.provider.vertex.ts create mode 100644 platform/lib/model.provider.vertex.utest.js create mode 100644 platform/lib/model.provider.zai.adaptor.ts create mode 100644 platform/lib/model.provider.zai.adaptor.utest.js create mode 100644 platform/lib/model.provider.zai.conv.ts create mode 100644 platform/lib/model.provider.zai.ts create mode 100644 platform/lib/model.provider.zai.utest.js create mode 100644 platform/lib/model.retry.ts create mode 100644 platform/lib/model.retry.utest.js create mode 100644 platform/lib/model.types.ts create mode 100644 platform/lib/model.utils.js create mode 100644 platform/lib/model.utils.utest.js create mode 100644 platform/lib/moderation.ts create mode 100644 platform/lib/moderation.utest.js create mode 100644 platform/lib/name.icon.ts create mode 100644 platform/lib/name.icon.utest.js create mode 100644 platform/lib/namespace.attachment.js create mode 100644 platform/lib/namespace.attachment.utest.js create mode 100644 platform/lib/namespace.safe.ts create mode 100644 platform/lib/namespace.safe.utest.js create mode 100644 platform/lib/nextjs.config.rewrites.js create mode 100644 platform/lib/nextjs.config.rewrites.utest.js create mode 100644 platform/lib/notify.tsx create mode 100644 platform/lib/notify.utest.js create mode 100644 platform/lib/number.ts create mode 100644 platform/lib/number.utest.js create mode 100644 platform/lib/oauth.authorization.ts create mode 100644 platform/lib/oauth.authorization.utest.js create mode 100644 platform/lib/oauth.connection.idp.ts create mode 100644 platform/lib/oauth.connection.idp.utest.js create mode 100644 platform/lib/oauth.discovery.ts create mode 100644 platform/lib/oauth.discovery.utest.js create mode 100644 platform/lib/oauth.jwt.ts create mode 100644 platform/lib/oauth.jwt.utest.js create mode 100644 platform/lib/oauth.pkce.ts create mode 100644 platform/lib/oauth.pkce.utest.js create mode 100644 platform/lib/oauth.registration.ts create mode 100644 platform/lib/oauth.registration.utest.js create mode 100644 platform/lib/oauth.revoke.ts create mode 100644 platform/lib/oauth.revoke.utest.js create mode 100644 platform/lib/oauth.server.js create mode 100644 platform/lib/oauth.server.utest.js create mode 100644 platform/lib/oauth.token.js create mode 100644 platform/lib/oauth.token.utest.js create mode 100644 platform/lib/object.js create mode 100644 platform/lib/object.utest.js create mode 100644 platform/lib/og.ts create mode 100644 platform/lib/og.utest.js create mode 100644 platform/lib/partner.auth.ts create mode 100644 platform/lib/partner.auth.utest.js create mode 100644 platform/lib/partner.helpers.ts create mode 100644 platform/lib/partner.helpers.utest.js create mode 100644 platform/lib/path.ts create mode 100644 platform/lib/pii.ts create mode 100644 platform/lib/plan.compare.ts create mode 100644 platform/lib/plan.compare.utest.js create mode 100644 platform/lib/plan.label.ts create mode 100644 platform/lib/plan.label.utest.js create mode 100644 platform/lib/policy.config.ts create mode 100644 platform/lib/policy.config.utest.js create mode 100644 platform/lib/policy.text.ts create mode 100644 platform/lib/policy.text.utest.js create mode 100644 platform/lib/policy.text.utest.ts create mode 100644 platform/lib/portal.auth.ts create mode 100644 platform/lib/portal.auth.utest.js create mode 100644 platform/lib/portal.config.ts create mode 100644 platform/lib/portal.config.utest.js create mode 100644 platform/lib/portal.hostname.ts create mode 100644 platform/lib/portal.hostname.utest.js create mode 100644 platform/lib/portal.slug.ts create mode 100644 platform/lib/portal.slug.utest.js create mode 100644 platform/lib/promise.ts create mode 100644 platform/lib/promise.utest.js create mode 100644 platform/lib/prompt.ts create mode 100644 platform/lib/prompt.utest.js create mode 100644 platform/lib/query.get.ts create mode 100644 platform/lib/query.get.utest.js create mode 100644 platform/lib/query.helpers.ts create mode 100644 platform/lib/query.helpers.utest.js create mode 100644 platform/lib/query.ts create mode 100644 platform/lib/query.utest.js create mode 100644 platform/lib/queue.ts create mode 100644 platform/lib/queue.utest.js create mode 100644 platform/lib/queue2.ts create mode 100644 platform/lib/queue2.utest.js create mode 100644 platform/lib/random.ts create mode 100644 platform/lib/random.utest.js create mode 100644 platform/lib/rate.ts create mode 100644 platform/lib/rate.utest.js create mode 100644 platform/lib/ratelimit.ts create mode 100644 platform/lib/ratelimit.utest.js create mode 100644 platform/lib/rating.ts create mode 100644 platform/lib/rating.utest.js create mode 100644 platform/lib/react.ts create mode 100644 platform/lib/react.utest.js create mode 100644 platform/lib/react.utest.jsx create mode 100644 platform/lib/realtime.client.ts create mode 100644 platform/lib/realtime.client.utest.js create mode 100644 platform/lib/realtime.client.utest.ts create mode 100644 platform/lib/realtime.session.ts create mode 100644 platform/lib/realtime.session.utest.js create mode 100644 platform/lib/recall.bot.ts create mode 100644 platform/lib/recall.bot.utest.js create mode 100644 platform/lib/recall.constants.ts create mode 100644 platform/lib/recall.constants.utest.js create mode 100644 platform/lib/recall.schemas.ts create mode 100644 platform/lib/recall.schemas.utest.js create mode 100644 platform/lib/recall.screenshare.ts create mode 100644 platform/lib/recall.screenshare.utest.js create mode 100644 platform/lib/recall.session.ts create mode 100644 platform/lib/recall.session.utest.js create mode 100644 platform/lib/recall.signature.ts create mode 100644 platform/lib/recall.signature.utest.js create mode 100644 platform/lib/record.ts create mode 100644 platform/lib/record.utest.js create mode 100644 platform/lib/redact.entropy.ts create mode 100644 platform/lib/redact.entropy.utest.js create mode 100644 platform/lib/redact.secrets.ts create mode 100644 platform/lib/redact.secrets.utest.js create mode 100644 platform/lib/regex.ts create mode 100644 platform/lib/rehype.plugins.ts create mode 100644 platform/lib/rehype.plugins.utest.js create mode 100644 platform/lib/report.ts create mode 100644 platform/lib/report.utest.js create mode 100644 platform/lib/request.ts create mode 100644 platform/lib/request.utest.js create mode 100644 platform/lib/rerank.ts create mode 100644 platform/lib/rerank.utest.js create mode 100644 platform/lib/response.js create mode 100644 platform/lib/response.utest.js create mode 100644 platform/lib/result.ts create mode 100644 platform/lib/result.utest.js create mode 100644 platform/lib/rss.ts create mode 100644 platform/lib/rss.utest.js create mode 100644 platform/lib/runas.ts create mode 100644 platform/lib/runas.utest.js create mode 100644 platform/lib/sandbox.shell.ts create mode 100644 platform/lib/sandbox.shell.utest.js create mode 100644 platform/lib/satori.ts create mode 100644 platform/lib/satori.utest.js create mode 100644 platform/lib/save.ts create mode 100644 platform/lib/save.utest.js create mode 100644 platform/lib/schedule.ts create mode 100644 platform/lib/schedule.utest.js create mode 100644 platform/lib/scope.client.ts create mode 100644 platform/lib/scope.client.utest.js create mode 100644 platform/lib/scope.server.ts create mode 100644 platform/lib/scope.server.utest.js create mode 100644 platform/lib/script.ts create mode 100644 platform/lib/scroll.manager.ts create mode 100644 platform/lib/scroll.manager.utest.js create mode 100644 platform/lib/secret.access.ts create mode 100644 platform/lib/secret.access.utest.js create mode 100644 platform/lib/secret.authenticate.ts create mode 100644 platform/lib/secret.authenticate.utest.ts create mode 100644 platform/lib/secret.authorize.ts create mode 100644 platform/lib/secret.authorize.utest.js create mode 100644 platform/lib/secret.constants.ts create mode 100644 platform/lib/secret.constants.utest.js create mode 100644 platform/lib/secret.extract.ts create mode 100644 platform/lib/secret.extract.utest.js create mode 100644 platform/lib/secret.icon.ts create mode 100644 platform/lib/secret.icon.utest.js create mode 100644 platform/lib/secret.manager.ts create mode 100644 platform/lib/secret.manager.utest.js create mode 100644 platform/lib/secret.mint.ts create mode 100644 platform/lib/secret.mint.utest.js create mode 100644 platform/lib/secret.name.ts create mode 100644 platform/lib/secret.name.utest.js create mode 100644 platform/lib/secret.oauth.ts create mode 100644 platform/lib/secret.oauth.utest.js create mode 100644 platform/lib/secret.proxy.ts create mode 100644 platform/lib/secret.proxy.utest.js create mode 100644 platform/lib/secret.reference.ts create mode 100644 platform/lib/secret.reference.utest.js create mode 100644 platform/lib/secret.template.ts create mode 100644 platform/lib/secret.template.utest.js create mode 100644 platform/lib/secret.value.ts create mode 100644 platform/lib/secret.value.utest.js create mode 100644 platform/lib/security.headers.js create mode 100644 platform/lib/security.headers.utest.js create mode 100644 platform/lib/selector.ts create mode 100644 platform/lib/selector.utest.js create mode 100644 platform/lib/session.context.ts create mode 100644 platform/lib/session.context.utest.js create mode 100644 platform/lib/session.duration.ts create mode 100644 platform/lib/session.duration.utest.js create mode 100644 platform/lib/session.file.ts create mode 100644 platform/lib/session.file.utest.js create mode 100644 platform/lib/session.get.js create mode 100644 platform/lib/session.get.utest.js create mode 100644 platform/lib/session.handler.ts create mode 100644 platform/lib/session.handler.utest.js create mode 100644 platform/lib/session.temp.ts create mode 100644 platform/lib/session.temp.utest.js create mode 100644 platform/lib/short.ts create mode 100644 platform/lib/short.utest.js create mode 100644 platform/lib/signature.url.ts create mode 100644 platform/lib/signature.url.utest.js create mode 100644 platform/lib/skillserver.ts create mode 100644 platform/lib/skillserver.utest.js create mode 100644 platform/lib/skillset.abilities.ts create mode 100644 platform/lib/skillset.abilities.utest.js create mode 100644 platform/lib/skillset.access.ts create mode 100644 platform/lib/skillset.access.utest.js create mode 100644 platform/lib/skillset.apply.js create mode 100644 platform/lib/skillset.apply.utest.js create mode 100644 platform/lib/skillset.chunk.ts create mode 100644 platform/lib/skillset.chunk.utest.js create mode 100644 platform/lib/skillset.delete.ts create mode 100644 platform/lib/skillset.delete.utest.js create mode 100644 platform/lib/slack.channel.ts create mode 100644 platform/lib/slack.channel.utest.js create mode 100644 platform/lib/slack.manifest.ts create mode 100644 platform/lib/slack.manifest.utest.js create mode 100644 platform/lib/slack.markdown.ts create mode 100644 platform/lib/slack.markdown.utest.js create mode 100644 platform/lib/slack.references.ts create mode 100644 platform/lib/slack.references.utest.js create mode 100644 platform/lib/slack.signature.ts create mode 100644 platform/lib/slack.signature.utest.js create mode 100644 platform/lib/slack.types.ts create mode 100644 platform/lib/slack.types.utest.js create mode 100644 platform/lib/slack.user.ts create mode 100644 platform/lib/slack.user.utest.js create mode 100644 platform/lib/slack.validation.ts create mode 100644 platform/lib/slack.validation.utest.js create mode 100644 platform/lib/slug.js create mode 100644 platform/lib/slug.utest.js create mode 100644 platform/lib/solution.js create mode 100644 platform/lib/solution.utest.js create mode 100644 platform/lib/sort.ts create mode 100644 platform/lib/sort.utest.js create mode 100644 platform/lib/source.ts create mode 100644 platform/lib/source.utest.js create mode 100644 platform/lib/space.access.ts create mode 100644 platform/lib/space.access.utest.js create mode 100644 platform/lib/space.delete.ts create mode 100644 platform/lib/space.delete.utest.js create mode 100644 platform/lib/space.site.serve.ts create mode 100644 platform/lib/space.site.serve.utest.ts create mode 100644 platform/lib/space.site.ts create mode 100644 platform/lib/space.site.utest.js create mode 100644 platform/lib/space.storage.async.utest.js create mode 100644 platform/lib/space.storage.ts create mode 100644 platform/lib/space.storage.utest.js create mode 100644 platform/lib/stale.ts create mode 100644 platform/lib/stale.utest.js create mode 100644 platform/lib/static.ts create mode 100644 platform/lib/static.utest.js create mode 100644 platform/lib/storage.ts create mode 100644 platform/lib/store.filter.ts create mode 100644 platform/lib/store.filter.utest.js create mode 100644 platform/lib/store.memory.ts create mode 100644 platform/lib/store.memory.utest.js create mode 100644 platform/lib/store.types.ts create mode 100644 platform/lib/store.types.utest.js create mode 100644 platform/lib/store.vector.ts create mode 100644 platform/lib/store.vector.utest.js create mode 100644 platform/lib/stream.ts create mode 100644 platform/lib/stream.utest.js create mode 100644 platform/lib/string.ts create mode 100644 platform/lib/struct.ts create mode 100644 platform/lib/structstr.ts create mode 100644 platform/lib/structstr.utest.js create mode 100644 platform/lib/swagger.ts create mode 100644 platform/lib/swagger.utest.js create mode 100644 platform/lib/system.metrics.behavior.utest.js create mode 100644 platform/lib/system.metrics.ts create mode 100644 platform/lib/system.metrics.utest.js create mode 100644 platform/lib/task.access.ts create mode 100644 platform/lib/task.access.utest.js create mode 100644 platform/lib/task.schedule.ts create mode 100644 platform/lib/task.schedule.utest.js create mode 100644 platform/lib/task.validation.ts create mode 100644 platform/lib/task.validation.utest.js create mode 100644 platform/lib/task.workflow.channel.ts create mode 100644 platform/lib/task.workflow.channel.utest.js create mode 100644 platform/lib/team.get.ts create mode 100644 platform/lib/team.get.utest.js create mode 100644 platform/lib/telegram.config.js create mode 100644 platform/lib/telegram.config.utest.js create mode 100644 platform/lib/telegram.markdown.ts create mode 100644 platform/lib/telegram.markdown.utest.js create mode 100644 platform/lib/telegram.signature.ts create mode 100644 platform/lib/telegram.signature.utest.js create mode 100644 platform/lib/telegram.validation.ts create mode 100644 platform/lib/telegram.validation.utest.js create mode 100644 platform/lib/telegram.webhook.ts create mode 100644 platform/lib/telegram.webhook.utest.js create mode 100644 platform/lib/temp.file.ts create mode 100644 platform/lib/temp.file.utest.js create mode 100644 platform/lib/template.ts create mode 100644 platform/lib/template.utest.js create mode 100644 platform/lib/text.search.ts create mode 100644 platform/lib/text.search.utest.js create mode 100644 platform/lib/theme.ts create mode 100644 platform/lib/theme.utest.js create mode 100644 platform/lib/thread.ts create mode 100644 platform/lib/thread.utest.js create mode 100644 platform/lib/timeout.monitor.ts create mode 100644 platform/lib/timeout.monitor.utest.js create mode 100644 platform/lib/toast.ts create mode 100644 platform/lib/toast.utest.js create mode 100644 platform/lib/token.ts create mode 100644 platform/lib/token.utest.js create mode 100644 platform/lib/tool.environment.ts create mode 100644 platform/lib/tool.environment.utest.js create mode 100644 platform/lib/transform.ts create mode 100644 platform/lib/transform.utest.js create mode 100644 platform/lib/twilio.api.ts create mode 100644 platform/lib/twilio.api.utest.js create mode 100644 platform/lib/twilio.markdown.ts create mode 100644 platform/lib/twilio.markdown.utest.js create mode 100644 platform/lib/twilio.phone.ts create mode 100644 platform/lib/twilio.phone.utest.js create mode 100644 platform/lib/twilio.phone.utest.ts create mode 100644 platform/lib/twilio.signature.ts create mode 100644 platform/lib/twilio.signature.utest.js create mode 100644 platform/lib/twilio.twiml.ts create mode 100644 platform/lib/twilio.twiml.utest.js create mode 100644 platform/lib/twilio.validation.ts create mode 100644 platform/lib/twilio.validation.utest.js create mode 100644 platform/lib/twilio.voice.ts create mode 100644 platform/lib/twilio.voice.utest.js create mode 100644 platform/lib/twilio.webhook.ts create mode 100644 platform/lib/twilio.webhook.utest.js create mode 100644 platform/lib/ua.ts create mode 100644 platform/lib/ua.utest.js create mode 100644 platform/lib/unfurl.fetch.ts create mode 100644 platform/lib/unfurl.fetch.utest.js create mode 100644 platform/lib/unfurl.url.ts create mode 100644 platform/lib/unfurl.url.utest.js create mode 100644 platform/lib/upload.ts create mode 100644 platform/lib/upload.utest.js create mode 100644 platform/lib/url.js create mode 100644 platform/lib/url.utest.js create mode 100644 platform/lib/url3.ts create mode 100644 platform/lib/url3.utest.js create mode 100644 platform/lib/usage.cleanup.ts create mode 100644 platform/lib/usage.cleanup.utest.js create mode 100644 platform/lib/usage.get.ts create mode 100644 platform/lib/usage.get.utest.js create mode 100644 platform/lib/usage.model.ts create mode 100644 platform/lib/usage.model.utest.js create mode 100644 platform/lib/usage.period.ts create mode 100644 platform/lib/usage.period.utest.js create mode 100644 platform/lib/usage.policy.ts create mode 100644 platform/lib/usage.policy.utest.js create mode 100644 platform/lib/usage.record.ts create mode 100644 platform/lib/usage.record.utest.js create mode 100644 platform/lib/usage.types.ts create mode 100644 platform/lib/usage.types.utest.js create mode 100644 platform/lib/user.delete.ts create mode 100644 platform/lib/user.delete.utest.js create mode 100644 platform/lib/user.find.js create mode 100644 platform/lib/user.find.utest.js create mode 100644 platform/lib/user.get.ts create mode 100644 platform/lib/user.get.utest.js create mode 100644 platform/lib/user.handler.ts create mode 100644 platform/lib/user.handler.utest.js create mode 100644 platform/lib/user.identity.ts create mode 100644 platform/lib/user.identity.utest.js create mode 100644 platform/lib/user.limits.ts create mode 100644 platform/lib/user.limits.utest.js create mode 100644 platform/lib/user.plan.ts create mode 100644 platform/lib/user.plan.utest.js create mode 100644 platform/lib/user.relation.ts create mode 100644 platform/lib/user.relation.utest.js create mode 100644 platform/lib/user.session.ts create mode 100644 platform/lib/user.session.utest.js create mode 100644 platform/lib/user.type.ts create mode 100644 platform/lib/user.type.utest.js create mode 100644 platform/lib/util.ts create mode 100644 platform/lib/util.utest.js create mode 100644 platform/lib/uuid.ts create mode 100644 platform/lib/uuid.utest.js create mode 100644 platform/lib/video.ts create mode 100644 platform/lib/video.utest.js create mode 100644 platform/lib/webcrypto.ts create mode 100644 platform/lib/webcrypto.utest.js create mode 100644 platform/lib/webshot.ts create mode 100644 platform/lib/webshot.utest.js create mode 100644 platform/lib/whatsapp.config.js create mode 100644 platform/lib/whatsapp.config.utest.js create mode 100644 platform/lib/whatsapp.markdown.ts create mode 100644 platform/lib/whatsapp.markdown.utest.js create mode 100644 platform/lib/whatsapp.signature.ts create mode 100644 platform/lib/whatsapp.signature.utest.js create mode 100644 platform/lib/whatsapp.validation.ts create mode 100644 platform/lib/whatsapp.validation.utest.js create mode 100644 platform/lib/widget.ts create mode 100644 platform/lib/widget.utest.js create mode 100644 platform/lib/workflow.ts create mode 100644 platform/lib/workflow.utest.js create mode 100644 platform/lib/yaml.repair.ts create mode 100644 platform/lib/yaml.repair.utest.js create mode 100644 platform/lib/yaml.ts create mode 100644 platform/lib/youtube.ts create mode 100644 platform/lib/youtube.utest.js create mode 100644 platform/lib/zlib.ts create mode 100644 platform/lib/zlib.utest.js create mode 100644 platform/lib/zod.error.ts create mode 100644 platform/lib/zod.error.utest.js create mode 100644 platform/lib/zod.error.utest.ts create mode 100644 platform/lib/zod.handler.ts create mode 100644 platform/lib/zod.handler.utest.js create mode 100644 platform/lib/zod.jsonschema.ts create mode 100644 platform/lib/zod.jsonschema.utest.js create mode 100644 platform/lib/zod.schema.ts create mode 100644 platform/lib/zod.schema.utest.js create mode 100644 platform/lib/zod.schema.utest.ts create mode 100644 platform/lib/zod.test.ts create mode 100644 platform/lib/zod.test.utest.js create mode 100644 platform/lib/zod.types.ts create mode 100644 platform/lib/zod.types.utest.js create mode 100644 platform/lib/zod.types.utest.ts create mode 100644 platform/next-sitemap.config.js create mode 100644 platform/next.config.d/actions.config.js create mode 100644 platform/next.config.d/actions.config.utest.js create mode 100644 platform/next.config.d/api.config.js create mode 100644 platform/next.config.d/api.config.utest.js create mode 100644 platform/next.config.d/apps.config.js create mode 100644 platform/next.config.d/base.config.js create mode 100644 platform/next.config.d/bundling.config.js create mode 100644 platform/next.config.d/content.config.js create mode 100644 platform/next.config.d/content.config.utest.js create mode 100644 platform/next.config.d/cq.config.js create mode 100644 platform/next.config.d/discovery.config.js create mode 100644 platform/next.config.d/embeds.config.js create mode 100644 platform/next.config.d/env.config.js create mode 100644 platform/next.config.d/host.config.js create mode 100644 platform/next.config.d/i18n.config.js create mode 100644 platform/next.config.d/images.config.js create mode 100644 platform/next.config.d/llms.config.js create mode 100644 platform/next.config.d/modularize.config.js create mode 100644 platform/next.config.d/oauth.config.js create mode 100644 platform/next.config.d/output.config.js create mode 100644 platform/next.config.d/partner.config.js create mode 100644 platform/next.config.d/portals.config.js create mode 100644 platform/next.config.d/profiling.config.js create mode 100644 platform/next.config.d/security.config.js create mode 100644 platform/next.config.d/security.config.utest.js create mode 100644 platform/next.config.d/seo.config.js create mode 100644 platform/next.config.d/short.config.js create mode 100644 platform/next.config.d/spaces.config.js create mode 100644 platform/next.config.d/static.config.js create mode 100644 platform/next.config.d/static.config.utest.js create mode 100644 platform/next.config.d/transpile.config.js create mode 100644 platform/next.config.d/zone.config.js create mode 100644 platform/next.config.d/zone.config.utest.js create mode 100644 platform/next.config.js create mode 100644 platform/package.json create mode 100644 platform/pages/404.jsx create mode 100644 platform/pages/500.jsx create mode 100644 platform/pages/_app.jsx create mode 100644 platform/pages/_document.jsx create mode 100644 platform/pages/_error.jsx create mode 100644 platform/pages/abilities/[abilityId]/index.jsx create mode 100644 platform/pages/abilities/index.jsx create mode 100644 platform/pages/admin/index.jsx create mode 100644 platform/pages/admin/system/index.jsx create mode 100644 platform/pages/admin/users/[userId]/_index.utest.js create mode 100644 platform/pages/admin/users/[userId]/index.jsx create mode 100644 platform/pages/admin/users/index.jsx create mode 100644 platform/pages/alerts/index.jsx create mode 100644 platform/pages/analytics/index.jsx create mode 100644 platform/pages/api/.well-known/_api-catalog.utest.js create mode 100644 platform/pages/api/.well-known/_microsoft-identity-association.utest.js create mode 100644 platform/pages/api/.well-known/api-catalog.ts create mode 100644 platform/pages/api/.well-known/microsoft-identity-association.json.ts create mode 100644 platform/pages/api/404.js create mode 100644 platform/pages/api/admin/debug/header.ts create mode 100644 platform/pages/api/admin/debug/throw.ts create mode 100644 platform/pages/api/admin/user/[userId]/_delete.utest.js create mode 100644 platform/pages/api/admin/user/[userId]/_switch.utest.js create mode 100644 platform/pages/api/admin/user/[userId]/delete.js create mode 100644 platform/pages/api/admin/user/[userId]/switch.js create mode 100644 platform/pages/api/admin/user/[userId]/usage/adjust.js create mode 100644 platform/pages/api/auth/[...nextauth].js create mode 100644 platform/pages/api/auth/_nextauth.utest.js create mode 100644 platform/pages/api/auxiliary/README.md create mode 100644 platform/pages/api/auxiliary/dataset/_chunk.utest.js create mode 100644 platform/pages/api/auxiliary/dataset/chunk.js create mode 100644 platform/pages/api/auxiliary/playground/ability.ts create mode 100644 platform/pages/api/auxiliary/secret/oauth/pipedream/_authorize.utest.js create mode 100644 platform/pages/api/auxiliary/secret/oauth/pipedream/_revoke.utest.js create mode 100644 platform/pages/api/auxiliary/secret/oauth/pipedream/_token.utest.js create mode 100644 platform/pages/api/auxiliary/secret/oauth/pipedream/_validate.utest.js create mode 100644 platform/pages/api/auxiliary/secret/oauth/pipedream/authorize.ts create mode 100644 platform/pages/api/auxiliary/secret/oauth/pipedream/revoke.ts create mode 100644 platform/pages/api/auxiliary/secret/oauth/pipedream/token.ts create mode 100644 platform/pages/api/auxiliary/secret/oauth/pipedream/validate.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/apollo/handler.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/atlassian/proxy.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/attio/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/attio/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/_search.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/create.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/file/_attach.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/file/attach.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/record/_create.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/record/_delete.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/record/_update.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/record/create.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/record/delete.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/record/update.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/dataset/search.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/file/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/file/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_anam.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_avatar.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_discord.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_email.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_recall.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_slack.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_telegram.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_twilio.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/_whatsapp.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/anam.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/avatar.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/discord.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/email.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/github.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/googlechat.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/instagram.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/messenger.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/microsoftteams.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/recall.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/slack.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/telegram.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/twilio.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/integration/whatsapp.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/math/_evaluate.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/math/evaluate.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/mcp/tool/_call.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/mcp/tool/_install.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/mcp/tool/call.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/mcp/tool/install.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/url/_git.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/url/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/url/_unfurl.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/url/git.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/url/screenshot.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/url/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/chatbotkit/url/unfurl.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/clawhub/handler.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/docusign/esignature/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/financialmodelingprep/proxy.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/github/tools.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/gohighlevel/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/gohighlevel/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/_ads.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/_calendar.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/_drive.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/_mail.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/_sheets.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/ads.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/calendar.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/docs.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/drive.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/mail.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/meet.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/google/sheets.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/hubspot/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/hubspot/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/instantly/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/instantly/proxy.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/instantly/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/matillion/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/matillion/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/_search.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/database/item/create.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/database/item/delete.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/database/item/fetch.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/database/item/list.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/database/item/update.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/database/list.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/database/property/introspect.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/database/property/search.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/page/fetch.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/page/list.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/notion/search.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/nubela/proxy.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/openstreetmap/overpass/amenity/around/_list.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/openstreetmap/overpass/amenity/around/list.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/postgrest/rpc.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/postgrest/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/reddit/_post.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/reddit/post.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/serpapi/proxy.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/skillssh/handler.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/space/_skill.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/space/skill.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/supabase/rpc.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/supabase/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/twitter/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/twitter/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/usefind/proxy.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/xero/accounting/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/xero/accounting/sql.ts create mode 100644 platform/pages/api/auxiliary/skillset/ability/zendesk/_sql.utest.js create mode 100644 platform/pages/api/auxiliary/skillset/ability/zendesk/sql.ts create mode 100644 platform/pages/api/billing/_checkout.utest.js create mode 100644 platform/pages/api/billing/_event.utest.js create mode 100644 platform/pages/api/billing/checkout.js create mode 100644 platform/pages/api/billing/event.js create mode 100644 platform/pages/api/billing/session.js create mode 100644 platform/pages/api/billing/skip.js create mode 100644 platform/pages/api/index.js create mode 100644 platform/pages/api/me/team/[teamId]/_switch.utest.js create mode 100644 platform/pages/api/me/team/[teamId]/switch.ts create mode 100644 platform/pages/api/me/team/list.ts create mode 100644 platform/pages/api/me/team/unswitch.ts create mode 100644 platform/pages/api/me/user/[userId]/_switch.utest.js create mode 100644 platform/pages/api/me/user/[userId]/switch.ts create mode 100644 platform/pages/api/me/user/list.ts create mode 100644 platform/pages/api/me/user/unswitch.ts create mode 100644 platform/pages/api/oauth/_authorize.utest.js create mode 100644 platform/pages/api/oauth/_revoke.utest.js create mode 100644 platform/pages/api/oauth/_token.utest.js create mode 100644 platform/pages/api/oauth/authorize.js create mode 100644 platform/pages/api/oauth/connection/_callback.utest.js create mode 100644 platform/pages/api/oauth/connection/callback.ts create mode 100644 platform/pages/api/oauth/refresh.js create mode 100644 platform/pages/api/oauth/revoke.js create mode 100644 platform/pages/api/oauth/secret/_callback.utest.js create mode 100644 platform/pages/api/oauth/secret/callback.js create mode 100644 platform/pages/api/oauth/token.js create mode 100644 platform/pages/api/session/_queue.utest.js create mode 100644 platform/pages/api/session/queue.js create mode 100644 platform/pages/api/short/[shortId]/_redirect.utest.js create mode 100644 platform/pages/api/short/[shortId]/redirect.js create mode 100644 platform/pages/api/system/clock/_queue.utest.js create mode 100644 platform/pages/api/system/clock/queue.js create mode 100644 platform/pages/api/system/queue/callback/_post.utest.js create mode 100644 platform/pages/api/system/queue/callback/failure/_post.utest.js create mode 100644 platform/pages/api/system/queue/callback/failure/post.js create mode 100644 platform/pages/api/system/queue/callback/post.js create mode 100644 platform/pages/api/system/status/fetch.js create mode 100644 platform/pages/api/system/status/ping.js create mode 100644 platform/pages/api/user/[userId]/_queue.utest.js create mode 100644 platform/pages/api/user/[userId]/queue.js create mode 100644 platform/pages/api/v1/_index.utest.js create mode 100644 platform/pages/api/v1/_spec.utest.js create mode 100644 platform/pages/api/v1/ability/[abilityId]/_delete.utest.js create mode 100644 platform/pages/api/v1/ability/[abilityId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/ability/[abilityId]/_update.utest.js create mode 100644 platform/pages/api/v1/ability/[abilityId]/delete.js create mode 100644 platform/pages/api/v1/ability/[abilityId]/fetch.js create mode 100644 platform/pages/api/v1/ability/[abilityId]/update.js create mode 100644 platform/pages/api/v1/ability/_create.utest.js create mode 100644 platform/pages/api/v1/ability/_export.utest.js create mode 100644 platform/pages/api/v1/ability/_list.utest.js create mode 100644 platform/pages/api/v1/ability/create.js create mode 100644 platform/pages/api/v1/ability/export.js create mode 100644 platform/pages/api/v1/ability/list.js create mode 100644 platform/pages/api/v1/audit/log/_list.utest.js create mode 100644 platform/pages/api/v1/audit/log/_queue.utest.js create mode 100644 platform/pages/api/v1/audit/log/export.js create mode 100644 platform/pages/api/v1/audit/log/list.js create mode 100644 platform/pages/api/v1/audit/log/queue.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/_clone.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/_delete.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/_update.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/bulletin/_create.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/bulletin/_list.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/bulletin/create.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/bulletin/list.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/clone.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/delete.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/fetch.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/resource/_export.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/resource/_import.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/resource/_list.utest.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/resource/export.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/resource/import.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/resource/list.js create mode 100644 platform/pages/api/v1/blueprint/[blueprintId]/update.js create mode 100644 platform/pages/api/v1/blueprint/_create.utest.js create mode 100644 platform/pages/api/v1/blueprint/_list.utest.js create mode 100644 platform/pages/api/v1/blueprint/create.js create mode 100644 platform/pages/api/v1/blueprint/list.js create mode 100644 platform/pages/api/v1/bot/[botId]/_clone.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/_delete.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/_downvote.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/_update.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/_upvote.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/access/_fetch.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/access/_unblock.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/access/fetch.js create mode 100644 platform/pages/api/v1/bot/[botId]/access/unblock.js create mode 100644 platform/pages/api/v1/bot/[botId]/clone.js create mode 100644 platform/pages/api/v1/bot/[botId]/delete.js create mode 100644 platform/pages/api/v1/bot/[botId]/downvote.js create mode 100644 platform/pages/api/v1/bot/[botId]/fetch.js create mode 100644 platform/pages/api/v1/bot/[botId]/memory/_search.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/memory/search.js create mode 100644 platform/pages/api/v1/bot/[botId]/realtime/websocket/_create.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/realtime/websocket/create.js create mode 100644 platform/pages/api/v1/bot/[botId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/session/create.js create mode 100644 platform/pages/api/v1/bot/[botId]/update.js create mode 100644 platform/pages/api/v1/bot/[botId]/upvote.js create mode 100644 platform/pages/api/v1/bot/[botId]/usage/_fetch.utest.js create mode 100644 platform/pages/api/v1/bot/[botId]/usage/fetch.js create mode 100644 platform/pages/api/v1/bot/_create.utest.js create mode 100644 platform/pages/api/v1/bot/_list.utest.js create mode 100644 platform/pages/api/v1/bot/create.js create mode 100644 platform/pages/api/v1/bot/list.js create mode 100644 platform/pages/api/v1/channel/[channelId]/_publish.utest.js create mode 100644 platform/pages/api/v1/channel/[channelId]/_subscribe.utest.js create mode 100644 platform/pages/api/v1/channel/[channelId]/publish.js create mode 100644 platform/pages/api/v1/channel/[channelId]/subscribe.js create mode 100644 platform/pages/api/v1/contact/[contactId]/_delete.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/_update.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/conversation/_list.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/conversation/list.js create mode 100644 platform/pages/api/v1/contact/[contactId]/delete.js create mode 100644 platform/pages/api/v1/contact/[contactId]/fetch.js create mode 100644 platform/pages/api/v1/contact/[contactId]/memory/_list.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/memory/_search.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/memory/list.js create mode 100644 platform/pages/api/v1/contact/[contactId]/memory/search.js create mode 100644 platform/pages/api/v1/contact/[contactId]/rating/list.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/_authenticate.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/_mint.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/_proxy.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/_revoke.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/_verify.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/authenticate.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/mint.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/proxy.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/revoke.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/[secretId]/verify.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/_list.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/secret/list.js create mode 100644 platform/pages/api/v1/contact/[contactId]/space/list.js create mode 100644 platform/pages/api/v1/contact/[contactId]/task/_list.utest.js create mode 100644 platform/pages/api/v1/contact/[contactId]/task/list.js create mode 100644 platform/pages/api/v1/contact/[contactId]/update.js create mode 100644 platform/pages/api/v1/contact/_create.utest.js create mode 100644 platform/pages/api/v1/contact/_ensure.utest.js create mode 100644 platform/pages/api/v1/contact/_list.utest.js create mode 100644 platform/pages/api/v1/contact/create.js create mode 100644 platform/pages/api/v1/contact/ensure.js create mode 100644 platform/pages/api/v1/contact/export.js create mode 100644 platform/pages/api/v1/contact/list.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_apply.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_callback.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_compact.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_complete.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_dispatch.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_downvote.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_receive.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_send.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_update.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/_upvote.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/apply.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/attachment/[attachmentId]/_download.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/attachment/[attachmentId]/download.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/attachment/_list.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/attachment/_upload.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/attachment/list.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/attachment/upload.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/callback.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/channel/subscribe.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/compact.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/complete.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/contact/_upsert.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/contact/upsert.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/delete.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/dispatch.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/downvote.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/fetch.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/initiate.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/_delete.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/_downvote.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/_synthesize.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/_update.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/_upvote.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/delete.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/downvote.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/fetch.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/synthesize.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/update.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/[messageId]/upvote.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/_create.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/_list.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/batch/_create.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/batch/create.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/create.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/message/list.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/queue.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/realtime/websocket/_create.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/realtime/websocket/create.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/receive.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/send.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/session/create.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/update.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/upvote.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/usage/_fetch.utest.js create mode 100644 platform/pages/api/v1/conversation/[conversationId]/usage/fetch.js create mode 100644 platform/pages/api/v1/conversation/_apply.utest.js create mode 100644 platform/pages/api/v1/conversation/_compact.utest.js create mode 100644 platform/pages/api/v1/conversation/_complete.utest.js create mode 100644 platform/pages/api/v1/conversation/_create.utest.js create mode 100644 platform/pages/api/v1/conversation/_dispatch.utest.js create mode 100644 platform/pages/api/v1/conversation/_export.utest.js create mode 100644 platform/pages/api/v1/conversation/_list.utest.js create mode 100644 platform/pages/api/v1/conversation/_queue.utest.js create mode 100644 platform/pages/api/v1/conversation/apply.js create mode 100644 platform/pages/api/v1/conversation/compact.js create mode 100644 platform/pages/api/v1/conversation/complete.js create mode 100644 platform/pages/api/v1/conversation/create.js create mode 100644 platform/pages/api/v1/conversation/dispatch.js create mode 100644 platform/pages/api/v1/conversation/export.js create mode 100644 platform/pages/api/v1/conversation/list.js create mode 100644 platform/pages/api/v1/conversation/queue.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/_delete.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/_import.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/_queue.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/_search.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/_update.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/delete.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/fetch.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/file/[fileId]/_attach.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/file/[fileId]/_detach.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/file/[fileId]/_sync.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/file/[fileId]/attach.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/file/[fileId]/detach.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/file/[fileId]/sync.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/file/_list.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/file/list.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/import.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/ingest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/queue.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/[recordId]/_delete.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/[recordId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/[recordId]/_update.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/[recordId]/delete.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/[recordId]/fetch.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/[recordId]/update.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/_create.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/_export.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/_list.utest.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/create.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/export.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/record/list.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/search.js create mode 100644 platform/pages/api/v1/dataset/[datasetId]/update.js create mode 100644 platform/pages/api/v1/dataset/_create.utest.js create mode 100644 platform/pages/api/v1/dataset/_list.utest.js create mode 100644 platform/pages/api/v1/dataset/_queue.utest.js create mode 100644 platform/pages/api/v1/dataset/create.js create mode 100644 platform/pages/api/v1/dataset/list.js create mode 100644 platform/pages/api/v1/dataset/queue.js create mode 100644 platform/pages/api/v1/entity/text/_redact.utest.js create mode 100644 platform/pages/api/v1/entity/text/redact.js create mode 100644 platform/pages/api/v1/event/log/_export.utest.js create mode 100644 platform/pages/api/v1/event/log/_list.utest.js create mode 100644 platform/pages/api/v1/event/log/_queue.utest.js create mode 100644 platform/pages/api/v1/event/log/_subscribe.utest.js create mode 100644 platform/pages/api/v1/event/log/export.js create mode 100644 platform/pages/api/v1/event/log/list.js create mode 100644 platform/pages/api/v1/event/log/queue.js create mode 100644 platform/pages/api/v1/event/log/subscribe.js create mode 100644 platform/pages/api/v1/event/metric/_export.utest.js create mode 100644 platform/pages/api/v1/event/metric/_list.utest.js create mode 100644 platform/pages/api/v1/event/metric/_queue.utest.js create mode 100644 platform/pages/api/v1/event/metric/export.js create mode 100644 platform/pages/api/v1/event/metric/list.js create mode 100644 platform/pages/api/v1/event/metric/queue.js create mode 100644 platform/pages/api/v1/event/metric/series/_fetch.utest.js create mode 100644 platform/pages/api/v1/event/metric/series/fetch.js create mode 100644 platform/pages/api/v1/file/[fileId]/_delete.utest.js create mode 100644 platform/pages/api/v1/file/[fileId]/_download.utest.js create mode 100644 platform/pages/api/v1/file/[fileId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/file/[fileId]/_sync.utest.js create mode 100644 platform/pages/api/v1/file/[fileId]/_update.utest.js create mode 100644 platform/pages/api/v1/file/[fileId]/_upload.utest.js create mode 100644 platform/pages/api/v1/file/[fileId]/delete.js create mode 100644 platform/pages/api/v1/file/[fileId]/download.js create mode 100644 platform/pages/api/v1/file/[fileId]/fetch.js create mode 100644 platform/pages/api/v1/file/[fileId]/portrait/_download.utest.js create mode 100644 platform/pages/api/v1/file/[fileId]/portrait/download.js create mode 100644 platform/pages/api/v1/file/[fileId]/sync.js create mode 100644 platform/pages/api/v1/file/[fileId]/thumbnail/_download.utest.js create mode 100644 platform/pages/api/v1/file/[fileId]/thumbnail/download.js create mode 100644 platform/pages/api/v1/file/[fileId]/update.js create mode 100644 platform/pages/api/v1/file/[fileId]/upload.js create mode 100644 platform/pages/api/v1/file/_create.utest.js create mode 100644 platform/pages/api/v1/file/_list.utest.js create mode 100644 platform/pages/api/v1/file/create.js create mode 100644 platform/pages/api/v1/file/list.js create mode 100644 platform/pages/api/v1/graphql.ts create mode 100644 platform/pages/api/v1/hub/blueprint/[blueprintId]/_publish.utest.js create mode 100644 platform/pages/api/v1/hub/blueprint/[blueprintId]/_unpublish.utest.js create mode 100644 platform/pages/api/v1/hub/blueprint/[blueprintId]/publish.js create mode 100644 platform/pages/api/v1/hub/blueprint/[blueprintId]/unpublish.js create mode 100644 platform/pages/api/v1/hub/bot/[botId]/_publish.utest.js create mode 100644 platform/pages/api/v1/hub/bot/[botId]/_unpublish.utest.js create mode 100644 platform/pages/api/v1/hub/bot/[botId]/publish.js create mode 100644 platform/pages/api/v1/hub/bot/[botId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/hub/bot/[botId]/session/create.js create mode 100644 platform/pages/api/v1/hub/bot/[botId]/unpublish.js create mode 100644 platform/pages/api/v1/hub/dataset/[datasetId]/_publish.utest.js create mode 100644 platform/pages/api/v1/hub/dataset/[datasetId]/_unpublish.utest.js create mode 100644 platform/pages/api/v1/hub/dataset/[datasetId]/publish.js create mode 100644 platform/pages/api/v1/hub/dataset/[datasetId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/hub/dataset/[datasetId]/session/create.js create mode 100644 platform/pages/api/v1/hub/dataset/[datasetId]/unpublish.js create mode 100644 platform/pages/api/v1/hub/skillset/[skillsetId]/_publish.utest.js create mode 100644 platform/pages/api/v1/hub/skillset/[skillsetId]/_unpublish.utest.js create mode 100644 platform/pages/api/v1/hub/skillset/[skillsetId]/publish.js create mode 100644 platform/pages/api/v1/hub/skillset/[skillsetId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/hub/skillset/[skillsetId]/session/create.js create mode 100644 platform/pages/api/v1/hub/skillset/[skillsetId]/unpublish.js create mode 100644 platform/pages/api/v1/hub/widget/[widgetId]/_publish.utest.js create mode 100644 platform/pages/api/v1/hub/widget/[widgetId]/_unpublish.utest.js create mode 100644 platform/pages/api/v1/hub/widget/[widgetId]/publish.js create mode 100644 platform/pages/api/v1/hub/widget/[widgetId]/unpublish.js create mode 100644 platform/pages/api/v1/image/[imageId]/_download.utest.js create mode 100644 platform/pages/api/v1/image/[imageId]/download.js create mode 100644 platform/pages/api/v1/image/_create.utest.js create mode 100644 platform/pages/api/v1/image/_edit.utest.js create mode 100644 platform/pages/api/v1/image/create.js create mode 100644 platform/pages/api/v1/image/edit.js create mode 100644 platform/pages/api/v1/index.js create mode 100644 platform/pages/api/v1/integration/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/anam/[anamIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/anam/[anamIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/anam/[anamIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/anam/[anamIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/anam/[anamIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/anam/[anamIntegrationId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/integration/anam/[anamIntegrationId]/session/create.js create mode 100644 platform/pages/api/v1/integration/anam/[anamIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/anam/_create.utest.js create mode 100644 platform/pages/api/v1/integration/anam/_list.utest.js create mode 100644 platform/pages/api/v1/integration/anam/create.js create mode 100644 platform/pages/api/v1/integration/anam/list.js create mode 100644 platform/pages/api/v1/integration/avatar/[avatarIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/avatar/[avatarIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/avatar/[avatarIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/avatar/[avatarIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/avatar/[avatarIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/avatar/[avatarIntegrationId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/integration/avatar/[avatarIntegrationId]/session/create.js create mode 100644 platform/pages/api/v1/integration/avatar/[avatarIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/avatar/_create.utest.js create mode 100644 platform/pages/api/v1/integration/avatar/_list.utest.js create mode 100644 platform/pages/api/v1/integration/avatar/create.js create mode 100644 platform/pages/api/v1/integration/avatar/list.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/_interact.utest.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/interact.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/discord/[discordIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/discord/_create.utest.js create mode 100644 platform/pages/api/v1/integration/discord/_list.utest.js create mode 100644 platform/pages/api/v1/integration/discord/create.js create mode 100644 platform/pages/api/v1/integration/discord/list.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/email/[emailIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/email/_create.utest.js create mode 100644 platform/pages/api/v1/integration/email/_parse.utest.js create mode 100644 platform/pages/api/v1/integration/email/create.js create mode 100644 platform/pages/api/v1/integration/email/list.js create mode 100644 platform/pages/api/v1/integration/email/parse.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/_trigger.utest.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/item/_export.utest.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/item/_list.utest.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/item/export.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/item/list.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/trigger.js create mode 100644 platform/pages/api/v1/integration/extract/[extractIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/extract/_create.utest.js create mode 100644 platform/pages/api/v1/integration/extract/_list.utest.js create mode 100644 platform/pages/api/v1/integration/extract/create.js create mode 100644 platform/pages/api/v1/integration/extract/list.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/_event.utest.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/event.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/github/[githubIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/github/_create.utest.js create mode 100644 platform/pages/api/v1/integration/github/_list.utest.js create mode 100644 platform/pages/api/v1/integration/github/create.js create mode 100644 platform/pages/api/v1/integration/github/list.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/_event.utest.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/event.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/googlechat/[googlechatIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/googlechat/create.js create mode 100644 platform/pages/api/v1/integration/googlechat/list.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/_callback.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/callback.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/instagram/[instagramIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/instagram/_create.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/_list.utest.js create mode 100644 platform/pages/api/v1/integration/instagram/create.js create mode 100644 platform/pages/api/v1/integration/instagram/list.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/_mcp.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/mcp.ts create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/_authorize.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/_register.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/_revoke.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/_token.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/authorize.ts create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/register.ts create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/revoke.ts create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/token.ts create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/well-known/_authorization-server.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/well-known/_protected-resource.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/well-known/authorization-server.ts create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/oauth/well-known/protected-resource.ts create mode 100644 platform/pages/api/v1/integration/mcpserver/[mcpserverIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/mcpserver/_create.utest.js create mode 100644 platform/pages/api/v1/integration/mcpserver/create.js create mode 100644 platform/pages/api/v1/integration/mcpserver/list.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/_callback.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/callback.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/messenger/[messengerIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/messenger/_create.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/_list.utest.js create mode 100644 platform/pages/api/v1/integration/messenger/create.js create mode 100644 platform/pages/api/v1/integration/messenger/list.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/_callback.utest.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/callback.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/[microsoftteamsIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/create.js create mode 100644 platform/pages/api/v1/integration/microsoftteams/list.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/_sync.utest.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/sync.js create mode 100644 platform/pages/api/v1/integration/notion/[notionIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/notion/_create.utest.js create mode 100644 platform/pages/api/v1/integration/notion/_list.utest.js create mode 100644 platform/pages/api/v1/integration/notion/create.js create mode 100644 platform/pages/api/v1/integration/notion/list.js create mode 100644 platform/pages/api/v1/integration/queue.ts create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/_webhook.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/_leave.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/leave.ts create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/message/_send.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/message/send.ts create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/recording/_pause.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/recording/_resume.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/recording/pause.ts create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/recording/resume.ts create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/screenshare/_start.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/screenshare/_stop.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/screenshare/start.ts create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/[sessionId]/screenshare/stop.ts create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/session/create.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/recall/[recallIntegrationId]/webhook.js create mode 100644 platform/pages/api/v1/integration/recall/_create.utest.js create mode 100644 platform/pages/api/v1/integration/recall/_list.utest.js create mode 100644 platform/pages/api/v1/integration/recall/create.js create mode 100644 platform/pages/api/v1/integration/recall/list.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/_sync.utest.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/sync.js create mode 100644 platform/pages/api/v1/integration/sitemap/[sitemapIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/sitemap/_create.utest.js create mode 100644 platform/pages/api/v1/integration/sitemap/create.js create mode 100644 platform/pages/api/v1/integration/sitemap/list.js create mode 100644 platform/pages/api/v1/integration/skillserver/[skillserverIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/skillserver/[skillserverIntegrationId]/_invoke.utest.js create mode 100644 platform/pages/api/v1/integration/skillserver/[skillserverIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/skillserver/[skillserverIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/skillserver/[skillserverIntegrationId]/invoke.js create mode 100644 platform/pages/api/v1/integration/skillserver/[skillserverIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/skillserver/_create.utest.js create mode 100644 platform/pages/api/v1/integration/skillserver/create.js create mode 100644 platform/pages/api/v1/integration/skillserver/list.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/_command.utest.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/_event.utest.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/_interaction.utest.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/command.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/event.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/interaction.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/slack/[slackIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/slack/_create.utest.js create mode 100644 platform/pages/api/v1/integration/slack/_list.utest.js create mode 100644 platform/pages/api/v1/integration/slack/create.js create mode 100644 platform/pages/api/v1/integration/slack/list.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/_trigger.utest.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/trigger.js create mode 100644 platform/pages/api/v1/integration/support/[supportIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/support/_create.utest.js create mode 100644 platform/pages/api/v1/integration/support/_list.utest.js create mode 100644 platform/pages/api/v1/integration/support/create.js create mode 100644 platform/pages/api/v1/integration/support/list.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/_webhook.utest.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/telegram/[telegramIntegrationId]/webhook.js create mode 100644 platform/pages/api/v1/integration/telegram/_list.utest.js create mode 100644 platform/pages/api/v1/integration/telegram/create.js create mode 100644 platform/pages/api/v1/integration/telegram/list.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/_event.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/_invoke.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/event.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/invoke.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/trigger/[triggerIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/trigger/_create.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/_list.utest.js create mode 100644 platform/pages/api/v1/integration/trigger/create.js create mode 100644 platform/pages/api/v1/integration/trigger/list.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/_webhook.utest.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/twilio/[twilioIntegrationId]/webhook.js create mode 100644 platform/pages/api/v1/integration/twilio/create.js create mode 100644 platform/pages/api/v1/integration/twilio/list.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/_callback.utest.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/_initiate.utest.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/_queue.utest.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/callback.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/initiate.ts create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/queue.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/whatsapp/[whatsappIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/whatsapp/_create.utest.js create mode 100644 platform/pages/api/v1/integration/whatsapp/create.js create mode 100644 platform/pages/api/v1/integration/whatsapp/list.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/_clone.utest.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/_delete.utest.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/_setup.utest.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/_update.utest.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/clone.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/delete.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/fetch.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/file/[fileId]/_attach.utest.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/file/[fileId]/_detach.utest.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/file/[fileId]/attach.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/file/[fileId]/detach.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/session/create.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/setup.js create mode 100644 platform/pages/api/v1/integration/widget/[widgetIntegrationId]/update.js create mode 100644 platform/pages/api/v1/integration/widget/_create.utest.js create mode 100644 platform/pages/api/v1/integration/widget/_list.utest.js create mode 100644 platform/pages/api/v1/integration/widget/create.js create mode 100644 platform/pages/api/v1/integration/widget/list.js create mode 100644 platform/pages/api/v1/magic/[promptId]/_generate.utest.js create mode 100644 platform/pages/api/v1/magic/[promptId]/generate.js create mode 100644 platform/pages/api/v1/magic/_list.utest.js create mode 100644 platform/pages/api/v1/magic/list.js create mode 100644 platform/pages/api/v1/me/_fetch.utest.js create mode 100644 platform/pages/api/v1/me/_update.utest.js create mode 100644 platform/pages/api/v1/me/fetch.js create mode 100644 platform/pages/api/v1/me/update.js create mode 100644 platform/pages/api/v1/memory/[memoryId]/_delete.utest.js create mode 100644 platform/pages/api/v1/memory/[memoryId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/memory/[memoryId]/_update.utest.js create mode 100644 platform/pages/api/v1/memory/[memoryId]/delete.js create mode 100644 platform/pages/api/v1/memory/[memoryId]/fetch.js create mode 100644 platform/pages/api/v1/memory/[memoryId]/update.js create mode 100644 platform/pages/api/v1/memory/_create.utest.js create mode 100644 platform/pages/api/v1/memory/_export.utest.js create mode 100644 platform/pages/api/v1/memory/_list.utest.js create mode 100644 platform/pages/api/v1/memory/_queue.utest.js create mode 100644 platform/pages/api/v1/memory/_search.utest.js create mode 100644 platform/pages/api/v1/memory/create.js create mode 100644 platform/pages/api/v1/memory/export.js create mode 100644 platform/pages/api/v1/memory/list.js create mode 100644 platform/pages/api/v1/memory/queue.js create mode 100644 platform/pages/api/v1/memory/search.js create mode 100644 platform/pages/api/v1/oauth/application/[oAuthApplicationId]/.gitkeep create mode 100644 platform/pages/api/v1/oauth/application/_queue.utest.js create mode 100644 platform/pages/api/v1/oauth/application/queue.js create mode 100644 platform/pages/api/v1/oauth/connection/[oAuthConnectionId]/_delete.utest.js create mode 100644 platform/pages/api/v1/oauth/connection/[oAuthConnectionId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/oauth/connection/[oAuthConnectionId]/_update.utest.js create mode 100644 platform/pages/api/v1/oauth/connection/[oAuthConnectionId]/delete.js create mode 100644 platform/pages/api/v1/oauth/connection/[oAuthConnectionId]/fetch.js create mode 100644 platform/pages/api/v1/oauth/connection/[oAuthConnectionId]/update.js create mode 100644 platform/pages/api/v1/oauth/connection/_create.utest.js create mode 100644 platform/pages/api/v1/oauth/connection/_list.utest.js create mode 100644 platform/pages/api/v1/oauth/connection/create.js create mode 100644 platform/pages/api/v1/oauth/connection/list.js create mode 100644 platform/pages/api/v1/openai/chat/_completions.utest.js create mode 100644 platform/pages/api/v1/openai/chat/completions.ts create mode 100644 platform/pages/api/v1/platform/ability/_list.utest.js create mode 100644 platform/pages/api/v1/platform/ability/_search.utest.js create mode 100644 platform/pages/api/v1/platform/ability/list.js create mode 100644 platform/pages/api/v1/platform/ability/search.js create mode 100644 platform/pages/api/v1/platform/action/_list.utest.js create mode 100644 platform/pages/api/v1/platform/action/list.js create mode 100644 platform/pages/api/v1/platform/example/[exampleId]/_clone.utest.js create mode 100644 platform/pages/api/v1/platform/example/[exampleId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/platform/example/[exampleId]/clone.js create mode 100644 platform/pages/api/v1/platform/example/[exampleId]/fetch.js create mode 100644 platform/pages/api/v1/platform/example/_list.utest.js create mode 100644 platform/pages/api/v1/platform/example/_search.utest.js create mode 100644 platform/pages/api/v1/platform/example/list.js create mode 100644 platform/pages/api/v1/platform/example/search.js create mode 100644 platform/pages/api/v1/platform/limit/_list.utest.js create mode 100644 platform/pages/api/v1/platform/limit/list.js create mode 100644 platform/pages/api/v1/platform/model/_list.utest.js create mode 100644 platform/pages/api/v1/platform/model/list.js create mode 100644 platform/pages/api/v1/platform/report/[reportId]/_generate.utest.js create mode 100644 platform/pages/api/v1/platform/report/[reportId]/generate.ts create mode 100644 platform/pages/api/v1/platform/report/_generate.utest.js create mode 100644 platform/pages/api/v1/platform/report/_list.utest.js create mode 100644 platform/pages/api/v1/platform/report/generate.ts create mode 100644 platform/pages/api/v1/platform/report/list.ts create mode 100644 platform/pages/api/v1/platform/secret/_list.utest.js create mode 100644 platform/pages/api/v1/platform/secret/_search.utest.js create mode 100644 platform/pages/api/v1/platform/secret/list.js create mode 100644 platform/pages/api/v1/platform/secret/search.js create mode 100644 platform/pages/api/v1/platform/subscription/_list.utest.js create mode 100644 platform/pages/api/v1/platform/subscription/list.js create mode 100644 platform/pages/api/v1/policy/[policyId]/_delete.utest.js create mode 100644 platform/pages/api/v1/policy/[policyId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/policy/[policyId]/_update.utest.js create mode 100644 platform/pages/api/v1/policy/[policyId]/block/_clear.utest.js create mode 100644 platform/pages/api/v1/policy/[policyId]/block/_list.utest.js create mode 100644 platform/pages/api/v1/policy/[policyId]/block/clear.js create mode 100644 platform/pages/api/v1/policy/[policyId]/block/list.js create mode 100644 platform/pages/api/v1/policy/[policyId]/delete.js create mode 100644 platform/pages/api/v1/policy/[policyId]/fetch.js create mode 100644 platform/pages/api/v1/policy/[policyId]/update.js create mode 100644 platform/pages/api/v1/policy/_create.utest.js create mode 100644 platform/pages/api/v1/policy/_list.utest.js create mode 100644 platform/pages/api/v1/policy/create.js create mode 100644 platform/pages/api/v1/policy/list.js create mode 100644 platform/pages/api/v1/portal/[portalId]/_delete.utest.js create mode 100644 platform/pages/api/v1/portal/[portalId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/portal/[portalId]/_update.utest.js create mode 100644 platform/pages/api/v1/portal/[portalId]/delete.js create mode 100644 platform/pages/api/v1/portal/[portalId]/fetch.js create mode 100644 platform/pages/api/v1/portal/[portalId]/update.js create mode 100644 platform/pages/api/v1/portal/_create.utest.js create mode 100644 platform/pages/api/v1/portal/_list.utest.js create mode 100644 platform/pages/api/v1/portal/create.js create mode 100644 platform/pages/api/v1/portal/list.js create mode 100644 platform/pages/api/v1/rating/[ratingId]/_delete.utest.js create mode 100644 platform/pages/api/v1/rating/[ratingId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/rating/[ratingId]/_update.utest.js create mode 100644 platform/pages/api/v1/rating/[ratingId]/delete.js create mode 100644 platform/pages/api/v1/rating/[ratingId]/fetch.js create mode 100644 platform/pages/api/v1/rating/[ratingId]/update.js create mode 100644 platform/pages/api/v1/rating/_create.utest.js create mode 100644 platform/pages/api/v1/rating/_export.utest.js create mode 100644 platform/pages/api/v1/rating/_list.utest.js create mode 100644 platform/pages/api/v1/rating/create.js create mode 100644 platform/pages/api/v1/rating/export.js create mode 100644 platform/pages/api/v1/rating/list.js create mode 100644 platform/pages/api/v1/secret/[secretId]/_authenticate.utest.js create mode 100644 platform/pages/api/v1/secret/[secretId]/_delete.utest.js create mode 100644 platform/pages/api/v1/secret/[secretId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/secret/[secretId]/_mint.utest.js create mode 100644 platform/pages/api/v1/secret/[secretId]/_proxy.utest.js create mode 100644 platform/pages/api/v1/secret/[secretId]/_revoke.utest.js create mode 100644 platform/pages/api/v1/secret/[secretId]/_update.utest.js create mode 100644 platform/pages/api/v1/secret/[secretId]/_verify.utest.js create mode 100644 platform/pages/api/v1/secret/[secretId]/authenticate.js create mode 100644 platform/pages/api/v1/secret/[secretId]/delete.js create mode 100644 platform/pages/api/v1/secret/[secretId]/fetch.js create mode 100644 platform/pages/api/v1/secret/[secretId]/mint.js create mode 100644 platform/pages/api/v1/secret/[secretId]/proxy.js create mode 100644 platform/pages/api/v1/secret/[secretId]/revoke.js create mode 100644 platform/pages/api/v1/secret/[secretId]/update.js create mode 100644 platform/pages/api/v1/secret/[secretId]/verify.js create mode 100644 platform/pages/api/v1/secret/_create.utest.js create mode 100644 platform/pages/api/v1/secret/_list.utest.js create mode 100644 platform/pages/api/v1/secret/create.js create mode 100644 platform/pages/api/v1/secret/list.js create mode 100644 platform/pages/api/v1/session/file/_upload.utest.js create mode 100644 platform/pages/api/v1/session/file/upload.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/_delete.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/_update.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/[abilityId]/_delete.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/[abilityId]/_execute.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/[abilityId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/[abilityId]/_update.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/[abilityId]/delete.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/[abilityId]/execute.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/[abilityId]/fetch.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/[abilityId]/update.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/_create.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/_export.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/_list.utest.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/create.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/export.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/ability/list.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/delete.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/fetch.js create mode 100644 platform/pages/api/v1/skillset/[skillsetId]/update.js create mode 100644 platform/pages/api/v1/skillset/_create.utest.js create mode 100644 platform/pages/api/v1/skillset/_list.utest.js create mode 100644 platform/pages/api/v1/skillset/create.js create mode 100644 platform/pages/api/v1/skillset/list.js create mode 100644 platform/pages/api/v1/space/[spaceId]/_delete.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/_update.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/delete.js create mode 100644 platform/pages/api/v1/space/[spaceId]/fetch.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/[siteId]/_delete.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/[siteId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/[siteId]/_update.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/[siteId]/delete.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/[siteId]/fetch.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/[siteId]/update.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/_create.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/_list.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/create.js create mode 100644 platform/pages/api/v1/space/[spaceId]/site/list.js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/copy/[[...path]].js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/copy/_path.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/delete/[[...path]].js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/delete/_path.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/download/[[...path]].js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/download/_path.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/list/[[...path]].js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/list/_path.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/move/[[...path]].js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/move/_path.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/upload/[[...path]].js create mode 100644 platform/pages/api/v1/space/[spaceId]/storage/upload/_path.utest.js create mode 100644 platform/pages/api/v1/space/[spaceId]/update.js create mode 100644 platform/pages/api/v1/space/_create.utest.js create mode 100644 platform/pages/api/v1/space/_export.utest.js create mode 100644 platform/pages/api/v1/space/_list.utest.js create mode 100644 platform/pages/api/v1/space/create.js create mode 100644 platform/pages/api/v1/space/export.js create mode 100644 platform/pages/api/v1/space/list.js create mode 100644 platform/pages/api/v1/space/system/site/[[...path]].js create mode 100644 platform/pages/api/v1/spec.ts create mode 100644 platform/pages/api/v1/status/_ping.utest.js create mode 100644 platform/pages/api/v1/status/ping.js create mode 100644 platform/pages/api/v1/task/[taskId]/_cancel.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/_delete.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/_subscribe.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/_trigger.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/_update.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/_workflow.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/cancel.js create mode 100644 platform/pages/api/v1/task/[taskId]/delete.js create mode 100644 platform/pages/api/v1/task/[taskId]/execution/[taskExecutionId]/_cancel.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/execution/[taskExecutionId]/cancel.js create mode 100644 platform/pages/api/v1/task/[taskId]/execution/_list.utest.js create mode 100644 platform/pages/api/v1/task/[taskId]/execution/list.js create mode 100644 platform/pages/api/v1/task/[taskId]/fetch.js create mode 100644 platform/pages/api/v1/task/[taskId]/subscribe.ts create mode 100644 platform/pages/api/v1/task/[taskId]/trigger.js create mode 100644 platform/pages/api/v1/task/[taskId]/update.js create mode 100644 platform/pages/api/v1/task/[taskId]/workflow.ts create mode 100644 platform/pages/api/v1/task/_create.utest.js create mode 100644 platform/pages/api/v1/task/_export.utest.js create mode 100644 platform/pages/api/v1/task/_list.utest.js create mode 100644 platform/pages/api/v1/task/_queue.utest.js create mode 100644 platform/pages/api/v1/task/create.js create mode 100644 platform/pages/api/v1/task/export.js create mode 100644 platform/pages/api/v1/task/list.js create mode 100644 platform/pages/api/v1/task/queue.ts create mode 100644 platform/pages/api/v1/team/[teamId]/_delete.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/_update.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/delete.js create mode 100644 platform/pages/api/v1/team/[teamId]/fetch.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/[teamMembershipId]/_delete.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/[teamMembershipId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/[teamMembershipId]/_update.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/[teamMembershipId]/delete.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/[teamMembershipId]/fetch.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/[teamMembershipId]/invite/_resend.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/[teamMembershipId]/invite/resend.ts create mode 100644 platform/pages/api/v1/team/[teamId]/membership/[teamMembershipId]/update.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/_create.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/_list.utest.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/create.js create mode 100644 platform/pages/api/v1/team/[teamId]/membership/list.js create mode 100644 platform/pages/api/v1/team/[teamId]/update.js create mode 100644 platform/pages/api/v1/team/_create.utest.js create mode 100644 platform/pages/api/v1/team/_list.utest.js create mode 100644 platform/pages/api/v1/team/create.js create mode 100644 platform/pages/api/v1/team/list.js create mode 100644 platform/pages/api/v1/text/_complete.utest.js create mode 100644 platform/pages/api/v1/text/_insert.utest.js create mode 100644 platform/pages/api/v1/text/complete.js create mode 100644 platform/pages/api/v1/text/insert.js create mode 100644 platform/pages/api/v1/token/[tokenId]/_delete.utest.js create mode 100644 platform/pages/api/v1/token/[tokenId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/token/[tokenId]/_update.utest.js create mode 100644 platform/pages/api/v1/token/[tokenId]/delete.js create mode 100644 platform/pages/api/v1/token/[tokenId]/fetch.js create mode 100644 platform/pages/api/v1/token/[tokenId]/update.js create mode 100644 platform/pages/api/v1/token/_create.utest.js create mode 100644 platform/pages/api/v1/token/_list.utest.js create mode 100644 platform/pages/api/v1/token/create.js create mode 100644 platform/pages/api/v1/token/list.js create mode 100644 platform/pages/api/v1/url/_fetch.utest.js create mode 100644 platform/pages/api/v1/url/_sign.utest.js create mode 100644 platform/pages/api/v1/url/_unfurl.utest.js create mode 100644 platform/pages/api/v1/url/fetch.js create mode 100644 platform/pages/api/v1/url/sign.js create mode 100644 platform/pages/api/v1/url/unfurl.js create mode 100644 platform/pages/api/v1/usage/_export.utest.js create mode 100644 platform/pages/api/v1/usage/_fetch.utest.js create mode 100644 platform/pages/api/v1/usage/_list.utest.js create mode 100644 platform/pages/api/v1/usage/_queue.utest.js create mode 100644 platform/pages/api/v1/usage/export.js create mode 100644 platform/pages/api/v1/usage/fetch.js create mode 100644 platform/pages/api/v1/usage/list.js create mode 100644 platform/pages/api/v1/usage/queue.js create mode 100644 platform/pages/api/v1/usage/series/_fetch.utest.js create mode 100644 platform/pages/api/v1/usage/series/fetch.js create mode 100644 platform/pages/api/v1/user/[userId]/_delete.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/_update.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/context/[contextId]/_delete.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/context/[contextId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/context/[contextId]/_update.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/context/[contextId]/delete.js create mode 100644 platform/pages/api/v1/user/[userId]/context/[contextId]/fetch.js create mode 100644 platform/pages/api/v1/user/[userId]/context/[contextId]/update.js create mode 100644 platform/pages/api/v1/user/[userId]/context/_create.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/context/_list.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/context/create.js create mode 100644 platform/pages/api/v1/user/[userId]/context/list.js create mode 100644 platform/pages/api/v1/user/[userId]/delete.js create mode 100644 platform/pages/api/v1/user/[userId]/fetch.js create mode 100644 platform/pages/api/v1/user/[userId]/session/_create.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/session/create.js create mode 100644 platform/pages/api/v1/user/[userId]/token/[tokenId]/_delete.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/token/[tokenId]/_update.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/token/[tokenId]/delete.js create mode 100644 platform/pages/api/v1/user/[userId]/token/[tokenId]/update.js create mode 100644 platform/pages/api/v1/user/[userId]/token/_create.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/token/_list.utest.js create mode 100644 platform/pages/api/v1/user/[userId]/token/create.js create mode 100644 platform/pages/api/v1/user/[userId]/token/list.js create mode 100644 platform/pages/api/v1/user/[userId]/update.js create mode 100644 platform/pages/api/v1/user/_create.utest.js create mode 100644 platform/pages/api/v1/user/_list.utest.js create mode 100644 platform/pages/api/v1/user/create.js create mode 100644 platform/pages/api/v1/user/list.js create mode 100644 platform/pages/api/v1/video/[videoId]/_download.utest.js create mode 100644 platform/pages/api/v1/video/[videoId]/download.js create mode 100644 platform/pages/api/v1/video/_create.utest.js create mode 100644 platform/pages/api/v1/video/_edit.utest.js create mode 100644 platform/pages/api/v1/video/create.js create mode 100644 platform/pages/api/v1/video/edit.js create mode 100644 platform/pages/api/v1/webhook/[webhookId]/_delete.utest.js create mode 100644 platform/pages/api/v1/webhook/[webhookId]/_fetch.utest.js create mode 100644 platform/pages/api/v1/webhook/[webhookId]/_queue.utest.js create mode 100644 platform/pages/api/v1/webhook/[webhookId]/_update.utest.js create mode 100644 platform/pages/api/v1/webhook/[webhookId]/delete.js create mode 100644 platform/pages/api/v1/webhook/[webhookId]/fetch.js create mode 100644 platform/pages/api/v1/webhook/[webhookId]/queue.js create mode 100644 platform/pages/api/v1/webhook/[webhookId]/update.js create mode 100644 platform/pages/api/v1/webhook/_create.utest.js create mode 100644 platform/pages/api/v1/webhook/_list.utest.js create mode 100644 platform/pages/api/v1/webhook/create.js create mode 100644 platform/pages/api/v1/webhook/list.js create mode 100644 platform/pages/audit/index.jsx create mode 100644 platform/pages/auto/widget/frame.jsx create mode 100644 platform/pages/billing/checkout.jsx create mode 100644 platform/pages/billing/index.jsx create mode 100644 platform/pages/billing/upgrade.jsx create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.applyBlueprintFromText.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.buildSecretResources.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.computeAutoConnections.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.contextInput.utest.jsx create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.genericConfigurator.utest.jsx create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.getAbilityWarnings.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.getAllowFromWarnings.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.getBlueprintGraph.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.getDatasetWarnings.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.getFilteredDanglingResources.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.getResourceReferenceEntries.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.getSecretTooltipPreface.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.getSecretWarnings.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.group.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.hasBlueprintGraphChanges.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.keyboardShortcuts.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.noteSurfaceClasses.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.resourceIdConnectionStatus.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.secretConfigurator.utest.jsx create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.serializeBlueprintToText.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.summarizeBlueprintLintResults.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.tagTruncation.utest.jsx create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.toolbarFacets.production.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.toolbarFacets.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/_designer.validateResourceData.utest.js create mode 100644 platform/pages/blueprints/[blueprintId]/designer.jsx create mode 100644 platform/pages/blueprints/[blueprintId]/index.jsx create mode 100644 platform/pages/blueprints/[blueprintId]/preview.jsx create mode 100644 platform/pages/blueprints/index.jsx create mode 100644 platform/pages/bots/[botId]/_index.utest.jsx create mode 100644 platform/pages/bots/[botId]/index.jsx create mode 100644 platform/pages/bots/index.jsx create mode 100644 platform/pages/card.jsx create mode 100644 platform/pages/connections/[slug]/card.jsx create mode 100644 platform/pages/connections/[slug]/index.jsx create mode 100644 platform/pages/connections/[slug]/mcp.jsx create mode 100644 platform/pages/connections/index.jsx create mode 100644 platform/pages/connections/sitemap.xml/index.jsx create mode 100644 platform/pages/contacts/[contactId]/index.jsx create mode 100644 platform/pages/contacts/index.jsx create mode 100644 platform/pages/conversations/[conversationId]/index.jsx create mode 100644 platform/pages/conversations/index.jsx create mode 100644 platform/pages/datasets/[datasetId]/_index.utest.jsx create mode 100644 platform/pages/datasets/[datasetId]/index.jsx create mode 100644 platform/pages/datasets/[datasetId]/records/[recordId]/index.jsx create mode 100644 platform/pages/datasets/index.jsx create mode 100644 platform/pages/events/index.jsx create mode 100644 platform/pages/examples/[slug]/card.jsx create mode 100644 platform/pages/examples/[slug]/designer.jsx create mode 100644 platform/pages/examples/[slug]/editor.jsx create mode 100644 platform/pages/examples/[slug]/files.json/index.jsx create mode 100644 platform/pages/examples/[slug]/frame.jsx create mode 100644 platform/pages/examples/[slug]/index.jsx create mode 100644 platform/pages/examples/[slug]/preview.jsx create mode 100644 platform/pages/examples/airline-booking/demo/index.jsx create mode 100644 platform/pages/examples/call-analyzer/demo/index.jsx create mode 100644 platform/pages/examples/client-side-functions/demo/index.jsx create mode 100644 platform/pages/examples/custom-components/demo/index.jsx create mode 100644 platform/pages/examples/email-template-generator/demo/index.jsx create mode 100644 platform/pages/examples/form-autofill/demo/index.jsx create mode 100644 platform/pages/examples/index.jsx create mode 100644 platform/pages/examples/message-sending/demo/index.jsx create mode 100644 platform/pages/examples/mini-game-maker/demo/index.jsx create mode 100644 platform/pages/examples/patch-assistant/demo/index.jsx create mode 100644 platform/pages/examples/physics-playground/demo/index.jsx create mode 100644 platform/pages/examples/rss.xml/index.jsx create mode 100644 platform/pages/examples/sitemap.xml/index.jsx create mode 100644 platform/pages/examples/theme-color-builder/demo/index.jsx create mode 100644 platform/pages/examples/world-map/demo/index.jsx create mode 100644 platform/pages/files/[fileId]/index.jsx create mode 100644 platform/pages/files/index.jsx create mode 100644 platform/pages/hub/blueprints/[blueprintId]/card.jsx create mode 100644 platform/pages/hub/blueprints/[blueprintId]/conversations/[conversationId]/index.jsx create mode 100644 platform/pages/hub/blueprints/[blueprintId]/conversations/rss.xml/index.jsx create mode 100644 platform/pages/hub/blueprints/[blueprintId]/designer.jsx create mode 100644 platform/pages/hub/blueprints/[blueprintId]/index.jsx create mode 100644 platform/pages/hub/blueprints/index.jsx create mode 100644 platform/pages/hub/blueprints/latest/index.jsx create mode 100644 platform/pages/hub/blueprints/latest/rss.xml/index.jsx create mode 100644 platform/pages/hub/blueprints/latest/sitemap.xml/index.jsx create mode 100644 platform/pages/hub/bots/[botId]/card.jsx create mode 100644 platform/pages/hub/bots/[botId]/index.jsx create mode 100644 platform/pages/hub/bots/index.jsx create mode 100644 platform/pages/hub/bots/latest/index.jsx create mode 100644 platform/pages/hub/bots/latest/rss.xml/index.jsx create mode 100644 platform/pages/hub/bots/latest/sitemap.xml/index.jsx create mode 100644 platform/pages/hub/card.jsx create mode 100644 platform/pages/hub/datasets/[datasetId]/card.jsx create mode 100644 platform/pages/hub/datasets/[datasetId]/index.jsx create mode 100644 platform/pages/hub/datasets/index.jsx create mode 100644 platform/pages/hub/datasets/latest/index.jsx create mode 100644 platform/pages/hub/datasets/latest/rss.xml/index.jsx create mode 100644 platform/pages/hub/datasets/latest/sitemap.xml/index.jsx create mode 100644 platform/pages/hub/index.jsx create mode 100644 platform/pages/hub/rss.xml/index.jsx create mode 100644 platform/pages/hub/skillsets/[skillsetId]/card.jsx create mode 100644 platform/pages/hub/skillsets/[skillsetId]/index.jsx create mode 100644 platform/pages/hub/skillsets/index.jsx create mode 100644 platform/pages/hub/skillsets/latest/index.jsx create mode 100644 platform/pages/hub/skillsets/latest/rss.xml/index.jsx create mode 100644 platform/pages/hub/skillsets/latest/sitemap.xml/index.jsx create mode 100644 platform/pages/hub/users/[userId]/card.jsx create mode 100644 platform/pages/hub/users/[userId]/index.jsx create mode 100644 platform/pages/hub/widgets/[widgetId]/card.jsx create mode 100644 platform/pages/hub/widgets/[widgetId]/index.jsx create mode 100644 platform/pages/hub/widgets/index.jsx create mode 100644 platform/pages/hub/widgets/latest/index.jsx create mode 100644 platform/pages/hub/widgets/latest/rss.xml/index.jsx create mode 100644 platform/pages/hub/widgets/latest/sitemap.xml/index.jsx create mode 100644 platform/pages/index.jsx create mode 100644 platform/pages/integrations/_index.utest.js create mode 100644 platform/pages/integrations/anam/[anamIntegrationId]/404.jsx create mode 100644 platform/pages/integrations/anam/[anamIntegrationId]/_frame.contract.utest.js create mode 100644 platform/pages/integrations/anam/[anamIntegrationId]/_frame.utest.js create mode 100644 platform/pages/integrations/anam/[anamIntegrationId]/frame.jsx create mode 100644 platform/pages/integrations/anam/[anamIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/anam/[anamIntegrationId]/test.jsx create mode 100644 platform/pages/integrations/avatar/[avatarIntegrationId]/404.jsx create mode 100644 platform/pages/integrations/avatar/[avatarIntegrationId]/_frame.contract.utest.js create mode 100644 platform/pages/integrations/avatar/[avatarIntegrationId]/_frame.utest.js create mode 100644 platform/pages/integrations/avatar/[avatarIntegrationId]/frame.jsx create mode 100644 platform/pages/integrations/avatar/[avatarIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/avatar/[avatarIntegrationId]/test.jsx create mode 100644 platform/pages/integrations/discord/[discordIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/email/[emailIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/extract/[extractIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/github/[githubIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/googlechat/[googlechatIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/index.jsx create mode 100644 platform/pages/integrations/instagram/[instagramIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/mcpserver/[mcpserverIntegrationId]/frame.jsx create mode 100644 platform/pages/integrations/mcpserver/[mcpserverIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/messenger/[messengerIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/microsoftteams/[microsoftteamsIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/notion/[notionIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/recall/[recallIntegrationId]/_camera.utest.js create mode 100644 platform/pages/integrations/recall/[recallIntegrationId]/camera.tsx create mode 100644 platform/pages/integrations/recall/[recallIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/recall/[recallIntegrationId]/screenshare.tsx create mode 100644 platform/pages/integrations/sitemap/[sitemapIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/skillserver/[skillserverIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/slack/[slackIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/support/[supportIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/telegram/[telegramIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/trigger/[triggerIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/twilio/[twilioIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/whatsapp/[whatsappIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/404.jsx create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.Messages.stories.jsx create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.Micro.stories.jsx create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.ThemeWrapper.utest.js create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.isSafeMessageEvent.utest.js create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.postToParent.utest.js create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.resolvePoweredByColors.utest.js create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.sessionItems.utest.js create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.useSessionChannel.utest.js create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.useSessionCleanup.utest.js create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/_frame.useSessionSaveRestoreSync.utest.js create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/frame.jsx create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/index.jsx create mode 100644 platform/pages/integrations/widget/[widgetIntegrationId]/test.jsx create mode 100644 platform/pages/memories/[memoryId]/index.jsx create mode 100644 platform/pages/memories/index.jsx create mode 100644 platform/pages/new/_example.utest.jsx create mode 100644 platform/pages/new/_examples.utest.jsx create mode 100644 platform/pages/new/_index.utest.js create mode 100644 platform/pages/new/_success.utest.js create mode 100644 platform/pages/new/channel.jsx create mode 100644 platform/pages/new/details.jsx create mode 100644 platform/pages/new/docs.jsx create mode 100644 platform/pages/new/example.jsx create mode 100644 platform/pages/new/examples.jsx create mode 100644 platform/pages/new/files.jsx create mode 100644 platform/pages/new/goal.jsx create mode 100644 platform/pages/new/hub.jsx create mode 100644 platform/pages/new/index.jsx create mode 100644 platform/pages/new/integrations.jsx create mode 100644 platform/pages/new/intent.jsx create mode 100644 platform/pages/new/messaging.jsx create mode 100644 platform/pages/new/success.jsx create mode 100644 platform/pages/new/user.jsx create mode 100644 platform/pages/new/website.jsx create mode 100644 platform/pages/oauth/applications/tokens/index.jsx create mode 100644 platform/pages/oauth/consent.jsx create mode 100644 platform/pages/oauth/mcp/callback.jsx create mode 100644 platform/pages/overview/_index.utest.js create mode 100644 platform/pages/overview/index.jsx create mode 100644 platform/pages/partner/signin/[partnerId]/index.jsx create mode 100644 platform/pages/partner/signin/[partnerId]/verify.jsx create mode 100644 platform/pages/platform/actions/index.jsx create mode 100644 platform/pages/platform/index.jsx create mode 100644 platform/pages/platform/limits/index.jsx create mode 100644 platform/pages/platform/models/[modelId]/card.jsx create mode 100644 platform/pages/platform/models/[modelId]/index.jsx create mode 100644 platform/pages/platform/models/index.jsx create mode 100644 platform/pages/platform/models/sitemap.xml/index.jsx create mode 100644 platform/pages/platform/reports/index.jsx create mode 100644 platform/pages/playground/[slug]/card.jsx create mode 100644 platform/pages/playground/ability/index.jsx create mode 100644 platform/pages/playground/api/index.jsx create mode 100644 platform/pages/playground/backstory/index.jsx create mode 100644 platform/pages/playground/blueprint/embed/preview.jsx create mode 100644 platform/pages/playground/conversation/index.jsx create mode 100644 platform/pages/playground/entity/index.jsx create mode 100644 platform/pages/playground/graphql/card.jsx create mode 100644 platform/pages/playground/graphql/index.jsx create mode 100644 platform/pages/playground/html/index.jsx create mode 100644 platform/pages/playground/image/index.jsx create mode 100644 platform/pages/playground/index.jsx create mode 100644 platform/pages/playground/jmespath/index.jsx create mode 100644 platform/pages/playground/jsonpath/index.jsx create mode 100644 platform/pages/playground/message/index.jsx create mode 100644 platform/pages/playground/pdf/index.jsx create mode 100644 platform/pages/playground/record/index.jsx create mode 100644 platform/pages/playground/situation/index.jsx create mode 100644 platform/pages/playground/video/index.jsx create mode 100644 platform/pages/playground/widget/embed/designer.jsx create mode 100644 platform/pages/playground/widget/embed/preview.jsx create mode 100644 platform/pages/playground/widget/index.jsx create mode 100644 platform/pages/policies/[policyId]/index.jsx create mode 100644 platform/pages/policies/index.jsx create mode 100644 platform/pages/portals/[portalId]/index.jsx create mode 100644 platform/pages/portals/index.jsx create mode 100644 platform/pages/ratings/[ratingId]/index.jsx create mode 100644 platform/pages/ratings/index.jsx create mode 100644 platform/pages/redirect.jsx create mode 100644 platform/pages/secrets/[secretId]/_index.utest.js create mode 100644 platform/pages/secrets/[secretId]/index.jsx create mode 100644 platform/pages/secrets/[secretId]/manager/authenticate.tsx create mode 100644 platform/pages/secrets/[secretId]/manager/oauth/callback.tsx create mode 100644 platform/pages/secrets/index.jsx create mode 100644 platform/pages/secrets/oauth/callback.tsx create mode 100644 platform/pages/signin/index.jsx create mode 100644 platform/pages/signin/verify.jsx create mode 100644 platform/pages/skillsets/[skillsetId]/_index.utest.jsx create mode 100644 platform/pages/skillsets/[skillsetId]/abilities/[abilityId]/index.jsx create mode 100644 platform/pages/skillsets/[skillsetId]/index.jsx create mode 100644 platform/pages/skillsets/index.jsx create mode 100644 platform/pages/spaces/[spaceId]/index.jsx create mode 100644 platform/pages/spaces/index.jsx create mode 100644 platform/pages/support/_index.utest.js create mode 100644 platform/pages/support/index.jsx create mode 100644 platform/pages/tasks/[taskId]/_index.utest.js create mode 100644 platform/pages/tasks/[taskId]/index.jsx create mode 100644 platform/pages/tasks/index.jsx create mode 100644 platform/pages/teams/[teamId]/index.jsx create mode 100644 platform/pages/teams/index.jsx create mode 100644 platform/pages/tokens/[tokenId]/_index.utest.js create mode 100644 platform/pages/tokens/[tokenId]/index.jsx create mode 100644 platform/pages/tokens/index.jsx create mode 100644 platform/pages/usage/index.jsx create mode 100644 platform/pages/users/[userId]/index.jsx create mode 100644 platform/pages/users/index.jsx create mode 100644 platform/pages/webhooks/[webhookId]/index.jsx create mode 100644 platform/pages/webhooks/index.jsx create mode 100644 platform/pages/welcome/_index.utest.js create mode 100644 platform/pages/welcome/index.jsx create mode 100644 platform/pages/widgets/preview/[...url].jsx create mode 100644 platform/pages/widgets/preview/index.jsx create mode 100644 platform/polyfills/async-hooks.js create mode 100644 platform/polyfills/client.js create mode 100644 platform/polyfills/server.js create mode 100644 platform/postcss.config.cjs create mode 100644 platform/prisma/audit.ts create mode 100644 platform/prisma/audit.utest.js create mode 100644 platform/prisma/cache.ts create mode 100644 platform/prisma/cache.utest.js create mode 100644 platform/prisma/client.ts create mode 100644 platform/prisma/constraints.ts create mode 100644 platform/prisma/encryption.ts create mode 100644 platform/prisma/encryption.utest.js create mode 100644 platform/prisma/enums.ts create mode 100644 platform/prisma/json.d.ts create mode 100644 platform/prisma/methods.ts create mode 100644 platform/prisma/methods.utest.js create mode 100644 platform/prisma/methods.utest.ts create mode 100644 platform/prisma/models.ts create mode 100644 platform/prisma/nulls.ts create mode 100644 platform/prisma/retry.ts create mode 100644 platform/prisma/retry.utest.js create mode 100644 platform/prisma/sql.ts create mode 100644 platform/prisma/types.ts create mode 100644 platform/prisma/utils.ts create mode 100644 platform/prisma/zod.ts create mode 100644 platform/prompts/answer_v1.yaml create mode 100644 platform/prompts/audio_generator_v1.yaml create mode 100644 platform/prompts/auth_generator_v1.yaml create mode 100644 platform/prompts/auto_agent_v1.yaml create mode 100644 platform/prompts/batch_v1.yaml create mode 100644 platform/prompts/blueprint_assistant_v1.yaml create mode 100644 platform/prompts/buttons_generator_v1.yaml create mode 100644 platform/prompts/canvas_generator_v1.yaml create mode 100644 platform/prompts/carousel_generator_v1.yaml create mode 100644 platform/prompts/dashboard_assistant_v1.yaml create mode 100644 platform/prompts/detect_intent_v1.yaml create mode 100644 platform/prompts/detect_intent_v2.yaml create mode 100644 platform/prompts/detect_intent_v3.yaml create mode 100644 platform/prompts/detect_intent_v4.yaml create mode 100644 platform/prompts/detect_search_intent_v1.yaml create mode 100644 platform/prompts/detect_search_intent_v2.yaml create mode 100644 platform/prompts/detect_search_intent_v3.yaml create mode 100644 platform/prompts/detect_search_intent_v4.yaml create mode 100644 platform/prompts/diligence_v1.yaml create mode 100644 platform/prompts/extract_contact_details_v1.yaml create mode 100644 platform/prompts/extract_contact_details_v2.yaml create mode 100644 platform/prompts/footnotes_generator_v1.yaml create mode 100644 platform/prompts/form_generator_v1.yaml create mode 100644 platform/prompts/generate_backstory_from_text_v1.yaml create mode 100644 platform/prompts/generate_backstory_from_text_v2.yaml create mode 100644 platform/prompts/generate_backstory_from_text_v3.yaml create mode 100644 platform/prompts/generate_backstory_from_text_v4.yaml create mode 100644 platform/prompts/generate_backstory_from_text_v5.yaml create mode 100644 platform/prompts/generate_cron_schedule_v1.yaml create mode 100644 platform/prompts/generate_description_v1.yaml create mode 100644 platform/prompts/generate_description_v2.yaml create mode 100644 platform/prompts/generate_description_v3.yaml create mode 100644 platform/prompts/generate_faq_from_record_v1.yaml create mode 100644 platform/prompts/generate_form_v1.yaml create mode 100644 platform/prompts/generate_instruction_from_text_v1.yaml create mode 100644 platform/prompts/generate_instruction_from_text_v2.yaml create mode 100644 platform/prompts/generate_instruction_from_text_v3.yaml create mode 100644 platform/prompts/generate_json_v1.yaml create mode 100644 platform/prompts/generate_text_v1.yaml create mode 100644 platform/prompts/initiate_message_v1.yaml create mode 100644 platform/prompts/language_selection_v1.yaml create mode 100644 platform/prompts/markdown_generator_v1.yaml create mode 100644 platform/prompts/math_generator_v1.yaml create mode 100644 platform/prompts/mermaid_generator_v1.yaml create mode 100644 platform/prompts/personalization_v1.yaml create mode 100644 platform/prompts/references_generator_v1.yaml create mode 100644 platform/prompts/reprogramming_v1.yaml create mode 100644 platform/prompts/silent_v1.yaml create mode 100644 platform/prompts/summarize_text_v1.yaml create mode 100644 platform/prompts/timeout_marks_v1.yaml create mode 100644 platform/prompts/transform_instruction_v1.yaml create mode 100644 platform/prompts/transform_instruction_v2.yaml create mode 100644 platform/prompts/transform_instruction_v3.yaml create mode 100644 platform/prompts/translate_text_v1.yaml create mode 100644 platform/prompts/types.d.ts create mode 100644 platform/prompts/website_assistant_v1.yaml create mode 100644 platform/public/404.txt create mode 100644 platform/public/android-chrome-192x192.png create mode 100644 platform/public/android-chrome-512x512.png create mode 100644 platform/public/apple-touch-icon.png create mode 100644 platform/public/apps/ab501ab5/banner.png create mode 100644 platform/public/apps/c0de9a7f/banner.png create mode 100644 platform/public/apps/chat/banner.png create mode 100644 platform/public/apps/f2a9c7d4/banner.png create mode 100644 platform/public/apps/inbox/banner.png create mode 100644 platform/public/avatars/friend.mp4 create mode 100644 platform/public/examples/ai-answers/chatgpt.png create mode 100644 platform/public/examples/airline-booking/banner.png create mode 100644 platform/public/examples/airline-booking/destinations/bali.jpg create mode 100644 platform/public/examples/airline-booking/destinations/dubai.jpg create mode 100644 platform/public/examples/airline-booking/destinations/newyork.jpg create mode 100644 platform/public/examples/airline-booking/destinations/paris.jpg create mode 100644 platform/public/examples/airline-booking/destinations/rome.jpg create mode 100644 platform/public/examples/airline-booking/destinations/tokyo.jpg create mode 100644 platform/public/examples/airline-booking/icon.png create mode 100644 platform/public/examples/discord-ai-image-generator-bot/logo.png create mode 100644 platform/public/examples/discord-community-bot/discord.png create mode 100644 platform/public/examples/discord-community-bot/user.png create mode 100644 platform/public/examples/hotel-booking-bot/deluxe-suite.jpg create mode 100644 platform/public/examples/hotel-booking-bot/double-queen-room.jpg create mode 100644 platform/public/examples/hotel-booking-bot/room-view.jpg create mode 100644 platform/public/examples/image-bot/logo.old.png create mode 100644 platform/public/examples/image-bot/logo.png create mode 100644 platform/public/examples/liquid-glass/background.jpg create mode 100644 platform/public/examples/magento-assistant/targus.png create mode 100644 platform/public/examples/movie-recommendation-chatbot/banner.png create mode 100644 platform/public/examples/personal-advisor/banner.png create mode 100644 platform/public/examples/product-finder/iphone-11.jpg create mode 100644 platform/public/examples/product-finder/iphone-12-mini.jpg create mode 100644 platform/public/examples/product-finder/iphone-se.jpg create mode 100644 platform/public/examples/retro-gaming-bot/amiga.png create mode 100644 platform/public/examples/shopify-assistant/burton.png create mode 100644 platform/public/examples/slack-corporate-ai-bot/avatar.png create mode 100644 platform/public/examples/slack-corporate-ai-bot/slack.png create mode 100644 platform/public/examples/super-clippy/clippy.png create mode 100644 platform/public/examples/whatsapp-support-ai-bot/whatsapp.png create mode 100644 platform/public/favicon-dark.ico create mode 100644 platform/public/favicon-light.ico create mode 100644 platform/public/favicon.ico create mode 100644 platform/public/fonts/inter/400.ttf create mode 100644 platform/public/fonts/inter/700.ttf create mode 100644 platform/public/fonts/inter/900.ttf create mode 100644 platform/public/fonts/twemoji/twemoji.woff2 create mode 100644 platform/public/icon-dark.svg create mode 100644 platform/public/icon-light.svg create mode 100644 platform/public/icon.png create mode 100644 platform/public/icon.svg create mode 100644 platform/public/llms.txt create mode 100644 platform/public/logo.png create mode 100644 platform/public/logo.svg create mode 100644 platform/public/media/docs/052dc948-8c4c-419f-b8eb-d4e31fd2983a.mp4 create mode 100644 platform/public/media/docs/09d1e92b-a469-42f2-803f-7530e9fa9c81.png create mode 100644 platform/public/media/docs/0ed3a64a-b5e6-44fe-b6cc-5266d63b6f4e.png create mode 100644 platform/public/media/docs/32aae0f1-9a8b-49c0-86ed-9f090339a037.png create mode 100644 platform/public/media/docs/37f42535-449b-4478-9a9e-8a5b651fdc91.png create mode 100644 platform/public/media/docs/385f0bc2-f9cc-425c-9b76-91641ebb32bf.png create mode 100644 platform/public/media/docs/402ce413-1afc-4978-b908-a3bf815714ea.png create mode 100644 platform/public/media/docs/44f544b1-08ac-466b-a535-2b633af7eb72.png create mode 100644 platform/public/media/docs/57685a09-e010-4596-a1cc-bc58aac2bdcd.png create mode 100644 platform/public/media/docs/5e48b323-26c5-4f0e-8425-b6a5eec4dd34.png create mode 100644 platform/public/media/docs/7923e23c-1008-47b2-ba9f-d2651dde5f8a.png create mode 100644 platform/public/media/docs/7cbc9f4d-11b2-457b-bcea-743784fbcf08.png create mode 100644 platform/public/media/docs/96ed5f99-9495-42bd-a26d-cab4b659be6c.png create mode 100644 platform/public/media/docs/99e54aaa-1fa8-4108-9bc5-0b3334fb4a6b.png create mode 100644 platform/public/media/docs/a4f64366-9d08-4e2d-80d4-70d463e22ca8.mp4 create mode 100644 platform/public/media/docs/abbd424d-f2b1-4070-9930-f868ee1e9f18.png create mode 100644 platform/public/media/docs/c2ced854-aecd-4f45-99f9-ba1b2ddc67ca.png create mode 100644 platform/public/media/docs/d7375c5d-a92d-45ca-ba08-f74d7140abb2.png create mode 100644 platform/public/media/docs/d82629ef-36ad-474a-ba70-6bb2a9932239.png create mode 100644 platform/public/media/docs/d8895253-04f5-4325-9064-c9c947f488ad.png create mode 100644 platform/public/media/docs/d8a98b56-2106-46c3-bc4e-ddabcd79686c.png create mode 100644 platform/public/media/docs/d921e502-3f3e-47e6-9aa8-1064a8a7d8b7.png create mode 100644 platform/public/media/docs/dac1ed5e-42cb-47be-8ec8-946ae82b9434.png create mode 100644 platform/public/media/docs/dde0daeb-2699-4111-b7b7-a7cb2c8edd37.png create mode 100644 platform/public/media/docs/e82a8649-8c59-452e-aacc-ae5ff059b4aa.png create mode 100644 platform/public/media/docs/ea2084b0-39c0-476f-a244-da827b3fe902.mp4 create mode 100644 platform/public/media/docs/ea648b32-84ef-4991-8960-f10053465647.png create mode 100644 platform/public/media/docs/ef2470a8-b8ed-4822-92cf-b8adea32a231.png create mode 100644 platform/public/media/docs/fb69bc0e-9274-4bb8-916e-d03e5b0efbe2.png create mode 100644 platform/public/media/docs/fda04eba-f291-4a67-88f8-aed4b8fafc6b.mp4 create mode 100644 platform/public/site.webmanifest create mode 100644 platform/schemas/_triggerIntegrationSchedule.utest.js create mode 100644 platform/schemas/abilityDescription.js create mode 100644 platform/schemas/abilityDescription.utest.js create mode 100644 platform/schemas/abilityInstruction.js create mode 100644 platform/schemas/abilityInstruction.utest.js create mode 100644 platform/schemas/abilityName.js create mode 100644 platform/schemas/abilityName.utest.js create mode 100644 platform/schemas/alias.js create mode 100644 platform/schemas/alias.utest.js create mode 100644 platform/schemas/api/v1/ability.ts create mode 100644 platform/schemas/api/v1/anamIntegration.ts create mode 100644 platform/schemas/api/v1/avatarIntegration.ts create mode 100644 platform/schemas/api/v1/bot.ts create mode 100644 platform/schemas/api/v1/dataset.ts create mode 100644 platform/schemas/api/v1/discordIntegration.ts create mode 100644 platform/schemas/api/v1/emailIntegration.ts create mode 100644 platform/schemas/api/v1/extractIntegration.ts create mode 100644 platform/schemas/api/v1/file.ts create mode 100644 platform/schemas/api/v1/githubIntegration.ts create mode 100644 platform/schemas/api/v1/googlechatIntegration.ts create mode 100644 platform/schemas/api/v1/instagramIntegration.ts create mode 100644 platform/schemas/api/v1/mcpserverIntegration.ts create mode 100644 platform/schemas/api/v1/messengerIntegration.ts create mode 100644 platform/schemas/api/v1/microsoftteamsIntegration.ts create mode 100644 platform/schemas/api/v1/notionIntegration.ts create mode 100644 platform/schemas/api/v1/oAuthConnection.ts create mode 100644 platform/schemas/api/v1/policy.ts create mode 100644 platform/schemas/api/v1/portal.ts create mode 100644 platform/schemas/api/v1/recallIntegration.ts create mode 100644 platform/schemas/api/v1/secret.ts create mode 100644 platform/schemas/api/v1/sitemapIntegration.ts create mode 100644 platform/schemas/api/v1/skillserverIntegration.ts create mode 100644 platform/schemas/api/v1/skillset.ts create mode 100644 platform/schemas/api/v1/slackIntegration.ts create mode 100644 platform/schemas/api/v1/space.ts create mode 100644 platform/schemas/api/v1/supportIntegration.ts create mode 100644 platform/schemas/api/v1/task.ts create mode 100644 platform/schemas/api/v1/telegramIntegration.ts create mode 100644 platform/schemas/api/v1/triggerIntegration.ts create mode 100644 platform/schemas/api/v1/twilioIntegration.ts create mode 100644 platform/schemas/api/v1/whatsappIntegration.ts create mode 100644 platform/schemas/api/v1/widgetIntegration.ts create mode 100644 platform/schemas/backstory.js create mode 100644 platform/schemas/backstory.utest.js create mode 100644 platform/schemas/blueprintConfig.js create mode 100644 platform/schemas/blueprintId.js create mode 100644 platform/schemas/blueprintId.utest.js create mode 100644 platform/schemas/botConfig.js create mode 100644 platform/schemas/botId.js create mode 100644 platform/schemas/botId.utest.js create mode 100644 platform/schemas/contactFingerprint.js create mode 100644 platform/schemas/contactFingerprint.utest.js create mode 100644 platform/schemas/contactId.js create mode 100644 platform/schemas/contactId.utest.js create mode 100644 platform/schemas/conversationId.js create mode 100644 platform/schemas/conversationId.utest.js create mode 100644 platform/schemas/datasetId.js create mode 100644 platform/schemas/datasetId.utest.js create mode 100644 platform/schemas/dbMediumText.js create mode 100644 platform/schemas/dbString.js create mode 100644 platform/schemas/dbString.utest.js create mode 100644 platform/schemas/dbText.js create mode 100644 platform/schemas/description.js create mode 100644 platform/schemas/description.utest.js create mode 100644 platform/schemas/description2.js create mode 100644 platform/schemas/description2.utest.js create mode 100644 platform/schemas/events.ts create mode 100644 platform/schemas/events.utest.js create mode 100644 platform/schemas/expiresAt.js create mode 100644 platform/schemas/externalUrl.js create mode 100644 platform/schemas/externalUrl.utest.js create mode 100644 platform/schemas/features.js create mode 100644 platform/schemas/features.utest.js create mode 100644 platform/schemas/fileId.js create mode 100644 platform/schemas/functionsSchema.js create mode 100644 platform/schemas/functionsSchema.utest.js create mode 100644 platform/schemas/icon.js create mode 100644 platform/schemas/icon.utest.js create mode 100644 platform/schemas/imageModel.js create mode 100644 platform/schemas/inlineExtensions.ts create mode 100644 platform/schemas/inlineExtensions.utest.js create mode 100644 platform/schemas/jsonSchema.js create mode 100644 platform/schemas/jsonSchema.utest.js create mode 100644 platform/schemas/languageModel.js create mode 100644 platform/schemas/languageModel.utest.js create mode 100644 platform/schemas/messageId.js create mode 100644 platform/schemas/messageId.utest.js create mode 100644 platform/schemas/messageText.js create mode 100644 platform/schemas/messageText.utest.js create mode 100644 platform/schemas/messageType.js create mode 100644 platform/schemas/messageType.utest.js create mode 100644 platform/schemas/messages.js create mode 100644 platform/schemas/messages.utest.js create mode 100644 platform/schemas/meta.js create mode 100644 platform/schemas/meta.utest.js create mode 100644 platform/schemas/name.js create mode 100644 platform/schemas/name.utest.js create mode 100644 platform/schemas/name2.js create mode 100644 platform/schemas/name2.utest.js create mode 100644 platform/schemas/namespace.js create mode 100644 platform/schemas/namespace.utest.js create mode 100644 platform/schemas/notUsed.js create mode 100644 platform/schemas/notUsed.utest.js create mode 100644 platform/schemas/oAuthConnectionId.js create mode 100644 platform/schemas/recordText.js create mode 100644 platform/schemas/recordText.utest.js create mode 100644 platform/schemas/request.js create mode 100644 platform/schemas/request.utest.js create mode 100644 platform/schemas/reranker.js create mode 100644 platform/schemas/reranker.utest.js create mode 100644 platform/schemas/schedule.js create mode 100644 platform/schemas/schedule.utest.js create mode 100644 platform/schemas/secretId.js create mode 100644 platform/schemas/secretId.utest.js create mode 100644 platform/schemas/skillsetId.js create mode 100644 platform/schemas/skillsetId.utest.js create mode 100644 platform/schemas/slug.js create mode 100644 platform/schemas/slug.utest.js create mode 100644 platform/schemas/source.js create mode 100644 platform/schemas/source.utest.js create mode 100644 platform/schemas/spaceId.js create mode 100644 platform/schemas/spaceSiteSlug.js create mode 100644 platform/schemas/spaceSiteSlug.utest.js create mode 100644 platform/schemas/state.js create mode 100644 platform/schemas/structstr.js create mode 100644 platform/schemas/structstr.utest.js create mode 100644 platform/schemas/taskId.js create mode 100644 platform/schemas/taskId.utest.js create mode 100644 platform/schemas/taskSchedule.js create mode 100644 platform/schemas/taskSchedule.utest.js create mode 100644 platform/schemas/theme.js create mode 100644 platform/schemas/theme.utest.js create mode 100644 platform/schemas/timezone.js create mode 100644 platform/schemas/trigger.js create mode 100644 platform/schemas/trigger.utest.js create mode 100644 platform/schemas/triggerIntegrationSchedule.js create mode 100644 platform/schemas/videoModel.js create mode 100644 platform/scripts/README.md create mode 100644 platform/scripts/adjust-record-usage.js create mode 100644 platform/scripts/adjust-usage.js create mode 100644 platform/scripts/backfill-database-encryption.js create mode 100644 platform/scripts/build-api-spec.ts create mode 100644 platform/scripts/build-templates.js create mode 100644 platform/scripts/cleanup-conversations.js create mode 100644 platform/scripts/cleanup-expired-conversations.js create mode 100644 platform/scripts/cleanup-old-audit-logs.js create mode 100644 platform/scripts/cleanup-old-event-logs.js create mode 100644 platform/scripts/cleanup-old-event-metrics.js create mode 100644 platform/scripts/cleanup-old-usage-records.js create mode 100644 platform/scripts/count-tokens.js create mode 100644 platform/scripts/create-team.js create mode 100644 platform/scripts/db-push.js create mode 100644 platform/scripts/decrypt.js create mode 100644 platform/scripts/delete-bot.js create mode 100644 platform/scripts/delete-dataset.js create mode 100644 platform/scripts/delete-skillset.js create mode 100644 platform/scripts/delete-user.js create mode 100644 platform/scripts/delete-users-by-domain.js create mode 100644 platform/scripts/emfile-guard.cjs create mode 100644 platform/scripts/encrypt.js create mode 100644 platform/scripts/explore-sandbox.js create mode 100644 platform/scripts/export-dataset-to-csv.js create mode 100644 platform/scripts/export-dataset-to-dataset.js create mode 100644 platform/scripts/export-dataset-to-json.js create mode 100644 platform/scripts/fetch-sitemap-urls.js create mode 100644 platform/scripts/fetch-url-content.js create mode 100644 platform/scripts/find-ability.js create mode 100644 platform/scripts/find-empty-basic-info.js create mode 100644 platform/scripts/find-empty-references.js create mode 100644 platform/scripts/find-expired-tasks.js create mode 100644 platform/scripts/find-session.js create mode 100644 platform/scripts/find-unscheduled-tasks.js create mode 100644 platform/scripts/find-user.js create mode 100644 platform/scripts/generate-api-types.js create mode 100644 platform/scripts/generate-encryption-key.js create mode 100644 platform/scripts/get-contact-secret.js create mode 100644 platform/scripts/get-idle-conversations.js create mode 100644 platform/scripts/get-usage.js create mode 100644 platform/scripts/inspect-user-usage.js create mode 100644 platform/scripts/introspect-db.js create mode 100644 platform/scripts/list-recent-messages.js create mode 100644 platform/scripts/list-recent-usage.js create mode 100644 platform/scripts/list-space-storage.js create mode 100644 platform/scripts/move-bot.js create mode 100644 platform/scripts/move-dataset.js create mode 100644 platform/scripts/nuke-account.js create mode 100644 platform/scripts/print-env.js create mode 100644 platform/scripts/print-system.js create mode 100644 platform/scripts/prisma-query.js create mode 100644 platform/scripts/rename-user-email.js create mode 100644 platform/scripts/reset-usage.js create mode 100644 platform/scripts/run-db-placeholder.js create mode 100644 platform/scripts/run-proxy.js create mode 100644 platform/scripts/run-with-environment.js create mode 100644 platform/scripts/seed-db.js create mode 100644 platform/scripts/setup-prisma-client.js create mode 100644 platform/scripts/show-conversation.js create mode 100644 platform/scripts/simulate-conversation.js create mode 100644 platform/scripts/smoke-selfhost.js create mode 100644 platform/scripts/test-mcp-server.js create mode 100644 platform/scripts/trigger-clock-10.js create mode 100644 platform/scripts/trigger-conversation-idle.js create mode 100644 platform/scripts/trigger-dataset-cleanup.js create mode 100644 platform/scripts/trigger-task-schedule.js create mode 100644 platform/scripts/trigger-task-stalled.js create mode 100644 platform/scripts/trigger-task-trigger.js create mode 100644 platform/scripts/verify-bundle-modules.js create mode 100644 platform/scripts/yaml-esm-hooks.mjs create mode 100644 platform/scripts/yaml-esm-register.mjs create mode 100644 platform/stories/Introduction.mdx create mode 100644 platform/stories/Welcome.stories.tsx create mode 100644 platform/stories/assets/.gitkeep create mode 100644 platform/styles/globals.css create mode 100644 platform/tailwind.config.js create mode 100644 platform/templates/agent.js create mode 100644 platform/templates/channels.js create mode 100644 platform/templates/channels.utest.js create mode 100644 platform/templates/example.js create mode 100644 platform/templates/example.utest.js create mode 100644 platform/templates/hub.js create mode 100644 platform/templates/hub.utest.js create mode 100644 platform/templates/index.js create mode 100644 platform/templates/messaging.js create mode 100644 platform/templates/onboarding.js create mode 100644 platform/templates/onboarding.utest.js create mode 100644 platform/templates/original.js create mode 100644 platform/templates/widget.js create mode 100644 platform/templates/widget.utest.js create mode 100644 platform/tests/config/apps.utest.js create mode 100644 platform/tests/config/env.utest.ts create mode 100644 platform/tests/config/events.utest.js create mode 100644 platform/tests/config/headers.utest.js create mode 100644 platform/tests/config/host-configuration.utest.js create mode 100644 platform/tests/config/limits.utest.ts create mode 100644 platform/tests/config/models.utest.js create mode 100644 platform/tests/config/platform.utest.js create mode 100644 platform/tests/config/providers.utest.js create mode 100644 platform/tests/config/site.utest.js create mode 100644 platform/tests/config/site.utest.ts create mode 100644 platform/tests/integration/api-security-headers.itest.js create mode 100644 platform/tests/integration/conversation.itest.js create mode 100644 platform/tests/integration/dataset.itest.js create mode 100644 platform/tests/integration/file.itest.js create mode 100644 platform/tests/integration/openai.itest.js create mode 100644 platform/tests/integration/skillset.itest.js create mode 100644 platform/tests/integration/status.itest.js create mode 100644 platform/tests/integration/widget.itest.js create mode 100644 platform/tests/patch/next-auth.utest.js create mode 100644 platform/tests/setup/msw.utest.js create mode 100644 platform/tsconfig.json create mode 100644 platform/types/global.d.ts create mode 100644 platform/types/next-auth.d.ts create mode 100644 platform/vercel.json create mode 100644 platform/webpack/README.md create mode 100644 platform/webpack/loaders/json-gz.cjs create mode 100644 platform/webpack/loaders/json-gz.utest.js create mode 100644 platform/webpack/loaders/md-frontmatter.js create mode 100644 platform/webpack/loaders/md-frontmatter.utest.js create mode 100644 platform/webpack/loaders/yaml.js create mode 100644 platform/webpack/loaders/yaml.utest.js create mode 100644 platform/webpack/plugins/validate-sourcemaps.js create mode 100644 platform/webpack/plugins/validate-sourcemaps.utest.js create mode 100644 platform/webpack/stubs/empty-page.js create mode 100644 platform/workers/file.worker.js create mode 100644 platform/workers/gpt.worker.js create mode 100644 platform/workers/highlighter.worker.js create mode 100644 platform/workers/highlighter.worker.utest.js create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 stubs/LICENSE create mode 100644 stubs/README.md create mode 100644 stubs/canvas/README.md create mode 100644 stubs/canvas/index.js create mode 100644 stubs/canvas/package.json create mode 100644 stubs/napi-rs-canvas/README.md create mode 100644 stubs/napi-rs-canvas/index.d.ts create mode 100644 stubs/napi-rs-canvas/index.js create mode 100644 stubs/napi-rs-canvas/package.json create mode 100644 stubs/nodemailer/README.md create mode 100644 stubs/nodemailer/index.js create mode 100644 stubs/nodemailer/package.json create mode 100644 stubs/pdfjs-dist/README.md create mode 100644 stubs/pdfjs-dist/index.js create mode 100644 stubs/pdfjs-dist/package.json create mode 100644 stubs/re2/README.md create mode 100644 stubs/re2/index.js create mode 100644 stubs/re2/package.json create mode 100644 turbo.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..108672a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "name": "ChatBotKit Platform", + + "image": "mcr.microsoft.com/devcontainers/javascript-node:24-bookworm", + + "features": { + "ghcr.io/devcontainers/features/git-lfs:1": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + + "postCreateCommand": "corepack enable" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6c4b52d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# Dependencies (installed fresh in the container) +node_modules +**/node_modules + +# Build outputs and caches +**/.next +**/dist +**/.turbo +**/.swc +**/*.tsbuildinfo +**/coverage +**/storybook-static +platform/sites + +# @note content trees stay IN the context deliberately - they are read +# during static generation. Tests do not (the image build skips them via +# SKIP_BUILD_TESTS), and underscore-prefixed files under pages/ are private +# partials and page tests that must not compile into routes - the same set +# the deploy workflow's "Remove private files" step strips, minus the Next.js +# specials at the pages root which are re-included below. +**/*.utest.* +**/*.itest.* +platform/pages/**/_* +!platform/pages/_* + +# Environment files (passed at runtime, never baked in) +**/.env +**/.env.* +!**/.env.example + +# Git metadata and CI definitions - nothing in the build reads them +.git +**/.github + +# Editor and CI +.devcontainer +.vscode +**/.storybook +# @note the storybook stories folder only - platform/content/stories is real +# site content the build imports +platform/stories + +# Logs and scratch +**/*.log + +# pnpm store fallback location (dev container) +**/.pnpm-store diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ede84e0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +# Binary assets - routed through Git LFS, with no text diffs and no eol +# normalization. The tree carries ~75MB of binaries across ~80 files (app +# banners, example imagery, fonts, media, and the generated embeddings +# archives; the largest single file is ~14MB), and that grows every time a +# screenshot or an asset lands. LFS keeps those bytes out of every clone's +# pack and out of PR diffs. +# +# This mirrors the monorepo root .gitattributes, which already LFS-tracks the +# same extensions, so a file's storage does not change depending on which +# repository it is checked out from. +# +# @note SVG is deliberately absent: it is XML, it diffs usefully, and the +# files here are small. +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text +*.ico filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.ogg filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text +*.eot filter=lfs diff=lfs merge=lfs -text +*.woff filter=lfs diff=lfs merge=lfs -text +*.woff2 filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.doc filter=lfs diff=lfs merge=lfs -text +*.docx filter=lfs diff=lfs merge=lfs -text +*.ppt filter=lfs diff=lfs merge=lfs -text +*.pptx filter=lfs diff=lfs merge=lfs -text +*.xls filter=lfs diff=lfs merge=lfs -text +*.xlsx filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text + +# Generated archives - keep them out of PR diff stats +*.embeddings.json.gz linguist-generated diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..47e9e90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,52 @@ +name: Bug report +description: Something does not work the way the code says it should +labels: ['bug'] +body: + - type: markdown + attributes: + value: >- + Thanks for taking the time. A reproducible report is the fastest path + to a fix - the closer you get us to the failure, the sooner it dies. + + - type: textarea + id: what-happened + attributes: + label: What happened + description: >- + What you did, what you expected, and what you got instead. Include the + exact error output where there is one. + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: How to reproduce it + description: >- + The smallest sequence of steps that shows the problem on a fresh + checkout. If it only happens with specific configuration, include the + relevant (redacted) settings. + placeholder: | + 1. pnpm install + 2. ... + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: >- + How you are running the platform and on what. + placeholder: | + - run mode: docker compose / pnpm dev / built image + - node: 24.x, pnpm: 11.x + - os: ... + validations: + required: true + + - type: textarea + id: extra + attributes: + label: Anything else + description: Logs, screenshots, or a theory about the cause. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c7daa11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +blank_issues_enabled: false +contact_links: + - name: Security report + url: https://github.com/chatbotkit/platform/blob/main/SECURITY.md + about: >- + Do not report security issues publicly. Email contact@cbk.ai + with "Security report" in the subject - see SECURITY.md. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..e4b9c17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,28 @@ +name: Feature request +description: Something the platform should do that it does not +labels: ['enhancement'] +body: + - type: textarea + id: problem + attributes: + label: The problem + description: >- + What you are trying to do and what stops you today. Lead with the + problem rather than the solution - it keeps the discussion honest + about alternatives. + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed shape + description: >- + How you imagine it working, if you have a shape in mind. API surface, + configuration, interface - whatever level you have. + + - type: textarea + id: alternatives + attributes: + label: What you do instead today + description: Workarounds you use now, and where they fall short. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..78a53f9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ + + +## What + + + +## Why + + + +## How it was verified + + diff --git a/.github/workflows/_verify.yaml b/.github/workflows/_verify.yaml new file mode 100644 index 0000000..2b46150 --- /dev/null +++ b/.github/workflows/_verify.yaml @@ -0,0 +1,142 @@ +# The repository's quality gate as a reusable workflow: install from the +# committed lockfile and type-check cleanly with the public module defaults +# - the exact environment every fresh checkout gets. +# +# Called from two places: +# - pull-request.yaml, where it gates every pull request (including forks; +# see the security notes there - this workflow uses no secrets and only +# ever needs `contents: read`) +# - publish-ghcr-platform.yaml, where it gates image builds on `next` +# pushes, which land directly without a pull request + +name: Verify + +on: + workflow_call: + +permissions: + contents: read + +env: + # pinned to the devcontainer versions + REQUIRED_PNPM_VERSION: 11.24.0 + REQUIRED_NODE_VERSION: 24.20.0 + +jobs: + verify: + runs-on: ubuntu-latest-8-cores-amd64 + + # @note a healthy sequential run takes low double-digit minutes; anything + # longer is wedged - cut it off and free the hosted runner + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v7 + with: + lfs: true + + - uses: pnpm/setup@v2 + with: + version: ${{ env.REQUIRED_PNPM_VERSION }} + runtime: node@${{ env.REQUIRED_NODE_VERSION }} + cache: true + install: false + + - name: Report runner resources + # @note exit 137 anywhere below is the runner kernel OOM-killing the + # workload: read this step's output for the VM this run actually + # landed on before blaming the workload + run: nproc && grep MemTotal /proc/meminfo && df -h . | tail -1 + + - name: Install from the lockfile + # @note --frozen-lockfile is the point: it fails when pnpm-lock.yaml + # has drifted from the manifests, which is the most common way a + # fresh checkout silently breaks + run: pnpm install --frozen-lockfile + + - name: Audit production dependencies + # @note the published baseline is no known production vulnerabilities, + # so every advisory fails this gate. A temporary exception must name + # the advisory, rationale, owner, compensating controls and expiry in + # the public change that adds the exception before it is ignored here. + run: pnpm audit --prod --audit-level low + + - name: Configure the application from the example environment + # @note the example file is the documented minimum a fresh checkout + # runs on; nothing here is a secret + working-directory: platform + run: cp .env.example .env + + - name: Create the database and generate its client + # @note runs before the package steps: any package that imports the + # database module needs the generated client to build or type-check. + # The client generator reads the typed SQL against a live database, so + # an empty SQLite file is pushed first. The absolute path is + # deliberate - the push runs from the database module's directory, + # the application from its own + working-directory: platform + run: | + mkdir -p .dev + touch .dev/platform.db + export PRISMA_DATABASE_URL="file:$PWD/.dev/platform.db" + pnpm --filter @chatbotkit-dev/db db:push + pnpm db:gen + + - name: Build every package + # @note the application is excluded from this step and the three + # below: its own steps further down provision the environment its + # config asserts at load. Its lint, type-check and tests run below; + # the trusted image-publication workflow builds and smoke-tests it. + run: pnpm turbo run build --continue --filter='!@chatbotkit/platform' + + - name: Lint every package + # @note the lint debt that kept this step out was paid down on + # August 24 2026 (13 packages); the gate is born green + run: pnpm turbo run lint --continue --filter='!@chatbotkit/platform' + + - name: Check every package + run: pnpm turbo run check --continue --filter='!@chatbotkit/platform' + + - name: Test every package + run: pnpm turbo run test --continue --filter='!@chatbotkit/platform' + + - name: Cache TypeScript build info + # @note persists only tsc's non-sensitive incremental project state. + # The SHA suffix lets each successful run advance the baseline, while + # the restore prefix selects the newest state built with the same + # lockfile and application compiler configuration. tsc validates file + # signatures, so a stale restore falls back to checking changed files. + # Fork pull requests can read the base cache but cannot write into its + # scope; never add generated environment or credential files here. + uses: actions/cache@v6 + with: + path: platform/tsconfig.tsbuildinfo + key: ${{ runner.os }}-${{ runner.arch }}-tsbuildinfo-application-${{ hashFiles('pnpm-lock.yaml', 'platform/tsconfig.json') }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-tsbuildinfo-application-${{ hashFiles('pnpm-lock.yaml', 'platform/tsconfig.json') }}- + + - name: Cache Jest transform cache + # @note persists only Jest's transform/haste cache - hashed transpiled + # module output, nothing sensitive. Jest validates entries by content + # hash, so a stale restore degrades to re-transforming changed files. + # Same key discipline as the tsbuildinfo cache above: the SHA suffix + # advances the baseline, the restore prefix picks the newest state for + # the same lockfile. + uses: actions/cache@v6 + with: + path: platform/.jest-cache + key: ${{ runner.os }}-${{ runner.arch }}-jest-application-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-jest-application-${{ hashFiles('pnpm-lock.yaml') }}- + + - name: Lint application + working-directory: platform + run: pnpm lint + + - name: Check application types + working-directory: platform + run: pnpm check + + - name: Test application with coverage + working-directory: platform + run: pnpm test diff --git a/.github/workflows/publish-ghcr-platform.yaml b/.github/workflows/publish-ghcr-platform.yaml new file mode 100644 index 0000000..773f249 --- /dev/null +++ b/.github/workflows/publish-ghcr-platform.yaml @@ -0,0 +1,415 @@ +name: Publish GHCR Platform + +on: + workflow_dispatch: + + # @note no `paths` filter here on purpose: the verify job must run on every + # push to next so its check lands on the SHA the promotion pull request + # (next -> main) requires - pull-request.yaml skips its own run for that + # head branch. Path filtering for the image build lives in the changes job + # below instead. + push: + branches: + - main + - next + +permissions: + contents: read + packages: write + +concurrency: + group: publish-ghcr-platform-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + +jobs: + # @note every job is guarded to the canonical repository and its + # `platform-*` siblings: a fork that pushes to main or next gets a clean + # skip rather than a queued build against runners and a registry it does + # not have. + # + # The trigger-level `paths` filter this job replaces could not coexist with + # running verify on every next push, so the build-relevance decision is made + # here from the actual diff of the push. Anything that prevents computing + # that diff (a brand-new branch, a force push, a manual dispatch) falls back + # to building - a spurious build is cheap, a silently skipped one is not. + changes: + if: >- + (github.repository == 'chatbotkit/platform' || startsWith(github.repository, 'chatbotkit/platform-')) && + github.actor != 'github-actions[bot]' + name: Detect build-relevant changes + runs-on: ubuntu-latest + outputs: + build: ${{ steps.diff.outputs.build }} + steps: + - uses: actions/checkout@v7 + + - name: Diff the push against build-relevant paths + id: diff + env: + BEFORE: ${{ github.event.before }} + run: | + # @note keep this pattern list in sync with the inputs the Docker + # build actually consumes (the former trigger paths filter) + pattern='^(packages/|patches/|platform/|stubs/|docker/|\.dockerignore$|\.pnpmfile\.cjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|turbo\.json$|\.github/workflows/(publish-ghcr-platform|_verify)\.yaml$)' + + if [ "$GITHUB_EVENT_NAME" != "push" ] \ + || [ -z "$BEFORE" ] \ + || [ "$BEFORE" = "0000000000000000000000000000000000000000" ] \ + || ! git fetch --quiet --depth=1 origin "$BEFORE" + then + echo "build=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if git diff --name-only "$BEFORE" "$GITHUB_SHA" | grep -qE "$pattern"; then + echo "build=true" >> "$GITHUB_OUTPUT" + else + echo "build=false" >> "$GITHUB_OUTPUT" + fi + + # @note `next` receives direct pushes, so nothing has vetted the code yet - + # run the quality gate before spending build minutes on it. It runs on + # every next push, not just build-relevant ones, so the promotion pull + # request always finds a verify check on the head SHA. `main` only moves + # through pull requests that already passed the same gate, so the job is + # skipped there and the build starts immediately. + verify: + if: >- + (github.repository == 'chatbotkit/platform' || startsWith(github.repository, 'chatbotkit/platform-')) && + github.actor != 'github-actions[bot]' && + github.ref == 'refs/heads/next' + uses: ./.github/workflows/_verify.yaml + + build: + needs: + - changes + - verify + # @note !cancelled() + accepting the skipped verify keeps main builds + # running while a failed gate on next still blocks them + if: >- + !cancelled() && + (github.repository == 'chatbotkit/platform' || startsWith(github.repository, 'chatbotkit/platform-')) && + needs.changes.outputs.build == 'true' && + (needs.verify.result == 'success' || needs.verify.result == 'skipped') && + github.actor != 'github-actions[bot]' && + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next') + name: Build ${{ matrix.flavor.name }} (${{ matrix.architecture.name }}) + runs-on: ${{ matrix.architecture.runner }} + strategy: + fail-fast: false + matrix: + flavor: + # @note package selections are compile-time image flavors. Add a + # flavor here only when its Docker target installs that exact package + # configuration. Runtime services such as Redis are not flavors. + - name: community + application_target: application + initializer_target: initializer + architecture: + - name: amd64 + platform: linux/amd64 + runner: ubuntu-latest-8-cores-amd64 + - name: arm64 + platform: linux/arm64 + runner: ubuntu-latest-8-cores-arm64 + + steps: + - uses: actions/checkout@v7 + with: + lfs: true + + # @note the naming grammar is -[-], with + # the tag carrying only the build axis (channel or sha). The bare + # - name is the Compose distribution artifact - + # the one consumers type - and -app/-init are its digest-pinned + # component images. + - name: Resolve image names + id: image + env: + FLAVOR: ${{ matrix.flavor.name }} + REPOSITORY_NAME: ${{ github.event.repository.name }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + run: | + owner="${REPOSITORY_OWNER,,}" + repository="${REPOSITORY_NAME,,}" + stack="${REGISTRY}/${owner}/${repository}-${FLAVOR}" + echo "stack=${stack}" >> "$GITHUB_OUTPUT" + echo "application=${stack}-app" >> "$GITHUB_OUTPUT" + echo "initializer=${stack}-init" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract application metadata + id: application-metadata + uses: docker/metadata-action@v6 + with: + images: ${{ steps.image.outputs.application }} + tags: | + type=raw,value=${{ github.ref_name }} + type=sha,format=short,prefix=sha- + type=raw,value=latest,enable=${{ github.ref_name == 'main' }} + + - name: Build and push application image + id: application + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile + target: ${{ matrix.flavor.application_target }} + platforms: ${{ matrix.architecture.platform }} + # @note community images embed full source maps on purpose - the + # source is public and self-hosted debugging needs them + build-args: | + BUILD_SOURCEMAPS=full + labels: ${{ steps.application-metadata.outputs.labels }} + outputs: type=image,name=${{ steps.image.outputs.application }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=${{ github.event.repository.name }}-${{ matrix.flavor.name }}-application-${{ matrix.architecture.name }} + cache-to: type=gha,mode=max,scope=${{ github.event.repository.name }}-${{ matrix.flavor.name }}-application-${{ matrix.architecture.name }} + + - name: Extract initializer metadata + id: initializer-metadata + uses: docker/metadata-action@v6 + with: + images: ${{ steps.image.outputs.initializer }} + tags: | + type=raw,value=${{ github.ref_name }} + type=sha,format=short,prefix=sha- + type=raw,value=latest,enable=${{ github.ref_name == 'main' }} + + - name: Build and push initializer image + id: initializer + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile + target: ${{ matrix.flavor.initializer_target }} + platforms: ${{ matrix.architecture.platform }} + build-args: | + BUILD_SOURCEMAPS=full + labels: ${{ steps.initializer-metadata.outputs.labels }} + outputs: type=image,name=${{ steps.image.outputs.initializer }},push-by-digest=true,name-canonical=true,push=true + cache-from: | + type=gha,scope=${{ github.event.repository.name }}-${{ matrix.flavor.name }}-application-${{ matrix.architecture.name }} + type=gha,scope=${{ github.event.repository.name }}-${{ matrix.flavor.name }}-initializer-${{ matrix.architecture.name }} + cache-to: type=gha,mode=max,scope=${{ github.event.repository.name }}-${{ matrix.flavor.name }}-initializer-${{ matrix.architecture.name }} + + - name: Smoke-test published images + env: + APPLICATION_IMAGE: ${{ steps.image.outputs.application }}@${{ steps.application.outputs.digest }} + ARCHITECTURE: ${{ matrix.architecture.name }} + FLAVOR: ${{ matrix.flavor.name }} + INITIALIZER_IMAGE: ${{ steps.image.outputs.initializer }}@${{ steps.initializer.outputs.digest }} + PLATFORM: ${{ matrix.architecture.platform }} + run: | + container="platform-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${FLAVOR}-${ARCHITECTURE}" + volume="platform-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${FLAVOR}-${ARCHITECTURE}" + + cleanup() { + docker rm --force "$container" >/dev/null 2>&1 || true + docker volume rm --force "$volume" >/dev/null 2>&1 || true + } + + trap cleanup EXIT + + docker volume create "$volume" + docker run --rm \ + --platform "$PLATFORM" \ + --env PRISMA_DATABASE_URL=file:/data/platform.db \ + --volume "$volume:/data" \ + "$INITIALIZER_IMAGE" + + # @note SITE_URL deliberately differs from the image's build-time + # value so the probe below proves the runtime environment actually + # reaches the served pages - a build-time bake cannot pass it + docker run --detach \ + --platform "$PLATFORM" \ + --name "$container" \ + --env NEXTAUTH_SECRET=published-image-smoke-test \ + --env NEXTAUTH_URL=http://smoke-test.invalid \ + --env PRISMA_DATABASE_URL=file:/data/platform.db \ + --env QUEUE_SECRET=published-image-smoke-test \ + --env SITE_URL=http://smoke-test.invalid \ + --volume "$volume:/data" \ + "$APPLICATION_IMAGE" + + # @note /signin renders per request, so its markup must carry the + # runtime site host stamped by the request context + for attempt in $(seq 1 90); do + if docker exec "$container" node -e \ + "fetch('http://127.0.0.1:3000/signin').then(async (response) => { const body = await response.text(); process.exit(response.status < 500 && body.includes('smoke-test.invalid') ? 0 : 1) }).catch(() => process.exit(1))" + then + exit 0 + fi + + sleep 2 + done + + docker logs "$container" + exit 1 + + # @note matrix job outputs cannot be aggregated reliably, so digest + # marker files carry both architecture results into the publish job + - name: Export image digests + env: + APPLICATION_DIGEST: ${{ steps.application.outputs.digest }} + INITIALIZER_DIGEST: ${{ steps.initializer.outputs.digest }} + run: | + mkdir -p "$RUNNER_TEMP/platform-digests/application" + mkdir -p "$RUNNER_TEMP/platform-digests/initializer" + touch "$RUNNER_TEMP/platform-digests/application/${APPLICATION_DIGEST#sha256:}" + touch "$RUNNER_TEMP/platform-digests/initializer/${INITIALIZER_DIGEST#sha256:}" + + - name: Upload image digests + uses: actions/upload-artifact@v7 + with: + name: platform-digests-${{ matrix.flavor.name }}-${{ matrix.architecture.name }} + path: ${{ runner.temp }}/platform-digests + retention-days: 1 + + publish: + # @note !cancelled() is required: the implicit success() looks at the + # whole needs chain, and verify is skipped on main + if: >- + !cancelled() && + (github.repository == 'chatbotkit/platform' || startsWith(github.repository, 'chatbotkit/platform-')) && + needs.build.result == 'success' && + github.actor != 'github-actions[bot]' && + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next') + name: Publish ${{ matrix.flavor.name }} + needs: build + runs-on: ${{ matrix.flavor.runner }} + strategy: + fail-fast: false + matrix: + flavor: + # @note keep this list in sync with the build job's flavor matrix + - name: community + runner: ubuntu-latest-8-cores-amd64 + + steps: + - uses: actions/checkout@v7 + with: + lfs: true + + - name: Resolve image names + id: image + env: + FLAVOR: ${{ matrix.flavor.name }} + REPOSITORY_NAME: ${{ github.event.repository.name }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + run: | + owner="${REPOSITORY_OWNER,,}" + repository="${REPOSITORY_NAME,,}" + stack="${REGISTRY}/${owner}/${repository}-${FLAVOR}" + echo "stack=${stack}" >> "$GITHUB_OUTPUT" + echo "application=${stack}-app" >> "$GITHUB_OUTPUT" + echo "initializer=${stack}-init" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + # @note `docker compose publish` requires Compose v2.34 or newer; the + # runner's bundled plugin is not guaranteed to be that new + - name: Set up Docker Compose + uses: docker/setup-compose-action@v2 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Download image digests + uses: actions/download-artifact@v7 + with: + pattern: platform-digests-${{ matrix.flavor.name }}-* + path: ${{ runner.temp }}/platform-digests + merge-multiple: true + + - name: Create multi-platform image manifests + id: manifest + env: + APPLICATION_IMAGE: ${{ steps.image.outputs.application }} + CHANNEL: ${{ github.ref_name }} + DIGESTS_DIR: ${{ runner.temp }}/platform-digests + INITIALIZER_IMAGE: ${{ steps.image.outputs.initializer }} + run: | + application_sources=() + for digest_file in "$DIGESTS_DIR"/application/*; do + [ -f "$digest_file" ] || continue + application_sources+=("${APPLICATION_IMAGE}@sha256:$(basename "$digest_file")") + done + + initializer_sources=() + for digest_file in "$DIGESTS_DIR"/initializer/*; do + [ -f "$digest_file" ] || continue + initializer_sources+=("${INITIALIZER_IMAGE}@sha256:$(basename "$digest_file")") + done + + if [ "${#application_sources[@]}" -ne 2 ] || [ "${#initializer_sources[@]}" -ne 2 ]; then + echo "Expected two architecture digests for each image" >&2 + exit 1 + fi + + short_sha="${GITHUB_SHA:0:7}" + application_tags=(--tag "${APPLICATION_IMAGE}:${CHANNEL}" --tag "${APPLICATION_IMAGE}:sha-${short_sha}") + initializer_tags=(--tag "${INITIALIZER_IMAGE}:${CHANNEL}" --tag "${INITIALIZER_IMAGE}:sha-${short_sha}") + + if [ "$CHANNEL" = "main" ]; then + application_tags+=(--tag "${APPLICATION_IMAGE}:latest") + initializer_tags+=(--tag "${INITIALIZER_IMAGE}:latest") + fi + + docker buildx imagetools create "${application_tags[@]}" "${application_sources[@]}" + docker buildx imagetools create "${initializer_tags[@]}" "${initializer_sources[@]}" + + application_digest=$(docker buildx imagetools inspect "${APPLICATION_IMAGE}:${CHANNEL}" --format '{{.Manifest.Digest}}') + initializer_digest=$(docker buildx imagetools inspect "${INITIALIZER_IMAGE}:${CHANNEL}" --format '{{.Manifest.Digest}}') + echo "application_digest=${application_digest}" >> "$GITHUB_OUTPUT" + echo "initializer_digest=${initializer_digest}" >> "$GITHUB_OUTPUT" + + # @note the one-command distribution route: the flavor's Compose file is + # published as an OCI artifact under -, with every + # image reference resolved to a digest so the artifact tag identifies an + # exact, immutable stack. Consumers run: + # + # docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest up + - name: Publish Compose distribution artifact + env: + APPLICATION_IMAGE: ${{ steps.image.outputs.application }}@${{ steps.manifest.outputs.application_digest }} + CHANNEL: ${{ github.ref_name }} + FLAVOR: ${{ matrix.flavor.name }} + INITIALIZER_IMAGE: ${{ steps.image.outputs.initializer }}@${{ steps.manifest.outputs.initializer_digest }} + STACK_IMAGE: ${{ steps.image.outputs.stack }} + run: | + publish() { + PLATFORM_IMAGE="$APPLICATION_IMAGE" \ + PLATFORM_INIT_IMAGE="$INITIALIZER_IMAGE" \ + docker compose --file "docker/distro/${FLAVOR}/compose.yml" \ + publish -y --resolve-image-digests "$1" + } + + publish "${STACK_IMAGE}:${CHANNEL}" + + if [ "$CHANNEL" = "main" ]; then + publish "${STACK_IMAGE}:latest" + fi + + - name: Smoke-test published distribution artifact + env: + CHANNEL: ${{ github.ref_name }} + STACK_IMAGE: ${{ steps.image.outputs.stack }} + run: | + docker compose --file "oci://${STACK_IMAGE}:${CHANNEL}" config --quiet diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml new file mode 100644 index 0000000..b3ef4e4 --- /dev/null +++ b/.github/workflows/pull-request.yaml @@ -0,0 +1,52 @@ +# The quality gate for this repository: every pull request must install from +# the committed lockfile and type-check cleanly with the public module defaults +# - the exact environment every fresh checkout gets. The gate itself lives in +# _verify.yaml so pushes to `next` can run the same checks before an image +# build. +# +# SECURITY: this workflow is safe to run on code from strangers, and it is +# designed to stay that way. Fork pull requests run through the plain +# `pull_request` event, which GitHub executes on the fork's merge ref with a +# read-only token and NO access to repository secrets; the job runs on a +# GitHub-hosted runner that is discarded afterwards; the workflow declares +# `contents: read` and uses no secrets of its own. Together those are the +# whole boundary, so there is no fork guard: an approved outside +# contribution gets the same check as a maintainer's branch, which is what +# "required check" has to mean for it to be worth requiring. +# +# What keeps the boundary: +# - never use `pull_request_target` here, and never check out and execute +# pull request code from a job that has secrets or a write token +# - a runner must not survive the job; anything it ran is a foothold. The +# GitHub-hosted runner below is a fresh VM for the job and is discarded +# afterwards. A persistent self-hosted runner must never be used here +# - never add secrets to this workflow or to _verify.yaml; a step that +# needs one belongs in a separate workflow that does not execute +# contributed code +# - keep the repository setting "Require approval for all outside +# collaborators" (Settings > Actions > General) so a stranger's first run +# is still a maintainer's deliberate click, and their compute is not +# spent on drive-by pull requests + +name: Pull Request Checks + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + # @note the promotion pull request (next -> main) is exempt: its head + # commit is the tip of next, and the push-triggered publish workflow has + # already run this exact gate on that SHA - check runs attach to the + # commit, so the pull request inherits the green check without a second + # run. Every other head branch (forks included) still gets its own run. + if: github.event_name != 'pull_request' || github.head_ref != 'next' + uses: ./.github/workflows/_verify.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0b2c58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +# @note this is the ignore policy for the published repository, where this +# directory is the root. Inside the monorepo the root .gitignore covers most +# of what follows, which is exactly why it has to be repeated here: a +# standalone checkout has no such parent, and without this file a contributor +# running the documented setup sees build output and dependencies reported as +# untracked changes. + +# Dependencies +node_modules/ + +# pnpm store fallback location (dev container) +.pnpm-store/ + +# Local development state (dev-profile database and the like) +.dev/ + +# Environment files - .env.example is the tracked template, real ones never are +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Build output +.next +out +dist +build +*.tsbuildinfo +next-env.d.ts + +# Task and compiler caches +.turbo +.swc +.eslintcache +.cache + +# Test output +coverage +*.lcov +.nyc_output + +# Logs and diagnostics +logs +*.log +.pnpm-debug.log* +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Generated at build time rather than tracked +**/public/integrations/widget/* +**/public/api/*/spec.json +**/public/robots.txt +**/public/sitemap* + +# Storybook build output +storybook-static + +# react-email preview build +.react-email + +# Deployment tooling +.vercel + +# Editor and OS noise +.DS_Store + +# Scratch files +**/*.todo +**/*.bak diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs new file mode 100644 index 0000000..de5abc0 --- /dev/null +++ b/.pnpmfile.cjs @@ -0,0 +1,146 @@ +/** + * @note Install-time dependency fixups for the platform core. When this + * folder is installed standalone (e.g. the public repository) pnpm loads the + * exported `hooks` directly. An enclosing workspace can import + * `createReadPackage` and pass its own path prefix because the `file:`/`link:` + * stub specifiers resolve relative to the install root rather than this file. + * + * @see https://pnpm.io/pnpmfile#hooksreadpackagepkg-context + */ + +/** + * Creates a readPackage hook with stub paths resolved against the given + * install-root-relative prefix ('' when platform is the install root, + * 'platform/' when this folder is embedded in another workspace). + * + * @param {string} pathPrefix + * @returns {(pkg: object) => object} + */ +function createReadPackage(pathPrefix) { + // @note Packages that incorrectly declare peer dependencies as + // devDependencies. pnpm's strict isolation prevents these packages from + // accessing their implicit dependencies, so the missing peerDependencies + // are injected at install time. + + // @note guard against the single most destructive mistake available in this + // folder: running `pnpm install` here while the folder sits inside an + // enclosing workspace. pnpm would faithfully install the standalone + // workspace in place, clobbering the enclosing workspace's module links and + // splitting singletons like react into two instances. The pnpmfile is the + // only hook pnpm loads before touching disk, so the refusal happens before + // any damage. The empty path prefix identifies this folder as the active + // install root; the enclosing workspace imports this hook with its own path + // prefix. Other exemptions are --lockfile-only (writes no modules) and + // PLATFORM_NESTED_INSTALL=1 for deliberate vendored setups. + let installOwnershipValidated = false + + function validateInstallOwnership() { + if (installOwnershipValidated) { + return + } + + installOwnershipValidated = true + + const fs = require('node:fs') + const path = require('node:path') + + const here = __dirname + const cwd = process.cwd() + + const installCommand = ['install', 'i', 'add', 'update', 'up'].some((c) => + process.argv.includes(c) + ) + + const nested = fs.existsSync(path.join(here, '..', 'pnpm-workspace.yaml')) + const inHere = cwd === here || cwd.startsWith(here + path.sep) + const platformOwnsInstall = pathPrefix === '' + + if ( + platformOwnsInstall && + nested && + inHere && + installCommand && + !process.argv.includes('--lockfile-only') && + !process.env.PLATFORM_NESTED_INSTALL + ) { + throw new Error( + 'Refusing to install in place: this folder sits inside an enclosing pnpm workspace whose install owns these directories. Install from the workspace root instead, or use `docker compose up` for an isolated dev environment. Set PLATFORM_NESTED_INSTALL=1 to override for a deliberate vendored setup.' + ) + } + } + + const missingPeerDependenciesMap = { + // @note tailwind-gradient-mask-image requires 'tailwindcss/plugin' but + // declares tailwindcss as a devDependency instead of peerDependency. With + // multiple tailwindcss versions in the workspace, pnpm cannot hoist it + // publicly, causing "Cannot find module 'tailwindcss/plugin'" errors. + // @see https://github.com/juhanakristian/tailwind-gradient-mask-image/issues + + 'tailwind-gradient-mask-image': { + tailwindcss: '*', + }, + + // @note @metascraper/helpers uses re2 but doesn't declare it as a peer + // dependency. We use a local shim at stubs/re2 to avoid native + // compilation issues. + + '@metascraper/helpers': { + re2: `file:${pathPrefix}stubs/re2`, + }, + } + + // @note Packages whose dependencies need to be overridden with stubs or + // alternative implementations. This is used when a package imports a + // dependency that causes build issues (e.g., ESM-only packages in webpack) + // but that functionality is not actually needed. + + const dependencyOverridesMap = { + // @note officeparser imports pdfjs-dist at the top level for PDF parsing, + // but pdfjs-dist is ESM-only and causes webpack bundling issues in + // Next.js. We use officeparser only for docx/pptx/xlsx parsing (not + // PDF), so we stub out pdfjs-dist with an empty implementation at + // stubs/pdfjs-dist. + + officeparser: { + 'pdfjs-dist': `link:${pathPrefix}stubs/pdfjs-dist`, + }, + } + + return function readPackage(pkg) { + validateInstallOwnership() + + // inject missing peer dependencies for packages that incorrectly declare them + { + const missingPeerDependencies = missingPeerDependenciesMap[pkg.name] || {} + + for (const [peerPackage, peerVersion] of Object.entries( + missingPeerDependencies + )) { + pkg.peerDependencies = pkg.peerDependencies || {} + pkg.peerDependencies[peerPackage] = peerVersion + } + } + + // override dependencies with stubs or alternative implementations + { + const dependencyOverrides = dependencyOverridesMap[pkg.name] || {} + + for (const [depPackage, depVersion] of Object.entries( + dependencyOverrides + )) { + if (pkg.dependencies?.[depPackage]) { + pkg.dependencies[depPackage] = depVersion + } + } + } + + return pkg + } +} + +module.exports = { + createReadPackage, + hooks: { + readPackage: createReadPackage(''), + }, +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..be7f691 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "streetsidesoftware.code-spell-checker", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "csstools.postcss", + "bradlc.vscode-tailwindcss", + "Prisma.prisma" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d9e512f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch @chatbotkit/platform", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["-F", "@chatbotkit/platform", "dev"], + "restart": true, + "console": "integratedTerminal" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..879ea08 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": false, + "typescript.disableAutomaticTypeAcquisition": true, + "typescript.tsserver.maxTsServerMemory": 8192, + "typescript.tsserver.watchOptions": "vscode", + "editor.tabSize": 2, + "editor.rulers": [80], + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "explorer.sortOrder": "foldersNestsFiles", + "files.autoSave": "off" +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..4224695 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,14 @@ +# Initial ownership for the public platform repository. Replace individual +# ownership with maintainer teams as the maintainer group grows. +# +# @note paths here are relative to THIS directory, which becomes the root of +# the published repository. The monorepo keeps its own CODEOWNERS covering the +# wider tree; this file governs reviews on the mirror only. + +* @pdparchitect + +/.github/ @pdparchitect +/packages/ @pdparchitect +/patches/ @pdparchitect +/platform/ @pdparchitect +/stubs/ @pdparchitect diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a55a436 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,30 @@ +# Code of Conduct + +## Our Standard + +We want participation in the CBK community to be respectful, safe, +and productive. Contributors are expected to: + +- Be considerate and constructive. +- Discuss ideas and code without attacking people. +- Respect different backgrounds, experiences, and levels of expertise. +- Give and receive technical feedback in good faith. +- Protect private, personal, customer, and security-sensitive information. + +Harassment, discrimination, threats, deliberate disruption, and publication of +another person's private information are not acceptable. + +## Enforcement + +Project maintainers may edit or remove contributions, comments, or other +participation that violates these expectations. Repeated or serious violations +may result in temporary or permanent exclusion from project spaces. + +Report conduct concerns privately to `contact@cbk.ai` with +`Code of conduct report` in the subject. Maintainers will review reports fairly +and protect the reporter's privacy as far as practical. + +## Scope + +This code applies in repository discussions, issues, pull requests, community +spaces, and public interactions where someone represents the project. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..07dc87f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,121 @@ +# Contributing + +Thank you for helping improve CBK. + +Contributions are welcome. By submitting a contribution for inclusion in CBK, +you agree to license it under the Apache License 2.0 and confirm that you have +the right to submit it. + +Start with [Architecture](./docs/architecture.md). It explains the two things +every contributor needs before touching the tree: how the swappable module +packages and the application fit together, and the conventions - the two +routers, the `lib/` naming scheme, new TypeScript source with new JavaScript +tests - that are not self-evident from the directory layout. + +## Before starting + +- Search existing issues and pull requests before beginning work. +- Open an issue before a large feature, architectural, or behavioral change - + the issue is where the shape gets agreed, the pull request is where it gets + reviewed. +- Report security vulnerabilities privately, per [SECURITY.md](./SECURITY.md), + never through the issue tracker. +- Keep changes focused. One concern per pull request; unrelated cleanup slows + the review of both. + +## Branches + +Open contributor pull requests against `next`, the development branch. The +`main` branch is the stable release branch and accepts reviewed promotions from +`next`, not direct feature pull requests. + +## Development setup + +Node.js 24.20 and pnpm 11.24 or later, plus Git LFS - binary assets (images, +fonts, sample documents) are LFS pointers, so run `git lfs install` before +cloning or `git lfs pull` afterwards. + +```sh +pnpm install +``` + +To run the application: + +```sh +cd platform +cp .env.example .env +pnpm db:push # provision the local SQLite database +pnpm dev # → http://127.0.0.1:8080 +``` + +A fresh checkout boots after copying `.env.example`, with no vendor or +deployment-specific configuration: the module defaults are a working +vendor-free deployment (email prints to the console, caching is in-process, +queue delivery is immediate and non-durable, and there is no plan or billing +concept). The one default that needs a backing service to do anything is +storage. The public module speaks the S3 protocol, so file flows refuse at the +point of use until the storage block in `.env.example` is uncommented (it +points at the Compose `garage` service: `docker compose up garage +garage-init`). Anything that needs credentials documents them in its own +package README. + +Alternatively, `docker compose up` at this root is the complete default stack - +the dev server with SQLite, Redis, Qdrant and Garage - `docker compose up redis +qdrant garage garage-init` starts the backing services only for host-side +development, and `docker compose --profile distro up --build platform` builds +and serves the full compiled stack. + +## Quality checks + +The CI quality gate (`.github/workflows/_verify.yaml`) runs on every pull +request, and it is exactly what a fresh checkout gets: + +```sh +# every package +pnpm install --frozen-lockfile +pnpm audit --prod --audit-level low +pnpm turbo run build --continue --filter='!@chatbotkit/platform' +pnpm turbo run lint --continue --filter='!@chatbotkit/platform' +pnpm turbo run check --continue --filter='!@chatbotkit/platform' +pnpm turbo run test --continue --filter='!@chatbotkit/platform' + +# the application - from .env.example and a fresh SQLite database +cd platform +pnpm lint # eslint across app and scripts +pnpm check # typescript, no emit +pnpm test # unit suite with coverage +``` + +The package steps exclude the application only because it has its own steps: +the gate copies `.env.example`, pushes an empty SQLite database through the +community database module, generates the client, and then lints, type-checks +and tests the application against that clean-clone module graph. + +**Not in the gate: the application build, a formatting check, and the +self-host smoke test.** The image-publication workflow builds and smoke-tests +the application on pushes to `next`; if your change touches build +configuration, run `pnpm build` locally before you push. + +Two more practical consequences. If you change any `package.json`, refresh +`pnpm-lock.yaml` in the same commit or the frozen install fails. And new +behavior or a bug fix should come with a test - unit tests are `*.utest.js` or +`*.utest.jsx` co-located with their source in the application and `*.test.js` +in packages, written in JavaScript by convention (see `docs/architecture.md` +for why). + +Inside `platform/`, the narrower loops are `pnpm check`, `pnpm lint`, +`pnpm test:unit path/to/file.utest.js`, and `pnpm storybook`. + +## Pull requests + +- Explain the problem and the outcome, not just the diff. +- Link the relevant issue or discussion. +- Describe how the change was verified. +- Call out schema changes, compatibility concerns, security implications, and + follow-up work explicitly. +- Never include credentials, production data, customer information, or + generated build artifacts. + +## Conduct + +Participation is covered by [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000..58d3e4a --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,19 @@ +# Licensing + +The `platform/` distribution is licensed under the +[Apache License, Version 2.0](./LICENSE), except for the independently licensed +components listed below. The repository attribution is recorded in +[NOTICE](./NOTICE). + +## Scoped licences + +| Path | Licence | Licence file | +| ---------------------------- | ------- | -------------------------------------------------------------------------- | +| `packages/cloak/` | MIT | [`packages/cloak/LICENSE.md`](./packages/cloak/LICENSE.md) | +| `packages/mcp-widgets/` | MIT | [`packages/mcp-widgets/LICENSE`](./packages/mcp-widgets/LICENSE) | +| `packages/react-prompt-kit/` | MIT | [`packages/react-prompt-kit/LICENSE`](./packages/react-prompt-kit/LICENSE) | +| `stubs/` | ISC | [`stubs/LICENSE`](./stubs/LICENSE) | + +The scoped licence for each path takes precedence over the repository licence +for files within that path. Package manifests declare the same SPDX licence +identifier as their effective licence. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..0ca0cfc --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +ChatBotKit Platform +Copyright 2026 CBK.AI LTD + +Licensed under Apache License 2.0. See LICENSE for terms. diff --git a/README.md b/README.md index 02ddf20..e108ad4 100644 --- a/README.md +++ b/README.md @@ -1 +1,99 @@ -# platform \ No newline at end of file +
+ + + + ChatBotKit + + +
+ +

AI platform in a Box

+ +

+ A modern, sovereign AI backend for products
+ and enterprise deployments.
+

+ +

+ Node 24+ + pnpm 11 + TypeScript 6 + Next.js 16 + Docker Compose +

+ +

+ Run it · + Try it · + Documentation · + Architecture · + Contributing · +

+ +
+ +

+ AI Platform +

+ +Get the breadth of a managed AI platform with control over the infrastructure, +data and extension points. Use it behind customer products, internal systems, +and regulated deployments without handing the AI control plane to a managed +provider. + +## A complete platform + +- Agent builder and runtime +- Multi-provider model gateway +- Knowledge ingestion and retrieval +- More than 200 typed integrations +- MCP, OpenAPI, GraphQL and code tools +- Sandboxed code and shell execution +- Web widgets, portals and messaging channels +- REST and GraphQL APIs, webhooks and generated client types +- Node.js, Python and Go SDKs and a Terraform provider +- Authentication, users, teams, contacts and multi-tenant identity +- Access control, moderation, PII protection and audit +- Traces, events, ratings, usage and operational logs +- Replaceable database, storage, cache, queue and vector infrastructure + +## Run it + +Run the complete prebuilt stack with one command - no checkout, no build: + +```bash +docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest up +``` + +Open . Sign in with any email address and read the +six-digit code from the platform container log: + +```bash +docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest logs platform +``` + +See [Deployment](./docs/deployment.md) for details. + +## Local development + +Binary assets are stored with Git LFS, so install it (`git lfs install`) +before cloning. From a fresh checkout, run + +```bash +docker compose up +``` + +Open . See +[Getting started](./docs/getting-started.md) for host-side development, +storage configuration and the first model connection. + +## Documentation + +- [Getting started](./docs/getting-started.md) +- [Deployment and production status](./docs/deployment.md) +- [Module defaults](./docs/module-defaults.md) +- [Operator configuration](./docs/configuration.md) +- [Architecture and repository map](./docs/architecture.md) +- [Licensing](./LICENSING.md) +- [Contributing](./CONTRIBUTING.md) +- [Security](./SECURITY.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3d8da84 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Security Policy + +## Reporting a Vulnerability + +Do not report security vulnerabilities in a public issue or discussion. + +Email `contact@cbk.ai` with `Security report` in the subject. Include the +affected component, reproduction steps, potential impact, and any suggested +mitigation. Avoid including sensitive customer data unless it is necessary to +understand the issue. + +We will acknowledge the report as soon as practical, investigate it, and +coordinate disclosure after a fix or mitigation is available. + +## Scope + +This policy covers source maintained in this repository. Product account, +billing, and general support requests should follow [SUPPORT.md](./SUPPORT.md). diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..a5ee489 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,24 @@ +# Support + +## Source Code Questions + +Use a GitHub issue for reproducible defects, documentation problems, and +well-scoped feature proposals related to this repository. Search for an +existing issue before opening a new one and include enough detail to reproduce +the behavior. + +## Product and Account Support + +For the hosted ChatBotKit product, account, and billing support, email +`support@chatbotkit.com`. Do not include API keys, passwords, or other secrets. + +## Security Issues + +Security vulnerabilities must be reported privately according to +[SECURITY.md](./SECURITY.md), not through a public support issue. + +## Private Operations + +The maintainers cannot provide public access to production environments, +customer records, internal logs, or private operational systems when helping +with a source code issue. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..921f6fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,267 @@ +# ============================================================================= +# ChatBotKit Platform - full local stack in one command +# ============================================================================= +# Three ways to use it - no mode ever builds or starts another mode's +# services: +# +# docker compose up # THE DEFAULT: a ready dev +# # server. The checkout is +# # copied into a container, +# # installed, provisioned +# # (SQLite), and `next dev` +# # runs against redis - hot +# # reload, no image build +# docker compose --profile distro up --build platform +# # the full compiled stack. +# # WARNING: this compiles the +# # entire platform - expect +# # 10-15+ minutes on first +# # build depending on hardware +# docker compose --profile distro up --no-build --pull always platform +# # pull the prebuilt community +# # app and initializer images +# docker compose up redis # backing services only, +# # then `pnpm dev` on the host +# +# The dev server mounts the checkout READ-ONLY and copies it into the +# container, installing there - the host tree is never written, so it runs +# identically from a plain checkout and when nested in another workspace. The +# .devcontainer provides the host-side toolchain and works with all three. +# +# The end-user distribution - the image-only stack published as a Compose OCI +# artifact (`docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest +# up -d`) - lives separately in docker/distro/, one folder per package flavor. +# ============================================================================= + +# @note the storage environment every application service shares. The store +# is Garage, spoken to over the plain S3 protocol - see docker/garage/. +# The credentials are the known development values garage-init provisions. +# +# @note presigned upload/download URLs carry this endpoint, so a browser on +# the host needs to resolve it too: add `127.0.0.1 garage` to /etc/hosts to +# use browser-facing file flows with the containerized modes. (When running +# `pnpm dev` on the host instead, point SERVICE_AWS_ENDPOINT at +# http://localhost:3900 in platform/.env and the question does not arise.) +x-storage-env: &storage-env + SERVICE_AWS_ENDPOINT: http://garage:3900 + SERVICE_AWS_REGION: garage + SERVICE_AWS_ACCESS_KEY_ID: GK31e57eba9df26b2e7e1b0eaa + SERVICE_AWS_SECRET_ACCESS_KEY: 9f3c1e2b8a4d5f6071829304a5b6c7d8e9f00112233445566778899aabbccdde + SERVICE_AWS_FORCE_PATH_STYLE: 'true' + FILE_S3_BUCKET_NAME: file + IMAGE_S3_BUCKET_NAME: image + VIDEO_S3_BUCKET_NAME: video + AUDIO_S3_BUCKET_NAME: audio + CONVERSATION_S3_BUCKET_NAME: conversation + NAMESPACE_S3_BUCKET_NAME: namespace + SESSION_S3_BUCKET_NAME: session + SPACE_S3_BUCKET_NAME: space + TEMP_S3_BUCKET_NAME: temp + OUTPUT_S3_BUCKET_NAME: output + +services: + platform: + profiles: ['distro'] + image: ${PLATFORM_IMAGE:-ghcr.io/chatbotkit/platform-community-app:next} + build: + context: . + dockerfile: docker/Dockerfile + args: + # @note override for a real deployment: the site url is baked into the + # build by Next.js + SITE_URL: ${SITE_URL:-http://localhost:3000} + ports: + - '3000:3000' + environment: + <<: *storage-env + NODE_ENV: production + PORT: 3000 + SITE_URL: ${SITE_URL:-http://localhost:3000} + NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000} + # @note left empty, the image generates these secrets on first boot and + # persists them in the platform-data volume - see docker/entrypoint.sh; + # set explicitly to override + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-} + QUEUE_SECRET: ${QUEUE_SECRET:-} + JWT_TOKEN_SECRET_KEY: ${JWT_TOKEN_SECRET_KEY:-} + PRISMA_DATABASE_URL: file:/data/chatbotkit.db + # @note optional: encrypts stored credentials in the database; unset, + # they are stored as given. See docs/configuration.md, "Encryption at rest" + PRISMA_FIELD_ENCRYPTION_KEY: ${PRISMA_FIELD_ENCRYPTION_KEY:-} + REDIS_URL: redis://redis:6379 + QDRANT_URL: http://qdrant:6333 + # @note extra configuration (provider keys and the like) goes here; the + # file is optional. Values can also be persisted in the platform-data + # volume with `docker compose run --rm --no-deps platform setup`; the + # environment and this file win over them - see docker/entrypoint.sh + env_file: + - path: .env + required: false + volumes: + - platform-data:/data + depends_on: + db-init: + condition: service_completed_successfully + redis: + condition: service_healthy + qdrant: + condition: service_healthy + garage-init: + condition: service_completed_successfully + restart: unless-stopped + healthcheck: + # Use node for the healthcheck since the alpine image has no wget/curl + test: + [ + 'CMD', + 'node', + '-e', + "fetch('http://localhost:3000/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s + + db-init: + profiles: ['distro'] + image: ${PLATFORM_INIT_IMAGE:-ghcr.io/chatbotkit/platform-community-init:next} + build: + context: . + dockerfile: docker/Dockerfile + target: initializer + environment: + PRISMA_DATABASE_URL: file:/data/chatbotkit.db + volumes: + - platform-data:/data + restart: 'no' + + dev: + image: mcr.microsoft.com/devcontainers/javascript-node:24-bookworm + working_dir: /workspace + # @note the checkout is mounted READ-ONLY at /src and copied into the + # container, where the install lives - the host tree is never written. A + # background loop re-syncs edits every 2 seconds for hot reload. + # Provisioning (.env from the example, SQLite client and schema) happens + # here in the compose file; the application itself has no provisioning + # logic. `corepack pnpm` dispatches the exact version pinned in + # package.json's packageManager field, immune to the image's own pnpm. + command: + - bash + - -lc + - >- + command -v rsync >/dev/null || { apt-get update -qq && apt-get install -y -qq rsync >/dev/null; }; + SYNC="rsync -a --delete + --exclude node_modules --exclude .next --exclude .turbo + --exclude .dev --exclude .env --exclude .pnpm-store + --exclude .git --exclude prisma/generated --exclude prisma/zod + /src/ /workspace/"; + $$SYNC && + corepack pnpm install && + { [ -f platform/.env ] || cp platform/.env.example platform/.env; } && + sed -i "s|^PRISMA_DATABASE_URL=.*|PRISMA_DATABASE_URL=file:/workspace/platform/.dev/platform.db|" platform/.env && + { ls node_modules/.pnpm/@pothos+plugin-prisma*/node_modules/@pothos/plugin-prisma/generated.js >/dev/null 2>&1 || corepack pnpm -F @chatbotkit-dev/db db:gen; } && + { [ -f platform/.dev/platform.db ] || { mkdir -p platform/.dev && PRISMA_DATABASE_URL=file:/workspace/platform/.dev/platform.db corepack pnpm -F @chatbotkit-dev/db db:push; }; } && + { while true; do sleep 2; $$SYNC; done & } && + corepack pnpm -F @chatbotkit/platform dev + environment: + <<: *storage-env + REDIS_URL: redis://redis:6379 + QDRANT_URL: http://qdrant:6333 + APP_PORT: 8080 + # @note the store lives in a volume so installs stay warm across + # container recreations + NPM_CONFIG_STORE_DIR: /pnpm-store + volumes: + - .:/src:ro + - dev-workspace:/workspace + - dev-pnpm-store:/pnpm-store + ports: + - '127.0.0.1:8080:8080' + depends_on: + redis: + condition: service_healthy + qdrant: + condition: service_healthy + garage-init: + condition: service_completed_successfully + + redis: + image: redis:7-alpine + command: ['redis-server', '--appendonly', 'yes'] + ports: + # @note published on localhost only, so `pnpm dev` on the host can use + # this same Redis (REDIS_URL=redis://localhost:6379) without building + # anything + - '127.0.0.1:6379:6379' + volumes: + - redis-data:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + qdrant: + image: qdrant/qdrant:v1.15.1 + ports: + # @note published on localhost only, so `pnpm dev` on the host can use + # this same Qdrant (QDRANT_URL=http://localhost:6333) without building + # anything + - '127.0.0.1:6333:6333' + volumes: + - qdrant-data:/qdrant/storage + healthcheck: + # @note the qdrant image ships no curl or wget, so the check is bash + # opening a TCP connection to the HTTP port + test: ['CMD-SHELL', 'bash -c ": > /dev/tcp/127.0.0.1/6333" || exit 1'] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + garage: + image: dxflrs/garage:v2.1.0 + ports: + # @note published on localhost only, so `pnpm dev` on the host can use + # this same store (SERVICE_AWS_ENDPOINT=http://localhost:3900) without + # building anything + - '127.0.0.1:3900:3900' + volumes: + - ./docker/garage/garage.toml:/etc/garage.toml:ro + - garage-data:/var/lib/garage + healthcheck: + # @note the image is a bare binary - no shell - so the check is the + # garage CLI talking to the daemon over RPC + test: ['CMD', '/garage', '-c', '/etc/garage.toml', 'status'] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + garage-init: + # @note one-shot: provisions the single-node layout, the development + # access key and one bucket per storage scope through Garage's admin API. + # Idempotent, so it reruns harmlessly on every `up`. + image: node:24.20.0-alpine + command: ['node', '/init.mjs'] + environment: + GARAGE_ADMIN_URL: http://garage:3903 + GARAGE_ADMIN_TOKEN: dev-admin-token + STORAGE_ACCESS_KEY_ID: GK31e57eba9df26b2e7e1b0eaa + STORAGE_SECRET_ACCESS_KEY: 9f3c1e2b8a4d5f6071829304a5b6c7d8e9f00112233445566778899aabbccdde + volumes: + - ./docker/garage/init.mjs:/init.mjs:ro + depends_on: + garage: + condition: service_healthy + restart: 'no' + +volumes: + platform-data: + redis-data: + qdrant-data: + garage-data: + dev-pnpm-store: + dev-workspace: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..3177baf --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,206 @@ +# ============================================================================= +# ChatBotKit Platform Image (standalone) +# ============================================================================= +# Builds the platform application from this folder alone, using this folder's +# own workspace manifests and lockfile - it does not reach into any parent +# repository, so it works identically in the public repository and inside the +# monorepo. +# +# In this context every `@chatbotkit-dev/*` module resolves to the open +# default implementation in ./packages: SQLite for the database, the +# in-process or Redis key-value store, and so on. The docker-compose.yml next +# to this file wires the full stack together; to build by hand: +# +# docker build -f Dockerfile -t chatbotkit-platform . # from this folder +# +# This is the only container image. A hosted-deployment image with the +# internal module overrides would be a deliberate variant of this file built +# from the monorepo root, not a separate lineage. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Stage 0: Base +# ----------------------------------------------------------------------------- + +FROM node:24.20.0-alpine AS base + +# Required system dependencies for native modules (better-sqlite3 builds from +# source on alpine) +RUN apk add --no-cache libc6-compat python3 make g++ + +# Enable the pnpm version required by package.json's packageManager field. +RUN corepack enable && corepack prepare pnpm@11.24.0 --activate + +WORKDIR /app + +# ----------------------------------------------------------------------------- +# Stage 1: Workspace dependencies +# ----------------------------------------------------------------------------- + +FROM base AS dependencies + +# @note image builds are non-interactive: pnpm must auto-confirm prompts +# (e.g. recreating the modules directory when hoist settings change) instead +# of aborting for want of a TTY +ENV CI=true + +WORKDIR /app + +# Workspace configuration first, for layer caching +COPY pnpm-workspace.yaml pnpm-lock.yaml package.json .pnpmfile.cjs turbo.json ./ +COPY patches ./patches/ +COPY stubs ./stubs/ + +# @note the one local file: dependency that lives inside the application +# source - fetch scans local dependencies, so it must exist before fetch runs +COPY platform/eslint ./platform/eslint/ + +# @note fetch resolves from the lockfile alone, so editing source does not +# invalidate the dependency layer - the install below links offline from the +# fetched store +RUN pnpm fetch + +# Workspace members +COPY packages ./packages/ +COPY platform ./platform/ + +RUN pnpm install --frozen-lockfile --offline + +# ----------------------------------------------------------------------------- +# Stage 2: Application builder +# ----------------------------------------------------------------------------- + +FROM dependencies AS builder + +ENV NEXT_OUTPUT_MODE=standalone +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +# @note the Next.js build needs far more heap than node's ~4GB default - the +# hosted CI grants it 80% of a heavy runner's memory. Lower this only if the +# build host must, and expect the build to fail under ~8GB. +ARG NODE_HEAP_MB=16384 + +# @note the emfile guard makes fs.promises retry when the descriptor table +# is full - next's trace collection otherwise dies with EMFILE on hosts with +# modest ulimits, even after the raise below +ENV NODE_OPTIONS="--max-old-space-size=$NODE_HEAP_MB --require /app/platform/scripts/emfile-guard.cjs" + +# @note the build asserts a site url; override for a real deployment. Secrets +# should NOT be passed as build args - use BuildKit secret mounts. +ARG SITE_URL=http://localhost:3000 +ENV SITE_URL=$SITE_URL + +# @note source maps ship without source content by default; pass 'full' +# explicitly to embed the source for debuggable self-hosted images +ARG BUILD_SOURCEMAPS=nosources +ENV BUILD_SOURCEMAPS=$BUILD_SOURCEMAPS + +# @note build-time database: the graphql codegen and static generation +# construct the client, so they need a real (empty) schema to point at. This +# file never leaves the builder stage - the runtime database comes from +# PRISMA_DATABASE_URL at run time (the compose file sets it to the volume). +ENV PRISMA_DATABASE_URL=file:/tmp/build.db +RUN pnpm -F @chatbotkit-dev/db db:push + +# @note unit tests, lint and type validation run as separate CI gates rather +# than during image assembly. Keep the image build focused on producing the +# deployable artifact from an already-validated checkout. +ENV SKIP_BUILD_TESTS=true +ENV SKIP_LINT=1 +ENV SKIP_CHECK=1 + +WORKDIR /app/platform + +# @note the build imports every route module while collecting page data, and +# some modules assert their configuration at import. The example env provides +# documented placeholder values for exactly that; the runtime environment +# comes from the compose file and .env at run time, never from here. +RUN cp .env.example .env + +# @note the cache mount persists Next's incremental compiler cache across +# image rebuilds on the build host, so a source edit costs an incremental +# build rather than a cold one (BuildKit required, which is docker's default) +# @note next's build tracing opens thousands of files concurrently; raise +# the descriptor limit to the hard maximum or the trace dies with EMFILE +RUN --mount=type=cache,target=/app/platform/.next/cache,uid=0,gid=0 ulimit -n $(ulimit -H -n) && pnpm build + +# @note Next copies the builder's .env into the standalone output, where its +# documented placeholder values would silently backfill anything the runtime +# environment leaves unset. Strip it so a missing variable fails loudly at +# boot instead. +RUN rm -f .next/standalone/platform/.env .next/standalone/platform/.env.production + +# ----------------------------------------------------------------------------- +# Stage 3: Database initializer +# ----------------------------------------------------------------------------- + +# Produce a portable package containing the database implementation, schema +# tooling and workspace dependencies, without carrying the application build +# or pnpm store into the published initializer image. +FROM dependencies AS initializer-deployer + +WORKDIR /app + +RUN pnpm --filter @chatbotkit-dev/db deploy --legacy /initializer + +FROM node:24.20.0-alpine AS initializer + +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +COPY --from=initializer-deployer /initializer ./ + +# @note the Garage provisioning script rides along so the distribution stack +# (docker/distro/*) can run it from this image instead of bind-mounting the +# checkout - a published Compose OCI artifact cannot carry bind mounts +COPY docker/garage/init.mjs /garage-init.mjs + +RUN mkdir -p /data + +# @note schema derivation writes inside the installed database package, so the +# one-shot initializer runs as root. The final chown hands SQLite files to the +# uid/gid used by the non-root application image. Network databases ignore the +# otherwise empty /data directory. +CMD ["sh", "-c", "npm run db:push && chown -R 1001:1001 /data"] + +# ----------------------------------------------------------------------------- +# Stage 4: Application +# ----------------------------------------------------------------------------- + +FROM node:24.20.0-alpine AS application + +WORKDIR /app + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# Standalone output preserves the workspace structure (platform/...) +COPY --from=builder --chown=nextjs:nodejs /app/platform/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/platform/public ./platform/public +COPY --from=builder --chown=nextjs:nodejs /app/platform/.next/static ./platform/.next/static + +RUN mkdir -p platform/.next +RUN chown nextjs:nodejs platform/.next + +# Writable location for the SQLite database volume +RUN mkdir -p /data +RUN chown nextjs:nodejs /data + +# @note generates missing secrets on first boot - see docker/entrypoint.sh; +# a no-op for deployments that set them +COPY --chmod=755 docker/entrypoint.sh /entrypoint.sh + +USER nextjs + +EXPOSE 3000 + +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["node", "platform/server.js"] diff --git a/docker/distro/AGENTS.md b/docker/distro/AGENTS.md new file mode 100644 index 0000000..a456d3d --- /dev/null +++ b/docker/distro/AGENTS.md @@ -0,0 +1,60 @@ +# Distribution stacks + +One folder per package flavor. Each folder holds a self-contained, image-only +`compose.yml` that CI publishes as a Compose OCI artifact to +`ghcr.io/chatbotkit/platform-`, digest-pinned to the matching +application and initializer images. Consumers run the whole stack with: + +```bash +docker compose -f oci://ghcr.io/chatbotkit/platform-:latest up -d +``` + +## Naming + +- One grammar for every published package: + `platform-[-]:>`. The name says + what it is; the tag says which build it is. +- The bare `platform-` name is the Compose distribution artifact - + the one consumers type. Its component images are `platform--app` + and `platform--init` (e.g. `platform-community-app:next`). +- Tags are single-axis: channels `next`, `main` and `latest` (follows + `main`), plus immutable `sha-` tags on the component images. +- Never mix flavors or revisions between the application and initializer + images inside one stack - schema and client must come from the same build. + +## Hard constraints + +- **No bind mounts, no `build:`, no profiles.** `docker compose publish` + rejects bind mounts, and plain `up -d` must start the stack. Configuration + is inlined via the top-level `configs` element; scripts ship inside + published images (the Garage provisioning script rides in the initializer + image as `/garage-init.mjs`). +- Inline `configs.content` is interpolated: `${...}` must be escaped as + `$${...}`. This is why scripts are baked into images instead of inlined. +- The inline Garage configuration duplicates `docker/garage/garage.toml` - + keep them in sync. +- `env_file` entries are dropped at publish; operator overrides flow through + interpolated variables (`${VAR:-default}`), which Compose presents to the + consumer as a confirmation table on `up`. + +## Adding a flavor + +1. A flavor is a compile-time package selection (e.g. the database package), + not a runtime service swap. Add the Docker build path for it first (see + the note in `docker/Dockerfile` and the matrix comment in the deploy + workflow). +2. Create `docker/distro//compose.yml`, starting from `community/`. + Swap only what the flavor changes (e.g. a `postgres` service replacing the + SQLite volume); keep service names, healthchecks and the variable surface + consistent. +3. Add the flavor to the matrix in + `.github/workflows/publish-ghcr-platform.yaml` with its application and + initializer targets. Everything downstream - images, smoke test, artifact + publish - is parameterized on `matrix.flavor` and needs no other change. +4. Verify before relying on CI: `docker compose -f + docker/distro//compose.yml config --quiet`, then publish to a + scratch registry (ttl.sh works) and boot the artifact from an empty + directory with `docker compose -f oci://... up -d -y`. + +Do not create a flavor folder before the flavor's image build exists - the +convention is the structure; empty placeholders are not. diff --git a/docker/distro/community/compose.yml b/docker/distro/community/compose.yml new file mode 100644 index 0000000..a6744a8 --- /dev/null +++ b/docker/distro/community/compose.yml @@ -0,0 +1,231 @@ +# ============================================================================= +# ChatBotKit Platform - community distribution stack +# ============================================================================= +# The self-contained, image-only Compose application published to +# ghcr.io/chatbotkit/platform-community as an OCI artifact. Consumers run the +# whole platform - application, database initialization, Redis, Qdrant and +# Garage object storage - with a single command and no checkout: +# +# docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest up +# +# The same file runs directly from the repository: +# +# docker compose -f docker/distro/community/compose.yml up -d +# +# Everything a service needs travels inside this file or inside a published +# image: configuration is inlined through the top-level `configs` element and +# the Garage provisioning script ships inside the initializer image, so there +# are no bind mounts - the one restriction `docker compose publish` enforces. +# The developer stack with profiles, source builds and hot reload lives in +# docker-compose.yml at the repository root; this file never builds anything. +# +# One folder per package flavor lives under docker/distro/. Each publishes as +# ghcr.io/chatbotkit/platform-, pinned by CI to the matching +# application and initializer image digests of the same flavor. +# ============================================================================= + +# @note the storage environment every application service shares. The store +# is Garage, spoken to over the plain S3 protocol. Left empty, the access key +# is generated by Garage on first boot and persisted in the platform-data +# volume (garage-init provisions it, the application entrypoint sources it); +# set STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEY to use a fixed pair. +# +# @note presigned upload/download URLs carry this endpoint, so a browser on +# the host needs to resolve it too: add `127.0.0.1 garage` to /etc/hosts to +# use browser-facing file flows. +x-storage-env: &storage-env + SERVICE_AWS_ENDPOINT: http://garage:3900 + SERVICE_AWS_REGION: garage + SERVICE_AWS_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-} + SERVICE_AWS_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY:-} + SERVICE_AWS_FORCE_PATH_STYLE: 'true' + FILE_S3_BUCKET_NAME: file + IMAGE_S3_BUCKET_NAME: image + VIDEO_S3_BUCKET_NAME: video + AUDIO_S3_BUCKET_NAME: audio + CONVERSATION_S3_BUCKET_NAME: conversation + NAMESPACE_S3_BUCKET_NAME: namespace + SESSION_S3_BUCKET_NAME: session + SPACE_S3_BUCKET_NAME: space + TEMP_S3_BUCKET_NAME: temp + OUTPUT_S3_BUCKET_NAME: output + +services: + platform: + image: ${PLATFORM_IMAGE:-ghcr.io/chatbotkit/platform-community-app:next} + ports: + - '3000:3000' + environment: + <<: *storage-env + NODE_ENV: production + PORT: 3000 + SITE_URL: ${SITE_URL:-http://localhost:3000} + NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000} + # @note left empty, the image generates these secrets on first boot and + # persists them in the platform-data volume; set explicitly to override + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-} + QUEUE_SECRET: ${QUEUE_SECRET:-} + JWT_TOKEN_SECRET_KEY: ${JWT_TOKEN_SECRET_KEY:-} + PRISMA_DATABASE_URL: file:/data/chatbotkit.db + # @note optional: encrypts stored credentials in the database; unset, + # they are stored as given. See docs/configuration.md, "Encryption at rest" + PRISMA_FIELD_ENCRYPTION_KEY: ${PRISMA_FIELD_ENCRYPTION_KEY:-} + # @note optional platform-wide provider keys; users can also supply + # their own keys through the application itself. Any variable the + # application honours (further providers, the *_CONFIG seams - see + # docs/configuration.md) can be persisted in the platform-data volume + # instead of a .env file, prompted for or set directly: + # docker compose -f oci://... run --rm --no-deps platform setup + # docker compose -f oci://... run --rm --no-deps platform setup OPENROUTER_MODELS_API_KEY=... + # Values given here or in .env win over persisted ones - see + # docker/entrypoint.sh. An override file remains the other route: + # docker compose -f oci://... -f my-override.yml up -d + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + OPENROUTER_MODELS_API_KEY: ${OPENROUTER_MODELS_API_KEY:-} + VERCEL_MODELS_API_KEY: ${VERCEL_MODELS_API_KEY:-} + REDIS_URL: redis://redis:6379 + QDRANT_URL: http://qdrant:6333 + volumes: + - platform-data:/data + depends_on: + db-init: + condition: service_completed_successfully + redis: + condition: service_healthy + qdrant: + condition: service_healthy + garage-init: + condition: service_completed_successfully + restart: unless-stopped + healthcheck: + # Use node for the healthcheck since the alpine image has no wget/curl + test: + [ + 'CMD', + 'node', + '-e', + "fetch('http://localhost:3000/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s + + db-init: + image: ${PLATFORM_INIT_IMAGE:-ghcr.io/chatbotkit/platform-community-init:next} + environment: + PRISMA_DATABASE_URL: file:/data/chatbotkit.db + volumes: + - platform-data:/data + restart: 'no' + + redis: + image: redis:7-alpine + command: ['redis-server', '--appendonly', 'yes'] + volumes: + - redis-data:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + qdrant: + image: qdrant/qdrant:v1.15.1 + volumes: + - qdrant-data:/qdrant/storage + healthcheck: + # @note the qdrant image ships no curl or wget, so the check is bash + # opening a TCP connection to the HTTP port + test: ['CMD-SHELL', 'bash -c ": > /dev/tcp/127.0.0.1/6333" || exit 1'] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + garage: + image: dxflrs/garage:v2.1.0 + ports: + # @note published on localhost so presigned URLs (which carry the + # garage:3900 endpoint) work from a host browser with the /etc/hosts + # entry described above + - '127.0.0.1:3900:3900' + configs: + - source: garage-config + target: /etc/garage.toml + volumes: + - garage-data:/var/lib/garage + healthcheck: + # @note the image is a bare binary - no shell - so the check is the + # garage CLI talking to the daemon over RPC + test: ['CMD', '/garage', '-c', '/etc/garage.toml', 'status'] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + garage-init: + # @note one-shot: provisions the single-node layout, the development + # access key and one bucket per storage scope through Garage's admin API. + # Idempotent, so it reruns harmlessly on every `up`. The script ships + # inside the initializer image (the same one db-init runs), so this stack + # needs no bind mount into a checkout. + image: ${PLATFORM_INIT_IMAGE:-ghcr.io/chatbotkit/platform-community-init:next} + command: ['node', '/garage-init.mjs'] + environment: + GARAGE_ADMIN_URL: http://garage:3903 + GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN:-dev-admin-token} + STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-} + STORAGE_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY:-} + volumes: + # @note shares the application volume so the generated access key + # persists where the application entrypoint can source it + - platform-data:/data + depends_on: + garage: + condition: service_healthy + restart: 'no' + +configs: + # @note the inline copy of docker/garage/garage.toml, embedded so the + # published OCI artifact is self-contained. Keep the two in sync. + garage-config: + content: | + # Garage (S3-compatible object storage) - single-node configuration. + # + # WARNING: the rpc_secret and admin token default to known development + # values. Neither port is published outside the compose network, but a + # hardened setup overrides them (GARAGE_RPC_SECRET, `openssl rand -hex + # 32`, and GARAGE_ADMIN_TOKEN) - and a real deployment almost certainly + # runs a real store with replication rather than this single-node + # layout. + + metadata_dir = "/var/lib/garage/meta" + data_dir = "/var/lib/garage/data" + db_engine = "sqlite" + + replication_factor = 1 + + rpc_bind_addr = "[::]:3901" + rpc_public_addr = "127.0.0.1:3901" + rpc_secret = "${GARAGE_RPC_SECRET:-1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec}" + + [s3_api] + # @note the region is part of every SigV4 signature: SERVICE_AWS_REGION + # must match it, or every request fails authentication + s3_region = "garage" + api_bind_addr = "[::]:3900" + root_domain = ".s3.garage.localhost" + + [admin] + # @note the garage-init service provisions the layout, the access key + # and the buckets through this API + api_bind_addr = "[::]:3903" + admin_token = "${GARAGE_ADMIN_TOKEN:-dev-admin-token}" + +volumes: + platform-data: + redis-data: + qdrant-data: + garage-data: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..a1acdcb --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,171 @@ +#!/bin/sh +set -e + +# Configuration precedence, highest first: +# 1. the container environment - Compose `environment:`, `env_file:`, +# `docker run -e`, shell exports Compose interpolates +# 2. $DATA_DIR/config.env - operator values persisted in the data volume, +# written by `setup` below or by hand; one KEY=VALUE per line, no quoting +# 3. $DATA_DIR/.secrets.env - values generated on first boot +# An empty environment value counts as unset, so a Compose default such as +# `${OPENAI_API_KEY:-}` never masks a persisted value. + +DATA_DIR="${PLATFORM_DATA_DIR:-/data}" +CONFIG_FILE="$DATA_DIR/config.env" +SECRETS_FILE="$DATA_DIR/.secrets.env" +STORAGE_FILE="$DATA_DIR/.storage.env" + +DEFAULT_SETUP_KEYS="OPENAI_API_KEY OPENROUTER_MODELS_API_KEY VERCEL_MODELS_API_KEY" + +is_empty() { + eval "[ -z \"\${$1}\" ]" +} + +is_valid_key() { + case "$1" in + '' | [0-9]* | *[!A-Za-z0-9_]*) return 1 ;; + esac +} + +config_get() { + [ -f "$CONFIG_FILE" ] || return 0 + sed -n "s/^$1=//p" "$CONFIG_FILE" | tail -n 1 +} + +# config_set KEY VALUE - an empty VALUE removes the entry +config_set() { + umask 077 + mkdir -p "$DATA_DIR" + { + if [ -f "$CONFIG_FILE" ]; then + grep -v "^$1=" "$CONFIG_FILE" || true + fi + if [ -n "$2" ]; then + printf '%s=%s\n' "$1" "$2" + fi + } > "$CONFIG_FILE.tmp" + mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" +} + +config_load() { + [ -f "$CONFIG_FILE" ] || return 0 + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in '' | '#'*) continue ;; esac + key="${line%%=*}" + value="${line#*=}" + is_valid_key "$key" || continue + if is_empty "$key"; then + export "$key=$value" + fi + done < "$CONFIG_FILE" +} + +# read_secret KEY - prompts on the terminal without echo; Enter keeps the +# current value, a single "-" clears it +read_secret() { + current="$(config_get "$1")" + if [ -n "$current" ]; then + hint="currently set, ends with ${current#"${current%????}"}" + else + hint="not set" + fi + printf '%s [%s]: ' "$1" "$hint" >&2 + if [ -t 0 ]; then + trap 'stty echo; exit 130' INT TERM + stty -echo + IFS= read -r input + stty echo + trap - INT TERM + echo >&2 + else + IFS= read -r input + fi + case "$input" in + '') ;; + '-') config_set "$1" '' ; echo "$1 cleared" >&2 ;; + *) config_set "$1" "$input" ; echo "$1 saved" >&2 ;; + esac +} + +# setup [KEY | KEY=VALUE ...] - persists operator values in $CONFIG_FILE. +# Bare keys prompt; KEY=VALUE sets without prompting (KEY= removes). With no +# arguments the default provider keys are prompted for. +run_setup() { + [ $# -gt 0 ] || set -- $DEFAULT_SETUP_KEYS + for arg in "$@"; do + key="${arg%%=*}" + if ! is_valid_key "$key"; then + echo "ERROR: invalid variable name: $key" >&2 + exit 1 + fi + case "$arg" in + *=*) config_set "$key" "${arg#*=}" ; echo "$key saved" >&2 ;; + *) + if [ ! -t 0 ]; then + echo "ERROR: no terminal to prompt for $key - pass $key=VALUE or run with -it" >&2 + exit 1 + fi + read_secret "$key" + ;; + esac + done + echo "INFO: values persisted in $CONFIG_FILE - restart the platform service to apply" >&2 +} + +if [ "$1" = "setup" ]; then + shift + run_setup "$@" + exit 0 +fi + +config_load + +# Fills empty NEXTAUTH_SECRET / QUEUE_SECRET / JWT_TOKEN_SECRET_KEY with values +# generated once and persisted in $DATA_DIR, so sessions, queue signatures and +# issued tokens survive restarts as long as it is a volume. +if [ -z "$NEXTAUTH_SECRET" ] || [ -z "$QUEUE_SECRET" ] || [ -z "$JWT_TOKEN_SECRET_KEY" ]; then + generate_secret() { + node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("hex"))' + } + + if [ ! -f "$SECRETS_FILE" ]; then + umask 077 + printf 'GENERATED_NEXTAUTH_SECRET=%s\nGENERATED_QUEUE_SECRET=%s\n' "$(generate_secret)" "$(generate_secret)" > "$SECRETS_FILE" + fi + + . "$SECRETS_FILE" + + # @note volumes created before the JWT secret was generated here lack it + if [ -z "$GENERATED_JWT_TOKEN_SECRET_KEY" ]; then + GENERATED_JWT_TOKEN_SECRET_KEY="$(generate_secret)" + printf 'GENERATED_JWT_TOKEN_SECRET_KEY=%s\n' "$GENERATED_JWT_TOKEN_SECRET_KEY" >> "$SECRETS_FILE" + fi + + if [ -z "$NEXTAUTH_SECRET" ]; then + echo "WARNING: NEXTAUTH_SECRET is not set - using a generated value persisted in $SECRETS_FILE" >&2 + export NEXTAUTH_SECRET="$GENERATED_NEXTAUTH_SECRET" + fi + + if [ -z "$QUEUE_SECRET" ]; then + echo "WARNING: QUEUE_SECRET is not set - using a generated value persisted in $SECRETS_FILE" >&2 + export QUEUE_SECRET="$GENERATED_QUEUE_SECRET" + fi + + if [ -z "$JWT_TOKEN_SECRET_KEY" ]; then + echo "WARNING: JWT_TOKEN_SECRET_KEY is not set - using a generated value persisted in $SECRETS_FILE" >&2 + export JWT_TOKEN_SECRET_KEY="$GENERATED_JWT_TOKEN_SECRET_KEY" + fi +fi + +# Storage credentials generated by garage-init land in the shared data +# volume - see docker/garage/init.mjs. +if [ -z "$SERVICE_AWS_ACCESS_KEY_ID" ] && [ -f "$STORAGE_FILE" ]; then + . "$STORAGE_FILE" + + echo "INFO: using generated storage credentials from $STORAGE_FILE" >&2 + + export SERVICE_AWS_ACCESS_KEY_ID="$GENERATED_STORAGE_ACCESS_KEY_ID" + export SERVICE_AWS_SECRET_ACCESS_KEY="$GENERATED_STORAGE_SECRET_ACCESS_KEY" +fi + +exec "$@" diff --git a/docker/garage/garage.toml b/docker/garage/garage.toml new file mode 100644 index 0000000..143961b --- /dev/null +++ b/docker/garage/garage.toml @@ -0,0 +1,36 @@ +# Garage (S3-compatible object storage) - single-node LOCAL configuration, +# used by the docker compose stack. The storage module speaks the S3 protocol, +# so any S3-compatible store works; Garage is the reference store the compose +# file stands up. +# +# WARNING: the rpc_secret and admin token below are known development values +# for the local stack only. A real deployment generates its own +# (`openssl rand -hex 32`) - and almost certainly runs a real store with +# replication rather than this single-node layout. +# +# @note the distribution stack carries an inline copy of this file (a +# published Compose OCI artifact cannot bind-mount it) - keep +# docker/distro/community/compose.yml's garage-config in sync. + +metadata_dir = "/var/lib/garage/meta" +data_dir = "/var/lib/garage/data" +db_engine = "sqlite" + +replication_factor = 1 + +rpc_bind_addr = "[::]:3901" +rpc_public_addr = "127.0.0.1:3901" +rpc_secret = "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec" + +[s3_api] +# @note the region is part of every SigV4 signature: SERVICE_AWS_REGION must +# match it, or every request fails authentication +s3_region = "garage" +api_bind_addr = "[::]:3900" +root_domain = ".s3.garage.localhost" + +[admin] +# @note the garage-init compose service provisions the layout, the access key +# and the buckets through this API +api_bind_addr = "[::]:3903" +admin_token = "dev-admin-token" diff --git a/docker/garage/init.mjs b/docker/garage/init.mjs new file mode 100644 index 0000000..39e7261 --- /dev/null +++ b/docker/garage/init.mjs @@ -0,0 +1,160 @@ +// @note one-shot provisioning for the local Garage store, run by the +// garage-init compose service against Garage's admin API: assign the +// single-node layout, provision the access key, create one bucket per +// storage scope and grant the key access to each. Idempotent - every step +// checks before it acts, so the stack restarts cleanly. +// +// The access key comes from STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEY +// when set; otherwise one is generated by Garage on first boot and persisted +// in /data (the volume shared with the application, whose entrypoint sources +// it - see docker/entrypoint.sh). +// +// The bucket list mirrors the scopes in @chatbotkit-dev/storage; the compose +// file carries the matching *_S3_BUCKET_NAME variables. +import { chownSync, readFileSync, writeFileSync } from 'node:fs' + +const base = process.env.GARAGE_ADMIN_URL +const token = process.env.GARAGE_ADMIN_TOKEN + +let accessKeyId = process.env.STORAGE_ACCESS_KEY_ID +let secretAccessKey = process.env.STORAGE_SECRET_ACCESS_KEY + +const SECRETS_FILE = '/data/.storage.env' + +const BUCKETS = [ + 'file', + 'image', + 'video', + 'audio', + 'conversation', + 'namespace', + 'session', + 'space', + 'temp', + 'output', +] + +for (const [name, value] of Object.entries({ + GARAGE_ADMIN_URL: base, + GARAGE_ADMIN_TOKEN: token, +})) { + if (!value) { + console.error(`[garage-init] ${name} is not set`) + process.exit(1) + } +} + +if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) { + console.error( + '[garage-init] STORAGE_ACCESS_KEY_ID and STORAGE_SECRET_ACCESS_KEY must be set together' + ) + + process.exit(1) +} + +async function api(path, body) { + const response = await fetch(base + path, { + ...(body && { method: 'POST', body: JSON.stringify(body) }), + headers: { + authorization: `Bearer ${token}`, + 'content-type': 'application/json', + }, + }) + + const text = await response.text() + + if (!response.ok) { + throw new Error(`${path} -> ${response.status}: ${text.slice(0, 200)}`) + } + + return text ? JSON.parse(text) : null +} + +// @note the compose healthcheck gates on the RPC port; give the admin API a +// short grace period of its own +let status + +for (let attempt = 0; ; attempt++) { + try { + status = await api('/v2/GetClusterStatus') + + break + } catch (error) { + if (attempt >= 30) { + throw error + } + + await new Promise((resolve) => setTimeout(resolve, 1000)) + } +} + +const node = status.nodes[0] + +if (!node.role) { + await api('/v2/UpdateClusterLayout', { + roles: [{ id: node.id, zone: 'dc1', capacity: 1000000000, tags: [] }], + }) + + const layout = await api('/v2/GetClusterLayout') + + await api('/v2/ApplyClusterLayout', { version: layout.version + 1 }) + + console.log('[garage-init] single-node layout applied') +} + +if (!accessKeyId) { + try { + const saved = readFileSync(SECRETS_FILE, 'utf8') + + accessKeyId = saved.match(/^GENERATED_STORAGE_ACCESS_KEY_ID=(.+)$/m)?.[1] + secretAccessKey = saved.match( + /^GENERATED_STORAGE_SECRET_ACCESS_KEY=(.+)$/m + )?.[1] + } catch { + // no persisted key yet - generate one below + } +} + +if (!accessKeyId) { + const key = await api('/v2/CreateKey', { name: 'platform' }) + + accessKeyId = key.accessKeyId + secretAccessKey = key.secretAccessKey + + writeFileSync( + SECRETS_FILE, + `GENERATED_STORAGE_ACCESS_KEY_ID=${accessKeyId}\nGENERATED_STORAGE_SECRET_ACCESS_KEY=${secretAccessKey}\n`, + { mode: 0o600 } + ) + + // @note the application image runs as uid/gid 1001 and must read the file + chownSync(SECRETS_FILE, 1001, 1001) + + console.log('[garage-init] access key generated') +} + +const keys = await api('/v2/ListKeys') + +if (!keys.some((key) => key.id === accessKeyId)) { + await api('/v2/ImportKey', { accessKeyId, secretAccessKey, name: 'platform' }) + + console.log('[garage-init] access key imported') +} + +const buckets = await api('/v2/ListBuckets') + +for (const alias of BUCKETS) { + let bucket = buckets.find((entry) => entry.globalAliases?.includes(alias)) + + if (!bucket) { + bucket = await api('/v2/CreateBucket', { globalAlias: alias }) + } + + await api('/v2/AllowBucketKey', { + bucketId: bucket.id, + accessKeyId, + permissions: { read: true, write: true, owner: true }, + }) +} + +console.log(`[garage-init] buckets ready: ${BUCKETS.join(' ')}`) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..2e23632 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,32 @@ +# Documentation + +The repository landing page explains what the platform is and who it is for. +The documents here cover the operational detail needed to run and evaluate it. + +## Manuals + +- [docs.cbk.ai](https://docs.cbk.ai) - the platform's technical manuals, + generated from this source: API, SDKs, integrations and every configurable + surface +- [chatbotkit.com/docs](https://chatbotkit.com/docs) - the user manuals for the + hosted ChatBotKit product built on the platform + +## Start here + +- [Getting started](./getting-started.md) - run the complete local stack, sign + in and configure a model +- [Deployment](./deployment.md) - Compose profiles, current production status + and operator responsibilities +- [Module defaults](./module-defaults.md) - what each public module does with + nothing set, and what the distribution flavors change + +## Project guides + +- [Architecture](./architecture.md) - application structure, module boundaries + and contribution branches +- [Configuration](./configuration.md) - operator-owned configuration seams and + database credential encryption +- [Contributing](../CONTRIBUTING.md) - development workflow, quality checks and + pull requests +- [Security](../SECURITY.md) - private vulnerability reporting +- [Support](../SUPPORT.md) - community and hosted-product support boundaries diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..be1b0f4 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,343 @@ +# Architecture + +This repository is a self-contained pnpm monorepo with two kinds of members: +the shared and swappable module packages under `packages/`, and the platform +application under `platform/`. This document explains how they fit together +and the conventions that are not self-evident from the tree. All paths below +are relative to the open-code repository root. + +Read this before concluding anything from the directory layout. The two most +common misreadings - that `pages/` and `app/` are a stalled router migration, +and that the flat `lib/` directory is disorganized - are both wrong, and both +are explained below. + +## Repository layout + +| Path | What it is | +| ----------- | ----------------------------------------------------------------- | +| `packages/` | The `@chatbotkit-dev/*` libraries and module contracts (`*-spec`) | +| `platform/` | The platform application, `@chatbotkit/platform` (Next.js) | +| `stubs/` | Local dependency shims applied at install time | +| `patches/` | pnpm dependency patches | + +The `pnpm-workspace.yaml`, lockfile and overrides in the repository root govern +dependency resolution across the application and its packages. + +## Branches + +The `next` branch is the development branch, and contributor pull requests +target it. The `main` branch is the stable release branch and advances through +a reviewed promotion from `next` after the required checks pass. See +`CONTRIBUTING.md` for the current contribution workflow. + +## Swappable modules + +The platform's deployment-specific behavior - configuration catalogues, +storage, email delivery, caching, database engine - lives behind swappable +modules. A swappable module is three packages, not one: + +| Package | Contains | +| ---------------------- | --------------------------------------------------------------------------------------------------------- | +| `packages/-spec` | The contract, plus shared schemas or derivation code where the contract requires them | +| `packages/` | The public default. Must boot with no configuration; a backing service may be required to use the feature | +| (a deployment's own) | An implementation installed over the public name | + +A deployment replaces a module by adding a pnpm override that resolves +`@chatbotkit-dev/` to its own implementation package. Remove the +override and the platform resolves to the public default and still runs - +that is the property every module preserves, and it is why a fresh checkout +of this repository boots with no deployment-specific or vendor configuration. +Booting is the guarantee, not every feature: the public storage module is an +S3-protocol client that needs an endpoint, credentials and buckets before file +flows work (the Compose stack provisions Garage), and the public queue is +immediate and non-durable - delays, retries and ordering are accepted and +ignored. + +[Module defaults](./module-defaults.md) lists the rest. + +The public defaults differ in what "default" means, and the difference is +deliberate. `@chatbotkit-dev/email` logs messages to the console - a working, +if noisy, delivery path. The plan catalogue (`@/config/limits`, read from the +LIMITS_CONFIG environment variable) defaults to empty, which the platform +reads as "this deployment has no plan concept": every entitlement resolves +without limits and no interface renders a plan name. Defaults describe a +working planless, vendor-free deployment, not a crippled one. + +Module conventions, enforced across the set: + +- Configuration is resolved lazily, on first use, never at module load - so + importing the platform never requires a vendor credential. +- Every module exposes an `assertConfigured` readiness check through the + entry point defined by its contract. The application exercises every + installed module in `platform/tests/config/providers.utest.js`, so the + build and CI fail before an incomplete deployment reaches first use. +- A module's README is the authoritative environment-variable reference. The + application's `.env.example` includes only the values useful for the + supported baseline and common local setup. +- Application code depends on the public package name and contract, never on + a deployment implementation. Runtime-specific behavior is generally + concentrated in in-tree adapters such as `lib/queue.ts`, `lib/storage.ts` + and `prisma/client.ts`. + +## The application + +### Two routers, by design + +`platform/pages/` contains the dashboard, product surfaces and the entire +public API under `pages/api/v1/`. It uses the Pages Router. + +`platform/app/` is the apps runtime, and almost nothing else: a root layout and +`app/apps/`. It uses the App Router because its manifest-driven model fits that +router's layout system. + +The two coexist permanently. This is not an unfinished migration: they are +different products sharing one codebase. The platform is a multi-page +application; the apps runtime is a family of focused applications (chat, code, +tasks, usage, and others), served path-based by default or through configured +application hosts. Manifests define the application, while deployment +configuration owns its host topology. + +### The apps runtime + +Every app is a directory under `app/apps/` carrying an `app.manifest` - a +JSON file whose shape is declared in `app/apps/app.manifest.d.ts`. The +minimal complete example: + +```json +{ + "start": "/apps/connect", + "name": "Connect", + "description": "Connect to your favorite apps and services", + "icon": "@lucide/grid-2x2-plus", + "order": 30, + "category": "main", + "config": {} +} +``` + +The parts of the contract that are not obvious: + +- Manifests are discovered at build time by `next.config.d/apps.config.js`, + which walks `app/apps/`, validates every manifest against a schema, and + generates the host routing from the result. The app's slug is its + directory name. +- `start` is the app's entry path. Host mappings do not live in the manifest; + the deployment's app and shell configuration decides whether that path is + served under the main site or a dedicated host. +- `global` is the app's baseline in every context. `config` supplies defaults + on the dashboard and the app's standalone host; a portal uses its own + global, app and user overlays. The complete precedence rules live in + `lib/app.router.app.config.ts`. +- `order` and `category` provide listing defaults. App-shell and portal + configuration can override them for a particular context. +- `category` is one of nine: `main`, `support`, `admin`, `user`, + `developer`, `help`, `other`, `lab`, `service`. + +Two kinds of app directory exist side by side: platform apps with descriptive +names (`chat/`, `code/`, `task/`) and apps under `(adhoc)/` with stable, +opaque 8-hex-digit slugs. "Ad-hoc" describes the route identity, not the app's +importance or maturity. The opaque slug avoids URL churn when a surface is +renamed or repositioned, while the manifest carries its human name and +category. + +A catch-all route (`app/apps/[...path]/route.ts`) claims any `/apps/*` path +no named app claims, and mounts portal static content at the root - which is +what lets a site authored for root deployment resolve its absolute resource +paths when served through a portal. + +### `lib/` - a tree encoded in filenames + +`lib/` is one flat directory, with a co-located test beside most source files. +The organization is in the filename: dot-separated prefixes encode a +two-to-three-level tree, so `action.exec.mcp.ts` reads as _action → execution +→ MCP handler_ and `model.provider.openai.ts` as _model → provider → +OpenAI_. + +The largest families are real architectural units. `model.*` is the model +catalogue and provider layer, with `model.provider.*` holding provider +integrations. `action.*` is the ability execution engine; each +`action.exec.*` file is one action runtime (fetch, shell, image, email, MCP, +and others) following a common internal structure: schemas, operation-name +constants, `do*` implementations, and an `execute*` router whose switch is +exhaustiveness-checked at compile time. +Other families mirror the domain model (`conversation.*`, `bot.*`, +`dataset.*`, `skillset.*`, `user.*`, `usage.*`, `limit.*`, `session.*`) and +the messaging integrations (`slack.*`, `twilio.*`, `telegram.*`, +`whatsapp.*`, `discord.*`, and others) - the flat directory doubles as the +integration registry. + +When adding a file, join an existing family if one fits; a new prefix is a +new subsystem and should be a deliberate choice. + +### `components/` and `hooks/` + +Both are flat like `lib/`, but use word-prefix namespacing instead of dots: +`components/` primarily holds one PascalCase component per file +(`BotBlockStatus.jsx`, `DatasetList.jsx`) with its test and, where useful, a +Storybook story beside it; `hooks/` holds one `useThing` hook per file, with +the same co-location. +The prefix families (`Bot*`, `Dataset*`, `Conversation*`, `Theme*`, +`useConversation*`, `useScroll*`) mirror the same domain nouns as `lib/` and +the database schema. + +UI code prefers plain HTML with the shared class vocabulary from +`styles/globals.css` (`default-button`, `primary-button`, `default-input`, +and friends) over bespoke styled components. + +### `config/` + +`config/` is the application's deployment and product-configuration boundary. +Its TypeScript and JavaScript modules parse operator-owned environment values, +derive origins and host topology, and expose catalogues for apps, models, +limits, navigation and feature defaults. Strict schemas make malformed +operator configuration fail during startup or build instead of silently +changing behavior. + +YAML is used elsewhere for content, prompts and catalogue inputs. The webpack +YAML loader makes those imports available as JavaScript values and can select +entries through `lookupKey` and `lookupValue` resource queries. + +### `next.config.d/` + +`next.config.js` is a loader, not a config: it reads every `*.config.js` in +`next.config.d/`, orders them, and deep-merges the results (with defined +semantics for `webpack`, `headers`, `rewrites`, and `redirects`). Each +module owns one concern. + +They fall into two groups. Portable application configuration covers bundling +and output modes, transpiled packages, security headers and CSP, environment +exposure, image domains, embed script entry points, API discovery headers and +agent content negotiation. Deployment-controlled routing covers app shells, +standalone apps, portals, space sites, partner hosts, request-affine host +mappings and optional multi-zone proxies. With those values unset, the +corresponding rules are inert and the supported single-domain, path-based +topology remains. The module boundaries keep portable behavior and optional +host routing visible file by file. + +### `schemas/` - where authorization lives + +`schemas/` holds one Joi schema per request field, and these are not just +shape validators: identifier schemas resolve the referenced resource and +enforce access on it. `schemas/botId.js`, for example, looks up the bot and +applies use-versus-manipulate access checks, throwing the appropriate +authentication or authorization error. API routes compose their request +validation from these files, so authorization is enforced at the validation +boundary rather than ad hoc inside handlers. `schemas/api/v1/` adds +per-resource response schemas for the public API. + +### The database + +The one hand-edited Prisma schema lives in +`packages/db-spec/prisma/schema.prisma`, kept complete for the richest +supported engine on purpose: engine-specific information only flows +downhill, so deriving is subtractive. Each database implementation derives +its own `schema.prisma` from it - the derived copies are generated, marked +as such, and committed so schema changes show up in review for every engine +they affect. Derivation also runs automatically at the start of every +`db:push` and `db:gen`, so a stale schema cannot reach a database or a +generated client. + +`db-spec` also carries the shared analytics queries in `prisma/sql/`, +written to run unmodified on every supported engine. + +Inside the application, `prisma/` is the data-access layer around the +generated client - the client singleton, custom model methods, field-level +encryption, caching, auditing, and retry - not the schema. + +### Content and layouts + +`content/` contains only the small catalogues still coupled to the product +runtime: FAQs and connection metadata under `other/`. Source-level `@doc` and +`@manual` blocks are publication inputs consumed by the documentation release +tooling. Published documentation is maintained outside the product runtime, so +the application does not depend on generated manuals or documentation content. + +`layouts/` contains the reusable page shells used by the Pages Router, such as +the dashboard, app, exploration and administration layouts. App Router layouts +stay with their routes under `app/`. + +### Everything else, briefly + +`emails/` - React Email components, one per transactional message, +delivered through the swappable email module. `embeds/` - sources for the +embeddable widget and MCP scripts, injected as extra webpack entries. +`graphql/v1/` - the GraphQL schema and resolvers behind +`pages/api/v1/graphql`. `workers/` - browser web workers. `templates/` - +quick-setup wizard definitions. `data/` - ability, secret and other runtime +catalogues expressed as TypeScript DSLs and YAML or OpenAPI inputs. `prompts/` +contains versioned YAML prompt files. `scripts/` contains operational scripts +built mostly on a shared `runScript` harness with CLI and interactive modes. + +## Conventions + +### New source is TypeScript, new tests are JavaScript + +The source tree is being migrated from JavaScript and JSX to TypeScript and +TSX. New source and files converted as part of a change use `.ts` or `.tsx` +and are type-checked by `pnpm check`; existing `.js` and `.jsx` files remain +valid until they are migrated deliberately. + +New tests are JavaScript on purpose: a TypeScript test rejects the +wrong-on-purpose input that a test exists to cover. In the application, unit +tests are `*.utest.js` or `*.utest.jsx` and are normally co-located with their +source; in packages they are `*.test.js`. Existing TypeScript test files +predate this rule - leave them alone unless the surrounding test is already +being rewritten, and do not use them as models. + +One placement exception, about the router: tests under `pages/` stay +co-located but carry an underscore prefix (`pages/api/v1/bot/_create.utest.js`, +`pages/admin/users/[userId]/_index.utest.js`) because the router ignores +`_`-prefixed files, so test files are never exposed as routes. + +Integration tests are `*.itest.js` under `tests/integration/`, run +separately (`pnpm test:integration`). + +### Comments + +`@note` marks gotchas, side effects, and surprising behavior - the things +the next reader would otherwise rediscover the hard way. `@todo` marks +planned work. Both are single sentences, lowercase, no ending period. Plain +comments explain complex logic and use normal punctuation. The codebase +leans heavily on `@note`; when a piece of code depends on something the code +cannot show, that is where it is written down. + +### Custom lint rules + +The application ships its own ESLint rules under `eslint/custom-rules/`, each +encoding a repository invariant. They protect serialization, Prisma deletes, +typed SQL, disposable factory results, the custom router, package +transpilation, directive placement, controlled HTTP egress and centralized +documentation links. If one fires, its error message identifies the invariant; +the rule source and any co-located test show the exact boundary. + +### Build-time machinery worth knowing about + +The webpack layer under `platform/webpack/` carries the YAML loader described +above, a `.json.gz` loader, a markdown frontmatter loader +(`import meta from './file.md?frontmatter'`), and a source-map validation +plugin that fails the build if any emitted map embeds source content. +`app.manifest` files import as JSON via a dedicated rule. + +## Building and verifying + +From the repository root, begin with `pnpm install`. The CI quality gate runs +`build`, `lint`, `check` and `test` across the packages (filtering the +application out with `--filter='!@chatbotkit/platform'`) and then, in a +second job, type-checks the application and runs its unit suite from +`.env.example` and a fresh SQLite database. The application's lint and build +are still local responsibilities. See `CONTRIBUTING.md` for the exact commands +and setup. + +Inside `platform/`: `pnpm dev` starts the development server, `pnpm check` +type-checks, `pnpm lint` lints, `pnpm test:unit` runs the unit suite (or pass a +single test path), and `pnpm storybook` starts the component workbench. A full +`pnpm build` regenerates the database and GraphQL clients, builds templates and +the API specification, runs the unit suite, builds the application, and +generates the sitemap. The `SKIP_*` environment variables it honors exist for +CI stages that cover selected steps separately. + +The `docker-compose.yml` at this root offers two application modes. The +default profile starts a ready development server plus Redis, Qdrant and +Garage; it copies the read-only checkout into a container and preserves hot +reload. The `distro` profile builds and serves the compiled platform. The +backing services can also be started individually for host-side development. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..266014c --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,489 @@ +# Deployment configuration + +Most of what a deployment needs is an ordinary environment variable - a URL, a +credential, a feature toggle - and [`platform/.env.example`](../platform/.env.example) +documents those where they are used. + +This document covers the five variables that are different in kind. Each carries +a JSON document rather than a scalar, and each answers a question the code +cannot answer for you: who administers this deployment, what it sells, what a +plan grants, which hostnames it answers on, who gets exceptions, and which +paths belong to an external application zone. They are **operator-owned data**, +deliberately kept out of the source tree so that a deployment's business model +and routing topology are not code changes. + +Every one of them is optional. A deployment that sets none of them boots and +works: no administrators, no plans, no billing, no exceptions, no additional +host mappings, no external zones, and every surface served path-based on a +single host. That is the supported self-hosted default, not a degraded mode. + +| Variable | Answers | Unset means | +| ---------------------- | ------------------------------------------- | ---------------------- | +| `ADMINS_CONFIG` | Who reaches `/admin` | Nobody | +| `LIMITS_CONFIG` | What each plan grants | No plan concept at all | +| `OVERRIDES_CONFIG` | Per-account exceptions and grants | No exceptions | +| `HOSTS_CONFIG` | Request-affine host mappings | Scalar host defaults | +| `ZONE_CONFIG` | Paths proxied to external application zones | No external zones | + +The four application configuration variables are parsed in `platform/config/`. +`ZONE_CONFIG` is parsed by `platform/next.config.d/zone.config.js` when Next.js +builds its routing table. Those source headers are the authoritative shape +references. This page is the operator's view: what to set, what it costs you to +get it wrong, and what changes safely. + +## Validation rules + +**The four application configuration variables are parsed with strict schemas +when their modules load. An unrecognised key, a missing required key, or a +malformed document fails startup or the build. It does not warn and continue.** + +`ZONE_CONFIG` is different because it generates Next.js rewrites. It is parsed +at build time and validates route shapes as the rewrites are generated. Invalid +JSON or paths fail the build. Changes therefore require a rebuild and redeploy. +The variable must be present in the environment that runs `next build`; setting +it only in the final container's runtime environment is too late. + +This is deliberate. These variables decide entitlements, administrator access +and routing; a table that silently ignored a key it did not understand would +deny a customer, expose an admin route, or 404 a whole surface with nothing in +the logs to say why. Failing at boot is the safe direction. + +The cost is that **adding a required key to one of the four strict schemas is a +breaking change for every operator who sets that variable**. Their deployment +stops booting until they edit it. So: + +- Additive keys should land **optional with a documented default**. +- Unknown keys must keep failing loudly - do not relax `.strict()` to make a + migration easier. +- Any exception travels in the release notes as a configuration migration. + +`HOSTS_CONFIG` is shared by build-time routing and runtime URL selection. The +build flattens every configured API and static target into its routing rules. +At runtime, request-context setup selects a mapping once from the authenticated +frontend host or trusted normalized request host. URL helpers then read only +the resolved context; the raw mapping is not exposed to the browser. The +operator-defined mapping names are only stable configuration identifiers; they +do not select branding, tenancy, or a canonical host. + +## Validating before you deploy + +The four application values are parsed when their modules load, so the cheapest +check is a development boot: + +```bash +# from platform +ADMINS_CONFIG='["ops@example.com"]' pnpm dev +``` + +A malformed document fails immediately with a zod error naming the offending +path. Validating in the shell first is worthwhile for the larger ones: + +```bash +echo "$LIMITS_CONFIG" | jq . > /dev/null && echo "valid JSON" +``` + +Valid JSON is necessary but not sufficient. The application checks the four +strict schemas, while a Next.js build checks `ZONE_CONFIG` and generates its +rewrites. + +Two catalogues are **coupled**. A `plan` grant in `OVERRIDES_CONFIG` is +validated against `LIMITS_CONFIG` when the override module loads, and the +configuration conformity test suite checks the two together. Run the +application quality gate before deploying changes to these values. Set the +limits catalogue first so a granted plan is never left undefined. + +--- + +## `ADMINS_CONFIG` + +Who may reach the administration console at `/admin` and `/api/admin`. + +```json +["ops@example.com", "clxyz0000000000000000000n"] +``` + +An array of identifiers. An entry is **either** a user id **or** an email +address, with no marker saying which - the check compares both fields of the +signed-in user against every entry. + +That distinction matters when you add one. An **email** is a claim about a +person and follows whoever currently holds that address. An **id** is a claim +about an account and survives the person changing their address. Prefer the id +for anything long-lived. + +> **Security.** This is the whole gate. The console ships in every deployment +> and authorizes nobody until this is set, so an empty value is safe and a +> careless value is not. An address you do not control - a former employee's, +> or a domain you have let lapse - is an administrator. + +## `LIMITS_CONFIG` + +The plan catalogue: one **complete** limit table per plan name. + +```json +{ + "pro": { "tokens": 3000000, "conversations": 2500, "...": "every other key" } +} +``` + +The example above is **abbreviated and would not boot** - shown for shape only. +A real table carries every limit key; start from the one documented in +`platform/config/limits.ts`. + +Plan names are yours - `pro`, `team`, `enterprise`, whatever you sell. The +limit _keys_ are platform vocabulary and the table must be complete: the schema +is strict, so a missing or misspelled key fails the boot rather than quietly +resolving to zero. + +Unset is a **working configuration**, not an empty one. With no catalogue the +deployment has no plan concept: every limit lookup resolves to the unlimited +table, enumeration stays empty so no surface renders an invented plan name, and +the entitlement checks short-circuit. It is explicitly _not_ "everyone on the +lowest plan". + +The code reserves three structural names. `free` means no subscription or +grant, and `trial` means a trialing subscription. `unlimited` always resolves to +the unlimited table: it is implicitly available for grants and manual +subscriptions but is never enumerated in plan lists. Do not define +`unlimited` in `LIMITS_CONFIG`. + +## `OVERRIDES_CONFIG` + +Per-account exceptions. The key used depends on the field: plan grants are +looked up by email, while VIP status and enforcement-time limit overrides are +looked up by account id. Some account-summary paths fall back to email for +limits, but enforcement does not, so email-keyed limit entries are not a +dependable deployment contract. + +```json +{ + "ops@example.com": { "plan": "enterprise" }, + "clxyz0000000000000000000n": { + "limits": { "database": { "files": 300 } }, + "plans": { "premium": { "limits": { "tokens": 5000000 } } } + } +} +``` + +- **`plan`** is an email-keyed _grant_: treat this account as if it bought that + plan. No subscription, no billing. This is the comp mechanism. It may name a + catalogue plan other than `free` or `trial`, or the implicit `unlimited` + plan. +- **`vip`** is account-id keyed and skips the hub publishing review queue. +- **`limits`** bends specific values whatever plan the account is on. Key these + entries by account id so the enforcement paths apply them. +- **`plans[name].limits`** bends them only while on that plan, so a + grandfathered exception does not leak into a downgraded one. These keys must + name plans in `LIMITS_CONFIG`; `free` and `trial` are valid here when they are + present in the catalogue. The surrounding entry should also use the account + id. + +A malformed or unknown plan reference fails when the override configuration is +loaded. An id key follows the account; an address-keyed plan grant follows +whoever holds the address. A key matching nothing is silently an override that +never applies, which is why attribution matters. + +> **Operational note.** Record _which customer_ an entry belongs to as a comment +> beside the value in your encrypted environment file. An exception nobody can +> attribute is one nobody can ever remove. + +## Apex hostnames + +Four scalar variables define the apexes beneath which the deployment creates +subdomains. They identify canonical deployment-owned domains and are therefore +separate from the routing table. + +| Variable | Serves | Unset means | +| --------------- | ----------------------------------------------- | ------------------------------------------- | +| `APP_APEX` | Standalone apps at `.` | Standalone apps are path-based | +| `PORTAL_APEX` | Portals at `.` | Portals use custom domains or the site host | +| `SPACE_APEX` | Space sites at `.` | Deployment-issued space hostnames disabled | +| `PARTNERS_APEX` | Partner experiences at `.` | Partners use custom domains or the site URL | + +Values are hostnames without a protocol or wildcard, for example: + +```dotenv +APP_APEX=example.app +PORTAL_APEX=example.agency +SPACE_APEX=example.site +PARTNERS_APEX=example.partners +``` + +## App shell origins + +Two scalar origins identify the canonical app-shell endpoints. An origin must +include its protocol and must not include a path, query, hash, or trailing +slash. + +| Variable | Serves | Unset means | +| ----------------- | ----------------------- | -------------------------------- | +| `APP_MAIN_ORIGIN` | The main Apps shell | The shell remains path-based | +| `APP_LABS_ORIGIN` | The optional Labs shell | The Labs shell is not registered | + +```dotenv +APP_MAIN_ORIGIN=https://apps.example.com +APP_LABS_ORIGIN=https://labs.example.com +``` + +## `HOSTS_CONFIG` + +Optional request-affine host mappings. Each operator-defined key groups the +site, API, static, and widget hosts that must stay together when the deployment +answers on several domain families. + +```json +{ + "example": { + "match": [ + "example.com", + "api.example.com", + "static.example.com", + "widgets.example.com" + ], + "site": "example.com", + "api": "api.example.com", + "static": "static.example.com", + "widgets": "widgets.example.com" + } +} +``` + +| Field | Purpose | +| --------- | --------------------------------------------------------- | +| `match` | Exact incoming hostnames that select this mapping | +| `site` | Site or application host for request-affine frontend URLs | +| `api` | API host for request-affine API URLs and clean API routes | +| `static` | Static host for public assets and static-host routing | +| `widgets` | Host for private MCP widget bundles | + +Values are exact hostnames without a protocol, wildcard, path, query, or hash. +Every target that can receive a request should also appear in `match`, so a +request arriving on an API or static host selects the same mapping. + +At build time, every `api` and `static` target is enabled unconditionally. At +runtime, context injection selects the mapping once when the authenticated +frontend host or normalized request host appears in `match`. Server URL helpers +read the resolved targets from that context. The HTML document exposes only the +resolved site, API, static, and widget hosts for client hooks. An unknown host +keeps the existing custom-domain behavior. + +`SITE_URL` remains the canonical and requestless default. When no mapping is +selected, `API_URL`, `STATIC_URL`, and `WIDGET_URL` all fall back to +`SITE_URL` - the deployment then serves the API at `/api/v1`, and the static +and widget paths, on its own host, with no host-gated routing derived. The +scalar targets are also routed alongside the mapped ones: an `API_URL` naming +a host other than the site host is routed to the API just as a `STATIC_URL` +host is routed to the static rules, so a deployment with a single dedicated +API subdomain needs only the scalar. There is no implicit `api.` +derivation: advertised API URLs follow `API_URL`, and unset it they stay on +the site host. App shells and apex-based routing are controlled independently +by the scalar variables above. + +The configuration fails validation on malformed hostnames, missing fields, +unknown fields, or a hostname matched by more than one mapping. + +## `ZONE_CONFIG` + +Optional build-time routing for applications deployed separately while this +application remains the public domain gateway. The value is an array of zones; +the legacy single-zone object is also accepted. + +```json +[ + { + "origin": "https://marketing.example.com", + "hosts": ["example.com", "www.example.com"], + "root": true, + "paths": ["/pricing", "/careers"], + "exactPaths": ["/platform"], + "prefixes": ["/media/marketing"], + "exceptions": ["/pricing/internal-tool"], + "aliases": { "/pricing-preview": "/pricing" }, + "assetPrefix": "/marketing-static" + } +] +``` + +| Field | Purpose | +| ------------- | ---------------------------------------------------------------- | +| `origin` | Deployment origin to which the owned routes are proxied | +| `hosts` | Exact incoming hostnames on which this zone applies | +| `root` | Whether the bare `/` belongs to the zone | +| `paths` | Top-level segments, their subtrees, and Pages Router data routes | +| `exactPaths` | Top-level paths owned only at the exact path | +| `prefixes` | Multi-segment subtrees whose parent segment remains shared | +| `exceptions` | Subtrees retained locally beneath a segment listed in `paths` | +| `aliases` | Temporary local paths that proxy to another path in the zone | +| `assetPrefix` | Unique JS and CSS chunk prefix, defaulting to `/zone-static` | + +Only zones with both a non-empty `origin` and at least one host become active. +Hosts not listed in a zone, including white-label and preview hosts, stay with +this application. `paths` and `exactPaths` accept single top-level segments; +`prefixes` and `exceptions` accept multi-segment paths. An exception must sit +beneath an entry in `paths`. + +This variable is deployment topology rather than tenant or branding data. To +return a path to this application, remove it from the table and rebuild. The +experimental `distro` Dockerfile does not currently forward operator `.env` +values into the build, so extending that build definition is required for a +zoned compiled image. + +--- + +## Built-in assistants and public examples + +The assistant and example widgets need two decisions that application code +cannot make for an operator: which model to use, and which account owns an +unauthenticated public conversation. + +| Variable | Purpose | Unset means | +| ------------------------ | ------------------------------------------------------------------ | -------------------------------------- | +| `AUTO_WIDGET_MODEL` | Model for the built-in dashboard, blueprint and website assistants | Built-in assistants are disabled | +| `AUTO_WIDGET_USER_ID` | Service-account owner for the unauthenticated website assistant | The website assistant is not public | +| `EXAMPLE_WIDGET_USER_ID` | Service-account owner for unauthenticated live examples | Live examples require a signed-in user | + +Authenticated dashboard, blueprint and example requests always use the +signed-in account. A service account is only a deliberate owner for an +unauthenticated public surface; it is never substituted for a signed-in user. +If no owner can be resolved, conversation creation fails closed with a 401. + +## Portal rewrite assertions + +Most community deployments do not need internal routing headers. When the +separate portal frontend rewrites a public or custom domain to the platform, +however, the platform still needs the original frontend host for tenant +selection and URL generation, and may need the ingress-provided client address. + +Set the same `INTERNAL_HEADERS_SECRET` value, with at least 16 characters, in +the portal and platform environments. The portal serializes each value as an +independently authenticated assertion under a non-canonical wire name. The +platform verifies allowlisted assertions once and promotes them into request +context. Raw `x-chatbotkit-internal-*` headers, unknown assertions, malformed +values and invalid signatures are ignored. If the secret is unset, the +platform behaves as if no portal assertions were supplied. + +When this value is missing or shorter than 16 characters, the assertion sender +and receiver emit a debug message where the value is used. Incoming portal +assertions are treated as untrusted and internal self-calls emit no assertions. +The portal follows the same fail-closed behavior for outgoing assertions. Use a +randomly generated secret rather than treating the minimum length as an entropy +guarantee. + +This secret does not make ordinary `Host`, `x-forwarded-host` or client-IP +headers trustworthy. A deployment proxy must overwrite those headers and +prevent direct application access according to its own topology. + +## Reverse-proxy headers + +The platform trusts `x-forwarded-host`, `x-forwarded-proto` and the client +address headers (`x-real-ip`, else the last `x-forwarded-for` hop - the one +the proxy itself appended) only when the deployment sets +`TRUST_PROXY_HEADERS=true`. The values are normalized once +into request context and downstream code reads only that context; when trust +is disabled, the forwarded values are ignored and the application uses the +ordinary `Host` header, the request URL protocol where available, and the +directly connected socket address for rate limiting and audit records. + +Behind a reverse proxy this flag also decides whether the sign-in abuse +controls work at all: without it every client shares the proxy's socket +address, so the per-address budgets for code issuance and verification become +one global budget - a handful of failed attempts from anyone locks sign-in for +everyone until the window passes. + +The flag is a deployment-topology assertion, not authentication. Enable it +only when the reverse proxy removes client-supplied forwarded headers, writes +its own values, and prevents clients from reaching the application origin +directly. If those conditions cannot be guaranteed, leave it unset. + +## Platform capacity cap + +`PLATFORM_MAX_TOKENS_PER_MONTH` is an optional deployment-wide safety ceiling, +not a subscription plan. Set it to a positive number to stop non-exempt model +traffic after that many calibrated base tokens in a billing period. Leaving it +unset, or setting it to `Infinity`, gives a community deployment no artificial +hosted quota. Hosted and resource-constrained operators should set a finite +value explicitly. + +## Credential cache + +`PLATFORM_CREDENTIAL_CACHE_TTL` is the number of seconds an API secret key or +OAuth access token lookup may be served from cache when a request +authenticates. It defaults to `0`: every API request reads the credential row, +so revoking a key takes effect on the next request. A deployment whose API +volume makes that read expensive (a metered database, for instance) can set a +small positive value to trade a bounded delay for fewer reads. Be explicit +about what is bought: a revoked key or token keeps working for up to that many +seconds. There is no stale-while-revalidate on top, so the window is exactly +the value set. + +## Encryption at rest + +`PRISMA_FIELD_ENCRYPTION_KEY` encrypts the database columns that hold +credentials - every column carrying a `/// @encrypted` annotation in the +schema: stored secrets and their values, every integration's tokens, app +secrets, API keys, private keys and webhook secrets, the MCP identity +provider's client secret, next-auth's provider tokens and outbound webhook +secrets (`ENCRYPTED_FIELDS` in `platform/prisma/encryption.ts` is the list) - +through the Prisma extension in that module. Not encrypted, by design: the +columns the platform looks up by equality (API keys, its own OAuth server's +tokens and client secrets - `/// @digest` in the schema), because a +ciphertext with a random nonce cannot be searched. It is optional, and **setting it is the +decision to encrypt**: unset, the extension is inert and those columns are +stored as given. It is a separate concern from `CLOAK_ENCRYPTION_KEY`, the +general-purpose key the rest of the application uses (transient OAuth state, +values encrypted out of band with `pnpm script:encrypt`); the two are never +substituted for each other. + +**What you get.** Writes are encrypted on the way in, reads decrypted on the +way out, including `include`-d relations; a test fails if the schema +annotations and the extension's field map ever disagree. AES-256-GCM with a +fresh random nonce per value, and each ciphertext bound to its column +(`.` as authenticated data), so a value copied by raw SQL from +one column or model to another is rejected on read rather than granting the +target row a credential it never had. Audit rows only ever see ciphertext. +A copy of the database - a backup, a replica, an injection, a script gone +wrong - is useless without the key. It does not protect against someone who +holds the runtime, and it is one key for the whole deployment rather than +per tenant. + +**Generating a key.** + +```bash +# from platform +pnpm script:generate-encryption-key +``` + +prints a `k1.aesgcm256.<43 base64url characters>=` value - 32 random bytes. +Back it up somewhere that is not the database: losing every key loses every +encrypted value, and there is no recovery. A malformed value is not +silently ignored; the first write to an encrypted column fails. + +**Rotation.** The variable is a comma-separated keychain: the first key +encrypts, every key decrypts. Rotation therefore needs no downtime and no +window during which reads fail: + +1. Generate a new key and **prepend** it: `PRISMA_FIELD_ENCRYPTION_KEY=,`. + Deploy. New writes use the new key; existing rows still read. +2. Run `pnpm script:backfill-database-encryption`. It walks every encrypted + column and rewrites each value that is not already under the first key + with its column binding, in batches, and reports counts. Without + `--execute` it is a dry run and only counts. It is safe to re-run, and it + stops - rather than skipping - on a value that no configured key accepts. +3. Remove the old key. Do not do this before step 2 completes: values still + under it become unreadable, and the script cannot recover them. + +**Turning it on later.** The same script is the one-time migration for a +deployment that ran without a key: set the key, deploy, run the script once. +Reads pass plaintext rows through, so nothing breaks in the meantime, but a +production estate should not rely on "re-save to encrypt". Removing the key +later does not decrypt anything - rows encrypted while it was set come back +as ciphertext until it is restored. + +--- + +## Related + +- [`platform/.env.example`](../platform/.env.example) - every other variable +- [Architecture](./architecture.md) - the configuration boundary and module + architecture +- `packages/*/README.md` - each swappable module's own variables diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..a5c28f2 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,258 @@ +# Deployment + +The repository provides a complete development stack, a locally compiled image +profile and prebuilt community images produced by trusted pushes. These are +application distribution artifacts, not infrastructure provisioning recipes. +Production operators remain responsible for the surrounding deployment and +its operational guarantees. + +## Compose profiles + +Run the complete development stack: + +```bash +docker compose up +``` + +Run only backing services for host-side development: + +```bash +docker compose up redis qdrant garage garage-init +``` + +Build and run the experimental compiled image: + +```bash +docker compose --profile distro up --build platform +``` + +The first compiled build can take more than 15 minutes depending on available +CPU, memory and network cache state. + +## Pull the community image + +Trusted pushes to `main` and `next` publish matching application and database +initializer images. Names follow `platform--`; tags carry +only the build: the moving `main` and `next` tags are channels (`latest` +follows `main`), and `sha-` tags identify an immutable source +revision. + +The publication workflow derives the registry owner and image name from the +GitHub repository. In `chatbotkit/platform` this resolves to the official image +names below without repository-specific workflow configuration. + +The public ChatBotKit images use `ghcr.io/chatbotkit/platform-community-app` +and `ghcr.io/chatbotkit/platform-community-init`. Start the Compose profile +without allowing a source build. Each tag is a multi-platform image for +`linux/amd64` and `linux/arm64`, so Docker selects the host architecture +automatically: + +```bash +docker compose --profile distro up --no-build --pull always platform +``` + +By default this pulls: + +```text +ghcr.io/chatbotkit/platform-community-app:next +ghcr.io/chatbotkit/platform-community-init:next +``` + +Select another matching channel or immutable revision by setting both image +references: + +```bash +PLATFORM_IMAGE=ghcr.io/chatbotkit/platform-community-app:main \ +PLATFORM_INIT_IMAGE=ghcr.io/chatbotkit/platform-community-init:main \ + docker compose --profile distro up --no-build --pull always platform +``` + +Never mix application and initializer revisions. The database schema and +generated application client must come from the same source and package flavor. + +## One-command distribution stack + +Each trusted push also publishes the flavor's complete Compose application as +an OCI artifact under `ghcr.io/chatbotkit/platform-`. The artifact is +self-contained - the application, database initializer, Redis, Qdrant and +Garage with its configuration and provisioning - and every image reference in +it is resolved to a digest, so an artifact tag identifies an exact, immutable +stack. No checkout, no bind mounts: + +```bash +docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest up +``` + +The `latest` tag follows `main`; `next` follows the `next` branch. Compose +v2.34 or newer is required. On `up`, Compose shows the stack's variables - +site URL, secrets, optional provider keys - and their defaults before +proceeding; set them in the shell, in a `.env` file in the directory the +command runs from (picked up automatically), or via an explicit `--env-file`, +and pass `-y` to skip the confirmation in scripts. Shell values win over +`.env`, and only variables the stack declares are consumed - the published +artifact carries no `env_file` mounts, so arbitrary extra entries do nothing. + +### Persisted configuration + +Values can also live in the platform data volume, where they survive +restarts and upgrades and never touch a file on the host. The application +entrypoint reads `/data/config.env` (one `KEY=VALUE` per line, no quoting) +and exports every entry the container environment does not already set. Any +variable the application honours is accepted, not only the ones the stack +declares. `setup` writes the file: + +```bash +# prompts for the provider keys, input hidden; Enter keeps, "-" clears +docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest run --rm --no-deps platform setup + +# prompts for named variables, or sets them without a terminal +docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest run --rm --no-deps platform setup OPENROUTER_MODELS_API_KEY +docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest run --rm --no-deps platform setup OPENROUTER_MODELS_API_KEY=sk-or-... OPENAI_API_KEY= +``` + +Restart the `platform` service afterwards. Precedence, highest first: the +container environment (shell, `.env`, `--env-file`, `-e`), then +`config.env`, then the secrets generated on first boot. An empty environment +value counts as unset, so the stack's `${OPENAI_API_KEY:-}` defaults never +mask a persisted value; to override one for a single run, set it in the +shell. The file is owned by the application user with mode `0600`; back it +up with the volume, and prefer `PRISMA_FIELD_ENCRYPTION_KEY` in the +environment rather than next to the database it protects. + +### Several instances on one host + +The stack declares no project name, so Compose derives one. Two instances +started from the same artifact would share that project - and with it the +`platform-data` volume - and both would try to publish port 3000. Give each +instance its own project with `-p` and move its published ports with an +override file; the override applies after the artifact, so pass both, in that +order, on every command for that instance: + +```yaml +# staging.yml +services: + platform: + ports: !override + - '3001:3000' + garage: + ports: !override + - '127.0.0.1:3901:3900' +``` + +```bash +docker compose -p cbk-staging \ + -f oci://ghcr.io/chatbotkit/platform-community:latest -f staging.yml up -d +docker compose -p cbk-staging \ + -f oci://ghcr.io/chatbotkit/platform-community:latest -f staging.yml logs platform +``` + +Set `SITE_URL` and `NEXTAUTH_URL` to the instance's published address +(`http://localhost:3001` here) - in the shell or through `--env-file`, since a +single `.env` in the working directory cannot describe both instances. Volumes, +networks and container names are all prefixed with the project name, so each +instance keeps its own database, generated secrets, object store and vector +index, and `-p` is also how `logs`, `ps` and `down` find the right one. +Presigned storage URLs carry `garage:3900`, so the `/etc/hosts` entry serves the +instance that keeps port 3900; browser-facing file flows on the others need +their own store host name and endpoint. + +The artifact is published from +[docker/distro/community/compose.yml](../docker/distro/community/compose.yml), +which also runs directly from a checkout. One folder per package flavor lives +under `docker/distro/`; a future PostgreSQL flavor publishes as +`ghcr.io/chatbotkit/platform-postgresql` from its own folder, built from the +matching image flavor. + +Browser-facing file upload and download flows presign URLs against the +in-stack store; add `127.0.0.1 garage` to `/etc/hosts` on the host to use +them, as with the development stack. + +### Distribution flavors + +A flavor is the baseline of [module defaults](./module-defaults.md) plus the +backing services its stack provisions. Everything not listed keeps the +default - in the community flavor the database is SQLite in the platform data +volume, the queue is immediate and non-durable, sign-in codes are read from +the container log, and the sandbox refuses under `NODE_ENV=production`. + +| Flavor | Database | Cache | Vector | Storage | +| ----------- | ----------------------- | ----- | ------ | ------- | +| `community` | SQLite (default module) | Redis | Qdrant | Garage | + +A PostgreSQL flavor would swap the database column only; the other services +travel unchanged. + +## Production boundary + +The `distro` profile demonstrates that the application can be compiled and run +from the published tree. It deliberately does not stand up an operator's +production infrastructure. A production deployment still needs: + +- TLS termination and a trusted reverse proxy that overwrites forwarded headers +- protection and backup of the runtime secrets generated into the persistent + platform data volume, or explicit operator-provided values +- durable database, object-storage and backup policies +- a durable queue when delayed delivery, retries, callbacks or ordering matter +- a production-safe isolated sandbox implementation if agent code execution is + enabled +- monitoring, restore testing and an upgrade and rollback procedure + +The repository does not yet publish versioned releases, SBOMs or signed +provenance, so branch and commit images remain pre-release artifacts. This +status concerns release provenance and compatibility, not whether Compose +should provision the operator-owned infrastructure listed above. + +The experimental Dockerfile builds with `.env.example`; Compose attaches the +optional operator `.env` file only to the running container. Configuration +consumed by Next.js while it builds, including `ZONE_CONFIG` and the build-time +parts of host and subscription configuration, is therefore not baked into the +`distro` image. A production pipeline must supply those values during the build +and keep secrets out of image layers. + +The current community image deliberately bakes the neutral single-host +topology: `SITE_URL=http://localhost:3000`, with no app-shell origins, apexes or +external zones. Runtime service variables such as the database, Redis, Qdrant +and S3-compatible storage endpoints remain configurable. Deployment identity +that Next currently exposes through `next.config.js` is still frozen at build +time; do not present the same digest as portable across arbitrary public domains +until that migration is complete. + +## API endpoint + +Every deployment serves the API at `/api/v1` on its own host - nothing to +configure. To advertise and serve it on a dedicated origin instead, set +`API_URL` (e.g. `https://api.example.com`), point that DNS name at the +deployment, and rebuild: the host is then routed to the API (answering under +the clean `/v1` path) and every externally advertised URL - webhook +registrations, embeds, the OpenAPI spec - follows it. Unset, advertised URLs +stay on the site host under `/api`. Multi-domain deployments name their API +hosts in `HOSTS_CONFIG` instead; see +[Configuration](./configuration.md#hosts_config). Both are read at build time, +so changing them requires a rebuild, not just a restart. + +## Reverse proxy trust + +Set `TRUST_PROXY_HEADERS=true` only when the reverse proxy overwrites forwarded +host, protocol and client-address headers and the application origin cannot be +reached directly. See [Configuration](./configuration.md) for the complete +trust-boundary requirements. + +## Persistent state + +The development stack stores state in Compose volumes. A production design must +make the retention, backup and restore behavior explicit for: + +- the application database +- object-storage buckets +- Redis when it carries shared rate-limit or cache state +- Qdrant or the selected vector implementation +- encryption keys and other runtime secrets + +Backing up ciphertext without its encryption key is not a recoverable backup. + +## Module limits + +The public defaults prioritize a vendor-free boot and an honest development +experience. Some defaults are intentionally not production implementations. +Read [Module defaults](./module-defaults.md) before selecting production +backends. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..1465ac8 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,168 @@ +# Getting started + +The complete supported local baseline runs with Docker Compose. A host-side +development setup is available when you want a faster edit loop. + +To run the prebuilt platform without a checkout, use the one-command +distribution stack described in [Deployment](./deployment.md) instead. + +## Run the complete local stack + +From the repository root (with Git LFS installed before cloning, so the binary +assets arrive as files rather than pointers): + +```bash +docker compose up +``` + +Open . Sign in with any email address and read the +six-digit sign-in code from the `dev` service log (`docker compose logs -f +dev`). + +This command starts: + +- the platform development server on SQLite +- Redis for shared caching +- Qdrant for vector storage +- Garage as an S3-protocol object store +- `garage-init`, which provisions the development key and storage buckets + +The checkout is mounted read-only and synchronized into the development +container. Editing the host working tree still triggers hot reload. + +Browser-facing file flows use presigned URLs containing the Compose service +hostname. Add `127.0.0.1 garage` to the host machine's hosts file before testing +uploads or downloads in a containerized mode. Host-side development configured +with `SERVICE_AWS_ENDPOINT=http://localhost:3900` does not need that entry. + +No hosted account, billing configuration or vendor credential is required to +boot. Model-backed agent responses require at least one model provider key. + +## Configure the development container + +The Compose service keeps its working copy in a writable volume and +deliberately excludes the host `.env` file from source synchronization. In a +second terminal, create or edit the host file and copy it into the running +container: + +```bash +test -f platform/.env || cp platform/.env.example platform/.env +# Edit platform/.env, then: +docker compose cp platform/.env dev:/workspace/platform/.env +docker compose restart dev +``` + +Repeat the copy and restart after changing environment variables. A host-side +development server reads `platform/.env` directly and does not need this step. + +## Add a model provider + +The platform only advertises provider models when the matching credential is +configured. For example, add this to `platform/.env`, then update the running +container as described above when using Compose: + +```bash +OPENAI_API_KEY=sk-... +``` + +The same key also powers dataset embeddings in the default vector setup. Other +provider variables are documented in `platform/.env.example`. + +## Run the application on the host + +Requirements: + +- Node.js 24.20 or newer +- pnpm 11.24.0 or newer +- Git LFS - binary assets are LFS pointers; run `git lfs install` before + cloning, or `git lfs pull` in an existing checkout + +From the repository root: + +```bash +pnpm install +cd platform +cp .env.example .env +pnpm db:push +pnpm dev +``` + +Open . + +Run `pnpm db:push` from the application directory rather than invoking the +database module's CLI directly. The wrapper resolves the relative SQLite URL +against the application directory before the module runs. + +The host-side baseline uses: + +- SQLite on disk +- an in-process cache when `REDIS_URL` is unset +- local JSON vector files when `QDRANT_URL` is unset +- console email delivery +- no plan, billing or entitlement catalogue + +## Configure object storage on the host + +The public storage module is an S3-protocol client, not a local-filesystem +store. The application boots without storage, but file uploads, generated +images and speech, space assets and sandbox storage mounts refuse at the point +of use until a store is configured. + +The quickest local option is the Compose Garage service: + +```bash +docker compose up garage garage-init +``` + +It publishes a provisioned store on `127.0.0.1:3900`. Uncomment the matching +block in `.env.example`, including: + +- `SERVICE_AWS_ENDPOINT` +- `SERVICE_AWS_REGION` +- `SERVICE_AWS_ACCESS_KEY_ID` +- `SERVICE_AWS_SECRET_ACCESS_KEY` +- `SERVICE_AWS_FORCE_PATH_STYLE` +- the `*_S3_BUCKET_NAME` variables + +AWS S3, Cloudflare R2, SeaweedFS and other S3-compatible stores can be used with +their own values. Sandbox storage mounts additionally require +`SERVICE_AWS_STORAGE_ROLE_ARN` and an STS-capable store. + +## Configure shared cache and vector storage + +These services are optional for a host-side development server: + +```bash +docker compose up redis qdrant +``` + +Then configure: + +```bash +REDIS_URL=redis://localhost:6379 +QDRANT_URL=http://localhost:6333 +``` + +## Protect stored credentials + +Before storing real credentials, configure `PRISMA_FIELD_ENCRYPTION_KEY`. An +unset value means scalar credential columns are stored as provided. + +Generate a key from the application directory: + +```bash +pnpm script:generate-encryption-key +``` + +Keep the key safe. Losing it makes encrypted credentials unrecoverable. See +[Configuration](./configuration.md#encryption-at-rest) for backfill and key +rotation. + +## Next steps + +- Review the [module defaults](./module-defaults.md) before evaluating + queueing, storage, sandboxing or multi-process behavior. +- Read [deployment](./deployment.md) before exposing the application outside a + local development environment. +- Read [configuration](./configuration.md) before setting any `*_CONFIG` + variable. Leaving them unset is the supported default. diff --git a/docs/module-defaults.md b/docs/module-defaults.md new file mode 100644 index 0000000..aba4a4d --- /dev/null +++ b/docs/module-defaults.md @@ -0,0 +1,80 @@ +# Module defaults + +Every swappable module ships a public default that boots with nothing set. +This page records what each of those defaults actually does, so a deployment +can tell a working baseline from a feature that needs an operator +implementation. Booting is the guarantee, not production semantics. + +How modules are swapped, and the conventions every module keeps, are in +[Architecture](./architecture.md#swappable-modules). The distribution flavors +bundle backing services over these defaults; +[Deployment](./deployment.md#distribution-flavors) lists what each flavor +changes. Each module's package README owns its environment-variable reference. + +## Default behavior + +### Database + +The public database module uses SQLite in a file. It is suitable for the local +single-process baseline. Production operators own database durability, +concurrency, backup and restore behavior. + +### Storage + +The public storage module is an S3-protocol client with no built-in store. Bare +`docker compose up` provisions Garage. A host-side checkout must configure an +S3-compatible endpoint and buckets before storage-backed features work. + +### Queue + +The public queue delivers immediately and is non-durable. Publishing sends a +request back to the local route. Nothing outlives that request. + +It accepts `delayInSeconds`, retries, flow ordering and callbacks but does not +act on them. A delayed message fires immediately, a failed delivery is not +retried and `parallel: 1` does not serialize work. Install a durable queue for +features that depend on those semantics. It does suppress duplicate deliveries +within one process for 30 minutes, but that memory is neither shared nor +durable. + +### Cache + +The public cache is a bounded in-process LRU unless `REDIS_URL` is set. State is +per process and is lost on restart. In a multi-process deployment, rate-limit +and cache state does not coordinate without a shared implementation. + +### Vector storage + +The public vector module stores records in local JSON files unless `QDRANT_URL` +is set. Embedding still requires a configured model provider, such as +`OPENAI_API_KEY`. + +### Email + +The public email module writes delivery information to the console. This makes +local email-code sign-in usable without SMTP but does not deliver external +mail. + +### Sandbox + +The public sandbox runs code in the application process for development and +refuses under `NODE_ENV=production`. Production code execution requires an +isolated implementation with explicit CPU, memory, disk, network, lifetime and +tenant boundaries. + +### Unavailable service defaults + +The public batch runner, realtime relay, screenshot capture and response +delivery modules keep the application importable but refuse their service +operations. Their `assertConfigured` checks fail so deployment readiness tests +cannot mistake an unavailable capability for a production backend. Features +that need scheduled batch work, live relay channels, captured pages, or +outbound response delivery require an operator implementation. + +### Optional and no-op defaults + +The default search engine finds nothing and the PII module passes content +through without detecting or redacting anything. Platform-secret and partner +catalogues are empty. Observability writes exceptions and messages to the +console; tags and spans are debug-only, and the framework adapters are no-ops. +Callers handle these states without requiring a vendor. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..8395090 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,22 @@ +import { FlatCompat } from '@eslint/eslintrc' + +const compat = new FlatCompat({ + baseDirectory: import.meta.dirname, + resolvePluginsRelativeTo: import.meta.dirname, +}) + +const config = [ + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/types/**', + '**/.next/**', + '**/coverage/**', + '**/cdn/**', + ], + }, + ...compat.extends('@chatbotkit-dev/eslint-config'), +] + +export default config diff --git a/package.json b/package.json new file mode 100644 index 0000000..c57b743 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "platform", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "packageManager": "pnpm@11.24.0", + "engines": { + "node": ">=24.20.0", + "pnpm": ">=11.24.0 <12.0.0" + }, + "scripts": { + "build": "turbo run build", + "check": "turbo run check", + "clean": "turbo run clean", + "format": "run-p format:*", + "format:00-all": "turbo run format", + "format:01-package": "format-package -w", + "format:02-prettier": "prettier -w .", + "lint": "turbo run lint", + "test": "turbo run test" + }, + "dependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@eslint/eslintrc": "^3.3.6", + "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@tsconfig/recommended": "^1.0.2", + "@types/node": "^24.0.0", + "dotenv": "^16.0.3", + "eslint": "^9.0.0", + "format-package": "^7.0.0", + "npm-run-all2": "^9.0.3", + "prettier": "^2.8.7", + "rimraf": "^5.0.5", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "turbo": "^2.10.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/AGENTS.md b/packages/AGENTS.md new file mode 100644 index 0000000..83cf122 --- /dev/null +++ b/packages/AGENTS.md @@ -0,0 +1,261 @@ +# Packages + +Shared code that the platform consumes as real dependencies. The complete tree +is published with the platform, so every package must work from a standalone +checkout. A package may also be published independently when its own manifest +and release configuration say so. + +Some packages here are the public default half of a swappable module and can be +replaced at install time by a deployment-specific implementation. Those have +the extra requirements below. + +## Nothing here may depend on an unpublished counterpart + +Deployment operators may replace a public default with their own implementation. +That implementation is not part of this repository and must not be required to +understand, install or run the public package. + +Three things must never appear in a public package - not in source, not in a +comment, not in a runtime message, not in a README: + +| Never | Instead | +| ------------------------------------------------------------------ | ------------------------------- | +| a deployment-specific package's concrete name | the contract it satisfies | +| an operator's service or directory name | what that class of backend does | +| the infrastructure behind one (a named VMM, orchestrator, or host) | the property that matters | + +The rule is about _identity_, not about technical detail. Explaining that a +backend which embeds server-side cannot be handed vectors is exactly the kind of +reasoning these files should carry. Naming the service that does it is what +turns a design note into a disclosure. + +A public third-party library is not covered by this. `@chatbotkit-dev/memcache` +documents at length that its serialization is a deliberate port of a published +Redis client's, and it should: a reader can go and check that claim. + +### The two places this leaks without anyone noticing + +**Runtime messages.** `assertConfigured` and unsupported-operation errors are +written for whoever is deploying, so the temptation is to name the package they +should install. That string ends up in logs, in support tickets and in the +terminal of someone who has never heard of the private tree. Name the override +point and the contract: + +```ts +'no batch backend is installed, so container jobs cannot run - override +@chatbotkit-dev/batch with a package whose default export satisfies +BatchProvider from @chatbotkit-dev/batch-spec' +``` + +**README override examples.** Use a placeholder rather than an implementation +from one deployment, and say what qualifies: + +```yaml +overrides: + '@chatbotkit-dev/': npm:your--implementation@* +``` + +### Checking + +Search for deployment-specific package scopes, service prefixes, repository +names and infrastructure nouns before publishing. This is especially important +for a new spec: a contract is usually written by someone who has just finished +reading an implementation, and that implementation's vocabulary comes along +with it. + +## Source is TypeScript, tests are JavaScript + +This split is deliberate, not an accident of history. + +**Source files are TypeScript.** Every `.ts` file is type checked by +`pnpm check`, which is what makes a shared package safe to depend on: a change +to an exported signature fails at the call site rather than at runtime in +whichever site imported it. A package whose source is `.js` is not checked at +all — `checkJs` is off — so its exports are effectively untyped no matter how +much JSDoc it carries. + +Typing means real types on the exported surface, not a `.ts` extension. JSDoc +annotations are **ignored** in a `.ts` file, so moving a documented `.js` file +across without writing its signatures silently turns every optional parameter +into a required one and breaks arity at every call site. + +The same applies to JSDoc _cast expressions_, which are easier to miss because +the code still reads as though it asserts something: + +```ts +// inert in a .ts file - the cast does nothing and the type is whatever was +// inferred, which here loses the inner wrapper's options entirely +export const fetchPlusPlus = + /** @type {FetchFn} */ ( + withRetry(withTimeout(fetch)) + ) + +// what it has to become +export const fetchPlusPlus = withRetry(withTimeout(fetch)) as FetchFn< + withTimeoutOptions & withRetryOptions +> +``` + +Grep a converted file for `/** @type` before calling it done. Nothing warns you: +the file compiles, and the error surfaces at a call site in another package. + +**Test files are JavaScript** — `*.test.js`, and `*.utest.js` in the sites. +Tests exercise the package the way a consumer does, including the shapes a +consumer can actually pass. Writing them in TypeScript makes the compiler reject +the wrong-on-purpose input a test exists to cover, so the test either gets +weakened until it type checks or acquires casts that assert away the very thing +under test. Tests are run, not compiled — see the `checkJs` note under Jest +configuration for what actually enforces that, and exclude `**/*.test.js` from +the package `tsconfig.json` so `pnpm check` does not pick them up either. + +``` +src/index.ts <- TypeScript, type checked by `pnpm check` +src/index.test.js <- JavaScript, run by `pnpm test` +``` + +**Two exceptions, and both are load-bearing.** `partners` has JavaScript source +because `platform/next.config.d/partner.config.js` reads the partner catalogue +and Node loads `next.config.js` directly, with no bundler and no transpile. A +`.ts` entry point there fails to import at build time, so renaming it breaks +`next build` rather than `pnpm check`. The contract is still enforced: the +package sets `checkJs`, its catalogue's JSDoc annotation is checked against +`partners-spec`, and the exported surface is declared in a hand-written +`src/index.d.ts`. Anything the catalogue imports at module scope inherits the +same constraint, which is why the mail transport in `packages/partners` defers +its `@chatbotkit-dev/email` import to send time. + +The `observability` package's `next/config` entry point is JavaScript for the +same reason. `next.config.js` loads it directly before webpack exists, including +from the materialized `node_modules` tree produced by `pnpm deploy`. Its runtime +client and server entry points remain TypeScript because Next transpiles those. + +Some older packages still have `.test.ts` files. They predate this rule; leave +them alone unless you are already rewriting the test, and do not add new ones. + +## Jest configuration + +A package's `jest.config.js` is three or four lines. Anything longer is usually +a workaround for something that already works. + +```js +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} +``` + +with `"test": "NODE_OPTIONS=--experimental-vm-modules jest"`. + +**Never add a `moduleNameMapper` for a workspace package.** pnpm already +symlinks `@chatbotkit-dev/x` into the package's `node_modules`, and that +package's `exports` already points at `./src/index.ts`, so jest resolves it with +no help. A hand-written map is redundant on the day it is written and wrong +soon after: it has to be extended for every new dependency, and nothing fails +when an entry is stale, so entries rot in place. One such block shipped a +mapping for `@chatbotkit-dev/http` against a package actually named +`http-codes` — dead the moment it was written, silently. + +**Use the CommonJS preset only when the tests need it.** Under the ESM preset +there is no `jest` global and no `jest.mock` hoisting; the ESM way is +`import { jest } from '@jest/globals'` plus `jest.unstable_mockModule`. Tests +that rely on hoisted `jest.mock` need the CommonJS preset instead, and the +config should say why: + +```js +// @note CommonJS transform: these tests use `jest.mock` hoisting and the `jest` +// global, neither of which is available under the ESM preset. + +export default { + preset: 'ts-jest', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', + + transform: { + '^.+\\.[jt]sx?$': [ + 'ts-jest', + { + useESM: false, + // @note transpile only. Type checking is the `check` script's job. + isolatedModules: true, + tsconfig: { module: 'commonjs', esModuleInterop: true, allowJs: true }, + }, + ], + }, +} +``` + +**`checkJs` defeats the tests-are-not-type-checked rule.** ts-jest compiles a +test with the package's own `tsconfig.json`, so `"checkJs": true` makes the ESM +preset type check `.test.js` files and reject exactly the loose fixtures a test +exists to hold. Since every source in a package is TypeScript, `checkJs` has +nothing legitimate left to check — leave it `false`. + +## Everything in a package is TypeScript + +Not just the entry point. A package with `index.ts` next to an unchecked +`helpers.js` gets no error when `helpers.js` stops matching how `index.ts` calls +it, which is the failure the extension was supposed to prevent. + +There is a second reason, and it only bites in a materialized deployment. +**TypeScript does not read +`.js` files inside `node_modules`** - `maxNodeModuleJsDepth` defaults to `0`. +See the section below for why the local workspace does not expose the problem. + +So a JavaScript source in a package is not a slightly-weaker package. It can +pass through workspace symlinks and fail after deployment packaging, with an +error that points at the importing application rather than the cause. If a file +is hard to type, that is a reason to type it carefully, not a reason to leave +it. + +## Packages resolve differently after deployment packaging + +This is the single most expensive thing to know about this repository, and it +has produced four separate CI failures that were all invisible on the branch. + +Locally, pnpm **symlinks** a workspace package into `node_modules`. Its real +path is `packages//src/index.ts` — outside `node_modules` — so every tool +treats it as ordinary source. + +The production packaging path runs `pnpm deploy --legacy`, which +**materialises** the same package at +`node_modules/.pnpm/@scope+name@file+packages+name_/…`. That +path contains `/node_modules/`, and every tool in the chain has a rule about not +processing `node_modules`: + +| Tool | The rule | What breaks | +| ---------------- | ------------------------- | --------------------------------------------------------------------------------- | +| `tsc6` | `maxNodeModuleJsDepth: 0` | a `.js` source in a package loses all its named exports | +| `jest` | `transformIgnorePatterns` | a `.ts` source is fed to node raw: "Cannot use import statement outside a module" | +| `next` / webpack | `transpilePackages` | "Module parse failed: Unexpected token" on the first `import type` | + +Two further traps in the same family: + +- **Undeclared `@types/*` resolve by accident.** A package that uses typings it + does not declare finds nothing locally, so the import degrades to `any` and is + never checked. In the deploy layout, resolution walks into pnpm's hoist + directory, finds the typings some _other_ package declared, and type checks the + file for the first time — surfacing bugs that were always there. Declare every + `@types/*` your source uses, in `dependencies` rather than `devDependencies`, + because `pnpm deploy` prunes devDependencies of transitive workspace packages. +- **The `.pnpm` directory name is not a semver.** A workspace package appears as + `@scope+name@file+packages+name_`, so allowlist patterns written as + `\.pnpm/@\d+\.\d+\.\d+` never match it. + +### Verifying + +`pnpm check`, `pnpm test` and `pnpm lint` all run in the local layout, so none +of them can see this class of failure. Reproduce the packaged layout directly: + +```bash +pnpm deploy --legacy -F @chatbotkit/platform platform/build-artifacts +cd platform/build-artifacts +# then whichever of tsc6 / jest / next build is in question +``` + +Do this after moving code into a package, after adding a package to the +platform's dependencies, and after changing anything that names packages — +`transpilePackages`, `transformIgnorePatterns`, `.pnpmfile.cjs`. `pnpm deploy` +takes a few minutes; each of the four failures above cost considerably more. + +`platform/build-artifacts` is not gitignored. Remove it when you are done. diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 0000000..74dcfce --- /dev/null +++ b/packages/README.md @@ -0,0 +1,11 @@ +# Packages + +This directory contains the public libraries and module contracts used by the +platform application. Some pairs define a swappable module: a `*-spec` package +owns the contract, while its sibling package supplies the default implementation +that a standalone checkout runs. + +Every package must build, test and document its configuration without relying on +code outside this repository. Package-specific behavior and environment +variables are documented in each package's README. The shared authoring and +verification rules live in [AGENTS.md](./AGENTS.md). diff --git a/packages/auxiliary-google-calendar/README.md b/packages/auxiliary-google-calendar/README.md new file mode 100644 index 0000000..02bbe05 --- /dev/null +++ b/packages/auxiliary-google-calendar/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/auxiliary-google-calendar diff --git a/packages/auxiliary-google-calendar/jest.config.js b/packages/auxiliary-google-calendar/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/auxiliary-google-calendar/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/auxiliary-google-calendar/package.json b/packages/auxiliary-google-calendar/package.json new file mode 100644 index 0000000..0f8795d --- /dev/null +++ b/packages/auxiliary-google-calendar/package.json @@ -0,0 +1,31 @@ +{ + "name": "@chatbotkit-dev/auxiliary-google-calendar", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/auxiliary-google-calendar/src/index.test.ts b/packages/auxiliary-google-calendar/src/index.test.ts new file mode 100644 index 0000000..1df72bb --- /dev/null +++ b/packages/auxiliary-google-calendar/src/index.test.ts @@ -0,0 +1,5 @@ +describe('true', () => { + it('should be true', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/auxiliary-google-calendar/src/index.ts b/packages/auxiliary-google-calendar/src/index.ts new file mode 100644 index 0000000..d68947d --- /dev/null +++ b/packages/auxiliary-google-calendar/src/index.ts @@ -0,0 +1 @@ +export const PLACEHOLDER = 'placeholder' diff --git a/packages/auxiliary-google-calendar/tsconfig.json b/packages/auxiliary-google-calendar/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/auxiliary-google-calendar/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/auxiliary-google-docs/README.md b/packages/auxiliary-google-docs/README.md new file mode 100644 index 0000000..c8dda16 --- /dev/null +++ b/packages/auxiliary-google-docs/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/auxiliary-google-docs diff --git a/packages/auxiliary-google-docs/jest.config.js b/packages/auxiliary-google-docs/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/auxiliary-google-docs/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/auxiliary-google-docs/package.json b/packages/auxiliary-google-docs/package.json new file mode 100644 index 0000000..da555b8 --- /dev/null +++ b/packages/auxiliary-google-docs/package.json @@ -0,0 +1,31 @@ +{ + "name": "@chatbotkit-dev/auxiliary-google-docs", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/auxiliary-google-docs/src/index.test.ts b/packages/auxiliary-google-docs/src/index.test.ts new file mode 100644 index 0000000..1df72bb --- /dev/null +++ b/packages/auxiliary-google-docs/src/index.test.ts @@ -0,0 +1,5 @@ +describe('true', () => { + it('should be true', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/auxiliary-google-docs/src/index.ts b/packages/auxiliary-google-docs/src/index.ts new file mode 100644 index 0000000..d68947d --- /dev/null +++ b/packages/auxiliary-google-docs/src/index.ts @@ -0,0 +1 @@ +export const PLACEHOLDER = 'placeholder' diff --git a/packages/auxiliary-google-docs/tsconfig.json b/packages/auxiliary-google-docs/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/auxiliary-google-docs/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/auxiliary-google-mail/README.md b/packages/auxiliary-google-mail/README.md new file mode 100644 index 0000000..ea4c072 --- /dev/null +++ b/packages/auxiliary-google-mail/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/auxiliary-google-mail diff --git a/packages/auxiliary-google-mail/jest.config.js b/packages/auxiliary-google-mail/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/auxiliary-google-mail/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/auxiliary-google-mail/package.json b/packages/auxiliary-google-mail/package.json new file mode 100644 index 0000000..47bfecb --- /dev/null +++ b/packages/auxiliary-google-mail/package.json @@ -0,0 +1,31 @@ +{ + "name": "@chatbotkit-dev/auxiliary-google-mail", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/auxiliary-google-mail/src/index.test.ts b/packages/auxiliary-google-mail/src/index.test.ts new file mode 100644 index 0000000..1df72bb --- /dev/null +++ b/packages/auxiliary-google-mail/src/index.test.ts @@ -0,0 +1,5 @@ +describe('true', () => { + it('should be true', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/auxiliary-google-mail/src/index.ts b/packages/auxiliary-google-mail/src/index.ts new file mode 100644 index 0000000..d68947d --- /dev/null +++ b/packages/auxiliary-google-mail/src/index.ts @@ -0,0 +1 @@ +export const PLACEHOLDER = 'placeholder' diff --git a/packages/auxiliary-google-mail/tsconfig.json b/packages/auxiliary-google-mail/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/auxiliary-google-mail/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/auxiliary-google-meet/README.md b/packages/auxiliary-google-meet/README.md new file mode 100644 index 0000000..f9fa624 --- /dev/null +++ b/packages/auxiliary-google-meet/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/auxiliary-google-meet diff --git a/packages/auxiliary-google-meet/jest.config.js b/packages/auxiliary-google-meet/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/auxiliary-google-meet/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/auxiliary-google-meet/package.json b/packages/auxiliary-google-meet/package.json new file mode 100644 index 0000000..e9caafc --- /dev/null +++ b/packages/auxiliary-google-meet/package.json @@ -0,0 +1,31 @@ +{ + "name": "@chatbotkit-dev/auxiliary-google-meet", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/auxiliary-google-meet/src/index.test.ts b/packages/auxiliary-google-meet/src/index.test.ts new file mode 100644 index 0000000..1df72bb --- /dev/null +++ b/packages/auxiliary-google-meet/src/index.test.ts @@ -0,0 +1,5 @@ +describe('true', () => { + it('should be true', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/auxiliary-google-meet/src/index.ts b/packages/auxiliary-google-meet/src/index.ts new file mode 100644 index 0000000..d68947d --- /dev/null +++ b/packages/auxiliary-google-meet/src/index.ts @@ -0,0 +1 @@ +export const PLACEHOLDER = 'placeholder' diff --git a/packages/auxiliary-google-meet/tsconfig.json b/packages/auxiliary-google-meet/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/auxiliary-google-meet/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/auxiliary-graphql/README.md b/packages/auxiliary-graphql/README.md new file mode 100644 index 0000000..de941a1 --- /dev/null +++ b/packages/auxiliary-graphql/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/auxiliary-graphql diff --git a/packages/auxiliary-graphql/jest.config.js b/packages/auxiliary-graphql/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/auxiliary-graphql/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/auxiliary-graphql/package.json b/packages/auxiliary-graphql/package.json new file mode 100644 index 0000000..b365b6e --- /dev/null +++ b/packages/auxiliary-graphql/package.json @@ -0,0 +1,45 @@ +{ + "name": "@chatbotkit-dev/auxiliary-graphql", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./notion": { + "import": "./src/notion.ts" + }, + "./slack": { + "import": "./src/slack.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@pothos/core": "^4.8.1", + "graphql": "^16.11.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/auxiliary-graphql/src/builder.ts b/packages/auxiliary-graphql/src/builder.ts new file mode 100644 index 0000000..af7b342 --- /dev/null +++ b/packages/auxiliary-graphql/src/builder.ts @@ -0,0 +1,24 @@ +import SchemaBuilder from '@pothos/core' + +/** + * Context interface for the auxiliary GraphQL schema. + * Contains authentication session and caller information. + */ +export interface Context { + session?: { + user?: { + id?: string | null + email?: string | null + name?: string | null + } | null + } | null + caller?: string | null +} + +/** + * Shared schema builder instance for all auxiliary service schemas. + * This builder is used across notion.ts, slack.ts, and other service modules. + */ +export const builder = new SchemaBuilder<{ + Context: Context +}>({}) diff --git a/packages/auxiliary-graphql/src/http-field.ts b/packages/auxiliary-graphql/src/http-field.ts new file mode 100644 index 0000000..eafebd2 --- /dev/null +++ b/packages/auxiliary-graphql/src/http-field.ts @@ -0,0 +1,107 @@ +/** + * HTTP Field Helper for Pothos GraphQL + * + * This module provides utilities for creating GraphQL fields that automatically + * make HTTP requests and handle responses. + */ + +/** + * HTTP request configuration for automatic resolver generation + */ +export interface HttpFieldConfig> { + // @note http method to use + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + + // @note url template (can use {arg} placeholders) or function + url: string | ((args: TArgs) => string) + + // @note optional headers + headers?: Record | ((args: TArgs) => Record) + + // @note optional body transformer (for POST/PUT/PATCH) + body?: (args: TArgs) => unknown + + // @note optional response transformer + transform?: (response: unknown) => unknown + + // @note optional error handler + onError?: (error: Error) => unknown +} + +/** + * Creates an HTTP field resolver that automatically handles requests + * + * @example + * ```typescript + * search: t.field({ + * type: SearchResult, + * args: { query: t.arg.string({ required: true }) }, + * ...createHttpField({ + * method: 'GET', + * url: (args) => `https://api.example.com/search?q=${args.query}`, + * headers: { 'Authorization': 'Bearer token' }, + * transform: (data) => ({ items: data.results, total: data.count }) + * }) + * }) + * ``` + */ +export function createHttpField>( + config: HttpFieldConfig +) { + return { + resolve: async (_parent: unknown, args: TArgs) => { + try { + // @note build url from template or function + const url = + typeof config.url === 'function' + ? config.url(args) + : config.url.replace(/\{(\w+)\}/g, (_, key) => + encodeURIComponent( + String((args as Record)[key] || '') + ) + ) + + // @note build headers + const headers = + typeof config.headers === 'function' + ? config.headers(args) + : config.headers || {} + + // @note build request body if applicable + const body = + config.body && ['POST', 'PUT', 'PATCH'].includes(config.method) + ? JSON.stringify(config.body(args)) + : undefined + + // @note make http request using native fetch (this is a runtime-agnostic library package) + // eslint-disable-next-line no-restricted-globals + const response = await fetch(url, { + method: config.method, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + body, + }) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + // @note transform response if transformer provided + return config.transform ? config.transform(data) : data + } catch (error) { + // @note handle error if handler provided + if (config.onError) { + return config.onError( + error instanceof Error ? error : new Error(String(error)) + ) + } + + throw error + } + }, + } +} diff --git a/packages/auxiliary-graphql/src/index.test.ts b/packages/auxiliary-graphql/src/index.test.ts new file mode 100644 index 0000000..6511909 --- /dev/null +++ b/packages/auxiliary-graphql/src/index.test.ts @@ -0,0 +1,41 @@ +import { schema } from './index' + +describe('Auxiliary GraphQL Schema', () => { + it('should build a valid schema', () => { + expect(schema).toBeDefined() + expect(schema.getQueryType()).toBeDefined() + expect(schema.getMutationType()).toBeDefined() + }) + + it('should have notion namespace in query', () => { + const queryType = schema.getQueryType() + const fields = queryType?.getFields() + + expect(fields?.notion).toBeDefined() + expect(fields?.notion.type.toString()).toContain('NotionNamespace') + }) + + it('should have slack namespace in query', () => { + const queryType = schema.getQueryType() + const fields = queryType?.getFields() + + expect(fields?.slack).toBeDefined() + expect(fields?.slack.type.toString()).toContain('SlackNamespace') + }) + + it('should have notion namespace in mutation', () => { + const mutationType = schema.getMutationType() + const fields = mutationType?.getFields() + + expect(fields?.notion).toBeDefined() + expect(fields?.notion.type.toString()).toContain('NotionNamespace') + }) + + it('should have slack namespace in mutation', () => { + const mutationType = schema.getMutationType() + const fields = mutationType?.getFields() + + expect(fields?.slack).toBeDefined() + expect(fields?.slack.type.toString()).toContain('SlackNamespace') + }) +}) diff --git a/packages/auxiliary-graphql/src/index.ts b/packages/auxiliary-graphql/src/index.ts new file mode 100644 index 0000000..5d2f25c --- /dev/null +++ b/packages/auxiliary-graphql/src/index.ts @@ -0,0 +1,12 @@ +// @note export the builder and context type for external use +export { builder, type Context } from './builder' + +// @note export HTTP field helper +export { createHttpField, type HttpFieldConfig } from './http-field' + +// @note export service namespaces +export { NotionNamespace } from './notion' +export { SlackNamespace } from './slack' + +// @note export the combined schema +export { schema } from './schema' diff --git a/packages/auxiliary-graphql/src/notion.ts b/packages/auxiliary-graphql/src/notion.ts new file mode 100644 index 0000000..9ecca73 --- /dev/null +++ b/packages/auxiliary-graphql/src/notion.ts @@ -0,0 +1,97 @@ +import { builder } from './builder' + +/** + * Notion auxiliary schema. + * This module provides namespaced GraphQL types and operations for Notion integration. + */ + +// @note notion search result item type +const NotionSearchResultItem = builder + .objectRef<{ + id: string + title: string + type: string + url?: string + }>('NotionSearchResultItem') + .implement({ + fields: (t) => ({ + id: t.exposeString('id', { + description: 'Unique identifier of the Notion item', + }), + title: t.exposeString('title', { + description: 'Title of the Notion page or database', + }), + type: t.exposeString('type', { + description: 'Type of the item (page, database, etc.)', + }), + url: t.exposeString('url', { + nullable: true, + description: 'URL to the Notion item', + }), + }), + }) + +// @note notion search result type +const NotionSearchResult = builder + .objectRef<{ + items: Array<{ + id: string + title: string + type: string + url?: string + }> + total: number + }>('NotionSearchResult') + .implement({ + fields: (t) => ({ + items: t.field({ + type: [NotionSearchResultItem], + resolve: (parent) => parent.items, + description: 'Array of search result items', + }), + total: t.exposeInt('total', { + description: 'Total number of results found', + }), + }), + }) + +// @note notion namespace type for organizing all notion-related queries and mutations +export const NotionNamespace = builder + .objectRef('NotionNamespace') + .implement({ + fields: (t) => ({ + // @todo add notion-specific query fields here + version: t.string({ + resolve: () => '1.0.0', + }), + + // @note search notion content + search: t.field({ + type: NotionSearchResult, + args: { + query: t.arg.string({ + required: true, + description: 'Search query string', + }), + }, + resolve: (_parent, args) => { + // @todo implement actual notion search logic + return { + items: [ + { + id: 'notion-1', + title: `Notion result for: ${args.query}`, + type: 'page', + url: 'https://notion.so/example', + }, + ], + total: 1, + } + }, + }), + }), + }) + +// @todo add notion-specific object types +// @todo add notion-specific queries +// @todo add notion-specific mutations diff --git a/packages/auxiliary-graphql/src/schema.ts b/packages/auxiliary-graphql/src/schema.ts new file mode 100644 index 0000000..eab4c9e --- /dev/null +++ b/packages/auxiliary-graphql/src/schema.ts @@ -0,0 +1,48 @@ +import { builder } from './builder' +// @note import all service schemas to register their types +import { NotionNamespace } from './notion' +import { SlackNamespace } from './slack' + +// @note create the root query type +builder.queryType({ + fields: (t) => ({ + // @note notion namespace for all notion-related queries + notion: t.field({ + type: NotionNamespace, + resolve: () => ({}), + }), + + // @note slack namespace for all slack-related queries + slack: t.field({ + type: SlackNamespace, + resolve: () => ({}), + }), + + // @todo add additional auxiliary service namespaces here + }), +}) + +// @note create the root mutation type +builder.mutationType({ + fields: (t) => ({ + // @note notion namespace for all notion-related mutations + notion: t.field({ + type: NotionNamespace, + resolve: () => ({}), + }), + + // @note slack namespace for all slack-related mutations + slack: t.field({ + type: SlackNamespace, + resolve: () => ({}), + }), + + // @todo add additional auxiliary service namespaces here + }), +}) + +/** + * Build and export the final GraphQL schema. + * This schema combines all auxiliary service schemas into a single executable schema. + */ +export const schema = builder.toSchema() diff --git a/packages/auxiliary-graphql/src/slack.ts b/packages/auxiliary-graphql/src/slack.ts new file mode 100644 index 0000000..082c510 --- /dev/null +++ b/packages/auxiliary-graphql/src/slack.ts @@ -0,0 +1,183 @@ +import { builder } from './builder' + +/** + * Slack auxiliary schema. + * This module provides namespaced GraphQL types and operations for Slack integration. + */ + +// @note slack search result item type +const SlackSearchResultItem = builder + .objectRef<{ + id: string + text: string + channel: string + user?: string + timestamp: string + }>('SlackSearchResultItem') + .implement({ + fields: (t) => ({ + id: t.exposeString('id', { + description: 'Unique identifier of the Slack message', + }), + text: t.exposeString('text', { + description: 'Message text content', + }), + channel: t.exposeString('channel', { + description: 'Channel ID or name where the message was posted', + }), + user: t.exposeString('user', { + nullable: true, + description: 'User ID who posted the message', + }), + timestamp: t.exposeString('timestamp', { + description: 'Message timestamp', + }), + }), + }) + +// @note slack search result type +const SlackSearchResult = builder + .objectRef<{ + items: Array<{ + id: string + text: string + channel: string + user?: string + timestamp: string + }> + total: number + }>('SlackSearchResult') + .implement({ + fields: (t) => ({ + items: t.field({ + type: [SlackSearchResultItem], + resolve: (parent) => parent.items, + description: 'Array of search result items', + }), + total: t.exposeInt('total', { + description: 'Total number of results found', + }), + }), + }) + +// @note slack send message result type +const SlackSendMessageResult = builder + .objectRef<{ + success: boolean + messageId?: string + timestamp?: string + error?: string + }>('SlackSendMessageResult') + .implement({ + fields: (t) => ({ + success: t.exposeBoolean('success', { + description: 'Whether the message was sent successfully', + }), + messageId: t.exposeString('messageId', { + nullable: true, + description: 'ID of the sent message', + }), + timestamp: t.exposeString('timestamp', { + nullable: true, + description: 'Timestamp of the sent message', + }), + error: t.exposeString('error', { + nullable: true, + description: 'Error message if the operation failed', + }), + }), + }) + +// @note slack namespace type for organizing all slack-related queries and mutations +export const SlackNamespace = builder + .objectRef('SlackNamespace') + .implement({ + fields: (t) => ({ + // @todo add slack-specific query fields here + version: t.string({ + resolve: () => '1.0.0', + }), + + // @note search slack messages and channels + search: t.field({ + type: SlackSearchResult, + args: { + query: t.arg.string({ + required: true, + description: 'Search query string', + }), + }, + resolve: (_parent, args) => { + // @todo implement actual slack search logic + return { + items: [ + { + id: 'slack-1', + text: `Slack message containing: ${args.query}`, + channel: 'general', + user: 'U12345', + timestamp: new Date().toISOString(), + }, + ], + total: 1, + } + }, + }), + + // @note send a message to a slack channel + sendMessage: t.field({ + type: SlackSendMessageResult, + args: { + channel: t.arg.string({ + required: true, + description: 'Channel ID or name to send the message to', + }), + text: t.arg.string({ + required: true, + description: 'Message text content', + }), + }, + resolve: (_parent, _args) => { + // @todo implement actual slack send message logic + return { + success: true, + messageId: `msg-${Date.now()}`, + timestamp: new Date().toISOString(), + } + }, + }), + }), + }) + +// @todo add slack-specific object types +// @todo add slack-specific queries +// @todo add slack-specific mutations + +/** + * Example usage of createHttpField helper: + * + * import { createHttpField } from './http-field' + * + * getChannels: t.field({ + * type: ChannelListResult, + * args: { + * limit: t.arg.int({ required: false }), + * }, + * ...createHttpField({ + * method: 'GET', + * url: (args) => `https://slack.com/api/conversations.list?limit=${args.limit || 100}`, + * headers: (args) => ({ + * 'Authorization': `Bearer ${process.env.SLACK_TOKEN}`, + * }), + * transform: (data) => ({ + * channels: data.channels, + * total: data.channels.length, + * }), + * onError: (error) => ({ + * channels: [], + * total: 0, + * error: error.message, + * }), + * }), + * }), + */ diff --git a/packages/auxiliary-graphql/tsconfig.json b/packages/auxiliary-graphql/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/auxiliary-graphql/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/auxiliary-microsoft-drive/README.md b/packages/auxiliary-microsoft-drive/README.md new file mode 100644 index 0000000..7487051 --- /dev/null +++ b/packages/auxiliary-microsoft-drive/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/auxiliary-microsoft-drive diff --git a/packages/auxiliary-microsoft-drive/jest.config.js b/packages/auxiliary-microsoft-drive/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/auxiliary-microsoft-drive/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/auxiliary-microsoft-drive/package.json b/packages/auxiliary-microsoft-drive/package.json new file mode 100644 index 0000000..2c76b67 --- /dev/null +++ b/packages/auxiliary-microsoft-drive/package.json @@ -0,0 +1,35 @@ +{ + "name": "@chatbotkit-dev/auxiliary-microsoft-drive", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/auxiliary-microsoft-drive/src/index.test.ts b/packages/auxiliary-microsoft-drive/src/index.test.ts new file mode 100644 index 0000000..1df72bb --- /dev/null +++ b/packages/auxiliary-microsoft-drive/src/index.test.ts @@ -0,0 +1,5 @@ +describe('true', () => { + it('should be true', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/auxiliary-microsoft-drive/src/index.ts b/packages/auxiliary-microsoft-drive/src/index.ts new file mode 100644 index 0000000..d68947d --- /dev/null +++ b/packages/auxiliary-microsoft-drive/src/index.ts @@ -0,0 +1 @@ +export const PLACEHOLDER = 'placeholder' diff --git a/packages/auxiliary-microsoft-drive/tsconfig.json b/packages/auxiliary-microsoft-drive/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/auxiliary-microsoft-drive/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/auxiliary-microsoft-sharepoint/README.md b/packages/auxiliary-microsoft-sharepoint/README.md new file mode 100644 index 0000000..df45888 --- /dev/null +++ b/packages/auxiliary-microsoft-sharepoint/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/auxiliary-microsoft-sharepoint diff --git a/packages/auxiliary-microsoft-sharepoint/jest.config.js b/packages/auxiliary-microsoft-sharepoint/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/auxiliary-microsoft-sharepoint/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/auxiliary-microsoft-sharepoint/package.json b/packages/auxiliary-microsoft-sharepoint/package.json new file mode 100644 index 0000000..71fe408 --- /dev/null +++ b/packages/auxiliary-microsoft-sharepoint/package.json @@ -0,0 +1,35 @@ +{ + "name": "@chatbotkit-dev/auxiliary-microsoft-sharepoint", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/auxiliary-microsoft-sharepoint/src/index.test.ts b/packages/auxiliary-microsoft-sharepoint/src/index.test.ts new file mode 100644 index 0000000..1df72bb --- /dev/null +++ b/packages/auxiliary-microsoft-sharepoint/src/index.test.ts @@ -0,0 +1,5 @@ +describe('true', () => { + it('should be true', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/auxiliary-microsoft-sharepoint/src/index.ts b/packages/auxiliary-microsoft-sharepoint/src/index.ts new file mode 100644 index 0000000..d68947d --- /dev/null +++ b/packages/auxiliary-microsoft-sharepoint/src/index.ts @@ -0,0 +1 @@ +export const PLACEHOLDER = 'placeholder' diff --git a/packages/auxiliary-microsoft-sharepoint/tsconfig.json b/packages/auxiliary-microsoft-sharepoint/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/auxiliary-microsoft-sharepoint/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/auxiliary-notion/README.md b/packages/auxiliary-notion/README.md new file mode 100644 index 0000000..9f04f51 --- /dev/null +++ b/packages/auxiliary-notion/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/auxiliary-notion diff --git a/packages/auxiliary-notion/jest.config.js b/packages/auxiliary-notion/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/auxiliary-notion/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/auxiliary-notion/package.json b/packages/auxiliary-notion/package.json new file mode 100644 index 0000000..2fad6c3 --- /dev/null +++ b/packages/auxiliary-notion/package.json @@ -0,0 +1,55 @@ +{ + "name": "@chatbotkit-dev/auxiliary-notion", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./properties": { + "import": "./src/properties.ts" + }, + "./client": { + "import": "./src/client.ts" + }, + "./contents": { + "import": "./src/contents.ts" + }, + "./database": { + "import": "./src/database.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/fetch": "workspace:*", + "@chatbotkit-dev/sql": "workspace:*", + "@notionhq/client": "^2.2.4", + "notion-to-md": "2.5.5" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@chatbotkit-dev/typescript-utils": "workspace:*", + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/auxiliary-notion/src/client.test.ts b/packages/auxiliary-notion/src/client.test.ts new file mode 100644 index 0000000..ff03d3f --- /dev/null +++ b/packages/auxiliary-notion/src/client.test.ts @@ -0,0 +1,19 @@ +import type { TrimmedNonEmptyString } from '@chatbotkit-dev/typescript-utils' + +import { getClient } from './client' + +describe('getClient', () => { + it('should create a Notion client with the provided auth token', () => { + expect(getClient('test_token' as TrimmedNonEmptyString)).toBeDefined() + }) + + it('should create a Notion client with the provided bearer token', () => { + expect( + getClient('Bearer test_token' as TrimmedNonEmptyString) + ).toBeDefined() + }) + + it('should throw if no auth token is provided', () => { + expect(() => getClient('' as TrimmedNonEmptyString)).toThrow() + }) +}) diff --git a/packages/auxiliary-notion/src/client.ts b/packages/auxiliary-notion/src/client.ts new file mode 100644 index 0000000..43d8668 --- /dev/null +++ b/packages/auxiliary-notion/src/client.ts @@ -0,0 +1,28 @@ +import type { TrimmedNonEmptyString } from '@chatbotkit-dev/typescript-utils' + +import { fetch as cbkFetch, withRetry, withTimeout } from '@chatbotkit-dev/fetch' +import { Client } from '@notionhq/client' + +export function getClient( + auth: TrimmedNonEmptyString, + options?: { fetch?: typeof cbkFetch } +): Client { + const clientAuth = auth.replace(/^Bearer\s+/i, '').trim() + + if (!clientAuth) { + throw new Error(`Authentication token not provided`) + } + + return new Client({ + auth: clientAuth as TrimmedNonEmptyString, + + fetch: withRetry( + withTimeout(options?.fetch || cbkFetch, { timeout: 10000 }), + { + retries: 5, + retryDelay: 250, + retryTimeout: true, + } + ), + }) +} diff --git a/packages/auxiliary-notion/src/contents.ts b/packages/auxiliary-notion/src/contents.ts new file mode 100644 index 0000000..2150cf3 --- /dev/null +++ b/packages/auxiliary-notion/src/contents.ts @@ -0,0 +1,26 @@ +import type { fetch as chatbotkitFetch } from '@chatbotkit-dev/fetch' + +import type { TrimmedNonEmptyString } from '@chatbotkit-dev/typescript-utils' + +import { getClient } from './client' + +import { NotionToMarkdown } from 'notion-to-md' + +export async function getContents({ + auth, + pageId, + fetch, +}: { + auth: TrimmedNonEmptyString + pageId: TrimmedNonEmptyString + fetch?: typeof chatbotkitFetch +}): Promise { + const notionClient = getClient(auth, { fetch }) + + const n2m = new NotionToMarkdown({ notionClient }) + + const mdblocks = await n2m.pageToMarkdown(pageId) + const contents = n2m.toMarkdownString(mdblocks) + + return contents +} diff --git a/packages/auxiliary-notion/src/database.test.ts b/packages/auxiliary-notion/src/database.test.ts new file mode 100644 index 0000000..379b3b2 --- /dev/null +++ b/packages/auxiliary-notion/src/database.test.ts @@ -0,0 +1,577 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { + DatabaseProperties} from './database'; +import { + convertDatabaseCreatePropertiesFromKnownProperties, + getMultiSelectUpsertValues, + getSimplifiedDatabaseProperties, +} from './database' + +describe('getSimplifiedDatabaseProperties', () => { + test('should handle title property', () => { + const properties: DatabaseProperties = { + Name: { + id: 'title', + type: 'title', + name: 'Test Title', + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Name: 'Test Title' }) + }) + + test('should handle title property when null', () => { + const properties: DatabaseProperties = { + Name: { + id: 'title', + type: 'title', + name: null, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Name: null }) + }) + + test('should handle status property', () => { + const properties: DatabaseProperties = { + Status: { + id: 'status', + type: 'status', + name: 'In Progress', + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Status: 'In Progress' }) + }) + + test('should handle rich_text property', () => { + const properties: DatabaseProperties = { + Description: { + id: 'desc', + type: 'rich_text', + name: 'Text Description', + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Description: 'Text Description' }) + }) + + test('should handle number property', () => { + const properties: DatabaseProperties = { + Count: { + id: 'count', + type: 'number', + number: 42, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Count: 42 }) + }) + + test('should handle checkbox property', () => { + const properties: DatabaseProperties = { + Completed: { + id: 'completed', + type: 'checkbox', + checkbox: true, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Completed: true }) + }) + + test('should handle select property', () => { + const properties: DatabaseProperties = { + Category: { + id: 'category', + type: 'select', + select: { + options: [ + { name: 'Work', id: '1', color: 'blue' }, + { name: 'Personal', id: '2', color: 'green' }, + ], + }, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Category: ['Work', 'Personal'] }) + }) + + test('should handle multi_select property', () => { + const properties: DatabaseProperties = { + Tags: { + id: 'tags', + type: 'multi_select', + multi_select: { + options: [ + { name: 'urgent', id: '1', color: 'red' }, + { name: 'bug', id: '2', color: 'orange' }, + ], + }, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Tags: ['urgent', 'bug'] }) + }) + + test('should handle date property', () => { + const properties: DatabaseProperties = { + DueDate: { + id: 'due', + type: 'date', + date: { start: '2023-01-01' }, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ DueDate: '2023-01-01' }) + }) + + test('should handle date property when null', () => { + const properties: DatabaseProperties = { + DueDate: { + id: 'due', + type: 'date', + date: null, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ DueDate: null }) + }) + + test('should handle url property', () => { + const properties: DatabaseProperties = { + Website: { + id: 'website', + type: 'url', + url: 'https://example.com', + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Website: 'https://example.com' }) + }) + + test('should handle url property when null', () => { + const properties: DatabaseProperties = { + Website: { + id: 'website', + type: 'url', + url: null, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Website: null }) + }) + + test('should handle email property', () => { + const properties: DatabaseProperties = { + Email: { + id: 'email', + type: 'email', + email: 'test@example.com', + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Email: 'test@example.com' }) + }) + + test('should handle phone_number property', () => { + const properties: DatabaseProperties = { + Phone: { + id: 'phone', + type: 'phone_number', + phone_number: '123-456-7890', + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Phone: '123-456-7890' }) + }) + + test('should handle created_time property', () => { + const timestamp = '2023-01-01T12:00:00Z' + const properties: DatabaseProperties = { + Created: { + id: 'created', + type: 'created_time', + created_time: timestamp, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Created: timestamp }) + }) + + test('should handle last_edited_time property', () => { + const timestamp = '2023-01-01T12:00:00Z' + const properties: DatabaseProperties = { + LastEdited: { + id: 'edited', + type: 'last_edited_time', + last_edited_time: timestamp, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ LastEdited: timestamp }) + }) + + test('should skip ignored property types', () => { + const properties: DatabaseProperties = { + UniqueID: { + id: 'uid', + type: 'unique_id', + unique_id: { number: 123 }, + }, + Formula: { + id: 'formula', + type: 'formula', + formula: { string: 'result' }, + }, + Rollup: { + id: 'rollup', + type: 'rollup', + rollup: { number: 42 }, + }, + Relation: { + id: 'relation', + type: 'relation', + relation: {}, + }, + People: { + id: 'people', + type: 'people', + people: {}, + }, + Files: { + id: 'files', + type: 'files', + files: [], + }, + CreatedBy: { + id: 'created_by', + type: 'created_by', + created_by: {}, + }, + LastEditedBy: { + id: 'last_edited_by', + type: 'last_edited_by', + last_edited_by: {}, + }, + Name: { + id: 'title', + type: 'title', + name: 'Test', + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ Name: 'Test' }) + }) + + test('should handle multiple properties', () => { + const properties: DatabaseProperties = { + Name: { + id: 'title', + type: 'title', + name: 'Test Database', + }, + Done: { + id: 'done', + type: 'checkbox', + checkbox: false, + }, + Priority: { + id: 'priority', + type: 'select', + select: { + options: [ + { name: 'High', id: '1', color: 'red' }, + { name: 'Medium', id: '2', color: 'yellow' }, + { name: 'Low', id: '3', color: 'green' }, + ], + }, + }, + } as any + + const result = getSimplifiedDatabaseProperties(properties) + + expect(result).toEqual({ + Name: 'Test Database', + Done: false, + Priority: ['High', 'Medium', 'Low'], + }) + }) +}) + +describe('getMultiSelectUpsertValues', () => { + test('should return array of trimmed strings from array input', () => { + const input = ['foo', 'bar', 'baz'] + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar', 'baz']) + }) + + test('should split comma-separated strings in array input', () => { + const input = ['foo, bar', 'baz'] + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar', 'baz']) + }) + + test('should trim whitespace from each value in array input', () => { + const input = [' foo ', ' bar ', 'baz'] + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar', 'baz']) + }) + + test('should filter out empty strings in array input', () => { + const input = ['foo', '', ' ', 'bar'] + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar']) + }) + + test('should ignore non-string values in array input', () => { + const input = ['foo', 123 as any, null as any, 'bar'] + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar']) + }) + + test('should return array of trimmed strings from comma-separated string input', () => { + const input = 'foo, bar, baz' + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar', 'baz']) + }) + + test('should handle single string input', () => { + const input = 'foo' + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo']) + }) + + test('should trim whitespace from string input', () => { + const input = ' foo , bar ,baz ' + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar', 'baz']) + }) + + test('should filter out empty strings in string input', () => { + const input = 'foo, , ,bar' + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar']) + }) + + test('should return empty array for non-string, non-array input', () => { + expect(getMultiSelectUpsertValues(123)).toEqual([]) + expect(getMultiSelectUpsertValues(null)).toEqual([]) + expect(getMultiSelectUpsertValues(undefined)).toEqual([]) + expect(getMultiSelectUpsertValues({})).toEqual([]) + }) + + test('should handle String object input', () => { + const input = new String('foo,bar') as any + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar']) + }) + + test('should handle array with String objects', () => { + const input = [new String('foo, bar'), 'baz'] as any + const result = getMultiSelectUpsertValues(input) + + expect(result).toEqual(['foo', 'bar', 'baz']) + }) +}) + +describe('convertDatabaseCreatePropertiesFromKnownProperties', () => { + test('should truncate rich_text content exceeding 2000 characters', () => { + const knownDatabaseProperties = { + Description: { + format: 'rich_text' as const, + type: 'string' as const, + }, + } + + const longContent = 'a'.repeat(2060) + + const result = convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties: { Description: longContent }, + }) + + const richText = result.properties.Description as { + rich_text: { text: { content: string } }[] + } + + expect(richText.rich_text[0].text.content.length).toBeLessThanOrEqual(2000) + expect(richText.rich_text[0].text.content).toBe('a'.repeat(2000)) + }) + + test('should truncate title content exceeding 2000 characters', () => { + const knownDatabaseProperties = { + Name: { + format: 'title' as const, + type: 'string' as const, + }, + } + + const longContent = 'b'.repeat(2050) + + const result = convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties: { Name: longContent }, + }) + + const title = result.properties.Name as { + title: { text: { content: string } }[] + } + + expect(title.title[0].text.content.length).toBeLessThanOrEqual(2000) + expect(title.title[0].text.content).toBe('b'.repeat(2000)) + }) + + test('should not truncate rich_text content within 2000 characters', () => { + const knownDatabaseProperties = { + Description: { + format: 'rich_text' as const, + type: 'string' as const, + }, + } + + const normalContent = 'Hello world' + + const result = convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties: { Description: normalContent }, + }) + + const richText = result.properties.Description as { + rich_text: { text: { content: string } }[] + } + + expect(richText.rich_text[0].text.content).toBe('Hello world') + }) + + test('should handle email property with empty string', () => { + const knownDatabaseProperties = { + Email: { + format: 'email' as const, + type: 'string' as const, + description: 'Valid email address', + }, + } + + const properties = { + Email: '', + } + + const result = convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties, + }) + + // @note Notion API requires email to be null, not empty string + expect(result.properties.Email).toEqual({ email: null }) + }) + + test('should handle email property with valid email', () => { + const knownDatabaseProperties = { + Email: { + format: 'email' as const, + type: 'string' as const, + description: 'Valid email address', + }, + } + + const properties = { + Email: 'test@example.com', + } + + const result = convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties, + }) + + expect(result.properties.Email).toEqual({ email: 'test@example.com' }) + }) + + test('should handle url property with empty string', () => { + const knownDatabaseProperties = { + Website: { + format: 'url' as const, + type: 'string' as const, + description: 'Valid URL', + }, + } + + const properties = { + Website: '', + } + + const result = convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties, + }) + + // @note Notion API requires url to be null, not empty string + expect(result.properties.Website).toEqual({ url: null }) + }) + + test('should handle phone_number property with empty string', () => { + const knownDatabaseProperties = { + Phone: { + format: 'phone_number' as const, + type: 'string' as const, + description: 'Valid phone number', + }, + } + + const properties = { + Phone: '', + } + + const result = convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties, + }) + + // @note Notion API requires phone_number to be null, not empty string + expect(result.properties.Phone).toEqual({ phone_number: null }) + }) +}) diff --git a/packages/auxiliary-notion/src/database.ts b/packages/auxiliary-notion/src/database.ts new file mode 100644 index 0000000..e1ba80d --- /dev/null +++ b/packages/auxiliary-notion/src/database.ts @@ -0,0 +1,766 @@ +import type { fetch as chatbotkitFetch } from '@chatbotkit-dev/fetch' + +import type { Immutable } from '@chatbotkit-dev/typescript-utils/object' +import type { ToReadonlyRecord } from '@chatbotkit-dev/typescript-utils/record' +import type { TrimmedNonEmptyString } from '@chatbotkit-dev/typescript-utils/string' + +import { getClient } from './client' + +import type { + CreatePageParameters, + DatabaseObjectResponse, + QueryDatabaseParameters, + UpdatePageParameters, +} from '@notionhq/client/build/src/api-endpoints' + +export type DatabaseProperties = DatabaseObjectResponse['properties'] + +export type SimplifiedDatabaseProperties = Record + +export function getSimplifiedDatabaseProperties( + properties: Immutable +): SimplifiedDatabaseProperties { + const simplified: SimplifiedDatabaseProperties = {} + + for (const [key, value] of Object.entries(properties)) { + if ( + value.type === 'unique_id' || + value.type === 'formula' || + value.type === 'rollup' || + value.type === 'relation' || + value.type === 'people' || + value.type === 'files' || + value.type === 'created_by' || + value.type === 'last_edited_by' + ) { + continue + } + + switch (value.type) { + case 'title': { + simplified[key] = value.name || null + + break + } + + case 'status': { + simplified[key] = value.name || null + + break + } + + case 'rich_text': { + simplified[key] = value.name || null + + break + } + + case 'number': { + simplified[key] = value.number + + break + } + + case 'checkbox': { + simplified[key] = value.checkbox + + break + } + + case 'select': { + simplified[key] = value.select.options.map( + (item: { name: string }) => item.name + ) + + break + } + + case 'multi_select': { + simplified[key] = value.multi_select.options.map( + (item: { name: string }) => item.name + ) + + break + } + + case 'date': { + if (value.date) { + simplified[key] = value.date.start + } else { + simplified[key] = null + } + + break + } + + case 'url': { + simplified[key] = value.url || null + + break + } + + case 'email': { + simplified[key] = value.email || null + + break + } + + case 'phone_number': { + simplified[key] = value.phone_number || null + + break + } + + case 'created_time': { + simplified[key] = value.created_time + + break + } + + case 'last_edited_time': { + simplified[key] = value.last_edited_time + + break + } + + default: { + const x: never = value + + x + } + } + } + + return simplified +} + +export interface IntrospectedDatabaseProperty { + format: Exclude< + DatabaseProperties[string]['type'], + | 'unique_id' + | 'formula' + | 'rollup' + | 'relation' + | 'people' + | 'files' + | 'created_by' + | 'last_edited_by' + > + type: 'string' | 'number' | 'boolean' + enum?: string[] + description?: string +} + +export async function introspectDatabaseProperties({ + auth, + databaseId, + fetch, +}: { + auth: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + fetch?: typeof chatbotkitFetch +}): Promise> { + const client = getClient(auth, { fetch }) + + const data = await client.databases.retrieve({ + database_id: databaseId, + }) + + const properties: Record = {} + + for (const [name, value] of Object.entries(data.properties)) { + if ( + value.type === 'unique_id' || + value.type === 'formula' || + value.type === 'rollup' || + value.type === 'relation' || + value.type === 'people' || + value.type === 'files' || + value.type === 'created_by' || + value.type === 'last_edited_by' + ) { + continue + } + + switch (value.type) { + case 'title': { + properties[name] = { + format: 'title', + type: 'string', + } + + break + } + + case 'status': { + properties[name] = { + format: 'status', + type: 'string', + enum: value.status.options.map((option) => option.name), + } + + break + } + + case 'rich_text': { + properties[name] = { + format: 'rich_text', + type: 'string', + } + + break + } + + case 'number': { + properties[name] = { + format: 'number', + type: 'number', + } + + break + } + + case 'checkbox': { + properties[name] = { + format: 'checkbox', + type: 'boolean', + } + + break + } + + case 'select': { + properties[name] = { + format: 'select', + type: 'string', + enum: value.select.options.map((option) => option.name), + } + + break + } + + case 'multi_select': { + properties[name] = { + format: 'multi_select', + type: 'string', + description: 'Comma-separated values', + } + + break + } + + case 'date': { + properties[name] = { + format: 'date', + type: 'string', + description: 'Date in ISO 8601 format', + } + + break + } + + case 'url': { + properties[name] = { + format: 'url', + type: 'string', + description: 'Valid URL', + } + + break + } + + case 'email': { + properties[name] = { + format: 'email', + type: 'string', + description: 'Valid email address', + } + + break + } + + case 'phone_number': { + properties[name] = { + format: 'phone_number', + type: 'string', + description: 'Valid phone number', + } + + break + } + + case 'created_time': { + properties[name] = { + format: 'created_time', + type: 'string', + description: 'Creation time in ISO 8601 format', + } + + break + } + + case 'last_edited_time': { + properties[name] = { + format: 'last_edited_time', + type: 'string', + description: 'Last edited time in ISO 8601 format', + } + + break + } + + default: { + const x: never = value + + x + } + } + } + + return properties +} + +export function getMultiSelectUpsertValues(input: unknown): string[] { + if (Array.isArray(input)) { + return input + .filter((item) => typeof item === 'string' || item instanceof String) + .flatMap((item) => item.split(',')) + .map((item) => item.trim()) + .filter(Boolean) + } + + if (typeof input === 'string' || input instanceof String) { + return input + .split(',') + .map((item) => item.trim()) + .filter(Boolean) + } + + return [] +} + +export type DatabaseCreateItemProperties = CreatePageParameters['properties'] + +export function convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties, +}: { + knownDatabaseProperties: ToReadonlyRecord< + Record + > + properties: ToReadonlyRecord> +}): { + unsupported: string[] + properties: DatabaseCreateItemProperties +} { + const databaseProperties: DatabaseCreateItemProperties = {} + + const unsupportedProperties: string[] = Object.keys(properties).filter( + (key) => !(key in knownDatabaseProperties) + ) + + for (const [key, value] of Object.entries(knownDatabaseProperties)) { + if (!(key in properties)) { + continue + } + + if ( + value.format === 'created_time' || + value.format === 'last_edited_time' + ) { + continue + } + + switch (value.format) { + case 'title': { + // @note Notion API limits rich_text content to 2000 characters per block + databaseProperties[key] = { + title: [ + { + type: 'text', + text: { + content: String(properties[key]).slice(0, 2000), + }, + }, + ], + } + + break + } + + case 'status': { + databaseProperties[key] = { + status: { + name: String(properties[key]), + }, + } + + break + } + + case 'rich_text': { + // @note Notion API limits rich_text content to 2000 characters per block + databaseProperties[key] = { + rich_text: [ + { + type: 'text', + text: { + content: String(properties[key]).slice(0, 2000), + }, + }, + ], + } + + break + } + + case 'number': { + databaseProperties[key] = { + number: parseFloat(String(properties[key])), + } + + break + } + + case 'checkbox': { + databaseProperties[key] = { + checkbox: Boolean(properties[key]), + } + + break + } + + case 'select': { + databaseProperties[key] = { + select: { + name: String(properties[key]), + }, + } + + break + } + + case 'multi_select': { + const values = getMultiSelectUpsertValues(properties[key]) + + if (values.length > 0) { + databaseProperties[key] = { + multi_select: values.map((item) => ({ + name: item, + })), + } + } + + break + } + + case 'date': { + databaseProperties[key] = { + date: { + start: String(properties[key]), + }, + } + + break + } + + case 'url': { + const urlValue = String(properties[key]) + + databaseProperties[key] = { + url: urlValue.trim() === '' ? null : urlValue, + } + + break + } + + case 'email': { + const emailValue = String(properties[key]) + + databaseProperties[key] = { + email: emailValue.trim() === '' ? null : emailValue, + } + + break + } + + case 'phone_number': { + const phoneValue = String(properties[key]) + + databaseProperties[key] = { + phone_number: phoneValue.trim() === '' ? null : phoneValue, + } + + break + } + + default: { + const x: never = value.format + + x + } + } + } + + return { + unsupported: unsupportedProperties, + properties: databaseProperties, + } +} + +export async function convertDatabaseCreateProperties({ + auth, + databaseId, + properties, +}: { + auth: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + properties: ToReadonlyRecord> +}): Promise<{ + unsupported: string[] + properties: DatabaseCreateItemProperties +}> { + const knownDatabaseProperties = await introspectDatabaseProperties({ + auth, + databaseId, + }) + + return convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties, + }) +} + +export type DatabaseUpdateItemProperties = UpdatePageParameters['properties'] + +export function convertDatabaseUpdatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties, +}: { + knownDatabaseProperties: ToReadonlyRecord< + Record + > + properties: ToReadonlyRecord> +}): { + unsupported: string[] + properties: DatabaseUpdateItemProperties +} { + return convertDatabaseCreatePropertiesFromKnownProperties({ + knownDatabaseProperties, + properties, + }) +} + +export async function convertDatabaseUpdateProperties({ + auth, + databaseId, + properties, +}: { + auth: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + properties: ToReadonlyRecord> +}): Promise<{ + unsupported: string[] + properties: DatabaseUpdateItemProperties +}> { + const { + unsupported: unsupportedProperties, + properties: convertedProperties, + } = await convertDatabaseCreateProperties({ + auth, + databaseId, + properties, + }) + + return { + unsupported: unsupportedProperties, + properties: convertedProperties, + } +} + +export type DatabaseItemFilter = QueryDatabaseParameters['filter'] + +export function convertDatabaseItemFilterFromKnownProperties({ + knownDatabaseProperties, + query, +}: { + knownDatabaseProperties: ToReadonlyRecord< + Record + > + query: ToReadonlyRecord> +}): { unsupported: string[]; filter: DatabaseItemFilter | undefined } { + const filter: DatabaseItemFilter = { + or: [], + } + + const unsupported: string[] = Object.keys(query).filter( + (key) => !(key in knownDatabaseProperties) + ) + + for (const [key, value] of Object.entries(knownDatabaseProperties)) { + if (!(key in query)) { + continue + } + + if ( + value.format === 'created_time' || + value.format === 'last_edited_time' + ) { + continue + } + + switch (value.format) { + case 'title': { + filter.or.push({ + property: key, + title: { + contains: String(query[key]), + }, + }) + + break + } + + case 'status': { + filter.or.push({ + property: key, + status: { + equals: String(query[key]), + }, + }) + + break + } + + case 'rich_text': { + filter.or.push({ + property: key, + rich_text: { + contains: String(query[key]), + }, + }) + + break + } + + case 'number': { + filter.or.push({ + property: key, + number: { + equals: parseFloat(String(query[key])), + }, + }) + + break + } + + case 'checkbox': { + filter.or.push({ + property: key, + checkbox: { + equals: Boolean(query[key]), + }, + }) + + break + } + + case 'select': { + filter.or.push({ + property: key, + select: { + equals: String(query[key]), + }, + }) + + break + } + + case 'multi_select': { + filter.or.push({ + property: key, + multi_select: { + contains: String(query[key]), + }, + }) + + break + } + + case 'date': { + filter.or.push({ + property: key, + date: { + equals: String(query[key]), + }, + }) + + break + } + + case 'url': { + filter.or.push({ + property: key, + url: { + equals: String(query[key]), + }, + }) + + break + } + + case 'email': { + filter.or.push({ + property: key, + email: { + equals: String(query[key]), + }, + }) + + break + } + + case 'phone_number': { + filter.or.push({ + property: key, + phone_number: { + equals: String(query[key]), + }, + }) + + break + } + + default: { + const x: never = value.format + + x + } + } + } + + return { + unsupported: unsupported, + filter: filter.or.length > 0 ? filter : undefined, + } +} + +export async function convertDatabaseItemFilter({ + auth, + databaseId, + query, +}: { + auth: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + query: ToReadonlyRecord> +}): Promise<{ unsupported: string[]; filter: DatabaseItemFilter | undefined }> { + const knownDatabaseProperties = await introspectDatabaseProperties({ + auth, + databaseId, + }) + + return convertDatabaseItemFilterFromKnownProperties({ + knownDatabaseProperties, + query, + }) +} diff --git a/packages/auxiliary-notion/src/errors.test.ts b/packages/auxiliary-notion/src/errors.test.ts new file mode 100644 index 0000000..76894ce --- /dev/null +++ b/packages/auxiliary-notion/src/errors.test.ts @@ -0,0 +1,23 @@ +import { UnsupportedPropertiesError } from './errors' + +describe('UnsupportedPropertiesError', () => { + it('should format the message from the property names', () => { + const error = new UnsupportedPropertiesError(['Long Summary', 'Priority']) + + expect(error.message).toBe('Unsupported properties: Long Summary, Priority') + }) + + it('should expose the property names', () => { + const error = new UnsupportedPropertiesError(['Long Summary']) + + expect(error.properties).toEqual(['Long Summary']) + }) + + it('should be an instance of Error and carry its own name', () => { + const error = new UnsupportedPropertiesError(['x']) + + expect(error).toBeInstanceOf(Error) + expect(error).toBeInstanceOf(UnsupportedPropertiesError) + expect(error.name).toBe('UnsupportedPropertiesError') + }) +}) diff --git a/packages/auxiliary-notion/src/errors.ts b/packages/auxiliary-notion/src/errors.ts new file mode 100644 index 0000000..7101456 --- /dev/null +++ b/packages/auxiliary-notion/src/errors.ts @@ -0,0 +1,16 @@ +/** + * Thrown when the caller supplies property (or filter) names that do not exist + * in the target Notion database schema. This is a caller-side validation + * problem, not a server or upstream fault, so consumers should surface it as a + * bad request rather than capturing it as an unexpected exception. + */ +export class UnsupportedPropertiesError extends Error { + readonly properties: string[] + + constructor(properties: string[]) { + super(`Unsupported properties: ${properties.join(', ')}`) + + this.name = 'UnsupportedPropertiesError' + this.properties = properties + } +} diff --git a/packages/auxiliary-notion/src/handler.ts b/packages/auxiliary-notion/src/handler.ts new file mode 100644 index 0000000..b296510 --- /dev/null +++ b/packages/auxiliary-notion/src/handler.ts @@ -0,0 +1,847 @@ +import type { fetch as chatbotkitFetch } from '@chatbotkit-dev/fetch' + +import type { PositiveNumber } from '@chatbotkit-dev/typescript-utils/number' +import type { ToReadonlyRecord } from '@chatbotkit-dev/typescript-utils/record' +import type { TrimmedNonEmptyString } from '@chatbotkit-dev/typescript-utils/string' + +import { getClient } from './client' +import { getContents } from './contents' +import type { + DatabaseProperties, + IntrospectedDatabaseProperty, + SimplifiedDatabaseProperties, +} from './database' +import { + convertDatabaseCreateProperties, + convertDatabaseItemFilter, + convertDatabaseUpdateProperties, + getSimplifiedDatabaseProperties, + introspectDatabaseProperties, +} from './database' +import { UnsupportedPropertiesError } from './errors' +import type { PageProperties, SimplifiedPageProperties } from './page' +import { getSimplifiedPageProperties } from './page' + +export type PageEnumerationItem = { + id: string + object: 'page' + parent?: object + created_time?: string + last_edited_time?: string + properties: + | ReturnType + | Record + url?: string +} + +export type DatabaseEnumerationItem = { + id: string + object: 'database' + parent?: object + title?: string + created_time?: string + last_edited_time?: string + properties: + | ReturnType + | Record + url?: string +} + +export type EnumerationItem = PageEnumerationItem | DatabaseEnumerationItem + +export async function searchHandler({ + token, + query, + startCursor: _startCursor, + pageSize: _pageSize, + simplifiedProperties, + fetch, +}: { + token: TrimmedNonEmptyString + query?: TrimmedNonEmptyString + startCursor?: TrimmedNonEmptyString + pageSize?: PositiveNumber + simplifiedProperties?: boolean + fetch?: typeof chatbotkitFetch +}): Promise<{ + items: EnumerationItem[] + cursor: string | undefined +}> { + const client = getClient(token, { fetch }) + + let startCursor = _startCursor ? _startCursor.trim() : undefined + + startCursor = startCursor && startCursor.length > 0 ? startCursor : undefined + + let pageSize = _pageSize ? _pageSize : 20 + + pageSize = Math.min(pageSize, 100) + + const data = await client.search({ + query: query, + start_cursor: startCursor, + page_size: pageSize, + }) + + const { results } = data + + const items: EnumerationItem[] = [] + + for (const item of results) { + switch (item.object) { + case 'page': { + const id = item.id + + const object = item.object + + const parent = 'parent' in item ? item.parent : undefined + + const created_time = + 'created_time' in item ? item.created_time : undefined + const last_edited_time = + 'last_edited_time' in item ? item.last_edited_time : undefined + + const properties = 'properties' in item ? item.properties : {} + + const url = 'url' in item ? item.url : undefined + + items.push({ + id, + + object, + + parent, + + created_time, + last_edited_time, + + properties: simplifiedProperties + ? getSimplifiedPageProperties(properties) + : properties, + + url, + }) + + break + } + + case 'database': { + const id = item.id + + const object = item.object + + const parent = 'parent' in item ? item.parent : undefined + + const title = + 'title' in item + ? item.title.map(({ plain_text }) => plain_text).join(' ') + : undefined + + const created_time = + 'created_time' in item ? item.created_time : undefined + const last_edited_time = + 'last_edited_time' in item ? item.last_edited_time : undefined + + const properties = 'properties' in item ? item.properties : {} + + const url = 'url' in item ? item.url : undefined + + items.push({ + id, + + object, + + parent, + + title, + + created_time, + last_edited_time, + + properties: simplifiedProperties + ? getSimplifiedDatabaseProperties(properties) + : properties, + + url, + }) + + break + } + + default: { + const x: never = item + + x + } + } + } + + return { + items, + cursor: data.next_cursor ? data.next_cursor : undefined, + } +} + +export async function listHandler({ + token, + startCursor, + pageSize, + simplifiedProperties, +}: { + token: TrimmedNonEmptyString + startCursor?: TrimmedNonEmptyString + pageSize?: PositiveNumber + simplifiedProperties?: boolean +}): Promise<{ + items: EnumerationItem[] + cursor: string | undefined +}> { + return await searchHandler({ + token, + startCursor, + pageSize, + simplifiedProperties, + }) +} + +export async function listPagesHandler({ + token, + startCursor: _startCursor, + pageSize: _pageSize, + simplifiedProperties, + fetch, +}: { + token: TrimmedNonEmptyString + startCursor?: TrimmedNonEmptyString + pageSize?: PositiveNumber + simplifiedProperties?: boolean + fetch?: typeof chatbotkitFetch +}): Promise<{ + pages: PageEnumerationItem[] + cursor: string | undefined +}> { + const client = getClient(token, { fetch }) + + let startCursor = _startCursor ? _startCursor.trim() : undefined + + startCursor = startCursor && startCursor.length > 0 ? startCursor : undefined + + let pageSize = _pageSize ? _pageSize : 20 + + pageSize = Math.min(pageSize, 100) + + const data = await client.search({ + filter: { + property: 'object', + value: 'page', + }, + start_cursor: startCursor, + page_size: pageSize, + }) + + const { results } = data + + const pages: PageEnumerationItem[] = [] + + for (const item of results) { + switch (item.object) { + case 'page': { + const id = item.id + + const object = item.object + + const parent = 'parent' in item ? item.parent : undefined + + const created_time = + 'created_time' in item ? item.created_time : undefined + const last_edited_time = + 'last_edited_time' in item ? item.last_edited_time : undefined + + const properties = 'properties' in item ? item.properties : {} + + const url = 'url' in item ? item.url : undefined + + pages.push({ + id, + + object, + + parent, + + created_time, + last_edited_time, + + properties: simplifiedProperties + ? getSimplifiedPageProperties(properties) + : properties, + + url, + }) + + break + } + + case 'database': { + break + } + + default: { + const x: never = item + + x + } + } + } + + return { + pages, + cursor: data.next_cursor ? data.next_cursor : undefined, + } +} + +export async function listDatabasesHandler({ + token, + startCursor: _startCursor, + pageSize: _pageSize, + simplifiedProperties, + fetch, +}: { + token: TrimmedNonEmptyString + startCursor?: TrimmedNonEmptyString + pageSize?: PositiveNumber + simplifiedProperties?: boolean + fetch?: typeof chatbotkitFetch +}): Promise<{ + databases: DatabaseEnumerationItem[] + cursor: string | undefined +}> { + const client = getClient(token, { fetch }) + + let startCursor = _startCursor ? _startCursor.trim() : undefined + + startCursor = startCursor && startCursor.length > 0 ? startCursor : undefined + + let pageSize = _pageSize ? _pageSize : 20 + + pageSize = Math.min(pageSize, 100) + + const data = await client.search({ + filter: { + property: 'object', + value: 'database', + }, + start_cursor: startCursor, + page_size: pageSize, + }) + + const { results } = data + + const databases: DatabaseEnumerationItem[] = [] + + for (const item of results) { + switch (item.object) { + case 'page': { + break + } + + case 'database': { + const id = item.id + + const object = item.object + + const parent = 'parent' in item ? item.parent : undefined + + const title = + 'title' in item + ? item.title.map(({ plain_text }) => plain_text).join(' ') + : undefined + + const created_time = + 'created_time' in item ? item.created_time : undefined + const last_edited_time = + 'last_edited_time' in item ? item.last_edited_time : undefined + + const properties = 'properties' in item ? item.properties : {} + + const url = 'url' in item ? item.url : undefined + + databases.push({ + id, + + object, + + parent, + + title, + + created_time, + last_edited_time, + + properties: simplifiedProperties + ? getSimplifiedDatabaseProperties(properties) + : properties, + + url, + }) + + break + } + + default: { + const x: never = item + + x + } + } + } + + return { + databases, + cursor: data.next_cursor ? data.next_cursor : undefined, + } +} + +export async function fetchPageHandler({ + token, + pageId, + simplifiedProperties, + fetch, +}: { + token: TrimmedNonEmptyString + pageId: TrimmedNonEmptyString + simplifiedProperties?: boolean + fetch?: typeof chatbotkitFetch +}): Promise<{ + page: { + id: string + + object: 'page' + + created_time?: string + last_edited_time?: string + + properties: SimplifiedPageProperties | PageProperties + + url?: string + } + contents: string +}> { + const client = getClient(token, { fetch }) + + const page = await client.pages.retrieve({ + page_id: pageId, + }) + + const id = page.id + + const object = page.object + + const created_time = 'created_time' in page ? page.created_time : undefined + const last_edited_time = + 'last_edited_time' in page ? page.last_edited_time : undefined + + const properties = 'properties' in page ? page.properties : {} + + const url = 'url' in page ? page.url : undefined + + const contents = await getContents({ auth: token, pageId }) + + return { + page: { + id, + + object, + + created_time, + last_edited_time, + + properties: simplifiedProperties + ? getSimplifiedPageProperties(properties) + : properties, + + url, + }, + contents, + } +} + +export async function introspectDatabaseHandler({ + token, + databaseId, + fetch, +}: { + token: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + fetch?: typeof chatbotkitFetch +}): Promise> { + return await introspectDatabaseProperties({ auth: token, databaseId, fetch }) +} + +export async function searchDatabaseHandler({ + token, + databaseId, + query, + startCursor: _startCursor, + pageSize: _pageSize, + simplifiedProperties, + fetch, +}: { + token: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + query?: ToReadonlyRecord> + startCursor?: TrimmedNonEmptyString + pageSize?: PositiveNumber + simplifiedProperties?: boolean + fetch?: typeof chatbotkitFetch +}): Promise<{ + items: EnumerationItem[] + cursor: string | undefined +}> { + const client = getClient(token, { fetch }) + + let filter + + { + if (query) { + const { unsupported, filter: _filter } = await convertDatabaseItemFilter({ + auth: token, + databaseId, + query, + }) + + if (unsupported.length > 0) { + throw new UnsupportedPropertiesError(unsupported) + } + + filter = _filter + } else { + filter = undefined + } + } + + let startCursor = _startCursor ? _startCursor.trim() : undefined + + startCursor = startCursor && startCursor.length > 0 ? startCursor : undefined + + let pageSize = _pageSize ? _pageSize : 20 + + pageSize = Math.min(pageSize, 100) + + const data = await client.databases.query({ + database_id: databaseId, + filter: filter, + start_cursor: startCursor, + page_size: pageSize, + }) + + const { results } = data + + const items: EnumerationItem[] = [] + + for (const item of results) { + switch (item.object) { + case 'page': { + const id = item.id + + const object = item.object + + const parent = 'parent' in item ? item.parent : undefined + + const created_time = + 'created_time' in item ? item.created_time : undefined + const last_edited_time = + 'last_edited_time' in item ? item.last_edited_time : undefined + + const properties = 'properties' in item ? item.properties : {} + + const url = 'url' in item ? item.url : undefined + + items.push({ + id, + + object, + + parent, + + created_time, + last_edited_time, + + properties: simplifiedProperties + ? getSimplifiedPageProperties(properties) + : properties, + + url, + }) + + break + } + + case 'database': { + const id = item.id + + const object = item.object + + const parent = 'parent' in item ? item.parent : undefined + + const title = + 'title' in item + ? item.title.map(({ plain_text }) => plain_text).join(' ') + : undefined + + const created_time = + 'created_time' in item ? item.created_time : undefined + const last_edited_time = + 'last_edited_time' in item ? item.last_edited_time : undefined + + const properties = 'properties' in item ? item.properties : {} + + const url = 'url' in item ? item.url : undefined + + items.push({ + id, + + object, + + parent, + + title, + + created_time, + last_edited_time, + + properties: simplifiedProperties + ? getSimplifiedDatabaseProperties(properties) + : properties, + + url, + }) + + break + } + + default: { + const x: never = item + + x + } + } + } + + return { + items, + cursor: data.next_cursor ? data.next_cursor : undefined, + } +} + +export async function listDatabaseItemsHandler({ + token, + databaseId, + startCursor, + pageSize, + simplifiedProperties, + fetch, +}: { + token: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + startCursor?: TrimmedNonEmptyString + pageSize?: PositiveNumber + simplifiedProperties?: boolean + fetch?: typeof chatbotkitFetch +}): Promise<{ + items: EnumerationItem[] + cursor: string | undefined +}> { + return await searchDatabaseHandler({ + token, + databaseId, + startCursor, + pageSize, + simplifiedProperties, + fetch, + }) +} + +export async function fetchDatabaseItemHandler({ + token, + databaseId, + itemId, + simplifiedProperties, + fetch, +}: { + token: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + itemId: TrimmedNonEmptyString + simplifiedProperties?: boolean + fetch?: typeof chatbotkitFetch +}): Promise<{ + item: { + id: string + + object: 'page' | 'database' + + created_time?: string + last_edited_time?: string + + properties: SimplifiedDatabaseProperties | DatabaseProperties + + url?: string + } + contents: string +}> { + databaseId // @todo validate the item belongs to the database + + const client = getClient(token, { fetch }) + + const data = await client.pages.retrieve({ + page_id: itemId, + }) + + const id = data.id + + const created_time = 'created_time' in data ? data.created_time : undefined + const last_edited_time = + 'last_edited_time' in data ? data.last_edited_time : undefined + + const properties = 'properties' in data ? data.properties : {} + + const url = 'url' in data ? data.url : undefined + + return { + item: { + id, + + object: data.object, + + created_time, + last_edited_time, + + properties: simplifiedProperties + ? getSimplifiedPageProperties(properties) + : properties, + + url, + }, + contents: await getContents({ auth: token, pageId: itemId }), + } +} + +export async function createDatabaseItemHandler({ + token, + databaseId, + properties, + fetch, +}: { + token: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + properties: ToReadonlyRecord> + fetch?: typeof chatbotkitFetch +}): Promise<{ + id: string + object: 'page' + url?: string +}> { + const client = getClient(token, { fetch }) + + const { unsupported: unsupportedProperties, properties: databaseProperties } = + await convertDatabaseCreateProperties({ + auth: token, + databaseId, + properties, + }) + + if (unsupportedProperties.length > 0) { + throw new UnsupportedPropertiesError(unsupportedProperties) + } + + const data = await client.pages.create({ + parent: { + database_id: databaseId, + }, + properties: databaseProperties, + }) + + return { + id: data.id, + + object: data.object, + + url: 'url' in data ? data.url : undefined, + } +} + +export async function updateDatabaseItemHandler({ + token, + databaseId, + itemId, + properties, + fetch, +}: { + token: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + itemId: TrimmedNonEmptyString + properties: ToReadonlyRecord> + fetch?: typeof chatbotkitFetch +}): Promise<{ + id: string + object: 'page' + url?: string +}> { + const client = getClient(token, { fetch }) + + const { unsupported: unsupportedProperties, properties: databaseProperties } = + await convertDatabaseUpdateProperties({ + auth: token, + databaseId, + properties, + }) + + if (unsupportedProperties.length > 0) { + throw new UnsupportedPropertiesError(unsupportedProperties) + } + + const data = await client.pages.update({ + page_id: itemId, + properties: databaseProperties, + }) + + return { + id: data.id, + + object: data.object, + + url: 'url' in data ? data.url : undefined, + } +} + +export async function deleteDatabaseItemHandler({ + token, + databaseId, + itemId, + fetch, +}: { + token: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + itemId: TrimmedNonEmptyString + fetch?: typeof chatbotkitFetch +}): Promise<{ + id: string + object: 'page' + url?: string +}> { + databaseId // @todo validate the item belongs to the database + + const client = getClient(token, { fetch }) + + const data = await client.pages.update({ + page_id: itemId, + archived: true, + }) + + return { + id: data.id, + + object: data.object, + + url: 'url' in data ? data.url : undefined, + } +} diff --git a/packages/auxiliary-notion/src/index.ts b/packages/auxiliary-notion/src/index.ts new file mode 100644 index 0000000..e0ce61f --- /dev/null +++ b/packages/auxiliary-notion/src/index.ts @@ -0,0 +1,6 @@ +export * from './client' +export * from './contents' +export * from './errors' +export * from './page' +export * from './database' +export * from './handler' diff --git a/packages/auxiliary-notion/src/page.test.ts b/packages/auxiliary-notion/src/page.test.ts new file mode 100644 index 0000000..07c94d9 --- /dev/null +++ b/packages/auxiliary-notion/src/page.test.ts @@ -0,0 +1,319 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { PageProperties} from './page'; +import { getSimplifiedPageProperties } from './page' + +describe('getSimplifiedPageProperties', () => { + test('should handle title property', () => { + const properties: PageProperties = { + Title: { + id: 'title', + type: 'title', + title: [ + { plain_text: 'Hello', type: 'text', annotations: {}, href: null }, + ], + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Title: 'Hello' }) + }) + + test('should handle status property', () => { + const properties: PageProperties = { + Status: { + id: 'status', + type: 'status', + status: { name: 'Done', color: 'green', id: '1' }, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Status: 'Done' }) + }) + + test('should handle status property when null', () => { + const properties: PageProperties = { + Status: { + id: 'status', + type: 'status', + status: null, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Status: null }) + }) + + test('should handle rich_text property', () => { + const properties: PageProperties = { + Description: { + id: 'desc', + type: 'rich_text', + rich_text: [ + { plain_text: 'Hello', type: 'text', annotations: {}, href: null }, + { plain_text: 'World', type: 'text', annotations: {}, href: null }, + ], + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Description: 'Hello World' }) + }) + + test('should handle number property', () => { + const properties: PageProperties = { + Count: { + id: 'count', + type: 'number', + number: 42, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Count: 42 }) + }) + + test('should handle checkbox property', () => { + const properties: PageProperties = { + Completed: { + id: 'completed', + type: 'checkbox', + checkbox: true, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Completed: true }) + }) + + test('should handle select property', () => { + const properties: PageProperties = { + Category: { + id: 'category', + type: 'select', + select: { name: 'Work', color: 'blue', id: '1' }, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Category: 'Work' }) + }) + + test('should handle select property when null', () => { + const properties: PageProperties = { + Category: { + id: 'category', + type: 'select', + select: null, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Category: null }) + }) + + test('should handle multi_select property', () => { + const properties: PageProperties = { + Tags: { + id: 'tags', + type: 'multi_select', + multi_select: [ + { name: 'urgent', color: 'red', id: '1' }, + { name: 'bug', color: 'orange', id: '2' }, + ], + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Tags: ['urgent', 'bug'] }) + }) + + test('should handle date property', () => { + const properties: PageProperties = { + DueDate: { + id: 'due', + type: 'date', + date: { start: '2023-01-01', end: null, time_zone: null }, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ DueDate: '2023-01-01' }) + }) + + test('should handle date property when null', () => { + const properties: PageProperties = { + DueDate: { + id: 'due', + type: 'date', + date: null, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ DueDate: null }) + }) + + test('should handle url property', () => { + const properties: PageProperties = { + Website: { + id: 'website', + type: 'url', + url: 'https://example.com', + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Website: 'https://example.com' }) + }) + + test('should handle url property when null', () => { + const properties: PageProperties = { + Website: { + id: 'website', + type: 'url', + url: null, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Website: null }) + }) + + test('should handle email property', () => { + const properties: PageProperties = { + Email: { + id: 'email', + type: 'email', + email: 'test@example.com', + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Email: 'test@example.com' }) + }) + + test('should handle phone_number property', () => { + const properties: PageProperties = { + Phone: { + id: 'phone', + type: 'phone_number', + phone_number: '123-456-7890', + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Phone: '123-456-7890' }) + }) + + test('should handle created_time property', () => { + const timestamp = '2023-01-01T12:00:00Z' + const properties: PageProperties = { + Created: { + id: 'created', + type: 'created_time', + created_time: timestamp, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Created: timestamp }) + }) + + test('should handle last_edited_time property', () => { + const timestamp = '2023-01-01T12:00:00Z' + const properties: PageProperties = { + LastEdited: { + id: 'edited', + type: 'last_edited_time', + last_edited_time: timestamp, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ LastEdited: timestamp }) + }) + + test('should skip ignored property types', () => { + const properties: PageProperties = { + UniqueID: { + id: 'uid', + type: 'unique_id', + unique_id: { number: 123, prefix: null }, + }, + Formula: { + id: 'formula', + type: 'formula', + formula: { string: 'result' }, + }, + Rollup: { + id: 'rollup', + type: 'rollup', + rollup: { number: 42 }, + }, + Name: { + id: 'title', + type: 'title', + title: [ + { plain_text: 'Test', type: 'text', annotations: {}, href: null }, + ], + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ Name: 'Test' }) + }) + + test('should handle multiple properties', () => { + const properties: PageProperties = { + Name: { + id: 'title', + type: 'title', + title: [ + { + plain_text: 'Test Task', + type: 'text', + annotations: {}, + href: null, + }, + ], + }, + Done: { + id: 'done', + type: 'checkbox', + checkbox: false, + }, + Priority: { + id: 'priority', + type: 'select', + select: { name: 'High', color: 'red', id: '1' }, + }, + } as any + + const result = getSimplifiedPageProperties(properties) + + expect(result).toEqual({ + Name: 'Test Task', + Done: false, + Priority: 'High', + }) + }) +}) diff --git a/packages/auxiliary-notion/src/page.ts b/packages/auxiliary-notion/src/page.ts new file mode 100644 index 0000000..4e3d4a9 --- /dev/null +++ b/packages/auxiliary-notion/src/page.ts @@ -0,0 +1,128 @@ +import type { Immutable } from '@chatbotkit-dev/typescript-utils/object' + +import type { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints' + +export type PageProperties = PageObjectResponse['properties'] + +export type SimplifiedPageProperties = Record + +export function getSimplifiedPageProperties( + properties: Immutable +): SimplifiedPageProperties { + const simplified: SimplifiedPageProperties = {} + + for (const [key, value] of Object.entries(properties)) { + if ( + value.type === 'unique_id' || + value.type === 'formula' || + value.type === 'rollup' || + value.type === 'relation' || + value.type === 'people' || + value.type === 'files' || + value.type === 'button' || + value.type === 'created_by' || + value.type === 'last_edited_by' || + value.type === 'verification' + ) { + continue + } + + switch (value.type) { + case 'title': { + simplified[key] = value.title + .map((item: { plain_text: string }) => item.plain_text) + .join(' ') + + break + } + + case 'status': { + simplified[key] = value.status?.name || null + + break + } + + case 'rich_text': { + simplified[key] = value.rich_text + .map((item: { plain_text: string }) => item.plain_text) + .join(' ') + + break + } + + case 'number': { + simplified[key] = value.number + + break + } + + case 'checkbox': { + simplified[key] = value.checkbox + + break + } + + case 'select': { + simplified[key] = value.select?.name || null + + break + } + + case 'multi_select': { + simplified[key] = value.multi_select.map( + (item: { name: string }) => item.name + ) + + break + } + + case 'date': { + if (value.date) { + simplified[key] = value.date.start + } else { + simplified[key] = null + } + + break + } + + case 'url': { + simplified[key] = value.url || null + + break + } + + case 'email': { + simplified[key] = value.email || null + + break + } + + case 'phone_number': { + simplified[key] = value.phone_number || null + + break + } + + case 'created_time': { + simplified[key] = value.created_time + + break + } + + case 'last_edited_time': { + simplified[key] = value.last_edited_time + + break + } + + default: { + const x: never = value + + x + } + } + } + + return simplified +} diff --git a/packages/auxiliary-notion/src/sql.test.ts b/packages/auxiliary-notion/src/sql.test.ts new file mode 100644 index 0000000..401dc35 --- /dev/null +++ b/packages/auxiliary-notion/src/sql.test.ts @@ -0,0 +1,397 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { TrimmedNonEmptyString } from '@chatbotkit-dev/typescript-utils' + +import { jest } from '@jest/globals' + +jest.unstable_mockModule('./client', () => ({ + getClient: jest.fn(), +})) + +jest.unstable_mockModule('./database', () => ({ + introspectDatabaseProperties: jest.fn(), + convertDatabaseItemFilter: jest.fn(), + convertDatabaseCreateProperties: jest.fn(), + convertDatabaseUpdateProperties: jest.fn(), + getSimplifiedDatabaseProperties: jest.fn(), +})) + +jest.unstable_mockModule('./page', () => ({ + getSimplifiedPageProperties: jest.fn(), +})) + +const { getClient } = await import('./client') +const { + introspectDatabaseProperties, + convertDatabaseItemFilter, + convertDatabaseCreateProperties, + convertDatabaseUpdateProperties, +} = await import('./database') +const { getSimplifiedPageProperties } = await import('./page') +const { UnsupportedPropertiesError } = await import('./errors') +const { DatabaseDriver } = await import('./sql') + +const TOKEN = 'test_token' as TrimmedNonEmptyString +const DATABASE_ID = 'test_database_id' as TrimmedNonEmptyString + +const mockedGetClient = getClient as jest.MockedFunction +const mockedIntrospect = introspectDatabaseProperties as jest.MockedFunction< + typeof introspectDatabaseProperties +> +const mockedConvertFilter = convertDatabaseItemFilter as jest.MockedFunction< + typeof convertDatabaseItemFilter +> +const mockedConvertCreate = + convertDatabaseCreateProperties as jest.MockedFunction< + typeof convertDatabaseCreateProperties + > +const mockedConvertUpdate = + convertDatabaseUpdateProperties as jest.MockedFunction< + typeof convertDatabaseUpdateProperties + > +const mockedGetSimplifiedPage = + getSimplifiedPageProperties as jest.MockedFunction< + typeof getSimplifiedPageProperties + > + +describe('DatabaseDriver', () => { + let mockClient: any + + beforeEach(() => { + jest.clearAllMocks() + + mockClient = { + pages: { + retrieve: jest.fn(), + create: jest.fn(), + update: jest.fn(), + }, + databases: { + query: jest.fn(), + }, + } + + mockedGetClient.mockReturnValue(mockClient) + }) + + describe('describeColumns', () => { + it('should return id column plus introspected database columns', async () => { + mockedIntrospect.mockResolvedValue({ + Name: { type: 'string', format: 'title', enum: undefined }, + Status: { + type: 'string', + format: 'select', + enum: ['Active', 'Inactive'], + }, + }) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + const columns = await driver.describeColumns() + + expect(columns).toEqual([ + { name: 'id', type: 'string' }, + { name: 'Name', type: 'string', options: undefined }, + { name: 'Status', type: 'string', options: ['Active', 'Inactive'] }, + ]) + + expect(introspectDatabaseProperties).toHaveBeenCalledWith({ + auth: TOKEN, + databaseId: DATABASE_ID, + }) + }) + }) + + describe('doSelect', () => { + it('should query by page id when id is in where clause', async () => { + const mockPage = { + id: 'page_123', + object: 'page', + properties: { + Name: { type: 'title', title: [{ plain_text: 'Test' }] }, + }, + url: 'https://notion.so/test', + } + + mockClient.pages.retrieve.mockResolvedValue(mockPage) + mockedGetSimplifiedPage.mockReturnValue({ Name: 'Test' } as any) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + const results = await driver.doSelect([], { + or: [ + { + and: [ + { + column: 'id', + operator: 'EQ', + criteria: { type: 'string', value: 'page_123' }, + }, + ], + }, + ], + }) + + expect(mockClient.pages.retrieve).toHaveBeenCalledWith({ + page_id: 'page_123', + }) + + expect(results).toEqual([ + { + row: { id: 'page_123', Name: 'Test' }, + url: 'https://notion.so/test', + }, + ]) + }) + + it('should query database using constructor databaseId, not where properties', async () => { + mockedConvertFilter.mockResolvedValue({ + unsupported: [], + filter: { property: 'Name', rich_text: { equals: 'John' } }, + }) + + mockClient.databases.query.mockResolvedValue({ + results: [ + { + object: 'page', + properties: {}, + url: 'https://notion.so/test', + }, + ], + }) + + mockedGetSimplifiedPage.mockReturnValue({ Name: 'John' } as any) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await driver.doSelect([], { + or: [ + { + and: [ + { + column: 'Name', + operator: 'EQ', + criteria: { type: 'string', value: 'John' }, + }, + ], + }, + ], + }) + + expect(mockClient.databases.query).toHaveBeenCalledWith( + expect.objectContaining({ + database_id: DATABASE_ID, + }) + ) + }) + + it('should not use database_id from where properties', async () => { + mockedConvertFilter.mockResolvedValue({ + unsupported: [], + filter: undefined, + }) + + mockClient.databases.query.mockResolvedValue({ results: [] }) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await driver.doSelect([], { + or: [ + { + and: [ + { + column: 'database_id', + operator: 'EQ', + criteria: { type: 'string', value: 'some_other_database' }, + }, + ], + }, + ], + }) + + const callArg = mockClient.databases.query.mock.calls[0][0] + + expect(callArg.database_id).toBe(DATABASE_ID) + expect(callArg.database_id).not.toBe('some_other_database') + }) + + it('should throw when unsupported properties are present in filter', async () => { + mockedConvertFilter.mockResolvedValue({ + unsupported: ['unknownField'], + filter: undefined, + }) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await expect( + driver.doSelect([], { + or: [ + { + and: [ + { + column: 'unknownField', + operator: 'EQ', + criteria: { type: 'string', value: 'x' }, + }, + ], + }, + ], + }) + ).rejects.toThrow('Unsupported properties: unknownField') + }) + + it('should return empty array when database query returns no results', async () => { + mockedConvertFilter.mockResolvedValue({ + unsupported: [], + filter: undefined, + }) + + mockClient.databases.query.mockResolvedValue({ results: [] }) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + const results = await driver.doSelect([]) + + expect(results).toEqual([]) + }) + }) + + describe('doInsert', () => { + it('should create a page in the correct database', async () => { + mockedConvertCreate.mockResolvedValue({ + unsupported: [], + properties: { Name: { title: [{ text: { content: 'New Item' } }] } }, + }) + + mockClient.pages.create.mockResolvedValue({}) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await driver.doInsert({ Name: 'New Item' }) + + expect(mockClient.pages.create).toHaveBeenCalledWith({ + parent: { database_id: DATABASE_ID }, + properties: { Name: { title: [{ text: { content: 'New Item' } }] } }, + }) + }) + + it('should throw when insert has unsupported properties', async () => { + mockedConvertCreate.mockResolvedValue({ + unsupported: ['unknownProp'], + properties: {}, + }) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await expect(driver.doInsert({ unknownProp: 'value' })).rejects.toThrow( + 'Unsupported properties: unknownProp' + ) + + await expect( + driver.doInsert({ unknownProp: 'value' }) + ).rejects.toBeInstanceOf(UnsupportedPropertiesError) + }) + }) + + describe('doUpdate', () => { + it('should update a page using the row id', async () => { + mockedConvertUpdate.mockResolvedValue({ + unsupported: [], + properties: { Status: { select: { name: 'Done' } } }, + }) + + mockClient.pages.update.mockResolvedValue({}) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await driver.doUpdate({ row: { id: 'page_456' } }, { Status: 'Done' }) + + expect(mockClient.pages.update).toHaveBeenCalledWith({ + page_id: 'page_456', + properties: { Status: { select: { name: 'Done' } } }, + }) + }) + + it('should throw when row id is missing', async () => { + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await expect( + driver.doUpdate({ row: {} }, { Status: 'Done' }) + ).rejects.toThrow('Cannot update row: missing id') + }) + + it('should throw when update has unsupported properties', async () => { + mockedConvertUpdate.mockResolvedValue({ + unsupported: ['unknownProp'], + properties: {}, + }) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await expect( + driver.doUpdate({ row: { id: 'page_456' } }, { unknownProp: 'value' }) + ).rejects.toThrow('Unsupported properties: unknownProp') + }) + }) + + describe('doDelete', () => { + it('should archive a page using the row id', async () => { + mockClient.pages.update.mockResolvedValue({}) + + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await driver.doDelete({ row: { id: 'page_789' } }) + + expect(mockClient.pages.update).toHaveBeenCalledWith({ + page_id: 'page_789', + archived: true, + }) + }) + + it('should throw when row id is missing', async () => { + const driver = new DatabaseDriver({ + token: TOKEN, + databaseId: DATABASE_ID, + }) + + await expect(driver.doDelete({ row: {} })).rejects.toThrow( + 'Cannot delete row: missing id' + ) + }) + }) +}) diff --git a/packages/auxiliary-notion/src/sql.ts b/packages/auxiliary-notion/src/sql.ts new file mode 100644 index 0000000..cfd2d4b --- /dev/null +++ b/packages/auxiliary-notion/src/sql.ts @@ -0,0 +1,221 @@ +import type { fetch as chatbotkitFetch } from '@chatbotkit-dev/fetch' + +import type { + Column, + WhereStatement} from '@chatbotkit-dev/sql'; +import { + GenericDriver, + getWhereProperties, +} from '@chatbotkit-dev/sql' +import type { TrimmedNonEmptyString } from '@chatbotkit-dev/typescript-utils' + +import { getClient } from './client' +import { + convertDatabaseCreateProperties, + convertDatabaseItemFilter, + convertDatabaseUpdateProperties, + getSimplifiedDatabaseProperties, + introspectDatabaseProperties, +} from './database' +import { UnsupportedPropertiesError } from './errors' +import { getSimplifiedPageProperties } from './page' + +type DatabaseRow = Record + +export class DatabaseDriver extends GenericDriver { + #token: TrimmedNonEmptyString + #databaseId: TrimmedNonEmptyString + fetch?: typeof chatbotkitFetch + + constructor({ + token, + databaseId, + fetch, + }: { + token: TrimmedNonEmptyString + databaseId: TrimmedNonEmptyString + fetch?: typeof chatbotkitFetch + }) { + super() + + this.#token = token + this.#databaseId = databaseId + this.fetch = fetch + } + + async describeColumns(): Promise { + const properties = await introspectDatabaseProperties({ + auth: this.#token, + databaseId: this.#databaseId, + }) + + return [ + { + name: 'id', + type: 'string', + }, + + ...Object.entries(properties).map( + ([name, { type, ['enum']: options }]) => { + return { + name, + type, + options, + } + } + ), + ] + } + + async doSelect(columns: string[], where?: WhereStatement) { + const properties = where ? getWhereProperties(where) : {} + + const client = getClient(this.#token, { fetch: this.fetch }) + + if ('id' in properties) { + const data = await client.pages.retrieve({ + page_id: properties.id as string, + }) + + // @todo validate against this.#databaseId + + return [ + { + row: { + id: data.id, + ...('properties' in data + ? getSimplifiedPageProperties(data.properties) + : {}), + }, + + url: 'url' in data ? data.url : undefined, + }, + ] + } else { + const { unsupported, filter } = await convertDatabaseItemFilter({ + auth: this.#token, + databaseId: this.#databaseId, + query: properties, + }) + + if (unsupported.length > 0) { + throw new UnsupportedPropertiesError(unsupported) + } + + const data = await client.databases.query({ + database_id: this.#databaseId, + filter: filter, + }) + + const { results } = data + + return results.map((result) => { + switch (result.object) { + case 'page': { + return { + row: { + id: result.id, + ...('properties' in result + ? getSimplifiedPageProperties(result.properties) + : {}), + }, + + url: 'url' in result ? result.url : undefined, + } + } + + case 'database': { + return { + row: { + id: result.id, + ...('properties' in result + ? getSimplifiedDatabaseProperties(result.properties) + : {}), + }, + + url: 'url' in result ? result.url : undefined, + } + } + + default: { + const x: never = result + + x + + throw new Error(`Unsupported property type`) + } + } + }) + } + } + + async doInsert(parameters: Record) { + const client = getClient(this.#token, { fetch: this.fetch }) + + const { + unsupported: unsupportedProperties, + properties: databaseProperties, + } = await convertDatabaseCreateProperties({ + auth: this.#token, + databaseId: this.#databaseId, + properties: parameters, + }) + + if (unsupportedProperties.length > 0) { + throw new UnsupportedPropertiesError(unsupportedProperties) + } + + await client.pages.create({ + parent: { + database_id: this.#databaseId, + }, + properties: databaseProperties, + }) + } + + async doUpdate( + { row }: { row: DatabaseRow }, + parameters: Record + ) { + const pageId = row.id as string + + if (!pageId) { + throw new Error('Cannot update row: missing id') + } + + const client = getClient(this.#token, { fetch: this.fetch }) + + const { + unsupported: unsupportedProperties, + properties: databaseProperties, + } = await convertDatabaseUpdateProperties({ + auth: this.#token, + databaseId: this.#databaseId, + properties: parameters, + }) + + if (unsupportedProperties.length > 0) { + throw new UnsupportedPropertiesError(unsupportedProperties) + } + + await client.pages.update({ + page_id: pageId, + properties: databaseProperties, + }) + } + + async doDelete({ row }: { row: DatabaseRow }) { + const pageId = row.id as string + + if (!pageId) { + throw new Error('Cannot delete row: missing id') + } + + const client = getClient(this.#token, { fetch: this.fetch }) + + await client.pages.update({ + page_id: pageId, + archived: true, + }) + } +} diff --git a/packages/auxiliary-notion/tsconfig.json b/packages/auxiliary-notion/tsconfig.json new file mode 100644 index 0000000..2000c74 --- /dev/null +++ b/packages/auxiliary-notion/tsconfig.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": [ + "jest" + ], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2019" + ], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strictNullChecks": true, + "skipLibCheck": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/batch-spec/package.json b/packages/batch-spec/package.json new file mode 100644 index 0000000..f7a80f2 --- /dev/null +++ b/packages/batch-spec/package.json @@ -0,0 +1,34 @@ +{ + "name": "@chatbotkit-dev/batch-spec", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "true" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "eslint": "^9.0.0", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/batch-spec/src/index.ts b/packages/batch-spec/src/index.ts new file mode 100644 index 0000000..604d3be --- /dev/null +++ b/packages/batch-spec/src/index.ts @@ -0,0 +1,209 @@ +// @note the batch execution contract. +// +// The platform occasionally needs a container image run somewhere that is not +// the request that asked for it: a crawl of someone's site, an import that +// takes twenty minutes, work that wants eight gigabytes of disk and must not +// hold a Node process open while it uses them. Where that runs is a +// deployment's choice. +// +// The altitude is the decision worth explaining, for the same reason it was on +// the sandbox contract. A deployment that already has a typed client for its own +// job-running service will be tempted to wrap that, because the client exists +// and is already typed. It is the wrong shape. Such a surface carries warm pool +// status, job cloning, server log tailing, registry credentials, a manifest TTL +// on the create body and a vCPU count nobody sets, of which the platform calls +// exactly one method. A contract shaped like one vendor's REST API is a contract +// only that vendor can implement. +// +// So there are two operations here, and the vocabulary is the platform's. There +// is no VM, no queue, and no pool. `id` is whatever the implementation issues +// and the platform only ever hands it back; making it resolve to the same job +// later is the implementation's problem. That is what lets one deployment map +// it to a microVM and another to a Kubernetes Job, a container on the host, or +// a row in a table, without the platform holding a lifecycle only one of them +// has. +// +// What is deliberately absent is as much of the design as what is here. See +// `BatchProvider` at the bottom of the file. + +/** + * Where a job is in its life. + * + * @note five states because the platform can distinguish five outcomes and no + * more. An implementation with a richer machine - pulling, scheduling, + * uploading - collapses into these on the way out, the way the error codes do. + */ +export type BatchJobStatus = + | 'queued' + | 'running' + | 'completed' + | 'failed' + | 'cancelled' + +/** + * How much machine to give a job, when the implementation has a say. + * + * @note advisory, like the sandbox contract's resources. A backend with nothing + * to allocate ignores this rather than failing, and no caller checks whether it + * was honoured. It is a preference, not a requirement. + */ +export interface BatchResources { + memoryMb?: number + diskMb?: number +} + +export interface BatchRunOptions { + /** + * The container image to run, as a registry reference. + * + * @note the one field with no plausible alternative. A backend that cannot + * pull OCI images cannot implement this contract at all, which is why the + * community default refuses rather than approximating. + */ + image: string + + /** + * Environment for the container. + * + * @note this is how the platform passes a job its input - there is no + * argument list here, because the platform has never set one and every image + * it runs reads its own `BATCH_INPUT`. Adding `command` would be adding a way + * to run something other than what an image was built to do. + */ + env?: Record + + /** + * Wall clock limit, in **seconds**. + * + * @note seconds rather than the milliseconds the sandbox contract uses, and + * the inconsistency is deliberate: these jobs are budgeted in minutes by the + * plan limits that produce this number, and a millisecond field would invite + * the conversion to be done twice + */ + timeout?: number + + resources?: BatchResources + + /** + * How long a registry may serve a cached manifest for `image`, in seconds. + * + * @note advisory, and the only field here that leaks an implementation detail + * - it exists because a `:latest` tag that is re-pushed hourly is otherwise + * pinned by whatever proxy sits in front of the registry. A backend with no + * proxy ignores it. It is kept rather than dropped because the caller that + * sets it is expressing something real: this image moves, do not cache it + */ + manifestTtl?: number +} + +export interface BatchRunResult { + /** The implementation's identifier for the job. */ + id: string +} + +/** + * A job as the platform can observe it. + * + * @note there are no timestamps on this, and that is not an oversight. The + * platform starts jobs and finds out how they ended; nothing reads when one was + * queued, and a `startedAt` that some backends can supply and others cannot is + * a field every caller has to treat as absent anyway. + */ +export interface BatchJob { + id: string + + status: BatchJobStatus + + /** The container's exit code. Present once the job has finished. */ + exitCode?: number + + /** Why it failed, when it did. For logs; never shown to a user unedited. */ + error?: string +} + +// --- errors --- + +/** + * @note coarser than the codes a VM-backed service returns, and the collapsing + * is not lossy in any way the platform can observe. One such service separates + * a VM it could not acquire from one it could not reach from one it could not + * release; the platform cannot respond to those differently, and codes naming + * VMs would be unimplementable by a backend that has none. + * + * `UNSUPPORTED_OPERATION` is the one code with no vendor ancestor, and it is + * what makes a partial implementation honest - see `run` on the provider. + */ +export type BatchErrorCode = + | 'JOB_NOT_FOUND' + | 'BATCH_UNAVAILABLE' + | 'NOT_AUTHORIZED' + | 'VALIDATION_FAILED' + | 'UNSUPPORTED_OPERATION' + | 'UNKNOWN' + +/** + * The shape an implementation's errors have to carry so the platform can decide + * what to tell the caller and whether to report the failure. + * + * @note an interface rather than a base class, and detected structurally rather + * than with `instanceof`, for the same two reasons the sandbox contract gives: + * the spec packages in this repository hold no behaviour, and `instanceof` + * across a package boundary is a bet on module identity that a bundler is free + * to lose. A structural brand cannot fail that way. + */ +export interface BatchErrorLike extends Error { + /** The brand. Always `true`, present so the check is not a guess. */ + readonly batch: true + + readonly code: BatchErrorCode + + /** The underlying failure, for logs. */ + readonly detail?: string +} + +// --- provider --- + +export interface BatchProvider { + /** + * Starts a job and returns as soon as it has been accepted. + * + * @note asynchronous by construction, and there is no synchronous sibling. + * Every job the platform runs reports its own results back through the API + * when it is done, so the caller has nothing to wait for and the request that + * started it is long gone by the time the job finishes. + * + * An implementation that cannot run containers must throw + * `UNSUPPORTED_OPERATION` naming the override that can, rather than + * approximating. A batch runner that silently does nothing looks exactly like + * one whose jobs are slow, and the deployment finds out weeks later from a + * dataset that never filled. + */ + run(options: BatchRunOptions): Promise + + /** + * Reports a job's current state. + * + * @note there is no `waitForCompletion` here, and the omission is the reason + * this method exists at all. Waiting is a caller's timing policy - how often + * to look, how long to give up after - and putting it in the contract makes + * every backend reimplement the same poll loop slightly differently. The + * platform runs one loop over this. + * + * @throws `JOB_NOT_FOUND` for an id the implementation has forgotten, which + * it is entitled to do: jobs are retained for a while after they finish and + * then they are not. + */ + get(id: string): Promise + + // @note there is no `cancel`, no `logs` and no `list`, and their absence was + // checked rather than assumed. The service client offers all three and + // nothing in the repository calls any of them; three operations no caller + // wants are still three operations every future backend has to implement. + // Whoever needs one should add it here first, which is the point of the seam. + + /** + * @note the convention every swappable module follows. See + * packages/AGENTS.md. + */ + assertConfigured(): Promise +} diff --git a/packages/batch-spec/tsconfig.json b/packages/batch-spec/tsconfig.json new file mode 100644 index 0000000..6e97651 --- /dev/null +++ b/packages/batch-spec/tsconfig.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/batch/jest.config.js b/packages/batch/jest.config.js new file mode 100644 index 0000000..19ff3e6 --- /dev/null +++ b/packages/batch/jest.config.js @@ -0,0 +1,11 @@ +// @note the node environment, not the shared jsdom one. `just-bash` picks its +// bundle from the resolver's export conditions, and under jsdom it resolves the +// browser build - whose `python3` is a stub that reports "not available in +// browser environments". The platform runs these commands in node, so testing +// against the browser bundle would be testing a build nothing uses. + +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: 'node', +} diff --git a/packages/batch/package.json b/packages/batch/package.json new file mode 100644 index 0000000..2d23d08 --- /dev/null +++ b/packages/batch/package.json @@ -0,0 +1,41 @@ +{ + "name": "@chatbotkit-dev/batch", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "dependencies": { + "@chatbotkit-dev/batch-spec": "workspace:*" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/batch/src/index.test.js b/packages/batch/src/index.test.js new file mode 100644 index 0000000..5584a12 --- /dev/null +++ b/packages/batch/src/index.test.js @@ -0,0 +1,58 @@ +import { assertConfigured, get, run } from './index' + +// @note this default refuses, so what is worth testing is that it refuses +// *legibly*. A backend that cannot run containers is a normal deployment state; +// one that fails without naming the override is a support ticket. + +describe('run', () => { + it('refuses with UNSUPPORTED_OPERATION', async () => { + await expect( + run({ image: 'ghcr.io/chatbotkit/runner-sitemap:latest' }) + ).rejects.toMatchObject({ + batch: true, + code: 'UNSUPPORTED_OPERATION', + }) + }) + + // @note the message has to name the public override point and contract + it('names the override point and the contract in the message', async () => { + await expect(run({ image: 'example:latest' })).rejects.toThrow( + /@chatbotkit-dev\/batch.*BatchProvider.*@chatbotkit-dev\/batch-spec/ + ) + }) + + it('names the image it was asked to run in the detail', async () => { + await expect(run({ image: 'example:latest' })).rejects.toMatchObject({ + detail: expect.stringContaining('example:latest'), + }) + }) + + // @note the brand is what the platform detects errors with - structurally, + // never `instanceof` - so a missing one is silently a different failure path + it('brands the error so the platform recognises it', async () => { + const error = await run({ image: 'example:latest' }).catch((e) => e) + + expect(error).toBeInstanceOf(Error) + expect(error.batch).toBe(true) + expect(typeof error.code).toBe('string') + }) +}) + +describe('get', () => { + it('reports JOB_NOT_FOUND rather than refusing', async () => { + await expect(get('job-123')).rejects.toMatchObject({ + batch: true, + code: 'JOB_NOT_FOUND', + }) + }) +}) + +describe('assertConfigured', () => { + // @note unlike most public defaults this one throws, because nothing can be + // served from it - see the note in index.ts + it('fails the deployment readiness check', async () => { + await expect(assertConfigured()).rejects.toThrow( + /@chatbotkit-dev\/batch-spec/ + ) + }) +}) diff --git a/packages/batch/src/index.ts b/packages/batch/src/index.ts new file mode 100644 index 0000000..886f1e6 --- /dev/null +++ b/packages/batch/src/index.ts @@ -0,0 +1,126 @@ +// @note the community default for batch execution. +// +// This one refuses, and the choice is worth stating rather than discovering. +// The other public defaults in this repository degrade: `@chatbotkit-dev/email` +// logs to the console, `@chatbotkit-dev/searchengine` finds nothing, and +// `@chatbotkit-dev/sandbox` genuinely interprets bash in-process. Each of those +// has a cheap, honest approximation of the real thing. This module does not. +// Running an arbitrary OCI image with its own kernel, disk and network is not +// something a Node process can approximate, and every approximation on offer is +// worse than refusing: +// +// shell out to `docker` - needs a daemon, which is configuration, and turns +// a missing socket into a failure that reads like a +// broken job rather than an absent backend. +// +// run the entrypoint - runs the platform's own process as the job, with +// in-process the platform's credentials. Not a sandbox at all. +// +// accept and drop - the worst one, and the reason this file exists. +// A batch runner that silently succeeds and does +// nothing is indistinguishable from one whose jobs +// are slow, so the deployment finds out weeks later +// from a dataset that never filled. +// +// So `run` throws `UNSUPPORTED_OPERATION` naming the override, and +// `assertConfigured` throws too, rather than following `@chatbotkit-dev/email` +// and resolving anyway: a module that cannot serve any request +// should fail the deployment's readiness check instead of waiting to fail the +// first user. See packages/AGENTS.md. +// +// The platform still imports and boots on this. What it loses is the features +// that launch jobs - today, sitemap and crawl imports - which fail at the point +// of use with a message naming what to install. + +import type { + BatchErrorCode, + BatchErrorLike, + BatchJob, + BatchProvider, + BatchRunOptions, + BatchRunResult, +} from '@chatbotkit-dev/batch-spec' + +export type * from '@chatbotkit-dev/batch-spec' + +// @note the parameters below are named for the contract rather than for what +// this implementation does with them, which is nothing +/* eslint-disable unused-imports/no-unused-vars */ + +export class BatchError extends Error implements BatchErrorLike { + readonly batch = true as const + + readonly code: BatchErrorCode + + readonly detail?: string + + constructor( + code: BatchErrorCode, + message: string, + options?: { detail?: string; cause?: unknown } + ) { + super(message) + + this.name = 'BatchError' + this.code = code + this.detail = options?.detail + + // @note assigned rather than passed to `super`, because the two-argument + // `Error` constructor is ES2022 and these packages compile against ES2021 + + if (options?.cause !== undefined) { + ;(this as { cause?: unknown }).cause = options.cause + } + } +} + +const UNSUPPORTED = + 'no batch backend is installed, so container jobs cannot run - override @chatbotkit-dev/batch with a package whose default export satisfies BatchProvider from @chatbotkit-dev/batch-spec' + +/** + * Refuses, because nothing here can run a container image. + * + * @throws always, with `UNSUPPORTED_OPERATION` + */ +export async function run(options: BatchRunOptions): Promise { + throw new BatchError('UNSUPPORTED_OPERATION', UNSUPPORTED, { + detail: `cannot run ${options.image}`, + }) +} + +/** + * Reports that the job is not here, because no job ever started here. + * + * @note `JOB_NOT_FOUND` rather than `UNSUPPORTED_OPERATION`, and the difference + * matters to the caller. An id can only reach this function if something else + * issued it, which means the caller is polling a job started by a backend that + * has since been swapped out - and "that job is gone" is both true and the + * thing it needs to stop polling. + */ +export async function get(id: string): Promise { + throw new BatchError('JOB_NOT_FOUND', `no batch job ${id}`, { + detail: UNSUPPORTED, + }) +} + +/** + * @note throws, unlike most of the public defaults. Nothing can be served from + * this module - every job would fail at the point of use - so it fails the + * deployment's readiness check instead, the way an empty model catalogue does. + * A deployment that genuinely wants no batch backend removes the case from + * `platform/tests/config/providers.utest.js`, which is a decision someone + * makes on purpose rather than one that happens quietly. + */ +export async function assertConfigured(): Promise { + throw new Error( + `@chatbotkit-dev/batch is the community default and ${UNSUPPORTED}` + ) +} + +const provider: BatchProvider = { + run, + get, + assertConfigured, +} + +export default provider diff --git a/packages/batch/tsconfig.json b/packages/batch/tsconfig.json new file mode 100644 index 0000000..eb04375 --- /dev/null +++ b/packages/batch/tsconfig.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/billing-spec/package.json b/packages/billing-spec/package.json new file mode 100644 index 0000000..a180b64 --- /dev/null +++ b/packages/billing-spec/package.json @@ -0,0 +1,34 @@ +{ + "name": "@chatbotkit-dev/billing-spec", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "true" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "eslint": "^9.0.0", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/billing-spec/src/index.ts b/packages/billing-spec/src/index.ts new file mode 100644 index 0000000..fd56bc7 --- /dev/null +++ b/packages/billing-spec/src/index.ts @@ -0,0 +1,148 @@ +// @note the billing module contract: the surface a billing module exposes, +// as function and value shapes an implementation conforms to. How an +// implementation configures itself - environment variables, configuration +// formats, payment provider - is its own business and deliberately absent +// here. The default implementation lives in @chatbotkit-dev/billing. + +/** + * The classic surface most call sites read: trial policy plus a plan-to-price + * table. A null/absent price presents as Infinity, matching how the unbounded + * tier has always rendered. + */ +export interface Subscriptions { + trialDays: number + + trialPlans?: string[] + + pricing: Record +} + +/** + * The platform facts the subscription model closes over: its plan + * vocabulary, its installed catalogue and its grant mechanism. Nothing else + * crosses the boundary. + */ +export interface SubscriptionFacts { + /** + * The structural plan names: `free` is the plan of an account with no + * subscription and no grant, `trial` the plan of a trialing subscription, + * and `unlimited` the implicit full-access plan every deployment has. + */ + structuralPlans: { + free: string + trial: string + unlimited: string + } + + /** The plan names of the installed limits catalogue. */ + planKeys: readonly string[] + + /** The plan granted to an email address by a platform override, if any. */ + grantedPlan: (email: string) => string | undefined +} + +/** + * The account shape the subscription checks read. + * + * @note the property names are the account schema's billing columns. They + * are the billing module's own recording (null when the account has none) + * and an implementation detail everywhere else: the platform transports them + * on account rows but never interprets them. + */ +export interface SubscriptionHolder { + email: string + + billingSubscriptionId?: string | null + + billingSubscriptionStatus?: string | null +} + +/** + * The subscription model surface: the sellable catalogue, the id/name + * mappings and the subscription checks, closed over one deployment's facts. + */ +export interface SubscriptionModel { + /** + * The display name of the plan an account is recorded on - undefined for + * an account with no subscription or one recorded on an id the deployment + * no longer knows. Tolerant on purpose: it feeds listings and exports. + */ + recordedPlanName( + account: Pick + ): string | undefined + + /** Whether the account holds a live subscription or a grant. */ + hasSubscription(user: SubscriptionHolder): boolean + + /** + * Whether the account has ever consumed its trial - one per account, + * regardless of the plan it ran on or how the trial ended. + */ + hasTrialed(account: { + billingSubscriptionTrialedAt?: Date | string | null + }): boolean + + /** The plan the account is on, resolved from its own subscription facts. */ + userToPlan(user: SubscriptionHolder): string +} + +/** Builds the subscription model for one deployment's facts. */ +export type CreateSubscriptionModel = ( + facts: SubscriptionFacts +) => SubscriptionModel + +/** + * The platform facts the billing gates close over. + */ +export interface BillingGatesFacts { + /** Whether the platform has an installed plan catalogue. */ + hasPlans: boolean + + /** Whether an account is structurally owned by another account. */ + isChildUser: (user: unknown) => boolean +} + +/** + * The deployment gates: whether this deployment sells at all, and whether a + * given account may use billing. + */ +export interface BillingGates { + /** + * Whether this deployment sells plans at all. False means no pricing + * surface, no upgrade affordance and no trials. + */ + isSellable: boolean + + /** Whether a payment provider is configured. Read lazily. */ + isBillingConfigured(): boolean + + /** Whether this account may reach the billing surface at all. */ + canDoBilling(user: unknown): boolean +} + +/** Builds the billing gates for one deployment's facts. */ +export type CreateBillingGates = (facts: BillingGatesFacts) => BillingGates + +/** + * The trial policy, derived from a subscriptions configuration. + */ +export interface TrialPolicy { + /** + * The plans on which a free trial can be started - the single switch for + * trials, an empty list disabling them everywhere. + */ + trialPlans: readonly string[] + + /** + * The plan a trial is presented as by default, undefined when trials are + * disabled. + */ + primaryTrialPlan: string | undefined + + canTrialPlan(plan: string): boolean +} + +/** Derives the trial policy from a subscriptions configuration. */ +export type CreateTrialPolicy = ( + subscriptions: Pick +) => TrialPolicy diff --git a/packages/billing-spec/tsconfig.json b/packages/billing-spec/tsconfig.json new file mode 100644 index 0000000..6e97651 --- /dev/null +++ b/packages/billing-spec/tsconfig.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/billing/package.json b/packages/billing/package.json new file mode 100644 index 0000000..4add1ac --- /dev/null +++ b/packages/billing/package.json @@ -0,0 +1,45 @@ +{ + "name": "@chatbotkit-dev/billing", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + }, + "./assert": { + "import": "./src/assert.ts", + "default": "./src/assert.ts" + }, + "./provider": { + "import": "./src/provider.ts", + "default": "./src/provider.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "true" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "dependencies": { + "@chatbotkit-dev/billing-spec": "workspace:*" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "eslint": "^9.0.0", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/billing/src/assert.ts b/packages/billing/src/assert.ts new file mode 100644 index 0000000..5b2cd1b --- /dev/null +++ b/packages/billing/src/assert.ts @@ -0,0 +1,8 @@ +// @note boot/CI-time validation, exposed at `@chatbotkit-dev/billing/assert` +// by convention. See packages/AGENTS.md. + +/** + * Throws when this module is not usable with the current environment. This + * module sells nothing and has nothing to misconfigure, so it resolves. + */ +export async function assertConfigured(_input?: unknown): Promise {} diff --git a/packages/billing/src/config.ts b/packages/billing/src/config.ts new file mode 100644 index 0000000..0a6c17e --- /dev/null +++ b/packages/billing/src/config.ts @@ -0,0 +1,15 @@ +import type { Subscriptions } from '@chatbotkit-dev/billing-spec' + +// @note nothing is sold and there is nothing to configure - no pricing +// surface, no upgrade affordance, no trials. + +export const isConfigured = false + +export const subscriptionsConfig: Subscriptions = Object.freeze({ + trialDays: 0, + + trialPlans: [], + + pricing: {}, +}) + diff --git a/packages/billing/src/gates.ts b/packages/billing/src/gates.ts new file mode 100644 index 0000000..7afd9dd --- /dev/null +++ b/packages/billing/src/gates.ts @@ -0,0 +1,22 @@ +import type { + BillingGates, + BillingGatesFacts, +} from '@chatbotkit-dev/billing-spec' + +/** + * Builds the billing gates for one deployment's facts. With nothing sold and + * no payment provider, every gate is closed. + */ +export function createBillingGates(_facts: BillingGatesFacts): BillingGates { + return Object.freeze({ + isSellable: false, + + isBillingConfigured(): boolean { + return false + }, + + canDoBilling(_user: unknown): boolean { + return false + }, + }) +} diff --git a/packages/billing/src/index.ts b/packages/billing/src/index.ts new file mode 100644 index 0000000..65614fd --- /dev/null +++ b/packages/billing/src/index.ts @@ -0,0 +1,10 @@ +// @note the public default billing module: a deployment that sells nothing. +// Plans remain pure entitlement tiers assigned through grants and overrides - +// a working deployment shape, not a broken one. A deployment that sells +// overrides this package with an implementation satisfying +// @chatbotkit-dev/billing-spec. + +export * from './config' +export * from './model' +export * from './trial' +export * from './gates' diff --git a/packages/billing/src/model.ts b/packages/billing/src/model.ts new file mode 100644 index 0000000..893a9be --- /dev/null +++ b/packages/billing/src/model.ts @@ -0,0 +1,49 @@ +import type { + SubscriptionFacts, + SubscriptionModel, +} from '@chatbotkit-dev/billing-spec' + +export type { SubscriptionHolder } from '@chatbotkit-dev/billing-spec' + +/** + * Builds the subscription model for one deployment's facts. With nothing + * sold, an account's plan comes from its grant alone. + */ +export function createSubscriptionModel( + facts: SubscriptionFacts +): SubscriptionModel { + const { structuralPlans, planKeys, grantedPlan } = facts + + // @note `free` and `trial` are not tiers - they are the no-subscription + // and trialing states - so they are never grantable. The structural + // unlimited plan is implicitly part of every deployment. + const planNames: ReadonlySet = new Set( + [...planKeys, structuralPlans.unlimited].filter( + (plan) => plan !== structuralPlans.free && plan !== structuralPlans.trial + ) + ) + + return Object.freeze({ + recordedPlanName() { + return undefined + }, + + hasSubscription(user) { + return grantedPlan(user.email) !== undefined + }, + + hasTrialed() { + return false + }, + + userToPlan(user) { + const granted = grantedPlan(user.email) + + if (granted && planNames.has(granted)) { + return granted + } + + return structuralPlans.free + }, + } satisfies SubscriptionModel) +} diff --git a/packages/billing/src/provider.ts b/packages/billing/src/provider.ts new file mode 100644 index 0000000..4d95f46 --- /dev/null +++ b/packages/billing/src/provider.ts @@ -0,0 +1,135 @@ +// @note the payment-provider operations, exposed at +// `@chatbotkit-dev/billing/provider`. This default module has no payment +// provider, so every operation refuses. +// +// @note this subpath is not part of @chatbotkit-dev/billing-spec: the +// platform's billing routes import these operations and their result shapes +// directly, which couples them to whichever implementation is installed. The +// shapes below mirror that de-facto contract so the routes compile - a +// deployment that sells must override @chatbotkit-dev/billing with an +// implementation providing the same operations. + +/** The account shape the customer operations read. */ +export interface BillingAccount { + billingCustomerId?: string | null + + billingSubscriptionId?: string | null +} + +export interface CustomerAccount extends BillingAccount { + id: string + + email?: string | null + + name?: string | null +} + +function unsupported(): never { + throw new Error( + 'no payment provider is installed, so billing operations cannot run - ' + + 'override @chatbotkit-dev/billing with a package that satisfies ' + + '@chatbotkit-dev/billing-spec and provides these operations' + ) +} + +/** + * Retrieves one price from the provider - the probe the boot-time assertions + * check the selling configuration against. + */ +export async function retrievePrice(_priceId: string): Promise { + return unsupported() +} + +export type OpenBillingPortalResult = + | { outcome: 'redirect'; url: string } + | { outcome: 'failed' } + +export async function openBillingPortal( + _db: unknown, + _account: CustomerAccount, + _options: { returnUrl: string } +): Promise { + return unsupported() +} + +export type StartCheckoutResult = + | { outcome: 'redirect'; url: string } + | { outcome: 'unknown_plan' } + | { outcome: 'trial_unavailable' } + | { outcome: 'possibly_fraudulent' } + | { outcome: 'already_subscribed' } + | { outcome: 'customer_gone' } + | { outcome: 'delinquent' } + | { outcome: 'failed' } + +export interface CheckoutIntent { + plan: string + + trial: boolean + + coupon?: string | null + + referral?: string | null + + returnUrl: string +} + +export async function startCheckout( + _db: unknown, + _account: CustomerAccount, + _intent: CheckoutIntent +): Promise { + return unsupported() +} + +export type SkipTrialResult = + | { outcome: 'skipped'; subscriptionId: string } + | { outcome: 'not_trialing' } + | { outcome: 'no_subscription' } + +export async function skipTrial( + _db: unknown, + _account: BillingAccount & { + id: string + billingSubscriptionStatus?: string | null + } +): Promise { + return unsupported() +} + +export async function deleteCustomer( + _account: Pick +): Promise { + return unsupported() +} + +export type WebhookFollowUp = + | { action: 'notify_trial_start' } + | { action: 'notify_trial_duplicate_card' } + | { action: 'reset_account_limits' } + | { action: 'notify_subscription_deleted' } + | { action: 'notify_invoice_payment_succeeded' } + | { action: 'notify_invoice_payment_failed' } + | { action: 'credit_booster_tokens'; userId: string } + | { action: 'delete_account' } + +export type HandleWebhookEventResult = + | { outcome: 'missing_signature' } + | { outcome: 'unconfigured' } + | { outcome: 'invalid'; message: string } + | { outcome: 'unknown_account'; customerId: string } + | { + outcome: 'handled' + type: string + account?: { id?: string } | null + followUps: WebhookFollowUp[] + messages: string[] + } + +export async function handleWebhookEvent( + _db: unknown, + _slidingWindow: unknown, + _request: { payload: string; headers: Record } +): Promise { + return unsupported() +} diff --git a/packages/billing/src/trial.ts b/packages/billing/src/trial.ts new file mode 100644 index 0000000..ee0de1f --- /dev/null +++ b/packages/billing/src/trial.ts @@ -0,0 +1,27 @@ +import type { Subscriptions, TrialPolicy } from '@chatbotkit-dev/billing-spec' + +import { subscriptionsConfig } from './config' + +/** + * Derives the trial policy from a subscriptions configuration. + */ +export function createTrialPolicy( + subscriptions: Pick +): TrialPolicy { + const trialPlans: readonly string[] = Object.freeze([ + ...(subscriptions.trialPlans ?? []), + ]) + + return Object.freeze({ + trialPlans, + + primaryTrialPlan: trialPlans[0], + + canTrialPlan(plan: string): boolean { + return trialPlans.includes(plan) + }, + }) +} + +export const { trialPlans, primaryTrialPlan, canTrialPlan } = + createTrialPolicy(subscriptionsConfig) diff --git a/packages/billing/tsconfig.json b/packages/billing/tsconfig.json new file mode 100644 index 0000000..6e97651 --- /dev/null +++ b/packages/billing/tsconfig.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/blacklists/jest.config.js b/packages/blacklists/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/blacklists/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/blacklists/package.json b/packages/blacklists/package.json new file mode 100644 index 0000000..87dd965 --- /dev/null +++ b/packages/blacklists/package.json @@ -0,0 +1,45 @@ +{ + "name": "@chatbotkit-dev/blacklists", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + }, + "./domains": { + "import": "./src/domains.ts", + "default": "./src/domains.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "dependencies": { + "disposable-email-domains": "^1.0.62" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/blacklists/src/domains.ts b/packages/blacklists/src/domains.ts new file mode 100644 index 0000000..0e1423a --- /dev/null +++ b/packages/blacklists/src/domains.ts @@ -0,0 +1,87 @@ +// @note the signup domain blacklist - a built-in anti-abuse control against +// disposable and throwaway addresses, maintained here for every deployment. +// One shared list is better protection than a swappable one that most +// deployments would leave empty. +// +// An entry matches the domain itself and every subdomain of it (`example.com` +// bans `example.com` and `mail.example.com`). A bare TLD entry (`cfd`) +// therefore bans every domain on that TLD - use those only for TLDs that +// produce nothing but abuse. + +import wildcard from 'disposable-email-domains/wildcard.json' + +/** Email domains refused at signup. */ +export const domains: string[] = [ + // The maintained disposable-email wildcard list. + ...wildcard, + + // Hand-collected abuse intelligence gathered from signup traffic. + 'myhome-server.de', + 'lebetrust.org', + 'throwawaymail.com', + 'lanxiu233.com', + 'nimail.cn', + 'indoxs.bond', + 'sportcornwall.org', + 'dmcelements.org', + 'havertz.tk', + '0cpub2.tech', + 'dynv6.net', + 'yorushika.one', + 'siempre.gratis', + 'aaconservation.org', + 'linlin.cloud', + 'linux-do.me', + 'indevs.in', + 'hulisang.edu.kg', + 'freemails.pp.ua', + 'abrdns.com', + 'plugintonature.org.uk', + 'ycglobalmovement.com', + 'nyc.mn', + 'novaprime.vip', + 'tokenized.name', + 'thinktank.edu.kg', + 'ahavaexperience.com', + 'chanceforfuture.org', + 'mwalshabs.dev', + 'positivevoiceteesvalley.co.uk', + 'us.ci', + 'alarafoundation.com', + 'mailtao.me', + 'acg-news.com', + 'churchinsouthampton.org.uk', + 'wearvault.biz.id', + 'sylu.net', + 'thameschamberorchestra.co.uk', + 'baileybridge.org', + 'jobsma.pp.ua', + 'freeddns.org', + 'abilityaccesslegal.org', + 'xiwinnie.icu', + 'cuvrs.info', + 'lordfortescue.org.uk', + 'qzz.io', + 'fightingzebras.org', + 'vvvv.ee', + 'slimirin.com', + 'de5.net', + 'zutomayo.best', + 'idu4aa4.info', + 'markableytrust.org.uk', + 'x80la.shop', + 'bcmail.pro', + 'caowo.online', + 'dpdns.org', + 'ggff.net', + 'supergrok.site', + 'eu.org', + 'cc.cd', + + // Bare TLD bans. + 'cfd', + 'cc', + 'top', + 'xyz', + 'sbs', +] diff --git a/packages/blacklists/src/index.test.ts b/packages/blacklists/src/index.test.ts new file mode 100644 index 0000000..29cf4d3 --- /dev/null +++ b/packages/blacklists/src/index.test.ts @@ -0,0 +1,29 @@ +import blacklist, { domains } from './index' + +describe('blacklists', () => { + it('exports the domain list both ways', () => { + expect(blacklist.domains).toBe(domains) + }) + + it('carries the maintained disposable-email list plus the curated entries', () => { + // @note the wildcard list alone is hundreds of entries; a shrunken list + // means the dependency stopped resolving and signups lost their cover + expect(domains.length).toBeGreaterThan(300) + + expect(domains).toContain('throwawaymail.com') + expect(domains).toContain('qzz.io') + }) + + it('carries the bare TLD bans', () => { + for (const tld of ['cfd', 'cc', 'top', 'xyz', 'sbs']) { + expect(domains).toContain(tld) + } + }) + + it('contains only plausible domain entries', () => { + for (const domain of domains) { + expect(domain).toMatch(/^[a-z0-9*.-]+$/i) + expect(domain).not.toMatch(/\s|@|\/$/) + } + }) +}) diff --git a/packages/blacklists/src/index.ts b/packages/blacklists/src/index.ts new file mode 100644 index 0000000..855c5d4 --- /dev/null +++ b/packages/blacklists/src/index.ts @@ -0,0 +1,18 @@ +// @note built-in blacklists the platform enforces for every deployment. Not a +// swappable configuration module - the lists ship with the platform and are +// maintained for everyone. See src/domains.ts for the signup domain list. + +import { domains } from './domains' + +export { domains } + +export interface Blacklist { + /** Email domains refused at signup. */ + domains: string[] +} + +const blacklist: Blacklist = { + domains, +} + +export default blacklist diff --git a/packages/blacklists/tsconfig.json b/packages/blacklists/tsconfig.json new file mode 100644 index 0000000..72b364c --- /dev/null +++ b/packages/blacklists/tsconfig.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node", + "jest" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/buffer/README.md b/packages/buffer/README.md new file mode 100644 index 0000000..b8b0fec --- /dev/null +++ b/packages/buffer/README.md @@ -0,0 +1,12 @@ +``` + .d8888b. 888888b. 888 d8P + d88P Y88b 888 "88b 888 d8P + 888 888 888 .88P 888 d8P + 888 8888888K. 888d88K + 888 888 "Y88b 8888888b + 888 888 888 888 888 Y88b + Y88b d88P 888 d88P 888 Y88b + "Y8888P" 8888888P" 888 Y88b +``` + +For external use where it is needed. diff --git a/packages/buffer/jest.config.js b/packages/buffer/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/buffer/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/buffer/package.json b/packages/buffer/package.json new file mode 100644 index 0000000..15bdfda --- /dev/null +++ b/packages/buffer/package.json @@ -0,0 +1,40 @@ +{ + "name": "@chatbotkit-dev/buffer", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "dependencies": { + "js-base64": "^3.7.7" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/buffer/src/index.test.ts b/packages/buffer/src/index.test.ts new file mode 100644 index 0000000..b255dc3 --- /dev/null +++ b/packages/buffer/src/index.test.ts @@ -0,0 +1,135 @@ +import { + b64d2buf, + buf2b64d, + buf2hex, + buf2str, + buf2stream, + concatBufs, + hex2buf, + str2buf, + stream2buf, +} from './index' + +describe('buf2str', () => { + test('converts an ArrayBuffer to a string', () => { + const testArrayBuffer = str2buf('Hello, World!') + + expect(buf2str(testArrayBuffer)).toBe('Hello, World!') + }) +}) + +describe('str2buf', () => { + test('converts a string to an ArrayBuffer', () => { + const testString = 'Hello, World!' + + expect(new TextDecoder().decode(str2buf(testString))).toBe(testString) + }) +}) + +describe('hex2buf', () => { + test('converts a hex string to a Uint8Array', () => { + const testHex = '48656c6c6f2c20576f726c6421' + const expectedArray = Uint8Array.from( + new TextEncoder().encode('Hello, World!') + ) + + expect(hex2buf(testHex)).toEqual(expectedArray) + }) + + test('should match Buffer from Node.js', () => { + const testHexBuf = hex2buf('48656c6c6f2c20576f726c6421') + const expectedHexBuf = new Uint8Array( + Buffer.from('48656c6c6f2c20576f726c6421', 'hex') + ) + + expect(testHexBuf).toEqual(expectedHexBuf) + }) + + test('should throw on odd-length hex string', () => { + expect(() => hex2buf('abc')).toThrow('odd length') + expect(() => hex2buf('a')).toThrow('odd length') + }) +}) + +describe('buf2hex', () => { + test('converts a Uint8Array to a hex string', () => { + const testBuffer = new TextEncoder().encode('Hello, World!') + const testHex = '48656c6c6f2c20576f726c6421' + + expect(buf2hex(testBuffer)).toBe(testHex) + }) +}) + +describe('b64d2buf', () => { + test('converts a base64 encoded string to a Uint8Array', () => { + const testBase64 = 'SGVsbG8sIFdvcmxkIQ==' + const testBuffer = new TextEncoder().encode('Hello, World!') + + expect(b64d2buf(testBase64)).toEqual(testBuffer) + }) +}) + +describe('buf2b64d', () => { + test('converts a Uint8Array to a base64 string', () => { + const testBuffer = new TextEncoder().encode('Hello, World!') + const testBase64 = 'SGVsbG8sIFdvcmxkIQ==' + + expect(buf2b64d(testBuffer)).toBe(testBase64) + }) +}) + +describe('concatBufs', () => { + test('concatenates multiple ArrayBuffers into a single ArrayBuffer', () => { + const buffer1 = str2buf('Hello') + const buffer2 = str2buf(' ') + const buffer3 = str2buf('World') + const concatenatedBuffer = concatBufs(buffer1, buffer2, buffer3) + + expect(new TextDecoder().decode(concatenatedBuffer)).toBe('Hello World') + }) + + test('matches Buffer.concat from Node.js', () => { + const testBuf = new Uint8Array( + concatBufs(str2buf('Hello'), str2buf(' '), str2buf('World')) + ) + const expectedBuf = new Uint8Array( + Buffer.concat([ + Buffer.from('Hello'), + Buffer.from(' '), + Buffer.from('World'), + ]) + ) + + expect(testBuf).toEqual(expectedBuf) + }) +}) + +describe('stream2buf', () => { + test('converts a ReadableStream to an ArrayBuffer', async () => { + const testArrayBuffer = str2buf('Hello, World!') + const stream = await buf2stream(testArrayBuffer) + const outputBuffer = await stream2buf(stream) + + expect(new Uint8Array(outputBuffer)).toEqual( + new Uint8Array(testArrayBuffer) + ) + }) +}) + +describe('buf2stream', () => { + test('converts an ArrayBuffer to a ReadableStream', async () => { + const testArrayBuffer = str2buf('Hello, World!') + const stream = await buf2stream(testArrayBuffer) + const reader = stream.getReader() + const result = await reader.read() + + expect(result.done).toBeFalsy() + expect(new Uint8Array(result.value!)).toEqual( + new Uint8Array(testArrayBuffer) + ) + + const end = await reader.read() + + expect(end.done).toBeTruthy() + }) +}) diff --git a/packages/buffer/src/index.ts b/packages/buffer/src/index.ts new file mode 100644 index 0000000..9465fda --- /dev/null +++ b/packages/buffer/src/index.ts @@ -0,0 +1,111 @@ +import { Base64 } from 'js-base64' + +// we need to polyfill the ReadableStream for chrome and Safari +{ + if ( + typeof globalThis.ReadableStream === 'function' && + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Symbol is not defined + typeof globalThis.ReadableStream.prototype[Symbol.asyncIterator] !== + 'function' + ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Symbol is not defined + globalThis.ReadableStream.prototype[Symbol.asyncIterator] = function () { + const reader = this.getReader() + + return { + next: () => reader.read(), + return: () => { + reader.releaseLock() + + return Promise.resolve({ done: true }) + }, + } + } + } +} + +export function buf2str( + buf: ArrayBuffer | ArrayBufferView, + encoding: string | undefined = 'utf-8' +): string { + return new TextDecoder(encoding).decode(buf) +} + +export function str2buf(str: string): Uint8Array { + return new TextEncoder().encode(str) +} + +export function hex2buf(hex: string): Uint8Array { + if (hex.length % 2 !== 0) { + throw new Error(`Invalid hex string: odd length (${hex.length})`) + } + + const byteArray = new Uint8Array(hex.length / 2) + + for (let i = 0; i < byteArray.length; i++) { + const byteCode = hex.substring(i * 2, i * 2 + 2) + + byteArray[i] = parseInt(byteCode, 16) + } + + return byteArray +} + +export function buf2hex(buf: Uint8Array): string { + return Array.from(buf) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join('') +} + +export function b64d2buf(b64d: string): Uint8Array { + return Base64.toUint8Array(b64d) +} + +export function buf2b64d(buf: Uint8Array): string { + return Base64.fromUint8Array(buf) +} + +export function concatBufs(...bufs: (Uint8Array | ArrayBuffer)[]): ArrayBuffer { + const totalLength = bufs.reduce((acc, buf) => acc + buf.byteLength, 0) + + const result = new Uint8Array(totalLength) + + let offset = 0 + + for (const buf of bufs) { + const view = new Uint8Array(buf) + + result.set(view, offset) + + offset += buf.byteLength + } + + return result.buffer +} + +export async function stream2buf( + stream: ReadableStream +): Promise { + const chunks: (ArrayBuffer | Uint8Array)[] = [] + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this may not longer be necessary as streams are now iterable + for await (const chunk of stream) { + chunks.push(chunk) + } + + return concatBufs(...chunks) +} + +export async function buf2stream( + buf: ArrayBuffer | Uint8Array +): Promise> { + return new ReadableStream({ + start(controller) { + controller.enqueue(buf) + controller.close() + }, + }) +} diff --git a/packages/buffer/tsconfig.json b/packages/buffer/tsconfig.json new file mode 100644 index 0000000..3d5f905 --- /dev/null +++ b/packages/buffer/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/cloak/LICENSE.md b/packages/cloak/LICENSE.md new file mode 100644 index 0000000..e81cad1 --- /dev/null +++ b/packages/cloak/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2019 François Best +Copyright (c) 2026 CBK.AI LTD (modifications) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cloak/README.md b/packages/cloak/README.md new file mode 100644 index 0000000..4f5c749 --- /dev/null +++ b/packages/cloak/README.md @@ -0,0 +1,12 @@ +# @chatbotkit-dev/cloak + +AES-GCM encryption, decryption, and key management for the Web Crypto API. + +This package is based on +[`@47ng/cloak` v1.2.0](https://github.com/47ng/cloak/tree/v1.2.0) +(`fc619b746ce25b862d7510ad51231264516db798`). It preserves the upstream data +formats while adapting the implementation for this platform and adding features +such as additional authenticated data support. + +The original work and CBK.AI's modifications are distributed under the MIT +License. See [LICENSE.md](./LICENSE.md). diff --git a/packages/cloak/jest.config.js b/packages/cloak/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/cloak/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/cloak/package.json b/packages/cloak/package.json new file mode 100644 index 0000000..dd6082b --- /dev/null +++ b/packages/cloak/package.json @@ -0,0 +1,39 @@ +{ + "name": "@chatbotkit-dev/cloak", + "version": "0.0.0", + "license": "MIT", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@47ng/codec": "^1.1.0" + }, + "devDependencies": { + "@47ng/cloak": "^1.2.0", + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/cloak/src/ciphers/aes-gcm.ts b/packages/cloak/src/ciphers/aes-gcm.ts new file mode 100644 index 0000000..c028be0 --- /dev/null +++ b/packages/cloak/src/ciphers/aes-gcm.ts @@ -0,0 +1,63 @@ +import { utf8 } from '@47ng/codec' + +export interface AesCipher { + iv: Uint8Array + text: Uint8Array +} + +export interface AesGcmOptions { + /** + * Additional authenticated data. Not encrypted and not carried in the + * ciphertext, but bound to it: decryption fails unless the exact same + * bytes are supplied again. Callers use it to tie a ciphertext to its + * context (a column, a record) so it cannot be moved somewhere else. + */ + additionalData?: Uint8Array +} + +export async function encryptAesGcm( + key: CryptoKey, + message: string, + options: AesGcmOptions = {} +): Promise { + const buf = utf8.encode(message) + + const iv = crypto.getRandomValues(new Uint8Array(12)) + + const cipherText = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + ...(options.additionalData + ? { additionalData: new Uint8Array(options.additionalData) } + : {}), + }, + key, + new Uint8Array(buf) + ) + + return { + text: new Uint8Array(cipherText), + iv, + } +} + +export async function decryptAesGcm( + key: CryptoKey, + cipher: AesCipher, + options: AesGcmOptions = {} +): Promise { + const buf = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: new Uint8Array(cipher.iv), + ...(options.additionalData + ? { additionalData: new Uint8Array(options.additionalData) } + : {}), + }, + key, + new Uint8Array(cipher.text) + ) + + return utf8.decode(new Uint8Array(buf)) +} diff --git a/packages/cloak/src/index.test.ts b/packages/cloak/src/index.test.ts new file mode 100644 index 0000000..2e95285 --- /dev/null +++ b/packages/cloak/src/index.test.ts @@ -0,0 +1,197 @@ +import { + decryptString, + encryptString, + findKeyForMessage, + generateKey, + makeKeychain, + parseCloakedString, + parseKey, +} from './index' + +import { + decryptString as decryptString47ng, + encryptString as encryptString47ng, +} from '@47ng/cloak' + +test('Key generation', () => { + const key = generateKey() + + expect(key.startsWith('k1.aesgcm256.')).toBeTruthy() + expect(key.length).toEqual(57) +}) + +describe('v1 format', () => { + test('Encrypt / decrypt', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const expected = 'Hello, World !' + const cipher = await encryptString(expected, key) + const received = await decryptString(cipher, key) + + expect(received).toEqual(expected) + }) + + test('Encrypt / decrypt compatibility 001', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const expected = 'Hello, World !' + const cipher = await encryptString(expected, key) + const received = await decryptString47ng(cipher, key) + + expect(received).toEqual(expected) + }) + + test('Encrypt / decrypt compatibility 002', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const expected = 'Hello, World !' + const cipher = await encryptString47ng(expected, key) + const received = await decryptString(cipher, key) + + expect(received).toEqual(expected) + }) + + test('Encrypt / decrypt 4 MiB string', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const expected = 'a'.repeat(4_194_304) // 2 ** 22 = 4 MiB + const cipher = await encryptString(expected, key) + const received = await decryptString(cipher, key) + + expect(received).toEqual(expected) + }) + + test('Encrypt empty string', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const expected = '' + const cipher = await encryptString(expected, key) + const received = await decryptString(cipher, key) + + expect(received).toEqual(expected) + }) + + test('Decrypt known message (empty string)', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const cipher = + 'v1.aesgcm256.710bb0e2.9tZkprVBt4L7ZW_U.GDrlM3U_P0UnHf38HvOCgQ==' + const expected = '' + const received = await decryptString(cipher, key) + + expect(received).toEqual(expected) + }) + + test('Decrypt known message', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const cipher = + 'v1.aesgcm256.710bb0e2.F5wkSytfdVv4xvtN.8uNajc7ufhVmMFpDdzWgKMKhOY4ZR2OSv1DFjvnm' + const expected = 'Hello, World !' + const received = await decryptString(cipher, key) + + expect(received).toEqual(expected) + }) + + test('Decrypt known message from browser', async () => { + const key = 'k1.aesgcm256.CO6hoJ8l1nAmXpuCcuNg-l5g3Nn63X36lBwhsNepUEY=' + const cipher = + 'v1.aesgcm256.4eb11c57.UAuPXcQZV_e40NP6.OvVOoWCXhMB_G-giNtAbDYZI0sfJomHUAW0vpxKV' + const expected = 'Hello, World !' + const received = await decryptString(cipher, key) + + expect(received).toEqual(expected) + }) + + test('Ciphertext & IV are rotated', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const cipher1 = await encryptString('Hello, World !', key) + const cipher2 = await encryptString('Hello, World !', key) + + expect(cipher1).not.toEqual(cipher2) + }) + + test('Fingerprinting & keychain', async () => { + const keyA = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const keyB = 'k1.aesgcm256.caNwte-JDsVUATl3qCQgu9ZPuHAiJhWSOn0pcgGhwyE=' + const cipherA = await encryptString('Hello', keyA) + const cipherB = await encryptString('Hello', keyB) + const keychain = await makeKeychain([keyA, keyB]) + const keyForA = findKeyForMessage(cipherA, keychain) + const keyForB = findKeyForMessage(cipherB, keychain) + + expect(keyForA).toEqual(await parseKey(keyA)) + + expect(keyForB).toEqual(await parseKey(keyB)) + }) + + test('Parse key', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const parsedKey = await parseKey(key) + const expected = 'Hello, World !' + const cipher = await encryptString(expected, parsedKey) + const received = await decryptString(cipher, parsedKey) + + expect(received).toEqual(expected) + }) + + test('Rejects ciphertext with invalid characters', () => { + // @note ensures parseCloakedString returns false for malformed ciphertexts + // instead of passing through to b64.decode which would throw an unexpected error + const malformed = + 'v1.aesgcm256.710bb0e2.9tZkprVBt4L7ZW_U.!!!!!!!!!!!!!!!!!!!!!!!!!=' + + expect(parseCloakedString(malformed)).toBe(false) + }) +}) + +describe('additional authenticated data', () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + + test('round trips with matching AAD', async () => { + const cipher = await encryptString('Hello', key, { + additionalData: 'Secret.value', + }) + const received = await decryptString(cipher, key, { + additionalData: 'Secret.value', + }) + + expect(received).toEqual('Hello') + }) + + test('AAD is not carried in the message', async () => { + const cipher = await encryptString('Hello', key, { + additionalData: 'Secret.value', + }) + + expect(parseCloakedString(cipher)).toBeTruthy() + expect(cipher).not.toContain('Secret') + }) + + test('rejects a different AAD', async () => { + const cipher = await encryptString('Hello', key, { + additionalData: 'Secret.value', + }) + + await expect( + decryptString(cipher, key, { additionalData: 'SecretValue.value' }) + ).rejects.toThrow() + }) + + test('rejects a missing AAD', async () => { + const cipher = await encryptString('Hello', key, { + additionalData: 'Secret.value', + }) + + await expect(decryptString(cipher, key)).rejects.toThrow() + }) + + test('rejects an unexpected AAD on a message encrypted without one', async () => { + const cipher = await encryptString('Hello', key) + + await expect( + decryptString(cipher, key, { additionalData: 'Secret.value' }) + ).rejects.toThrow() + }) + + test('accepts raw bytes', async () => { + const aad = new Uint8Array([1, 2, 3]) + const cipher = await encryptString('Hello', key, { additionalData: aad }) + const received = await decryptString(cipher, key, { additionalData: aad }) + + expect(received).toEqual('Hello') + }) +}) diff --git a/packages/cloak/src/index.ts b/packages/cloak/src/index.ts new file mode 100644 index 0000000..27724e1 --- /dev/null +++ b/packages/cloak/src/index.ts @@ -0,0 +1,10 @@ +export { + cloakKeyRegex, + exportCryptoKey, + generateKey, + parseKey, + serializeKey, +} from './key' +export type { TextCloakKey, ParsedCloakKey } from './key' +export * from './keychain' +export * from './message' diff --git a/packages/cloak/src/key.test.ts b/packages/cloak/src/key.test.ts new file mode 100644 index 0000000..9dbba82 --- /dev/null +++ b/packages/cloak/src/key.test.ts @@ -0,0 +1,32 @@ +import { + cloakKeyRegex, + exportKey, + formatKey, + generateKey, + parseKey, +} from './key' + +describe('key', () => { + test('formatKey', () => { + const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) + const received = formatKey(bytes) + const expected = 'k1.aesgcm256.AQIDBAUGBwg=' + + expect(received).toEqual(expected) + }) + + test('parseKey + formatKey', async () => { + const key = 'k1.aesgcm256.Q46Y_L1Vx3KBVQ1POmtuGo2IdWalnbWQzxigxC-vEqo=' + const parsed = await parseKey(key) + const received = formatKey(await exportKey(parsed.key)) + + expect(parsed.fingerprint).toEqual('1fb314a0') + expect(received).toEqual(key) + }) + + test('generateKey', () => { + const key = generateKey() + + expect(key).toMatch(cloakKeyRegex) + }) +}) diff --git a/packages/cloak/src/key.ts b/packages/cloak/src/key.ts new file mode 100644 index 0000000..2e4c33a --- /dev/null +++ b/packages/cloak/src/key.ts @@ -0,0 +1,84 @@ +import { b64, hex, utf8 } from '@47ng/codec' + +export const FINGERPRINT_LENGTH = 8 + +export const cloakKeyRegex = /^k1\.aesgcm256\.(?[a-zA-Z0-9-_]{43}=?)$/ + +export type TextCloakKey = string + +export interface ParsedCloakKey { + key: CryptoKey + fingerprint: string +} + +export function formatKey(raw: Uint8Array) { + return ['k1', 'aesgcm256', b64.encode(raw)].join('.') +} + +export async function parseKey( + key: TextCloakKey, + usage?: 'encrypt' | 'decrypt' +): Promise { + return { + key: await importKey(key, usage), + fingerprint: await getKeyFingerprint(key), + } +} + +export async function serializeKey(key: ParsedCloakKey): Promise { + return formatKey(await exportKey(key.key)) +} + +export function generateKey(): TextCloakKey { + const keyLength = 32 + + const key = crypto.getRandomValues(new Uint8Array(keyLength)) + + return formatKey(key) +} + +export async function exportCryptoKey(key: CryptoKey): Promise { + const algo = key.algorithm as AesKeyAlgorithm + + if (algo.name !== 'AES-GCM' || algo.length !== 256) { + throw new Error('Unsupported key type') + } + + return formatKey(await exportKey(key)) +} + +export async function importKey( + key: TextCloakKey, + usage?: 'encrypt' | 'decrypt' +): Promise { + const match = key.match(cloakKeyRegex) + + if (!match) { + throw new Error('Unknown key format') + } + + const raw = b64.decode(match.groups!.key) + + return await crypto.subtle.importKey( + 'raw', + new Uint8Array(raw), + { + name: 'AES-GCM', + length: 256, + }, + true, + usage ? [usage] : ['encrypt', 'decrypt'] + ) +} + +export async function exportKey(key: CryptoKey): Promise { + return new Uint8Array(await crypto.subtle.exportKey('raw', key)) +} + +export async function getKeyFingerprint(key: TextCloakKey): Promise { + const data = utf8.encode(key) + + const hash = await crypto.subtle.digest('SHA-256', new Uint8Array(data)) + + return hex.encode(new Uint8Array(hash)).slice(0, FINGERPRINT_LENGTH) +} diff --git a/packages/cloak/src/keychain.ts b/packages/cloak/src/keychain.ts new file mode 100644 index 0000000..3b2a8f9 --- /dev/null +++ b/packages/cloak/src/keychain.ts @@ -0,0 +1,104 @@ +import type { ParsedCloakKey, TextCloakKey } from './key' +import { parseKey, serializeKey } from './key' +import type { CloakedString } from './message' +import { + decryptString, + encryptString, + getMessageKeyFingerprint, +} from './message' + +export interface KeychainEntry { + key: ParsedCloakKey + createdAt: number // timestamp + label?: string +} + +interface SerializedKeychainEntry { + key: TextCloakKey + createdAt: number // timestamp + label?: string +} + +export type CloakKeychain = { + [fingerprint: string]: KeychainEntry +} + +export async function makeKeychain( + keys: TextCloakKey[] +): Promise { + const keychain: CloakKeychain = {} + + for (const key of keys) { + const parsedKey = await parseKey(key) + + keychain[parsedKey.fingerprint] = { + key: parsedKey, + createdAt: Date.now(), + } + } + + return keychain +} + +export async function importKeychain( + encryptedKeychain: CloakedString, + masterKey: TextCloakKey +): Promise { + const json = await decryptString(encryptedKeychain, masterKey) + const keys: SerializedKeychainEntry[] = JSON.parse(json) + const keychain: CloakKeychain = {} + + for (const { key, ...rest } of keys) { + const parsedKey = await parseKey(key) + + keychain[parsedKey.fingerprint] = { + key: parsedKey, + ...rest, + } + } + + return keychain +} + +export async function exportKeychain( + keychain: CloakKeychain, + masterKey: TextCloakKey | ParsedCloakKey +): Promise { + const rawEntries: KeychainEntry[] = Object.values(keychain) + const entries: SerializedKeychainEntry[] = [] + + for (const entry of rawEntries) { + entries.push({ + key: await serializeKey(entry.key), + createdAt: entry.createdAt, + label: entry.label, + }) + } + + return await encryptString(JSON.stringify(entries), masterKey) +} + +export function findKeyForMessage( + message: CloakedString, + keychain: CloakKeychain +): ParsedCloakKey { + const fingerprint = getMessageKeyFingerprint(message) + + if (!(fingerprint in keychain)) { + throw new Error('Key is not available') + } + + return keychain[fingerprint].key +} + +export function getKeyAge( + fingerprint: string, + keychain: CloakKeychain, + now: number = Date.now() +) { + if (!(fingerprint in keychain)) { + throw new Error('Key is not available') + } + + return now - keychain[fingerprint].createdAt +} diff --git a/packages/cloak/src/message.ts b/packages/cloak/src/message.ts new file mode 100644 index 0000000..73ffce0 --- /dev/null +++ b/packages/cloak/src/message.ts @@ -0,0 +1,145 @@ +import { decryptAesGcm, encryptAesGcm } from './ciphers/aes-gcm' +import type { ParsedCloakKey, TextCloakKey } from './key' +import { importKey, parseKey } from './key' + +import { b64, utf8 } from '@47ng/codec' + +export type CloakedString = string + +export interface CloakOptions { + /** + * Additional authenticated data (AEAD). A string is UTF-8 encoded. The + * value is not stored in the message; the same value must be supplied to + * `decryptString` or decryption fails with an authentication error. + */ + additionalData?: string | Uint8Array +} + +function toAdditionalData( + value: string | Uint8Array | undefined +): Uint8Array | undefined { + if (value === undefined) { + return undefined + } + + return typeof value === 'string' ? new Uint8Array(utf8.encode(value)) : value +} + +export function encodeEncryptedString( + fingerprint: string, + iv: Uint8Array, + ciphertext: Uint8Array +) { + return [ + 'v1', + 'aesgcm256', + fingerprint, + b64.encode(iv), + b64.encode(ciphertext), + ].join('.') +} + +export async function encryptString( + input: string, + key: TextCloakKey | ParsedCloakKey, + options: CloakOptions = {} +): Promise { + if (typeof key === 'string') { + key = await parseKey(key, 'encrypt') + } + + const { text: ciphertext, iv } = await encryptAesGcm(key.key, input, { + additionalData: toAdditionalData(options.additionalData), + }) + + return encodeEncryptedString(key.fingerprint, iv, ciphertext) +} + +export const cloakedStringRegex = + /^v1\.aesgcm256\.(?[0-9a-fA-F]{8})\.(?[a-zA-Z0-9-_]{16})\.(?[a-zA-Z0-9-_]{22,})={0,2}$/ + +function isBase64(str: string) { + // @note validate all characters are valid base64url characters before checking padding + if (!/^[a-zA-Z0-9-_]*={0,2}$/.test(str)) { + return false + } + + const len = str.length + const firstPaddingChar = str.indexOf('=') + + return ( + firstPaddingChar === -1 || + firstPaddingChar === len - 1 || + (firstPaddingChar === len - 2 && str[len - 1] === '=') + ) +} + +export function parseCloakedString(input: CloakedString) { + const [version, algorithm, fingerprint, iv, ciphertext, nothing] = + input.split('.') + + const isCloakedString = + version === 'v1' && + algorithm === 'aesgcm256' && + /^[0-9a-f]{8}$/i.test(fingerprint) && + /^[a-zA-Z0-9-_]{16}$/.test(iv) && + isBase64(ciphertext) && + ciphertext.length >= 24 && + nothing === undefined + + if (isCloakedString === false) { + return false + } else { + return { + groups: { + fingerprint, + iv, + ciphertext, + }, + } + } +} + +export async function decryptString( + input: CloakedString, + key: TextCloakKey | ParsedCloakKey, + options: CloakOptions = {} +): Promise { + const match = parseCloakedString(input) + + if (!match) { + throw new Error(`Unknown message format`) + } + + const iv = match.groups.iv + const ciphertext = match.groups.ciphertext + + let aesKey: CryptoKey + + if (typeof key === 'string') { + aesKey = await importKey(key, 'decrypt') + } else { + aesKey = key.key + } + + return await decryptAesGcm( + aesKey, + { + iv: b64.decode(iv), + text: b64.decode(ciphertext), + }, + { + additionalData: toAdditionalData(options.additionalData), + } + ) +} + +export function getMessageKeyFingerprint(message: CloakedString) { + const match = parseCloakedString(message) + + if (!match) { + throw new Error('Unknown message format') + } + + return match.groups.fingerprint +} diff --git a/packages/cloak/tsconfig.json b/packages/cloak/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/cloak/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/db-spec/README.md b/packages/db-spec/README.md new file mode 100644 index 0000000..2d082db --- /dev/null +++ b/packages/db-spec/README.md @@ -0,0 +1,35 @@ +# @chatbotkit-dev/db-spec + +The database contract, laid out like an implementation: `prisma/schema.prisma` +is THE schema - the one hand-edited source - with the shared analytics SQL and +the zod generator config beside it. + +Every implementation derives its own `prisma/` from this one by importing the +renderers from `@chatbotkit-dev/db-spec/derive` in a small script of its own. +The direction is deliberate: this package does not know who implements it. +Implementations depend on the spec, never the reverse. + +## Changing the schema + +1. Edit `prisma/schema.prisma` +2. `pnpm -r derive` - every implementation re-derives itself; outputs are + committed, so the change shows up in review on every engine it affects +3. Push and generate through the platform as usual (`pnpm db` in `platform`) - + each implementation also re-derives automatically at the start of its own + `db:push` and `db:gen`, so a stale schema cannot reach a database or a + generated client + +The blueprint stays MySQL-complete on purpose - the engine-specific information +only flows downhill, so deriving is subtractive. The 48 queries in `prisma/sql` +are shared by every engine: `?` placeholders and `DATE()` work on MySQL and +SQLite alike. Keep them free of engine-specific expressions - dates are computed +by the caller and passed as parameters. + +## Why `zod` is pinned exactly + +The Json column shapes this package exports are zod schema *instances*, and the +platform passes them into code that checks `instanceof` against its own zod. Two +resolved zod versions mean two instances and a failure that reads as "not a Zod +schema" far from the cause. The exact pin keeps this package on the platform's +resolved version - if the platform upgrades zod, bump this pin in the same +change. diff --git a/packages/db-spec/jest.config.js b/packages/db-spec/jest.config.js new file mode 100644 index 0000000..add9213 --- /dev/null +++ b/packages/db-spec/jest.config.js @@ -0,0 +1,16 @@ +// @note diagnostics are off. This package sets `checkJs`, because its source is +// JavaScript and the JSDoc annotation is what enforces the spec - see +// src/index.js - and ts-jest would then type check every .js file it transforms, +// including the test environment itself, which was never written against this +// tsconfig. Tests are run, not compiled; `pnpm check` is what type checks this +// package. See packages/AGENTS.md. + +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', + + transform: { + '^.+\\.[tj]sx?$': ['ts-jest', { useESM: true, diagnostics: false }], + }, +} diff --git a/packages/db-spec/package.json b/packages/db-spec/package.json new file mode 100644 index 0000000..828a925 --- /dev/null +++ b/packages/db-spec/package.json @@ -0,0 +1,40 @@ +{ + "name": "@chatbotkit-dev/db-spec", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "rimraf node_modules *.tsbuildinfo", + "format": "prisma format --schema prisma/schema.prisma", + "lint": "eslint src scripts --ext .ts,.js", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "eslint": "^9.0.0", + "jest": "^29", + "prisma": "^7.3.0", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + }, + "exports": { + "./derive": { + "import": "./src/derive.js", + "default": "./src/derive.js" + }, + "./types": { + "import": "./src/types.ts", + "default": "./src/types.ts" + }, + "./scripts/post-generate-zod.js": "./scripts/post-generate-zod.js" + }, + "dependencies": { + "zod": "3.25.67" + } +} diff --git a/packages/db-spec/prisma/schema.prisma b/packages/db-spec/prisma/schema.prisma new file mode 100644 index 0000000..5d38f67 --- /dev/null +++ b/packages/db-spec/prisma/schema.prisma @@ -0,0 +1,3500 @@ +// @note THE schema - the one hand-edited source, here in +// @chatbotkit-dev/db-spec. Every implementation derives its own +// prisma/schema.prisma from this file; none of those is edited directly. It stays +// MySQL-complete on purpose: the engine-specific information (native types, the +// expression defaults TEXT columns force) only flows downhill, so deriving is +// subtractive. See src/derive.js for exactly what each engine changes. + +generator client { + provider = "prisma-client" + output = "./generated/prisma" + previewFeatures = ["typedSql"] +} + +generator json { + provider = "prisma-json-types-generator" + namespace = "PrismaJson" + allowAny = false +} + +generator zod { + provider = "prisma-zod-generator" + output = "./zod/schemas" + config = "./zod-generator.config.json" +} + +generator pothos { + provider = "prisma-pothos-types" + output = "./generated/pothos.ts" + generateDatamodel = true + documentation = false + clientOutput = "./prisma" +} + +// --- +// --- +// --- + +datasource db { + provider = "mysql" + relationMode = "prisma" +} + +// --- +// --- +// --- + +// @see https://next-auth.js.org/adapters/prisma + +model Account { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + type String + + provider String + providerAccountId String + + refresh_token String? @db.Text /// @encrypted + access_token String? @db.Text /// @encrypted + id_token String? @db.Text /// @encrypted + token_type String? + + expires_at Int? + + scope String? + + session_state String? + + refresh_token_expires_in Int? + + meta Json? /// [Meta] + + // timestamps + // @note disabled because new + // createdAt DateTime @default(now()) + // updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([provider, providerAccountId]) + // indexes: other + @@index(fields: [userId]) +} + +model Session { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String? // @note deliberately optional + description String? // @note deliberately optional + + sessionToken String @unique + + expires DateTime + + audience String? + + payload Json? /// [JsonRecord] + + options Json? /// [JsonRecord] + + meta Json? /// [Meta] + + // timestamps + // @note disabled because new + // createdAt DateTime @default(now()) + // updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [userId]) +} + +model VerificationToken { + // fields + identifier String + + token String @unique + + expires DateTime + + meta Json? /// [Meta] + + // timestamps + // @note disabled because new + // createdAt DateTime @default(now()) + // updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([identifier, token]) +} + +// --- +// --- +// --- + +model User { + id String @id @default(cuid()) + + // relationships + parentId String? + parent User? @relation("ParentChild", fields: [parentId], references: [id], onDelete: NoAction, onUpdate: NoAction) // @note prevents deletion - must use deleteUser function + + // ref + alias String? // @note only used for child Users (parentId must be set) + + // fields + name String? // @note deliberately optional + description String? @db.Text // @note deliberately optional + + email String @unique + emailVerified DateTime? + + parentContextName String? + + parentContextEmail String? + parentContextEmailVerified DateTime? + + image String? + + billingCustomerId String? @unique + billingSubscriptionId String? + billingSubscriptionStatus String? + billingSubscriptionStartedAt DateTime? + billingSubscriptionTrialedAt DateTime? + + channel String? + organization String? + industry String? + role String? + goal String? @db.Text + + limits Json? /// [Limits] + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: account + accounts Account[] + sessions Session[] + + // connections: children + children User[] @relation("ParentChild") + + // connections: teams + teams Team[] + + // connections: developer + tokens Token[] + webhooks Webhook[] + + // connections: objects + contacts Contact[] + conversations Conversation[] + tasks Task[] + ratings Rating[] + memories Memory[] + spaces Space[] + + // connections: executions + taskExecutions TaskExecution[] + + // connections: resources + blueprints Blueprint[] + bots Bot[] + datasets Dataset[] + skillsets Skillset[] + abilities Ability[] + secrets Secret[] + files File[] + portals Portal[] + policies Policy[] + + // connections: publishing + spaceSites SpaceSite[] + + // connections: special + contexts Context[] + + // connections: integrations + triggerIntegrations TriggerIntegration[] + widgetIntegrations WidgetIntegration[] + slackIntegrations SlackIntegration[] + discordIntegrations DiscordIntegration[] + microsoftteamsIntegrations MicrosoftteamsIntegration[] + googlechatIntegrations GooglechatIntegration[] + whatsappIntegrations WhatsappIntegration[] + messengerIntegrations MessengerIntegration[] + instagramIntegrations InstagramIntegration[] + telegramIntegrations TelegramIntegration[] + twilioIntegrations TwilioIntegration[] + anamIntegrations AnamIntegration[] + avatarIntegrations AvatarIntegration[] + recallIntegrations RecallIntegration[] + githubIntegrations GithubIntegration[] + emailIntegrations EmailIntegration[] + sitemapIntegrations SitemapIntegration[] + notionIntegrations NotionIntegration[] + supportIntegrations SupportIntegration[] + extractIntegrations ExtractIntegration[] + mcpserverIntegrations McpserverIntegration[] + skillserverIntegrations SkillserverIntegration[] + + // connections: oauth + oAuthConnections OAuthConnection[] + oAuthApplications OAuthApplication[] + oAuthApplicationTokens OAuthApplicationToken[] + + // connections: hub + hubBotPages HubBotPage[] + hubDatasetPages HubDatasetPage[] + hubSkillsetPages HubSkillsetPage[] + hubBlueprintPages HubBlueprintPage[] + hubWidgetPages HubWidgetPage[] + + // connections: secret values + secretValues SecretValue[] + + // connections: lock + locks Lock[] + + // connections: usage + usages Usage[] @relation("UserUsage") + childUsages Usage[] @relation("ParentUserUsage") + + // connections: observability + eventLogs EventLog[] + eventMetrics EventMetric[] + auditLogs AuditLog[] + + // indexes: unique + @@unique([parentId, alias]) // for alias lookup by parent + @@unique([parentId, parentContextEmail]) + // indexes: other + @@index(fields: [parentId]) + @@index(fields: [id, parentId]) +} + +// --- +// --- +// --- + +enum ResourceState { + // @note the order is important because the default value is the first one + + enabled + disabled +} + +// --- +// --- +// --- + +model Team { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: team + memberships TeamMembership[] + + // indexes: other + @@index(fields: [userId]) +} + +model TeamMembership { + id String @id @default(cuid()) + + // relationships + teamId String + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + email String + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([teamId, email]) + // indexes: other + @@index(fields: [email]) +} + +// --- +// --- +// --- + +model OAuthConnection { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + issuer String? + clientId String? + clientSecret String? @db.Text /// @encrypted + + scopes String @default("openid email profile") + + allowedDomains String? @db.Text + requiredClaims Json? /// [JsonRecord] + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: integrations + mcpserverIntegrations McpserverIntegration[] + + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) +} + +model OAuthApplication { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + clientId String @unique + clientSecret String @unique /// @digest + + redirectUris Json @default("[]") /// [OAuthRedirectUris] + + scopes Json @default("[]") /// [OAuthScopes] + + grants Json @default("[]") /// [OAuthGrants] + + accessTokenLifetime Int? + refreshTokenLifetime Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: oauth + oAuthApplicationTokens OAuthApplicationToken[] + + // indexes: other + @@index(fields: [userId]) +} + +model OAuthApplicationToken { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + applicationId String + application OAuthApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + accessToken String @unique /// @digest + accessTokenExpiresAt DateTime? + + refreshToken String? @unique /// @digest + refreshTokenExpiresAt DateTime? + + scopes Json @default("[]") /// [OAuthScopes] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [userId]) + @@index(fields: [applicationId]) +} + +// --- +// --- +// --- + +enum Visibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +// --- +// --- +// --- + +model Lock { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: resources + blueprints Blueprint[] + bots Bot[] + datasets Dataset[] + skillsets Skillset[] + files File[] + secrets Secret[] + policies Policy[] + portals Portal[] + + // connections: integrations + triggerIntegrations TriggerIntegration[] + widgetIntegrations WidgetIntegration[] + slackIntegrations SlackIntegration[] + discordIntegrations DiscordIntegration[] + microsoftteamsIntegrations MicrosoftteamsIntegration[] + googlechatIntegrations GooglechatIntegration[] + whatsappIntegrations WhatsappIntegration[] + messengerIntegrations MessengerIntegration[] + instagramIntegrations InstagramIntegration[] + telegramIntegrations TelegramIntegration[] + twilioIntegrations TwilioIntegration[] + githubIntegrations GithubIntegration[] + emailIntegrations EmailIntegration[] + sitemapIntegrations SitemapIntegration[] + notionIntegrations NotionIntegration[] + supportIntegrations SupportIntegration[] + extractIntegrations ExtractIntegration[] + mcpserverIntegrations McpserverIntegration[] + skillserverIntegrations SkillserverIntegration[] +} + +// --- +// --- +// --- + +model Context { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + payload Json? /// [JsonRecord] + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) + @@index(fields: [botId]) + @@index(fields: [datasetId]) + @@index(fields: [skillsetId]) + @@index(fields: [contactId]) +} + +// --- +// --- +// --- + +enum BlueprintVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Blueprint { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + visibility BlueprintVisibility @default(private) + + config Json? /// [JsonRecord] + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: hub + hubBlueprintPage HubBlueprintPage? + + // connections: resources + bots Bot[] /// @resource + datasets Dataset[] /// @resource + skillsets Skillset[] /// @resource + abilities Ability[] /// @resource + secrets Secret[] /// @resource + files File[] /// @resource + spaces Space[] /// @resource + portals Portal[] /// @resource + policies Policy[] /// @resource + tasks Task[] /// @resource + + // connections: special + contexts Context[] + + // connections: oauth + oAuthConnections OAuthConnection[] /// @resource + + // connections: integrations + triggerIntegrations TriggerIntegration[] /// @resource + widgetIntegrations WidgetIntegration[] /// @resource + slackIntegrations SlackIntegration[] /// @resource + discordIntegrations DiscordIntegration[] /// @resource + microsoftteamsIntegrations MicrosoftteamsIntegration[] /// @resource + googlechatIntegrations GooglechatIntegration[] /// @resource + whatsappIntegrations WhatsappIntegration[] /// @resource + messengerIntegrations MessengerIntegration[] /// @resource + instagramIntegrations InstagramIntegration[] /// @resource + telegramIntegrations TelegramIntegration[] /// @resource + twilioIntegrations TwilioIntegration[] /// @resource + avatarIntegrations AvatarIntegration[] /// @resource + anamIntegrations AnamIntegration[] /// @resource + recallIntegrations RecallIntegration[] /// @resource + githubIntegrations GithubIntegration[] /// @resource + emailIntegrations EmailIntegration[] /// @resource + supportIntegrations SupportIntegration[] /// @resource + extractIntegrations ExtractIntegration[] /// @resource + sitemapIntegrations SitemapIntegration[] /// @resource + notionIntegrations NotionIntegration[] /// @resource + mcpserverIntegrations McpserverIntegration[] /// @resource + skillserverIntegrations SkillserverIntegration[] /// @resource + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) +} + +// --- +// --- +// --- + +enum BotVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Bot { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + backstory String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.MediumText + + model String @default("") @db.VarChar(512) + + privacy Boolean @default(false) + moderation Boolean @default(false) + + visibility BotVisibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: hub + hubBotPage HubBotPage? + + // connections: objects + conversations Conversation[] + tasks Task[] + ratings Rating[] + memories Memory[] + + // connections: resources + abilities Ability[] + policies Policy[] + + // connections: special + contexts Context[] + + // connections: integrations + triggerIntegrations TriggerIntegration[] + widgetIntegrations WidgetIntegration[] + slackIntegrations SlackIntegration[] + discordIntegrations DiscordIntegration[] + microsoftteamsIntegrations MicrosoftteamsIntegration[] + googlechatIntegrations GooglechatIntegration[] + whatsappIntegrations WhatsappIntegration[] + messengerIntegrations MessengerIntegration[] + instagramIntegrations InstagramIntegration[] + telegramIntegrations TelegramIntegration[] + twilioIntegrations TwilioIntegration[] + anamIntegrations AnamIntegration[] + avatarIntegrations AvatarIntegration[] + recallIntegrations RecallIntegration[] + githubIntegrations GithubIntegration[] + emailIntegrations EmailIntegration[] + supportIntegrations SupportIntegration[] + extractIntegrations ExtractIntegration[] + + // connections: connections + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) + @@index(fields: [datasetId]) + @@index(fields: [skillsetId]) + @@index(fields: [id, datasetId]) +} + +// --- +// --- +// --- + +// @idea A/B Testing / Experiments +// +// This is a proposed design for A/B testing bots. The idea is that an +// Experiment acts as a "router" that distributes traffic across multiple +// bot variants based on configurable weights. +// +// How it works: +// 1. User creates multiple bots (each bot is effectively a "version") +// 2. User creates an Experiment with ExperimentVariants pointing to those bots +// 3. Integration (Widget, Slack, etc.) references the experimentId +// 4. Session creation selects a variant based on weights and routes to that bot +// 5. Conversation records experimentId + experimentVariantId for analytics +// +// Precedence logic for integrations: +// - If experimentId is set and experiment is active → use experiment routing +// - Otherwise fall back to botId +// +// Example: +// Experiment "New Prompt Test" +// ├─ Variant "Control" (50%) → Bot A +// └─ Variant "Friendly Tone" (50%) → Bot B +// +// Analytics can then compare ratings, engagement, etc. per variant. +// +// Schema: +// +// enum ExperimentStatus { +// draft // not yet running +// active // accepting traffic +// paused // temporarily stopped +// completed // finished, winner declared +// } +// +// model Experiment { +// id String @id @default(cuid()) +// +// // owner +// userId String +// user User @relation(fields: [userId], references: [id], onDelete: Cascade) +// +// // relationships +// blueprintId String? +// blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) +// +// // fields +// name String @default("") +// description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text +// +// status ExperimentStatus @default(draft) +// +// startedAt DateTime? +// completedAt DateTime? +// +// // optional: reference to winning variant after experiment concludes +// winnerVariantId String? +// +// meta Json? /// [Meta] +// +// // timestamps +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// +// // connections +// variants ExperimentVariant[] +// conversations Conversation[] +// +// // indexes +// @@index(fields: [userId, status, createdAt(sort: Desc)]) +// @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) +// } +// +// model ExperimentVariant { +// id String @id @default(cuid()) +// +// // relationships +// experimentId String +// experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) +// botId String +// bot Bot @relation(fields: [botId], references: [id], onDelete: Cascade) +// +// // fields +// name String @default("") // e.g., "Control", "Variant A", "Friendly Tone" +// weight Int @default(1) // relative weight (not percentage) - traffic is distributed proportionally +// isControl Boolean @default(false) +// +// meta Json? /// [Meta] +// +// // timestamps +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// +// // connections +// conversations Conversation[] +// +// // indexes +// @@index(fields: [experimentId]) +// @@index(fields: [botId]) +// } +// +// Additional changes needed: +// +// 1. Add to integrations (WidgetIntegration, SlackIntegration, etc.): +// experimentId String? +// experiment Experiment? @relation(fields: [experimentId], references: [id], onDelete: SetNull) +// +// 2. Add to Conversation: +// experimentId String? +// experiment Experiment? @relation(fields: [experimentId], references: [id], onDelete: SetNull) +// experimentVariantId String? +// experimentVariant ExperimentVariant? @relation(fields: [experimentVariantId], references: [id], onDelete: SetNull) +// +// 3. Add to Bot: +// experimentVariants ExperimentVariant[] + +// --- +// --- +// --- + +enum DatasetVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Dataset { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + reranker String? + + recordMaxTokens Int? + + searchMinScore Float? + searchMaxRecords Int? + searchMaxTokens Int? + + separators String? + + matchInstruction String? @db.Text // do not tempt to set to empty string + mismatchInstruction String? @db.Text // do not tempt to set to empty string + + visibility DatasetVisibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: hub + hubDatasetPage HubDatasetPage? + + // connections: objects + conversations Conversation[] + + // connections: resources + bots Bot[] + files DatasetFileAttachment[] + + // connections: special + contexts Context[] + + // connections: integrations + sitemapIntegrations SitemapIntegration[] + notionIntegrations NotionIntegration[] + + // connections: many-to-many + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) +} + +// --- +// --- +// --- + +enum SkillsetVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Skillset { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + visibility SkillsetVisibility @default(private) + + // lifecycle state - toggle the whole skillset on/off without deleting it + state ResourceState @default(enabled) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: conversation + conversations Conversation[] + + // connections: resources + bots Bot[] + abilities Ability[] + + // connections: special + contexts Context[] + + // connections: integrations + mcpserverIntegrations McpserverIntegration[] + skillserverIntegrations SkillserverIntegration[] + + // connections: hub + hubSkillsetPage HubSkillsetPage? + + // connections: many-to-many + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) +} + +model Ability { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + // @note the resource an ability is linked to (what it acts on), as opposed + // to the owner/container relations above + linkedSecretId String? + linkedSecret Secret? @relation(fields: [linkedSecretId], references: [id], onDelete: SetNull) + linkedFileId String? + linkedFile File? @relation(fields: [linkedFileId], references: [id], onDelete: SetNull) + linkedBotId String? + linkedBot Bot? @relation(fields: [linkedBotId], references: [id], onDelete: SetNull) + linkedSpaceId String? + linkedSpace Space? @relation(fields: [linkedSpaceId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + instruction String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + // lifecycle state - toggle the ability on/off without deleting it + state ResourceState @default(enabled) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [blueprintId]) + @@index(fields: [skillsetId]) +} + +// --- +// --- +// --- + +enum FileVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model File { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + visibility FileVisibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: resources + abilities Ability[] + datasets DatasetFileAttachment[] + widgetIntegrations WidgetIntegrationFileAttachment[] + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) + @@index(fields: [createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +enum SecretKind { + shared + personal +} + +enum SecretType { + plain + basic + bearer + jwt + oauth + template + reference +} + +enum SecretVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Secret { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + kind SecretKind @default(shared) + + type SecretType @default(plain) + + value String? @db.Text /// @encrypted + + config Json? /// [SecretConfig] @encrypted @see prisma/post-generate-zod.js + + visibility SecretVisibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: resources + abilities Ability[] + + // connections: secret values + secretValues SecretValue[] + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) +} + +model SecretValue { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + secretId String? + secret Secret? @relation(fields: [secretId], references: [id], onDelete: Cascade) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + value String @db.Text /// @encrypted + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, secretId, contactId]) + // indexes: other + @@index(fields: [userId]) +} + +// --- +// --- +// --- + +model Portal { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + slug String @unique + + config Json? /// [PortalConfig] @see prisma/post-generate-zod.js + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) +} + +// --- +// --- +// --- + +enum Trigger { + never + + automatic +} + +// --- + +enum Schedule { + never + + quarterhourly + halfhourly + hourly + + twicedaily + daily + + twiceweekly + weekly + + twicemonthly + monthly +} + +// --- + +enum SyncStatus { + pending + synced + error +} + +// --- +// --- +// --- + +model TriggerIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + secret String @db.Text /// @encrypted + + authenticate Boolean @default(true) + + sessionDuration Float? + + schedule String? + timezone String? + + lastTriggerAt DateTime? + nextTriggerAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [schedule, lastTriggerAt]) + @@index(fields: [nextTriggerAt]) + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model WidgetIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + theme String? @db.Text + + layout String? @db.Text + + title String? + + intro String? @db.Text + + initial String? @db.Text + + placeholder String? + + origin String? @db.Text + + sessionDuration Float? + + language String? + + plugins String? @db.Text + + stream Boolean @default(true) + + verbose Boolean @default(true) + + tools Boolean @default(false) + + unfurl Boolean @default(true) + + math Boolean @default(false) + + carousel Boolean @default(false) + + form Boolean @default(false) + + attachments Boolean @default(false) + + autoScroll Boolean @default(true) + + startFirst Boolean @default(false) + + contactCollection Boolean @default(false) + + exportConversation Boolean @default(true) + restartConversation Boolean @default(true) + + maximize Boolean @default(true) + + messagePeek Boolean @default(true) + + voiceIn Boolean @default(false) + voiceOut Boolean @default(false) + + poweredBy Boolean @default(true) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: resources + files WidgetIntegrationFileAttachment[] + + // connections: hub + hubWidgetPage HubWidgetPage? + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model SlackIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + signingSecret String? @db.Text /// @encrypted + + botToken String? @db.Text /// @encrypted + userToken String? @db.Text /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + references Boolean @default(false) + + ratings Boolean @default(false) + + visibleMessages Int? + + autoRespond String? @db.Text + + allowFrom String? @default(dbgenerated("(_utf8mb4\\'*\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model DiscordIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + appId String? + botToken String? @db.Text /// @encrypted + publicKey String? + + handle String? + + ephemeral Boolean? + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default(dbgenerated("(_utf8mb4\\'*\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model MicrosoftteamsIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + botFrameworkAppId String? + botFrameworkAppSecret String? @db.Text /// @encrypted + tenantId String? + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default(dbgenerated("(_utf8mb4\\'*\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model GooglechatIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + serviceAccountKey String? @db.Text /// @encrypted + + projectNumber String? // @note Google Cloud project number used to verify the JWT audience on incoming events + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + autoRespond String? @db.Text + + allowFrom String? @default(dbgenerated("(_utf8mb4\\'*\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model WhatsappIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + verifyToken String @db.Text /// @encrypted + + appSecret String? @db.Text /// @encrypted + + phoneNumberId String? + + accessToken String? @db.Text /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default(dbgenerated("(_utf8mb4\\'*\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model MessengerIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + verifyToken String @db.Text /// @encrypted + + accessToken String? @db.Text /// @encrypted + + // @note the customer's own Meta APP secret - the key X-Hub-Signature-256 + // callbacks are signed with. Optional: without it callbacks are accepted + // unverified, with a logged bypass. + appSecret String? @db.Text /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model InstagramIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + verifyToken String @db.Text /// @encrypted + + accessToken String? @db.Text /// @encrypted + + // @note the customer's own Meta APP secret - the key X-Hub-Signature-256 + // callbacks are signed with. Optional: without it callbacks are accepted + // unverified, with a logged bypass. + appSecret String? @db.Text /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model TelegramIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + botToken String? @db.Text /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default(dbgenerated("(_utf8mb4\\'*\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model TwilioIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + accountSid String? @db.Text + authToken String? @db.Text /// @encrypted + + voice String? @db.Text + + contactCollection Boolean @default(false) + + sessionDuration Float? + + allowFrom String? @default(dbgenerated("(_utf8mb4\\'*\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model AnamIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + apiKey String? @db.Text /// @encrypted + + personaId String? + + visibility Visibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [blueprintId]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model AvatarIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + visibility Visibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [blueprintId]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model RecallIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + apiKey String? @db.Text /// @encrypted + + // @note the signing secret of the Recall webhook endpoint (Svix, `whsec_...`). + // Optional: without it status callbacks are accepted unverified, with a logged + // bypass. + webhookSecret String? @db.Text /// @encrypted + + region String? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [blueprintId]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model GithubIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + // this integration's GitHub App identity + credentials. Each integration is + // its own GitHub App. The installation id is NOT stored: it rides in every + // event payload and is combined with the App key to mint a token to reply. + appId String? // the GitHub App id (public-ish; signs the App JWT as `iss`) + privateKey String? @db.Text /// @encrypted - the App's RSA private key (PEM) + webhookSecret String? @db.Text /// @encrypted - validates x-hub-signature-256 HMAC-SHA256 + + contactCollection Boolean @default(false) + + sessionDuration Float? + + // @note unlike the other integrations this defaults to `@collaborators` and + // not `*`: an installed App hears from everyone who can comment, which on a + // public repository is every GitHub account. Rows predating this field were + // backfilled to `*` to preserve their behaviour, so the column default only + // governs new integrations. See lib/github.validation.ts + allowFrom String? @default(dbgenerated("(_utf8mb4\\'@collaborators\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model EmailIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default(dbgenerated("(_utf8mb4\\'*\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model SitemapIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + url String? + + glob String? + + selectors String? + + javascript Boolean? + + expiresIn Float? + + syncStatus SyncStatus @default(pending) + syncSchedule Schedule @default(never) + + lastSyncedAt DateTime @default(now()) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [syncSchedule, lastSyncedAt]) // used to find sitemap integrations to sync +} + +// --- +// --- +// --- + +model NotionIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + token String @db.Text /// @encrypted + + expiresIn Float? + + syncStatus SyncStatus @default(pending) + syncSchedule Schedule @default(never) + + lastSyncedAt DateTime @default(now()) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [syncSchedule, lastSyncedAt]) // used to find notion integrations to sync +} + +// --- +// --- +// --- + +model SupportIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + email String? + + trigger Trigger? @default(automatic) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [botId]) + @@index(fields: [userId, botId]) +} + +// --- +// --- +// --- + +model ExtractIntegrationItem { + id String @id @default(cuid()) + + // relationships + extractIntegrationId String + extractIntegration ExtractIntegration @relation(fields: [extractIntegrationId], references: [id], onDelete: Cascade) + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + + // fields + data Json? /// [JsonRecord] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([extractIntegrationId, conversationId]) + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [extractIntegrationId, createdAt(sort: Desc)]) // used for listing +} + +model ExtractIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + schema Json? /// [JsonRecord] + + request String? @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + model String? @db.VarChar(512) + + trigger Trigger? @default(automatic) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: items + items ExtractIntegrationItem[] + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model McpserverIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + oAuthConnectionId String? + oAuthConnection OAuthConnection? @relation(fields: [oAuthConnectionId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + accessToken String @db.Text /// @encrypted + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model SkillserverIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + accessToken String @db.Text /// @encrypted + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +enum PolicyType { + // @note the order is important because of the default value is the first one + + retention + usage +} + +model Policy { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: Cascade) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + type PolicyType @default(retention) + + // lifecycle state - toggle the policy on/off without deleting it + state ResourceState @default(enabled) + + config Json? /// [PolicyConfig] @see prisma/post-generate-zod.js + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) + @@index(fields: [userId, type]) + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [botId, type]) +} + +// --- +// --- +// --- + +model HubBotPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + botId String @unique + bot Bot @relation(fields: [botId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + slug String? @unique + icon String? + rank Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model HubDatasetPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + datasetId String @unique + dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + slug String? @unique + icon String? + rank Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model HubSkillsetPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + skillsetId String @unique + skillset Skillset @relation(fields: [skillsetId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + slug String? @unique + icon String? + rank Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model HubBlueprintPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String @unique + blueprint Blueprint @relation(fields: [blueprintId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + slug String? @unique + icon String? + rank Int? + + shareLog Boolean @default(false) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model HubWidgetPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + widgetId String @unique + widget WidgetIntegration @relation(fields: [widgetId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + slug String? @unique + icon String? + rank Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// @note UseType was migrated from an enum to a plain String to avoid requiring +// database migrations every time a new model is added. Valid use type values +// are now derived from the model config at runtime. See lib/usage.types.js. + +model Usage { + id String @id @default(cuid()) + + // owner + // @note usage records must survive user deletion with userId intact + userId String + user User @relation("UserUsage", fields: [userId], references: [id], onDelete: NoAction) + + // parent + // @note parent usage records must survive user deletion with parentUserId intact + parentUserId String? + parentUser User? @relation("ParentUserUsage", fields: [parentUserId], references: [id], onDelete: NoAction) + + // relationships: shallow + conversationId String? + messageId String? + taskId String? + contactId String? + blueprintId String? + botId String? + datasetId String? + skillsetId String? + abilityId String? + + // fields + type String @db.VarChar(191) + + count Int + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [type, createdAt(sort: Desc)]) // used for listing + @@index(fields: [userId, type, createdAt(sort: Desc)]) + @@index(fields: [userId, botId, createdAt(sort: Desc)]) // used by bot usage stats query + @@index(fields: [parentUserId, type, createdAt(sort: Desc)]) // used for parent account usage aggregation +} + +// --- +// --- +// --- + +model EventLog { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships: shallow + conversationId String? + taskId String? + contactId String? + spaceId String? + blueprintId String? + botId String? + datasetId String? + recordId String? + skillsetId String? + abilityId String? + fileId String? + secretId String? + portalId String? + policyId String? + widgetIntegrationId String? + slackIntegrationId String? + discordIntegrationId String? + microsoftteamsIntegrationId String? @map("teamsIntegrationId") + googlechatIntegrationId String? + whatsappIntegrationId String? + messengerIntegrationId String? + instagramIntegrationId String? + telegramIntegrationId String? + twilioIntegrationId String? + githubIntegrationId String? + emailIntegrationId String? + sitemapIntegrationId String? + notionIntegrationId String? + triggerIntegrationId String? + supportIntegrationId String? + extractIntegrationId String? + mcpserverIntegrationId String? + skillserverIntegrationId String? + // @note the event log tracks conversational integrations only; anam, + // avatar and recall are excluded by design (they emit no events). If that + // ever changes, add the three `*IntegrationId` columns here, the matching + // entries in the event log/metric list+export whitelists and the + // EventLogItem type, then regenerate the client. + webhookId String? + taskExecutionId String? + triggerExecutionId String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + type String + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [userId, type, createdAt(sort: Desc)]) +} + +model EventMetric { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships: shallow + conversationId String? + taskId String? + contactId String? + spaceId String? + blueprintId String? + botId String? + datasetId String? + recordId String? + skillsetId String? + abilityId String? + fileId String? + secretId String? + portalId String? + policyId String? + widgetIntegrationId String? + slackIntegrationId String? + discordIntegrationId String? + microsoftteamsIntegrationId String? @map("teamsIntegrationId") + googlechatIntegrationId String? + whatsappIntegrationId String? + messengerIntegrationId String? + instagramIntegrationId String? + telegramIntegrationId String? + twilioIntegrationId String? + githubIntegrationId String? + emailIntegrationId String? + sitemapIntegrationId String? + notionIntegrationId String? + triggerIntegrationId String? + supportIntegrationId String? + extractIntegrationId String? + mcpserverIntegrationId String? + skillserverIntegrationId String? + // @note the event log tracks conversational integrations only; anam, + // avatar and recall are excluded by design (they emit no events). If that + // ever changes, add the three `*IntegrationId` columns here, the matching + // entries in the event log/metric list+export whitelists and the + // EventLogItem type, then regenerate the client. + webhookId String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + type String + + value Float? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [userId, type, createdAt(sort: Desc)]) +} + +// --- + +model AuditLog { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + conversationId String? + taskId String? + contactId String? + spaceId String? + blueprintId String? + botId String? + datasetId String? + recordId String? + skillsetId String? + abilityId String? + fileId String? + secretId String? + portalId String? + policyId String? + webhookId String? + sessionId String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + action String + + oldValues Json? /// [JsonRecord] + newValues Json? /// [JsonRecord] + + ipAddress String? + userAgent String? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [userId, action, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model Contact { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + preferences String? @db.Text + + fingerprint String @default(cuid()) + + email String? + phone String? + nick String? + + verifiedAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: objects + conversations Conversation[] + tasks Task[] + ratings Rating[] + memories Memory[] + spaces Space[] + contexts Context[] + + // connections: secret values + secretValues SecretValue[] + + // indexes: unique + @@unique([userId, fingerprint]) + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [userId, email]) +} + +// --- + +model Conversation { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + spaceId String? + space Space? @relation(fields: [spaceId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + taskId String? + task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + backstory String? @default(dbgenerated("(_utf8mb4\\'\\')")) @db.MediumText + + model String? @default("") @db.VarChar(512) + + privacy Boolean? @default(false) + moderation Boolean? @default(false) + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: objects + messages Message[] + ratings Rating[] + + // connections: tasks + taskExecutions TaskExecution[] + + // connections: extractIntegrationItems + extractIntegrationItems ExtractIntegrationItem[] + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) // used for listing + @@index(fields: [userId, contactId, createdAt(sort: Desc)]) // used for listing + @@index(fields: [id, botId]) + @@index(fields: [id, datasetId]) + @@index(fields: [id, skillsetId]) + @@index(fields: [expiresAt]) // used for cleaning up expired conversations + @@index(fields: [createdAt]) // used for cleaning up empty conversations + @@index(fields: [datasetId]) + @@index(fields: [skillsetId]) + @@index(fields: [botId]) + @@index(fields: [contactId, createdAt(sort: Desc)]) // used by conversation fetch in actions + @@index(fields: [spaceId, createdAt(sort: Desc)]) // used by conversation fetch in actions + @@index(fields: [taskId, createdAt(sort: Desc)]) // used by task fetch in actions + @@index(fields: [contactId, taskId, createdAt(sort: Desc)]) // used by task fetch in actions +} + +// --- + +enum MessageType { + // stable types + + user // a message from the user + bot // a message from the bot + reasoning // a message that carries reasoning such as a thought process + context // a message that carries some context such as additional information + instruction // a message that carries some instruction such as a command + backstory // a message that describes the backstory of the bot + activity // a message that describes an activity + checkpoint // a message that describes a compact summary of the conversation + // notification // a message that describes a notification +} + +model Message { + id String @id @default(cuid()) + + // relationships + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + type MessageType + + text String @db.Text + + nps Int? + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: objects + ratings Rating[] + + // connections: tasks + taskExecutionsAsStart TaskExecution[] @relation("TaskExecutionStart") + taskExecutionsAsEnd TaskExecution[] @relation("TaskExecutionEnd") + + // indexes: other + @@index(fields: [conversationId, type, createdAt(sort: Desc), id(sort: Desc)]) // supports reverse-chronological engine reads within one conversation and message type; id breaks timestamp ties + @@index(fields: [conversationId, type, createdAt(sort: Desc)]) // specifically used in /api/v1/conversation/[conversationId]/send to get messages fast + @@index(fields: [conversationId, createdAt(sort: Desc)]) // @note all messages are almost always pulled in reverse order +} + +// --- + +model Rating { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + conversationId String? + conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull) + messageId String? + message Message? @relation(fields: [messageId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + value Int + + reason String? @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [botId]) + @@index(fields: [conversationId]) + @@index(fields: [messageId]) +} + +// --- + +enum TaskStatus { + idle // not currently running + running // actively executing + canceled // execution was canceled before completion +} + +enum TaskOutcome { + pending // never run yet + success // last run completed successfully + failure // last run failed or was incomplete +} + +model TaskExecution { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + taskId String + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + conversationId String? + conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull) + startMessageId String? + startMessage Message? @relation("TaskExecutionStart", fields: [startMessageId], references: [id], onDelete: SetNull) + endMessageId String? + endMessage Message? @relation("TaskExecutionEnd", fields: [endMessageId], references: [id], onDelete: SetNull) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + status TaskStatus @default(idle) + outcome TaskOutcome @default(pending) + + completedAt DateTime? + + resumeAt DateTime? + + keepAliveUntil DateTime? + + summary String? @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [taskId, createdAt(sort: Desc)]) + // indexes: reaper (stalled sweep over running executions) + @@index(fields: [status, keepAliveUntil]) + // indexes: foreign keys + @@index(fields: [userId]) + @@index(fields: [taskId]) + @@index(fields: [conversationId]) + @@index(fields: [startMessageId]) + @@index(fields: [endMessageId]) +} + +// --- + +model Task { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + status TaskStatus @default(idle) + outcome TaskOutcome @default(pending) + + sessionDuration Float? + + maxIterations Int? + maxTime Float? + maxCalls Int? + + schedule String? + timezone String? + + nextRunAt DateTime? + lastRunAt DateTime? + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: tasks + taskExecutions TaskExecution[] + + // connections: objects + conversations Conversation[] + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) + @@index(fields: [schedule, lastRunAt]) + @@index(fields: [nextRunAt]) +} + +// --- + +model Memory { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + text String @db.Text + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [userId]) + @@index(fields: [botId]) +} + +// --- + +model Space { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: conversation + conversations Conversation[] + + // connections: resources + abilities Ability[] + + // connections: publishing + sites SpaceSite[] + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [blueprintId]) +} + +// --- + +model SpaceSite { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // attachment (many sites per space) + spaceId String + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + + // ref + alias String? + + // basic information + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + // host binding + slug String @unique + + // serving config + prefix String? + index String @default("index.html") + notFound String @default("404.html") + + // meta and others + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) + @@index(fields: [spaceId]) +} + +// --- +// --- +// --- + +// @note `Token` is the API token (auth credential); LLM token counting is +// `usage` and `limits` vocabulary. Both keep the word by decision - the two +// never share a type or a table, and prose distinguishes "API token" from +// "usage tokens". + +model Token { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + token String @unique /// @digest + + config Json? /// [TokenConfig] @see prisma/post-generate-zod.js + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [createdAt(sort: Desc)]) // used for listing + @@index(fields: [userId]) +} + +// --- +// --- +// --- + +model Webhook { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + request String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + events String @default(dbgenerated("(_utf8mb4\\'\\')")) @db.Text + + secret String @unique /// @encrypted + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [userId]) + @@index(fields: [createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +enum DatasetFileAttachmentType { + source +} + +model DatasetFileAttachment { + // relationships + datasetId String + dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade) + fileId String + file File @relation(fields: [fileId], references: [id], onDelete: Cascade) + + // fields + type DatasetFileAttachmentType + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@id([datasetId, fileId]) + @@index(fields: [fileId]) // because of @relation + @@index(fields: [datasetId, createdAt(sort: Desc)]) // used for listing + @@index(fields: [datasetId, type]) +} + +// --- + +enum WidgetIntegrationFileAttachmentType { + bar + user + bot + button +} + +model WidgetIntegrationFileAttachment { + // relationships + widgetIntegrationId String + widgetIntegration WidgetIntegration @relation(fields: [widgetIntegrationId], references: [id], onDelete: Cascade) + fileId String + file File @relation(fields: [fileId], references: [id], onDelete: Cascade) + + // fields + type WidgetIntegrationFileAttachmentType + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@id([widgetIntegrationId, type]) + @@index(fields: [fileId]) // because of @relation +} diff --git a/packages/db-spec/prisma/sql/breakdownTotalContactsWithConversationsOverPeriod.sql b/packages/db-spec/prisma/sql/breakdownTotalContactsWithConversationsOverPeriod.sql new file mode 100644 index 0000000..cba8623 --- /dev/null +++ b/packages/db-spec/prisma/sql/breakdownTotalContactsWithConversationsOverPeriod.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + DATE(c.createdAt) AS date, + COUNT(DISTINCT c.contactId) AS total +FROM Conversation c +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY DATE(c.createdAt) +ORDER BY date ASC diff --git a/packages/db-spec/prisma/sql/breakdownTotalConversationsOverPeriod.sql b/packages/db-spec/prisma/sql/breakdownTotalConversationsOverPeriod.sql new file mode 100644 index 0000000..16f2b42 --- /dev/null +++ b/packages/db-spec/prisma/sql/breakdownTotalConversationsOverPeriod.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + DATE(c.createdAt) AS date, + COUNT(c.id) AS total +FROM Conversation c +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY DATE(c.createdAt) +ORDER BY date ASC diff --git a/packages/db-spec/prisma/sql/breakdownTotalMessagesOfTypeOverPeriod.sql b/packages/db-spec/prisma/sql/breakdownTotalMessagesOfTypeOverPeriod.sql new file mode 100644 index 0000000..45a966a --- /dev/null +++ b/packages/db-spec/prisma/sql/breakdownTotalMessagesOfTypeOverPeriod.sql @@ -0,0 +1,16 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:type The message type ('user', 'bot', 'activity') +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT + DATE(m.createdAt) AS date, + COUNT(m.id) AS total +FROM Message m +JOIN Conversation c ON m.conversationId = c.id +WHERE c.userId = ? + AND m.type = ? + AND c.contactId IS NOT NULL + AND m.createdAt >= ? + AND m.createdAt <= ? +GROUP BY DATE(m.createdAt) +ORDER BY date ASC diff --git a/packages/db-spec/prisma/sql/breakdownTotalMessagesOverPeriod.sql b/packages/db-spec/prisma/sql/breakdownTotalMessagesOverPeriod.sql new file mode 100644 index 0000000..c67aae1 --- /dev/null +++ b/packages/db-spec/prisma/sql/breakdownTotalMessagesOverPeriod.sql @@ -0,0 +1,14 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + DATE(m.createdAt) AS date, + COUNT(m.id) AS total +FROM Message m +JOIN Conversation c ON m.conversationId = c.id +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND m.createdAt >= ? + AND m.createdAt <= ? +GROUP BY DATE(m.createdAt) +ORDER BY date ASC diff --git a/packages/db-spec/prisma/sql/breakdownTotalRatingsOverPeriod.sql b/packages/db-spec/prisma/sql/breakdownTotalRatingsOverPeriod.sql new file mode 100644 index 0000000..b923cd8 --- /dev/null +++ b/packages/db-spec/prisma/sql/breakdownTotalRatingsOverPeriod.sql @@ -0,0 +1,14 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +SELECT + DATE(createdAt) as date, + COUNT(*) as total, + COUNT(CASE WHEN value > 0 THEN 1 END) as thumbsUp, + COUNT(CASE WHEN value < 0 THEN 1 END) as thumbsDown +FROM Rating +WHERE userId = ? + AND createdAt >= ? + AND createdAt <= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db-spec/prisma/sql/breakdownTotalUsageTokensOverPeriod.sql b/packages/db-spec/prisma/sql/breakdownTotalUsageTokensOverPeriod.sql new file mode 100644 index 0000000..c312f22 --- /dev/null +++ b/packages/db-spec/prisma/sql/breakdownTotalUsageTokensOverPeriod.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + DATE(u.createdAt) AS date, + COALESCE(SUM(u.count), 0) AS total +FROM Usage u +WHERE u.userId = ? + AND u.type LIKE '%_TOKEN' + AND u.createdAt >= ? + AND u.createdAt <= ? +GROUP BY DATE(u.createdAt) +ORDER BY date ASC; diff --git a/packages/db-spec/prisma/sql/getAverageMessagesOfTypeOverPeriod.sql b/packages/db-spec/prisma/sql/getAverageMessagesOfTypeOverPeriod.sql new file mode 100644 index 0000000..18c984f --- /dev/null +++ b/packages/db-spec/prisma/sql/getAverageMessagesOfTypeOverPeriod.sql @@ -0,0 +1,15 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:messageType The type of the message (e.g., 'bot') +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT COALESCE(AVG(msg_count), 0) AS average +FROM ( + SELECT m.conversationId, COUNT(*) AS msg_count + FROM Message m + JOIN Conversation c ON c.id = m.conversationId + WHERE c.userId = ? + AND m.type = ? + AND m.createdAt >= ? + AND m.createdAt <= ? + GROUP BY m.conversationId +) AS messages_per_conversation_in_period; diff --git a/packages/db-spec/prisma/sql/getBotConversationCountByDay.sql b/packages/db-spec/prisma/sql/getBotConversationCountByDay.sql new file mode 100644 index 0000000..0d876f3 --- /dev/null +++ b/packages/db-spec/prisma/sql/getBotConversationCountByDay.sql @@ -0,0 +1,12 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:botId The ID of the bot +-- @param {DateTime} $3:fromDate Inclusive start of the period +-- @param {DateTime} $4:toDate Inclusive end of the period +SELECT DATE(createdAt) AS date, COUNT(*) AS total +FROM Conversation +WHERE userId = ? + AND botId = ? + AND createdAt >= ? + AND createdAt <= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db-spec/prisma/sql/getBotConversationsWithUserMessageCount.sql b/packages/db-spec/prisma/sql/getBotConversationsWithUserMessageCount.sql new file mode 100644 index 0000000..bc9feb2 --- /dev/null +++ b/packages/db-spec/prisma/sql/getBotConversationsWithUserMessageCount.sql @@ -0,0 +1,16 @@ +-- @param {String} $1:userMessageType The message type that marks a user turn +-- @param {String} $2:userId The ID of the user +-- @param {String} $3:botId The ID of the bot +-- @param {DateTime} $4:fromDate Inclusive start of the period +-- @param {DateTime} $5:toDate Inclusive end of the period +SELECT COUNT(*) AS total +FROM ( + SELECT c.id + FROM Conversation c + JOIN Message m ON m.conversationId = c.id AND m.type = ? + WHERE c.userId = ? + AND c.botId = ? + AND c.createdAt >= ? + AND c.createdAt <= ? + GROUP BY c.id +) AS conversationsWithUserMessages; diff --git a/packages/db-spec/prisma/sql/getBotMessageCountByDay.sql b/packages/db-spec/prisma/sql/getBotMessageCountByDay.sql new file mode 100644 index 0000000..4243a0e --- /dev/null +++ b/packages/db-spec/prisma/sql/getBotMessageCountByDay.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:botId The ID of the bot +-- @param {DateTime} $3:fromDate Inclusive start of the period +-- @param {DateTime} $4:toDate Inclusive end of the period +SELECT DATE(m.createdAt) AS date, COUNT(*) AS total +FROM Message m +JOIN Conversation c ON c.id = m.conversationId +WHERE c.userId = ? + AND c.botId = ? + AND m.createdAt >= ? + AND m.createdAt <= ? +GROUP BY DATE(m.createdAt) +ORDER BY date ASC; diff --git a/packages/db-spec/prisma/sql/getBotSingleTurnConversationCount.sql b/packages/db-spec/prisma/sql/getBotSingleTurnConversationCount.sql new file mode 100644 index 0000000..74f9adf --- /dev/null +++ b/packages/db-spec/prisma/sql/getBotSingleTurnConversationCount.sql @@ -0,0 +1,17 @@ +-- @param {String} $1:userMessageType The message type that marks a user turn +-- @param {String} $2:userId The ID of the user +-- @param {String} $3:botId The ID of the bot +-- @param {DateTime} $4:fromDate Inclusive start of the period +-- @param {DateTime} $5:toDate Inclusive end of the period +SELECT COUNT(*) AS total +FROM ( + SELECT c.id + FROM Conversation c + LEFT JOIN Message m ON m.conversationId = c.id AND m.type = ? + WHERE c.userId = ? + AND c.botId = ? + AND c.createdAt >= ? + AND c.createdAt <= ? + GROUP BY c.id + HAVING COUNT(m.id) = 1 +) AS singleTurnConvs; diff --git a/packages/db-spec/prisma/sql/getBotUsageStats.sql b/packages/db-spec/prisma/sql/getBotUsageStats.sql new file mode 100644 index 0000000..226b040 --- /dev/null +++ b/packages/db-spec/prisma/sql/getBotUsageStats.sql @@ -0,0 +1,22 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:botId The ID of the bot +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_BASE_TOKEN' THEN count + ELSE 0 + END), 0) AS totalTokens, + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_CONVERSATION' THEN count + ELSE 0 + END), 0) AS totalConversations, + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_MESSAGE' THEN count + ELSE 0 + END), 0) AS totalMessages +FROM Usage +WHERE userId = ? + AND botId = ? + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db-spec/prisma/sql/getConversationUsageStats.sql b/packages/db-spec/prisma/sql/getConversationUsageStats.sql new file mode 100644 index 0000000..0afa09b --- /dev/null +++ b/packages/db-spec/prisma/sql/getConversationUsageStats.sql @@ -0,0 +1,18 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:conversationId The ID of the conversation +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_BASE_TOKEN' THEN count + ELSE 0 + END), 0) AS totalTokens, + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_MESSAGE' THEN count + ELSE 0 + END), 0) AS totalMessages +FROM Usage +WHERE userId = ? + AND conversationId = ? + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db-spec/prisma/sql/getDailyNegativeRatingCount.sql b/packages/db-spec/prisma/sql/getDailyNegativeRatingCount.sql new file mode 100644 index 0000000..a374304 --- /dev/null +++ b/packages/db-spec/prisma/sql/getDailyNegativeRatingCount.sql @@ -0,0 +1,11 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Inclusive start of the period +-- @param {DateTime} $3:toDate Inclusive end of the period +SELECT DATE(createdAt) AS date, COUNT(*) AS total +FROM Rating +WHERE userId = ? + AND value < 0 + AND createdAt >= ? + AND createdAt <= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db-spec/prisma/sql/getEventMetricSeriesOverPeriod.sql b/packages/db-spec/prisma/sql/getEventMetricSeriesOverPeriod.sql new file mode 100644 index 0000000..a920e11 --- /dev/null +++ b/packages/db-spec/prisma/sql/getEventMetricSeriesOverPeriod.sql @@ -0,0 +1,12 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:type The event metric type +-- @param {DateTime} $3:fromDate Inclusive start of the period (DateTime) +SELECT + DATE(createdAt) AS date, + COALESCE(SUM(value), 0) AS total +FROM EventMetric +WHERE userId = ? + AND type = ? + AND createdAt >= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db-spec/prisma/sql/getTotalAbilitiesForUser.sql b/packages/db-spec/prisma/sql/getTotalAbilitiesForUser.sql new file mode 100644 index 0000000..a478fd6 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalAbilitiesForUser.sql @@ -0,0 +1,7 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Ability a +JOIN Skillset s ON a.skillsetId = s.id +JOIN User u ON s.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalBotsForUser.sql b/packages/db-spec/prisma/sql/getTotalBotsForUser.sql new file mode 100644 index 0000000..2cf95ec --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalBotsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Bot b +JOIN User u ON b.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalContacts.sql b/packages/db-spec/prisma/sql/getTotalContacts.sql new file mode 100644 index 0000000..3f89622 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalContacts.sql @@ -0,0 +1,4 @@ +-- @param {String} $1:userId The ID of the user +SELECT count(*) as total +FROM Contact +WHERE userId = ?; diff --git a/packages/db-spec/prisma/sql/getTotalContactsWithConversationsOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalContactsWithConversationsOverPeriod.sql new file mode 100644 index 0000000..ca20908 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalContactsWithConversationsOverPeriod.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT COUNT(*) AS total +FROM ( + SELECT c.contactId + FROM Conversation c + WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? + GROUP BY c.contactId +) AS distinct_contacts_with_conversations_in_period; diff --git a/packages/db-spec/prisma/sql/getTotalConversationsOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalConversationsOverPeriod.sql new file mode 100644 index 0000000..91e1662 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalConversationsOverPeriod.sql @@ -0,0 +1,8 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT COUNT(*) AS total +FROM Conversation c +WHERE c.userId = ? + AND c.createdAt >= ? + AND c.createdAt <= ? diff --git a/packages/db-spec/prisma/sql/getTotalConversationsWithNumberedMessagesOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalConversationsWithNumberedMessagesOverPeriod.sql new file mode 100644 index 0000000..a1c74ff --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalConversationsWithNumberedMessagesOverPeriod.sql @@ -0,0 +1,15 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:minMessages Minimum number of messages per conversation in the period +SELECT COUNT(*) AS total +FROM ( + SELECT m.conversationId + FROM Message m + JOIN Conversation c ON c.id = m.conversationId + WHERE c.userId = ? + AND m.createdAt >= ? + AND m.createdAt <= ? + GROUP BY m.conversationId + HAVING COUNT(*) >= ? +) AS conversations_with_min_messages_in_period; diff --git a/packages/db-spec/prisma/sql/getTotalDatasetsForUser.sql b/packages/db-spec/prisma/sql/getTotalDatasetsForUser.sql new file mode 100644 index 0000000..4b37715 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalDatasetsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Dataset d +JOIN User u ON d.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalFilesForUser.sql b/packages/db-spec/prisma/sql/getTotalFilesForUser.sql new file mode 100644 index 0000000..7f90e5c --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalFilesForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM File f +JOIN User u ON f.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalMessagesOfTypeOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalMessagesOfTypeOverPeriod.sql new file mode 100644 index 0000000..62da574 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalMessagesOfTypeOverPeriod.sql @@ -0,0 +1,11 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:messageType The type of the message +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT COUNT(*) AS total +FROM Message m +JOIN Conversation c ON c.id = m.conversationId +WHERE c.userId = ? + AND m.type = ? + AND m.createdAt >= ? + AND m.createdAt <= ?; diff --git a/packages/db-spec/prisma/sql/getTotalMessagesOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalMessagesOverPeriod.sql new file mode 100644 index 0000000..b67c687 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalMessagesOverPeriod.sql @@ -0,0 +1,9 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT COUNT(*) AS total +FROM Message m +JOIN Conversation c ON c.id = m.conversationId +WHERE c.userId = ? + AND m.createdAt >= ? + AND m.createdAt <= ?; diff --git a/packages/db-spec/prisma/sql/getTotalPoliciesForUser.sql b/packages/db-spec/prisma/sql/getTotalPoliciesForUser.sql new file mode 100644 index 0000000..4bcf107 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalPoliciesForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Policy p +JOIN User u ON p.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalPortalsForUser.sql b/packages/db-spec/prisma/sql/getTotalPortalsForUser.sql new file mode 100644 index 0000000..63b09e3 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalPortalsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Portal p +JOIN User u ON p.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalRatingsOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalRatingsOverPeriod.sql new file mode 100644 index 0000000..159ba8d --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalRatingsOverPeriod.sql @@ -0,0 +1,8 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +SELECT count(*) as total +FROM Rating +WHERE userId = ? + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db-spec/prisma/sql/getTotalSkillsetsForUser.sql b/packages/db-spec/prisma/sql/getTotalSkillsetsForUser.sql new file mode 100644 index 0000000..cf28f88 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalSkillsetsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Skillset s +JOIN User u ON s.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalTeamMembersForUser.sql b/packages/db-spec/prisma/sql/getTotalTeamMembersForUser.sql new file mode 100644 index 0000000..8295fc1 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalTeamMembersForUser.sql @@ -0,0 +1,7 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM TeamMembership tm +JOIN Team t ON tm.teamId = t.id +JOIN User u ON t.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalTeamsForUser.sql b/packages/db-spec/prisma/sql/getTotalTeamsForUser.sql new file mode 100644 index 0000000..564fd21 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalTeamsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Team t +JOIN User u ON t.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db-spec/prisma/sql/getTotalThumbsDownOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalThumbsDownOverPeriod.sql new file mode 100644 index 0000000..3bae679 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalThumbsDownOverPeriod.sql @@ -0,0 +1,9 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +SELECT count(*) as total +FROM Rating +WHERE userId = ? + AND value <= 0 + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db-spec/prisma/sql/getTotalThumbsUpOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalThumbsUpOverPeriod.sql new file mode 100644 index 0000000..75769a7 --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalThumbsUpOverPeriod.sql @@ -0,0 +1,9 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +SELECT count(*) as total +FROM Rating +WHERE userId = ? + AND value > 0 + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db-spec/prisma/sql/getTotalUsageTokensOverPeriod.sql b/packages/db-spec/prisma/sql/getTotalUsageTokensOverPeriod.sql new file mode 100644 index 0000000..4e6b89c --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalUsageTokensOverPeriod.sql @@ -0,0 +1,10 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + COALESCE(SUM(u.count), 0) AS total +FROM Usage u +WHERE u.userId = ? + AND u.type LIKE '%_TOKEN' + AND u.createdAt >= ? + AND u.createdAt <= ?; diff --git a/packages/db-spec/prisma/sql/getTotalUsersForUser.sql b/packages/db-spec/prisma/sql/getTotalUsersForUser.sql new file mode 100644 index 0000000..5b87daf --- /dev/null +++ b/packages/db-spec/prisma/sql/getTotalUsersForUser.sql @@ -0,0 +1,4 @@ +-- @param {String} $1:userId The ID of the user +SELECT COUNT(*) as count +FROM User u +WHERE u.parentId = ?; diff --git a/packages/db-spec/prisma/sql/getUsageCountByTypeOverPeriod.sql b/packages/db-spec/prisma/sql/getUsageCountByTypeOverPeriod.sql new file mode 100644 index 0000000..6472805 --- /dev/null +++ b/packages/db-spec/prisma/sql/getUsageCountByTypeOverPeriod.sql @@ -0,0 +1,14 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:type The usage type +-- @param {DateTime} $3:fromDate Inclusive start of the period +-- @param {DateTime} $4:toDate Inclusive end of the period +SELECT + DATE(createdAt) AS date, + COALESCE(SUM(count), 0) AS total +FROM Usage +WHERE userId = ? + AND type = ? + AND createdAt >= ? + AND createdAt <= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db-spec/prisma/sql/getUsageCountByTypeSince.sql b/packages/db-spec/prisma/sql/getUsageCountByTypeSince.sql new file mode 100644 index 0000000..1bdf8b3 --- /dev/null +++ b/packages/db-spec/prisma/sql/getUsageCountByTypeSince.sql @@ -0,0 +1,12 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:type The usage type +-- @param {DateTime} $3:since Inclusive start of the period +SELECT + DATE(createdAt) AS date, + COALESCE(SUM(count), 0) AS total +FROM Usage +WHERE userId = ? + AND type = ? + AND createdAt >= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db-spec/prisma/sql/listContacts.sql b/packages/db-spec/prisma/sql/listContacts.sql new file mode 100644 index 0000000..2836206 --- /dev/null +++ b/packages/db-spec/prisma/sql/listContacts.sql @@ -0,0 +1,7 @@ +-- @param {String} $1:userId The ID of the user +-- @param {Int} $2:limit The maximum number of contacts to return (optional) +SELECT id, name, description, email, nick, meta, createdAt +FROM Contact +WHERE userId = ? +ORDER BY createdAt DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listContactsWithConversationsOverPeriod.sql b/packages/db-spec/prisma/sql/listContactsWithConversationsOverPeriod.sql new file mode 100644 index 0000000..6315e7b --- /dev/null +++ b/packages/db-spec/prisma/sql/listContactsWithConversationsOverPeriod.sql @@ -0,0 +1,23 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of contacts to return (optional) +SELECT + co.id, + co.name, + co.description, + co.email, + co.nick, + co.meta, + co.createdAt, + COUNT(c.id) AS _countValue, + 'conversation' AS _countType +FROM Conversation c +JOIN Contact co ON c.contactId = co.id +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY co.id, co.name, co.description, co.email, co.nick +ORDER BY _countValue DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listContactsWithMessagesOverPeriod.sql b/packages/db-spec/prisma/sql/listContactsWithMessagesOverPeriod.sql new file mode 100644 index 0000000..fd3e718 --- /dev/null +++ b/packages/db-spec/prisma/sql/listContactsWithMessagesOverPeriod.sql @@ -0,0 +1,24 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of contacts to return (optional) +SELECT + co.id, + co.name, + co.description, + co.email, + co.nick, + co.meta, + co.createdAt, + COUNT(m.id) AS _countValue, + 'message' AS _countType +FROM Message m +JOIN Conversation c ON m.conversationId = c.id +JOIN Contact co ON c.contactId = co.id +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND m.createdAt >= ? + AND m.createdAt <= ? +GROUP BY co.id, co.name, co.description, co.email, co.nick +ORDER BY _countValue DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listContactsWithRatingsOverPeriod.sql b/packages/db-spec/prisma/sql/listContactsWithRatingsOverPeriod.sql new file mode 100644 index 0000000..1347c6a --- /dev/null +++ b/packages/db-spec/prisma/sql/listContactsWithRatingsOverPeriod.sql @@ -0,0 +1,26 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +-- @param {Int} $4:limit The maximum number of contacts to return +SELECT + c.id, + c.name, + c.description, + c.email, + c.nick, + c.meta, + c.createdAt, + COUNT(CASE WHEN r.value > 0 THEN 1 END) as _upvoteCount, + COUNT(CASE WHEN r.value < 0 THEN 1 END) as _downvoteCount, + COUNT(r.id) as _countValue, + 'rating' as _countType +FROM Contact c +LEFT JOIN Rating r ON c.id = r.contactId + AND r.userId = ? + AND r.createdAt >= ? + AND r.createdAt <= ? +WHERE c.userId = r.userId +GROUP BY c.id, c.name, c.description, c.email, c.nick, c.meta, c.createdAt +HAVING _countValue > 0 +ORDER BY _countValue DESC, _upvoteCount DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listConversationsOverPeriod.sql b/packages/db-spec/prisma/sql/listConversationsOverPeriod.sql new file mode 100644 index 0000000..c85e953 --- /dev/null +++ b/packages/db-spec/prisma/sql/listConversationsOverPeriod.sql @@ -0,0 +1,25 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of conversations to return (optional) +SELECT + c.id, + co.id AS contactId, + co.name, + co.email, + co.nick, + co.description, + co.meta, + c.createdAt, + COUNT(m.id) AS _countValue, + 'message' AS _countType +FROM Conversation c +JOIN Contact co ON c.contactId = co.id +LEFT JOIN Message m ON c.id = m.conversationId +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY c.id, c.createdAt, co.id, co.name, co.email, co.nick, co.description +ORDER BY _countValue DESC, c.createdAt DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listConversationsWithNumberedMessagesOverPeriod.sql b/packages/db-spec/prisma/sql/listConversationsWithNumberedMessagesOverPeriod.sql new file mode 100644 index 0000000..8949a8e --- /dev/null +++ b/packages/db-spec/prisma/sql/listConversationsWithNumberedMessagesOverPeriod.sql @@ -0,0 +1,24 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:messageCount Minimum number of messages for follow-ups +-- @param {Int} $5:limit The maximum number of conversations to return (optional) +SELECT + c.id, + c.createdAt, + co.name, + co.email, + co.nick, + co.description, + COUNT(m.id) AS messageCount +FROM Conversation c +JOIN Contact co ON c.contactId = co.id +LEFT JOIN Message m ON c.id = m.conversationId +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY c.id, c.createdAt, co.name, co.email, co.nick, co.description +HAVING COUNT(m.id) >= ? +ORDER BY messageCount DESC, c.createdAt DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listEventLogsOfTypeActionsGroupedByTypeOverPeriod.sql b/packages/db-spec/prisma/sql/listEventLogsOfTypeActionsGroupedByTypeOverPeriod.sql new file mode 100644 index 0000000..b46093b --- /dev/null +++ b/packages/db-spec/prisma/sql/listEventLogsOfTypeActionsGroupedByTypeOverPeriod.sql @@ -0,0 +1,18 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of action types to return (optional) +SELECT + el.type, + el.name, + el.description, + COUNT(el.id) AS _countValue, + 'action' AS _countType +FROM EventLog el +WHERE el.userId = ? + AND el.type LIKE 'action.%' + AND el.createdAt >= ? + AND el.createdAt <= ? +GROUP BY el.type, el.name, el.description +ORDER BY _countValue DESC, el.type ASC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listTopBotsByTokenUsageOverPeriod.sql b/packages/db-spec/prisma/sql/listTopBotsByTokenUsageOverPeriod.sql new file mode 100644 index 0000000..3e2cfe8 --- /dev/null +++ b/packages/db-spec/prisma/sql/listTopBotsByTokenUsageOverPeriod.sql @@ -0,0 +1,19 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of results to return +SELECT + u.botId AS id, + b.name AS name, + b.description AS description, + COALESCE(SUM(u.count), 0) AS total +FROM Usage u +LEFT JOIN Bot b ON u.botId = b.id +WHERE u.userId = ? + AND u.botId IS NOT NULL + AND u.type LIKE '%_TOKEN' + AND u.createdAt >= ? + AND u.createdAt <= ? +GROUP BY u.botId, b.name, b.description +ORDER BY total DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listTopContactsByTokenUsageOverPeriod.sql b/packages/db-spec/prisma/sql/listTopContactsByTokenUsageOverPeriod.sql new file mode 100644 index 0000000..4852c9c --- /dev/null +++ b/packages/db-spec/prisma/sql/listTopContactsByTokenUsageOverPeriod.sql @@ -0,0 +1,19 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of results to return +SELECT + u.contactId AS id, + c.name AS name, + c.description AS description, + COALESCE(SUM(u.count), 0) AS total +FROM Usage u +LEFT JOIN Contact c ON u.contactId = c.id +WHERE u.userId = ? + AND u.contactId IS NOT NULL + AND u.type LIKE '%_TOKEN' + AND u.createdAt >= ? + AND u.createdAt <= ? +GROUP BY u.contactId, c.name, c.description +ORDER BY total DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listTopDownvotersOverPeriod.sql b/packages/db-spec/prisma/sql/listTopDownvotersOverPeriod.sql new file mode 100644 index 0000000..f37a07c --- /dev/null +++ b/packages/db-spec/prisma/sql/listTopDownvotersOverPeriod.sql @@ -0,0 +1,24 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +-- @param {Int} $4:limit The maximum number of contacts to return +SELECT + c.id, + c.name, + c.description, + c.email, + c.nick, + c.meta, + c.createdAt, + COUNT(CASE WHEN r.value < 0 THEN 1 END) as _countValue, + 'downvote' as _countType +FROM Contact c +LEFT JOIN Rating r ON c.id = r.contactId + AND r.userId = ? + AND r.createdAt >= ? + AND r.createdAt <= ? +WHERE c.userId = r.userId +GROUP BY c.id, c.name, c.description, c.email, c.nick, c.meta, c.createdAt +HAVING _countValue > 0 +ORDER BY _countValue DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/sql/listTopUpvotersOverPeriod.sql b/packages/db-spec/prisma/sql/listTopUpvotersOverPeriod.sql new file mode 100644 index 0000000..4c8ac91 --- /dev/null +++ b/packages/db-spec/prisma/sql/listTopUpvotersOverPeriod.sql @@ -0,0 +1,24 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +-- @param {Int} $4:limit The maximum number of contacts to return +SELECT + c.id, + c.name, + c.description, + c.email, + c.nick, + c.meta, + c.createdAt, + COUNT(CASE WHEN r.value > 0 THEN 1 END) as _countValue, + 'upvote' as _countType +FROM Contact c +LEFT JOIN Rating r ON c.id = r.contactId + AND r.userId = ? + AND r.createdAt >= ? + AND r.createdAt <= ? +WHERE c.userId = r.userId +GROUP BY c.id, c.name, c.description, c.email, c.nick, c.meta, c.createdAt +HAVING _countValue > 0 +ORDER BY _countValue DESC +LIMIT ?; diff --git a/packages/db-spec/prisma/zod-generator.config.json b/packages/db-spec/prisma/zod-generator.config.json new file mode 100644 index 0000000..eec0afa --- /dev/null +++ b/packages/db-spec/prisma/zod-generator.config.json @@ -0,0 +1,24 @@ +{ + "mode": "custom", + "pureModels": true, + "pureModelsLean": true, + "pureModelsIncludeRelations": false, + "dateTimeStrategy": "date", + "emit": { + "enums": true, + "objects": false, + "crud": false, + "results": false, + "pureModels": true, + "variants": false + }, + "naming": { + "pureModel": { + "filePattern": "{model}.ts", + "schemaSuffix": "Model", + "typeSuffix": "", + "exportNamePattern": "{Model}Model", + "legacyAliases": false + } + } +} diff --git a/packages/db-spec/scripts/post-generate-zod.js b/packages/db-spec/scripts/post-generate-zod.js new file mode 100644 index 0000000..307541f --- /dev/null +++ b/packages/db-spec/scripts/post-generate-zod.js @@ -0,0 +1,145 @@ +/* eslint-disable no-console */ + +/** + * @file post-generate-zod.js + * + * Post-generation script for prisma-zod-generator compatibility. + * + * Creates re-export files at the old zod-prisma output paths so that + * existing consumer imports (e.g. `from 'prisma/zod/schemas/bot'`) + * continue to work without changes. + * + * Also patches the 4 models that use custom Zod types for their + * `config` JSON field (Secret, Portal, Policy, Token). + * + * Run after `prisma generate`: + * node prisma/post-generate-zod.js + * + * @todo Simplify this script by migrating all consumer imports to use the new + * prisma-zod-generator paths directly (e.g. `from 'prisma/zod/schemas/models/Bot'`). + * Once imports are updated, remove the lowercase re-export logic. Consider using + * a codemod or find-replace to update ~100+ import statements across the codebase. + * The custom config type overrides (Secret, Portal, Policy, Token) will still need + * special handling - explore if prisma-zod-generator supports custom type injection + * via schema annotations or generator options. + */ +import { readFileSync, readdirSync, writeFileSync } from 'fs' +import { basename, join } from 'path' + +// @note the prisma directory to post-process comes from the caller - this +// script is shared by every db module, each of which generates its own zod +// output next to its own schema +const PRISMA_DIR = process.argv[2] + +if (!PRISMA_DIR) { + console.error('usage: node post-generate-zod.js ') + + process.exit(1) +} + +const SCHEMAS_DIR = new URL( + 'zod/schemas', + `file://${PRISMA_DIR.endsWith('/') ? PRISMA_DIR : PRISMA_DIR + '/'}` +).pathname +const MODELS_DIR = join(SCHEMAS_DIR, 'models') + +/** + * Models that need custom type overrides for their `config` field. + * Maps model file name (in models/) to the custom type import. + */ +const CUSTOM_CONFIG_MODELS = { + 'secret.ts': { type: 'SecretConfig', nullable: true }, + 'portal.ts': { type: 'PortalConfig', nullable: true }, + 'policy.ts': { type: 'PolicyConfig', nullable: true }, + 'token.ts': { type: 'TokenConfig', nullable: true }, +} + +// Read all generated model files +const modelFiles = readdirSync(MODELS_DIR).filter( + (f) => f.endsWith('.ts') && f !== 'index.ts' +) + +// @note Patch models whose exported type name shadows a TypeScript built-in. +// The zod generator creates `export type Record = z.infer<...>` which shadows +// the global `Record` utility type, breaking any `Record` +// usage inside the same file's refinement functions. + +const TS_BUILTIN_SHADOWS = ['record.ts'] + +for (const shadowFile of TS_BUILTIN_SHADOWS) { + const filePath = join(MODELS_DIR, shadowFile) + + try { + let content = readFileSync(filePath, 'utf-8') + + // Replace `Record<` with `globalThis.Record<` inside refinement code + // but NOT in the `export type Record = ...` line + content = content.replace(/(\bas\s+)Record { + // @note comments are rewritten like everything else - a commented-out + // field must stay valid for this engine when uncommented, and the + // patterns are token-anchored so prose never matches them + return line + .replace(/\s*@db\.\w+(\([^)]*\))?/g, '') + .replace( + /@default\(dbgenerated\("\(_utf8mb4\\\\'(.*?)\\\\'\)"\)\)/g, + '@default("$1")' + ) + .replace( + 'onDelete: NoAction, onUpdate: NoAction', + 'onDelete: Restrict, onUpdate: Restrict' + ) + .replace('onDelete: NoAction', 'onDelete: Restrict') + .replace(/provider\s*=\s*"mysql"/, 'provider = "sqlite"') + }) + .join('\n') +} + +/** + * Derives an implementation's prisma directory from the blueprint. + * + * Writes the rendered schema and copies the shared SQL and the zod generator + * config alongside it, which is everything TypedSQL and the generators resolve + * relative to the schema. + * + * @param {object} options + * @param {string} options.prismaDir - the implementation's prisma directory + * @param {(source: string) => string} options.render - the engine's renderer + * @returns {Promise} + */ +export async function derive({ prismaDir, render }) { + const source = await fs.readFile( + path.join(SPEC_PRISMA, 'schema.prisma'), + 'utf8' + ) + + // @note the source file opens with a note describing itself as the one + // hand-edited schema - true there, false in anything derived from it. Strip + // the leading comment block; the derived header names the source instead. + const lines = source.split('\n') + + let body = 0 + + while ( + body < lines.length && + (lines[body].trim() === '' || lines[body].trimStart().startsWith('//')) + ) { + body += 1 + } + + const stripped = lines.slice(body).join('\n') + + await fs.mkdir(path.join(prismaDir, 'sql'), { recursive: true }) + + await fs.writeFile( + path.join(prismaDir, 'schema.prisma'), + HEADER + render(stripped) + ) + + const sqlDir = path.join(SPEC_PRISMA, 'sql') + + for (const file of await fs.readdir(sqlDir)) { + await fs.copyFile( + path.join(sqlDir, file), + path.join(prismaDir, 'sql', file) + ) + } + + await fs.copyFile( + path.join(SPEC_PRISMA, 'zod-generator.config.json'), + path.join(prismaDir, 'zod-generator.config.json') + ) + + console.log(`derived ${path.relative(process.cwd(), prismaDir)}`) +} diff --git a/packages/db-spec/src/types.test.js b/packages/db-spec/src/types.test.js new file mode 100644 index 0000000..fbff386 --- /dev/null +++ b/packages/db-spec/src/types.test.js @@ -0,0 +1,296 @@ +import { PortalConfig, SecretConfig } from './types' + +import { ZodError } from 'zod' + +describe('PortalConfig', () => { + describe('valid configurations', () => { + it('should accept empty object', () => { + const validConfig = {} + + const result = PortalConfig.parse(validConfig) + + expect(result).toEqual({}) + }) + + it('should accept valid apps configuration', () => { + const validConfig = { + apps: { + app1: {}, + app2: {}, + }, + } + + const result = PortalConfig.parse(validConfig) + + expect(result).toEqual(validConfig) + }) + + it('should accept valid apps with random configuration properties', () => { + const validConfig = { + apps: { + app1: { customProp: 'value', anotherProp: 123 }, + app2: { enabled: true, version: '1.0.0' }, + }, + } + + const result = PortalConfig.parse(validConfig) + + expect(result).toEqual(validConfig) + }) + + it('should accept valid users configuration', () => { + const validConfig = { + users: { + user1: {}, + user2: {}, + }, + } + + const result = PortalConfig.parse(validConfig) + + expect(result).toEqual(validConfig) + }) + + it('should accept valid auth configuration', () => { + const validConfig = { + auth: {}, + } + + const result = PortalConfig.parse(validConfig) + + expect(result).toEqual(validConfig) + }) + + it('should accept valid signin configuration', () => { + const validConfig = { + signin: { + title: 'Welcome', + headline: 'Sign in to your account', + }, + } + + const result = PortalConfig.parse(validConfig) + + expect(result).toEqual(validConfig) + }) + + it('should accept valid layout configuration', () => { + const validConfig = { + layout: { + madeWith: true, + }, + } + + const result = PortalConfig.parse(validConfig) + + expect(result).toEqual(validConfig) + }) + + it('should accept complete valid configuration', () => { + const validConfig = { + apps: { app1: {} }, + users: { user1: {} }, + auth: {}, + signin: { + title: 'Portal', + headline: 'Welcome back', + }, + layout: { + madeWith: false, + }, + } + + const result = PortalConfig.parse(validConfig) + + expect(result).toEqual(validConfig) + }) + }) + + describe('non-standard properties handling', () => { + // @note for now we allow this to happen + it.skip('should strip non-standard properties at root level (strict behavior)', () => { + const configWithExtra = { + apps: { app1: {} }, + extraProperty: 'this will be stripped', + } + + const result = PortalConfig.parse(configWithExtra) + + expect(result).toEqual({ + apps: { app1: {} }, + }) + expect(result.extraProperty).toBeUndefined() + }) + + // @note for now we allow this to happen + it.skip('should strip non-standard properties in nested objects', () => { + const configWithExtra = { + signin: { + title: 'Welcome', + headline: 'Sign in', + customProperty: 'will be stripped', + }, + layout: { + madeWith: true, + customLayoutProp: 'will be stripped', + }, + } + + const result = PortalConfig.parse(configWithExtra) + + expect(result).toEqual({ + signin: { + title: 'Welcome', + headline: 'Sign in', + }, + layout: { + madeWith: true, + }, + }) + expect(result.signin.customProperty).toBeUndefined() + expect(result.layout.customLayoutProp).toBeUndefined() + }) + + // @note for now we allow this to happen + it.skip('should strip all extra properties while preserving valid ones', () => { + const configWithMultipleExtras = { + apps: { app1: {} }, + users: { user1: {} }, + customProp1: 'stripped', + customProp2: 42, + customProp3: { nested: 'object' }, + signin: { + title: 'Valid title', + invalidProp: 'stripped', + }, + } + + const result = PortalConfig.parse(configWithMultipleExtras) + + expect(result).toEqual({ + apps: { app1: {} }, + users: { user1: {} }, + signin: { + title: 'Valid title', + }, + }) + + expect(result.customProp1).toBeUndefined() + expect(result.customProp2).toBeUndefined() + expect(result.customProp3).toBeUndefined() + expect(result.signin.invalidProp).toBeUndefined() + }) + + // @note for now we allow this to happen + it.skip('should demonstrate that PortalConfig does not accept non-standard properties', () => { + const input = { + invalidProperty: 'will be removed', + } + + const result = PortalConfig.parse(input) + + expect(Object.keys(result)).toEqual([]) + expect(result.invalidProperty).toBeUndefined() + }) + }) + + describe('type validation', () => { + it('should reject invalid types for signin properties', () => { + const invalidConfig = { + signin: { + title: 123, // should be string + headline: true, // should be string + }, + } + + expect(() => PortalConfig.parse(invalidConfig)).toThrow(ZodError) + }) + + it('should reject invalid type for layout.footer.madeWith', () => { + const invalidConfig = { + layout: { + footer: { + madeWith: 'yes', // should be boolean + }, + }, + } + + expect(() => PortalConfig.parse(invalidConfig)).toThrow(ZodError) + }) + + it('should reject invalid type for apps', () => { + const invalidConfig = { + apps: 'not an object', // should be record/object + } + + expect(() => PortalConfig.parse(invalidConfig)).toThrow(ZodError) + }) + + it('should reject invalid type for users', () => { + const invalidConfig = { + users: [], // should be record/object + } + + expect(() => PortalConfig.parse(invalidConfig)).toThrow(ZodError) + }) + }) +}) + +describe('SecretConfig', () => { + describe('valid configurations', () => { + it('should accept empty object', () => { + const validConfig = {} + + const result = SecretConfig.parse(validConfig) + + expect(result).toEqual({}) + }) + }) + + describe('non-standard properties handling', () => { + // @note for now we allow this to happen + it.skip('should strip any additional properties (strict empty object schema)', () => { + const configWithExtra = { + someProperty: 'will be stripped', + } + + const result = SecretConfig.parse(configWithExtra) + + expect(result).toEqual({}) + expect(result.someProperty).toBeUndefined() + }) + + // @note for now we allow this to happen + it.skip('should strip all additional properties regardless of type', () => { + const configWithMultipleProps = { + prop1: 'string value', + prop2: 123, + prop3: true, + prop4: { nested: 'object' }, + } + + const result = SecretConfig.parse(configWithMultipleProps) + + expect(result).toEqual({}) + expect(Object.keys(result)).toHaveLength(0) + expect(result.prop1).toBeUndefined() + expect(result.prop2).toBeUndefined() + expect(result.prop3).toBeUndefined() + expect(result.prop4).toBeUndefined() + }) + + // @note for now we allow this to happen + it.skip('should demonstrate that SecretConfig does not accept any non-standard properties', () => { + const input = { + secret: 'password', + apiKey: 'key123', + token: 'abc123', + } + + const result = SecretConfig.parse(input) + + expect(result).toEqual({}) + expect(Object.keys(result)).toHaveLength(0) + }) + }) +}) diff --git a/packages/db-spec/src/types.ts b/packages/db-spec/src/types.ts new file mode 100644 index 0000000..f343501 --- /dev/null +++ b/packages/db-spec/src/types.ts @@ -0,0 +1,271 @@ +import { z } from 'zod' + +/** + * SECRET CONFIG + */ +export const SecretConfig = z.union([ + // null + z.null(), + + // oauth + z + .object({ + clientId: z.string().optional(), + clientSecret: z.string().optional(), + authorizationUrl: z.string().url().optional(), + tokenUrl: z.string().url().optional(), + revokeUrl: z.string().url().optional(), + scope: z.string().optional(), + grantType: z.string().optional(), + }) + .passthrough(), + + // basic + z + .object({ + username: z.string().optional(), + password: z.string().optional(), + user: z.string().optional(), + pass: z.string().optional(), + }) + .passthrough(), +]) + +export type SecretConfigType = z.infer + +/** + * PORTAL CONFIG + */ +export const PortalConfig = z + .object({ + apps: z + .record( + z + .object({ + // @todo add app specific configs here + }) + .passthrough() + ) + .optional(), + + groups: z + .record( + z + .object({ + users: z.record( + z + .object({ + // @todo add user specific configs here + }) + .passthrough() + ), + + apps: z.record( + z + .object({ + // @todo add app specific configs here + }) + .passthrough() + ), + }) + .passthrough() + ) + .optional(), + + users: z + .record( + z + .object({ + // @todo add user specific configs here + }) + .passthrough() + ) + .optional(), + + auth: z + .object({ + // @todo add auth specific configs here + }) + .passthrough() + .optional(), + + signin: z + .object({ + title: z.string().optional(), + headline: z.string().optional(), + }) + .passthrough() + .optional(), + + layout: z + .object({ + header: z + .union([ + z.boolean(), + z + .object({ + // @todo add header specific configs here + }) + .passthrough(), + ]) + .optional(), + + footer: z + .union([ + z.boolean(), + z + .object({ + madeWith: z.boolean().optional(), + }) + .passthrough(), + ]) + .optional(), + + sidebar: z.union([ + z.boolean(), + z + .object({ + title: z.string().optional(), + logo: z.string().optional(), + icon: z.string().optional(), + link: z.string().url().optional(), + }) + .passthrough() + .optional(), + ]), + }) + .passthrough() + .optional(), + }) + .passthrough() + +export type PortalConfigType = z.infer + +/** + * TOKEN CONFIG + */ +export const TokenConfig = z + .object({ + allowedRoutes: z.array(z.string()).optional(), + + // @note when set, the token is bound to this contact: the session loader + // propagates it into the session payload so handlers such as + // conversation/complete attribute interactions to the contact as a hard + // override. @see @/lib/session.get and @/schemas/contactId + contactId: z.string().optional(), + }) + .passthrough() + +export type TokenConfigType = z.infer + +/** + * POLICY CONFIG + * + * Each policy type has its own config shape. The authoritative discriminator is + * the Policy row's `type` column (so policies can be selected by a real column + * rather than by querying inside the JSON), which is why `type` is intentionally + * not duplicated inside the config. The right shape is selected by `type` via + * `parsePolicyConfig` in `@/lib/policy.config`. + */ + +// retention: how long (in days) to keep a conversation before it expires. +export const RetentionPolicyConfig = z.object({ + expiresInDays: z.number().int().positive().optional(), +}) + +export type RetentionPolicyConfigType = z.infer + +const UsagePolicyEmailRecipient = z.string().email() + +const UsagePolicyEmailRecipients = z.array(UsagePolicyEmailRecipient).min(1) + +const UsagePolicyEmailAction = z.union([ + z.string().email(), + UsagePolicyEmailRecipients, + z.object({ + to: z + .union([UsagePolicyEmailRecipient, UsagePolicyEmailRecipients]) + .optional(), + }), +]) + +// usage: trip one or more actions when a bot's usage of `metric` exceeds +// `threshold` within a rolling `windowInSeconds` window. +export const UsagePolicyConfig = z.object({ + metric: z.enum(['tokens', 'messages', 'conversations']), + + threshold: z.number().int().positive(), + + windowInSeconds: z.number().int().positive(), + + actions: z + .object({ + // temporarily block the bot for `durationInSeconds`. + block: z + .object({ + durationInSeconds: z.number().int().positive(), + }) + .optional(), + + // send an email notification. A string or array sends to explicit + // recipients. An object without `to` notifies the policy owner. + email: UsagePolicyEmailAction.optional(), + }) + .refine((actions) => !!actions.block || !!actions.email, { + message: + 'a usage policy must define at least one action (block or email)', + }), +}) + +export type UsagePolicyConfigType = z.infer + +// @note a plain (non-discriminated) union: with `type` removed from the config +// this only describes the JSON column / provides a soft client-side hint. The +// authoritative validation is `parsePolicyConfig`, which selects by row `type`. +// UsagePolicyConfig must be tried first: zod unions return the first matching +// branch, and RetentionPolicyConfig (all-optional keys, strip mode) matches any +// object and would strip a usage config down to `{}`. +export const PolicyConfig = z.union([UsagePolicyConfig, RetentionPolicyConfig]) + +export type PolicyConfigType = z.infer + +/** + * USER LIMITS + */ +export const UserLimits = z + .object({ + tokens: z.number().int().min(0).optional(), + conversations: z.number().int().min(0).optional(), + messages: z.number().int().min(0).optional(), + database: z + .object({ + datasets: z.number().int().min(0).optional(), + records: z.number().int().min(0).optional(), + skillsets: z.number().int().min(0).optional(), + abilities: z.number().int().min(0).optional(), + files: z.number().int().min(0).optional(), + }) + .passthrough() + .optional(), + file: z + .object({ + maxFileSize: z.number().int().min(0).optional(), + }) + .passthrough() + .optional(), + attachment: z + .object({ + maxFileSize: z.number().int().min(0).optional(), + }) + .passthrough() + .optional(), + }) + .passthrough() + .nullable() + +export type UserLimitsType = z.infer + +/** + * CONVENIENCE EXPORT + */ +export default z diff --git a/packages/db-spec/tsconfig.json b/packages/db-spec/tsconfig.json new file mode 100644 index 0000000..b2a7c56 --- /dev/null +++ b/packages/db-spec/tsconfig.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node" + ], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js", + "./scripts/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/db/.gitignore b/packages/db/.gitignore new file mode 100644 index 0000000..731cdef --- /dev/null +++ b/packages/db/.gitignore @@ -0,0 +1,3 @@ +/prisma/generated +/prisma/zod +/prisma/.dev.db diff --git a/packages/db/README.md b/packages/db/README.md new file mode 100644 index 0000000..2c3a6cd --- /dev/null +++ b/packages/db/README.md @@ -0,0 +1,21 @@ +# @chatbotkit-dev/db + +The community database: SQLite in a file. The schema is derived from the +blueprint in `@chatbotkit-dev/db-spec`; the client is generated against a file +database this package creates itself, so generation needs nothing provisioned +and nothing reachable. + +## Environment + +| Variable | Purpose | +| -------- | ------- | +| `DATABASE_URL` | A `file:` url, e.g. `file:./data/cbk.db` | + +## Known differences from the MySQL module + +- `DATE(...)` expression columns in the analytics queries come back as + `'YYYY-MM-DD'` strings (typed loosely), where MySQL returns `Date` objects. + Plain columns are identical. Affects the date-bucketed usage/report series + only. +- Writes are as concurrent as SQLite is - fine for a single-process deployment, + which is what this module is for. diff --git a/packages/db/package.json b/packages/db/package.json new file mode 100644 index 0000000..0944e0e --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,68 @@ +{ + "name": "@chatbotkit-dev/db", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + }, + "./constraints": { + "import": "./src/constraints.ts", + "default": "./src/constraints.ts" + }, + "./client": { + "import": "./prisma/generated/prisma/client.ts", + "default": "./prisma/generated/prisma/client.ts" + }, + "./browser": { + "import": "./prisma/generated/prisma/browser.ts", + "default": "./prisma/generated/prisma/browser.ts" + }, + "./sql": { + "import": "./prisma/generated/prisma/sql.ts", + "default": "./prisma/generated/prisma/sql.ts" + }, + "./zod-models": { + "import": "./prisma/zod/schemas/models/index.ts", + "default": "./prisma/zod/schemas/models/index.ts" + }, + "./pothos": { + "import": "./prisma/generated/pothos.ts", + "default": "./prisma/generated/pothos.ts" + }, + "./package.json": "./package.json", + "./schema": "./prisma/schema.prisma" + }, + "main": "./src/index.ts", + "scripts": { + "build": "true", + "check": "if [ -d prisma/generated ]; then tsc6 --noEmit --incremental; else echo 'skipping check: prisma/generated missing (run db:gen via pnpm run with-env)'; fi", + "clean": "rimraf node_modules prisma/generated prisma/.dev.db *.tsbuildinfo", + "format": "true", + "lint": "eslint src scripts --ext .ts,.js", + "test": "true", + "db:gen": "node scripts/derive.js && node scripts/generate.js", + "derive": "node scripts/derive.js", + "db:push": "node scripts/derive.js && prisma db push" + }, + "access": "restricted", + "dependencies": { + "@prisma/adapter-better-sqlite3": "^7.3.0", + "@prisma/client": "^7.3.0", + "@pothos/plugin-prisma": "^4.10.0", + "prisma": "^7.3.0", + "prisma-json-types-generator": "^3.1.1", + "prisma-zod-generator": "^2.1.2", + "zod": "3.25.67" + }, + "devDependencies": { + "@chatbotkit-dev/db-spec": "workspace:*", + "@chatbotkit-dev/eslint-config": "workspace:*", + "eslint": "^9.0.0", + "rimraf": "^5.0.5", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/db/prisma.config.ts b/packages/db/prisma.config.ts new file mode 100644 index 0000000..d034018 --- /dev/null +++ b/packages/db/prisma.config.ts @@ -0,0 +1,19 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'prisma/config' + +const ROOT = path.dirname(fileURLToPath(import.meta.url)) + +// @note one variable, shared with the runtime. The generate step overrides it +// with a throwaway file so generation never touches a real database - TypedSQL +// needs a live database to typecheck the queries in prisma/sql against, and a +// file this package creates itself is the whole point of the default. +export default defineConfig({ + schema: 'prisma/schema.prisma', + + datasource: { + url: + process.env.PRISMA_DATABASE_URL || + `file:${path.join(ROOT, 'prisma', '.dev.db')}`, + }, +}) diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma new file mode 100644 index 0000000..7f1452a --- /dev/null +++ b/packages/db/prisma/schema.prisma @@ -0,0 +1,3500 @@ +// GENERATED - do not edit. +// +// The source is prisma/schema.prisma in @chatbotkit-dev/db-spec - that is the +// one hand-edited schema, and this file is derived from it by this package's +// `pnpm derive` (see @chatbotkit-dev/db-spec/derive for exactly what this +// engine changes). Edit the source, then run `pnpm -r derive`. + +generator client { + provider = "prisma-client" + output = "./generated/prisma" + previewFeatures = ["typedSql"] +} + +generator json { + provider = "prisma-json-types-generator" + namespace = "PrismaJson" + allowAny = false +} + +generator zod { + provider = "prisma-zod-generator" + output = "./zod/schemas" + config = "./zod-generator.config.json" +} + +generator pothos { + provider = "prisma-pothos-types" + output = "./generated/pothos.ts" + generateDatamodel = true + documentation = false + clientOutput = "./prisma" +} + +// --- +// --- +// --- + +datasource db { + provider = "sqlite" + relationMode = "prisma" +} + +// --- +// --- +// --- + +// @see https://next-auth.js.org/adapters/prisma + +model Account { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + type String + + provider String + providerAccountId String + + refresh_token String? /// @encrypted + access_token String? /// @encrypted + id_token String? /// @encrypted + token_type String? + + expires_at Int? + + scope String? + + session_state String? + + refresh_token_expires_in Int? + + meta Json? /// [Meta] + + // timestamps + // @note disabled because new + // createdAt DateTime @default(now()) + // updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([provider, providerAccountId]) + // indexes: other + @@index(fields: [userId]) +} + +model Session { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String? // @note deliberately optional + description String? // @note deliberately optional + + sessionToken String @unique + + expires DateTime + + audience String? + + payload Json? /// [JsonRecord] + + options Json? /// [JsonRecord] + + meta Json? /// [Meta] + + // timestamps + // @note disabled because new + // createdAt DateTime @default(now()) + // updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [userId]) +} + +model VerificationToken { + // fields + identifier String + + token String @unique + + expires DateTime + + meta Json? /// [Meta] + + // timestamps + // @note disabled because new + // createdAt DateTime @default(now()) + // updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([identifier, token]) +} + +// --- +// --- +// --- + +model User { + id String @id @default(cuid()) + + // relationships + parentId String? + parent User? @relation("ParentChild", fields: [parentId], references: [id], onDelete: Restrict, onUpdate: Restrict) // @note prevents deletion - must use deleteUser function + + // ref + alias String? // @note only used for child Users (parentId must be set) + + // fields + name String? // @note deliberately optional + description String? // @note deliberately optional + + email String @unique + emailVerified DateTime? + + parentContextName String? + + parentContextEmail String? + parentContextEmailVerified DateTime? + + image String? + + billingCustomerId String? @unique + billingSubscriptionId String? + billingSubscriptionStatus String? + billingSubscriptionStartedAt DateTime? + billingSubscriptionTrialedAt DateTime? + + channel String? + organization String? + industry String? + role String? + goal String? + + limits Json? /// [Limits] + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: account + accounts Account[] + sessions Session[] + + // connections: children + children User[] @relation("ParentChild") + + // connections: teams + teams Team[] + + // connections: developer + tokens Token[] + webhooks Webhook[] + + // connections: objects + contacts Contact[] + conversations Conversation[] + tasks Task[] + ratings Rating[] + memories Memory[] + spaces Space[] + + // connections: executions + taskExecutions TaskExecution[] + + // connections: resources + blueprints Blueprint[] + bots Bot[] + datasets Dataset[] + skillsets Skillset[] + abilities Ability[] + secrets Secret[] + files File[] + portals Portal[] + policies Policy[] + + // connections: publishing + spaceSites SpaceSite[] + + // connections: special + contexts Context[] + + // connections: integrations + triggerIntegrations TriggerIntegration[] + widgetIntegrations WidgetIntegration[] + slackIntegrations SlackIntegration[] + discordIntegrations DiscordIntegration[] + microsoftteamsIntegrations MicrosoftteamsIntegration[] + googlechatIntegrations GooglechatIntegration[] + whatsappIntegrations WhatsappIntegration[] + messengerIntegrations MessengerIntegration[] + instagramIntegrations InstagramIntegration[] + telegramIntegrations TelegramIntegration[] + twilioIntegrations TwilioIntegration[] + anamIntegrations AnamIntegration[] + avatarIntegrations AvatarIntegration[] + recallIntegrations RecallIntegration[] + githubIntegrations GithubIntegration[] + emailIntegrations EmailIntegration[] + sitemapIntegrations SitemapIntegration[] + notionIntegrations NotionIntegration[] + supportIntegrations SupportIntegration[] + extractIntegrations ExtractIntegration[] + mcpserverIntegrations McpserverIntegration[] + skillserverIntegrations SkillserverIntegration[] + + // connections: oauth + oAuthConnections OAuthConnection[] + oAuthApplications OAuthApplication[] + oAuthApplicationTokens OAuthApplicationToken[] + + // connections: hub + hubBotPages HubBotPage[] + hubDatasetPages HubDatasetPage[] + hubSkillsetPages HubSkillsetPage[] + hubBlueprintPages HubBlueprintPage[] + hubWidgetPages HubWidgetPage[] + + // connections: secret values + secretValues SecretValue[] + + // connections: lock + locks Lock[] + + // connections: usage + usages Usage[] @relation("UserUsage") + childUsages Usage[] @relation("ParentUserUsage") + + // connections: observability + eventLogs EventLog[] + eventMetrics EventMetric[] + auditLogs AuditLog[] + + // indexes: unique + @@unique([parentId, alias]) // for alias lookup by parent + @@unique([parentId, parentContextEmail]) + // indexes: other + @@index(fields: [parentId]) + @@index(fields: [id, parentId]) +} + +// --- +// --- +// --- + +enum ResourceState { + // @note the order is important because the default value is the first one + + enabled + disabled +} + +// --- +// --- +// --- + +model Team { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: team + memberships TeamMembership[] + + // indexes: other + @@index(fields: [userId]) +} + +model TeamMembership { + id String @id @default(cuid()) + + // relationships + teamId String + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + email String + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([teamId, email]) + // indexes: other + @@index(fields: [email]) +} + +// --- +// --- +// --- + +model OAuthConnection { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + + // fields + name String @default("") + description String @default("") + + issuer String? + clientId String? + clientSecret String? /// @encrypted + + scopes String @default("openid email profile") + + allowedDomains String? + requiredClaims Json? /// [JsonRecord] + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: integrations + mcpserverIntegrations McpserverIntegration[] + + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) +} + +model OAuthApplication { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + clientId String @unique + clientSecret String @unique /// @digest + + redirectUris Json @default("[]") /// [OAuthRedirectUris] + + scopes Json @default("[]") /// [OAuthScopes] + + grants Json @default("[]") /// [OAuthGrants] + + accessTokenLifetime Int? + refreshTokenLifetime Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: oauth + oAuthApplicationTokens OAuthApplicationToken[] + + // indexes: other + @@index(fields: [userId]) +} + +model OAuthApplicationToken { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + applicationId String + application OAuthApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + accessToken String @unique /// @digest + accessTokenExpiresAt DateTime? + + refreshToken String? @unique /// @digest + refreshTokenExpiresAt DateTime? + + scopes Json @default("[]") /// [OAuthScopes] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [userId]) + @@index(fields: [applicationId]) +} + +// --- +// --- +// --- + +enum Visibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +// --- +// --- +// --- + +model Lock { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: resources + blueprints Blueprint[] + bots Bot[] + datasets Dataset[] + skillsets Skillset[] + files File[] + secrets Secret[] + policies Policy[] + portals Portal[] + + // connections: integrations + triggerIntegrations TriggerIntegration[] + widgetIntegrations WidgetIntegration[] + slackIntegrations SlackIntegration[] + discordIntegrations DiscordIntegration[] + microsoftteamsIntegrations MicrosoftteamsIntegration[] + googlechatIntegrations GooglechatIntegration[] + whatsappIntegrations WhatsappIntegration[] + messengerIntegrations MessengerIntegration[] + instagramIntegrations InstagramIntegration[] + telegramIntegrations TelegramIntegration[] + twilioIntegrations TwilioIntegration[] + githubIntegrations GithubIntegration[] + emailIntegrations EmailIntegration[] + sitemapIntegrations SitemapIntegration[] + notionIntegrations NotionIntegration[] + supportIntegrations SupportIntegration[] + extractIntegrations ExtractIntegration[] + mcpserverIntegrations McpserverIntegration[] + skillserverIntegrations SkillserverIntegration[] +} + +// --- +// --- +// --- + +model Context { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + payload Json? /// [JsonRecord] + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) + @@index(fields: [botId]) + @@index(fields: [datasetId]) + @@index(fields: [skillsetId]) + @@index(fields: [contactId]) +} + +// --- +// --- +// --- + +enum BlueprintVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Blueprint { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + visibility BlueprintVisibility @default(private) + + config Json? /// [JsonRecord] + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: hub + hubBlueprintPage HubBlueprintPage? + + // connections: resources + bots Bot[] /// @resource + datasets Dataset[] /// @resource + skillsets Skillset[] /// @resource + abilities Ability[] /// @resource + secrets Secret[] /// @resource + files File[] /// @resource + spaces Space[] /// @resource + portals Portal[] /// @resource + policies Policy[] /// @resource + tasks Task[] /// @resource + + // connections: special + contexts Context[] + + // connections: oauth + oAuthConnections OAuthConnection[] /// @resource + + // connections: integrations + triggerIntegrations TriggerIntegration[] /// @resource + widgetIntegrations WidgetIntegration[] /// @resource + slackIntegrations SlackIntegration[] /// @resource + discordIntegrations DiscordIntegration[] /// @resource + microsoftteamsIntegrations MicrosoftteamsIntegration[] /// @resource + googlechatIntegrations GooglechatIntegration[] /// @resource + whatsappIntegrations WhatsappIntegration[] /// @resource + messengerIntegrations MessengerIntegration[] /// @resource + instagramIntegrations InstagramIntegration[] /// @resource + telegramIntegrations TelegramIntegration[] /// @resource + twilioIntegrations TwilioIntegration[] /// @resource + avatarIntegrations AvatarIntegration[] /// @resource + anamIntegrations AnamIntegration[] /// @resource + recallIntegrations RecallIntegration[] /// @resource + githubIntegrations GithubIntegration[] /// @resource + emailIntegrations EmailIntegration[] /// @resource + supportIntegrations SupportIntegration[] /// @resource + extractIntegrations ExtractIntegration[] /// @resource + sitemapIntegrations SitemapIntegration[] /// @resource + notionIntegrations NotionIntegration[] /// @resource + mcpserverIntegrations McpserverIntegration[] /// @resource + skillserverIntegrations SkillserverIntegration[] /// @resource + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) +} + +// --- +// --- +// --- + +enum BotVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Bot { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + backstory String @default("") + + model String @default("") + + privacy Boolean @default(false) + moderation Boolean @default(false) + + visibility BotVisibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: hub + hubBotPage HubBotPage? + + // connections: objects + conversations Conversation[] + tasks Task[] + ratings Rating[] + memories Memory[] + + // connections: resources + abilities Ability[] + policies Policy[] + + // connections: special + contexts Context[] + + // connections: integrations + triggerIntegrations TriggerIntegration[] + widgetIntegrations WidgetIntegration[] + slackIntegrations SlackIntegration[] + discordIntegrations DiscordIntegration[] + microsoftteamsIntegrations MicrosoftteamsIntegration[] + googlechatIntegrations GooglechatIntegration[] + whatsappIntegrations WhatsappIntegration[] + messengerIntegrations MessengerIntegration[] + instagramIntegrations InstagramIntegration[] + telegramIntegrations TelegramIntegration[] + twilioIntegrations TwilioIntegration[] + anamIntegrations AnamIntegration[] + avatarIntegrations AvatarIntegration[] + recallIntegrations RecallIntegration[] + githubIntegrations GithubIntegration[] + emailIntegrations EmailIntegration[] + supportIntegrations SupportIntegration[] + extractIntegrations ExtractIntegration[] + + // connections: connections + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) + @@index(fields: [datasetId]) + @@index(fields: [skillsetId]) + @@index(fields: [id, datasetId]) +} + +// --- +// --- +// --- + +// @idea A/B Testing / Experiments +// +// This is a proposed design for A/B testing bots. The idea is that an +// Experiment acts as a "router" that distributes traffic across multiple +// bot variants based on configurable weights. +// +// How it works: +// 1. User creates multiple bots (each bot is effectively a "version") +// 2. User creates an Experiment with ExperimentVariants pointing to those bots +// 3. Integration (Widget, Slack, etc.) references the experimentId +// 4. Session creation selects a variant based on weights and routes to that bot +// 5. Conversation records experimentId + experimentVariantId for analytics +// +// Precedence logic for integrations: +// - If experimentId is set and experiment is active → use experiment routing +// - Otherwise fall back to botId +// +// Example: +// Experiment "New Prompt Test" +// ├─ Variant "Control" (50%) → Bot A +// └─ Variant "Friendly Tone" (50%) → Bot B +// +// Analytics can then compare ratings, engagement, etc. per variant. +// +// Schema: +// +// enum ExperimentStatus { +// draft // not yet running +// active // accepting traffic +// paused // temporarily stopped +// completed // finished, winner declared +// } +// +// model Experiment { +// id String @id @default(cuid()) +// +// // owner +// userId String +// user User @relation(fields: [userId], references: [id], onDelete: Cascade) +// +// // relationships +// blueprintId String? +// blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) +// +// // fields +// name String @default("") +// description String @default("") +// +// status ExperimentStatus @default(draft) +// +// startedAt DateTime? +// completedAt DateTime? +// +// // optional: reference to winning variant after experiment concludes +// winnerVariantId String? +// +// meta Json? /// [Meta] +// +// // timestamps +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// +// // connections +// variants ExperimentVariant[] +// conversations Conversation[] +// +// // indexes +// @@index(fields: [userId, status, createdAt(sort: Desc)]) +// @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) +// } +// +// model ExperimentVariant { +// id String @id @default(cuid()) +// +// // relationships +// experimentId String +// experiment Experiment @relation(fields: [experimentId], references: [id], onDelete: Cascade) +// botId String +// bot Bot @relation(fields: [botId], references: [id], onDelete: Cascade) +// +// // fields +// name String @default("") // e.g., "Control", "Variant A", "Friendly Tone" +// weight Int @default(1) // relative weight (not percentage) - traffic is distributed proportionally +// isControl Boolean @default(false) +// +// meta Json? /// [Meta] +// +// // timestamps +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// +// // connections +// conversations Conversation[] +// +// // indexes +// @@index(fields: [experimentId]) +// @@index(fields: [botId]) +// } +// +// Additional changes needed: +// +// 1. Add to integrations (WidgetIntegration, SlackIntegration, etc.): +// experimentId String? +// experiment Experiment? @relation(fields: [experimentId], references: [id], onDelete: SetNull) +// +// 2. Add to Conversation: +// experimentId String? +// experiment Experiment? @relation(fields: [experimentId], references: [id], onDelete: SetNull) +// experimentVariantId String? +// experimentVariant ExperimentVariant? @relation(fields: [experimentVariantId], references: [id], onDelete: SetNull) +// +// 3. Add to Bot: +// experimentVariants ExperimentVariant[] + +// --- +// --- +// --- + +enum DatasetVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Dataset { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + reranker String? + + recordMaxTokens Int? + + searchMinScore Float? + searchMaxRecords Int? + searchMaxTokens Int? + + separators String? + + matchInstruction String? // do not tempt to set to empty string + mismatchInstruction String? // do not tempt to set to empty string + + visibility DatasetVisibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: hub + hubDatasetPage HubDatasetPage? + + // connections: objects + conversations Conversation[] + + // connections: resources + bots Bot[] + files DatasetFileAttachment[] + + // connections: special + contexts Context[] + + // connections: integrations + sitemapIntegrations SitemapIntegration[] + notionIntegrations NotionIntegration[] + + // connections: many-to-many + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) +} + +// --- +// --- +// --- + +enum SkillsetVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Skillset { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + visibility SkillsetVisibility @default(private) + + // lifecycle state - toggle the whole skillset on/off without deleting it + state ResourceState @default(enabled) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: conversation + conversations Conversation[] + + // connections: resources + bots Bot[] + abilities Ability[] + + // connections: special + contexts Context[] + + // connections: integrations + mcpserverIntegrations McpserverIntegration[] + skillserverIntegrations SkillserverIntegration[] + + // connections: hub + hubSkillsetPage HubSkillsetPage? + + // connections: many-to-many + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) +} + +model Ability { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + // @note the resource an ability is linked to (what it acts on), as opposed + // to the owner/container relations above + linkedSecretId String? + linkedSecret Secret? @relation(fields: [linkedSecretId], references: [id], onDelete: SetNull) + linkedFileId String? + linkedFile File? @relation(fields: [linkedFileId], references: [id], onDelete: SetNull) + linkedBotId String? + linkedBot Bot? @relation(fields: [linkedBotId], references: [id], onDelete: SetNull) + linkedSpaceId String? + linkedSpace Space? @relation(fields: [linkedSpaceId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + instruction String @default("") + + // lifecycle state - toggle the ability on/off without deleting it + state ResourceState @default(enabled) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [blueprintId]) + @@index(fields: [skillsetId]) +} + +// --- +// --- +// --- + +enum FileVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model File { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + visibility FileVisibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: resources + abilities Ability[] + datasets DatasetFileAttachment[] + widgetIntegrations WidgetIntegrationFileAttachment[] + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) + @@index(fields: [createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +enum SecretKind { + shared + personal +} + +enum SecretType { + plain + basic + bearer + jwt + oauth + template + reference +} + +enum SecretVisibility { + // @note the order is important because of the default value is the first one + + private + protected + public +} + +model Secret { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + kind SecretKind @default(shared) + + type SecretType @default(plain) + + value String? /// @encrypted + + config Json? /// [SecretConfig] @encrypted @see prisma/post-generate-zod.js + + visibility SecretVisibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: resources + abilities Ability[] + + // connections: secret values + secretValues SecretValue[] + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) +} + +model SecretValue { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + secretId String? + secret Secret? @relation(fields: [secretId], references: [id], onDelete: Cascade) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + value String /// @encrypted + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, secretId, contactId]) + // indexes: other + @@index(fields: [userId]) +} + +// --- +// --- +// --- + +model Portal { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + slug String @unique + + config Json? /// [PortalConfig] @see prisma/post-generate-zod.js + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) +} + +// --- +// --- +// --- + +enum Trigger { + never + + automatic +} + +// --- + +enum Schedule { + never + + quarterhourly + halfhourly + hourly + + twicedaily + daily + + twiceweekly + weekly + + twicemonthly + monthly +} + +// --- + +enum SyncStatus { + pending + synced + error +} + +// --- +// --- +// --- + +model TriggerIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + secret String /// @encrypted + + authenticate Boolean @default(true) + + sessionDuration Float? + + schedule String? + timezone String? + + lastTriggerAt DateTime? + nextTriggerAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [schedule, lastTriggerAt]) + @@index(fields: [nextTriggerAt]) + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model WidgetIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + theme String? + + layout String? + + title String? + + intro String? + + initial String? + + placeholder String? + + origin String? + + sessionDuration Float? + + language String? + + plugins String? + + stream Boolean @default(true) + + verbose Boolean @default(true) + + tools Boolean @default(false) + + unfurl Boolean @default(true) + + math Boolean @default(false) + + carousel Boolean @default(false) + + form Boolean @default(false) + + attachments Boolean @default(false) + + autoScroll Boolean @default(true) + + startFirst Boolean @default(false) + + contactCollection Boolean @default(false) + + exportConversation Boolean @default(true) + restartConversation Boolean @default(true) + + maximize Boolean @default(true) + + messagePeek Boolean @default(true) + + voiceIn Boolean @default(false) + voiceOut Boolean @default(false) + + poweredBy Boolean @default(true) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: resources + files WidgetIntegrationFileAttachment[] + + // connections: hub + hubWidgetPage HubWidgetPage? + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model SlackIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + signingSecret String? /// @encrypted + + botToken String? /// @encrypted + userToken String? /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + references Boolean @default(false) + + ratings Boolean @default(false) + + visibleMessages Int? + + autoRespond String? + + allowFrom String? @default("*") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model DiscordIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + appId String? + botToken String? /// @encrypted + publicKey String? + + handle String? + + ephemeral Boolean? + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default("*") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model MicrosoftteamsIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + botFrameworkAppId String? + botFrameworkAppSecret String? /// @encrypted + tenantId String? + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default("*") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model GooglechatIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + serviceAccountKey String? /// @encrypted + + projectNumber String? // @note Google Cloud project number used to verify the JWT audience on incoming events + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + autoRespond String? + + allowFrom String? @default("*") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model WhatsappIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + verifyToken String /// @encrypted + + appSecret String? /// @encrypted + + phoneNumberId String? + + accessToken String? /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default("*") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model MessengerIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + verifyToken String /// @encrypted + + accessToken String? /// @encrypted + + // @note the customer's own Meta APP secret - the key X-Hub-Signature-256 + // callbacks are signed with. Optional: without it callbacks are accepted + // unverified, with a logged bypass. + appSecret String? /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model InstagramIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + verifyToken String /// @encrypted + + accessToken String? /// @encrypted + + // @note the customer's own Meta APP secret - the key X-Hub-Signature-256 + // callbacks are signed with. Optional: without it callbacks are accepted + // unverified, with a logged bypass. + appSecret String? /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model TelegramIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + botToken String? /// @encrypted + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default("*") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model TwilioIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + accountSid String? + authToken String? /// @encrypted + + voice String? + + contactCollection Boolean @default(false) + + sessionDuration Float? + + allowFrom String? @default("*") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model AnamIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + apiKey String? /// @encrypted + + personaId String? + + visibility Visibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [blueprintId]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model AvatarIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + visibility Visibility @default(private) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [blueprintId]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model RecallIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + apiKey String? /// @encrypted + + // @note the signing secret of the Recall webhook endpoint (Svix, `whsec_...`). + // Optional: without it status callbacks are accepted unverified, with a logged + // bypass. + webhookSecret String? /// @encrypted + + region String? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [blueprintId]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model GithubIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + // this integration's GitHub App identity + credentials. Each integration is + // its own GitHub App. The installation id is NOT stored: it rides in every + // event payload and is combined with the App key to mint a token to reply. + appId String? // the GitHub App id (public-ish; signs the App JWT as `iss`) + privateKey String? /// @encrypted - the App's RSA private key (PEM) + webhookSecret String? /// @encrypted - validates x-hub-signature-256 HMAC-SHA256 + + contactCollection Boolean @default(false) + + sessionDuration Float? + + // @note unlike the other integrations this defaults to `@collaborators` and + // not `*`: an installed App hears from everyone who can comment, which on a + // public repository is every GitHub account. Rows predating this field were + // backfilled to `*` to preserve their behaviour, so the column default only + // governs new integrations. See lib/github.validation.ts + allowFrom String? @default("@collaborators") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model EmailIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + contactCollection Boolean @default(false) + + sessionDuration Float? + + attachments Boolean @default(false) + + allowFrom String? @default("*") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model SitemapIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + url String? + + glob String? + + selectors String? + + javascript Boolean? + + expiresIn Float? + + syncStatus SyncStatus @default(pending) + syncSchedule Schedule @default(never) + + lastSyncedAt DateTime @default(now()) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [syncSchedule, lastSyncedAt]) // used to find sitemap integrations to sync +} + +// --- +// --- +// --- + +model NotionIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + token String /// @encrypted + + expiresIn Float? + + syncStatus SyncStatus @default(pending) + syncSchedule Schedule @default(never) + + lastSyncedAt DateTime @default(now()) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [syncSchedule, lastSyncedAt]) // used to find notion integrations to sync +} + +// --- +// --- +// --- + +model SupportIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + email String? + + trigger Trigger? @default(automatic) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [botId]) + @@index(fields: [userId, botId]) +} + +// --- +// --- +// --- + +model ExtractIntegrationItem { + id String @id @default(cuid()) + + // relationships + extractIntegrationId String + extractIntegration ExtractIntegration @relation(fields: [extractIntegrationId], references: [id], onDelete: Cascade) + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + + // fields + data Json? /// [JsonRecord] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([extractIntegrationId, conversationId]) + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [extractIntegrationId, createdAt(sort: Desc)]) // used for listing +} + +model ExtractIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + schema Json? /// [JsonRecord] + + request String? @default("") + + model String? + + trigger Trigger? @default(automatic) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: items + items ExtractIntegrationItem[] + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [botId]) +} + +// --- +// --- +// --- + +model McpserverIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + oAuthConnectionId String? + oAuthConnection OAuthConnection? @relation(fields: [oAuthConnectionId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + accessToken String /// @encrypted + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model SkillserverIntegration { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + accessToken String /// @encrypted + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +enum PolicyType { + // @note the order is important because of the default value is the first one + + retention + usage +} + +model Policy { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + lockId String? + lock Lock? @relation(fields: [lockId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: Cascade) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + type PolicyType @default(retention) + + // lifecycle state - toggle the policy on/off without deleting it + state ResourceState @default(enabled) + + config Json? /// [PolicyConfig] @see prisma/post-generate-zod.js + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) + @@index(fields: [userId, type]) + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [botId, type]) +} + +// --- +// --- +// --- + +model HubBotPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + botId String @unique + bot Bot @relation(fields: [botId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + slug String? @unique + icon String? + rank Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model HubDatasetPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + datasetId String @unique + dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + slug String? @unique + icon String? + rank Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model HubSkillsetPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + skillsetId String @unique + skillset Skillset @relation(fields: [skillsetId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + slug String? @unique + icon String? + rank Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model HubBlueprintPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String @unique + blueprint Blueprint @relation(fields: [blueprintId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + slug String? @unique + icon String? + rank Int? + + shareLog Boolean @default(false) + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model HubWidgetPage { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + widgetId String @unique + widget WidgetIntegration @relation(fields: [widgetId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + slug String? @unique + icon String? + rank Int? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// @note UseType was migrated from an enum to a plain String to avoid requiring +// database migrations every time a new model is added. Valid use type values +// are now derived from the model config at runtime. See lib/usage.types.js. + +model Usage { + id String @id @default(cuid()) + + // owner + // @note usage records must survive user deletion with userId intact + userId String + user User @relation("UserUsage", fields: [userId], references: [id], onDelete: Restrict) + + // parent + // @note parent usage records must survive user deletion with parentUserId intact + parentUserId String? + parentUser User? @relation("ParentUserUsage", fields: [parentUserId], references: [id], onDelete: Restrict) + + // relationships: shallow + conversationId String? + messageId String? + taskId String? + contactId String? + blueprintId String? + botId String? + datasetId String? + skillsetId String? + abilityId String? + + // fields + type String + + count Int + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [type, createdAt(sort: Desc)]) // used for listing + @@index(fields: [userId, type, createdAt(sort: Desc)]) + @@index(fields: [userId, botId, createdAt(sort: Desc)]) // used by bot usage stats query + @@index(fields: [parentUserId, type, createdAt(sort: Desc)]) // used for parent account usage aggregation +} + +// --- +// --- +// --- + +model EventLog { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships: shallow + conversationId String? + taskId String? + contactId String? + spaceId String? + blueprintId String? + botId String? + datasetId String? + recordId String? + skillsetId String? + abilityId String? + fileId String? + secretId String? + portalId String? + policyId String? + widgetIntegrationId String? + slackIntegrationId String? + discordIntegrationId String? + microsoftteamsIntegrationId String? @map("teamsIntegrationId") + googlechatIntegrationId String? + whatsappIntegrationId String? + messengerIntegrationId String? + instagramIntegrationId String? + telegramIntegrationId String? + twilioIntegrationId String? + githubIntegrationId String? + emailIntegrationId String? + sitemapIntegrationId String? + notionIntegrationId String? + triggerIntegrationId String? + supportIntegrationId String? + extractIntegrationId String? + mcpserverIntegrationId String? + skillserverIntegrationId String? + // @note the event log tracks conversational integrations only; anam, + // avatar and recall are excluded by design (they emit no events). If that + // ever changes, add the three `*IntegrationId` columns here, the matching + // entries in the event log/metric list+export whitelists and the + // EventLogItem type, then regenerate the client. + webhookId String? + taskExecutionId String? + triggerExecutionId String? + + // fields + name String @default("") + description String @default("") + + type String + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [userId, type, createdAt(sort: Desc)]) +} + +model EventMetric { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships: shallow + conversationId String? + taskId String? + contactId String? + spaceId String? + blueprintId String? + botId String? + datasetId String? + recordId String? + skillsetId String? + abilityId String? + fileId String? + secretId String? + portalId String? + policyId String? + widgetIntegrationId String? + slackIntegrationId String? + discordIntegrationId String? + microsoftteamsIntegrationId String? @map("teamsIntegrationId") + googlechatIntegrationId String? + whatsappIntegrationId String? + messengerIntegrationId String? + instagramIntegrationId String? + telegramIntegrationId String? + twilioIntegrationId String? + githubIntegrationId String? + emailIntegrationId String? + sitemapIntegrationId String? + notionIntegrationId String? + triggerIntegrationId String? + supportIntegrationId String? + extractIntegrationId String? + mcpserverIntegrationId String? + skillserverIntegrationId String? + // @note the event log tracks conversational integrations only; anam, + // avatar and recall are excluded by design (they emit no events). If that + // ever changes, add the three `*IntegrationId` columns here, the matching + // entries in the event log/metric list+export whitelists and the + // EventLogItem type, then regenerate the client. + webhookId String? + + // fields + name String @default("") + description String @default("") + + type String + + value Float? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [userId, type, createdAt(sort: Desc)]) +} + +// --- + +model AuditLog { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + conversationId String? + taskId String? + contactId String? + spaceId String? + blueprintId String? + botId String? + datasetId String? + recordId String? + skillsetId String? + abilityId String? + fileId String? + secretId String? + portalId String? + policyId String? + webhookId String? + sessionId String? + + // fields + name String @default("") + description String @default("") + + action String + + oldValues Json? /// [JsonRecord] + newValues Json? /// [JsonRecord] + + ipAddress String? + userAgent String? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: delete + @@index(fields: [createdAt]) // used for purging old records + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [userId, action, createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +model Contact { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + preferences String? + + fingerprint String @default(cuid()) + + email String? + phone String? + nick String? + + verifiedAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: objects + conversations Conversation[] + tasks Task[] + ratings Rating[] + memories Memory[] + spaces Space[] + contexts Context[] + + // connections: secret values + secretValues SecretValue[] + + // indexes: unique + @@unique([userId, fingerprint]) + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [userId, email]) +} + +// --- + +model Conversation { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + spaceId String? + space Space? @relation(fields: [spaceId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + taskId String? + task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull) + datasetId String? + dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: SetNull) + + skillsetId String? + skillset Skillset? @relation(fields: [skillsetId], references: [id], onDelete: SetNull) + + // fields + name String @default("") + description String @default("") + + backstory String? @default("") + + model String? @default("") + + privacy Boolean? @default(false) + moderation Boolean? @default(false) + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: objects + messages Message[] + ratings Rating[] + + // connections: tasks + taskExecutions TaskExecution[] + + // connections: extractIntegrationItems + extractIntegrationItems ExtractIntegrationItem[] + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) // used for listing + @@index(fields: [userId, contactId, createdAt(sort: Desc)]) // used for listing + @@index(fields: [id, botId]) + @@index(fields: [id, datasetId]) + @@index(fields: [id, skillsetId]) + @@index(fields: [expiresAt]) // used for cleaning up expired conversations + @@index(fields: [createdAt]) // used for cleaning up empty conversations + @@index(fields: [datasetId]) + @@index(fields: [skillsetId]) + @@index(fields: [botId]) + @@index(fields: [contactId, createdAt(sort: Desc)]) // used by conversation fetch in actions + @@index(fields: [spaceId, createdAt(sort: Desc)]) // used by conversation fetch in actions + @@index(fields: [taskId, createdAt(sort: Desc)]) // used by task fetch in actions + @@index(fields: [contactId, taskId, createdAt(sort: Desc)]) // used by task fetch in actions +} + +// --- + +enum MessageType { + // stable types + + user // a message from the user + bot // a message from the bot + reasoning // a message that carries reasoning such as a thought process + context // a message that carries some context such as additional information + instruction // a message that carries some instruction such as a command + backstory // a message that describes the backstory of the bot + activity // a message that describes an activity + checkpoint // a message that describes a compact summary of the conversation + // notification // a message that describes a notification +} + +model Message { + id String @id @default(cuid()) + + // relationships + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + type MessageType + + text String + + nps Int? + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: objects + ratings Rating[] + + // connections: tasks + taskExecutionsAsStart TaskExecution[] @relation("TaskExecutionStart") + taskExecutionsAsEnd TaskExecution[] @relation("TaskExecutionEnd") + + // indexes: other + @@index(fields: [conversationId, type, createdAt(sort: Desc), id(sort: Desc)]) // supports reverse-chronological engine reads within one conversation and message type; id breaks timestamp ties + @@index(fields: [conversationId, type, createdAt(sort: Desc)]) // specifically used in /api/v1/conversation/[conversationId]/send to get messages fast + @@index(fields: [conversationId, createdAt(sort: Desc)]) // @note all messages are almost always pulled in reverse order +} + +// --- + +model Rating { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + conversationId String? + conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull) + messageId String? + message Message? @relation(fields: [messageId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + value Int + + reason String? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [botId]) + @@index(fields: [conversationId]) + @@index(fields: [messageId]) +} + +// --- + +enum TaskStatus { + idle // not currently running + running // actively executing + canceled // execution was canceled before completion +} + +enum TaskOutcome { + pending // never run yet + success // last run completed successfully + failure // last run failed or was incomplete +} + +model TaskExecution { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + taskId String + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + conversationId String? + conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull) + startMessageId String? + startMessage Message? @relation("TaskExecutionStart", fields: [startMessageId], references: [id], onDelete: SetNull) + endMessageId String? + endMessage Message? @relation("TaskExecutionEnd", fields: [endMessageId], references: [id], onDelete: SetNull) + + // fields + name String @default("") + description String @default("") + + status TaskStatus @default(idle) + outcome TaskOutcome @default(pending) + + completedAt DateTime? + + resumeAt DateTime? + + keepAliveUntil DateTime? + + summary String? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [taskId, createdAt(sort: Desc)]) + // indexes: reaper (stalled sweep over running executions) + @@index(fields: [status, keepAliveUntil]) + // indexes: foreign keys + @@index(fields: [userId]) + @@index(fields: [taskId]) + @@index(fields: [conversationId]) + @@index(fields: [startMessageId]) + @@index(fields: [endMessageId]) +} + +// --- + +model Task { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: SetNull) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + status TaskStatus @default(idle) + outcome TaskOutcome @default(pending) + + sessionDuration Float? + + maxIterations Int? + maxTime Float? + maxCalls Int? + + schedule String? + timezone String? + + nextRunAt DateTime? + lastRunAt DateTime? + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: tasks + taskExecutions TaskExecution[] + + // connections: objects + conversations Conversation[] + + // indexes: listing + @@index(fields: [userId, createdAt(sort: Desc), id(sort: Desc)]) // used for cursor pagination in list endpoint + // indexes: other + @@index(fields: [userId]) + @@index(fields: [blueprintId]) + @@index(fields: [schedule, lastRunAt]) + @@index(fields: [nextRunAt]) +} + +// --- + +model Memory { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + botId String? + bot Bot? @relation(fields: [botId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + text String + + expiresAt DateTime? + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [userId]) + @@index(fields: [botId]) +} + +// --- + +model Space { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // relationships + contactId String? + contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + + blueprintId String? + blueprint Blueprint? @relation(fields: [blueprintId], references: [id], onDelete: SetNull) + + // ref + alias String? + + // fields + name String @default("") + description String @default("") + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // connections: conversation + conversations Conversation[] + + // connections: resources + abilities Ability[] + + // connections: publishing + sites SpaceSite[] + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId, createdAt(sort: Desc)]) + @@index(fields: [blueprintId]) +} + +// --- + +model SpaceSite { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // attachment (many sites per space) + spaceId String + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + + // ref + alias String? + + // basic information + name String @default("") + description String @default("") + + // host binding + slug String @unique + + // serving config + prefix String? + index String @default("index.html") + notFound String @default("404.html") + + // meta and others + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: unique + @@unique([userId, alias]) // for alias lookup by user + // indexes: other + @@index(fields: [userId]) + @@index(fields: [spaceId]) +} + +// --- +// --- +// --- + +// @note `Token` is the API token (auth credential); LLM token counting is +// `usage` and `limits` vocabulary. Both keep the word by decision - the two +// never share a type or a table, and prose distinguishes "API token" from +// "usage tokens". + +model Token { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + token String @unique /// @digest + + config Json? /// [TokenConfig] @see prisma/post-generate-zod.js + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [createdAt(sort: Desc)]) // used for listing + @@index(fields: [userId]) +} + +// --- +// --- +// --- + +model Webhook { + id String @id @default(cuid()) + + // owner + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // fields + name String @default("") + description String @default("") + + request String @default("") + + events String @default("") + + secret String @unique /// @encrypted + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@index(fields: [userId]) + @@index(fields: [createdAt(sort: Desc)]) +} + +// --- +// --- +// --- + +enum DatasetFileAttachmentType { + source +} + +model DatasetFileAttachment { + // relationships + datasetId String + dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade) + fileId String + file File @relation(fields: [fileId], references: [id], onDelete: Cascade) + + // fields + type DatasetFileAttachmentType + + meta Json? /// [Meta] + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@id([datasetId, fileId]) + @@index(fields: [fileId]) // because of @relation + @@index(fields: [datasetId, createdAt(sort: Desc)]) // used for listing + @@index(fields: [datasetId, type]) +} + +// --- + +enum WidgetIntegrationFileAttachmentType { + bar + user + bot + button +} + +model WidgetIntegrationFileAttachment { + // relationships + widgetIntegrationId String + widgetIntegration WidgetIntegration @relation(fields: [widgetIntegrationId], references: [id], onDelete: Cascade) + fileId String + file File @relation(fields: [fileId], references: [id], onDelete: Cascade) + + // fields + type WidgetIntegrationFileAttachmentType + + // timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // indexes: other + @@id([widgetIntegrationId, type]) + @@index(fields: [fileId]) // because of @relation +} diff --git a/packages/db/prisma/sql/breakdownTotalContactsWithConversationsOverPeriod.sql b/packages/db/prisma/sql/breakdownTotalContactsWithConversationsOverPeriod.sql new file mode 100644 index 0000000..cba8623 --- /dev/null +++ b/packages/db/prisma/sql/breakdownTotalContactsWithConversationsOverPeriod.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + DATE(c.createdAt) AS date, + COUNT(DISTINCT c.contactId) AS total +FROM Conversation c +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY DATE(c.createdAt) +ORDER BY date ASC diff --git a/packages/db/prisma/sql/breakdownTotalConversationsOverPeriod.sql b/packages/db/prisma/sql/breakdownTotalConversationsOverPeriod.sql new file mode 100644 index 0000000..16f2b42 --- /dev/null +++ b/packages/db/prisma/sql/breakdownTotalConversationsOverPeriod.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + DATE(c.createdAt) AS date, + COUNT(c.id) AS total +FROM Conversation c +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY DATE(c.createdAt) +ORDER BY date ASC diff --git a/packages/db/prisma/sql/breakdownTotalMessagesOfTypeOverPeriod.sql b/packages/db/prisma/sql/breakdownTotalMessagesOfTypeOverPeriod.sql new file mode 100644 index 0000000..45a966a --- /dev/null +++ b/packages/db/prisma/sql/breakdownTotalMessagesOfTypeOverPeriod.sql @@ -0,0 +1,16 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:type The message type ('user', 'bot', 'activity') +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT + DATE(m.createdAt) AS date, + COUNT(m.id) AS total +FROM Message m +JOIN Conversation c ON m.conversationId = c.id +WHERE c.userId = ? + AND m.type = ? + AND c.contactId IS NOT NULL + AND m.createdAt >= ? + AND m.createdAt <= ? +GROUP BY DATE(m.createdAt) +ORDER BY date ASC diff --git a/packages/db/prisma/sql/breakdownTotalMessagesOverPeriod.sql b/packages/db/prisma/sql/breakdownTotalMessagesOverPeriod.sql new file mode 100644 index 0000000..c67aae1 --- /dev/null +++ b/packages/db/prisma/sql/breakdownTotalMessagesOverPeriod.sql @@ -0,0 +1,14 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + DATE(m.createdAt) AS date, + COUNT(m.id) AS total +FROM Message m +JOIN Conversation c ON m.conversationId = c.id +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND m.createdAt >= ? + AND m.createdAt <= ? +GROUP BY DATE(m.createdAt) +ORDER BY date ASC diff --git a/packages/db/prisma/sql/breakdownTotalRatingsOverPeriod.sql b/packages/db/prisma/sql/breakdownTotalRatingsOverPeriod.sql new file mode 100644 index 0000000..b923cd8 --- /dev/null +++ b/packages/db/prisma/sql/breakdownTotalRatingsOverPeriod.sql @@ -0,0 +1,14 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +SELECT + DATE(createdAt) as date, + COUNT(*) as total, + COUNT(CASE WHEN value > 0 THEN 1 END) as thumbsUp, + COUNT(CASE WHEN value < 0 THEN 1 END) as thumbsDown +FROM Rating +WHERE userId = ? + AND createdAt >= ? + AND createdAt <= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db/prisma/sql/breakdownTotalUsageTokensOverPeriod.sql b/packages/db/prisma/sql/breakdownTotalUsageTokensOverPeriod.sql new file mode 100644 index 0000000..c312f22 --- /dev/null +++ b/packages/db/prisma/sql/breakdownTotalUsageTokensOverPeriod.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + DATE(u.createdAt) AS date, + COALESCE(SUM(u.count), 0) AS total +FROM Usage u +WHERE u.userId = ? + AND u.type LIKE '%_TOKEN' + AND u.createdAt >= ? + AND u.createdAt <= ? +GROUP BY DATE(u.createdAt) +ORDER BY date ASC; diff --git a/packages/db/prisma/sql/getAverageMessagesOfTypeOverPeriod.sql b/packages/db/prisma/sql/getAverageMessagesOfTypeOverPeriod.sql new file mode 100644 index 0000000..18c984f --- /dev/null +++ b/packages/db/prisma/sql/getAverageMessagesOfTypeOverPeriod.sql @@ -0,0 +1,15 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:messageType The type of the message (e.g., 'bot') +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT COALESCE(AVG(msg_count), 0) AS average +FROM ( + SELECT m.conversationId, COUNT(*) AS msg_count + FROM Message m + JOIN Conversation c ON c.id = m.conversationId + WHERE c.userId = ? + AND m.type = ? + AND m.createdAt >= ? + AND m.createdAt <= ? + GROUP BY m.conversationId +) AS messages_per_conversation_in_period; diff --git a/packages/db/prisma/sql/getBotConversationCountByDay.sql b/packages/db/prisma/sql/getBotConversationCountByDay.sql new file mode 100644 index 0000000..0d876f3 --- /dev/null +++ b/packages/db/prisma/sql/getBotConversationCountByDay.sql @@ -0,0 +1,12 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:botId The ID of the bot +-- @param {DateTime} $3:fromDate Inclusive start of the period +-- @param {DateTime} $4:toDate Inclusive end of the period +SELECT DATE(createdAt) AS date, COUNT(*) AS total +FROM Conversation +WHERE userId = ? + AND botId = ? + AND createdAt >= ? + AND createdAt <= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db/prisma/sql/getBotConversationsWithUserMessageCount.sql b/packages/db/prisma/sql/getBotConversationsWithUserMessageCount.sql new file mode 100644 index 0000000..bc9feb2 --- /dev/null +++ b/packages/db/prisma/sql/getBotConversationsWithUserMessageCount.sql @@ -0,0 +1,16 @@ +-- @param {String} $1:userMessageType The message type that marks a user turn +-- @param {String} $2:userId The ID of the user +-- @param {String} $3:botId The ID of the bot +-- @param {DateTime} $4:fromDate Inclusive start of the period +-- @param {DateTime} $5:toDate Inclusive end of the period +SELECT COUNT(*) AS total +FROM ( + SELECT c.id + FROM Conversation c + JOIN Message m ON m.conversationId = c.id AND m.type = ? + WHERE c.userId = ? + AND c.botId = ? + AND c.createdAt >= ? + AND c.createdAt <= ? + GROUP BY c.id +) AS conversationsWithUserMessages; diff --git a/packages/db/prisma/sql/getBotMessageCountByDay.sql b/packages/db/prisma/sql/getBotMessageCountByDay.sql new file mode 100644 index 0000000..4243a0e --- /dev/null +++ b/packages/db/prisma/sql/getBotMessageCountByDay.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:botId The ID of the bot +-- @param {DateTime} $3:fromDate Inclusive start of the period +-- @param {DateTime} $4:toDate Inclusive end of the period +SELECT DATE(m.createdAt) AS date, COUNT(*) AS total +FROM Message m +JOIN Conversation c ON c.id = m.conversationId +WHERE c.userId = ? + AND c.botId = ? + AND m.createdAt >= ? + AND m.createdAt <= ? +GROUP BY DATE(m.createdAt) +ORDER BY date ASC; diff --git a/packages/db/prisma/sql/getBotSingleTurnConversationCount.sql b/packages/db/prisma/sql/getBotSingleTurnConversationCount.sql new file mode 100644 index 0000000..74f9adf --- /dev/null +++ b/packages/db/prisma/sql/getBotSingleTurnConversationCount.sql @@ -0,0 +1,17 @@ +-- @param {String} $1:userMessageType The message type that marks a user turn +-- @param {String} $2:userId The ID of the user +-- @param {String} $3:botId The ID of the bot +-- @param {DateTime} $4:fromDate Inclusive start of the period +-- @param {DateTime} $5:toDate Inclusive end of the period +SELECT COUNT(*) AS total +FROM ( + SELECT c.id + FROM Conversation c + LEFT JOIN Message m ON m.conversationId = c.id AND m.type = ? + WHERE c.userId = ? + AND c.botId = ? + AND c.createdAt >= ? + AND c.createdAt <= ? + GROUP BY c.id + HAVING COUNT(m.id) = 1 +) AS singleTurnConvs; diff --git a/packages/db/prisma/sql/getBotUsageStats.sql b/packages/db/prisma/sql/getBotUsageStats.sql new file mode 100644 index 0000000..226b040 --- /dev/null +++ b/packages/db/prisma/sql/getBotUsageStats.sql @@ -0,0 +1,22 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:botId The ID of the bot +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_BASE_TOKEN' THEN count + ELSE 0 + END), 0) AS totalTokens, + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_CONVERSATION' THEN count + ELSE 0 + END), 0) AS totalConversations, + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_MESSAGE' THEN count + ELSE 0 + END), 0) AS totalMessages +FROM Usage +WHERE userId = ? + AND botId = ? + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db/prisma/sql/getConversationUsageStats.sql b/packages/db/prisma/sql/getConversationUsageStats.sql new file mode 100644 index 0000000..0afa09b --- /dev/null +++ b/packages/db/prisma/sql/getConversationUsageStats.sql @@ -0,0 +1,18 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:conversationId The ID of the conversation +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_BASE_TOKEN' THEN count + ELSE 0 + END), 0) AS totalTokens, + COALESCE(SUM(CASE + WHEN type = 'CHATBOTKIT_MESSAGE' THEN count + ELSE 0 + END), 0) AS totalMessages +FROM Usage +WHERE userId = ? + AND conversationId = ? + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db/prisma/sql/getDailyNegativeRatingCount.sql b/packages/db/prisma/sql/getDailyNegativeRatingCount.sql new file mode 100644 index 0000000..a374304 --- /dev/null +++ b/packages/db/prisma/sql/getDailyNegativeRatingCount.sql @@ -0,0 +1,11 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Inclusive start of the period +-- @param {DateTime} $3:toDate Inclusive end of the period +SELECT DATE(createdAt) AS date, COUNT(*) AS total +FROM Rating +WHERE userId = ? + AND value < 0 + AND createdAt >= ? + AND createdAt <= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db/prisma/sql/getEventMetricSeriesOverPeriod.sql b/packages/db/prisma/sql/getEventMetricSeriesOverPeriod.sql new file mode 100644 index 0000000..a920e11 --- /dev/null +++ b/packages/db/prisma/sql/getEventMetricSeriesOverPeriod.sql @@ -0,0 +1,12 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:type The event metric type +-- @param {DateTime} $3:fromDate Inclusive start of the period (DateTime) +SELECT + DATE(createdAt) AS date, + COALESCE(SUM(value), 0) AS total +FROM EventMetric +WHERE userId = ? + AND type = ? + AND createdAt >= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db/prisma/sql/getTotalAbilitiesForUser.sql b/packages/db/prisma/sql/getTotalAbilitiesForUser.sql new file mode 100644 index 0000000..a478fd6 --- /dev/null +++ b/packages/db/prisma/sql/getTotalAbilitiesForUser.sql @@ -0,0 +1,7 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Ability a +JOIN Skillset s ON a.skillsetId = s.id +JOIN User u ON s.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalBotsForUser.sql b/packages/db/prisma/sql/getTotalBotsForUser.sql new file mode 100644 index 0000000..2cf95ec --- /dev/null +++ b/packages/db/prisma/sql/getTotalBotsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Bot b +JOIN User u ON b.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalContacts.sql b/packages/db/prisma/sql/getTotalContacts.sql new file mode 100644 index 0000000..3f89622 --- /dev/null +++ b/packages/db/prisma/sql/getTotalContacts.sql @@ -0,0 +1,4 @@ +-- @param {String} $1:userId The ID of the user +SELECT count(*) as total +FROM Contact +WHERE userId = ?; diff --git a/packages/db/prisma/sql/getTotalContactsWithConversationsOverPeriod.sql b/packages/db/prisma/sql/getTotalContactsWithConversationsOverPeriod.sql new file mode 100644 index 0000000..ca20908 --- /dev/null +++ b/packages/db/prisma/sql/getTotalContactsWithConversationsOverPeriod.sql @@ -0,0 +1,13 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT COUNT(*) AS total +FROM ( + SELECT c.contactId + FROM Conversation c + WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? + GROUP BY c.contactId +) AS distinct_contacts_with_conversations_in_period; diff --git a/packages/db/prisma/sql/getTotalConversationsOverPeriod.sql b/packages/db/prisma/sql/getTotalConversationsOverPeriod.sql new file mode 100644 index 0000000..91e1662 --- /dev/null +++ b/packages/db/prisma/sql/getTotalConversationsOverPeriod.sql @@ -0,0 +1,8 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT COUNT(*) AS total +FROM Conversation c +WHERE c.userId = ? + AND c.createdAt >= ? + AND c.createdAt <= ? diff --git a/packages/db/prisma/sql/getTotalConversationsWithNumberedMessagesOverPeriod.sql b/packages/db/prisma/sql/getTotalConversationsWithNumberedMessagesOverPeriod.sql new file mode 100644 index 0000000..a1c74ff --- /dev/null +++ b/packages/db/prisma/sql/getTotalConversationsWithNumberedMessagesOverPeriod.sql @@ -0,0 +1,15 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:minMessages Minimum number of messages per conversation in the period +SELECT COUNT(*) AS total +FROM ( + SELECT m.conversationId + FROM Message m + JOIN Conversation c ON c.id = m.conversationId + WHERE c.userId = ? + AND m.createdAt >= ? + AND m.createdAt <= ? + GROUP BY m.conversationId + HAVING COUNT(*) >= ? +) AS conversations_with_min_messages_in_period; diff --git a/packages/db/prisma/sql/getTotalDatasetsForUser.sql b/packages/db/prisma/sql/getTotalDatasetsForUser.sql new file mode 100644 index 0000000..4b37715 --- /dev/null +++ b/packages/db/prisma/sql/getTotalDatasetsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Dataset d +JOIN User u ON d.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalFilesForUser.sql b/packages/db/prisma/sql/getTotalFilesForUser.sql new file mode 100644 index 0000000..7f90e5c --- /dev/null +++ b/packages/db/prisma/sql/getTotalFilesForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM File f +JOIN User u ON f.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalMessagesOfTypeOverPeriod.sql b/packages/db/prisma/sql/getTotalMessagesOfTypeOverPeriod.sql new file mode 100644 index 0000000..62da574 --- /dev/null +++ b/packages/db/prisma/sql/getTotalMessagesOfTypeOverPeriod.sql @@ -0,0 +1,11 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:messageType The type of the message +-- @param {DateTime} $3:fromDate Start of the period (DateTime) +-- @param {DateTime} $4:toDate End of the period (DateTime) +SELECT COUNT(*) AS total +FROM Message m +JOIN Conversation c ON c.id = m.conversationId +WHERE c.userId = ? + AND m.type = ? + AND m.createdAt >= ? + AND m.createdAt <= ?; diff --git a/packages/db/prisma/sql/getTotalMessagesOverPeriod.sql b/packages/db/prisma/sql/getTotalMessagesOverPeriod.sql new file mode 100644 index 0000000..b67c687 --- /dev/null +++ b/packages/db/prisma/sql/getTotalMessagesOverPeriod.sql @@ -0,0 +1,9 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT COUNT(*) AS total +FROM Message m +JOIN Conversation c ON c.id = m.conversationId +WHERE c.userId = ? + AND m.createdAt >= ? + AND m.createdAt <= ?; diff --git a/packages/db/prisma/sql/getTotalPoliciesForUser.sql b/packages/db/prisma/sql/getTotalPoliciesForUser.sql new file mode 100644 index 0000000..4bcf107 --- /dev/null +++ b/packages/db/prisma/sql/getTotalPoliciesForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Policy p +JOIN User u ON p.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalPortalsForUser.sql b/packages/db/prisma/sql/getTotalPortalsForUser.sql new file mode 100644 index 0000000..63b09e3 --- /dev/null +++ b/packages/db/prisma/sql/getTotalPortalsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Portal p +JOIN User u ON p.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalRatingsOverPeriod.sql b/packages/db/prisma/sql/getTotalRatingsOverPeriod.sql new file mode 100644 index 0000000..159ba8d --- /dev/null +++ b/packages/db/prisma/sql/getTotalRatingsOverPeriod.sql @@ -0,0 +1,8 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +SELECT count(*) as total +FROM Rating +WHERE userId = ? + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db/prisma/sql/getTotalSkillsetsForUser.sql b/packages/db/prisma/sql/getTotalSkillsetsForUser.sql new file mode 100644 index 0000000..cf28f88 --- /dev/null +++ b/packages/db/prisma/sql/getTotalSkillsetsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Skillset s +JOIN User u ON s.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalTeamMembersForUser.sql b/packages/db/prisma/sql/getTotalTeamMembersForUser.sql new file mode 100644 index 0000000..8295fc1 --- /dev/null +++ b/packages/db/prisma/sql/getTotalTeamMembersForUser.sql @@ -0,0 +1,7 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM TeamMembership tm +JOIN Team t ON tm.teamId = t.id +JOIN User u ON t.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalTeamsForUser.sql b/packages/db/prisma/sql/getTotalTeamsForUser.sql new file mode 100644 index 0000000..564fd21 --- /dev/null +++ b/packages/db/prisma/sql/getTotalTeamsForUser.sql @@ -0,0 +1,6 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:effectiveUserId The ID of the user +SELECT COUNT(*) as count +FROM Team t +JOIN User u ON t.userId = u.id +WHERE u.parentId = ? OR u.id = ?; diff --git a/packages/db/prisma/sql/getTotalThumbsDownOverPeriod.sql b/packages/db/prisma/sql/getTotalThumbsDownOverPeriod.sql new file mode 100644 index 0000000..3bae679 --- /dev/null +++ b/packages/db/prisma/sql/getTotalThumbsDownOverPeriod.sql @@ -0,0 +1,9 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +SELECT count(*) as total +FROM Rating +WHERE userId = ? + AND value <= 0 + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db/prisma/sql/getTotalThumbsUpOverPeriod.sql b/packages/db/prisma/sql/getTotalThumbsUpOverPeriod.sql new file mode 100644 index 0000000..75769a7 --- /dev/null +++ b/packages/db/prisma/sql/getTotalThumbsUpOverPeriod.sql @@ -0,0 +1,9 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +SELECT count(*) as total +FROM Rating +WHERE userId = ? + AND value > 0 + AND createdAt >= ? + AND createdAt <= ?; diff --git a/packages/db/prisma/sql/getTotalUsageTokensOverPeriod.sql b/packages/db/prisma/sql/getTotalUsageTokensOverPeriod.sql new file mode 100644 index 0000000..4e6b89c --- /dev/null +++ b/packages/db/prisma/sql/getTotalUsageTokensOverPeriod.sql @@ -0,0 +1,10 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +SELECT + COALESCE(SUM(u.count), 0) AS total +FROM Usage u +WHERE u.userId = ? + AND u.type LIKE '%_TOKEN' + AND u.createdAt >= ? + AND u.createdAt <= ?; diff --git a/packages/db/prisma/sql/getTotalUsersForUser.sql b/packages/db/prisma/sql/getTotalUsersForUser.sql new file mode 100644 index 0000000..5b87daf --- /dev/null +++ b/packages/db/prisma/sql/getTotalUsersForUser.sql @@ -0,0 +1,4 @@ +-- @param {String} $1:userId The ID of the user +SELECT COUNT(*) as count +FROM User u +WHERE u.parentId = ?; diff --git a/packages/db/prisma/sql/getUsageCountByTypeOverPeriod.sql b/packages/db/prisma/sql/getUsageCountByTypeOverPeriod.sql new file mode 100644 index 0000000..6472805 --- /dev/null +++ b/packages/db/prisma/sql/getUsageCountByTypeOverPeriod.sql @@ -0,0 +1,14 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:type The usage type +-- @param {DateTime} $3:fromDate Inclusive start of the period +-- @param {DateTime} $4:toDate Inclusive end of the period +SELECT + DATE(createdAt) AS date, + COALESCE(SUM(count), 0) AS total +FROM Usage +WHERE userId = ? + AND type = ? + AND createdAt >= ? + AND createdAt <= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db/prisma/sql/getUsageCountByTypeSince.sql b/packages/db/prisma/sql/getUsageCountByTypeSince.sql new file mode 100644 index 0000000..1bdf8b3 --- /dev/null +++ b/packages/db/prisma/sql/getUsageCountByTypeSince.sql @@ -0,0 +1,12 @@ +-- @param {String} $1:userId The ID of the user +-- @param {String} $2:type The usage type +-- @param {DateTime} $3:since Inclusive start of the period +SELECT + DATE(createdAt) AS date, + COALESCE(SUM(count), 0) AS total +FROM Usage +WHERE userId = ? + AND type = ? + AND createdAt >= ? +GROUP BY DATE(createdAt) +ORDER BY date ASC; diff --git a/packages/db/prisma/sql/listContacts.sql b/packages/db/prisma/sql/listContacts.sql new file mode 100644 index 0000000..2836206 --- /dev/null +++ b/packages/db/prisma/sql/listContacts.sql @@ -0,0 +1,7 @@ +-- @param {String} $1:userId The ID of the user +-- @param {Int} $2:limit The maximum number of contacts to return (optional) +SELECT id, name, description, email, nick, meta, createdAt +FROM Contact +WHERE userId = ? +ORDER BY createdAt DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listContactsWithConversationsOverPeriod.sql b/packages/db/prisma/sql/listContactsWithConversationsOverPeriod.sql new file mode 100644 index 0000000..6315e7b --- /dev/null +++ b/packages/db/prisma/sql/listContactsWithConversationsOverPeriod.sql @@ -0,0 +1,23 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of contacts to return (optional) +SELECT + co.id, + co.name, + co.description, + co.email, + co.nick, + co.meta, + co.createdAt, + COUNT(c.id) AS _countValue, + 'conversation' AS _countType +FROM Conversation c +JOIN Contact co ON c.contactId = co.id +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY co.id, co.name, co.description, co.email, co.nick +ORDER BY _countValue DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listContactsWithMessagesOverPeriod.sql b/packages/db/prisma/sql/listContactsWithMessagesOverPeriod.sql new file mode 100644 index 0000000..fd3e718 --- /dev/null +++ b/packages/db/prisma/sql/listContactsWithMessagesOverPeriod.sql @@ -0,0 +1,24 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of contacts to return (optional) +SELECT + co.id, + co.name, + co.description, + co.email, + co.nick, + co.meta, + co.createdAt, + COUNT(m.id) AS _countValue, + 'message' AS _countType +FROM Message m +JOIN Conversation c ON m.conversationId = c.id +JOIN Contact co ON c.contactId = co.id +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND m.createdAt >= ? + AND m.createdAt <= ? +GROUP BY co.id, co.name, co.description, co.email, co.nick +ORDER BY _countValue DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listContactsWithRatingsOverPeriod.sql b/packages/db/prisma/sql/listContactsWithRatingsOverPeriod.sql new file mode 100644 index 0000000..1347c6a --- /dev/null +++ b/packages/db/prisma/sql/listContactsWithRatingsOverPeriod.sql @@ -0,0 +1,26 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +-- @param {Int} $4:limit The maximum number of contacts to return +SELECT + c.id, + c.name, + c.description, + c.email, + c.nick, + c.meta, + c.createdAt, + COUNT(CASE WHEN r.value > 0 THEN 1 END) as _upvoteCount, + COUNT(CASE WHEN r.value < 0 THEN 1 END) as _downvoteCount, + COUNT(r.id) as _countValue, + 'rating' as _countType +FROM Contact c +LEFT JOIN Rating r ON c.id = r.contactId + AND r.userId = ? + AND r.createdAt >= ? + AND r.createdAt <= ? +WHERE c.userId = r.userId +GROUP BY c.id, c.name, c.description, c.email, c.nick, c.meta, c.createdAt +HAVING _countValue > 0 +ORDER BY _countValue DESC, _upvoteCount DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listConversationsOverPeriod.sql b/packages/db/prisma/sql/listConversationsOverPeriod.sql new file mode 100644 index 0000000..c85e953 --- /dev/null +++ b/packages/db/prisma/sql/listConversationsOverPeriod.sql @@ -0,0 +1,25 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of conversations to return (optional) +SELECT + c.id, + co.id AS contactId, + co.name, + co.email, + co.nick, + co.description, + co.meta, + c.createdAt, + COUNT(m.id) AS _countValue, + 'message' AS _countType +FROM Conversation c +JOIN Contact co ON c.contactId = co.id +LEFT JOIN Message m ON c.id = m.conversationId +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY c.id, c.createdAt, co.id, co.name, co.email, co.nick, co.description +ORDER BY _countValue DESC, c.createdAt DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listConversationsWithNumberedMessagesOverPeriod.sql b/packages/db/prisma/sql/listConversationsWithNumberedMessagesOverPeriod.sql new file mode 100644 index 0000000..8949a8e --- /dev/null +++ b/packages/db/prisma/sql/listConversationsWithNumberedMessagesOverPeriod.sql @@ -0,0 +1,24 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:messageCount Minimum number of messages for follow-ups +-- @param {Int} $5:limit The maximum number of conversations to return (optional) +SELECT + c.id, + c.createdAt, + co.name, + co.email, + co.nick, + co.description, + COUNT(m.id) AS messageCount +FROM Conversation c +JOIN Contact co ON c.contactId = co.id +LEFT JOIN Message m ON c.id = m.conversationId +WHERE c.userId = ? + AND c.contactId IS NOT NULL + AND c.createdAt >= ? + AND c.createdAt <= ? +GROUP BY c.id, c.createdAt, co.name, co.email, co.nick, co.description +HAVING COUNT(m.id) >= ? +ORDER BY messageCount DESC, c.createdAt DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listEventLogsOfTypeActionsGroupedByTypeOverPeriod.sql b/packages/db/prisma/sql/listEventLogsOfTypeActionsGroupedByTypeOverPeriod.sql new file mode 100644 index 0000000..b46093b --- /dev/null +++ b/packages/db/prisma/sql/listEventLogsOfTypeActionsGroupedByTypeOverPeriod.sql @@ -0,0 +1,18 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of action types to return (optional) +SELECT + el.type, + el.name, + el.description, + COUNT(el.id) AS _countValue, + 'action' AS _countType +FROM EventLog el +WHERE el.userId = ? + AND el.type LIKE 'action.%' + AND el.createdAt >= ? + AND el.createdAt <= ? +GROUP BY el.type, el.name, el.description +ORDER BY _countValue DESC, el.type ASC +LIMIT ?; diff --git a/packages/db/prisma/sql/listTopBotsByTokenUsageOverPeriod.sql b/packages/db/prisma/sql/listTopBotsByTokenUsageOverPeriod.sql new file mode 100644 index 0000000..3e2cfe8 --- /dev/null +++ b/packages/db/prisma/sql/listTopBotsByTokenUsageOverPeriod.sql @@ -0,0 +1,19 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of results to return +SELECT + u.botId AS id, + b.name AS name, + b.description AS description, + COALESCE(SUM(u.count), 0) AS total +FROM Usage u +LEFT JOIN Bot b ON u.botId = b.id +WHERE u.userId = ? + AND u.botId IS NOT NULL + AND u.type LIKE '%_TOKEN' + AND u.createdAt >= ? + AND u.createdAt <= ? +GROUP BY u.botId, b.name, b.description +ORDER BY total DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listTopContactsByTokenUsageOverPeriod.sql b/packages/db/prisma/sql/listTopContactsByTokenUsageOverPeriod.sql new file mode 100644 index 0000000..4852c9c --- /dev/null +++ b/packages/db/prisma/sql/listTopContactsByTokenUsageOverPeriod.sql @@ -0,0 +1,19 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:fromDate Start of the period (DateTime) +-- @param {DateTime} $3:toDate End of the period (DateTime) +-- @param {Int} $4:limit The maximum number of results to return +SELECT + u.contactId AS id, + c.name AS name, + c.description AS description, + COALESCE(SUM(u.count), 0) AS total +FROM Usage u +LEFT JOIN Contact c ON u.contactId = c.id +WHERE u.userId = ? + AND u.contactId IS NOT NULL + AND u.type LIKE '%_TOKEN' + AND u.createdAt >= ? + AND u.createdAt <= ? +GROUP BY u.contactId, c.name, c.description +ORDER BY total DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listTopDownvotersOverPeriod.sql b/packages/db/prisma/sql/listTopDownvotersOverPeriod.sql new file mode 100644 index 0000000..f37a07c --- /dev/null +++ b/packages/db/prisma/sql/listTopDownvotersOverPeriod.sql @@ -0,0 +1,24 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +-- @param {Int} $4:limit The maximum number of contacts to return +SELECT + c.id, + c.name, + c.description, + c.email, + c.nick, + c.meta, + c.createdAt, + COUNT(CASE WHEN r.value < 0 THEN 1 END) as _countValue, + 'downvote' as _countType +FROM Contact c +LEFT JOIN Rating r ON c.id = r.contactId + AND r.userId = ? + AND r.createdAt >= ? + AND r.createdAt <= ? +WHERE c.userId = r.userId +GROUP BY c.id, c.name, c.description, c.email, c.nick, c.meta, c.createdAt +HAVING _countValue > 0 +ORDER BY _countValue DESC +LIMIT ?; diff --git a/packages/db/prisma/sql/listTopUpvotersOverPeriod.sql b/packages/db/prisma/sql/listTopUpvotersOverPeriod.sql new file mode 100644 index 0000000..4c8ac91 --- /dev/null +++ b/packages/db/prisma/sql/listTopUpvotersOverPeriod.sql @@ -0,0 +1,24 @@ +-- @param {String} $1:userId The ID of the user +-- @param {DateTime} $2:startDate The start date for the period +-- @param {DateTime} $3:endDate The end date for the period +-- @param {Int} $4:limit The maximum number of contacts to return +SELECT + c.id, + c.name, + c.description, + c.email, + c.nick, + c.meta, + c.createdAt, + COUNT(CASE WHEN r.value > 0 THEN 1 END) as _countValue, + 'upvote' as _countType +FROM Contact c +LEFT JOIN Rating r ON c.id = r.contactId + AND r.userId = ? + AND r.createdAt >= ? + AND r.createdAt <= ? +WHERE c.userId = r.userId +GROUP BY c.id, c.name, c.description, c.email, c.nick, c.meta, c.createdAt +HAVING _countValue > 0 +ORDER BY _countValue DESC +LIMIT ?; diff --git a/packages/db/prisma/zod-generator.config.json b/packages/db/prisma/zod-generator.config.json new file mode 100644 index 0000000..eec0afa --- /dev/null +++ b/packages/db/prisma/zod-generator.config.json @@ -0,0 +1,24 @@ +{ + "mode": "custom", + "pureModels": true, + "pureModelsLean": true, + "pureModelsIncludeRelations": false, + "dateTimeStrategy": "date", + "emit": { + "enums": true, + "objects": false, + "crud": false, + "results": false, + "pureModels": true, + "variants": false + }, + "naming": { + "pureModel": { + "filePattern": "{model}.ts", + "schemaSuffix": "Model", + "typeSuffix": "", + "exportNamePattern": "{Model}Model", + "legacyAliases": false + } + } +} diff --git a/packages/db/scripts/derive.js b/packages/db/scripts/derive.js new file mode 100644 index 0000000..3e16f47 --- /dev/null +++ b/packages/db/scripts/derive.js @@ -0,0 +1,16 @@ +// @note pulls this implementation's schema from the spec. The spec does not +// know who implements it - the dependency arrow points this way on purpose. +import { derive, renderSqlite } from '@chatbotkit-dev/db-spec/derive' + +import path from 'node:path' +import url from 'node:url' + +await derive({ + prismaDir: path.join( + path.dirname(url.fileURLToPath(import.meta.url)), + '..', + 'prisma' + ), + + render: renderSqlite, +}) diff --git a/packages/db/scripts/generate.js b/packages/db/scripts/generate.js new file mode 100644 index 0000000..2591d8e --- /dev/null +++ b/packages/db/scripts/generate.js @@ -0,0 +1,51 @@ +/** + * @file generate.js + * + * Generates this package's Prisma client, fully self-contained. + * + * TypedSQL typechecks the queries in prisma/sql against a live database, so a + * throwaway file database is pushed first - which is the whole point of the + * SQLite default: nothing to provision, nothing to reach. + */ +import { execSync } from 'node:child_process' +import fs from 'node:fs/promises' +import { createRequire } from 'node:module' +import path from 'node:path' +import url from 'node:url' + +const ROOT = path.join(path.dirname(url.fileURLToPath(import.meta.url)), '..') + +const run = (command) => + execSync(command, { + cwd: ROOT, + stdio: 'inherit', + + // @note the generators (json, zod, pothos) are resolved off PATH by the + // prisma CLI, so the package's own bin directory has to be on it + env: { + ...process.env, + + // @note generation always runs against its own throwaway database, never + // whatever PRISMA_DATABASE_URL happens to point at + PRISMA_DATABASE_URL: `file:${path.join(ROOT, 'prisma', '.dev.db')}`, + + PATH: `${path.join(ROOT, 'node_modules', '.bin')}:${process.env.PATH}`, + }, + }) + +const databasePath = path.join(ROOT, 'prisma', '.dev.db') + +await fs.rm(databasePath, { force: true }) +await fs.writeFile(databasePath, '') + +run('prisma db push --accept-data-loss') +run('prisma generate --sql') + +// @note the zod generator needs the shared post-processing pass - it fixes the +// generated files (a shadowed `Record` type) and recreates the compatibility +// re-exports. Shared with every db module, so it lives in the spec. +run( + `node ${createRequire(import.meta.url).resolve( + '@chatbotkit-dev/db-spec/scripts/post-generate-zod.js' + )} ${path.join(ROOT, 'prisma')}` +) diff --git a/packages/db/src/constraints.ts b/packages/db/src/constraints.ts new file mode 100644 index 0000000..522a676 --- /dev/null +++ b/packages/db/src/constraints.ts @@ -0,0 +1,48 @@ +/** + * @file constraints.ts + * + * The column size limits this engine imposes, in bytes. + * + * @note SQLite's own, not MySQL's. Deriving strips the native types, so + * `@db.VarChar(191)`, `@db.Text` and `@db.MediumText` are all a plain TEXT + * column here, and SQLite does not enforce a declared VARCHAR length at all. + * What is left is one engine-wide ceiling, `SQLITE_MAX_LENGTH`, which the three + * constants below therefore share. They are deliberately not the numbers the + * MySQL implementation exports - that is the point of each implementation + * declaring its own. + * + * @note bytes, not characters. Callers measure encoded length - a multi-byte + * character costs more than one. + * + * @note the ceiling is a build-time option, not a format limit. 1e9 is the + * default every stock build ships with, including the one better-sqlite3 + * bundles; a build that lowered it would need this file lowered to match. + * + * @note a consequence worth knowing: this database accepts values the MySQL + * implementation would reject, so a row written here need not fit there. Data + * that has to survive the move must be held to the stricter limits by whatever + * moves it - these constants describe the engine, and cannot describe both. + */ + +/** + * `SQLITE_MAX_LENGTH` - the max size of any TEXT or BLOB value. + */ +const SQLITE_MAX_LENGTH = 1000000000 + +/** + * The max length of what the blueprint declares `@db.VarChar(191)`, stored here + * as an unconstrained TEXT column. + */ +export const MAX_DB_STRING_BYTES_LENGTH = SQLITE_MAX_LENGTH + +/** + * The max length of what the blueprint declares `@db.Text`, stored here as an + * unconstrained TEXT column. + */ +export const MAX_DB_TEXT_BYTES_LENGTH = SQLITE_MAX_LENGTH + +/** + * The max length of what the blueprint declares `@db.MediumText`, stored here + * as an unconstrained TEXT column. + */ +export const MAX_DB_MEDIUMTEXT_BYTES_LENGTH = SQLITE_MAX_LENGTH diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts new file mode 100644 index 0000000..399a4e7 --- /dev/null +++ b/packages/db/src/index.ts @@ -0,0 +1,86 @@ +// @note the community database: SQLite in a file. +// +// This package is the proof that the platform's data layer runs somewhere a +// laptop can be: the schema is derived from the blueprint in +// `@chatbotkit-dev/db-spec`, the client is generated against a file database +// this package creates itself, and the 48 analytics queries typecheck and run +// against it unchanged. +// +// It is not the platform's client yet - the shim in `platform/prisma` still +// generates in place. Wiring this in behind that seam is the next step, and +// deliberately a separate one. + +import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' + +import { PrismaClient } from '../prisma/generated/prisma/client' + +export { Prisma, PrismaClient } from '../prisma/generated/prisma/client' + +export * as sql from '../prisma/generated/prisma/sql' + +/** + * @note resolved on first use, never at import - the convention every module + * follows, so importing this package never requires it to be configured. + * + * @throws when the url is missing or not a file: url, naming what to set + */ +function getUrl(): string { + const url = process.env.PRISMA_DATABASE_URL + + if (!url || !url.startsWith('file:')) { + throw new Error( + 'PRISMA_DATABASE_URL must be a file: url for the SQLite database, e.g. file:./data/cbk.db' + ) + } + + return url +} + +/** + * Constructs a raw client. + * + * @note raw on purpose, matching the contract every database module exports: + * the platform's own extensions (audit, cache, retry, methods) are platform + * behaviour and are applied by the platform over this, not baked in here. The + * platform also owns the lifecycle of the one shared instance, which is why + * this constructs rather than caches. + * + * @throws when the url is missing or not a file: url, naming what to set + */ +export function createInstance() { + return new PrismaClient({ + adapter: new PrismaBetterSqlite3({ url: getUrl() }), + }) +} + +let client: PrismaClient | undefined + +export function getClient(): PrismaClient { + if (!client) { + client = createInstance() + } + + return client +} + +/** + * @note exported for tests, which need a fresh client per case. + */ +export function resetClient(): void { + client = undefined +} + +/** + * @throws when the database cannot be opened or queried, naming what to set. + */ +export async function assertConfigured(): Promise { + try { + await getClient().$queryRaw`SELECT 1` + } catch (error) { + throw new Error( + `the SQLite database at ${process.env.PRISMA_DATABASE_URL} could not be opened, so nothing can be stored or read: ${ + error instanceof Error ? error.message : String(error) + }` + ) + } +} diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json new file mode 100644 index 0000000..fa98df2 --- /dev/null +++ b/packages/db/tsconfig.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node" + ], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js", + "./prisma/generated/**/*.ts", + "./scripts/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/debug/README.md b/packages/debug/README.md new file mode 100644 index 0000000..862e22d --- /dev/null +++ b/packages/debug/README.md @@ -0,0 +1,11 @@ +# @chatbotkit-dev/debug + +Namespaced logging, assertions and spans. + +`debug('...').log('key')` emits only when the key is enabled by the active +configuration. The built-in default is driven by the `DEBUG_KEYS`, `WARN_KEYS` +and `ERROR_KEYS` environment variables; a deployment with opinions about its +own subsystems supplies a full `DebugConfig` through `configure()` at boot. +Spans go to [`@chatbotkit-dev/observability`](../observability). + +Extracted from `platform/lib/debug.ts`. diff --git a/packages/debug/jest.config.js b/packages/debug/jest.config.js new file mode 100644 index 0000000..8a86699 --- /dev/null +++ b/packages/debug/jest.config.js @@ -0,0 +1,19 @@ +// @note CommonJS transform: these tests use `jest.mock` hoisting and the `jest` +// global, neither of which is available under the ESM preset. + +export default { + preset: 'ts-jest', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', + + transform: { + '^.+\\.[jt]sx?$': [ + 'ts-jest', + { + useESM: false, + // @note transpile only. Type checking is the `check` script's job. + tsconfig: { module: 'commonjs', esModuleInterop: true, allowJs: true }, + }, + ], + }, +} diff --git a/packages/debug/package.json b/packages/debug/package.json new file mode 100644 index 0000000..f5796ab --- /dev/null +++ b/packages/debug/package.json @@ -0,0 +1,42 @@ +{ + "name": "@chatbotkit-dev/debug", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "jest" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "dependencies": { + "@chatbotkit-dev/env": "workspace:*", + "@chatbotkit-dev/observability": "workspace:*" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/debug/src/index.test.js b/packages/debug/src/index.test.js new file mode 100644 index 0000000..28ffedb --- /dev/null +++ b/packages/debug/src/index.test.js @@ -0,0 +1,187 @@ +import { assert, debug } from './index' + +describe('assert', () => { + it('should not throw an error when the test is true', () => { + expect(() => { + assert(true, 'This should not throw an error') + }).not.toThrow() + }) + + it('should throw an error when the test is false', () => { + expect(() => { + assert(false, 'This should throw an error') + }).toThrow('This should throw an error') + }) +}) + +describe('debug logging', () => { + let consoleSpy + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}) + }) + + afterEach(() => { + consoleSpy.mockRestore() + }) + + it('should serialize nested objects properly in logs', () => { + const nestedObject = { + level1: { + level2: { + level3: { + value: 'test', + }, + }, + }, + } + + debug(nestedObject).log() + + expect(consoleSpy).toHaveBeenCalled() + + const loggedArg = consoleSpy.mock.calls[0][1] + + // should contain the nested value, not [Object] + expect(loggedArg).toContain('test') + expect(loggedArg).toContain('level3') + }) + + it('should limit depth of very deep objects', () => { + // create an object deeper than maxDepth (10 in development) + const deepObject = { + l1: { + l2: { + l3: { + l4: { + l5: { + l6: { + l7: { + l8: { + l9: { + l10: { + l11: { + deep: 'value', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + debug(deepObject).log() + + expect(consoleSpy).toHaveBeenCalled() + + const loggedArg = consoleSpy.mock.calls[0][1] + + // at depth 10, deeper objects should be [Object] + expect(loggedArg).toContain('[Object]') + }) + + it('should handle circular references', () => { + const circularObj = { name: 'test' } + + circularObj.self = circularObj + + debug(circularObj).log() + + expect(consoleSpy).toHaveBeenCalled() + + const loggedArg = consoleSpy.mock.calls[0][1] + + expect(loggedArg).toContain('[Circular]') + expect(loggedArg).toContain('test') + }) + + it('should handle arrays with depth limiting', () => { + const deepArray = { + items: [ + { + l1: { + l2: { + l3: { + l4: { + l5: { + l6: { + l7: { + l8: { + l9: { l10: { tooDeep: 'value' } }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + } + + debug(deepArray).log() + + expect(consoleSpy).toHaveBeenCalled() + + const loggedArg = consoleSpy.mock.calls[0][1] + + // should show [Object] for very deep arrays + expect(loggedArg).toContain('[Object]') + }) + + it('should truncate long strings', () => { + debug({ audio: 'a'.repeat(3000) }).log() + + expect(consoleSpy).toHaveBeenCalled() + + const loggedArg = consoleSpy.mock.calls[0][1] + + expect(loggedArg).toContain('[String(3000)]') + expect(loggedArg.length).toBeLessThan(2500) + }) + + it('should summarize binary data', () => { + debug({ buffer: Buffer.alloc(4096) }).log() + + expect(consoleSpy).toHaveBeenCalled() + + const loggedArg = consoleSpy.mock.calls[0][1] + + expect(loggedArg).toContain('[Buffer(4096)]') + expect(loggedArg).not.toContain('"0":') + }) + + it('should summarize class instances', () => { + class CustomRuntimeObject { + constructor() { + this._events = {} + this._buffer = Buffer.alloc(4096) + } + } + + debug({ socket: new CustomRuntimeObject() }).log() + + expect(consoleSpy).toHaveBeenCalled() + + const loggedArg = consoleSpy.mock.calls[0][1] + + expect(loggedArg).toContain('[CustomRuntimeObject]') + expect(loggedArg).not.toContain('_buffer') + }) + + it('should safely summarize invalid dates', () => { + debug({ createdAt: new Date('invalid') }).log() + + expect(consoleSpy).toHaveBeenCalled() + + const loggedArg = consoleSpy.mock.calls[0][1] + + expect(loggedArg).toContain('Invalid Date') + }) +}) diff --git a/packages/debug/src/index.ts b/packages/debug/src/index.ts new file mode 100644 index 0000000..aff1479 --- /dev/null +++ b/packages/debug/src/index.ts @@ -0,0 +1,635 @@ +import { isDevelopment, isTest } from '@chatbotkit-dev/env' +import observability from '@chatbotkit-dev/observability' + +import { ok } from 'assert' + +/** + * A map of log key patterns to whether they are enabled. Patterns may end in + * `*` to match a namespace, for example `conversation.*`. + */ +export type LogKeys = Record + +export interface DebugConfig { + /** Enables all debug output regardless of key. */ + debug: boolean + + /** Enables all error output regardless of key. */ + error: boolean + + /** Enables all warning output regardless of key. */ + warn: boolean + + log: { + debug: LogKeys + error: LogKeys + warn: LogKeys + } +} + +export type KnownKeys = string + +// @note always stringify objects to ensure consistent logging across +// environments and to properly serialize nested objects (avoids [Object] in +// logs) +const USE_SAFE_STRINGIFY = true + +// @note maximum depth for safeStringify +const DEFAULT_MAX_DEPTH = isDevelopment ? 10 : 5 + +const MAX_STRING_LENGTH = isDevelopment ? 2_048 : 1_024 +const MAX_ARRAY_LENGTH = isDevelopment ? 100 : 50 +const MAX_OBJECT_KEYS = isDevelopment ? 100 : 50 + +// --- sensitive-value redaction --- + +// @note outside development every log line is scrubbed of credential-shaped +// material before it is serialized. In development logs stay verbatim - the +// local email sign-in flow deliberately prints its code to the server log, +// and a developer machine is the one place full values are worth more than +// they cost. Persistence call sites (event metadata and similar) must not +// rely on this gate and should call redact() themselves, which always runs. + +export const REDACTED = '[REDACTED]' + +// key fragments that mark a value as a credential wherever the key appears +const SENSITIVE_KEY_FRAGMENTS = [ + 'auth', + 'cookie', + 'password', + 'passwd', + 'secret', + 'token', + 'credential', + 'signature', + 'apikey', + 'api-key', + 'api_key', + 'privatekey', + 'private-key', + 'private_key', +] + +// keys that contain a sensitive fragment but name a location, shape, or +// bookkeeping fact rather than a value ('tokenUrl', 'accessTokenExpiresAt', +// 'authorName', 'tokenType', 'tokens'/'maxTokens' usage counts) +const SAFE_KEY_SUFFIXES = [ + 'url', + 'uri', + 'endpoint', + 'id', + 'at', + 'name', + 'type', + 'method', + 'count', + 'length', + 'size', + 'hint', + 'mask', + 'kind', + 'mode', + 'tokens', +] + +/** + * Whether a key names credential-shaped material. Case-insensitive; a key + * counts when it contains a sensitive fragment and does not end in a suffix + * that marks it as a location or bookkeeping fact. + */ +export function isSensitiveKey(key: string): boolean { + const normalized = key.toLowerCase() + + if ( + !SENSITIVE_KEY_FRAGMENTS.some((fragment) => normalized.includes(fragment)) + ) { + return false + } + + return !SAFE_KEY_SUFFIXES.some((suffix) => normalized.endsWith(suffix)) +} + +/** + * Scrubs credential-shaped material out of a string regardless of the key it + * sits under: authorization scheme credentials ("Bearer x", "Basic y"), URL + * userinfo passwords, and sensitive query-string parameters (tokens, keys, + * codes, signatures, secrets, state), and slash-delimited configuration + * fields such as custom model credentials. + */ +export function redactString(value: string): string { + return value + .replace( + /\b(bearer|basic|digest)\s+[a-z0-9._~+/=-]+/gi, + (match, scheme) => `${scheme} ${REDACTED}` + ) + .replace(/(\/\/[^/\s@:]+):([^/\s@]+)@/g, `$1:${REDACTED}@`) + .replace( + /([?&](?:key|code|sig|state|[\w-]*(?:token|secret|password|passwd|signature|credential|auth|api[_-]?key)[\w-]*)=)[^&\s"']*/gi, + `$1${REDACTED}` + ) + .replace( + /((?:^|\/)(?:key|code|sig|state|token|secret|password|passwd|passphrase|authorization|auth|credentials?|signature|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|oauth[_-]?token|client[_-]?secret)=)[^/\s"']*/gi, + `$1${REDACTED}` + ) +} + +const MAX_REDACT_DEPTH = 8 + +function redactValue( + value: unknown, + forceAll: boolean, + depth: number +): unknown { + if (typeof value === 'string') { + return forceAll ? REDACTED : redactString(value) + } + + if (typeof value === 'number' || typeof value === 'bigint') { + return forceAll ? REDACTED : value + } + + if (typeof value !== 'object' || value === null) { + return value + } + + // @note beyond this depth the value is passed through untouched - the + // serializer's own depth limit sits lower, so nothing this deep is printed + // in any detail anyway, and the cap keeps cycles finite + + if (depth >= MAX_REDACT_DEPTH) { + return value + } + + if (Array.isArray(value)) { + return value.map((item) => redactValue(item, forceAll, depth + 1)) + } + + const prototype = Object.getPrototypeOf(value) + + if (prototype !== Object.prototype && prototype !== null) { + // @note class instances (Date, Error, Map, buffers) are summarized by the + // serializer rather than walked here + return value + } + + const result: Record = {} + + for (const [k, v] of Object.entries(value)) { + result[k] = redactValue(v, forceAll || isSensitiveKey(k), depth + 1) + } + + return result +} + +/** + * Deeply redacts credential-shaped material from a value: every string or + * number under a sensitive key becomes '[REDACTED]' (shape preserved), and + * every other string is scrubbed with redactString(). Always active - use + * this at persistence boundaries (event metadata, stored records) where the + * development-mode logging exemption must not apply. + */ +export function redact(value: unknown): unknown { + return redactValue(value, false, 0) +} + +// @note test runs can use injected staging credentials, so only interactive +// local development may bypass log redaction + +const REDACT_LOGS = !isDevelopment || isTest + +function prepareLogArg(arg: unknown): unknown { + if (typeof arg === 'string') { + return REDACT_LOGS ? redactString(arg) : arg + } + + return safeStringify(REDACT_LOGS ? redact(arg) : arg, 2) +} + +/** + * Safely stringify an object, handling circular references and limiting depth. + */ +function safeStringify( + obj: unknown, + indent?: number, + maxDepth: number = DEFAULT_MAX_DEPTH +): string { + const seen = new WeakSet() + + function summarizeString(value: string): string { + if (value.length <= MAX_STRING_LENGTH) { + return value + } + + return `${value.slice(0, MAX_STRING_LENGTH)}...[String(${value.length})]` + } + + function summarizeBinary(value: ArrayBufferView): string { + const constructorName = value.constructor?.name || 'ArrayBufferView' + + return `[${constructorName}(${value.byteLength})]` + } + + function isPlainObject(value: object): boolean { + const prototype = Object.getPrototypeOf(value) + + return prototype === Object.prototype || prototype === null + } + + function summarizeObject(value: object): unknown { + if (value instanceof Date) { + return Number.isNaN(value.getTime()) + ? value.toString() + : value.toISOString() + } + + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: summarizeString(value.stack || ''), + } + } + + if (value instanceof RegExp) { + return value.toString() + } + + if (value instanceof Map || value instanceof Set) { + return `[${value.constructor.name}(${value.size})]` + } + + return `[${value.constructor?.name || 'Object'}]` + } + + function processValue(value: unknown, currentDepth: number): unknown { + if (typeof value === 'string') { + return summarizeString(value) + } + + // handle primitives + + if (typeof value !== 'object' || value === null) { + return value + } + + if (ArrayBuffer.isView(value)) { + return summarizeBinary(value) + } + + if (value instanceof ArrayBuffer) { + return `[ArrayBuffer(${value.byteLength})]` + } + + // handle circular references + + if (seen.has(value)) { + return '[Circular]' + } + + seen.add(value) + + // handle max depth + + if (currentDepth >= maxDepth) { + if (Array.isArray(value)) { + return `[Array(${value.length})]` + } + + return '[Object]' + } + + // handle arrays + + if (Array.isArray(value)) { + const result = value + .slice(0, MAX_ARRAY_LENGTH) + .map((item) => processValue(item, currentDepth + 1)) + + if (value.length > MAX_ARRAY_LENGTH) { + result.push(`...[Array(${value.length})]`) + } + + return result + } + + if (!isPlainObject(value)) { + return summarizeObject(value) + } + + // handle objects + + const result: Record = {} + const entries = Object.entries(value) + + for (const [k, v] of entries.slice(0, MAX_OBJECT_KEYS)) { + result[k] = processValue(v, currentDepth + 1) + } + + if (entries.length > MAX_OBJECT_KEYS) { + result.__truncated = `...[Object(${entries.length})]` + } + + return result + } + + const processed = processValue(obj, 0) + + return JSON.stringify(processed, null, indent) +} + +function toKeys(value: string | undefined): LogKeys { + return Object.fromEntries( + (value || '') + .split(',') + .map((key) => key.trim()) + .filter(Boolean) + .map((key) => [key, true]) + ) +} + +// @note the built-in configuration enables nothing by default and is driven +// entirely by the environment. A deployment with opinions about its own +// subsystems supplies its own key map through configure(), typically at boot. + +function defaultConfig(): DebugConfig { + return { + debug: false, + error: false, + warn: false, + + log: { + debug: toKeys(process.env.DEBUG_KEYS), + error: toKeys(process.env.ERROR_KEYS), + warn: toKeys(process.env.WARN_KEYS), + }, + } +} + +type WildcardKeys = Record + +let activeConfig: DebugConfig | null = null +let wildcardKeys: WildcardKeys | null = null + +function getConfig(): DebugConfig { + if (!activeConfig) { + activeConfig = defaultConfig() + } + + return activeConfig +} + +/** + * Replaces the active debug configuration. Key matching is resolved against + * the current configuration on every call, so this can run at any point during + * boot and takes effect for all subsequent logging. + */ +export function configure(config: DebugConfig): void { + activeConfig = config + wildcardKeys = null +} + +function getWildcardKeys(): WildcardKeys { + if (!wildcardKeys) { + wildcardKeys = Object.entries(getConfig().log).reduce( + (acc, [category, value]) => { + if (value.all) { + acc[category] = true + } else { + const keys = Object.entries(value) + .filter(([, e]) => e) + .map(([k]) => (k.endsWith('*') ? k.slice(0, -1) : false)) + .filter((k): k is string => typeof k === 'string') + + acc[category] = keys + } + + return acc + }, + {} as WildcardKeys + ) + } + + return wildcardKeys +} + +function hasKey(key: string, category: keyof DebugConfig['log']): boolean { + const config = getConfig() + const wildcards = getWildcardKeys() + + return ( + config.log[category].all || + config.log[category][key] || + (Array.isArray(wildcards[category]) && + wildcards[category].length > 0 && + (wildcards[category] as string[]).some((k) => key.startsWith(k))) + ) +} + +export interface DebugResult { + log: (key?: KnownKeys) => DebugResult + trace: () => DebugResult +} + +export function print(...args: unknown[]): void { + // eslint-disable-next-line + console.log(...args) +} + +export function log(...args: unknown[]): void { + // eslint-disable-next-line + console.log('*', ...(USE_SAFE_STRINGIFY ? args.map(prepareLogArg) : args)) +} + +export function debuglog(...args: unknown[]): void { + // eslint-disable-next-line + ;(console.debug || console.log)( + ...(USE_SAFE_STRINGIFY ? args.map(prepareLogArg) : args) + ) + + if (process.env.TRACE_DEBUG) { + try { + // eslint-disable-next-line + console.trace?.(...args) + } catch { + // @note for whatever reason it could fail + } + } +} + +/** + * Prints a debug message to the console if the DEBUG environment variable is + * set. + */ +export function debug(...args: unknown[]): DebugResult { + if (!!process.env.DEBUG || getConfig().debug) { + debuglog('*', ...args) + } + + return { + log(key?: KnownKeys): DebugResult { + if (!key || hasKey(key, 'debug')) { + debuglog(key ? `[${key}]` : '*', ...args) + } + + return this + }, + + trace(): DebugResult { + try { + // eslint-disable-next-line + console.trace?.(...args) + } catch { + // @note for whatever reason it could fail + } + + return this + }, + } +} + +export function errorlog(...args: unknown[]): void { + // eslint-disable-next-line + ;(console.error || console.log)( + ...(USE_SAFE_STRINGIFY ? args.map(prepareLogArg) : args) + ) + + if (process.env.TRACE_ERROR) { + try { + // eslint-disable-next-line + console.trace?.(...args) + } catch { + // @note for whatever reason it could fail + } + } +} + +export function error(...args: unknown[]): DebugResult { + errorlog('*', ...args) + + return { + log(key?: KnownKeys): DebugResult { + if (!key || hasKey(key, 'error')) { + errorlog(key ? `[${key}]` : '*', ...args) + } + + return this + }, + + trace(): DebugResult { + try { + // eslint-disable-next-line + console.trace?.(...args) + } catch { + // @note for whatever reason it could fail + } + + return this + }, + } +} + +export function warnlog(...args: unknown[]): void { + // eslint-disable-next-line + ;(console.warn || console.log)( + ...(USE_SAFE_STRINGIFY ? args.map(prepareLogArg) : args) + ) + + if (process.env.TRACE_WARN) { + try { + // eslint-disable-next-line + console.trace?.(...args) + } catch { + // @note for whatever reason it could fail + } + } +} + +export function warn(...args: unknown[]): DebugResult { + if (!!process.env.WARN || getConfig().warn) { + warnlog('*', ...args) + } + + return { + log(key?: KnownKeys): DebugResult { + if (!key || hasKey(key, 'warn')) { + warnlog(key ? `[${key}]` : '*', ...args) + } + + return this + }, + + trace(): DebugResult { + try { + // eslint-disable-next-line + console.trace?.(...args) + } catch { + // @note for whatever reason it could fail + } + + return this + }, + } +} + +export function exit(...args: unknown[]): never { + if (args.length) { + // eslint-disable-next-line + ;(console.error || console.log)(...args) + } + + process.exit(1) +} + +export function assert(test: unknown, message: string): void { + ok(test, message) +} + +export function fassert(test: () => unknown, message: string): void { + ok(test(), message) +} + +/** + * @deprecated + * @throws {Error} Always throws an error indicating unreachable code was reached + */ +export function unreachable(test: never): never { + throw new Error(`Unreachable code reached: ${test}`) +} + +interface SpanOptions { + name: string + op?: string +} + +interface Span { + finish: () => void + setAttribute: (name: string, value: unknown) => void +} + +export function createSpan({ name, op }: SpanOptions): Span { + return observability.startSpan({ name, op }) +} + +export function span(options: { name: string }, fn: () => void): void { + const span = createSpan(options) + + try { + fn() + } finally { + span.finish() + } +} + +export async function spanAsync( + options: { name: string }, + fn: () => Promise +): Promise { + const span = createSpan(options) + + try { + await fn() + } finally { + span.finish() + } +} + +export default debug diff --git a/packages/debug/src/redact.test.js b/packages/debug/src/redact.test.js new file mode 100644 index 0000000..ebe0848 --- /dev/null +++ b/packages/debug/src/redact.test.js @@ -0,0 +1,244 @@ +/** + * Tests for the sensitive-value redaction layer. + * + * The production log gate is exercised by mocking the environment as + * non-development BEFORE the module loads - the gate is computed at import + * time. The redact()/redactString()/isSensitiveKey() primitives are + * environment-independent and are tested directly. + */ + +jest.mock('@chatbotkit-dev/env', () => ({ + isDevelopment: true, + isStaging: false, + isProduction: false, + isTest: true, +})) + +import { REDACTED, isSensitiveKey, log, redact, redactString } from './index' + +describe('isSensitiveKey', () => { + it.each([ + 'authorization', + 'Authorization', + 'proxy-authorization', + 'cookie', + 'set-cookie', + 'password', + 'clientSecret', + 'client_secret', + 'accessToken', + 'access_token', + 'refresh_token', + 'x-api-key', + 'apiKey', + 'x-hub-signature', + 'x-twilio-signature', + 'credentials', + 'privateKey', + 'x-secret-header', + 'oauthToken', + ])('treats %s as sensitive', (key) => { + expect(isSensitiveKey(key)).toBe(true) + }) + + it.each([ + 'tokenUrl', + 'authorizationUrl', + 'accessTokenExpiresAt', + 'tokenType', + 'authorName', + 'method', + 'url', + 'status', + 'email', + 'identifier', + 'body', + 'conversationId', + 'tokenEndpoint', + 'tokens', + 'maxTokens', + 'promptTokens', + ])('treats %s as not sensitive', (key) => { + expect(isSensitiveKey(key)).toBe(false) + }) +}) + +describe('redactString', () => { + it('redacts bearer scheme credentials', () => { + expect(redactString('Bearer sk-abc123.def')).toBe(`Bearer ${REDACTED}`) + expect(redactString('sending Authorization: Bearer eyJx.yy.zz now')).toBe( + `sending Authorization: Bearer ${REDACTED} now` + ) + }) + + it('redacts basic scheme credentials', () => { + expect(redactString('Basic dXNlcjpwYXNz')).toBe(`Basic ${REDACTED}`) + }) + + it('redacts url userinfo passwords', () => { + expect(redactString('https://user:hunter2@example.com/path')).toBe( + `https://user:${REDACTED}@example.com/path` + ) + }) + + it('redacts sensitive query parameters', () => { + expect( + redactString('https://example.com/cb?code=abc123&state=xyz&next=1') + ).toBe(`https://example.com/cb?code=${REDACTED}&state=${REDACTED}&next=1`) + + expect( + redactString('https://example.com/x?access_token=t1&api_key=t2') + ).toBe(`https://example.com/x?access_token=${REDACTED}&api_key=${REDACTED}`) + + expect(redactString('https://s3.example.com/f?sig=AAA&expires=9')).toBe( + `https://s3.example.com/f?sig=${REDACTED}&expires=9` + ) + }) + + it('redacts credentials embedded in slash-delimited model strings', () => { + expect( + redactString( + 'custom/name=gpt-4o/provider=openai/credentials=sk-live-123/maxTokens=1000' + ) + ).toBe( + `custom/name=gpt-4o/provider=openai/credentials=${REDACTED}/maxTokens=1000` + ) + }) + + it('does not redact look-alike parameter names', () => { + expect(redactString('https://example.com/?monkey=1&decode=2')).toBe( + 'https://example.com/?monkey=1&decode=2' + ) + }) + + it('leaves ordinary strings alone', () => { + expect(redactString('fetching the dataset list')).toBe( + 'fetching the dataset list' + ) + }) +}) + +describe('redact', () => { + it('redacts values under sensitive keys and preserves shape', () => { + expect( + redact({ + method: 'POST', + url: 'https://example.com/hook', + headers: { + 'content-type': 'application/json', + authorization: 'Bearer sk-live-123', + 'x-api-key': 'ak-42', + }, + }) + ).toEqual({ + method: 'POST', + url: 'https://example.com/hook', + headers: { + 'content-type': 'application/json', + authorization: REDACTED, + 'x-api-key': REDACTED, + }, + }) + }) + + it('redacts every leaf under a sensitive branch, including arrays', () => { + expect( + redact({ + secrets: [{ value: 'aaa', label: 'first' }, 'raw-token'], + }) + ).toEqual({ + secrets: [{ value: REDACTED, label: REDACTED }, REDACTED], + }) + }) + + it('preserves booleans and null under sensitive keys', () => { + expect(redact({ hasClientSecret: true, refreshToken: null })).toEqual({ + hasClientSecret: true, + refreshToken: null, + }) + }) + + it('redacts numbers under sensitive keys but not elsewhere', () => { + expect(redact({ pinToken: 123456, status: 200 })).toEqual({ + pinToken: REDACTED, + status: 200, + }) + }) + + it('leaves token usage counts alone', () => { + expect( + redact({ tokens: 205, model: 'gpt-5.4-mini', type: 'input', debit: 11 }) + ).toEqual({ tokens: 205, model: 'gpt-5.4-mini', type: 'input', debit: 11 }) + }) + + it('scrubs credential-shaped strings under non-sensitive keys', () => { + expect( + redact({ note: 'call used Bearer abc123', location: 'https://x.y/z' }) + ).toEqual({ + note: `call used Bearer ${REDACTED}`, + location: 'https://x.y/z', + }) + }) + + it('survives circular references', () => { + const value = { name: 'a' } + + value.self = value + + expect(() => redact(value)).not.toThrow() + }) +}) + +describe('non-interactive log output', () => { + it('redacts sensitive keys in logged objects', () => { + const spy = jest.spyOn(console, 'log').mockImplementation(() => {}) + + try { + log('request sent', { + url: 'https://example.com', + headers: { authorization: 'Bearer sk-live-9' }, + }) + + const output = spy.mock.calls.flat().join(' ') + + expect(output).toContain(REDACTED) + expect(output).not.toContain('sk-live-9') + expect(output).toContain('https://example.com') + } finally { + spy.mockRestore() + } + }) + + it('scrubs bare string arguments', () => { + const spy = jest.spyOn(console, 'log').mockImplementation(() => {}) + + try { + log('token was Bearer abc.def.ghi') + + const output = spy.mock.calls.flat().join(' ') + + expect(output).not.toContain('abc.def.ghi') + expect(output).toContain(REDACTED) + } finally { + spy.mockRestore() + } + }) + + it('scrubs model credentials nested under a non-sensitive key', () => { + const spy = jest.spyOn(console, 'log').mockImplementation(() => {}) + + try { + log('parsing language model', { + model: + 'custom/name=gpt-4o/provider=openai/credentials=sk-live-9/maxTokens=1000', + }) + + const output = spy.mock.calls.flat().join(' ') + + expect(output).not.toContain('sk-live-9') + expect(output).toContain(`credentials=${REDACTED}`) + } finally { + spy.mockRestore() + } + }) +}) diff --git a/packages/debug/tsconfig.json b/packages/debug/tsconfig.json new file mode 100644 index 0000000..72b364c --- /dev/null +++ b/packages/debug/tsconfig.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node", + "jest" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/email-spec/README.md b/packages/email-spec/README.md new file mode 100644 index 0000000..b5c3b17 --- /dev/null +++ b/packages/email-spec/README.md @@ -0,0 +1,28 @@ +# @chatbotkit-dev/email-spec + +The **contract** for outbound email. Types only. + +Separate from [`@chatbotkit-dev/email`](../email) because that package is +swappable: a deployment replaces it with its own implementation through a pnpm +override, and an implementation cannot import the package it replaces. + +## The two kinds of mail + +`sendEmailNotification` is the deployment writing to its own user: trial +notices, login links, limit warnings. The sending identity belongs to the +deployment, so the implementation owns the from address and the caller only +chooses where replies go. + +`sendEmailAction` is an agent or integration writing to a third party. The +caller may supply the sending mailbox, because the message comes from an address +the deployment does not necessarily own, and may carry a `messageId` to thread +against inbound mail. + +They are separate functions rather than one function with a flag because they +usually want separate sending domains and separate sending reputations. + +## What is deliberately absent + +Vendor concepts. Tracking flags, suppression groups, list-management bypasses +and sending domains are all decisions an implementation makes per purpose. A +platform caller says what kind of mail it is sending and nothing more. diff --git a/packages/email-spec/package.json b/packages/email-spec/package.json new file mode 100644 index 0000000..eb53b76 --- /dev/null +++ b/packages/email-spec/package.json @@ -0,0 +1,34 @@ +{ + "name": "@chatbotkit-dev/email-spec", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "true" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "eslint": "^9.0.0", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/email-spec/src/index.ts b/packages/email-spec/src/index.ts new file mode 100644 index 0000000..cbb412a --- /dev/null +++ b/packages/email-spec/src/index.ts @@ -0,0 +1,220 @@ +// @note the contract for outbound email. Implementations decide the sending +// identity, the delivery vendor, tracking and suppression behaviour. None of +// that appears here, because none of it is the platform's concern. +// +// There are two kinds of outbound mail and they differ in who the message is +// from, not merely in content: +// +// notification - the deployment writing to its own user. Sent from the +// deployment's identity, so the implementation owns the from +// address and the caller only chooses where replies go. +// +// action - an agent or integration writing to a third party. The +// caller may supply the sending identity, because the message +// is from a mailbox the deployment does not own. +// +// transport - the deployment writing to its own user, but as an identity it +// hosts on someone else's behalf: a whitelabel partner's +// domain, a portal's own host. The caller names that identity +// and gets something it can send through repeatedly. +// +// They are separate entry points rather than a flag because they usually want +// separate sending domains, reputations and often separate vendors - the +// identity a deployment hosts for a partner is verified wherever that partner's +// domain is verified, which need not be where the deployment's own mail goes. + +/** + * A fully rendered message body. Callers render before handing off. + */ +export interface EmailContent { + text: string + html: string +} + +/** + * Mail the deployment sends to its own user. + */ +export interface NotificationEmail { + to: string + subject: string + + /** + * The rendered body, both parts. Callers render before handing off: whether a + * body is markdown, HTML or plain text is something the caller knows and the + * provider should not have to guess. + */ + content: EmailContent + + /** + * Where replies should go, when this particular message needs them to go + * somewhere specific. + * + * @note omit it and the implementation uses its own reply address. Where a + * deployment's notifications should be replied to is the implementation's + * business, not the caller's, so the platform does not carry that address. + */ + replyTo?: string + + /** + * Marks mail the user must receive regardless of their subscription + * preferences: login links, invitations, security notices. + * + * @note this is a property of the message, not a delivery setting. An + * implementation decides what it means - typically suppressing tracking and + * bypassing list management - but the platform only states that the message + * is essential. + */ + essential?: boolean +} + +/** + * Mail an agent or integration sends to a third party. + */ +export interface ActionEmail { + to: string + subject: string + + /** + * The rendered body, both parts. See NotificationEmail.content. + */ + content: EmailContent + + /** + * The sending mailbox. Absent means the implementation supplies its own + * action identity. + */ + from?: string + + /** Display name for the sending mailbox. */ + name?: string + + replyTo?: string + + /** + * Threads the message against an existing conversation, for replies to + * inbound mail. + */ + messageId?: string +} + +/** + * A fully rendered message, ready to deliver. See NotificationEmail.content for + * why both parts are the caller's job. + */ +export interface EmailTransportMessage { + to: string + subject: string + text: string + html: string +} + +/** + * Delivers mail sent as one particular identity, which was fixed when the + * transport was created. + */ +export interface EmailTransport { + send(message: EmailTransportMessage): Promise +} + +/** + * An attachment carried by an inbound message, already extracted from the + * vendor's payload format. + */ +export interface InboundEmailAttachment { + name: string + size: number + type: string + data: ArrayBuffer +} + +/** + * An inbound message addressed to an email integration, normalized out of + * whatever payload format the implementation's inbound vendor delivers. + */ +export interface InboundEmail { + /** + * The integration whose inbox received the message - the implementation + * recognizes its own integration addresses and extracts the id. + */ + integrationId: string + + /** The integration inbox address the message arrived on. */ + to: string + + fromName?: string + fromEmail: string + + subject: string + + text?: string + html?: string + + /** Raw transport headers, for message-id / in-reply-to threading. */ + headers?: string + + senderIp?: string + + attachments: InboundEmailAttachment[] +} + +export interface EmailProvider { + sendEmailNotification(email: NotificationEmail): Promise + sendEmailAction(email: ActionEmail): Promise + + /** + * Composes the inbox address of an email integration. This is sending + * identity, so the implementation owns the address scheme: the hosted + * domain, the routing, and the inbound recognition are all its business. + * Implementations without a hosted domain derive a deterministic address + * from the deployment's site URL. + */ + formatIntegrationInbox(integrationId: string): string + + /** + * Mints a fresh RFC 5322 message-id for mail sent from an integration + * inbox. Sending identity again: the id-right part is the implementation's + * domain. The caller keeps the value for reply threading (replies echo it + * in In-Reply-To), which is why it is minted up front rather than by the + * delivery vendor. + */ + formatIntegrationMessageId(integrationId: string): string + + /** + * Parses an inbound-mail webhook payload into a normalized message, or + * returns null when the payload is not recognized or is not addressed to + * an integration inbox. The payload format belongs to whichever inbound + * vendor the implementation uses - the application never sees it. An + * implementation without inbound delivery logs and returns null. + */ + parseInboundEmail(form: FormData): Promise + + /** + * Creates a transport that sends as `source`, a full RFC 5322 address such as + * `Login `. + * + * @note this is how a deployment sends as an identity it hosts for someone + * else - a whitelabel partner, a portal on its own domain. Neither of the two + * functions above fits: a notification comes from the deployment's own + * identity, and an action comes from a mailbox the deployment does not own at + * all. Here the deployment does own the mail, but not the name on it. + * + * The sending domain has to be verified with whichever vendor the + * implementation delivers through, and which vendor that is - along with the + * credential it needs - is the implementation's business. Callers hold the + * returned transport and call `send`. + */ + createEmailTransport(source: string): EmailTransport + + /** + * Throws when this provider is not usable with the current configuration. + * + * @note an implementation resolves its own configuration lazily, so nothing + * that merely imports it needs that configuration present. This is how a + * deployment gets the guarantee back: it calls this where its environment is + * loaded - in its test suite or at startup - and finds out then rather than + * when a user fails to receive a login link. + * + * An implementation needing no configuration should resolve. + */ + assertConfigured(): Promise +} diff --git a/packages/email-spec/tsconfig.json b/packages/email-spec/tsconfig.json new file mode 100644 index 0000000..f12aa0f --- /dev/null +++ b/packages/email-spec/tsconfig.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2019" + ], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/email/README.md b/packages/email/README.md new file mode 100644 index 0000000..3ff9d97 --- /dev/null +++ b/packages/email/README.md @@ -0,0 +1,23 @@ +# @chatbotkit-dev/email + +The **community email provider**. It does not deliver mail: it writes a line to +the console describing what would have been sent, so a deployment runs and is +observable without an email vendor configured. + +The message body is deliberately never logged. Notification mail routinely +carries login links, and action mail carries conversation content. + +## Providing your own + +Replace this package at install time with one satisfying +[`@chatbotkit-dev/email-spec`](../email-spec): + +```yaml +# Root pnpm-workspace.yaml +overrides: + '@chatbotkit-dev/email': npm:your-email-implementation@* +``` + +An implementation owns the sending identity, the delivery vendor, tracking and +suppression. See the spec package for why notification and action mail are +separate functions. diff --git a/packages/email/jest.config.js b/packages/email/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/email/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/email/package.json b/packages/email/package.json new file mode 100644 index 0000000..6c2b057 --- /dev/null +++ b/packages/email/package.json @@ -0,0 +1,42 @@ +{ + "name": "@chatbotkit-dev/email", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + }, + "dependencies": { + "@chatbotkit-dev/email-spec": "workspace:*", + "@types/node": "^24.0.0" + } +} diff --git a/packages/email/src/index.test.js b/packages/email/src/index.test.js new file mode 100644 index 0000000..13ce64f --- /dev/null +++ b/packages/email/src/index.test.js @@ -0,0 +1,67 @@ +import provider, { sendEmailAction, sendEmailNotification } from './index' + +describe('community email provider', () => { + let logged + + // eslint-disable-next-line no-console + const original = console.log + + beforeEach(() => { + logged = [] + + // eslint-disable-next-line no-console + console.log = (...args) => { + logged.push(args.map(String).join(' ')) + } + }) + + afterEach(() => { + // eslint-disable-next-line no-console + console.log = original + }) + + it('satisfies the provider contract', () => { + expect(typeof provider.sendEmailNotification).toBe('function') + expect(typeof provider.sendEmailAction).toBe('function') + }) + + it('reports a notification without delivering it', async () => { + await sendEmailNotification({ + to: 'user@example.com', + subject: 'Your trial has started', + content: { text: 'hello', html: '

hello

' }, + }) + + expect(logged).toHaveLength(1) + expect(logged[0]).toContain('[email:notification]') + expect(logged[0]).toContain('user@example.com') + }) + + it('reports an action without delivering it', async () => { + await sendEmailAction({ + to: 'third@example.com', + subject: 'Re: your enquiry', + content: { text: 'hello', html: '

hello

' }, + from: 'agent@example.com', + }) + + expect(logged).toHaveLength(1) + expect(logged[0]).toContain('[email:action]') + }) + + it('logs the text body framed as mail, which is the only delivery there is', async () => { + await sendEmailNotification({ + to: 'user@example.com', + subject: 'Sign in', + content: { + text: 'your code:\n123456', + html: '123456', + }, + }) + + expect(logged.join('\n')).toContain('│ your code:') + expect(logged.join('\n')).toContain('│ 123456') + expect(logged.join('\n')).toContain('┌') + expect(logged.join('\n')).toContain('└') + }) +}) diff --git a/packages/email/src/index.ts b/packages/email/src/index.ts new file mode 100644 index 0000000..a5c7f7f --- /dev/null +++ b/packages/email/src/index.ts @@ -0,0 +1,112 @@ +import type { + ActionEmail, + EmailProvider, + EmailTransport, + InboundEmail, + NotificationEmail, +} from '@chatbotkit-dev/email-spec' + +export type * from '@chatbotkit-dev/email-spec' + +// @note the community implementation does not deliver mail. It writes what it +// would have sent to the console, text body included, so a deployment runs and +// stays usable without an email vendor configured - the console IS delivery +// here: sign-in codes and invitations reach the operator nowhere else. Replace +// this package to deliver for real (and to keep bodies out of logs). + +// @note the body is framed with an open left rail rather than a closed box: +// every line stands alone, so long URLs never break the frame and per-line +// log timestamps do not mangle it +function describe( + kind: string, + email: { to: string; subject: string }, + text: string +): void { + const rule = '─'.repeat(50) + + const body = text + .trim() + .split('\n') + .map((line) => `│ ${line}`) + .join('\n') + + // eslint-disable-next-line no-console + console.log( + `[email:${kind}] to=${email.to} subject=${JSON.stringify(email.subject)} (not delivered: no email provider configured)\n┌${rule}\n${body}\n└${rule}` + ) +} + +export async function sendEmailNotification( + email: NotificationEmail +): Promise { + describe('notification', email, email.content.text) +} + +export async function sendEmailAction(email: ActionEmail): Promise { + describe('action', email, email.content.text) +} + +/** + * @note the community implementation delivers nothing, so a transport is the + * same console line as anything else - with the identity it would have sent as, + * because that is the whole point of asking for one. + */ +export function createEmailTransport(source: string): EmailTransport { + return { + async send({ to, subject, text }) { + describe(`transport from=${source}`, { to, subject }, text) + }, + } +} + +/** + * @note the community provider needs no configuration, so there is nothing that + * can be misconfigured. + */ +export async function assertConfigured(): Promise { + // pass +} + +// @note the community implementation hosts no sending domain, so integration +// inboxes derive deterministically from the deployment's site URL - the +// address scheme an operator routes when they configure real email +function integrationHostname(): string { + try { + return new URL(process.env.SITE_URL ?? '').hostname || 'localhost' + } catch { + return 'localhost' + } +} + +export function formatIntegrationInbox(integrationId: string): string { + return `${integrationId}@integration.${integrationHostname()}` +} + +export function formatIntegrationMessageId(_integrationId: string): string { + return `<${crypto.randomUUID()}@integration.${integrationHostname()}>` +} + +// @note no inbound vendor means no inbound mail - describe and decline, the +// same posture as outbound delivery above +export async function parseInboundEmail( + _form: FormData +): Promise { + // eslint-disable-next-line no-console + console.log( + '[email:inbound] inbound message ignored (not parsed: no email provider configured)' + ) + + return null +} + +const provider: EmailProvider = { + sendEmailNotification, + sendEmailAction, + createEmailTransport, + assertConfigured, + formatIntegrationInbox, + formatIntegrationMessageId, + parseInboundEmail, +} + +export default provider diff --git a/packages/email/tsconfig.json b/packages/email/tsconfig.json new file mode 100644 index 0000000..53607f2 --- /dev/null +++ b/packages/email/tsconfig.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "noEmit": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "types": [ + "node" + ], + "lib": [ + "DOM", + "DOM.Iterable", + "ES2021" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "composite": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/encoding/README.md b/packages/encoding/README.md new file mode 100644 index 0000000..a99d948 --- /dev/null +++ b/packages/encoding/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/encoding diff --git a/packages/encoding/jest.config.js b/packages/encoding/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/encoding/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/encoding/package.json b/packages/encoding/package.json new file mode 100644 index 0000000..9b8cb8f --- /dev/null +++ b/packages/encoding/package.json @@ -0,0 +1,35 @@ +{ + "name": "@chatbotkit-dev/encoding", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/encoding/src/index.test.ts b/packages/encoding/src/index.test.ts new file mode 100644 index 0000000..d7178ff --- /dev/null +++ b/packages/encoding/src/index.test.ts @@ -0,0 +1,27 @@ +import { getEncoding } from './index' + +describe('getEncoding', () => { + it('should detect empty buffer', () => { + const buffer = new Uint8Array([]) + + expect(getEncoding(buffer)).toBe('utf8') + }) + + it('should detect buffer with space', () => { + const buffer = new Uint8Array([0x20]) // Space character + + expect(getEncoding(buffer)).toBe('utf8') + }) + + it('should detect utf8', () => { + const buffer = new Uint8Array([0xe2, 0x82, 0xac]) // € (euro sign) + + expect(getEncoding(buffer)).toBe('utf8') + }) + + it('should detect binary', () => { + const buffer = new Uint8Array([0xff, 0x00, 0x01]) // Non-UTF8 bytes + + expect(getEncoding(buffer)).toBe('binary') + }) +}) diff --git a/packages/encoding/src/index.ts b/packages/encoding/src/index.ts new file mode 100644 index 0000000..2469d4b --- /dev/null +++ b/packages/encoding/src/index.ts @@ -0,0 +1,200 @@ +// @note copied from https://github.com/bevry/istextorbinary because it does not +// compiled correctly + +export interface EncodingOpts { + /** Defaults to 24 */ + chunkLength?: number + + /** If not provided, will check the start, beginning, and end */ + chunkBegin?: number +} + +function getChunkBegin(buf: Uint8Array, chunkBegin: number) { + // If it's the beginning, just return. + if (chunkBegin === 0) { + return 0 + } + + if (!isLaterByteOfUtf8(buf[chunkBegin])) { + return chunkBegin + } + + let begin = chunkBegin - 3 + + if (begin >= 0) { + if (isFirstByteOf4ByteChar(buf[begin])) { + return begin + } + } + + begin = chunkBegin - 2 + + if (begin >= 0) { + if ( + isFirstByteOf4ByteChar(buf[begin]) || + isFirstByteOf3ByteChar(buf[begin]) + ) { + return begin + } + } + + begin = chunkBegin - 1 + + if (begin >= 0) { + // Is it a 4-byte, 3-byte utf8 character? + if ( + isFirstByteOf4ByteChar(buf[begin]) || + isFirstByteOf3ByteChar(buf[begin]) || + isFirstByteOf2ByteChar(buf[begin]) + ) { + return begin + } + } + + return -1 +} + +function getChunkEnd(buf: Uint8Array, chunkEnd: number) { + // If it's the end, just return. + if (chunkEnd === buf.byteLength) { + return chunkEnd + } + + let index = chunkEnd - 3 + + if (index >= 0) { + if (isFirstByteOf4ByteChar(buf[index])) { + return chunkEnd + 1 + } + } + + index = chunkEnd - 2 + + if (index >= 0) { + if (isFirstByteOf4ByteChar(buf[index])) { + return chunkEnd + 2 + } + + if (isFirstByteOf3ByteChar(buf[index])) { + return chunkEnd + 1 + } + } + + index = chunkEnd - 1 + + if (index >= 0) { + if (isFirstByteOf4ByteChar(buf[index])) { + return chunkEnd + 3 + } + + if (isFirstByteOf3ByteChar(buf[index])) { + return chunkEnd + 2 + } + + if (isFirstByteOf2ByteChar(buf[index])) { + return chunkEnd + 1 + } + } + + return chunkEnd +} + +function isFirstByteOf4ByteChar(byte: number) { + // eslint-disable-next-line no-bitwise + return byte >> 3 === 30 // 11110xxx? +} + +function isFirstByteOf3ByteChar(byte: number) { + // eslint-disable-next-line no-bitwise + return byte >> 4 === 14 // 1110xxxx? +} + +function isFirstByteOf2ByteChar(byte: number) { + // eslint-disable-next-line no-bitwise + return byte >> 5 === 6 // 110xxxxx? +} + +function isLaterByteOfUtf8(byte: number) { + // eslint-disable-next-line no-bitwise + return byte >> 6 === 2 // 10xxxxxx? +} + +export function getEncoding( + buffer: Uint8Array | null, + opts?: EncodingOpts +): 'utf8' | 'binary' | null { + // Check + if (!buffer) { + return null + } + + // Prepare + const textEncoding = 'utf8' + const binaryEncoding = 'binary' + const chunkLength = opts?.chunkLength ?? 24 + let chunkBegin = opts?.chunkBegin ?? 0 + + // Discover + if (opts?.chunkBegin == null) { + // Start + let encoding = getEncoding(buffer, { chunkLength, chunkBegin }) + + if (encoding === textEncoding) { + // Middle + chunkBegin = Math.max(0, Math.floor(buffer.byteLength / 2) - chunkLength) + encoding = getEncoding(buffer, { + chunkLength, + chunkBegin, + }) + + if (encoding === textEncoding) { + // End + chunkBegin = Math.max(0, buffer.byteLength - chunkLength) + encoding = getEncoding(buffer, { + chunkLength, + chunkBegin, + }) + } + } + + // Return + return encoding + } else { + // Extract + chunkBegin = getChunkBegin(buffer, chunkBegin) + + if (chunkBegin === -1) { + return binaryEncoding + } + + const chunkEnd = getChunkEnd( + buffer, + Math.min(buffer.byteLength, chunkBegin + chunkLength) + ) + + if (chunkEnd > buffer.byteLength) { + return binaryEncoding + } + + const decoder = new TextDecoder(textEncoding, { + fatal: false, + }) + + const contentChunkUTF8 = decoder.decode(buffer).slice(chunkBegin, chunkEnd) + + // Detect encoding + for (let i = 0; i < contentChunkUTF8.length; ++i) { + const charCode = contentChunkUTF8.charCodeAt(i) + + if (charCode === 65533 || charCode <= 8) { + // 8 and below are control characters (e.g. backspace, null, eof, etc.) + // 65533 is the unknown character + // console.log(charCode, contentChunkUTF8[i]) + return binaryEncoding + } + } + + // Return + return textEncoding + } +} diff --git a/packages/encoding/tsconfig.json b/packages/encoding/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/encoding/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/env/README.md b/packages/env/README.md new file mode 100644 index 0000000..68458c1 --- /dev/null +++ b/packages/env/README.md @@ -0,0 +1,8 @@ +# @chatbotkit-dev/env + +Which environment the process is running in. + +Exports `isDevelopment`, `isStaging`, `isProduction` and `isTest`, resolved once +from the provider-neutral `NODE_ENV` and `TARGET_ENV` variables. + +Extracted from `platform/lib/env.ts`. Zero dependencies. diff --git a/packages/env/jest.config.js b/packages/env/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/env/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/env/package.json b/packages/env/package.json new file mode 100644 index 0000000..3011a84 --- /dev/null +++ b/packages/env/package.json @@ -0,0 +1,38 @@ +{ + "name": "@chatbotkit-dev/env", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/env/src/index.test.js b/packages/env/src/index.test.js new file mode 100644 index 0000000..ca9f7ca --- /dev/null +++ b/packages/env/src/index.test.js @@ -0,0 +1,94 @@ +import { jest } from '@jest/globals' + +const ENV_KEYS = [ + 'NODE_ENV', + 'TARGET_ENV', + 'VERCEL_ENV', + 'VERCEL_URL', + 'NEXT_PUBLIC_VERCEL_URL', +] + +function setEnv(name, value) { + if (value === undefined) { + delete process.env[name] + + return + } + + process.env[name] = value +} + +async function withEnvironment(environment, fn) { + const previousEnv = Object.fromEntries( + ENV_KEYS.map((name) => [name, process.env[name]]) + ) + + try { + for (const name of ENV_KEYS) { + setEnv(name, environment[name]) + } + + jest.resetModules() + + await jest.isolateModulesAsync(async () => { + await fn(await import('./index')) + }) + } finally { + for (const [name, value] of Object.entries(previousEnv)) { + setEnv(name, value) + } + + jest.resetModules() + } +} + +describe('environment identity', () => { + it('identifies test from NODE_ENV', async () => { + await withEnvironment({ NODE_ENV: 'test' }, async (environment) => { + expect(environment.isTest).toBe(true) + expect(environment.isDevelopment).toBe(true) + expect(environment.isStaging).toBe(false) + expect(environment.isProduction).toBe(false) + }) + }) + + it('identifies staging from TARGET_ENV', async () => { + await withEnvironment( + { NODE_ENV: 'production', TARGET_ENV: 'staging' }, + async (environment) => { + expect(environment.isTest).toBe(false) + expect(environment.isDevelopment).toBe(false) + expect(environment.isStaging).toBe(true) + expect(environment.isProduction).toBe(false) + } + ) + }) + + it('identifies production when TARGET_ENV is absent', async () => { + await withEnvironment( + { NODE_ENV: 'production' }, + async (environment) => { + expect(environment.isTest).toBe(false) + expect(environment.isDevelopment).toBe(false) + expect(environment.isStaging).toBe(false) + expect(environment.isProduction).toBe(true) + } + ) + }) + + it('does not derive environment identity from Vercel variables', async () => { + await withEnvironment( + { + NODE_ENV: 'production', + VERCEL_ENV: 'preview', + VERCEL_URL: 'preview.example.com', + }, + async (environment) => { + expect(environment.isStaging).toBe(false) + expect(environment.isProduction).toBe(true) + expect(environment).not.toHaveProperty('isOnVercel') + expect(environment).not.toHaveProperty('isOnVercelPreview') + } + ) + }) +}) diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts new file mode 100644 index 0000000..f345541 --- /dev/null +++ b/packages/env/src/index.ts @@ -0,0 +1,27 @@ +import { ok } from 'assert' + +export const isTest: boolean = + process.env.NODE_ENV === 'test' || process.env.TARGET_ENV === 'test' + +export const isDevelopment: boolean = + isTest || + process.env.NODE_ENV === 'development' || + process.env.TARGET_ENV === 'development' + +export const isStaging: boolean = + !isTest && + process.env.NODE_ENV === 'production' && + process.env.TARGET_ENV === 'staging' + +export const isProduction: boolean = + !isTest && + process.env.NODE_ENV === 'production' && + (process.env.TARGET_ENV === 'production' || !process.env.TARGET_ENV) + +/** + * Inline assertions to ensure the environment is correctly set. + */ +ok(isDevelopment || isStaging || isProduction, 'unknown environment') +ok(!(isDevelopment && isStaging), 'multiple environments detected') +ok(!(isDevelopment && isProduction), 'multiple environments detected') +ok(!(isStaging && isProduction), 'multiple environments detected') diff --git a/packages/env/tsconfig.json b/packages/env/tsconfig.json new file mode 100644 index 0000000..72b364c --- /dev/null +++ b/packages/env/tsconfig.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node", + "jest" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/errors/README.md b/packages/errors/README.md new file mode 100644 index 0000000..0adea55 --- /dev/null +++ b/packages/errors/README.md @@ -0,0 +1,12 @@ +# @chatbotkit-dev/errors + +The platform's error taxonomy. + +`SystemError` and its subclasses, and the reporting helpers that decide which errors are worth +reporting. The distinction matters: `captureException` filters on this taxonomy, so an error that +does not extend it is classified differently. + +Reports through [`@chatbotkit-dev/observability`](../observability), so it does not know which error +tracker, if any, a deployment runs. + +Extracted from `platform/lib/error.js`. diff --git a/packages/errors/jest.config.js b/packages/errors/jest.config.js new file mode 100644 index 0000000..8a86699 --- /dev/null +++ b/packages/errors/jest.config.js @@ -0,0 +1,19 @@ +// @note CommonJS transform: these tests use `jest.mock` hoisting and the `jest` +// global, neither of which is available under the ESM preset. + +export default { + preset: 'ts-jest', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', + + transform: { + '^.+\\.[jt]sx?$': [ + 'ts-jest', + { + useESM: false, + // @note transpile only. Type checking is the `check` script's job. + tsconfig: { module: 'commonjs', esModuleInterop: true, allowJs: true }, + }, + ], + }, +} diff --git a/packages/errors/package.json b/packages/errors/package.json new file mode 100644 index 0000000..6ee0818 --- /dev/null +++ b/packages/errors/package.json @@ -0,0 +1,42 @@ +{ + "name": "@chatbotkit-dev/errors", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "jest" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "dependencies": { + "@chatbotkit-dev/observability": "workspace:*", + "joi": "^17.7.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/errors/src/index.test.js b/packages/errors/src/index.test.js new file mode 100644 index 0000000..eea8d3e --- /dev/null +++ b/packages/errors/src/index.test.js @@ -0,0 +1,904 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +import { UnexpectedStateError, captureUnexpectedState } from './index' + +jest.mock('@chatbotkit-dev/observability', () => ({ + __esModule: true, + + default: { + captureException: jest.fn(), + captureMessage: jest.fn(), + setTag: jest.fn(), + }, +})) + +describe('UnexpectedStateError', () => { + it('should create error with message only', () => { + const message = 'Test error message' + const error = new UnexpectedStateError(message) + + expect(error.message).toBe(message) + expect(error.data).toBeUndefined() + expect(error).toBeInstanceOf(Error) + expect(error).toBeInstanceOf(UnexpectedStateError) + }) + + it('should create error with message and context', () => { + const message = 'Test error message' + const context = { key: 'value' } + const error = new UnexpectedStateError(message, context) + + expect(error.message).toBe(message) + expect(error.data).toEqual(context) + expect(error).toBeInstanceOf(Error) + expect(error).toBeInstanceOf(UnexpectedStateError) + }) + + it('should handle null context', () => { + const message = 'Test error message' + const error = new UnexpectedStateError(message, null) + + expect(error.message).toBe(message) + expect(error.data).toBeUndefined() + }) + + it('should handle undefined context', () => { + const message = 'Test error message' + const error = new UnexpectedStateError(message, undefined) + + expect(error.message).toBe(message) + expect(error.data).toBeUndefined() + }) + + it('should handle string context', () => { + const message = 'Test error message' + const context = 'string context' + const error = new UnexpectedStateError(message, context) + + expect(error.message).toBe(message) + expect(error.data).toEqual(context) + }) + + it('should handle complex object context', () => { + const message = 'Test error message' + + const context = { + module: 'test.js', + function: 'testFunction', + details: { key: 'value', nested: { prop: 'test' } }, + } + + const error = new UnexpectedStateError(message, context) + + expect(error.message).toBe(message) + expect(error.data).toEqual(context) + expect(error.data.module).toBe('test.js') + expect(error.data.function).toBe('testFunction') + expect(error.data.details.nested.prop).toBe('test') + }) +}) + +describe('captureUnexpectedState observability integration', () => { + const observability = jest.requireMock('@chatbotkit-dev/observability') + .default + const mockCaptureException = jest.mocked(observability.captureException) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should send context data to the reporter when provided', async () => { + const message = 'Test error with context' + + const context = { + userId: '123', + action: 'testAction', + requestId: 'req-456', + } + + await captureUnexpectedState(message, context) + + expect(mockCaptureException).toHaveBeenCalledTimes(1) + + const [error, reportedContext] = mockCaptureException.mock.calls[0] + + // verify the error is an UnexpectedStateError with correct message + + expect(error).toBeInstanceOf(UnexpectedStateError) + expect(error.message).toBe(message) + expect(error.data).toEqual(context) + + // verify the context is passed to the reporter + + expect(reportedContext).toEqual(context) + }) + + it('should send string context to the reporter', async () => { + const message = 'Test error with string context' + const context = 'debug information string' + + await captureUnexpectedState(message, context) + + expect(mockCaptureException).toHaveBeenCalledTimes(1) + + const [error, reportedContext] = mockCaptureException.mock.calls[0] + + expect(error).toBeInstanceOf(UnexpectedStateError) + expect(error.message).toBe(message) + expect(error.data).toBe(context) + expect(reportedContext).toBe(context) + }) + + it('should send complex nested context to the reporter', async () => { + const message = 'Test error with complex context' + + const context = { + request: { + method: 'POST', + url: '/api/test', + headers: { 'content-type': 'application/json' }, + body: { field: 'value' }, + }, + user: { + id: 'user-123', + email: 'test@example.com', + }, + metadata: { + timestamp: '2025-08-13T10:00:00Z', + version: '1.0.0', + }, + } + + await captureUnexpectedState(message, context) + + expect(mockCaptureException).toHaveBeenCalledTimes(1) + + const [error, reportedContext] = mockCaptureException.mock.calls[0] + + expect(error).toBeInstanceOf(UnexpectedStateError) + expect(error.message).toBe(message) + expect(error.data).toEqual(context) + expect(reportedContext).toEqual(context) + + // verify nested structure is preserved + + expect(reportedContext.request.method).toBe('POST') + expect(reportedContext.user.id).toBe('user-123') + expect(reportedContext.metadata.version).toBe('1.0.0') + }) + + it('should send undefined to the reporter when no context provided', async () => { + const message = 'Test error without context' + + await captureUnexpectedState(message) + + expect(mockCaptureException).toHaveBeenCalledTimes(1) + + const [error, reportedContext] = mockCaptureException.mock.calls[0] + + expect(error).toBeInstanceOf(UnexpectedStateError) + expect(error.message).toBe(message) + expect(error.data).toBeUndefined() + expect(reportedContext).toBeUndefined() + }) + + it('should send undefined to the reporter when null context provided', async () => { + const message = 'Test error with null context' + + await captureUnexpectedState(message, null) + + expect(mockCaptureException).toHaveBeenCalledTimes(1) + + const [error, reportedContext] = mockCaptureException.mock.calls[0] + + expect(error).toBeInstanceOf(UnexpectedStateError) + expect(error.message).toBe(message) + expect(error.data).toBeUndefined() + expect(reportedContext).toBeUndefined() + }) + + it('should send undefined to the reporter when undefined context provided', async () => { + const message = 'Test error with undefined context' + + await captureUnexpectedState(message, undefined) + + expect(mockCaptureException).toHaveBeenCalledTimes(1) + + const [error, reportedContext] = mockCaptureException.mock.calls[0] + + expect(error).toBeInstanceOf(UnexpectedStateError) + expect(error.message).toBe(message) + expect(error.data).toBeUndefined() + expect(reportedContext).toBeUndefined() + }) + + it('should handle reporter errors gracefully', async () => { + const message = 'Test error with the reporter failure' + const context = { key: 'value' } + + // mock the reporter to throw an error + + mockCaptureException.mockRejectedValueOnce( + new Error('the reporter service unavailable') + ) + + // this should not throw even if the reporter fails + + await expect( + captureUnexpectedState(message, context) + ).resolves.toBeUndefined() + + expect(mockCaptureException).toHaveBeenCalledTimes(1) + }) +}) + +describe('captureUnexpectedState backward compatibility', () => { + it('should accept message-only calls without throwing', async () => { + // this should not throw an error + + await expect( + captureUnexpectedState('Test message') + ).resolves.toBeUndefined() + }) + + it('should accept message and context calls without throwing', async () => { + // this should not throw an error + + await expect( + captureUnexpectedState('Test message', { key: 'value' }) + ).resolves.toBeUndefined() + }) + + it('should accept message and string context calls without throwing', async () => { + // this should not throw an error + + await expect( + captureUnexpectedState('Test message', 'string context') + ).resolves.toBeUndefined() + }) + + it('should accept message and null context calls without throwing', async () => { + // this should not throw an error + + await expect( + captureUnexpectedState('Test message', null) + ).resolves.toBeUndefined() + }) + + it('should accept message and undefined context calls without throwing', async () => { + // this should not throw an error + + await expect( + captureUnexpectedState('Test message', undefined) + ).resolves.toBeUndefined() + }) +}) + +describe('captureException excluded errors', () => { + const observability = jest.requireMock('@chatbotkit-dev/observability') + .default + const mockCaptureException = jest.mocked(observability.captureException) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should not send BotInputError to the reporter', async () => { + const { BotInputError, captureException } = await import('./index') + + const error = new BotInputError( + 'Invalid input for ability "test": missing required field' + ) + + await captureException(error) + + // @note BotInputError should be excluded from the reporter logging + expect(mockCaptureException).not.toHaveBeenCalled() + }) + + it('should not send UserInputError to the reporter', async () => { + const { UserInputError, captureException } = await import('./index') + + const error = new UserInputError('Invalid user input') + + await captureException(error) + + expect(mockCaptureException).not.toHaveBeenCalled() + }) + + it('should not send UserAuthError to the reporter', async () => { + const { UserAuthError, captureException } = await import('./index') + + const error = new UserAuthError('Authentication failed') + + await captureException(error) + + expect(mockCaptureException).not.toHaveBeenCalled() + }) + + it('should send other errors to the reporter', async () => { + const { captureException } = await import('./index') + + const error = new Error('Some other error') + + await captureException(error) + + expect(mockCaptureException).toHaveBeenCalledWith(error, undefined) + }) + + it('should not send ObservationError to the reporter', async () => { + const { ObservationError, captureException } = await import('./index') + + const error = new ObservationError( + 'skillset action returned large response', + { tokenCount: 150000 } + ) + + await captureException(error) + + // @note ObservationError should be excluded from the reporter logging + expect(mockCaptureException).not.toHaveBeenCalled() + }) + + it('should not send ContentModerationError to the reporter', async () => { + const { ContentModerationError, captureException } = await import('./index') + + const error = new ContentModerationError('Inappropriate content (400)') + + await captureException(error) + + // @note content moderation rejections are expected provider behaviour + expect(mockCaptureException).not.toHaveBeenCalled() + }) + + it('should not send UnexpectedStateError via captureObservation to the reporter', async () => { + const { captureObservation } = await import('./index') + + await captureObservation('Chunked handler aborted', { runCount: 3 }) + + // @note captureObservation creates ObservationError which should not go to the reporter + expect(mockCaptureException).not.toHaveBeenCalled() + }) +}) + +describe('captureObservation the reporter opt-in', () => { + const observability = jest.requireMock('@chatbotkit-dev/observability') + .default + const mockCaptureException = jest.mocked(observability.captureException) + const mockCaptureMessage = jest.mocked(observability.captureMessage) + + beforeEach(() => { + jest.clearAllMocks() + jest.spyOn(console, 'log').mockImplementation() + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('reports an opted-in observation to the reporter as a message, not an exception', async () => { + const { captureObservation } = await import('./index') + + const context = { + event: 'runaway_text_run_detected', + repeatedPhrase: 'let me call lint', + repeatCount: 6, + } + + await captureObservation('runaway text run detected', context, { + sentry: true, + level: 'warning', + }) + + expect(mockCaptureException).not.toHaveBeenCalled() + expect(mockCaptureMessage).toHaveBeenCalledTimes(1) + + const [message, captureContext] = mockCaptureMessage.mock.calls[0] + + expect(message).toBe('runaway text run detected') + expect(captureContext.level).toBe('warning') + expect(captureContext.extra).toEqual(context) + // @note grouped by the observation event so related stuck runs cluster + expect(captureContext.tags.observation).toBe('runaway_text_run_detected') + expect(captureContext.fingerprint).toEqual([ + 'observation', + 'runaway_text_run_detected', + ]) + }) + + it('defaults the the reporter level to warning when not specified', async () => { + const { captureObservation } = await import('./index') + + await captureObservation('call limit max reached', { event: 'x' }, { + sentry: true, + }) + + expect(mockCaptureMessage.mock.calls[0][1].level).toBe('warning') + }) + + it('keeps non-opted-in observations out of the reporter entirely', async () => { + const { captureObservation } = await import('./index') + + await captureObservation('Slow function call', { ms: 1200 }) + + expect(mockCaptureException).not.toHaveBeenCalled() + expect(mockCaptureMessage).not.toHaveBeenCalled() + }) +}) + +describe('captureError console.trace behavior', () => { + let consoleTraceSpy + + beforeEach(() => { + consoleTraceSpy = jest.spyOn(console, 'trace').mockImplementation() + jest.spyOn(console, 'error').mockImplementation() + }) + + afterEach(() => { + consoleTraceSpy.mockRestore() + jest.restoreAllMocks() + }) + + it('should call console.trace when error has stack', async () => { + const { captureError } = await import('./index') + + const error = new Error('Test error with stack') + + await captureError(error) + + expect(consoleTraceSpy).toHaveBeenCalledWith(error) + }) + + it('should not call console.trace when error lacks stack', async () => { + const { captureError } = await import('./index') + + const error = { message: 'Error without stack' } + + await captureError(error) + + expect(consoleTraceSpy).not.toHaveBeenCalled() + }) + + it('should not call console.trace for undefined error', async () => { + const { captureError } = await import('./index') + + await captureError(undefined) + + expect(consoleTraceSpy).not.toHaveBeenCalled() + }) + + it('should not call console.trace for null error', async () => { + const { captureError } = await import('./index') + + await captureError(null) + + expect(consoleTraceSpy).not.toHaveBeenCalled() + }) + + it('should not call console.trace for ObservationError', async () => { + const { captureError, ObservationError } = await import('./index') + + const error = new ObservationError('Test observation', { data: 'test' }) + + await captureError(error) + + // @note ObservationError should not reach console.trace as it's filtered out + expect(consoleTraceSpy).not.toHaveBeenCalled() + }) +}) + +describe('captureException console.trace behavior', () => { + let consoleTraceSpy + + beforeEach(() => { + consoleTraceSpy = jest.spyOn(console, 'trace').mockImplementation() + jest.spyOn(console, 'error').mockImplementation() + }) + + afterEach(() => { + consoleTraceSpy.mockRestore() + jest.restoreAllMocks() + }) + + it('should call console.trace when error has stack', async () => { + const { captureException } = await import('./index') + + const error = new Error('Test error with stack') + + await captureException(error) + + expect(consoleTraceSpy).toHaveBeenCalledWith(error) + }) + + it('should not call console.trace when error lacks stack', async () => { + const { captureException } = await import('./index') + + const error = { message: 'Error without stack' } + + await captureException(error) + + expect(consoleTraceSpy).not.toHaveBeenCalled() + }) +}) + +describe('errorToErrorResponse', () => { + it('should handle undefined error', () => { + const { errorToErrorResponse } = require('./index') + + const result = errorToErrorResponse(undefined) + + expect(result).toEqual({ + code: 'GENERIC_ERROR', + message: 'An unknown error occurred', + }) + }) + + it('should handle null error', () => { + const { errorToErrorResponse } = require('./index') + + const result = errorToErrorResponse(null) + + expect(result).toEqual({ + code: 'GENERIC_ERROR', + message: 'An unknown error occurred', + }) + }) + + it('should handle Error instance', () => { + const { errorToErrorResponse } = require('./index') + + const error = new Error('Test error') + const result = errorToErrorResponse(error) + + expect(result).toEqual({ + code: 'GENERIC_ERROR', + message: 'Test error', + }) + }) + + it('should handle SystemError instance', () => { + const { errorToErrorResponse, SystemError } = require('./index') + + const error = new SystemError('Test system error', 'TEST_CODE') + const result = errorToErrorResponse(error) + + expect(result).toEqual({ + code: 'TEST_CODE', + message: 'Test system error', + }) + }) + + it('should handle string error', () => { + const { errorToErrorResponse } = require('./index') + + const result = errorToErrorResponse('String error message') + + expect(result).toEqual({ + code: 'GENERIC_ERROR', + message: 'String error message', + }) + }) +}) + +describe('errorToSafeErrorResponse', () => { + it('should expose SafeError details', () => { + const { SafeError, errorToSafeErrorResponse } = require('./index') + + const error = new SafeError('Visible message', 'VISIBLE_CODE') + const result = errorToSafeErrorResponse(error) + + expect(result).toEqual({ + code: 'VISIBLE_CODE', + message: 'Visible message', + }) + }) + + it('should hide regular Error details', () => { + const { errorToSafeErrorResponse } = require('./index') + + const result = errorToSafeErrorResponse(new Error('Internal detail')) + + expect(result).toEqual({ + code: 'GENERIC_ERROR', + message: 'Something went wrong', + }) + }) + + it('should hide SystemError details unless explicitly safe', () => { + const { SystemError, errorToSafeErrorResponse } = require('./index') + + const error = new SystemError('Internal system detail', 'INTERNAL_CODE') + const result = errorToSafeErrorResponse(error) + + expect(result).toEqual({ + code: 'GENERIC_ERROR', + message: 'Something went wrong', + }) + }) +}) + +describe('ContentModerationError', () => { + it('is a SafeError carrying the CONTENT_MODERATION code and no data', () => { + const { + ContentModerationError, + SafeError, + CONTENT_MODERATION_ERROR_CODE, + } = require('./index') + + const error = new ContentModerationError('Inappropriate content (400)') + + expect(error).toBeInstanceOf(SafeError) + expect(error.code).toBe(CONTENT_MODERATION_ERROR_CODE) + expect(error.message).toBe('Inappropriate content (400)') + expect(error.data).toBeUndefined() + }) + + it('does not leak any data when serialized', () => { + const { ContentModerationError } = require('./index') + + const serialized = JSON.parse( + JSON.stringify(new ContentModerationError('blocked (400)')) + ) + + // @note message is non-enumerable on Error, code is the only own prop + expect(serialized).toEqual({ code: 'CONTENT_MODERATION' }) + expect(serialized).not.toHaveProperty('data') + expect(serialized).not.toHaveProperty('body') + }) + + it('is exposed through errorToSafeErrorResponse', () => { + const { ContentModerationError, errorToSafeErrorResponse } = require('./index') + + const result = errorToSafeErrorResponse( + new ContentModerationError('blocked (400)') + ) + + expect(result).toEqual({ + code: 'CONTENT_MODERATION', + message: 'blocked (400)', + }) + }) +}) + +describe('isContentModerationError', () => { + it('detects ContentModerationError instances', () => { + const { ContentModerationError, isContentModerationError } = require('./index') + + expect(isContentModerationError(new ContentModerationError('x'))).toBe(true) + }) + + it('detects errors carrying the CONTENT_MODERATION code', () => { + const { SystemError, isContentModerationError } = require('./index') + + expect( + isContentModerationError(new SystemError('x', 'CONTENT_MODERATION')) + ).toBe(true) + }) + + it('returns false for unrelated errors and nullish values', () => { + const { SystemError, isContentModerationError } = require('./index') + + expect(isContentModerationError(new SystemError('x', 'VR_BAD_REQUEST'))).toBe( + false + ) + expect(isContentModerationError(new Error('x'))).toBe(false) + expect(isContentModerationError(null)).toBe(false) + expect(isContentModerationError(undefined)).toBe(false) + }) +}) + +describe('extractCauseChain', () => { + it('returns undefined when there is no cause', () => { + const { extractCauseChain } = require('./index') + + expect(extractCauseChain(new Error('terminated'))).toBeUndefined() + expect(extractCauseChain(null)).toBeUndefined() + expect(extractCauseChain(undefined)).toBeUndefined() + expect(extractCauseChain('a raw string')).toBeUndefined() + }) + + it('summarizes a single undici-style cause (name, message, code)', () => { + const { extractCauseChain } = require('./index') + + const cause = Object.assign(new Error('other side closed'), { + code: 'UND_ERR_SOCKET', + }) + + const error = new Error('terminated', { cause }) + + expect(extractCauseChain(error)).toEqual([ + { name: 'Error', message: 'other side closed', code: 'UND_ERR_SOCKET' }, + ]) + }) + + it('walks a nested cause chain', () => { + const { extractCauseChain } = require('./index') + + const root = Object.assign(new Error('ECONNRESET'), { code: 'ECONNRESET' }) + const mid = new Error('socket hang up', { cause: root }) + const top = new Error('terminated', { cause: mid }) + + expect(extractCauseChain(top)).toEqual([ + { name: 'Error', message: 'socket hang up', code: undefined }, + { name: 'Error', message: 'ECONNRESET', code: 'ECONNRESET' }, + ]) + }) + + it('caps the chain at the maximum depth', () => { + const { extractCauseChain } = require('./index') + + let error = new Error('root') + + for (let i = 0; i < 8; i++) { + error = new Error(`level-${i}`, { cause: error }) + } + + expect(extractCauseChain(error)).toHaveLength(5) + }) + + it('does not loop forever on a circular cause chain', () => { + const { extractCauseChain } = require('./index') + + const a = new Error('a') + const b = new Error('b', { cause: a }) + + a.cause = b // @note circular + + expect(extractCauseChain(b)).toHaveLength(2) + }) + + it('handles a string cause', () => { + const { extractCauseChain } = require('./index') + + const error = new Error('wrap') + + error.cause = 'raw string reason' + + expect(extractCauseChain(error)).toEqual([ + { name: undefined, message: 'raw string reason', code: undefined }, + ]) + }) +}) + +describe('captureException cause context', () => { + const observability = jest.requireMock('@chatbotkit-dev/observability') + .default + const mockCaptureException = jest.mocked(observability.captureException) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('attaches the cause chain under extra.cause', async () => { + const { captureException } = require('./index') + + const cause = Object.assign(new Error('other side closed'), { + code: 'UND_ERR_SOCKET', + }) + + const error = new Error('terminated', { cause }) + + await captureException(error) + + const [captured, context] = mockCaptureException.mock.calls[0] + + expect(captured).toBe(error) + expect(context).toEqual({ + extra: { + cause: [ + { + name: 'Error', + message: 'other side closed', + code: 'UND_ERR_SOCKET', + }, + ], + }, + }) + }) + + it('passes undefined context when there is no cause and no data', async () => { + const { captureException } = require('./index') + + const error = new Error('plain') + + await captureException(error) + + expect(mockCaptureException).toHaveBeenCalledWith(error, undefined) + }) + + it('merges the cause into existing error.data without clobbering it', async () => { + const { SystemError, captureException } = require('./index') + + const cause = Object.assign(new Error('boom'), { code: 'ECONNRESET' }) + + const error = new SystemError('wrapped', 'GENERIC_ERROR', { + foo: 'bar', + extra: { existing: true }, + }) + + error.cause = cause + + await captureException(error) + + const [, context] = mockCaptureException.mock.calls[0] + + expect(context).toEqual({ + foo: 'bar', + extra: { + existing: true, + cause: [{ name: 'Error', message: 'boom', code: 'ECONNRESET' }], + }, + }) + }) +}) + +describe('errorToSystemError cause preservation', () => { + const observability = jest.requireMock('@chatbotkit-dev/observability') + .default + const mockCaptureException = jest.mocked(observability.captureException) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('preserves the original error as the cause when wrapping', () => { + const { SystemError, errorToSystemError } = require('./index') + + const original = Object.assign(new Error('terminated'), { + code: 'UND_ERR_SOCKET', + }) + + const wrapped = errorToSystemError(original) + + expect(wrapped).toBeInstanceOf(SystemError) + expect(wrapped).not.toBe(original) + expect(wrapped.cause).toBe(original) + }) + + it('returns an existing SystemError untouched (no cause added)', () => { + const { SystemError, errorToSystemError } = require('./index') + + const sys = new SystemError('x', 'CODE') + + expect(errorToSystemError(sys)).toBe(sys) + expect(sys.cause).toBeUndefined() + }) + + it('surfaces the preserved cause to the reporter after wrapping', async () => { + const { errorToSystemError, captureException } = require('./index') + + const original = Object.assign(new Error('terminated'), { + code: 'UND_ERR_SOCKET', + }) + + await captureException(errorToSystemError(original)) + + const [, context] = mockCaptureException.mock.calls[0] + + expect(context.extra.cause).toEqual([ + { name: 'Error', message: 'terminated', code: 'UND_ERR_SOCKET' }, + ]) + }) + + it('keeps the cause non-enumerable so it cannot leak via serialization', () => { + const { errorToSystemError } = require('./index') + + const original = Object.assign(new Error('terminated'), { + code: 'UND_ERR_SOCKET', + requestBody: 'super-secret', + }) + + const wrapped = errorToSystemError(original) + + // @note readable for extractCauseChain/the reporter... + expect(wrapped.cause).toBe(original) + + // @note ...but invisible to enumeration and JSON serialization, so an + // accidental raw serialization of the SystemError can never expose the + // underlying error to the client. + expect(Object.prototype.propertyIsEnumerable.call(wrapped, 'cause')).toBe( + false + ) + expect(Object.keys(wrapped)).not.toContain('cause') + expect(JSON.stringify(wrapped)).not.toContain('super-secret') + }) +}) diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts new file mode 100644 index 0000000..a8d84f4 --- /dev/null +++ b/packages/errors/src/index.ts @@ -0,0 +1,714 @@ +// @ts-check +import observability from '@chatbotkit-dev/observability' + +import joi from 'joi' + +/** + * @todo use from response if possible + */ +export const GENERIC_ERROR_CODE = 'GENERIC_ERROR' +export const TIMEOUT_ERROR_CODE = 'TIMEOUT' +export const BAD_REQUEST_ERROR_CODE = 'BAD_REQUEST' +export const CONFLICT_REQUEST_ERROR_CODE = 'CONFLICT' +export const NOT_FOUND_ERROR_CODE = 'NOT_FOUND' +export const NOT_AUTHENTICATED_ERROR_CODE = 'NOT_AUTHENTICATED' +export const CONTENT_MODERATION_ERROR_CODE = 'CONTENT_MODERATION' + +/** + * Represents any error that is thrown by the system. + */ +/** + * A value that was thrown. JavaScript lets anything be thrown, and the helpers + * below exist precisely to make sense of whatever arrived - an `Error`, a + * string, a plain object from a foreign SDK - so the parameter type is `any` + * on purpose, declared once here rather than at every signature. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Thrown = any + +export class SystemError extends Error { + public code: string + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public data: any + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(message: string, code: string, data?: any) { + super(message) + + this.code = code + this.data = data + } +} + +/** + * Represents an error that is composed of multiple errors. + */ +export class CompositeError extends SystemError { + public errors: Error[] + + constructor(message: string, code: string, errors: Error[]) { + super(message, code) + + this.errors = errors + } +} + +/** + * Represents an error that is safe to show to the user. + */ +export class SafeError extends SystemError { + constructor(message: string, code: string) { + super(message, code) + } +} + +/** + * Represents an error that is not safe to show to the user. + */ +export class UnsafeError extends SystemError { + constructor(message: string, code: string) { + super(message, code) + } +} + +/** + * Represents an error that is related to the user input. + */ +export class UserInputError extends SafeError { + constructor(message: string) { + super(message, BAD_REQUEST_ERROR_CODE) + } +} + +/** + * Represents an error that is related to the user authentication. + */ +export class UserAuthError extends SafeError { + constructor(message: string) { + super(message, NOT_AUTHENTICATED_ERROR_CODE) + } +} + +/** + * Represents an error that is related to a user resource not being found. + */ +export class UserResourceNotFoundError extends SafeError { + constructor(message: string) { + super(message, NOT_FOUND_ERROR_CODE) + } +} + +/** + * Represents an error that is related to bot input. + */ +export class SafeInputError extends SafeError { + constructor(message: string) { + super(message, BAD_REQUEST_ERROR_CODE) + } +} + +/** + * Represents an error that is related to bot input. + */ +export class BotInputError extends SafeError { + constructor(message: string) { + super(message, BAD_REQUEST_ERROR_CODE) + } +} + +/** + * Represents a rejection by a provider-side content moderation / safety filter. + * + * @note These are not malformed requests - the provider refused to process the + * input on policy grounds. They are expected provider behaviour rather than + * bugs, so they are SafeError (the message is meaningful to the caller) and are + * excluded from Sentry bug tracking. They deliberately carry no `data` payload + * so the offending request body is never serialized to a client. + */ +export class ContentModerationError extends SafeError { + constructor(message: string) { + super(message, CONTENT_MODERATION_ERROR_CODE) + } +} + +/** + * Returns true if the error represents a provider content moderation rejection. + * + */ +export function isContentModerationError(error: Thrown): boolean { + return ( + error instanceof ContentModerationError || + (!!error && error.code === CONTENT_MODERATION_ERROR_CODE) + ) +} + +/** + * Represents an error that is related to user configuration. + */ +export class UserConfigError extends UnsafeError { + constructor(message: string) { + super(message, BAD_REQUEST_ERROR_CODE) + } +} + +/** + * Represents an error that is related to the admin authentication + */ +export class AdminAuthError extends UnsafeError { + constructor(message: string) { + super(message, NOT_AUTHENTICATED_ERROR_CODE) + } +} + +/** + * + */ +export class ObservationError extends Error { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public data?: any + + /** + * Whether this observation should additionally be reported. Observations are + * log-only by default; high-signal ones (e.g. stuck-run detections) opt in so + * they are searchable and carry their context. + */ + public sentry: boolean + + /** + * Severity to use when the observation is reported. + */ + public level: 'info' | 'warning' | 'error' + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(message: string, data?: any) { + super(message) + + if (data !== undefined && data !== null) { + this.data = data + } + + this.sentry = false + this.level = 'warning' + } +} + +/** + */ +export class UnexpectedStateError extends Error { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public data?: any + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(message: string, data?: any) { + super(message) + + if (data !== undefined && data !== null) { + this.data = data + } + } +} + +/** + */ +export const KNOWN_PRISMA_ERRORS = [ + // prisma + // @see https://www.prisma.io/docs/reference/api-reference/error-reference#prismaclientvalidationerror + + 'PrismaClientKnownRequestError', + 'PrismaClientUnknownRequestError', + 'PrismaClientRustPanicError', + 'PrismaClientInitializationError', + 'PrismaClientValidationError', +] + +/** + */ +export const KNOWN_TIMEOUT_ERRORS = [ + // timeout + + 'TimeoutError', +] + +/** + */ +export const KNOWN_SUBSCRIPTION_ERRORS = [ + // limits + + 'Limits reached', + 'You have exceeded your allocated database limits: database/dataset', + 'You have exceeded your allocated database limits: database/record', + 'You have exceeded your allocated database limits: database/skillset', + 'You have exceeded your allocated database limits: database/ability', + 'You have exceeded your allocated database limits: database/file', +] + +/** + * @returns boolean + */ +export function errorIn(error: Error, collection: string[]) { + if (!error || typeof error !== 'object') { + return false + } + + return collection.includes(error.name) || collection.includes(error.message) +} + +export function isKnownError(error: Error|string): boolean { + if (typeof error === 'string') { + error = new Error(error) + } + + for (const collection of [ + KNOWN_PRISMA_ERRORS, + KNOWN_TIMEOUT_ERRORS, + KNOWN_SUBSCRIPTION_ERRORS, + ]) { + if (errorIn(error, collection)) { + return true + } + } + + return false +} + +/** + * Converts any error to a standardized error response format, which includes a + * code and a message. + * + */ +export function errorToErrorResponse(error: Thrown): { + code: string + message: string +} { + // @note handle undefined/null errors explicitly to avoid confusing trace logs + + if (error === undefined || error === null) { + void captureUnexpectedState('Undefined or null error encountered') + + return { code: GENERIC_ERROR_CODE, message: 'An unknown error occurred' } + } + + switch (true) { + case error instanceof SystemError: { + return { code: error.code, message: error.message.toString() } + } + + case error instanceof joi.ValidationError: { + return { code: BAD_REQUEST_ERROR_CODE, message: error.message.toString() } + } + + case errorIn(error, KNOWN_PRISMA_ERRORS): { + if (error.code === 'P2002') { + return { + code: CONFLICT_REQUEST_ERROR_CODE, + message: 'Unique constraint violation', + } + } else { + return { code: GENERIC_ERROR_CODE, message: 'System error occurred' } + } + } + + case errorIn(error, KNOWN_TIMEOUT_ERRORS): { + return { code: TIMEOUT_ERROR_CODE, message: 'Response timeout' } + } + + case error instanceof Error: { + return { code: GENERIC_ERROR_CODE, message: error.message.toString() } + } + + case typeof error === 'string': { + return { code: GENERIC_ERROR_CODE, message: error } + } + + default: { + return { code: GENERIC_ERROR_CODE, message: 'Something went wrong' } + } + } +} + +/** + * Converts an error to a public error response. Only SafeError messages are + * exposed; all other errors are intentionally collapsed to a generic response. + * + */ +export function errorToSafeErrorResponse(error: Thrown): { + code: string + message: string +} { + if (error instanceof SafeError) { + return errorToErrorResponse(error) + } + + return { + code: GENERIC_ERROR_CODE, + message: 'Something went wrong', + } +} + +/** + * Converts an error response to a SystemError instance. + */ +export function errorResponseToError( + errorResponse: { code: string; message: string }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any +): SystemError { + return new SystemError(errorResponse.message, errorResponse.code, data) +} + +/** + * Converts any error to a SystemError instance. + * + */ +export function errorToSystemError(error: Thrown, data?: unknown): SystemError { + if (error instanceof SystemError) { + return error + } + + const systemError = errorResponseToError(errorToErrorResponse(error), data) + + // @note preserve the original error as the `cause` so its underlying detail + // (e.g. an undici "terminated" whose real reason lives on its own `cause`) + // survives normalization and can be surfaced to Sentry via + // `extractCauseChain`. Defined non-enumerable - matching native `Error.cause` + // semantics - so that even an accidental raw serialization of the + // `SystemError` (e.g. `JSON.stringify`) can never leak the underlying error + // to the client. Client-facing responses only ever expose `{code, message}` + // via `errorToErrorResponse`/`errorToSafeErrorResponse`. + + if (error instanceof Error) { + Object.defineProperty(systemError, 'cause', { + value: error, + enumerable: false, + configurable: true, + writable: true, + }) + } + + return systemError +} + +export async function logError(e: Thrown): Promise { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line + console.error(e) + } +} + +export async function debugError(e: Thrown): Promise { + if (!!process.env.DEBUG) { + // eslint-disable-next-line + console.error(e) + } +} + +export function setTag(name: string, value: string) { + observability.setTag(name, value) +} + +/** + * Handles an `ObservationError`: always logs it, and additionally reports it to + * Sentry as a (non-exception) message when the observation opted in via + * `sentry`. Observations are not bugs, so they are sent with their own severity + * and a stable fingerprint (keyed on the observation `event`) so related stuck + * runs group together and carry the supporting context needed to troubleshoot. + * + */ +function reportObservation(e: ObservationError): void { + // eslint-disable-next-line + console.log(`ObservationError: ${e.message}`) + + if (!e.sentry) { + return + } + + const event = e.data?.event + + try { + observability.captureMessage(e.message, { + level: e.level || 'warning', + tags: { observation: event || 'observation' }, + fingerprint: ['observation', event || e.message], + extra: e.data || undefined, + }) + } catch (err) { + // eslint-disable-next-line + console.error(err) + } +} + +const MAX_CAUSE_DEPTH = 5 + +/** + * Walks the `cause` chain of an error and returns a compact, serializable + * summary of each link. + * + * This is what lets us see *why* an otherwise opaque error happened. A Node + * `fetch` (undici) failure, for example, surfaces only as `Error: terminated` + * while the real reason - `ECONNRESET`, "other side closed", a body timeout - + * lives on `error.cause`. We attach this chain to the Sentry event server-side + * only (as `extra.cause`); it is never emitted to the client, which only ever + * sees `{code, message}` via `errorToErrorResponse`/`errorToSafeErrorResponse`. + * + */ +export function extractCauseChain( + error: Thrown +): + | Array<{ name?: string; message?: string; code?: string }> + | undefined { + /** @type {Array<{name?: string, message?: string, code?: string}>} */ + const chain: { + name: string | undefined + message: string | undefined + code: string | undefined + }[] = [] + + const seen = new Set() + + let current = error?.cause + + while ( + current !== undefined && + current !== null && + chain.length < MAX_CAUSE_DEPTH && + !seen.has(current) + ) { + seen.add(current) + + chain.push({ + name: typeof current.name === 'string' ? current.name : undefined, + + message: + typeof current.message === 'string' + ? current.message + : typeof current === 'string' + ? current + : undefined, + + code: + current.code !== undefined && current.code !== null + ? String(current.code) + : undefined, + }) + + current = typeof current === 'object' ? current.cause : undefined + } + + return chain.length > 0 ? chain : undefined +} + +/** + * Builds the Sentry capture context for an error, merging any existing + * `error.data` context with the serialized `cause` chain (under `extra.cause`). + * + * When the error has no cause, the original `error.data` is returned untouched + * - preserving reference identity and the prior capture behaviour. + * + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function buildCaptureContext(e: Thrown): any { + const cause = extractCauseChain(e) + + const data = e?.data + + if (!cause) { + return data ?? undefined + } + + // @note merge the cause into a structured `extra` without clobbering any + // context the error already carries. If `data` is not a plain object (e.g. a + // raw string passed as context) keep it intact alongside the cause. + + if (data && typeof data === 'object' && !Array.isArray(data)) { + return { + ...data, + + extra: { + ...(data.extra && typeof data.extra === 'object' ? data.extra : {}), + + cause, + }, + } + } + + return { + extra: data === undefined ? { cause } : { cause, data }, + } +} + +export async function captureError(e: Thrown): Promise { + // @note it is important to exclude some errors from being logged or even sent + // to Sentry, because it is a user-facing error that contains sensitive + // information + + if (e instanceof UserAuthError) { + // eslint-disable-next-line + console.log(`UserAuthError: ${e.message}`) + + return + } + + if (e instanceof UserInputError) { + // eslint-disable-next-line + console.log(`UserInputError: ${e.message}`) + + return + } + + if (e instanceof BotInputError) { + // eslint-disable-next-line + console.log(`BotInputError: ${e.message}`) + + return + } + + if (e instanceof UserConfigError) { + // eslint-disable-next-line + console.log(`UserConfigError: ${e.message}`) + + return + } + + // @note content moderation rejections are expected provider behaviour, not + // bugs - log them but keep them out of Sentry + if (e instanceof ContentModerationError) { + // eslint-disable-next-line + console.log(`ContentModerationError: ${e.message}`) + + return + } + + // @note ObservationError is for logging/analysis, not bug tracking - but an + // observation may opt in to Sentry (as a message, not an exception) + if (e instanceof ObservationError) { + reportObservation(e) + + return + } + + // eslint-disable-next-line + console.error(e) + + // @note only trace if error has a stack property to avoid "Trace: undefined" + if (e?.stack) { + // eslint-disable-next-line + console.trace(e) + } + + try { + await observability.captureException(e, buildCaptureContext(e)) + } catch (e) { + // eslint-disable-next-line + console.error(e) + } +} + +export async function captureException(e: Thrown): Promise { + // @note it is important to exclude some errors from being logged or even sent + // to Sentry, because it is a user-facing error that contains sensitive + // information + + if (e instanceof UserAuthError) { + // eslint-disable-next-line + console.log(`UserAuthError: ${e.message}`) + + return + } + + if (e instanceof UserInputError) { + // eslint-disable-next-line + console.log(`UserInputError: ${e.message}`) + + return + } + + if (e instanceof BotInputError) { + // eslint-disable-next-line + console.log(`BotInputError: ${e.message}`) + + return + } + + if (e instanceof UserConfigError) { + // eslint-disable-next-line + console.log(`UserConfigError: ${e.message}`) + + return + } + + // @note content moderation rejections are expected provider behaviour, not + // bugs - log them but keep them out of Sentry + if (e instanceof ContentModerationError) { + // eslint-disable-next-line + console.log(`ContentModerationError: ${e.message}`) + + return + } + + // @note ObservationError is for logging/analysis, not bug tracking - but an + // observation may opt in to Sentry (as a message, not an exception) + if (e instanceof ObservationError) { + reportObservation(e) + + return + } + + // eslint-disable-next-line + console.error(e) + + // @note only trace if error has a stack property to avoid "Trace: undefined" + if (e?.stack) { + // eslint-disable-next-line + console.trace(e) + } + + try { + await observability.captureException(e, buildCaptureContext(e)) + } catch (e) { + // eslint-disable-next-line + console.error(e) + } +} + +export async function captureInputError(e: Thrown, data: unknown): Promise { + // eslint-disable-next-line + console.error(e) + + // @note only trace if error has a stack property to avoid "Trace: undefined" + if (e?.stack) { + // eslint-disable-next-line + console.trace(e) + } + + try { + await observability.captureException(e, { extra: { data } }) + } catch (e) { + // eslint-disable-next-line + console.error(e) + } +} + +export async function captureObservation( + message: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context?: any, + options: { sentry?: boolean; level?: 'info' | 'warning' | 'error' } = {} +): Promise { + const error = new ObservationError(message, context) + + if (options.sentry) { + error.sentry = true + error.level = options.level || 'warning' + } + + await captureError(error) +} + +export async function captureUnexpectedState( + message: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context?: any +): Promise { + await captureError(new UnexpectedStateError(message, context)) +} diff --git a/packages/errors/tsconfig.json b/packages/errors/tsconfig.json new file mode 100644 index 0000000..72b364c --- /dev/null +++ b/packages/errors/tsconfig.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node", + "jest" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js new file mode 100644 index 0000000..70b8f0c --- /dev/null +++ b/packages/eslint-config/index.js @@ -0,0 +1,202 @@ +module.exports = { + extends: ['plugin:@typescript-eslint/recommended', 'prettier'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, + plugins: ['@typescript-eslint', 'unused-imports', 'jsdoc'], + rules: { + 'no-console': 'error', + 'newline-before-return': 'error', + 'no-cond-assign': ['error', 'always'], + 'no-return-assign': ['error', 'always'], + curly: ['error', 'all'], + 'no-throw-literal': 'error', + 'space-before-blocks': 'error', + 'space-before-function-paren': [ + 'error', + { + named: 'never', + anonymous: 'always', + asyncArrow: 'always', + }, + ], + 'no-multi-spaces': 'error', + 'spaced-comment': ['error', 'always', { exceptions: ['*', '-', '+'] }], + 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }], + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: '*', next: 'return' }, + { blankLine: 'always', prev: 'return', next: '*' }, + // -- + { blankLine: 'always', prev: '*', next: 'for' }, + { blankLine: 'always', prev: 'for', next: '*' }, + { blankLine: 'always', prev: '*', next: 'while' }, + { blankLine: 'always', prev: 'while', next: '*' }, + { blankLine: 'always', prev: '*', next: 'do' }, + { blankLine: 'always', prev: 'do', next: '*' }, + // --- + { blankLine: 'always', prev: '*', next: 'if' }, + { blankLine: 'always', prev: 'if', next: '*' }, + { blankLine: 'always', prev: '*', next: 'switch' }, + { blankLine: 'always', prev: 'switch', next: '*' }, + // { blankLine: 'always', prev: '*', next: 'case' }, + // { blankLine: 'always', prev: 'case', next: '*' }, + { blankLine: 'always', prev: '*', next: 'break' }, + // --- + { blankLine: 'always', prev: 'break', next: '*' }, + { blankLine: 'always', prev: '*', next: 'continue' }, + { blankLine: 'always', prev: 'continue', next: '*' }, + // --- + { blankLine: 'always', prev: '*', next: 'class' }, + { blankLine: 'always', prev: 'class', next: '*' }, + { blankLine: 'always', prev: '*', next: 'function' }, + { blankLine: 'always', prev: 'function', next: '*' }, + // --- + { blankLine: 'always', prev: '*', next: 'try' }, + { blankLine: 'always', prev: 'try', next: '*' }, + { blankLine: 'always', prev: '*', next: 'throw' }, + { blankLine: 'always', prev: 'throw', next: '*' }, + // --- + { + blankLine: 'any', + prev: ['const', 'let', 'var'], + next: ['const', 'let', 'var'], + }, + { + blankLine: 'always', + prev: ['const', 'let', 'var'], + next: [ + 'expression', + 'block', + 'block-like', + 'if', + 'switch', + 'for', + 'while', + 'do', + 'try', + 'return', + 'throw', + 'function', + 'class', + ], + }, + { + blankLine: 'always', + prev: [ + 'expression', + 'block', + 'block-like', + 'if', + 'switch', + 'for', + 'while', + 'do', + 'try', + 'return', + 'throw', + 'function', + 'class', + ], + next: ['const', 'let', 'var'], + }, + // --- + // { + // blankLine: 'never', + // prev: 'singleline-const', + // next: 'singleline-const', + // }, + // { + // blankLine: 'never', + // prev: 'singleline-let', + // next: 'singleline-let', + // }, + // { + // blankLine: 'never', + // prev: 'singleline-var', + // next: 'singleline-var', + // }, + // --- + { + blankLine: 'always', + prev: ['block', 'block-like'], + next: ['multiline-const', 'multiline-let', 'multiline-var'], + }, + { + blankLine: 'always', + prev: ['multiline-const', 'multiline-let', 'multiline-var'], + next: ['block', 'block-like'], + }, + ], + '@typescript-eslint/no-wrapper-object-types': 'warn', + '@typescript-eslint/no-unused-vars': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + 'import/no-anonymous-default-export': ['off'], + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + 'jsdoc/require-throws': 'error', + yoda: 'error', + 'unused-imports/no-unused-imports': ['error', { caughtErrors: 'none' }], + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + 'no-restricted-globals': [ + 'error', + { + name: 'fetch', + message: + 'Please import fetch explicitly (e.g., from "@/lib/fetch") instead of using the global fetch.', + }, + ], + }, + env: { + browser: true, + node: true, + es6: true, + }, + ignorePatterns: ['node_modules/'], + overrides: [ + { + files: [ + '**/*.test.js', + '**/*.test.jsx', + '**/*.test.ts', + '**/*.test.tsx', + '**/*.utest.js', + '**/*.utest.jsx', + '**/*.utest.ts', + '**/*.utest.tsx', + '**/*.itest.js', + '**/*.itest.jsx', + '**/*.itest.ts', + '**/*.itest.tsx', + ], + env: { + jest: true, + }, + }, + { + files: ['**/*.jsx'], + extends: ['plugin:@typescript-eslint/disable-type-checked'], + rules: { + '@typescript-eslint/consistent-type-imports': 'off', + }, + }, + { + files: ['**/*.js'], + extends: ['plugin:@typescript-eslint/disable-type-checked'], + rules: { + '@typescript-eslint/consistent-type-imports': 'off', + }, + }, + ], +} diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json new file mode 100644 index 0000000..8554366 --- /dev/null +++ b/packages/eslint-config/package.json @@ -0,0 +1,36 @@ +{ + "name": "@chatbotkit-dev/eslint-config", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "commonjs", + "exports": { + ".": "./index.js" + }, + "scripts": { + "build": "true", + "check": "true", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "true", + "test": "true" + }, + "access": "restricted", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^8.56.0", + "@typescript-eslint/parser": "^8.56.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsdoc": "^63.3.2", + "eslint-plugin-unused-imports": "^4.4.1" + }, + "peerDependencies": { + "eslint": "^9.0.0" + }, + "devDependencies": { + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5" + } +} diff --git a/packages/fetch/README.md b/packages/fetch/README.md new file mode 100644 index 0000000..391e85c --- /dev/null +++ b/packages/fetch/README.md @@ -0,0 +1,11 @@ +# @chatbotkit-dev/fetch + +The platform's fetch client. + +`fetchPlusPlus` is `withRetry(withTimeout(fetch))`: five attempts by default with exponential +backoff, a thirty second timeout, and retries on failed responses as well as thrown errors. + +Use this rather than reimplementing a retry loop. An earlier hand rolled copy in the email provider +silently used three attempts with no backoff and no retry on failed responses. + +Extracted from `platform/lib/fetch.js`. diff --git a/packages/fetch/jest.config.js b/packages/fetch/jest.config.js new file mode 100644 index 0000000..8b4f6e4 --- /dev/null +++ b/packages/fetch/jest.config.js @@ -0,0 +1,19 @@ +// @note CommonJS transform: these tests use `jest.mock` hoisting, which is not +// available under ESM. + +export default { + preset: 'ts-jest', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', + + transform: { + '^.+\\.[jt]sx?$': [ + 'ts-jest', + { + useESM: false, + // @note transpile only. Type checking is the `check` script's job. + tsconfig: { module: 'commonjs', esModuleInterop: true, allowJs: true }, + }, + ], + }, +} diff --git a/packages/fetch/package.json b/packages/fetch/package.json new file mode 100644 index 0000000..b5a15bd --- /dev/null +++ b/packages/fetch/package.json @@ -0,0 +1,43 @@ +{ + "name": "@chatbotkit-dev/fetch", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "scripts": { + "build": "tsc6 --emitDeclarationOnly", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js", + "test": "jest" + }, + "access": "restricted", + "types": "./types/src/index.d.ts", + "dependencies": { + "@chatbotkit-dev/debug": "workspace:*", + "@chatbotkit-dev/errors": "workspace:*", + "@chatbotkit-dev/http-codes": "workspace:*" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/fetch/src/index.test.js b/packages/fetch/src/index.test.js new file mode 100644 index 0000000..bcd2bd0 --- /dev/null +++ b/packages/fetch/src/index.test.js @@ -0,0 +1,2009 @@ +import { + CONTEXT_MODEL_TAG, + FETCH_PHASE_RESPONSE_BODY, + FETCH_PHASE_RESPONSE_HEADERS, + FETCH_PHASE_TAG, + FetchError, + TIMEOUT_ERROR_NAME, + anySignal, + download, + fetch, + getFetchError, + isBodyStallTimeout, + jsonl, + withBodyTimeout, + withLimit, + withRetry, + withTimeout, +} from './index' + +jest.mock('@chatbotkit-dev/http-codes', () => ({ + statusToMessageMap: { + 400: 'Bad Request', + 401: 'Unauthorized', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + }, + statusToCodeMap: { + 400: 'BAD_REQUEST', + 401: 'UNAUTHORIZED', + 404: 'NOT_FOUND', + 500: 'INTERNAL_SERVER_ERROR', + 502: 'BAD_GATEWAY', + 503: 'SERVICE_UNAVAILABLE', + }, +})) + +describe('withTimeout', () => { + it('must timeout', async () => { + const fetch = withTimeout( + async (url, { signal }) => { + return new Promise((resolve, reject) => { + // We expect to receive an abort event from the timeout handler, which + // we use to reject the promise. + + signal.addEventListener('abort', () => { + reject(new Error(signal.reason)) + }) + }) + }, + { timeout: 1000 } + ) + + await expect(async () => { + await fetch() + }).rejects.toThrow() + }) + + it('attaches diagnostics (model, url, timeout budget) to the TimeoutError', async () => { + const fetch = withTimeout( + async (url, { signal }) => { + // @note mirror undici: a timed-out fetch rejects with the abort reason + // (the TimeoutError we passed to abort), not a wrapped Error + return new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason)) + }) + }, + { timeout: 5 } + ) + + let error + + try { + await fetch('https://gateway.example/v1/chat/completions', { + meta: { model: 'test-model' }, + }) + } catch (e) { + error = e + } + + expect(error?.name).toBe(TIMEOUT_ERROR_NAME) + expect(error?.data?.tags?.[CONTEXT_MODEL_TAG]).toBe('test-model') + expect(error?.data?.tags?.[FETCH_PHASE_TAG]).toBe( + FETCH_PHASE_RESPONSE_HEADERS + ) + // @note a header-phase timeout is retried at the fetch layer, so the + // streaming recogniser must NOT claim it as a (downstream) body stall + expect(isBodyStallTimeout(error)).toBe(false) + expect(error?.data?.extra?.fetch?.url).toBe( + 'https://gateway.example/v1/chat/completions' + ) + expect(error?.data?.extra?.fetch?.timeoutMs).toBe(5) + expect(error?.data?.extra?.fetch?.model).toBe('test-model') + }) + + if (process.env.SLOW_TESTS) { + it('httpstat: must timeout', async () => { + let error + + const f = withTimeout(fetch, { timeout: 5000 }) + + try { + await f('https://httpstat.us/504?sleep=60000') + } catch (e) { + error = e + } + + expect(error).toBeTruthy() + }) + } +}) + +describe('withBodyTimeout', () => { + const encoder = new TextEncoder() + + /** + * Build an ok Response that streams the given chunks and closes immediately. + * + * @param {string[]} chunks + * @param {{ status?: number }} [opts] + * @returns {Response} + */ + function makeStreamingResponse(chunks, { status = 200 } = {}) { + const stream = new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(encoder.encode(chunk)) + } + + controller.close() + }, + }) + + return new Response(stream, { status }) + } + + /** + * Build an ok Response whose headers are delivered but whose body never emits + * a byte and never closes - a stalled upstream. + * + * @returns {Response} + */ + function makeStallingResponse() { + const stream = new ReadableStream({ + start() { + // @note never enqueue, never close + }, + }) + + return new Response(stream, { status: 200 }) + } + + /** + * @param {Response} response + * @returns {Promise} + */ + async function readAll(response) { + const decoder = new TextDecoder() + + let out = '' + + // @ts-ignore - body is async-iterable (polyfilled in @/lib/fetch) + for await (const chunk of response.body) { + out += decoder.decode(chunk) + } + + return out + } + + it('times out and attributes the stall when the body goes silent', async () => { + jest.useFakeTimers() + + try { + const fetch = withBodyTimeout(async () => makeStallingResponse(), { + bodyTimeout: 1000, + }) + + const response = await fetch( + 'https://gateway.example/v1/chat/completions', + { meta: { model: 'test-model' } } + ) + + let settled = false + + // @note consume in the background; it must reject once the idle timer fires + const consume = (async () => { + try { + await readAll(response) + + return null + } catch (e) { + return e + } finally { + settled = true + } + })() + + // @note just shy of the deadline: still streaming, nothing settled + await jest.advanceTimersByTimeAsync(999) + + expect(settled).toBe(false) + + // @note crossing the deadline fires the idle timer + await jest.advanceTimersByTimeAsync(1) + + const error = await consume + + expect(settled).toBe(true) + expect(error?.name).toBe(TIMEOUT_ERROR_NAME) + expect(error?.data?.tags?.[FETCH_PHASE_TAG]).toBe(FETCH_PHASE_RESPONSE_BODY) + expect(error?.data?.tags?.[CONTEXT_MODEL_TAG]).toBe('test-model') + // @note the streaming layer keys its retry decision off exactly this + // error shape, so assert the recogniser agrees - this ties the producer + // (withBodyTimeout) to the consumer (isBodyStallTimeout) end to end + expect(isBodyStallTimeout(error)).toBe(true) + expect(error?.data?.extra?.fetch?.bodyTimeoutMs).toBe(1000) + expect(error?.data?.extra?.fetch?.url).toBe( + 'https://gateway.example/v1/chat/completions' + ) + } finally { + jest.useRealTimers() + } + }) + + it('resets the idle timer on each chunk so a steady stream outlives the timeout', async () => { + jest.useFakeTimers() + + try { + const gap = 800 + + const bodyTimeout = 1000 + + const count = 5 + + let i = 0 + + // @note deliver one chunk per pull, each `gap` (< bodyTimeout) after the + // previous read. Total elapsed ((count + 1) * gap = 4800ms) far exceeds + // bodyTimeout, so only a correct per-chunk reset keeps the stream alive; a + // broken reset would fire at t=1000ms (mid second gap) and reject. + // @note pull returns a promise that settles only after the delayed + // delivery, so the stream waits between pulls instead of re-invoking pull + // synchronously (which would schedule overlapping timers and double-close) + const source = new ReadableStream({ + pull(controller) { + return new Promise((resolve) => { + if (i < count) { + const value = String(i++) + + setTimeout(() => { + controller.enqueue(encoder.encode(value)) + + resolve() + }, gap) + } else { + setTimeout(() => { + controller.close() + + resolve() + }, gap) + } + }) + }, + }) + + const fetch = withBodyTimeout( + async () => new Response(source, { status: 200 }), + { bodyTimeout } + ) + + const response = await fetch('https://gateway.example') + + const collected = readAll(response) + + await jest.advanceTimersByTimeAsync((count + 1) * gap + 50) + + await expect(collected).resolves.toBe('01234') + } finally { + jest.useRealTimers() + } + }) + + it('preserves status, statusText, and headers on the wrapped response', async () => { + const original = new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode('hi')) + + controller.close() + }, + }), + { + status: 206, + statusText: 'Partial Content', + headers: { 'content-type': 'text/event-stream', 'x-trace': 'abc' }, + } + ) + + const fetch = withBodyTimeout(async () => original, { bodyTimeout: 1000 }) + + const response = await fetch('https://gateway.example') + + // @note an ok response IS wrapped (new object), but its metadata must carry + // over verbatim - downstream relies on status and content-type + expect(response).not.toBe(original) + expect(response.status).toBe(206) + expect(response.statusText).toBe('Partial Content') + expect(response.headers.get('content-type')).toBe('text/event-stream') + expect(response.headers.get('x-trace')).toBe('abc') + + await expect(readAll(response)).resolves.toBe('hi') + }) + + it('does not wrap non-ok responses', async () => { + const original = makeStreamingResponse(['boom'], { status: 500 }) + + const fetch = withBodyTimeout(async () => original, { bodyTimeout: 10 }) + + await expect(fetch('https://gateway.example')).resolves.toBe(original) + }) + + it('is a pass-through when the timeout is disabled', async () => { + const original = makeStreamingResponse(['x']) + + const fetch = withBodyTimeout(async () => original, { bodyTimeout: 0 }) + + await expect(fetch('https://gateway.example')).resolves.toBe(original) + }) + + it('lets a per-call bodyTimeout override the default', async () => { + jest.useFakeTimers() + + try { + // @note default disables the guard; the per-call option must re-enable it + const fetch = withBodyTimeout(async () => makeStallingResponse(), { + bodyTimeout: 0, + }) + + const response = await fetch('https://gateway.example', { + bodyTimeout: 500, + }) + + const consume = (async () => { + try { + await readAll(response) + + return null + } catch (e) { + return e + } + })() + + await jest.advanceTimersByTimeAsync(500) + + const error = await consume + + expect(error?.name).toBe('TimeoutError') + expect(error?.data?.extra?.fetch?.bodyTimeoutMs).toBe(500) + } finally { + jest.useRealTimers() + } + }) + + it('cancels the upstream body when the consumer stops early', async () => { + let cancelled = false + + const encoder = new TextEncoder() + + const source = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode('one')) + + // @note never close - the consumer will break out before the end + }, + + cancel() { + cancelled = true + }, + }) + + const fetch = withBodyTimeout( + async () => new Response(source, { status: 200 }), + { bodyTimeout: 1000 } + ) + + const response = await fetch('https://gateway.example') + + let first + + // @ts-ignore - body is async-iterable (polyfilled in @/lib/fetch) + for await (const chunk of response.body) { + first = new TextDecoder().decode(chunk) + + break + } + + // @note let the cancel propagate through guarded -> source + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(first).toBe('one') + expect(cancelled).toBe(true) + }) + + it('propagates a mid-stream source error unchanged (not masked as a timeout)', async () => { + const encoder = new TextEncoder() + + let delivered = false + + // @note deliver one chunk, then error on the next pull. Erroring + // synchronously right after enqueue would reset the queue (spec) and drop + // the chunk, so we split it across two pulls to mimic a real mid-stream cut. + const source = new ReadableStream({ + pull(controller) { + if (!delivered) { + controller.enqueue(encoder.encode('partial')) + + delivered = true + + return + } + + controller.error(new Error('terminated-ish')) + }, + }) + + const fetch = withBodyTimeout( + async () => new Response(source, { status: 200 }), + { bodyTimeout: 1000 } + ) + + const response = await fetch('https://gateway.example') + + let collected = '' + + let error + + try { + // @ts-ignore - body is async-iterable (polyfilled in @/lib/fetch) + for await (const chunk of response.body) { + collected += new TextDecoder().decode(chunk) + } + } catch (e) { + error = e + } + + expect(collected).toBe('partial') + expect(error?.name).not.toBe(TIMEOUT_ERROR_NAME) + expect(isBodyStallTimeout(error)).toBe(false) + expect(error?.message).toBe('terminated-ish') + }) +}) + +describe('isBodyStallTimeout', () => { + it('matches a body-phase TimeoutError', () => { + expect( + isBodyStallTimeout({ + name: TIMEOUT_ERROR_NAME, + data: { tags: { [FETCH_PHASE_TAG]: FETCH_PHASE_RESPONSE_BODY } }, + }) + ).toBe(true) + }) + + it('rejects a header-phase TimeoutError (retried at the fetch layer)', () => { + expect( + isBodyStallTimeout({ + name: TIMEOUT_ERROR_NAME, + data: { tags: { [FETCH_PHASE_TAG]: FETCH_PHASE_RESPONSE_HEADERS } }, + }) + ).toBe(false) + }) + + it('rejects a non-timeout error that happens to carry the body-phase tag', () => { + expect( + isBodyStallTimeout({ + name: 'FetchError', + data: { tags: { [FETCH_PHASE_TAG]: FETCH_PHASE_RESPONSE_BODY } }, + }) + ).toBe(false) + }) + + it('rejects a bare error, null, and undefined without throwing', () => { + expect(isBodyStallTimeout(new Error('boom'))).toBe(false) + expect(isBodyStallTimeout(null)).toBe(false) + expect(isBodyStallTimeout(undefined)).toBe(false) + }) +}) + +describe('withRetry', () => { + it('must retry without timeouts', async () => { + let count = 0 + + const fetch = withRetry( + async () => { + return new Promise((resolve, reject) => { + count += 1 + + reject(new Error(`Error`)) + }) + }, + { retries: 5, retryDelay: 1 } + ) + + await expect(async () => { + await fetch() + }).rejects.toThrow() + + expect(count).toEqual(6) + }) + + it('must retry timeouts', async () => { + let count = 0 + + const fetch = withRetry( + withTimeout( + async (url, { signal }) => { + return new Promise((resolve, reject) => { + count += 1 + + // We expect to receive an abort event from the timeout handler, + // which we use to reject the promise. + + signal.addEventListener('abort', () => { + reject(new Error(signal.reason)) + }) + }) + }, + { timeout: 1 } + ), + { retries: 5, retryDelay: 1, retryTimeout: true } + ) + + await expect(async () => { + await fetch() + }).rejects.toThrow() + + expect(count).toEqual(6) + }) + + it('records attempt count and elapsed time after exhausting timeout retries', async () => { + let count = 0 + + const fetch = withRetry( + withTimeout( + async (url, { signal }) => { + return new Promise((_resolve, reject) => { + count += 1 + + signal.addEventListener('abort', () => reject(signal.reason)) + }) + }, + { timeout: 1 } + ), + { retries: 3, retryDelay: 1, retryTimeout: true } + ) + + let error + + try { + await fetch('https://gateway.example/v1/chat/completions', { + meta: { model: 'test-model' }, + }) + } catch (e) { + error = e + } + + expect(count).toEqual(4) // 1 initial + 3 retries + + expect(error?.name).toBe('TimeoutError') + expect(error?.data?.tags?.['fetch.outcome']).toBe('timeout') + expect(error?.data?.tags?.['fetch.attempts']).toBe('4') + expect(error?.data?.extra?.fetchRetry?.attempts).toBe(4) + expect(error?.data?.extra?.fetchRetry?.maxAttempts).toBe(4) + expect(typeof error?.data?.extra?.fetchRetry?.elapsedMs).toBe('number') + + // @note the model context set by withTimeout survives the retry annotation + expect(error?.data?.tags?.['context.model']).toBe('test-model') + }) + + it('records a single attempt when retryTimeout is disabled', async () => { + let count = 0 + + const fetch = withRetry( + withTimeout( + async (url, { signal }) => { + return new Promise((_resolve, reject) => { + count += 1 + + signal.addEventListener('abort', () => reject(signal.reason)) + }) + }, + { timeout: 1 } + ), + { retries: 5, retryDelay: 1, retryTimeout: false } + ) + + let error + + try { + await fetch('https://gateway.example/v1/chat/completions') + } catch (e) { + error = e + } + + expect(count).toEqual(1) // not retried because retryTimeout is false + expect(error?.data?.tags?.['fetch.attempts']).toBe('1') + expect(error?.data?.extra?.fetchRetry?.attempts).toBe(1) + }) + + if (process.env.SLOW_TESTS) { + it('httpstat: must retry timeouts', async () => { + const f = withRetry(withTimeout(fetch, { timeout: 5000 }), { + retries: 5, + retryDelay: 1, + retryTimeout: true, + }) + + let error + + try { + await f('https://httpstat.us/504?sleep=60000') + } catch (e) { + error = e + } + + expect(error).toBeTruthy() + }) + } +}) + +describe('jsonl', () => { + it('should parse JSON lines correctly', async () => { + const input = '{"name": "Alice"}\n{"name": "Bob"}\n{"name": "Charlie"}' + + const body = new ReadableStream({ + async start(controller) { + controller.enqueue(new TextEncoder().encode(input)) + controller.close() + }, + }) + + const result = [] + + for await (const obj of jsonl(body)) { + result.push(obj) + } + + expect(result).toEqual([ + { name: 'Alice' }, + { name: 'Bob' }, + { name: 'Charlie' }, + ]) + }) + + it('should parse JSON lines correctly when last line does not end with newline character', async () => { + const input = + '{"name": "Alice"}\n{"name": "Bob"}\n{"name": "Charlie"}\n{"name": "Dave"}' + + const body = new ReadableStream({ + async start(controller) { + controller.enqueue(new TextEncoder().encode(input)) + controller.close() + }, + }) + + const result = [] + + for await (const obj of jsonl(body)) { + result.push(obj) + } + + expect(result).toEqual([ + { name: 'Alice' }, + { name: 'Bob' }, + { name: 'Charlie' }, + { name: 'Dave' }, + ]) + }) +}) + +describe('getFetchError', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('with valid JSON error response', () => { + it('should create FetchError with nested error object', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + error: { + message: 'Custom error message', + code: 'CUSTOM_ERROR_CODE', + }, + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result).toBeInstanceOf(FetchError) + expect(result.message).toBe('Custom error message') + expect(result.code).toBe('CUSTOM_ERROR_CODE') + expect(result.meta).toBeUndefined() + }) + + it('should create FetchError with flat error object', async () => { + const mockResponse = { + ok: false, + status: 404, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: 'Resource not found', + code: 'NOT_FOUND_RESOURCE', + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result).toBeInstanceOf(FetchError) + expect(result.message).toBe('Resource not found') + expect(result.code).toBe('NOT_FOUND_RESOURCE') + }) + + it('should prefer nested error over flat error', async () => { + const mockResponse = { + ok: false, + status: 500, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: 'Flat message', + code: 'FLAT_CODE', + error: { + message: 'Nested message', + code: 'NESTED_CODE', + }, + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Nested message') + expect(result.code).toBe('NESTED_CODE') + }) + + it('should handle partial nested error objects', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: 'Flat message', + code: 'FLAT_CODE', + error: { + message: 'Nested message only', + // no code in nested error + }, + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Nested message only') + expect(result.code).toBe('FLAT_CODE') + }) + + it('should surface a string `error` field', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest + .fn() + .mockResolvedValue(JSON.stringify({ error: 'The value is invalid' })), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('The value is invalid') + expect(result.code).toBe('BAD_REQUEST') + }) + + it('should surface detail from an `errors` array', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + errors: ['person_seniorities: c_level not allowed'], + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toContain('c_level not allowed') + expect(result.code).toBe('BAD_REQUEST') + }) + + it('should prefer a flat `message` over a string `error` and `errors` array', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: 'Primary message', + error: 'secondary error string', + errors: ['secondary detail'], + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Primary message') + }) + }) + + describe('with invalid JSON response', () => { + it('should fallback to status mappings when JSON parsing fails', async () => { + const mockResponse = { + ok: false, + status: 404, + text: jest.fn().mockResolvedValue('Not Found - invalid JSON'), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Not Found') + expect(result.code).toBe('NOT_FOUND') + }) + + it('should fallback to 500 mappings for unknown status codes', async () => { + const mockResponse = { + ok: false, + status: 999, // unknown status + text: jest.fn().mockResolvedValue('Unknown error'), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Internal Server Error') + expect(result.code).toBe('INTERNAL_SERVER_ERROR') + }) + + it('should handle empty response text', async () => { + const mockResponse = { + ok: false, + status: 500, + text: jest.fn().mockResolvedValue(''), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Internal Server Error') + expect(result.code).toBe('INTERNAL_SERVER_ERROR') + }) + + it('should handle null response text', async () => { + const mockResponse = { + ok: false, + status: 502, + text: jest.fn().mockResolvedValue(null), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Bad Gateway') + expect(result.code).toBe('BAD_GATEWAY') + }) + }) + + describe('with meta parameter', () => { + it('should pass meta parameter to FetchError constructor', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: 'Test error', + code: 'TEST_CODE', + }) + ), + } + + const meta = { url: 'https://api.example.com', requestId: 'req-123' } + + const result = await getFetchError(mockResponse, meta) + + expect(result.name).toBe( + 'FetchError({"url":"https://api.example.com","requestId":"req-123"})' + ) + }) + + it('should handle undefined meta parameter', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: 'Test error', + code: 'TEST_CODE', + }) + ), + } + + const result = await getFetchError(mockResponse, undefined) + + expect(result.name).toBe('FetchError') + }) + + it('should handle null meta parameter', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: 'Test error', + code: 'TEST_CODE', + }) + ), + } + + const result = await getFetchError(mockResponse, null) + + expect(result.name).toBe('FetchError') + }) + }) + + describe('fallback behavior', () => { + it('should fallback through all message options correctly', async () => { + const mockResponse = { + ok: false, + status: 401, + text: jest.fn().mockResolvedValue(JSON.stringify({})), // empty JSON + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Unauthorized') + expect(result.code).toBe('UNAUTHORIZED') + }) + + it('should fallback to 500 when all status lookups fail', async () => { + const mockResponse = { + ok: false, + status: 999, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + // no message or code fields + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Internal Server Error') + expect(result.code).toBe('INTERNAL_SERVER_ERROR') + }) + + it('should handle null and undefined values in JSON gracefully', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: null, + code: undefined, + error: { + message: undefined, + code: null, + }, + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Bad Request') + expect(result.code).toBe('BAD_REQUEST') + }) + }) + + describe('edge cases', () => { + it('should handle response.text() throwing an error', async () => { + const mockResponse = { + ok: false, + status: 500, + text: jest.fn().mockRejectedValue(new Error('Failed to read response')), + } + + await expect(getFetchError(mockResponse)).rejects.toThrow( + 'Failed to read response' + ) + }) + + it('should handle very large JSON responses', async () => { + const largeMessage = 'x'.repeat(10000) + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + message: largeMessage, + code: 'LARGE_ERROR', + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe(largeMessage) + expect(result.code).toBe('LARGE_ERROR') + }) + + it('should handle JSON with unexpected structure', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + error: 'not an object', + message: ['array', 'instead', 'of', 'string'], + code: 123, + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('array,instead,of,string') + // @note numeric json codes are ignored in favor of string codes from statusToCodeMap + expect(result.code).toBe('BAD_REQUEST') + }) + + it('should handle deeply nested error objects', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + error: { + nested: { + message: 'Deep message', + code: 'DEEP_CODE', + }, + message: 'Shallow message', + code: 'SHALLOW_CODE', + }, + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Shallow message') + expect(result.code).toBe('SHALLOW_CODE') + }) + + it('should normalize google api numeric error codes to string codes', async () => { + // google apis return numeric codes in the error body (e.g. {"error": {"code": 404, "message": "..."}}) + // these must map to string codes so isUnknownError() recognizes them as known errors + const mockResponse = { + ok: false, + status: 404, + text: jest.fn().mockResolvedValue( + JSON.stringify({ + error: { + code: 404, + message: 'Requested entity was not found.', + status: 'NOT_FOUND', + }, + }) + ), + } + + const result = await getFetchError(mockResponse) + + expect(result.message).toBe('Requested entity was not found.') + // @note must be the string 'NOT_FOUND', not the number 404, so isUnknownError() works + expect(result.code).toBe('NOT_FOUND') + }) + }) + + describe('assertion behavior', () => { + it('should assert that response is not ok', async () => { + const mockResponse = { + ok: false, + status: 400, + text: jest.fn().mockResolvedValue('{}'), + } + + const result = await getFetchError(mockResponse) + + expect(result).toBeInstanceOf(FetchError) + expect(result.message).toBe('Bad Request') + }) + + // @todo add test for when response.ok is true - should fail assertion + test('should throw assertion error when response is ok', async () => { + const mockResponse = { + ok: true, + status: 200, + text: jest.fn().mockResolvedValue('{}'), + } + + await expect(getFetchError(mockResponse)).rejects.toThrow('Response ok') + }) + }) + + describe('function integration', () => { + it('should work correctly with debug logging enabled', async () => { + const mockResponse = { + ok: false, + status: 404, + text: jest.fn().mockResolvedValue('{"message":"Not found"}'), + } + + const result = await getFetchError(mockResponse) + + expect(result).toBeInstanceOf(FetchError) + expect(result.message).toBe('Not found') + expect(result.code).toBe('NOT_FOUND') + }) + }) +}) + +describe('download', () => { + /** + * Helper to create a mock ReadableStream + * @param {Uint8Array[]} chunks + */ + function createMockStream(chunks) { + let index = 0 + + return { + getReader: () => ({ + read: async () => { + if (index >= chunks.length) { + return { done: true, value: undefined } + } + + return { done: false, value: chunks[index++] } + }, + releaseLock: jest.fn(), + }), + } + } + + describe('basic functionality', () => { + it('should download response within size limit', async () => { + const testData = new TextEncoder().encode('Hello, World!') + const mockResponse = { + body: createMockStream([testData]), + } + + const maxSize = 100 // 100 bytes + const result = await download(mockResponse, maxSize) + + expect(result).toHaveProperty('byteLength') + expect(result.byteLength).toBe(testData.length) + + // Verify the content + const decoder = new TextDecoder() + + expect(decoder.decode(result)).toBe('Hello, World!') + }) + + it('should handle multiple small chunks', async () => { + const chunks = [] + + for (let i = 0; i < 10; i++) { + chunks.push(new TextEncoder().encode(`chunk${i}`)) + } + + const mockResponse = { + body: createMockStream(chunks), + } + + const maxSize = 1024 + const result = await download(mockResponse, maxSize) + + expect(result).toHaveProperty('byteLength') + + const decoder = new TextDecoder() + const text = decoder.decode(result) + + expect(text).toBe( + 'chunk0chunk1chunk2chunk3chunk4chunk5chunk6chunk7chunk8chunk9' + ) + }) + }) + + describe('size limit enforcement', () => { + it('should truncate when single chunk exceeds limit', async () => { + const largeData = new TextEncoder().encode( + 'This is a very long string that exceeds the limit' + ) + const mockResponse = { + body: createMockStream([largeData]), + } + + const maxSize = 10 // Only allow 10 bytes + const result = await download(mockResponse, maxSize) + + expect(result).toHaveProperty('byteLength') + expect(result.byteLength).toBe(maxSize) + + const decoder = new TextDecoder() + + expect(decoder.decode(result)).toBe('This is a ') + }) + + it('should stop reading when cumulative size exceeds limit', async () => { + const chunk1 = new TextEncoder().encode('Hello, ') // 7 bytes + const chunk2 = new TextEncoder().encode('World!') // 6 bytes + const chunk3 = new TextEncoder().encode(' Extra data') // Should not be included + const mockResponse = { + body: createMockStream([chunk1, chunk2, chunk3]), + } + + const maxSize = 10 // Only allow 10 bytes total + const result = await download(mockResponse, maxSize) + + expect(result).toHaveProperty('byteLength') + expect(result.byteLength).toBe(maxSize) + + const decoder = new TextDecoder() + + expect(decoder.decode(result)).toBe('Hello, Wor') // 7 + 3 = 10 bytes + }) + + it('should handle exact boundary match', async () => { + const chunk1 = new Uint8Array(50) + const chunk2 = new Uint8Array(50) + const chunk3 = new Uint8Array(10) // Should not be included + const mockResponse = { + body: createMockStream([chunk1, chunk2, chunk3]), + } + + const maxSize = 100 + const result = await download(mockResponse, maxSize) + + expect(result.byteLength).toBe(maxSize) + }) + + it('should handle chunk that partially exceeds limit', async () => { + const chunk1 = new Uint8Array(300 * 1024) // 300 KB + const chunk2 = new Uint8Array(300 * 1024) // 300 KB - will be truncated + const mockResponse = { + body: createMockStream([chunk1, chunk2]), + } + + const maxSize = 0.5 * 1024 * 1024 // 0.5 MB (512 KB) + const result = await download(mockResponse, maxSize) + + expect(result.byteLength).toBe(maxSize) + }) + + it('should release reader lock when stopping at limit', async () => { + const chunk1 = new Uint8Array(60) + const chunk2 = new Uint8Array(60) // Will be truncated + const releaseLock = jest.fn() + + const mockResponse = { + body: { + getReader: () => { + let index = 0 + const chunks = [chunk1, chunk2] + + return { + read: async () => { + if (index >= chunks.length) { + return { done: true, value: undefined } + } + + return { done: false, value: chunks[index++] } + }, + releaseLock, + } + }, + }, + } + + const maxSize = 100 + const result = await download(mockResponse, maxSize) + + expect(result.byteLength).toBe(maxSize) + expect(releaseLock).toHaveBeenCalled() + }) + }) + + describe('edge cases', () => { + it('should handle empty response', async () => { + const mockResponse = { + body: createMockStream([]), + } + + const maxSize = 100 + const result = await download(mockResponse, maxSize) + + expect(result).toHaveProperty('byteLength') + expect(result.byteLength).toBe(0) + }) + + it('should return empty body when the response body is missing', async () => { + const mockResponse = { + body: null, + } + + const maxSize = 100 + + const result = await download(mockResponse, maxSize) + + expect(result).toBeInstanceOf(ArrayBuffer) + expect(result.byteLength).toBe(0) + }) + + it('should handle zero max size', async () => { + const testData = new TextEncoder().encode('test') + const mockResponse = { + body: createMockStream([testData]), + } + + const maxSize = 0 + const result = await download(mockResponse, maxSize) + + // With zero max size, should return empty buffer + expect(result.byteLength).toBe(0) + }) + + it('should handle very large max size', async () => { + const testData = new TextEncoder().encode('small data') + const mockResponse = { + body: createMockStream([testData]), + } + + const maxSize = Number.MAX_SAFE_INTEGER + const result = await download(mockResponse, maxSize) + + expect(result).toHaveProperty('byteLength') + expect(result.byteLength).toBe(testData.length) + }) + + it('should properly combine chunks in correct order', async () => { + const chunks = [ + new TextEncoder().encode('ABC'), + new TextEncoder().encode('DEF'), + new TextEncoder().encode('GHI'), + ] + + const mockResponse = { + body: createMockStream(chunks), + } + + const maxSize = 100 + const result = await download(mockResponse, maxSize) + + const decoder = new TextDecoder() + + expect(decoder.decode(result)).toBe('ABCDEFGHI') + }) + }) +}) + +describe('withLimit', () => { + /** + * Helper to create a mock fetch that returns a response with the given data + * + * @param {string|Uint8Array} data + * @param {number} [contentLength] + */ + function createMockFetch(data, contentLength) { + return async () => { + const buffer = + typeof data === 'string' ? new TextEncoder().encode(data) : data + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(buffer) + controller.close() + }, + }) + + const headers = new Headers({ + 'content-type': 'text/plain', + }) + + if (contentLength !== undefined) { + headers.set('content-length', contentLength.toString()) + } + + return new Response(stream, { + status: 200, + statusText: 'OK', + headers, + }) + } + } + + describe('basic functionality', () => { + it('should pass through when no maxSize is specified', async () => { + const mockFetch = createMockFetch('Hello, World!') + const limitedFetch = withLimit(mockFetch) + + const response = await limitedFetch('http://example.com') + const text = await response.text() + + expect(text).toBe('Hello, World!') + expect(response.headers.has('X-Content-Truncated')).toBe(false) + }) + + it('should pass through when maxSize is 0', async () => { + const mockFetch = createMockFetch('Hello, World!') + const limitedFetch = withLimit(mockFetch, { maxSize: 0 }) + + const response = await limitedFetch('http://example.com') + const text = await response.text() + + expect(text).toBe('Hello, World!') + expect(response.headers.has('X-Content-Truncated')).toBe(false) + }) + + it('should pass through when maxSize is Infinity', async () => { + const mockFetch = createMockFetch('Hello, World!') + const limitedFetch = withLimit(mockFetch, { maxSize: Infinity }) + + const response = await limitedFetch('http://example.com') + const text = await response.text() + + expect(text).toBe('Hello, World!') + expect(response.headers.has('X-Content-Truncated')).toBe(false) + }) + + it('should not truncate when content is within limit', async () => { + const mockFetch = createMockFetch('Hello!', 6) + const limitedFetch = withLimit(mockFetch, { maxSize: 100 }) + + const response = await limitedFetch('http://example.com') + const text = await response.text() + + expect(text).toBe('Hello!') + expect(response.headers.get('X-Content-Truncated')).toBe(null) + expect(response.headers.get('content-length')).toBe('6') + }) + + it('should preserve bodyless response statuses', async () => { + const limitedFetch = withLimit( + async () => new Response(null, { status: 204 }), + { maxSize: 100 } + ) + + const response = await limitedFetch('http://example.com') + + expect(response.status).toBe(204) + await expect(response.text()).resolves.toBe('') + }) + }) + + describe('truncation behavior', () => { + it('should truncate when content exceeds limit', async () => { + const mockFetch = createMockFetch( + 'This is a long message that will be truncated', + 46 + ) + const limitedFetch = withLimit(mockFetch, { maxSize: 10 }) + + const response = await limitedFetch('http://example.com') + const text = await response.text() + + expect(text).toBe('This is a ') + expect(text.length).toBe(10) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + expect(response.headers.get('X-Content-Original-Size')).toBe('46') + expect(response.headers.get('content-length')).toBe('10') + }) + + it('should truncate large binary data', async () => { + const largeData = new Uint8Array(1024 * 1024) // 1 MB + const mockFetch = createMockFetch(largeData, 1024 * 1024) + const limitedFetch = withLimit(mockFetch, { maxSize: 512 * 1024 }) // 512 KB + + const response = await limitedFetch('http://example.com') + const buffer = await response.arrayBuffer() + + expect(buffer.byteLength).toBe(512 * 1024) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + expect(response.headers.get('X-Content-Original-Size')).toBe( + (1024 * 1024).toString() + ) + expect(response.headers.get('content-length')).toBe( + (512 * 1024).toString() + ) + }) + + it('should handle exact boundary match', async () => { + const mockFetch = createMockFetch('Exact!', 6) + const limitedFetch = withLimit(mockFetch, { maxSize: 6 }) + + const response = await limitedFetch('http://example.com') + const text = await response.text() + + expect(text).toBe('Exact!') + + // @note at exact boundary, it's considered truncated because we reached + // the limit + + expect(response.headers.get('X-Content-Truncated')).toBe('true') + expect(response.headers.get('content-length')).toBe('6') + }) + }) + + describe('header handling', () => { + it('should preserve original headers', async () => { + const mockFetch = async () => { + const headers = new Headers({ + 'content-type': 'application/json', + 'x-custom-header': 'custom-value', + 'content-length': '50', + }) + + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('Short')) + controller.close() + }, + }) + + return new Response(stream, { + status: 200, + headers, + }) + } + + const limitedFetch = withLimit(mockFetch, { maxSize: 100 }) + const response = await limitedFetch('http://example.com') + + expect(response.headers.get('content-type')).toBe('application/json') + expect(response.headers.get('x-custom-header')).toBe('custom-value') + expect(response.headers.get('content-length')).toBe('5') + }) + + it('should handle missing content-length in original response', async () => { + const mockFetch = createMockFetch('Test data without content-length') + const limitedFetch = withLimit(mockFetch, { maxSize: 10 }) + + const response = await limitedFetch('http://example.com') + const text = await response.text() + + expect(text).toBe('Test data ') + expect(response.headers.get('X-Content-Truncated')).toBe('true') + expect(response.headers.get('X-Content-Original-Size')).toBe('unknown') + expect(response.headers.get('content-length')).toBe('10') + }) + + it('should preserve response status and statusText', async () => { + const mockFetch = async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('Error message')) + controller.close() + }, + }) + + return new Response(stream, { + status: 404, + statusText: 'Not Found', + headers: new Headers({ + 'content-type': 'text/plain', + 'content-length': '13', + }), + }) + } + + const limitedFetch = withLimit(mockFetch, { maxSize: 5 }) + const response = await limitedFetch('http://example.com') + + expect(response.status).toBe(404) + expect(response.statusText).toBe('Not Found') + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + }) + + describe('options handling', () => { + it('should use default maxSize from decorator', async () => { + const mockFetch = createMockFetch('This is a test message', 22) + const limitedFetch = withLimit(mockFetch, { maxSize: 10 }) + + const response = await limitedFetch('http://example.com') + const text = await response.text() + + expect(text.length).toBe(10) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + + it('should override default maxSize with option', async () => { + const mockFetch = createMockFetch('This is a test message', 22) + const limitedFetch = withLimit(mockFetch, { maxSize: 100 }) + + const response = await limitedFetch('http://example.com', { maxSize: 10 }) + const text = await response.text() + + expect(text.length).toBe(10) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + + it('should allow disabling limit with option', async () => { + const mockFetch = createMockFetch('This is a test message', 22) + const limitedFetch = withLimit(mockFetch, { maxSize: 10 }) + + const response = await limitedFetch('http://example.com', { + maxSize: Infinity, + }) + const text = await response.text() + + expect(text).toBe('This is a test message') + expect(response.headers.has('X-Content-Truncated')).toBe(false) + }) + }) + + describe('content-type handling', () => { + it('should preserve content-type in blob', async () => { + const mockFetch = async () => { + const headers = new Headers({ + 'content-type': 'application/pdf', + 'content-length': '100', + }) + + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(50)) + controller.close() + }, + }) + + return new Response(stream, { + status: 200, + headers, + }) + } + + const limitedFetch = withLimit(mockFetch, { maxSize: 20 }) + const response = await limitedFetch('http://example.com') + + expect(response.headers.get('content-type')).toBe('application/pdf') + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + + it('should default to application/octet-stream when content-type is missing', async () => { + const mockFetch = async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('Data')) + controller.close() + }, + }) + + return new Response(stream, { + status: 200, + headers: new Headers(), + }) + } + + const limitedFetch = withLimit(mockFetch, { maxSize: 100 }) + const response = await limitedFetch('http://example.com') + + const blob = await response.blob() + + expect(blob.type).toBe('application/octet-stream') + }) + }) + + describe('content type remapping', () => { + it('should remap content type when truncated with contentTypeRemap option', async () => { + const mockFetch = createMockFetch( + 'This is a long JSON response that will be truncated', + 52 + ) + + const jsonMockFetch = async () => { + const response = await mockFetch() + const headers = new Headers(response.headers) + + headers.set('content-type', 'application/json') + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }) + } + + const limitedFetch = withLimit(jsonMockFetch, { + maxSize: 10, + contentTypeRemap: { + 'application/json': 'text/plain', + }, + }) + + const response = await limitedFetch('http://example.com') + + expect(response.headers.get('content-type')).toBe('text/plain') + expect(response.headers.get('X-Content-Original-Type')).toBe( + 'application/json' + ) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + + it('should remap PNG to octet-stream when truncated', async () => { + const largeData = new Uint8Array(100) + + const mockFetch = async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(largeData) + controller.close() + }, + }) + + const headers = new Headers({ + 'content-type': 'image/png', + 'content-length': '100', + }) + + return new Response(stream, { + status: 200, + statusText: 'OK', + headers, + }) + } + + const limitedFetch = withLimit(mockFetch, { + maxSize: 10, + contentTypeRemap: { + 'image/png': 'application/octet-stream', + }, + }) + + const response = await limitedFetch('http://example.com') + + expect(response.headers.get('content-type')).toBe( + 'application/octet-stream' + ) + expect(response.headers.get('X-Content-Original-Type')).toBe('image/png') + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + + it('should not remap content type when not truncated', async () => { + const mockFetch = async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('Short')) + controller.close() + }, + }) + + const headers = new Headers({ + 'content-type': 'application/json', + 'content-length': '5', + }) + + return new Response(stream, { + status: 200, + statusText: 'OK', + headers, + }) + } + + const limitedFetch = withLimit(mockFetch, { + maxSize: 100, + contentTypeRemap: { + 'application/json': 'text/plain', + }, + }) + + const response = await limitedFetch('http://example.com') + + expect(response.headers.get('content-type')).toBe('application/json') + expect(response.headers.has('X-Content-Original-Type')).toBe(false) + expect(response.headers.get('X-Content-Truncated')).toBe(null) + }) + + it('should keep original content type when no remap is provided', async () => { + const mockFetch = async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('This is a long message') + ) + controller.close() + }, + }) + + const headers = new Headers({ + 'content-type': 'application/x-random', + 'content-length': '22', + }) + + return new Response(stream, { + status: 200, + statusText: 'OK', + headers, + }) + } + + const limitedFetch = withLimit(mockFetch, { + maxSize: 10, + }) + + const response = await limitedFetch('http://example.com') + + expect(response.headers.get('content-type')).toBe('application/x-random') + expect(response.headers.has('X-Content-Original-Type')).toBe(false) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + + it('should keep original content type when truncated but not in remap', async () => { + const mockFetch = async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('This is a long message') + ) + controller.close() + }, + }) + + const headers = new Headers({ + 'content-type': 'application/xml', + 'content-length': '22', + }) + + return new Response(stream, { + status: 200, + statusText: 'OK', + headers, + }) + } + + const limitedFetch = withLimit(mockFetch, { + maxSize: 10, + contentTypeRemap: { + 'application/json': 'text/plain', + }, + }) + + const response = await limitedFetch('http://example.com') + + expect(response.headers.get('content-type')).toBe('application/xml') + expect(response.headers.has('X-Content-Original-Type')).toBe(false) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + + it('should handle content type with charset parameters', async () => { + const mockFetch = async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('This is a long message') + ) + controller.close() + }, + }) + + const headers = new Headers({ + 'content-type': 'application/json; charset=utf-8', + 'content-length': '22', + }) + + return new Response(stream, { + status: 200, + statusText: 'OK', + headers, + }) + } + + const limitedFetch = withLimit(mockFetch, { + maxSize: 10, + contentTypeRemap: { + 'application/json': 'text/plain', + }, + }) + + const response = await limitedFetch('http://example.com') + + expect(response.headers.get('content-type')).toBe('text/plain') + expect(response.headers.get('X-Content-Original-Type')).toBe( + 'application/json; charset=utf-8' + ) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + + it('should support option-level contentTypeRemap override', async () => { + const mockFetch = async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('This is a long message') + ) + controller.close() + }, + }) + + const headers = new Headers({ + 'content-type': 'application/json', + 'content-length': '22', + }) + + return new Response(stream, { + status: 200, + statusText: 'OK', + headers, + }) + } + + const limitedFetch = withLimit(mockFetch, { + maxSize: 10, + contentTypeRemap: { + 'application/json': 'text/plain', + }, + }) + + const response = await limitedFetch('http://example.com', { + contentTypeRemap: { + 'application/json': 'application/octet-stream', + }, + }) + + expect(response.headers.get('content-type')).toBe( + 'application/octet-stream' + ) + expect(response.headers.get('X-Content-Original-Type')).toBe( + 'application/json' + ) + expect(response.headers.get('X-Content-Truncated')).toBe('true') + }) + }) +}) + +describe('anySignal', () => { + it('should return a signal that is not any of the input signals when one is pre-aborted', () => { + const ac = new AbortController() + + ac.abort('already done') + + const result = anySignal([ac.signal]) + + // @note the bug: anySignal returns the original input signal instead of + // controller.signal when given a pre-aborted signal + expect(result).not.toBe(ac.signal) + expect(result.aborted).toBe(true) + expect(result.reason).toBe('already done') + }) + + it('should return a consistent signal type regardless of pre-aborted input position', () => { + const ac1 = new AbortController() + const ac2 = new AbortController() + + ac2.abort('reason2') + + const result = anySignal([ac1.signal, ac2.signal]) + + expect(result).not.toBe(ac1.signal) + expect(result).not.toBe(ac2.signal) + expect(result.aborted).toBe(true) + expect(result.reason).toBe('reason2') + }) + + it('should return a non-aborted signal for empty array', () => { + const result = anySignal([]) + + expect(result.aborted).toBe(false) + }) + + it('should skip null and undefined entries', () => { + const result = anySignal([null, undefined, null]) + + expect(result.aborted).toBe(false) + }) + + it('should abort when any input signal aborts later', () => { + const ac1 = new AbortController() + const ac2 = new AbortController() + + const result = anySignal([ac1.signal, ac2.signal]) + + expect(result.aborted).toBe(false) + + ac1.abort('reason1') + + expect(result.aborted).toBe(true) + expect(result.reason).toBe('reason1') + }) + + it('should propagate abort reason from the second signal', () => { + const ac1 = new AbortController() + const ac2 = new AbortController() + + const result = anySignal([ac1.signal, ac2.signal]) + + ac2.abort('reason2') + + expect(result.aborted).toBe(true) + expect(result.reason).toBe('reason2') + }) +}) diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts new file mode 100644 index 0000000..849d789 --- /dev/null +++ b/packages/fetch/src/index.ts @@ -0,0 +1,1176 @@ +import debug, { assert, fassert } from '@chatbotkit-dev/debug' +import { SystemError } from '@chatbotkit-dev/errors' +import type { Thrown } from '@chatbotkit-dev/errors' +import { + FAILURE_CODE_HEADER_NAME, + LIMITS_REACHED_CODE, + statusToCodeMap, + statusToMessageMap, +} from '@chatbotkit-dev/http-codes' + +// @note `withNextCache` passes Next.js's `next` request option, which Next +// declares by augmenting RequestInit globally. Outside the application that +// augmentation is not in scope, so it is reproduced here - in the entry point, +// so consumers that type-check this package from source see it too. + +declare global { + interface RequestInit { + next?: { + tags?: string[] + revalidate?: number | false + } + } +} + +export const ABORT_ERROR_NAME = 'AbortError' +export const TIMEOUT_ERROR_NAME = 'TimeoutError' + +export const FETCH_PHASE_TAG = 'fetch.phase' +export const FETCH_PHASE_RESPONSE_HEADERS = 'response-headers' +export const FETCH_PHASE_RESPONSE_BODY = 'response-body' + +export const CONTEXT_MODEL_TAG = 'context.model' + +export const DEFAULT_TIMEOUT = 30000 + +export const DEFAULT_RETRIES = 5 +export const DEFAULT_RETRY_DELAY = 250 +export const DEFAULT_RETRY_TIMEOUT = false + +export const HEADER_CONTENT_TRUNCATED = 'x-content-truncated' +export const HEADER_CONTENT_ORIGINAL_SIZE = 'x-content-original-size' +export const HEADER_CONTENT_ORIGINAL_TYPE = 'x-content-original-type' + +const globalObject = typeof global !== 'undefined' ? global : globalThis + +// we need to polyfill the ReadableStream for chrome and Safari +{ + if ( + typeof globalObject !== 'undefined' && + typeof globalObject.ReadableStream === 'function' && + // @ts-ignore + typeof globalObject.ReadableStream.prototype[Symbol.asyncIterator] !== + 'function' + ) { + // @ts-ignore + globalObject.ReadableStream.prototype[Symbol.asyncIterator] = function () { + const reader = this.getReader() + + return { + next: () => reader.read(), + return: () => { + reader.releaseLock() + + return Promise.resolve({ done: true }) + }, + } + } + } +} + +/** + * Represents an error that occurs during a fetch operation. + */ +export class FetchError extends SystemError { + constructor(message: string, code: string, meta?: Record) { + super(message, code) + + this.name = meta + ? `FetchError(${JSON.stringify(meta || {})})` + : 'FetchError' + } +} + +/** + * Represents an error that occurs when a fetch operation is aborted. + */ +export class AbortError extends SystemError { + constructor(message?: string) { + super(message || ABORT_ERROR_NAME, ABORT_ERROR_NAME) + + this.name = ABORT_ERROR_NAME + } +} + +/** + * Represents an error that occurs when a fetch operation times out. + */ +export class TimeoutError extends SystemError { + /** + * `data` is Sentry capture context (e.g. `{ tags, extra }`) carried through to + * `captureError` so timeouts arrive with the url, timeout budget, model and + * retry stats attached instead of a bare `TimeoutError`. + */ + constructor(message?: string, data?: unknown) { + super(message || TIMEOUT_ERROR_NAME, TIMEOUT_ERROR_NAME, data) + + this.name = TIMEOUT_ERROR_NAME + } +} + +/** + * Merge Sentry capture context (`tags`/`extra`) into an error's `data` field + * (read by `buildCaptureContext` in `@/lib/error`) without clobbering anything + * already there. Used by the fetch wrappers to annotate the error they throw + * with diagnostics (url, timeout budget, attempt count, elapsed time) so the + * Sentry event explains *why* and *how hard* a request failed. + */ +function annotateCaptureData( + error: Thrown, + { + tags, + extra, + }: { tags?: Record; extra?: Record } = {} +): Thrown { + if (!error || typeof error !== 'object') { + return error + } + + const existing = + error.data && typeof error.data === 'object' && !Array.isArray(error.data) + ? error.data + : undefined + + // @note if the error already carried non-object `data` (e.g. a raw string), + // preserve it under extra rather than dropping it + + const preserved = + error.data !== undefined && !existing + ? { originalData: error.data } + : undefined + + error.data = { + ...(existing || {}), + + tags: { ...(existing?.tags || {}), ...(tags || {}) }, + + extra: { ...(existing?.extra || {}), ...preserved, ...(extra || {}) }, + } + + return error +} + +/** + * Fetches a resource from the network. + * + * @throws + */ +export function fetch( + url: string | URL, + init?: RequestInit +): Promise { + debug(`fetching`, { url, init }).log('fetch.fetch') + + fassert(() => { + let href + + if (typeof url === 'object' && url !== null && 'href' in url) { + href = url.href + } else { + href = url + } + + return /^(?:https?:\/\/|data:|blob:|\/)/i.test(href || '') + }, `url ${url} is not fetchable`) + + const nativeFetch = + typeof globalObject !== 'undefined' ? globalObject.fetch : undefined + + if (!nativeFetch) { + throw new Error(`No suitable fetch implementation found`) + } + + return nativeFetch(url, init) +} + +/** + * Expose a JSONL stream as an async iterable. + */ +export async function* jsonl( + body: ReadableStream & { + [Symbol.asyncIterator](): AsyncIterator + } +): AsyncGenerator> { + try { + const decoder = new TextDecoder() + + let previous = '' + + for await (const chunk of body) { + previous += decoder.decode(chunk) + + while (true) { + const eolIndex = previous.indexOf('\n') + + if (eolIndex < 0) { + break + } + + const line = previous.slice(0, eolIndex + 1) + + if (line) { + yield JSON.parse(line) + } + + previous = previous.slice(eolIndex + 1) + } + } + + if (previous.trim().length > 0) { + yield JSON.parse(previous) + } + } catch (error: Thrown) { + if (error?.name !== ABORT_ERROR_NAME) { + throw error + } + } +} + +/** + * Downloads a response up to the specified size limit. Consumes the stream and + * returns data up to maxSize bytes. * Does not throw on size limit - simply + * stops reading at the limit. + */ +export async function download( + response: Response, + maxSize: number +): Promise { + debug(`downloading with size limit`, { maxSize }).log('fetch.download') + + if (!response.body) { + return new ArrayBuffer(0) + } + + const reader = response.body.getReader() + + const chunks: Uint8Array[] = [] + + let receivedLength = 0 + + try { + while (true) { + const { done, value } = await reader.read() + + if (done) { + break + } + + const remainingSpace = maxSize - receivedLength + + if (value.length > remainingSpace) { + if (remainingSpace > 0) { + chunks.push(value.slice(0, remainingSpace)) + + receivedLength += remainingSpace + + debug(`chunk truncated to fit limit`, { + chunkSize: value.length, + taken: remainingSpace, + receivedLength, + }).log('fetch.download') + } + + break + } + + chunks.push(value) + + receivedLength += value.length + + debug(`received chunk`, { chunkSize: value.length, receivedLength }).log( + 'fetch.download' + ) + } + } finally { + reader.releaseLock() + } + + debug(`download complete`, { receivedLength }).log('fetch.download') + + const allChunks = new Uint8Array(receivedLength) + + let position = 0 + + for (const chunk of chunks) { + allChunks.set(chunk, position) + + position += chunk.length + } + + return allChunks.buffer +} + +/** + * Check if the response indicates an error and if so, create a FetchError. + */ +export async function getFetchError( + response: Response, + meta?: Record +): Promise { + assert(!response.ok, `Response ok`) + + const status = response.status + const text = await response.text() + + debug(`fetch error`, { status, text }).log('fetch.getFetchError') + + let json + + try { + json = JSON.parse(text) + } catch { + json = { + message: statusToMessageMap[status], + code: statusToCodeMap[status], + } + } + + // @note prefer string codes from statusToCodeMap over numeric codes from json bodies, + // since numeric codes (e.g. google api returns 404 as a number) are not recognized + // by isUnknownError() which checks against string codes only + const codeFromJson = + (typeof json?.error?.code === 'string' && json.error.code) || + (typeof json?.code === 'string' && json.code) || + null + + // @note surface upstream validation detail held in less common shapes (a + // string `error`, or an `errors` array) so callers - e.g. agents - can see + // exactly what was rejected and self-correct, instead of a generic status + // message + + return new FetchError( + json?.error?.message || + json?.message || + (typeof json?.error === 'string' ? json.error : undefined) || + (Array.isArray(json?.errors) && json.errors.length + ? JSON.stringify(json.errors) + : undefined) || + statusToMessageMap[status] || + statusToMessageMap[500], + codeFromJson || statusToCodeMap[status] || statusToCodeMap[500], + meta + ) +} + +/** + * Returns an AbortSignal that resolves when any of the provided signals abort. + */ +export function anySignal( + signals: (AbortSignal | null | undefined)[] +): AbortSignal { + const controller = new AbortController() + + for (const signal of signals) { + if (!signal) { + continue + } + + if (signal.aborted) { + controller.abort(signal.reason) + + return controller.signal + } + + signal.addEventListener('abort', () => controller.abort(signal.reason), { + signal: controller.signal, + }) + } + + return controller.signal +} + +export type FetchFn = ( + url: string | URL, + options?: RequestInit & T +) => Promise + +export type withDebugOptions = object + +/** + * @todo move to @chatbotkit/fetch sdk + */ +export function withDebug( + fetch: FetchFn, + defaultOptions?: withDebugOptions +): FetchFn { + debug(`with debug`, { defaultOptions }).log('fetch.withDebug') + + return async function fetchWithDebug( + url: string | URL, + options?: RequestInit & withDebugOptions + ): Promise { + debug(`fetching`, { url, options }).log('fetch.withDebug.fetchWithDebug') + + const response = await fetch(url, { ...options }) + + debug(`fetched`, { url, options, response }).log( + 'fetch.withDebug.fetchWithDebug' + ) + + return response + } +} + +export type withInitOptions = { + duplex?: 'half' +} + +/** + * @todo move to @chatbotkit/fetch sdk + */ +export function withInit( + fetch: FetchFn, + defaultOptions?: withInitOptions +): FetchFn { + debug(`with init`, { defaultOptions }).log('fetch.withInit') + + return async function fetchWithInit( + url: string | URL, + options?: RequestInit & withInitOptions + ): Promise { + debug(`fetching`, { url, options, defaultOptions }).log( + 'fetch.withInit.fetchWithInit' + ) + + const response = await fetch(url, { ...options, ...defaultOptions }) + + debug(`fetched`, { url, options, response }).log( + 'fetch.withInit.fetchWithInit' + ) + + return response + } +} + +export type withCacheOptions = { + ttl?: number +} + +/** + * @todo move to @chatbotkit/fetch sdk + */ +export function withCache( + fetch: FetchFn, + defaultOptions?: withCacheOptions +): FetchFn { + debug(`with cache`, { defaultOptions }).log('fetch.withCache') + + const cache = new Map() + + return async function fetchWithCache( + url: string | URL, + options?: RequestInit & withCacheOptions + ): Promise { + const ttl = options?.ttl ?? defaultOptions?.ttl ?? 60000 + + const key = JSON.stringify([url, options]) + + const cachedEntry = cache.get(key) + + const currentTime = Date.now() + + if (cachedEntry && currentTime - cachedEntry.timestamp < ttl) { + return cachedEntry.response.clone() + } + + const response = await fetch(url, { ...options }) + + if (response.ok) { + // @todo make this work because it doesn't + // cache.set(key, { + // timestamp: currentTime, + // response: response.clone(), + // }) + } + + return response + } +} + +export type withNextCacheOptions = { + tags?: string[] + ttl?: number +} + +/** + * @todo move to @chatbotkit/fetch sdk + */ +export function withNextCache( + fetch: FetchFn, + defaultOptions?: withNextCacheOptions +): FetchFn { + debug(`with next cache`, { defaultOptions }).log('fetch.withNextCache') + + return async function fetchWithNextCache( + url: string | URL, + options?: RequestInit & withNextCacheOptions + ): Promise { + const tags = options?.tags ?? defaultOptions?.tags ?? [] + const ttl = options?.ttl ?? defaultOptions?.ttl ?? 60000 + + const response = await fetch(url, { + ...options, + + cache: 'force-cache', + + next: { + tags, + + revalidate: Math.round(ttl / 1000), + }, + }) + + return response + } +} + +/** + * Diagnostic identifiers for a fetch (e.g. the model being called). Ignored by + * the network layer; surfaced onto any `TimeoutError`'s Sentry context so the + * event records which upstream stalled. + */ +export type FetchMeta = { model?: string } & Record + +export type withTimeoutOptions = { + timeout?: number + meta?: FetchMeta +} + +/** + * Add timeout capabilities to any fetch implementation. + * + * @todo move to @chatbotkit/fetch sdk + */ +export function withTimeout( + fetch: FetchFn, + defaultOptions?: withTimeoutOptions +): FetchFn { + debug(`with timeout`, { defaultOptions }).log('fetch.withTimeout') + + return async function fetchWithTimeout( + url: string | URL, + options?: RequestInit & withTimeoutOptions + ): Promise { + const timeout = + options?.timeout ?? defaultOptions?.timeout ?? DEFAULT_TIMEOUT + + debug(`fetching with timeout`, { url, timeout }).log( + 'fetch.withTimeout.fetchWithTimeout' + ) + + // @note the timeout only covers time-to-response-headers - `fetch()` + // resolves once headers arrive and the body is streamed afterwards, outside + // this guard. A `TimeoutError` here therefore means the upstream never + // started responding within the budget (e.g. a stalled/overloaded gateway), + // NOT that generation was slow. We attach that context so the Sentry event + // says exactly what timed out instead of a bare `TimeoutError`. + + const meta = options?.meta + + const makeTimeoutError = () => + new TimeoutError(undefined, { + tags: { + [FETCH_PHASE_TAG]: FETCH_PHASE_RESPONSE_HEADERS, + + ...(meta?.model ? { [CONTEXT_MODEL_TAG]: String(meta.model) } : {}), + }, + + extra: { + fetch: { + url: typeof url === 'string' ? url : url?.toString?.(), + + timeoutMs: timeout, + + ...(meta || {}), + }, + }, + }) + + let signal + let handler + + let isTimeOutAbort = false + + if (timeout > 0 && timeout !== Infinity) { + const abortController = new AbortController() + + // @todo use AbortSignal.timeout(n) when widely supported, right now there + // in fact little to no support with known bugs in Chrome + + handler = setTimeout(() => { + debug(`aborting fetch`, { url }).log( + 'fetch.withTimeout.fetchWithTimeout' + ) + + isTimeOutAbort = true + + abortController.abort(makeTimeoutError()) + }, timeout) + + // @todo use AbortSignal.any([]) when widely supported, right now most + // implementation simply do not have it + + signal = options?.signal + ? anySignal([abortController.signal, options.signal]) + : abortController.signal + } else { + signal = options?.signal + } + + let response + + try { + response = await fetch(url, { + ...options, + + signal, + }) + } catch (error: Thrown) { + // @note we have a problem because some implementation (Chrome) do not + // correctly transfer the real reason for the abort i.e. the timeout + // error, so we need to check if we have raised a timeout above and if so + // we need to throw the correct error + + if ([error?.name, error?.message].includes(ABORT_ERROR_NAME)) { + if (isTimeOutAbort) { + throw makeTimeoutError() + } + } + + throw error + } finally { + clearTimeout(handler) + } + + return response + } +} + +export type withBodyTimeoutOptions = { + bodyTimeout?: number + meta?: FetchMeta +} + +/** + * Guard the *body* (post-headers) phase of a streaming response against a + * stalled upstream. {@link withTimeout} only covers time-to-response-headers - + * once headers arrive the body is streamed afterwards, outside that guard, so a + * gateway that returns headers and then goes silent (sends no tokens) hangs + * until undici's ~300s default body timeout. That surfaces as a bare + * `TypeError: terminated` (cause `UND_ERR_BODY_TIMEOUT`) with no model + * attribution, burns the whole turn for 0 tokens, and leaves it "incomplete". + * + * This wrapper caps the gap between body chunks - including time-to-first-chunk. + * If no chunk arrives within `bodyTimeout` ms it cancels the underlying body + * (releasing the socket) and surfaces the same annotated {@link TimeoutError} + * that {@link withTimeout} throws, so the stall is attributable in Sentry + * (`context.model`, `fetch.phase: response-body`) instead of opaque. The timer + * resets on every received chunk, so a slow-but-steady stream is never killed. + * + * @todo move to @chatbotkit/fetch sdk + */ +export function withBodyTimeout( + fetch: FetchFn, + defaultOptions?: withBodyTimeoutOptions +): FetchFn { + debug(`with body timeout`, { defaultOptions }).log('fetch.withBodyTimeout') + + return async function fetchWithBodyTimeout( + url: string | URL, + options?: RequestInit & withBodyTimeoutOptions + ): Promise { + const bodyTimeout = options?.bodyTimeout ?? defaultOptions?.bodyTimeout ?? 0 + + const response = await fetch(url, options) + + // @note nothing to guard: disabled, no body, or an error response the caller + // will read in full (e.g. to extract the upstream error message) rather than + // stream. Wrapping a non-ok body would arm a timer on a stream the caller may + // never iterate. + + if ( + !bodyTimeout || + bodyTimeout === Infinity || + !response.body || + !response.ok + ) { + return response + } + + const meta = options?.meta ?? defaultOptions?.meta + + const makeTimeoutError = () => + new TimeoutError(undefined, { + tags: { + [FETCH_PHASE_TAG]: FETCH_PHASE_RESPONSE_BODY, + + ...(meta?.model ? { [CONTEXT_MODEL_TAG]: String(meta.model) } : {}), + }, + + extra: { + fetch: { + url: typeof url === 'string' ? url : url?.toString?.(), + + bodyTimeoutMs: bodyTimeout, + + ...(meta || {}), + }, + }, + }) + + const source = response.body + + // @note hoisted so `cancel` can tear down through the reader - `start` + // locks `source` with this reader, and a locked stream cannot be cancelled + // directly (it throws), so the cancel path must go through the reader. + let reader + + const guarded = new ReadableStream({ + start(controller) { + reader = source.getReader() + + let timer + + let settled = false + + // @note run every terminal controller transition exactly once, clearing + // the idle timer first. Wrapped in try/catch because a consumer that + // cancels mid-stream can move the controller to a closed state before we + // observe it - closing/erroring it again would otherwise throw. + + const settle = (transition) => { + if (settled) { + return + } + + settled = true + + if (timer) { + clearTimeout(timer) + + timer = undefined + } + + try { + transition() + } catch { + // @note controller already closed/errored (e.g. consumer cancelled) + } + } + + const pump = async () => { + try { + while (true) { + timer = setTimeout(() => { + const error = makeTimeoutError() + + // @note release the upstream socket, then surface the timeout to + // the consumer's read/`for await` + + void reader.cancel(error).catch(() => {}) + + settle(() => controller.error(error)) + }, bodyTimeout) + + const { done, value } = await reader.read() + + if (timer) { + clearTimeout(timer) + + timer = undefined + } + + if (settled) { + return + } + + if (done) { + settle(() => controller.close()) + + return + } + + controller.enqueue(value) + } + } catch (error: Thrown) { + settle(() => controller.error(error)) + } + } + + void pump() + }, + + cancel(reason) { + // @note cancel through the reader (it holds the lock on `source`). + // Swallow rejections: an already-errored/cancelled reader rejects here, + // and a consumer that stopped early does not care about the outcome. + return reader?.cancel(reason).catch(() => {}) + }, + }) + + return new Response(guarded, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }) + } +} + +/** + * Recognises the one error {@link withBodyTimeout} throws: a `TimeoutError` + * tagged `fetch.phase: response-body`. Lives here, next to the wrapper that + * produces it and keyed off the same {@link FETCH_PHASE_TAG} / + * {@link FETCH_PHASE_RESPONSE_BODY} constants, so the producer and this + * recogniser cannot drift. + * + * It exists because a body-phase stall is raised *while the streaming body is + * being consumed* - downstream of {@link withRetry}, which has already returned + * the (headers-ok) response and so never retries it. The streaming layer uses + * this to decide a stall is safe to re-issue (header-phase timeouts, already + * retried at the fetch layer, and real provider errors are deliberately + * excluded). + */ +export function isBodyStallTimeout(error: Thrown): boolean { + return ( + error?.name === TIMEOUT_ERROR_NAME && + error?.data?.tags?.[FETCH_PHASE_TAG] === FETCH_PHASE_RESPONSE_BODY + ) +} + +export type withRetryOptions = { + retries?: number + retryDelay?: number + retryTimeout?: boolean + meta?: FetchMeta +} + +/** + * Add retry capabilities to any fetch implementation. + * + * @todo move to @chatbotkit/fetch sdk + */ +export function withRetry( + fetch: FetchFn, + defaultOptions?: withRetryOptions +): FetchFn { + debug(`with retry`, { defaultOptions }).log('fetch.withRetry') + + return async function fetchWithRetry( + url: string | URL, + options?: RequestInit & withRetryOptions + ): Promise { + const maxRetries = + options?.retries ?? defaultOptions?.retries ?? DEFAULT_RETRIES + + const retryTimeout = + options?.retryTimeout ?? + defaultOptions?.retryTimeout ?? + DEFAULT_RETRY_TIMEOUT + + // @note attempts run in a loop rather than via recursion, so the stats we + // report on a final failure (how many attempts, how long they took) live in + // plain local state instead of being threaded through `options`. `attempt` + // is the 1-based number of the attempt currently running; `retriesLeft` + // counts down; `retryDelay` doubles each retry (exponential backoff). + + const startedAt = Date.now() + + let retriesLeft = maxRetries + + let retryDelay = + options?.retryDelay ?? defaultOptions?.retryDelay ?? DEFAULT_RETRY_DELAY + + let attempt = 0 + + /** + * Annotate the about-to-be-thrown error with retry diagnostics for Sentry. + */ + const annotateRetry = (error) => + annotateCaptureData(error, { + tags: { + 'fetch.attempts': String(attempt), + + 'fetch.outcome': [error?.name, error?.message].includes( + TIMEOUT_ERROR_NAME + ) + ? 'timeout' + : 'error', + }, + + extra: { + fetchRetry: { + url: typeof url === 'string' ? url : url?.toString?.(), + + attempts: attempt, + + maxAttempts: maxRetries + 1, + + elapsedMs: Date.now() - startedAt, + + retryTimeout, + }, + }, + }) + + while (true) { + attempt += 1 + + debug(`fetching with retry`, { + url, + retries: retriesLeft, + retryDelay, + retryTimeout, + }).log('fetch.withRetry.fetchWithRetry') + + let response + + try { + response = await fetch(url, { ...options }) + + if (!response.ok) { + debug(`response not ok`, { + url, + status: response.status, + statusText: response.statusText, + }).log('fetch.withRetry.fetchWithRetry') + + switch (response.status) { + // we always attempt to retry if it is one of the following error + // codes as long as we do not also have a corner case + + case 429: + // as a special case, we return 429 when we have exceeded account + + if ( + response.headers.get(FAILURE_CODE_HEADER_NAME) === + LIMITS_REACHED_CODE + ) { + return response + } + + case 500: + case 502: + case 503: + case 504: + // Previously we were not retrying status code 503 which meant that + // some OpenAI requests were failing due to 503 being treated + // similar to 429 when the model is overloaded. + + // @note we don't use getFetchError here because we don't want to + // read the response body, we just want to throw an error with the + // status code and the URL + + throw new FetchError( + `Fetch failed with status ${response.status} (${response.statusText})`, + statusToCodeMap[response.status] || statusToCodeMap[500], + { + url: new URL(url).href, + options: options, + } + ) + + // by default we return the response as is + + default: + return response + } + } + + return response + } catch (error: Thrown) { + debug(`fetch error`, { url, error }).log( + 'fetch.withRetry.fetchWithRetry' + ) + + // @note never retry a timeout when the caller opted out + + if ( + [error?.name, error?.message].includes(TIMEOUT_ERROR_NAME) && + !retryTimeout + ) { + debug(`not retrying timeout`, { url, error }).log( + 'fetch.withRetry.fetchWithRetry' + ) + + throw annotateRetry(error) + } + + // @note out of retries - surface the last bad response if we have one, + // otherwise throw the (annotated) error + + if (retriesLeft === 0) { + debug(`no retries left`, { url, error, retries: retriesLeft }).log( + 'fetch.withRetry.fetchWithRetry' + ) + + if (response) { + return response + } + + throw annotateRetry(error) + } + + debug(`sleeping`, { retryDelay }).log('fetch.withRetry.fetchWithRetry') + + await new Promise((resolve) => setTimeout(resolve, retryDelay)) + + retriesLeft -= 1 + retryDelay *= 2 + + debug(`retrying fetch`, { url, retries: retriesLeft, retryDelay }).log( + 'fetch.withRetry.fetchWithRetry' + ) + } + } + } +} + +export type withLimitOptions = { + maxSize?: number + contentTypeRemap?: Record +} + +/** + * Add response size limiting to any fetch implementation. Downloads response up + * to maxSize bytes and adds a header to indicate truncation. + * + * @todo move to @chatbotkit/fetch sdk + */ +export function withLimit( + fetch: FetchFn, + defaultOptions?: withLimitOptions +): FetchFn { + debug(`with limit`, { defaultOptions }).log('fetch.withLimit') + + return async function fetchWithLimit( + url: string | URL, + options?: RequestInit & withLimitOptions + ): Promise { + const maxSize = options?.maxSize ?? defaultOptions?.maxSize + + if (!maxSize || maxSize <= 0 || maxSize === Infinity) { + return await fetch(url, options) + } + + const contentTypeRemap = options?.contentTypeRemap ?? + defaultOptions?.contentTypeRemap ?? { + 'application/json': 'text/plain', + 'application/yaml': 'text/plain', + 'application/xml': 'text/plain', + 'text/xml': 'text/plain', + 'text/html': false, + 'application/xhtml+xml': 'text/html', + 'text/csv': false, + } + + debug(`fetching with size limit`, { url, maxSize, contentTypeRemap }).log( + 'fetch.withLimit.fetchWithLimit' + ) + + const response = await fetch(url, options) + + const contentLength = response.headers.get('content-length') + + const expectedSize = contentLength ? parseInt(contentLength, 10) : null + + const willTruncate = + expectedSize !== null && !isNaN(expectedSize) && expectedSize > maxSize + + debug(`response received`, { + contentLength, + expectedSize, + willTruncate, + }).log('fetch.withLimit.fetchWithLimit') + + const buffer = await download(response, maxSize) + + debug(`download complete`, { bufferSize: buffer.byteLength }).log( + 'fetch.withLimit.fetchWithLimit' + ) + + const wasTruncated = buffer.byteLength >= maxSize + + const originalContentType = + response.headers.get('content-type') || 'application/octet-stream' + + let contentType = originalContentType + + if (wasTruncated) { + if (contentTypeRemap) { + const mainType = originalContentType.split(';')[0].trim() + + switch (true) { + case mainType in contentTypeRemap: { + contentType = + contentTypeRemap[mainType] === false + ? mainType + : contentTypeRemap[mainType] + + break + } + + case originalContentType in contentTypeRemap: { + contentType = + contentTypeRemap[originalContentType] === false + ? originalContentType + : contentTypeRemap[originalContentType] + + break + } + } + } else { + contentType = 'application/octet-stream' + } + } + + const blob = new Blob([buffer], { + type: contentType, + }) + + const newHeaders = new Headers(response.headers) + + if (wasTruncated) { + newHeaders.set(HEADER_CONTENT_TRUNCATED, 'true') + newHeaders.set( + HEADER_CONTENT_ORIGINAL_SIZE, + expectedSize?.toString() || 'unknown' + ) + + if (contentType !== originalContentType) { + newHeaders.set('content-type', contentType) + + newHeaders.set(HEADER_CONTENT_ORIGINAL_TYPE, originalContentType) + } + } + + newHeaders.set('content-length', buffer.byteLength.toString()) + + const responseBody = [204, 205, 304].includes(response.status) ? null : blob + + const limitedResponse = new Response(responseBody, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, + }) + + debug(`response limited`, { + originalSize: expectedSize, + limitedSize: buffer.byteLength, + truncated: wasTruncated, + originalContentType, + contentType, + }).log('fetch.withLimit.fetchWithLimit') + + return limitedResponse + } +} + +// @note the composition erases the inner wrapper's options from the inferred +// type: withRetry only knows it returns a FetchFn. The +// assertion restores what a caller can actually pass. + +export const fetchPlusPlus = withRetry(withTimeout(fetch)) as FetchFn< + withTimeoutOptions & withRetryOptions +> + +export default fetch diff --git a/packages/fetch/tsconfig.json b/packages/fetch/tsconfig.json new file mode 100644 index 0000000..72b364c --- /dev/null +++ b/packages/fetch/tsconfig.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "noEmit": true, + "composite": true, + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "lib": [ + "DOM", + "ES2021" + ], + "types": [ + "node", + "jest" + ], + "allowJs": true, + "checkJs": false, + "declaration": true, + "strict": false, + "strictNullChecks": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ], + "exclude": [ + "node_modules", + "**/*.test.js" + ] +} diff --git a/packages/file-csv/README.md b/packages/file-csv/README.md new file mode 100644 index 0000000..a4e184d --- /dev/null +++ b/packages/file-csv/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-csv diff --git a/packages/file-csv/data/test01.csv b/packages/file-csv/data/test01.csv new file mode 100644 index 0000000..c1c37fa --- /dev/null +++ b/packages/file-csv/data/test01.csv @@ -0,0 +1,2 @@ +column1,column2,column3 +cell1,cell2,cell3 \ No newline at end of file diff --git a/packages/file-csv/jest.config.js b/packages/file-csv/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-csv/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-csv/package.json b/packages/file-csv/package.json new file mode 100644 index 0000000..632701f --- /dev/null +++ b/packages/file-csv/package.json @@ -0,0 +1,42 @@ +{ + "name": "@chatbotkit-dev/file-csv", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./parse": { + "import": "./src/parse.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "csv-parse": "^5.6.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-csv/src/index.ts b/packages/file-csv/src/index.ts new file mode 100644 index 0000000..395a310 --- /dev/null +++ b/packages/file-csv/src/index.ts @@ -0,0 +1,17 @@ +import { csv2blocks } from './parse' + +export { csv2blocks } + +interface Chunk { + text: string + meta: Record +} + +export async function* chunk(blob: Blob): AsyncGenerator { + for (const block of csv2blocks(await blob.text())) { + yield { + text: block, + meta: {}, + } + } +} diff --git a/packages/file-csv/src/parse.test.ts b/packages/file-csv/src/parse.test.ts new file mode 100644 index 0000000..5d861b4 --- /dev/null +++ b/packages/file-csv/src/parse.test.ts @@ -0,0 +1,21 @@ +import { csv2blocks } from './parse' + +import fs from 'node:fs' + +describe('csv2blocks', () => { + test('csv must produce blocks', () => { + expect(csv2blocks('1,2,3\na,b,c')).toEqual(['1: a\n2: b\n3: c']) + expect(csv2blocks('1,2,3\na,b,c\nx,y,z')).toEqual([ + '1: a\n2: b\n3: c', + '1: x\n2: y\n3: z', + ]) + }) + + test('should return text', async () => { + const data = fs.readFileSync('./data/test01.csv') + + const blocks = await csv2blocks(new Uint8Array(data)) + + expect(blocks).toEqual(['column1: cell1\ncolumn2: cell2\ncolumn3: cell3']) + }) +}) diff --git a/packages/file-csv/src/parse.ts b/packages/file-csv/src/parse.ts new file mode 100644 index 0000000..c449b8f --- /dev/null +++ b/packages/file-csv/src/parse.ts @@ -0,0 +1,26 @@ +import { parse } from 'csv-parse/sync' + +export function csv2blocks(input: string | Uint8Array): string[] { + const records = parse(input as string | Buffer, { + columns: true, + skip_empty_lines: true, + relax_quotes: true, + relax_column_count: true, + }) + + return records.map((record: Record) => { + return Object.entries(record) + .map(([name, value]) => { + name = name.replace(/\s+/g, ' ').trim() + value = value.replace(/\s+/g, ' ').trim() + + if (!name || !value) { + return + } + + return `${name}: ${value}` + }) + .filter((b) => b) + .join('\n') + }) +} diff --git a/packages/file-csv/tsconfig.json b/packages/file-csv/tsconfig.json new file mode 100644 index 0000000..3d5f905 --- /dev/null +++ b/packages/file-csv/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-docx/README.md b/packages/file-docx/README.md new file mode 100644 index 0000000..c2475c7 --- /dev/null +++ b/packages/file-docx/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-docx diff --git a/packages/file-docx/data/test01.docx b/packages/file-docx/data/test01.docx new file mode 100644 index 0000000..c206e7a --- /dev/null +++ b/packages/file-docx/data/test01.docx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c67b51a17dbca2cfc24a0756d560166f90af966515ceb217d42eb306e36d519 +size 7388 diff --git a/packages/file-docx/jest.config.js b/packages/file-docx/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-docx/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-docx/package.json b/packages/file-docx/package.json new file mode 100644 index 0000000..c1dc6d7 --- /dev/null +++ b/packages/file-docx/package.json @@ -0,0 +1,43 @@ +{ + "name": "@chatbotkit-dev/file-docx", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./parse": { + "import": "./src/parse.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/gpt": "workspace:*", + "officeparser": "7.8.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-docx/src/index.ts b/packages/file-docx/src/index.ts new file mode 100644 index 0000000..ae98913 --- /dev/null +++ b/packages/file-docx/src/index.ts @@ -0,0 +1,41 @@ +import { split } from '@chatbotkit-dev/gpt' +import { splitTextRecursiveByTokens } from '@chatbotkit-dev/gpt/text-splitter' + +import { docx2text } from './parse' + +export { docx2text } from './parse' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const text = await docx2text(new Uint8Array(await blob.arrayBuffer())) + + // @note when separators are provided use recursive character splitting with + // token-based length which matches the original Python service behavior + const blocks = options.separators + ? splitTextRecursiveByTokens(text, { + chunkSize: options.size || 512, + chunkOverlap: options.overlap || 0, + separators: options.separators, + }) + : split(text, options.size || 512, options.overlap || 0) + + for (const block of blocks) { + yield { + text: block, + meta: {}, + } + } +} diff --git a/packages/file-docx/src/parse.test.ts b/packages/file-docx/src/parse.test.ts new file mode 100644 index 0000000..3b6a0a1 --- /dev/null +++ b/packages/file-docx/src/parse.test.ts @@ -0,0 +1,13 @@ +import { docx2text } from './parse' + +import fs from 'node:fs' + +describe('pptx2text', () => { + test('should return text', async () => { + const data = fs.readFileSync('./data/test01.docx') + + const text = await docx2text(data) + + expect(text.trim()).toEqual('HELLO WORLD') + }) +}) diff --git a/packages/file-docx/src/parse.ts b/packages/file-docx/src/parse.ts new file mode 100644 index 0000000..0901be7 --- /dev/null +++ b/packages/file-docx/src/parse.ts @@ -0,0 +1,7 @@ +import { parseOffice } from 'officeparser' + +export async function docx2text(buffer: Uint8Array): Promise { + const result = await parseOffice(Buffer.from(buffer)) + + return result.toText() +} diff --git a/packages/file-docx/tsconfig.json b/packages/file-docx/tsconfig.json new file mode 100644 index 0000000..3d5f905 --- /dev/null +++ b/packages/file-docx/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-html/README.md b/packages/file-html/README.md new file mode 100644 index 0000000..bf8d1c1 --- /dev/null +++ b/packages/file-html/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-html diff --git a/packages/file-html/jest.config.js b/packages/file-html/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-html/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-html/package.json b/packages/file-html/package.json new file mode 100644 index 0000000..fa2df20 --- /dev/null +++ b/packages/file-html/package.json @@ -0,0 +1,44 @@ +{ + "name": "@chatbotkit-dev/file-html", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./parse": { + "import": "./src/parse.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/gpt": "workspace:*", + "html-to-text": "^9.0.5" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/html-to-text": "^9.0.4", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-html/src/index.ts b/packages/file-html/src/index.ts new file mode 100644 index 0000000..262e49a --- /dev/null +++ b/packages/file-html/src/index.ts @@ -0,0 +1,41 @@ +import { split } from '@chatbotkit-dev/gpt' +import { splitTextRecursiveByTokens } from '@chatbotkit-dev/gpt/text-splitter' + +import { html2text } from './parse' + +export { html2text, validateSelectors } from './parse' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const text = html2text(await blob.text()) + + // @note when separators are provided use recursive character splitting with + // token-based length which matches the original Python service behavior + const blocks = options.separators + ? splitTextRecursiveByTokens(text, { + chunkSize: options.size || 512, + chunkOverlap: options.overlap || 0, + separators: options.separators, + }) + : split(text, options.size || 512, options.overlap || 0) + + for (const block of blocks) { + yield { + text: block, + meta: {}, + } + } +} diff --git a/packages/file-html/src/parse.test.ts b/packages/file-html/src/parse.test.ts new file mode 100644 index 0000000..89f84ec --- /dev/null +++ b/packages/file-html/src/parse.test.ts @@ -0,0 +1,132 @@ +import { html2text, validateSelectors } from './parse' + +describe('html2text', () => { + it('must correctly get the text from html', () => { + expect(html2text('
test
')).toEqual('test') + }) + + it('must correctly get non-empty text from html with selectors', () => { + expect( + html2text('
main
footer
', { + selectors: 'main', + }) + ).toEqual('main') + }) + + it('must correctly get empty text from html with selectors', () => { + expect( + html2text('
main
footer
', { + selectors: 'article', + }) + ).toEqual('') + }) + + it('must correctly get empty text from html with non-existing selectors', () => { + expect( + html2text('
main
footer
', { + selectors: 'skip', + }) + ).toEqual('') + }) + + it('must correctly get text but also skip some elements such as nav', () => { + expect( + html2text( + '
main
footer
' + ) + ).toEqual('main') + }) + + it('must correctly get text but also skip element that have role of navigation', () => { + expect( + html2text( + '
nav
main
footer
' + ) + ).toEqual('main') + }) + + it('must be able to preserve image urls', () => { + expect( + html2text( + 'image' + ) + ).toEqual('image [https://example.com/image.png]') + }) + + describe('data url handling', () => { + it('must skip data urls in links by default', () => { + expect( + html2text( + 'click here' + ) + ).toEqual('') + }) + + it('must skip data urls in images by default', () => { + expect( + html2text( + 'inline image' + ) + ).toEqual('') + }) + + it('must include data urls when includeDataUrls is true', () => { + expect( + html2text( + 'click here', + { includeDataUrls: true } + ) + ).toEqual('click here [data:text/plain;base64,SGVsbG8=]') + }) + + it('must include data urls in images when includeDataUrls is true', () => { + expect( + html2text( + 'inline image', + { includeDataUrls: true } + ) + ).toEqual('inline image [data:image/png;base64,iVBORw0KGgo=]') + }) + }) + + it.skip('must be able to preserve video urls', () => { + expect( + html2text( + '' + ) + ).toEqual('video [https://example.com/video.mp4]') + }) + + // @note we have turn off this because it could be causing issues + + it.skip('must correctly skip footer classes', () => { + expect( + html2text( + '
main
footer
' + ) + ).toEqual('main') + }) + + it('test harness 001', () => { + const html = `

Launched in 2023, McLuck is a new sweeps cash casino that has taken the US by storm.

` + + const expected = `Launched in 2023, McLuck [https://casinos.com/us/mcluck-social-casino] is a new sweeps cash casino that has taken the US by storm.` + + const result = html2text(html, { + url: 'https://casinos.com/', + }) + + expect(result).toEqual(expected) + }) +}) + +describe('validateSelectors', () => { + it('must correctly validate selectors', () => { + expect(validateSelectors('html, body')).toEqual({ valid: true }) + expect(validateSelectors('html, body, jsonl')).toEqual({ valid: true }) + expect(validateSelectors('body div')).toEqual({ + valid: false, + message: 'Unsupported selector kind: combinator', + }) + }) +}) diff --git a/packages/file-html/src/parse.ts b/packages/file-html/src/parse.ts new file mode 100644 index 0000000..61b5c19 --- /dev/null +++ b/packages/file-html/src/parse.ts @@ -0,0 +1,196 @@ +import { compile } from 'html-to-text' + +export const DEFAULT_SELECTORS = ['article', 'main', 'body'] + +export function html2text( + html: string, + options?: { + url?: string + selectors?: string | string[] + includeDataUrls?: boolean + } +): string { + const { url, selectors, includeDataUrls = false } = options || {} + + let preferredSelectors: string[] = [] + + // add the selectors + { + let theseSelectors + + if (typeof selectors === 'string') { + theseSelectors = selectors + .split(',') + .map((i) => i.trim()) + .filter((i) => i) + } else if (Array.isArray(selectors)) { + theseSelectors = selectors + } + + theseSelectors = theseSelectors + ?.map((selector) => selector.trim()) + .filter(Boolean) + .filter((selector) => !selector.startsWith('@')) + + if (theseSelectors && theseSelectors.length) { + preferredSelectors.unshift(...theseSelectors) + } else { + preferredSelectors.unshift(...DEFAULT_SELECTORS) + } + } + + // normalize selectors + { + preferredSelectors = Array.from( + new Set( + preferredSelectors.map((selector) => selector.trim()).filter(Boolean) + ) + ) + } + + // skip helper + + function getSkipTagSelectorFor( + type: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + defaultValue?: any + ): { + selector: string + format: 'skip' + }[] { + return selectors?.includes(`@skiptag-${type}`) + ? [{ selector: type, format: 'skip' }] + : defaultValue + ? [defaultValue] + : [] + } + + // convert + + const convert = compile({ + wordwrap: false, + + baseElements: { + selectors: preferredSelectors, + + returnDomByDefault: false, + }, + + selectors: [ + // data urls + ...(includeDataUrls + ? [] + : [ + { selector: '[href^="data:"]', format: 'skip' }, + { selector: '[src^="data:"]', format: 'skip' }, + ]), + // links + ...getSkipTagSelectorFor('a', { + selector: 'a', + options: { + ignoreHref: false, + noAnchorUrl: true, + hideLinkHrefIfSameAsText: true, + baseUrl: url, + }, + }), + // media + ...getSkipTagSelectorFor('img'), + ...getSkipTagSelectorFor('audio'), + ...getSkipTagSelectorFor('video'), + { selector: 'object', format: 'skip' }, + { selector: 'canvas', format: 'skip' }, + // visual + ...getSkipTagSelectorFor('hr'), + // navigation + { selector: 'nav', format: 'skip' }, + { selector: 'header', format: 'skip' }, + { selector: 'footer', format: 'skip' }, + // roles + { selector: '[role="alert"]', format: 'skip' }, + { selector: '[role="alertdialog"]', format: 'skip' }, + { selector: '[role="application"]', format: 'skip' }, + { selector: '[role="banner"]', format: 'skip' }, + { selector: '[role="button"]', format: 'skip' }, + { selector: '[role="checkbox"]', format: 'skip' }, + { selector: '[role="combobox"]', format: 'skip' }, + { selector: '[role="command"]', format: 'skip' }, + { selector: '[role="dialog"]', format: 'skip' }, + { selector: '[role="form"]', format: 'skip' }, + { selector: '[role="input"]', format: 'skip' }, + { selector: '[role="menu"]', format: 'skip' }, + { selector: '[role="navigation"]', format: 'skip' }, + { selector: '[role="radio"]', format: 'skip' }, + { selector: '[role="radiogroup"]', format: 'skip' }, + { selector: '[role="range"]', format: 'skip' }, + { selector: '[role="scrollbar"]', format: 'skip' }, + { selector: '[role="search"]', format: 'skip' }, + { selector: '[role="searchbox"]', format: 'skip' }, + { selector: '[role="slider"]', format: 'skip' }, + { selector: '[role="spinbutton"]', format: 'skip' }, + { selector: '[role="status"]', format: 'skip' }, + { selector: '[role="suggestion"]', format: 'skip' }, + { selector: '[role="switch"]', format: 'skip' }, + // @note disabled because these could be useful content + // { selector: '[role="tab"]', format: 'skip' }, + // { selector: '[role="tabpanel"]', format: 'skip' }, + { selector: '[role="textbox"]', format: 'skip' }, + { selector: '[role="timer"]', format: 'skip' }, + { selector: '[role="toolbar"]', format: 'skip' }, + { selector: '[role="tooltip"]', format: 'skip' }, + { selector: '[role="widget"]', format: 'skip' }, + // interaction + { selector: 'form', format: 'skip' }, + { selector: 'button', format: 'skip' }, + { selector: 'input', format: 'skip' }, + { selector: 'textarea', format: 'skip' }, + { selector: 'dialog', format: 'skip' }, + // frames + { selector: 'iframe', format: 'skip' }, + { selector: 'frame', format: 'skip' }, + { selector: 'frameset', format: 'skip' }, + // functional + { selector: 'meta', format: 'skip' }, + { selector: 'script', format: 'skip' }, + { selector: 'noscript', format: 'skip' }, + // other + // @note disabled because these are sometimes causing issues + // @todo perhaps we should first try with these and if empty response try without them + // { selector: '[class*="header"]', format: 'skip' }, + // { selector: '[class*="footer"]', format: 'skip' }, + // { selector: '[class*="menu"]', format: 'skip' }, + // { selector: '[class*="breadcrumbs"]', format: 'skip' }, + ], + }) + + let text = convert(html) + + // remove double new lines + + text = text.replace(/\n+/g, '\n') + + // remove double spaces + + text = text.replace(/\s+/g, ' ') + + return text +} + +export function stripHtml(html: string): string { + return html2text(`
${html}
`, { selectors: ['main'] }) +} + +export function validateSelectors(selectors: string | string[]): { + valid: boolean + message?: string +} { + try { + html2text('', { selectors }) + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e) + + return { valid: false, message: errorMessage.trim() } + } + + return { valid: true } +} diff --git a/packages/file-html/tsconfig.json b/packages/file-html/tsconfig.json new file mode 100644 index 0000000..3d5f905 --- /dev/null +++ b/packages/file-html/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-json/README.md b/packages/file-json/README.md new file mode 100644 index 0000000..a5bff8a --- /dev/null +++ b/packages/file-json/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-json diff --git a/packages/file-json/jest.config.js b/packages/file-json/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-json/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-json/package.json b/packages/file-json/package.json new file mode 100644 index 0000000..970b517 --- /dev/null +++ b/packages/file-json/package.json @@ -0,0 +1,39 @@ +{ + "name": "@chatbotkit-dev/file-json", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/js-yaml": "^4.0.9", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-json/src/index.test.ts b/packages/file-json/src/index.test.ts new file mode 100644 index 0000000..1d8db57 --- /dev/null +++ b/packages/file-json/src/index.test.ts @@ -0,0 +1,172 @@ +import { chunk, split } from './index' + +describe('split', () => { + it('should return null for invalid JSON', () => { + expect(split('invalid')).toBeNull() + }) + + it('should return a Chunk with content and meta for valid JSON', () => { + const line = '{"name": "John", "age": 30}' + const result = split(line) + + expect(result).toEqual({ + text: 'name: John\nage: 30', + meta: { age: 30 }, + }) + }) + + it('should not move ints represented as strings to meta', () => { + const line = '{"name": "John", "age": "30"}' + const result = split(line) + + expect(result).toEqual({ + text: "name: John\nage: '30'", + meta: {}, + }) + }) + + it('should move non-alphanumeric fields to meta', () => { + const line = '{"name": "John", "_age": 30}' + const result = split(line) + + expect(result).toEqual({ + text: 'name: John', + meta: { age: 30 }, + }) + }) + + it('should move boolean fields to meta', () => { + const line = '{"name": "John", "active": true}' + const result = split(line) + + expect(result).toEqual({ + text: 'name: John\nactive: true', + meta: { active: true }, + }) + }) + + it('should return empty content for empty object', () => { + const line = '{}' + const result = split(line) + + expect(result).toEqual({ + text: '', + meta: {}, + }) + }) + + it('should return empty content for empty array', () => { + const line = '[]' + const result = split(line) + + expect(result).toEqual({ + text: '', + meta: {}, + }) + }) +}) + +describe('chunk', () => { + it('should yield Chunks for valid JSON array', async () => { + const data = '[{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]' + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/json' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([ + { text: 'name: John\nage: 30', meta: { age: 30 } }, + { text: 'name: Jane\nage: 25', meta: { age: 25 } }, + ]) + }) + + it('should yield a Chunk for valid JSON object', async () => { + const data = '{"name": "John", "age": 30}' + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/json' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([{ text: 'name: John\nage: 30', meta: { age: 30 } }]) + }) + + it('should yield a Chunk for each property array of objects in JSON', async () => { + const data = + '{"people": [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]}' + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/json' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([ + { text: 'name: John\nage: 30', meta: { age: 30 } }, + { text: 'name: Jane\nage: 25', meta: { age: 25 } }, + ]) + }) + + it('should not yield a Chunk for array properties that are not objects', async () => { + const data = '{"people": [1, 2, 3]}' + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/json' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([ + { + text: 'people:\n - 1\n - 2\n - 3', + meta: {}, + }, + ]) + }) + + it('should not yield anything for invalid JSON', async () => { + const data = 'invalid' + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/json' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([]) + }) + + it('should yield a single Chunk for a multi-key JSON object', async () => { + const data = '{"name": "John", "city": "NYC"}' + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/json' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([{ text: 'name: John\ncity: NYC', meta: {} }]) + }) + + it('should not throw for JSON object with single null property', async () => { + const data = JSON.stringify({ items: null }) + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/json' }) + )) { + chunks.push(c) + } + + expect(chunks.length).toBeGreaterThanOrEqual(0) + }) +}) diff --git a/packages/file-json/src/index.ts b/packages/file-json/src/index.ts new file mode 100644 index 0000000..50440f2 --- /dev/null +++ b/packages/file-json/src/index.ts @@ -0,0 +1,108 @@ +import jsYaml from 'js-yaml' + +interface Chunk { + text: string + meta: Record +} + +export function split(input: string): Chunk | null { + let data + + try { + data = JSON.parse(input) + } catch { + return null + } + + const meta: Record = {} + + // any field that starts with non-alphanumeric character should go into the meta data object + + for (const key in data) { + if (!/^[a-zA-Z]/.test(key)) { + meta[key.replace(/^([^a-zA-Z0-9]+)(.*)$/, '$2')] = data[key] + + delete data[key] + } + } + + // any field that is a number or a boolean should go into the meta data object + + for (const key in data) { + if (typeof data[key] === 'number' || typeof data[key] === 'boolean') { + meta[key] = data[key] + } + } + + // the final content is trimmed + + let text = jsYaml.dump(data, { lineWidth: -1 }).trim() + + // if the content is empty object or array, it should be an empty string + + text = text === '{}' || text === '[]' ? '' : text + + return { + text: text, + meta: meta, + } +} + +export async function* chunk(blob: Blob): AsyncGenerator { + let json + + try { + const data = await blob.text() + + json = JSON.parse(data) + } catch { + return + } + + // handle arrays + { + if (Array.isArray(json)) { + for (const item of json) { + const result = split(JSON.stringify(item)) + + if (result) { + yield result + } + } + + return + } + } + + // handle objects with a single property that is an array + { + if (Object.keys(json).length === 1) { + const firstValue = json[Object.keys(json)[0]] + + if (Array.isArray(firstValue) && typeof firstValue[0] === 'object' && firstValue[0] !== null) { + for (const item of firstValue) { + const result = split(JSON.stringify(item)) + + if (result) { + yield result + } + } + + return + } else { + yield { + text: jsYaml.dump(json, { lineWidth: -1 }).trim(), + meta: {}, + } + + return + } + } + } + + const result = split(JSON.stringify(json)) + + if (result) { + yield result + } +} diff --git a/packages/file-json/tsconfig.json b/packages/file-json/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/file-json/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-jsonl/README.md b/packages/file-jsonl/README.md new file mode 100644 index 0000000..33c5c77 --- /dev/null +++ b/packages/file-jsonl/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-jsonl diff --git a/packages/file-jsonl/jest.config.js b/packages/file-jsonl/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-jsonl/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-jsonl/package.json b/packages/file-jsonl/package.json new file mode 100644 index 0000000..e1edcfe --- /dev/null +++ b/packages/file-jsonl/package.json @@ -0,0 +1,38 @@ +{ + "name": "@chatbotkit-dev/file-jsonl", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/file-json": "workspace:*" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-jsonl/src/index.test.ts b/packages/file-jsonl/src/index.test.ts new file mode 100644 index 0000000..7bdd03e --- /dev/null +++ b/packages/file-jsonl/src/index.test.ts @@ -0,0 +1,101 @@ +import { chunk, split } from './index' + +describe('split', () => { + it('should return null for invalid JSON', () => { + const result = split('invalid json') + + expect(result).toBeNull() + }) + + it('should split valid JSON string into content and meta', () => { + const line = JSON.stringify({ + name: 'John', + age: 30, + }) + const result = split(line) + + expect(result).toEqual({ + text: 'name: John\nage: 30', + meta: { age: 30 }, + }) + }) + + it('should move non-alphanumeric fields to meta', () => { + const line = '{"name": "John", "_age": 30}' + const result = split(line) + + expect(result).toEqual({ + text: 'name: John', + meta: { age: 30 }, + }) + }) + + it('should handle JSON with only string fields', () => { + const line = JSON.stringify({ name: 'John', city: 'New York' }) + const result = split(line) + + expect(result).toEqual({ + text: 'name: John\ncity: New York', + meta: {}, + }) + }) + + it('should handle JSON with only non-string fields', () => { + const line = JSON.stringify({ age: 30, active: true }) + const result = split(line) + + expect(result).toEqual({ + text: 'age: 30\nactive: true', + meta: { age: 30, active: true }, + }) + }) +}) + +describe('chunk', () => { + it('should yield chunks for each valid JSON line', async () => { + const data = + JSON.stringify({ name: 'John' }) + '\n' + JSON.stringify({ age: 30 }) + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/jsonl' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([ + { text: 'name: John', meta: {} }, + { text: 'age: 30', meta: { age: 30 } }, + ]) + }) + + it('should skip empty lines', async () => { + const data = + JSON.stringify({ name: 'John' }) + '\n\n' + JSON.stringify({ age: 30 }) + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/jsonl' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([ + { text: 'name: John', meta: {} }, + { text: 'age: 30', meta: { age: 30 } }, + ]) + }) + + it('should handle all invalid JSON lines', async () => { + const data = 'invalid json' + JSON.stringify({ name: 'John' }) + const chunks = [] + + for await (const c of chunk( + new Blob([data], { type: 'application/jsonl' }) + )) { + chunks.push(c) + } + + expect(chunks).toEqual([]) + }) +}) diff --git a/packages/file-jsonl/src/index.ts b/packages/file-jsonl/src/index.ts new file mode 100644 index 0000000..e75e381 --- /dev/null +++ b/packages/file-jsonl/src/index.ts @@ -0,0 +1,32 @@ +import { split } from '@chatbotkit-dev/file-json' + +export { split } from '@chatbotkit-dev/file-json' + +interface Chunk { + text: string + meta: Record +} + +export async function* chunk(blob: Blob): AsyncGenerator { + let lines + + try { + const data = await blob.text() + + lines = data.split('\n') + } catch { + return + } + + for (const line of lines) { + if (!line) { + continue + } + + const item = split(line) + + if (item) { + yield item + } + } +} diff --git a/packages/file-jsonl/tsconfig.json b/packages/file-jsonl/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/file-jsonl/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-md/README.md b/packages/file-md/README.md new file mode 100644 index 0000000..742bf46 --- /dev/null +++ b/packages/file-md/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-md diff --git a/packages/file-md/jest.config.js b/packages/file-md/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-md/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-md/package.json b/packages/file-md/package.json new file mode 100644 index 0000000..46889ce --- /dev/null +++ b/packages/file-md/package.json @@ -0,0 +1,39 @@ +{ + "name": "@chatbotkit-dev/file-md", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/gpt": "workspace:*" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-md/src/index.test.ts b/packages/file-md/src/index.test.ts new file mode 100644 index 0000000..6374dc4 --- /dev/null +++ b/packages/file-md/src/index.test.ts @@ -0,0 +1,5 @@ +describe('pass', () => { + it('pass', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/file-md/src/index.ts b/packages/file-md/src/index.ts new file mode 100644 index 0000000..2387111 --- /dev/null +++ b/packages/file-md/src/index.ts @@ -0,0 +1,37 @@ +import { split } from '@chatbotkit-dev/gpt' +import { splitTextRecursiveByTokens } from '@chatbotkit-dev/gpt/text-splitter' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const text = await blob.text() + + // @note when separators are provided use recursive character splitting with + // token-based length which matches the original Python service behavior + const blocks = options.separators + ? splitTextRecursiveByTokens(text, { + chunkSize: options.size || 512, + chunkOverlap: options.overlap || 0, + separators: options.separators, + }) + : split(text, options.size || 512, options.overlap || 0) + + for (const block of blocks) { + yield { + text: block, + meta: {}, + } + } +} diff --git a/packages/file-md/tsconfig.json b/packages/file-md/tsconfig.json new file mode 100644 index 0000000..3d5f905 --- /dev/null +++ b/packages/file-md/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-pdf/README.md b/packages/file-pdf/README.md new file mode 100644 index 0000000..6fa441f --- /dev/null +++ b/packages/file-pdf/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-pdf diff --git a/packages/file-pdf/data/test01.pdf b/packages/file-pdf/data/test01.pdf new file mode 100644 index 0000000000000000000000000000000000000000..49e3a9ef9b59a233f166a76bbd19ad215c934ee3 GIT binary patch literal 130 zcmWm2K@P$o5CFh?U%>|~ECeY1%`PlLqSCn7gRi%qCYh7p(#K1+IS<*jzV~^1R9P?E zht@ovS`N_xg((+|*QSce{v(+H NZjt_F1z`QQ`~V6KC>{U+ literal 0 HcmV?d00001 diff --git a/packages/file-pdf/jest.config.js b/packages/file-pdf/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-pdf/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-pdf/package.json b/packages/file-pdf/package.json new file mode 100644 index 0000000..0006e60 --- /dev/null +++ b/packages/file-pdf/package.json @@ -0,0 +1,44 @@ +{ + "name": "@chatbotkit-dev/file-pdf", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./parse": { + "import": "./src/parse.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/gpt": "workspace:*", + "@napi-rs/canvas": "file:../../stubs/napi-rs-canvas", + "unpdf": "^1.4.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-pdf/src/index.ts b/packages/file-pdf/src/index.ts new file mode 100644 index 0000000..cea54d3 --- /dev/null +++ b/packages/file-pdf/src/index.ts @@ -0,0 +1,41 @@ +import { split } from '@chatbotkit-dev/gpt' +import { splitTextRecursiveByTokens } from '@chatbotkit-dev/gpt/text-splitter' + +import { pdf2text } from './parse' + +export { pdf2pages, pdf2text } from './parse' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const text = await pdf2text(new Uint8Array(await blob.arrayBuffer())) + + // @note when separators are provided use recursive character splitting with + // token-based length which matches the original Python service behavior + const blocks = options.separators + ? splitTextRecursiveByTokens(text, { + chunkSize: options.size || 512, + chunkOverlap: options.overlap || 0, + separators: options.separators, + }) + : split(text, options.size || 512, options.overlap || 0) + + for (const block of blocks) { + yield { + text: block, + meta: {}, + } + } +} diff --git a/packages/file-pdf/src/parse.test.ts b/packages/file-pdf/src/parse.test.ts new file mode 100644 index 0000000..963b5c0 --- /dev/null +++ b/packages/file-pdf/src/parse.test.ts @@ -0,0 +1,13 @@ +import { pdf2text } from './parse' + +import fs from 'node:fs' + +describe('pptx2text', () => { + test('should return text', async () => { + const data = fs.readFileSync('./data/test01.pdf') + + const text = await pdf2text(new Uint8Array(data)) + + expect(text.trim()).toEqual('HELLO WORLD') + }) +}) diff --git a/packages/file-pdf/src/parse.ts b/packages/file-pdf/src/parse.ts new file mode 100644 index 0000000..54f21e2 --- /dev/null +++ b/packages/file-pdf/src/parse.ts @@ -0,0 +1,15 @@ +import { extractText, getDocumentProxy } from 'unpdf' + +export async function pdf2pages(data: Uint8Array): Promise { + const document = await getDocumentProxy(data) + + const { text: pages } = await extractText(document) + + return pages +} + +export async function pdf2text(data: Uint8Array): Promise { + const pages = await pdf2pages(data) + + return pages.join('\n\n') +} diff --git a/packages/file-pdf/tsconfig.json b/packages/file-pdf/tsconfig.json new file mode 100644 index 0000000..73969da --- /dev/null +++ b/packages/file-pdf/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ESNext"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-pptx/README.md b/packages/file-pptx/README.md new file mode 100644 index 0000000..7584ebb --- /dev/null +++ b/packages/file-pptx/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-pptx diff --git a/packages/file-pptx/data/test01.pptx b/packages/file-pptx/data/test01.pptx new file mode 100644 index 0000000..cd094b2 --- /dev/null +++ b/packages/file-pptx/data/test01.pptx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdad238875e6402f7a90fa3216d15581c55a10f98dd4e704c90bc581206d463c +size 34431 diff --git a/packages/file-pptx/jest.config.js b/packages/file-pptx/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-pptx/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-pptx/package.json b/packages/file-pptx/package.json new file mode 100644 index 0000000..bb91520 --- /dev/null +++ b/packages/file-pptx/package.json @@ -0,0 +1,43 @@ +{ + "name": "@chatbotkit-dev/file-pptx", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./parse": { + "import": "./src/parse.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/gpt": "workspace:*", + "officeparser": "7.8.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-pptx/src/index.ts b/packages/file-pptx/src/index.ts new file mode 100644 index 0000000..283b729 --- /dev/null +++ b/packages/file-pptx/src/index.ts @@ -0,0 +1,41 @@ +import { split } from '@chatbotkit-dev/gpt' +import { splitTextRecursiveByTokens } from '@chatbotkit-dev/gpt/text-splitter' + +import { pptx2text } from './parse' + +export { pptx2text } from './parse' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const text = await pptx2text(new Uint8Array(await blob.arrayBuffer())) + + // @note when separators are provided use recursive character splitting with + // token-based length which matches the original Python service behavior + const blocks = options.separators + ? splitTextRecursiveByTokens(text, { + chunkSize: options.size || 512, + chunkOverlap: options.overlap || 0, + separators: options.separators, + }) + : split(text, options.size || 512, options.overlap || 0) + + for (const block of blocks) { + yield { + text: block, + meta: {}, + } + } +} diff --git a/packages/file-pptx/src/parse.test.ts b/packages/file-pptx/src/parse.test.ts new file mode 100644 index 0000000..da9e10f --- /dev/null +++ b/packages/file-pptx/src/parse.test.ts @@ -0,0 +1,13 @@ +import { pptx2text } from './parse' + +import fs from 'node:fs' + +describe('pptx2text', () => { + test('should return text', async () => { + const data = fs.readFileSync('./data/test01.pptx') + + const text = await pptx2text(data) + + expect(text.trim()).toEqual('HELLO\nWORLD') + }) +}) diff --git a/packages/file-pptx/src/parse.ts b/packages/file-pptx/src/parse.ts new file mode 100644 index 0000000..0cb4475 --- /dev/null +++ b/packages/file-pptx/src/parse.ts @@ -0,0 +1,7 @@ +import { parseOffice } from 'officeparser' + +export async function pptx2text(buffer: Uint8Array): Promise { + const result = await parseOffice(Buffer.from(buffer)) + + return result.toText() +} diff --git a/packages/file-pptx/tsconfig.json b/packages/file-pptx/tsconfig.json new file mode 100644 index 0000000..3d5f905 --- /dev/null +++ b/packages/file-pptx/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-txt/README.md b/packages/file-txt/README.md new file mode 100644 index 0000000..0ad3ef0 --- /dev/null +++ b/packages/file-txt/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-txt diff --git a/packages/file-txt/jest.config.js b/packages/file-txt/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-txt/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-txt/package.json b/packages/file-txt/package.json new file mode 100644 index 0000000..1c8c9e7 --- /dev/null +++ b/packages/file-txt/package.json @@ -0,0 +1,39 @@ +{ + "name": "@chatbotkit-dev/file-txt", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/file-json": "workspace:*", + "@chatbotkit-dev/gpt": "workspace:*" + }, + "devDependencies": { + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-txt/src/index.test.ts b/packages/file-txt/src/index.test.ts new file mode 100644 index 0000000..6374dc4 --- /dev/null +++ b/packages/file-txt/src/index.test.ts @@ -0,0 +1,5 @@ +describe('pass', () => { + it('pass', () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/file-txt/src/index.ts b/packages/file-txt/src/index.ts new file mode 100644 index 0000000..2387111 --- /dev/null +++ b/packages/file-txt/src/index.ts @@ -0,0 +1,37 @@ +import { split } from '@chatbotkit-dev/gpt' +import { splitTextRecursiveByTokens } from '@chatbotkit-dev/gpt/text-splitter' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const text = await blob.text() + + // @note when separators are provided use recursive character splitting with + // token-based length which matches the original Python service behavior + const blocks = options.separators + ? splitTextRecursiveByTokens(text, { + chunkSize: options.size || 512, + chunkOverlap: options.overlap || 0, + separators: options.separators, + }) + : split(text, options.size || 512, options.overlap || 0) + + for (const block of blocks) { + yield { + text: block, + meta: {}, + } + } +} diff --git a/packages/file-txt/tsconfig.json b/packages/file-txt/tsconfig.json new file mode 100644 index 0000000..3d5f905 --- /dev/null +++ b/packages/file-txt/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-xlsx/README.md b/packages/file-xlsx/README.md new file mode 100644 index 0000000..129a03d --- /dev/null +++ b/packages/file-xlsx/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-xlsx diff --git a/packages/file-xlsx/data/test01.xlsx b/packages/file-xlsx/data/test01.xlsx new file mode 100644 index 0000000..c631a35 --- /dev/null +++ b/packages/file-xlsx/data/test01.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211778744fbe178229fa8ee0e2d12b5a408ac1e20313c9b23db2cd6524cc5c35 +size 4740 diff --git a/packages/file-xlsx/jest.config.js b/packages/file-xlsx/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-xlsx/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-xlsx/package.json b/packages/file-xlsx/package.json new file mode 100644 index 0000000..8211b0f --- /dev/null +++ b/packages/file-xlsx/package.json @@ -0,0 +1,43 @@ +{ + "name": "@chatbotkit-dev/file-xlsx", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./parse": { + "import": "./src/parse.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/gpt": "workspace:*", + "officeparser": "7.8.0" + }, + "devDependencies": { + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/node": "^24.0.0", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-xlsx/src/index.ts b/packages/file-xlsx/src/index.ts new file mode 100644 index 0000000..7f4dba2 --- /dev/null +++ b/packages/file-xlsx/src/index.ts @@ -0,0 +1,41 @@ +import { split } from '@chatbotkit-dev/gpt' +import { splitTextRecursiveByTokens } from '@chatbotkit-dev/gpt/text-splitter' + +import { xlsx2text } from './parse' + +export { xlsx2text } from './parse' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const text = await xlsx2text(new Uint8Array(await blob.arrayBuffer())) + + // @note when separators are provided use recursive character splitting with + // token-based length which matches the original Python service behavior + const blocks = options.separators + ? splitTextRecursiveByTokens(text, { + chunkSize: options.size || 512, + chunkOverlap: options.overlap || 0, + separators: options.separators, + }) + : split(text, options.size || 512, options.overlap || 0) + + for (const block of blocks) { + yield { + text: block, + meta: {}, + } + } +} diff --git a/packages/file-xlsx/src/parse.test.ts b/packages/file-xlsx/src/parse.test.ts new file mode 100644 index 0000000..bac68bf --- /dev/null +++ b/packages/file-xlsx/src/parse.test.ts @@ -0,0 +1,15 @@ +import { xlsx2text } from './parse' + +import fs from 'node:fs' + +describe('xlsx2text', () => { + test('should return text', async () => { + const data = fs.readFileSync('./data/test01.xlsx') + + const text = await xlsx2text(data) + + expect(text.trim()).toEqual( + 'column1\ncolumn2\ncolumn3\ncell1\ncell2\ncell3' + ) + }) +}) diff --git a/packages/file-xlsx/src/parse.ts b/packages/file-xlsx/src/parse.ts new file mode 100644 index 0000000..99ffb05 --- /dev/null +++ b/packages/file-xlsx/src/parse.ts @@ -0,0 +1,7 @@ +import { parseOffice } from 'officeparser' + +export async function xlsx2text(buffer: Uint8Array): Promise { + const result = await parseOffice(Buffer.from(buffer)) + + return result.toText() +} diff --git a/packages/file-xlsx/tsconfig.json b/packages/file-xlsx/tsconfig.json new file mode 100644 index 0000000..3d5f905 --- /dev/null +++ b/packages/file-xlsx/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest", "node"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file-yaml/README.md b/packages/file-yaml/README.md new file mode 100644 index 0000000..e96913c --- /dev/null +++ b/packages/file-yaml/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file-yaml diff --git a/packages/file-yaml/jest.config.js b/packages/file-yaml/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file-yaml/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file-yaml/package.json b/packages/file-yaml/package.json new file mode 100644 index 0000000..e1d5881 --- /dev/null +++ b/packages/file-yaml/package.json @@ -0,0 +1,40 @@ +{ + "name": "@chatbotkit-dev/file-yaml", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/file-json": "workspace:*", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/js-yaml": "^4.0.9", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file-yaml/src/index.test.ts b/packages/file-yaml/src/index.test.ts new file mode 100644 index 0000000..44a842c --- /dev/null +++ b/packages/file-yaml/src/index.test.ts @@ -0,0 +1,82 @@ +import { chunk, split } from './index' + +async function collect(data: string, type = 'text/yaml') { + const chunks = [] + + for await (const c of chunk(new Blob([data], { type }))) { + chunks.push(c) + } + + return chunks +} + +describe('split', () => { + it('should be re-exported from the json chunker', () => { + expect(typeof split).toBe('function') + + expect(split('{"name": "John", "age": 30}')).toEqual({ + text: 'name: John\nage: 30', + meta: { age: 30 }, + }) + }) +}) + +describe('chunk', () => { + it('should yield a chunk per item for a top level list of maps', async () => { + const data = '- name: John\n age: 30\n- name: Jane\n age: 25' + + expect(await collect(data)).toEqual([ + { text: 'name: John\nage: 30', meta: { age: 30 } }, + { text: 'name: Jane\nage: 25', meta: { age: 25 } }, + ]) + }) + + it('should yield a single chunk for a multi-key map', async () => { + const data = 'name: John\nage: 30' + + expect(await collect(data)).toEqual([ + { text: 'name: John\nage: 30', meta: { age: 30 } }, + ]) + }) + + it('should yield a chunk per item for a single array property map', async () => { + const data = + 'people:\n - name: John\n age: 30\n - name: Jane\n age: 25' + + expect(await collect(data)).toEqual([ + { text: 'name: John\nage: 30', meta: { age: 30 } }, + { text: 'name: Jane\nage: 25', meta: { age: 25 } }, + ]) + }) + + it('should round-trip a nested map into yaml text', async () => { + const data = "name: John\naddress:\n city: NYC\n zip: '10001'" + + expect(await collect(data)).toEqual([ + { + text: "name: John\naddress:\n city: NYC\n zip: '10001'", + meta: {}, + }, + ]) + }) + + it('should also accept json (yaml is a json superset)', async () => { + const data = '{"name": "John", "age": 30}' + + expect(await collect(data, 'application/yaml')).toEqual([ + { text: 'name: John\nage: 30', meta: { age: 30 } }, + ]) + }) + + it('should yield nothing for an empty document', async () => { + expect(await collect('')).toEqual([]) + }) + + it('should yield nothing for a null document', async () => { + expect(await collect('null')).toEqual([]) + }) + + it('should yield nothing for malformed yaml', async () => { + expect(await collect('key: [unclosed')).toEqual([]) + }) +}) diff --git a/packages/file-yaml/src/index.ts b/packages/file-yaml/src/index.ts new file mode 100644 index 0000000..d3ff927 --- /dev/null +++ b/packages/file-yaml/src/index.ts @@ -0,0 +1,85 @@ +import { split } from '@chatbotkit-dev/file-json' + +import jsYaml from 'js-yaml' + +// @note yaml is structurally a superset of json so we reuse the exact same +// record splitting as the json chunker (which itself already emits yaml via +// js-yaml). the only difference is that we parse the document with yaml. + +export { split } from '@chatbotkit-dev/file-json' + +interface Chunk { + text: string + meta: Record +} + +export async function* chunk(blob: Blob): AsyncGenerator { + let doc: unknown + + try { + const data = await blob.text() + + doc = jsYaml.load(data) + } catch { + return + } + + // @note empty or explicitly null documents have nothing to chunk + + if (doc === null || doc === undefined) { + return + } + + // handle arrays + { + if (Array.isArray(doc)) { + for (const item of doc) { + const result = split(JSON.stringify(item)) + + if (result) { + yield result + } + } + + return + } + } + + // handle objects with a single property that is an array + { + if (typeof doc === 'object' && Object.keys(doc).length === 1) { + const record = doc as Record + + const firstValue = record[Object.keys(record)[0]] + + if ( + Array.isArray(firstValue) && + typeof firstValue[0] === 'object' && + firstValue[0] !== null + ) { + for (const item of firstValue) { + const result = split(JSON.stringify(item)) + + if (result) { + yield result + } + } + + return + } else { + yield { + text: jsYaml.dump(doc, { lineWidth: -1 }).trim(), + meta: {}, + } + + return + } + } + } + + const result = split(JSON.stringify(doc)) + + if (result) { + yield result + } +} diff --git a/packages/file-yaml/tsconfig.json b/packages/file-yaml/tsconfig.json new file mode 100644 index 0000000..54214b6 --- /dev/null +++ b/packages/file-yaml/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ES2019"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/file/README.md b/packages/file/README.md new file mode 100644 index 0000000..b664839 --- /dev/null +++ b/packages/file/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/file diff --git a/packages/file/jest.config.js b/packages/file/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/file/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/file/package.json b/packages/file/package.json new file mode 100644 index 0000000..3ac5a84 --- /dev/null +++ b/packages/file/package.json @@ -0,0 +1,55 @@ +{ + "name": "@chatbotkit-dev/file", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./index2": { + "import": "./src/index2.ts" + }, + "./support": { + "import": "./src/support.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/file-csv": "workspace:*", + "@chatbotkit-dev/file-docx": "workspace:*", + "@chatbotkit-dev/file-html": "workspace:*", + "@chatbotkit-dev/file-json": "workspace:*", + "@chatbotkit-dev/file-jsonl": "workspace:*", + "@chatbotkit-dev/file-md": "workspace:*", + "@chatbotkit-dev/file-pdf": "workspace:*", + "@chatbotkit-dev/file-pptx": "workspace:*", + "@chatbotkit-dev/file-txt": "workspace:*", + "@chatbotkit-dev/file-xlsx": "workspace:*", + "@chatbotkit-dev/file-yaml": "workspace:*" + }, + "devDependencies": { + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "@types/js-yaml": "^4.0.9", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/file/src/index.test.ts b/packages/file/src/index.test.ts new file mode 100644 index 0000000..93429cf --- /dev/null +++ b/packages/file/src/index.test.ts @@ -0,0 +1,35 @@ +import { canChunkContentType, chunk } from './index' + +const yamlContentTypes = [ + 'application/yaml', + 'application/x-yaml', + 'text/yaml', + 'text/x-yaml', +] + +async function collect(data: string, type: string) { + const chunks = [] + + for await (const c of chunk(new Blob([data], { type }), {})) { + chunks.push(c) + } + + return chunks +} + +describe('yaml content type support', () => { + it.each(yamlContentTypes)('chunks a %s document as core', async (type) => { + // @note core (no experimental flag) — yaml is treated like json + expect(canChunkContentType(type)).toBe(true) + + expect(await collect('name: John\nage: 30', type)).toEqual([ + { text: 'name: John\nage: 30', meta: { age: 30 } }, + ]) + }) + + it('still throws for a genuinely unsupported content type', async () => { + await expect(collect('hello', 'text/vnd.unknown')).rejects.toThrow( + 'Unsupported content type text/vnd.unknown' + ) + }) +}) diff --git a/packages/file/src/index.ts b/packages/file/src/index.ts new file mode 100644 index 0000000..206e000 --- /dev/null +++ b/packages/file/src/index.ts @@ -0,0 +1,109 @@ +import { chunk as chunkCsv } from '@chatbotkit-dev/file-csv' +import { chunk as chunkHtml } from '@chatbotkit-dev/file-html' +import { chunk as chunkJson } from '@chatbotkit-dev/file-json' +import { chunk as chunkJsonl } from '@chatbotkit-dev/file-jsonl' +import { chunk as chunkMd } from '@chatbotkit-dev/file-md' +import { chunk as chunkTxt } from '@chatbotkit-dev/file-txt' +import { chunk as chunkYaml } from '@chatbotkit-dev/file-yaml' + +import type { CoreContentType, ExperimentalContentType } from './support' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] + experimental?: boolean +} + +function withoutOptions(fn: (blob: Blob) => AsyncGenerator) { + return (blob: Blob, options: Options) => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + options + + return fn(blob) + } +} + +function getBlobContentType(blob: Blob): string { + return blob.type.toLowerCase().split(';')[0].trim() +} + +const coreFns: Record< + CoreContentType, + (blob: Blob, options: Options) => AsyncGenerator +> = { + 'text/csv': withoutOptions(chunkCsv), + 'application/json': withoutOptions(chunkJson), + 'application/jsonl': withoutOptions(chunkJsonl), + 'application/yaml': withoutOptions(chunkYaml), + 'application/x-yaml': withoutOptions(chunkYaml), + 'text/yaml': withoutOptions(chunkYaml), + 'text/x-yaml': withoutOptions(chunkYaml), +} + +const experimentalFns: Record< + ExperimentalContentType, + ((blob: Blob, options: Options) => AsyncGenerator) | null +> = { + 'text/plain': chunkTxt, + 'text/markdown': chunkMd, + 'text/html': chunkHtml, + // @note the reason these are disabled for now is because we use the this + // module inside components that are loaded in the edge environment and + // unfortunately the document APIs are not supported well + 'application/pdf': null, + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + null, + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + null, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': null, +} + +export function getChunkFunction( + blob: Blob, + options?: Options +): ((blob: Blob, options: Options) => AsyncGenerator) | null { + const type = getBlobContentType(blob) + + let fn = type in coreFns ? coreFns[type as CoreContentType] : null + + if (options?.experimental) { + fn ??= + type in experimentalFns + ? experimentalFns[type as ExperimentalContentType] + : null + } + + return fn +} + +export function canChunk(blob: Blob, options?: Options): boolean { + return !!getChunkFunction(blob, options) +} + +export function canChunkContentType( + contentType: string, + options?: Options +): boolean { + return !!getChunkFunction(new Blob([], { type: contentType }), options) +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const fn = getChunkFunction(blob, options) + + if (!fn) { + throw new Error(`Unsupported content type ${getBlobContentType(blob)}`) + } + + for await (const chunk of fn(blob, options)) { + yield chunk + } +} diff --git a/packages/file/src/index2.test.ts b/packages/file/src/index2.test.ts new file mode 100644 index 0000000..993dcf3 --- /dev/null +++ b/packages/file/src/index2.test.ts @@ -0,0 +1,44 @@ +import { canChunkContentType, chunk } from './index2' + +const yamlContentTypes = [ + 'application/yaml', + 'application/x-yaml', + 'text/yaml', + 'text/x-yaml', +] + +async function collect(data: string, type: string) { + const chunks = [] + + for await (const c of chunk(new Blob([data], { type }), {})) { + chunks.push(c) + } + + return chunks +} + +describe('yaml content type support', () => { + // @note this is the map used by the /api/auxiliary/dataset/chunk endpoint; + // before yaml was wired in, a text/yaml url served through chunkUrl threw + // "Unsupported content type text/yaml" + + it.each(yamlContentTypes)('chunks a %s document as core', async (type) => { + expect(canChunkContentType(type)).toBe(true) + + expect(await collect('name: John\nage: 30', type)).toEqual([ + { text: 'name: John\nage: 30', meta: { age: 30 } }, + ]) + }) + + it('normalises a content type with charset params', async () => { + expect( + await collect('name: John\nage: 30', 'text/yaml; charset=utf-8') + ).toEqual([{ text: 'name: John\nage: 30', meta: { age: 30 } }]) + }) + + it('still throws for a genuinely unsupported content type', async () => { + await expect(collect('hello', 'text/vnd.unknown')).rejects.toThrow( + 'Unsupported content type text/vnd.unknown' + ) + }) +}) diff --git a/packages/file/src/index2.ts b/packages/file/src/index2.ts new file mode 100644 index 0000000..ec84c86 --- /dev/null +++ b/packages/file/src/index2.ts @@ -0,0 +1,111 @@ +import { chunk as chunkCsv } from '@chatbotkit-dev/file-csv' +import { chunk as chunkDocx } from '@chatbotkit-dev/file-docx' +import { chunk as chunkHtml } from '@chatbotkit-dev/file-html' +import { chunk as chunkJson } from '@chatbotkit-dev/file-json' +import { chunk as chunkJsonl } from '@chatbotkit-dev/file-jsonl' +import { chunk as chunkMd } from '@chatbotkit-dev/file-md' +import { chunk as chunkPdf } from '@chatbotkit-dev/file-pdf' +import { chunk as chunkPptx } from '@chatbotkit-dev/file-pptx' +import { chunk as chunkTxt } from '@chatbotkit-dev/file-txt' +import { chunk as chunkXlsx } from '@chatbotkit-dev/file-xlsx' +import { chunk as chunkYaml } from '@chatbotkit-dev/file-yaml' + +import type { CoreContentType, ExperimentalContentType } from './support' + +interface Chunk { + text: string + meta: Record +} + +interface Options { + size?: number + overlap?: number + separators?: string[] + experimental?: boolean +} + +function withoutOptions(fn: (blob: Blob) => AsyncGenerator) { + return (blob: Blob, options: Options) => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + options + + return fn(blob) + } +} + +function getBlobContentType(blob: Blob): string { + return blob.type.toLowerCase().split(';')[0].trim() +} + +const coreFns: Record< + CoreContentType, + (blob: Blob, options: Options) => AsyncGenerator +> = { + 'text/csv': withoutOptions(chunkCsv), + 'application/json': withoutOptions(chunkJson), + 'application/jsonl': withoutOptions(chunkJsonl), + 'application/yaml': withoutOptions(chunkYaml), + 'application/x-yaml': withoutOptions(chunkYaml), + 'text/yaml': withoutOptions(chunkYaml), + 'text/x-yaml': withoutOptions(chunkYaml), +} + +const experimentalFns: Record< + ExperimentalContentType, + (blob: Blob, options: Options) => AsyncGenerator +> = { + 'text/plain': chunkTxt, + 'text/markdown': chunkMd, + 'text/html': chunkHtml, + 'application/pdf': chunkPdf, + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + chunkDocx, + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + chunkPptx, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + chunkXlsx, +} + +export function getChunkFunction( + blob: Blob, + options?: Options +): ((blob: Blob, options: Options) => AsyncGenerator) | null { + const type = getBlobContentType(blob) + + let fn = type in coreFns ? coreFns[type as CoreContentType] : null + + if (options?.experimental) { + fn ??= + type in experimentalFns + ? experimentalFns[type as ExperimentalContentType] + : null + } + + return fn +} + +export function canChunk(blob: Blob, options?: Options): boolean { + return !!getChunkFunction(blob, options) +} + +export function canChunkContentType( + contentType: string, + options?: Options +): boolean { + return !!getChunkFunction(new Blob([], { type: contentType }), options) +} + +export async function* chunk( + blob: Blob, + options: Options +): AsyncGenerator { + const fn = getChunkFunction(blob, options) + + if (!fn) { + throw new Error(`Unsupported content type ${getBlobContentType(blob)}`) + } + + for await (const chunk of fn(blob, options)) { + yield chunk + } +} diff --git a/packages/file/src/support.ts b/packages/file/src/support.ts new file mode 100644 index 0000000..4f183d9 --- /dev/null +++ b/packages/file/src/support.ts @@ -0,0 +1,36 @@ +export const coreContentTypes = [ + 'text/csv', + 'application/json', + 'application/jsonl', + 'application/yaml', + 'application/x-yaml', + 'text/yaml', + 'text/x-yaml', +] as const + +export const experimentalContentTypes = [ + 'text/plain', + 'text/markdown', + 'text/html', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', +] as const + +export type CoreContentType = (typeof coreContentTypes)[number] +export type ExperimentalContentType = (typeof experimentalContentTypes)[number] + +export type SupportedContentType = + Experimental extends true + ? CoreContentType | ExperimentalContentType + : CoreContentType + +export function getSupportedContentTypes(options?: { + experimental?: E +}): SupportedContentType[] { + return [ + ...coreContentTypes, + ...(options?.experimental ? experimentalContentTypes : []), + ] as SupportedContentType[] +} diff --git a/packages/file/tsconfig.json b/packages/file/tsconfig.json new file mode 100644 index 0000000..cb51e43 --- /dev/null +++ b/packages/file/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "isolatedModules": true, + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "composite": true, + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["DOM", "ESNext"], + "allowJs": true, + "checkJs": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*.ts", "./src/**/*.js"], + "exclude": ["node_modules"] +} diff --git a/packages/gpt/README.md b/packages/gpt/README.md new file mode 100644 index 0000000..4ee1ab6 --- /dev/null +++ b/packages/gpt/README.md @@ -0,0 +1 @@ +# @chatbotkit-dev/gpt diff --git a/packages/gpt/jest.config.js b/packages/gpt/jest.config.js new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/packages/gpt/jest.config.js @@ -0,0 +1,5 @@ +export default { + preset: 'ts-jest/presets/js-with-ts-esm', + roots: ['/src'], + testEnvironment: '@chatbotkit-dev/jest-jsdom', +} diff --git a/packages/gpt/package.json b/packages/gpt/package.json new file mode 100644 index 0000000..41782e6 --- /dev/null +++ b/packages/gpt/package.json @@ -0,0 +1,42 @@ +{ + "name": "@chatbotkit-dev/gpt", + "version": "0.0.0", + "license": "Apache-2.0", + "private": true, + "type": "module", + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./text-splitter": { + "import": "./src/text-splitter.ts" + } + }, + "scripts": { + "build": "true", + "check": "tsc6 --noEmit --incremental", + "clean": "run-s clean:*", + "clean:01-tsbuildinfo": "rimraf *.tsbuildinfo", + "clean:02-dist": "rimraf dist", + "clean:03-node_modules": "rimraf node_modules", + "format": "true", + "lint": "eslint src --ext .ts,.js,.tsx,.jsx", + "test": "NODE_OPTIONS=--experimental-vm-modules jest" + }, + "access": "restricted", + "dependencies": { + "@types/node": "^24.0.0", + "gpt-tokenizer": "^2.9.0" + }, + "devDependencies": { + "@chatbotkit-dev/eslint-config": "workspace:*", + "@chatbotkit-dev/jest-jsdom": "workspace:*", + "@types/jest": "^29.5.11", + "eslint": "^9.0.0", + "jest": "^29", + "npm-run-all2": "^9.0.3", + "rimraf": "^5.0.5", + "ts-jest": "^29.4.12", + "typescript": "npm:@typescript/typescript6@^6.0.2" + } +} diff --git a/packages/gpt/src/index.test.ts b/packages/gpt/src/index.test.ts new file mode 100644 index 0000000..d19c66c --- /dev/null +++ b/packages/gpt/src/index.test.ts @@ -0,0 +1,519 @@ +import { + ELLIPSIS, + SEP, + adjoinTokens, + batchTokens, + connectText, + countFrequencies, + getBytePairEncodingFrequencies, + getBytePairEncodings, + getTextTokens, + separateText, + slice, + split, + splitTextBlocks, + tokenizeTextBlocks, + trimTextBlockL, + trimTextBlockR, +} from './index' + +function toa(it: Iterable): T[] { + const array = [] + + for (const i of it) { + array.push(i) + } + + return array +} + +describe('getBytePairEncodings', () => { + it('returns correct tokens', () => { + expect(getBytePairEncodings('abc xyz')).toEqual([13997, 41611]) + expect(getBytePairEncodings('abc xyz').length).toEqual( + getTextTokens('abc xyz').length + ) + }) + + it('handles special tokens without throwing', () => { + // @note these special tokens previously caused "Disallowed special token found" errors + expect(() => getBytePairEncodings('<|im_end|>')).not.toThrow() + expect(() => getBytePairEncodings('<|im_start|>')).not.toThrow() + expect(() => getBytePairEncodings('<|endoftext|>')).not.toThrow() + expect(() => getBytePairEncodings('Hello <|im_end|> world')).not.toThrow() + }) + + it('returns tokens for text containing special tokens', () => { + const tokens = getBytePairEncodings('<|im_end|>') + + expect(Array.isArray(tokens)).toBe(true) + expect(tokens.length).toBeGreaterThan(0) + }) +}) + +describe('getBytePairEncodingFrequencies', () => { + it('returns the correct frequencies', () => { + expect(getBytePairEncodingFrequencies('abc xyz')).toEqual({ + 13997: 1, + 41611: 1, + }) + expect(getBytePairEncodingFrequencies('test')).toEqual({ 1985: 1 }) + expect(getBytePairEncodingFrequencies('train train train')).toEqual({ + 10613: 1, + 5542: 2, + }) + }) +}) + +describe('getTextTokens', () => { + it('returns correct tokens', async () => { + expect(getTextTokens('abc xyz')).toEqual(['abc', ' xyz']) + expect(getTextTokens('abc xyz').length).toEqual( + getBytePairEncodings('abc xyz').length + ) + }) + + it('handles special tokens without throwing', () => { + // @note these special tokens previously caused "Disallowed special token found" errors + expect(() => getTextTokens('<|im_end|>')).not.toThrow() + expect(() => getTextTokens('<|im_start|>')).not.toThrow() + expect(() => getTextTokens('<|endoftext|>')).not.toThrow() + expect(() => getTextTokens('Hello <|im_end|> world')).not.toThrow() + }) + + it('returns tokens for text containing special tokens', () => { + const tokens = getTextTokens('<|im_end|>') + + expect(Array.isArray(tokens)).toBe(true) + expect(tokens.length).toBeGreaterThan(0) + }) +}) + +describe('adjoinTokens', () => { + it('must concat tokens basic cases', () => { + expect(adjoinTokens([], [], [], 123, 123)).toEqual([]) + expect(adjoinTokens([], ['a'], [], 123, 123)).toEqual(['a']) + expect(adjoinTokens(['l'], ['a'], [], 123, 123)).toEqual(['l', 'a']) + expect(adjoinTokens(['l'], ['a'], ['r'], 123, 123)).toEqual(['l', 'a', 'r']) + }) + + it('must concat tokens left side trim cases', () => { + expect(adjoinTokens(['a', 'b', 'c', 'd', 'e'], ['-'], [], 123, 3)).toEqual([ + ELLIPSIS, + 'd', + 'e', + '-', + ]) + expect( + adjoinTokens(['a', 'b', 'c', 'd', 'e'], ['-'], [], 123, 3, '') + ).toEqual(['c', 'd', 'e', '-']) + }) + + it('must concat tokens right side trim cases', () => { + expect(adjoinTokens([], ['-'], ['v', 'w', 'x', 'y', 'z'], 123, 3)).toEqual([ + '-', + 'v', + 'w', + ELLIPSIS, + ]) + expect( + adjoinTokens([], ['-'], ['v', 'w', 'x', 'y', 'z'], 123, 3, '') + ).toEqual(['-', 'v', 'w', 'x']) + }) + + it('must concat tokens equal site cases', () => { + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 123, + 3 + ) + ).toEqual([ELLIPSIS, 'd', 'e', '-', 'v', 'w', ELLIPSIS]) + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 123, + 3, + '' + ) + ).toEqual(['c', 'd', 'e', '-', 'v', 'w', 'x']) + }) + + it('must concat tokens odd cases', () => { + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 1, + 3 + ) + ).toEqual(['-']) + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 1, + 3, + '' + ) + ).toEqual(['-']) + + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 2, + 3 + ) + ).toEqual(['-']) + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 2, + 3, + '' + ) + ).toEqual(['-']) + + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 3, + 3 + ) + ).toEqual(['e', '-', 'v']) + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 3, + 3, + '' + ) + ).toEqual(['e', '-', 'v']) + + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 4, + 3 + ) + ).toEqual(['e', '-', 'v']) + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 4, + 3, + '' + ) + ).toEqual(['e', '-', 'v']) + + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 5, + 3 + ) + ).toEqual([ELLIPSIS, 'e', '-', 'v', ELLIPSIS]) + expect( + adjoinTokens( + ['a', 'b', 'c', 'd', 'e'], + ['-'], + ['v', 'w', 'x', 'y', 'z'], + 5, + 3, + '' + ) + ).toEqual(['d', 'e', '-', 'v', 'w']) + }) +}) + +describe('separateText', () => { + it('must correctly separate the text', () => { + expect( + connectText( + separateText( + 'Hello.\nThis is a long text.\n\nSome parts of it should be on sep lines.', + ['\n\n'] + ), + '<-|->' + ) + ).toEqual( + 'Hello.\nThis is a long text.<-|->Some parts of it should be on sep lines.' + ) + + expect( + connectText( + separateText( + 'Hello.\nThis is a long text.\n\nSome parts of it should be on sep lines.\n\nAnd more lines.', + ['\n\n'] + ), + '<-|->' + ) + ).toEqual( + 'Hello.\nThis is a long text.<-|->Some parts of it should be on sep lines.<-|->And more lines.' + ) + + expect( + connectText( + separateText( + 'Hello.\nThis is a long text.\n\n\n\nSome parts of it should be on sep lines.', + ['\n\n'] + ), + '<-|->' + ) + ).toEqual( + 'Hello.\nThis is a long text.<-|->Some parts of it should be on sep lines.' + ) + }) +}) + +describe('tokenizeTextBlocks', () => { + it('must correctly tokenize text blocks', () => { + expect(toa(tokenizeTextBlocks(['a']))).toEqual(['a']) + expect(toa(tokenizeTextBlocks(['a', 'b']))).toEqual(['a', SEP, 'b']) + expect(toa(tokenizeTextBlocks(['hello world', 'hello world']))).toEqual([ + 'hello', + ' world', + SEP, + 'hello', + ' world', + ]) + }) +}) + +describe('batchTokens', () => { + it('must correctly batch tokens', () => { + expect(toa(batchTokens(['a', 'b', 'c', 'd', 'e'], 1))).toEqual([ + ['a', 'b', ELLIPSIS], + [ELLIPSIS, 'c', ELLIPSIS], + [ELLIPSIS, 'd', ELLIPSIS], + [ELLIPSIS, 'e'], + ]) + expect(toa(batchTokens(['a', 'b', 'c', 'd', 'e'], 2))).toEqual([ + ['a', 'b', ELLIPSIS], + [ELLIPSIS, 'c', ELLIPSIS], + [ELLIPSIS, 'd', ELLIPSIS], + [ELLIPSIS, 'e'], + ]) + expect(toa(batchTokens(['a', 'b', 'c', 'd', 'e'], 3))).toEqual([ + ['a', 'b', ELLIPSIS], + [ELLIPSIS, 'c', ELLIPSIS], + [ELLIPSIS, 'd', ELLIPSIS], + [ELLIPSIS, 'e'], + ]) + expect(toa(batchTokens(['a', 'b', 'c', 'd', 'e'], 4))).toEqual([ + ['a', 'b', 'c', ELLIPSIS], + [ELLIPSIS, 'd', 'e'], + ]) + expect(toa(batchTokens(['a', 'b', 'c', 'd', 'e'], 10))).toEqual([ + ['a', 'b', 'c', 'd', 'e'], + ]) + }) +}) + +describe('trimTextBlockL', () => { + it('must trim l', () => { + expect(trimTextBlockL('')).toEqual('') + expect(trimTextBlockL('.')).toEqual('.') + expect(trimTextBlockL('..')).toEqual('..') + expect(trimTextBlockL('...')).toEqual('...') + expect(trimTextBlockL('....')).toEqual('...') + expect(trimTextBlockL('.....')).toEqual('....') + }) +}) + +describe('trimTextBlockR', () => { + it('must trim r', () => { + expect(trimTextBlockR('')).toEqual('') + expect(trimTextBlockR('.')).toEqual('.') + expect(trimTextBlockR('..')).toEqual('..') + expect(trimTextBlockR('...')).toEqual('...') + expect(trimTextBlockR('....')).toEqual('...') + expect(trimTextBlockR('.....')).toEqual('....') + }) +}) + +describe('splitTextBlocks', () => { + it('must correctly split text blocks', () => { + expect( + toa( + splitTextBlocks( + 'Hello.\nThis is a long text.\n\nSome parts of it should be on sep lines.', + 100, + 5, + ['\n\n'] + ) + ) + ).toEqual([ + 'Hello.\nThis is a long text. Some parts of it...', + '... long text. Some parts of it should be on sep lines.', + ]) + + expect( + toa( + splitTextBlocks( + 'Hello.\nThis is a long text.\n\nSome parts of it should be on sep lines.\n\nAnd more lines.', + 100, + 3, + ['\n\n'] + ) + ) + ).toEqual([ + 'Hello.\nThis is a long text. Some parts...', + '... Some parts of it should be on sep lines. And more...', + '... And more lines.', + ]) + }) +}) + +describe('slice', () => { + it('must correctly slice the text', () => { + expect(slice('this is a test', 1)).toEqual(' is a test') + expect(slice('this is a test', 1, -1)).toEqual(' is a') + }) + + it('correctly slices the text between start and stop tokens', () => { + expect(slice('this is a test', 1, 3)).toEqual(' is a') + expect(slice('hello world', 0, -1)).toEqual('hello') + }) + + it('slices to the end if stop token is omitted', () => { + expect(slice('this is a test', 2)).toEqual(' a test') + }) + + it('handles negative indices correctly', () => { + expect(slice('this is a test', -3, -1)).toEqual(' is a') + }) + + it('returns an empty string if startToken equals stopToken', () => { + expect(slice('this is a test', 2, 2)).toEqual('') + }) + + it('handles out of bounds indices gracefully', () => { + expect(slice('this is a test', 10, 20)).toEqual('') + expect(slice('this is a test', -20, -10)).toEqual('') + }) + + it('returns an empty string if startToken is greater than stopToken', () => { + expect(slice('this is a test', 3, 1)).toEqual('') + }) + + it('handles empty input strings', () => { + expect(slice('', 0, 1)).toEqual('') + }) + + it('returns the entire string if both startToken and stopToken are out of bounds', () => { + expect(slice('hello world', -100, 100)).toEqual('hello world') + }) +}) + +describe('split', () => { + it('returns an empty array for empty input', () => { + expect(split('', 5)).toEqual([]) + }) + + it('returns single word as chunk irrespective of maxTokens or overlapTokens', () => { + expect(split('hello', 1, 0)).toEqual(['hello']) + expect(split('hello', 10, 2)).toEqual(['hello']) + }) + + it('returns entire input as a single chunk when maxTokens exceeds number of words', () => { + expect(split('hello world', 5)).toEqual(['hello world']) + }) + + it('throws error when overlapTokens is equal to maxTokens', () => { + expect(() => split('hello world', 5, 5)).toThrow( + 'overlapTokens must be less than maxTokens' + ) + }) + + it('throws error when overlapTokens is negative', () => { + expect(() => split('hello world', 5, -1)).toThrow( + 'overlapTokens must be greater than or equal to 0' + ) + }) + + it('handles fewer tokens than maxTokens without issue', () => { + expect(split('one two', 5)).toEqual(['one two']) + }) + + it('must correctly split the text', () => { + expect(split('The quick brown fox jumps over the lazy dog', 3)).toEqual([ + 'The quick brown', + ' fox jumps over', + ' the lazy dog', + ]) + expect(split('The quick brown fox jumps over the lazy dog', 3, 1)).toEqual([ + 'The quick brown', + ' brown fox jumps', + ' jumps over the', + ' the lazy dog', + ]) + expect( + split( + 'The quick brown fox jumps over the lazy dog and feels as if he were in the seventh heaven', + 3, + 1 + ) + ).toEqual([ + 'The quick brown', + ' brown fox jumps', + ' jumps over the', + ' the lazy dog', + ' dog and feels', + ' feels as if', + ' if he were', + ' were in the', + ' the seventh heaven', + ]) + }) + + it('must correctly spit out single chunk', () => { + expect(split('The quick brown fox jumps over the lazy dog', 400)).toEqual([ + 'The quick brown fox jumps over the lazy dog', + ]) + }) +}) + +describe('countFrequencies', () => { + it('returns an empty object if given an empty array', () => { + expect(countFrequencies([])).toEqual({}) + }) + + it('returns the correct frequency counts for a simple array', () => { + const arr = [1, 2, 2, 3, 3, 3] + + expect(countFrequencies(arr)).toEqual({ 1: 1, 2: 2, 3: 3 }) + }) + + it('returns the correct frequency counts for an array with string elements', () => { + const arr = ['foo', 'bar', 'foo', 'baz', 'baz'] + + expect(countFrequencies(arr)).toEqual({ foo: 2, bar: 1, baz: 2 }) + }) + + it('returns the correct frequency counts for an array with null and undefined elements', () => { + const arr = [null, undefined, null, undefined, null, undefined] + + expect(countFrequencies(arr)).toEqual({ null: 3, undefined: 3 }) + }) +}) diff --git a/packages/gpt/src/index.ts b/packages/gpt/src/index.ts new file mode 100644 index 0000000..723507e --- /dev/null +++ b/packages/gpt/src/index.ts @@ -0,0 +1,484 @@ +import { ok as assert } from 'assert' +import { decodeGenerator, encode } from 'gpt-tokenizer' + +// should be cl100k_base + +// --- +// --- +// --- + +export const SEP = null +export const ELLIPSIS = '...' + +type Model = string + +// --- +// --- +// --- + +export function countFrequencies(arr: T[]): Record { + const frequency: Record = {} + + for (const elem of arr) { + const key = `${elem}` + + if (typeof frequency[key] === 'undefined') { + frequency[key] = 1 + } else { + frequency[key]++ + } + } + + return frequency +} + +export function getBytePairEncodings( + text: string, + _model: Model = 'gpt-4' +): number[] { + // @todo deal with the model + + // @note allow all special tokens to prevent crashes when user input contains tokens like <|im_end|> + const result = encode(text, { allowedSpecial: 'all' }) + + return result +} + +export function getBytePairEncodingFrequencies( + text: string, + model: Model = 'gpt-4' +): Record { + const result = countFrequencies(getBytePairEncodings(text, model)) + + return result +} + +export function getTextTokens(text: string, _model: Model = 'gpt-4'): string[] { + // @todo deal with the model + + const tokens: string[] = [] + + // @note allow all special tokens to prevent crashes when user input contains tokens like <|im_end|> + for (const token of decodeGenerator( + encode(text, { allowedSpecial: 'all' }) + )) { + tokens.push(token) + } + + return tokens +} + +export function getTextTokensLength( + text: string, + model: Model = 'gpt-4' +): number { + const result = getBytePairEncodings(text, model) + + return result.length +} + +// --- +// --- +// --- + +/** + * @deprecated + */ +export function adjoinTokens( + prevTokens: string[], + tokens: string[], + nextTokens: string[], + maxTokens: number, + overlapTokens: number, + ellipsis: string = ELLIPSIS +): string[] { + // @note This function is not designed to accurately slice the tokens hence we + // use whichever is the maximum tokens size. Why is this? Well, where should + // we cut from? It is not clearly defined, thus we do not do it. The maxTokens + // should be always greater than or equal to tokens.length. + + maxTokens = Math.max(tokens.length, maxTokens) + + if (maxTokens === tokens.length) { + return tokens + } + + // @note The overlapTokens cannot be negative thus we always peg it to zero to + // avoid any future problems. + + overlapTokens = Math.max(0, overlapTokens) + + if (overlapTokens == 0) { + return tokens + } + + // @note Here we calculate how to trim the prev and next tokens. We need to + // carefully consider if we should trim both side or one side only. + + let trimByL + let trimByR + + switch (true) { + case !!prevTokens.length && !!nextTokens.length: + trimByL = trimByR = Math.floor((maxTokens - tokens.length) / 2) + + break + + case !!prevTokens.length && !nextTokens.length: + trimByL = maxTokens - tokens.length + trimByR = 0 + + break + + case !prevTokens.length && !!nextTokens.length: + trimByL = 0 + trimByR = maxTokens - tokens.length + + break + + default: + trimByL = 0 + trimByR = 0 + + break + } + + // @note Bot the left and right trim cannot exceed overlapTokens or the tokens + // length. We take whatever value is the lowest. The assumption is that with + // all of the previous steps in order we cannot get negative values or zero. + + trimByL = Math.min(trimByL, overlapTokens, prevTokens.length) + trimByR = Math.min(trimByR, overlapTokens, nextTokens.length) + + // @note Calculate the trim and the ellipsis on the left side. Not only the + // trim value must be positive but also less than prevTokens length. + + if (trimByL && trimByL <= prevTokens.length) { + let ellipsisL + + if (ellipsis && trimByL > 1 && prevTokens.length > trimByL) { + ellipsisL = ellipsis + } + + prevTokens = prevTokens.slice(-trimByL) + + if (ellipsisL) { + prevTokens[0] = ellipsis + } + } else { + prevTokens = [] + } + + // @note Calculate the trim and the ellipsis on the right side. Not only the + // trim value must be positive but also less than nextTokens length. + + if (trimByR && trimByR <= nextTokens.length) { + let ellipsisR + + if (ellipsis && trimByR > 1 && nextTokens.length > trimByR) { + ellipsisR = ellipsis + } + + nextTokens = nextTokens.slice(0, trimByR) + + if (ellipsisR) { + nextTokens[nextTokens.length - 1] = ellipsis + } + } else { + nextTokens = [] + } + + // @note Build the final list tokens. + + return [...prevTokens, ...tokens, ...nextTokens] +} + +// --- +// --- +// --- + +/** + * Recursively split a text into text blocks. + * + * @deprecated + */ +export function* separateText( + text: string, + separators: string[] = [] +): Generator { + separators = separators.slice(0) + + const sep = separators.shift() + + if (!sep) { + yield text + + return + } + + const sections = text.split(sep) + + do { + const section = sections.shift() + + if (section) { + yield* separateText(section, separators) + } + } while (sections.length) +} + +/** + * Connect text blocks into contingent text. + * + * @deprecated + */ +export function connectText( + iterator: Iterable, + connector: string = '\n\n' +): string { + const tokens: string[] = [] + + for (const text of iterator) { + tokens.push(text) + } + + return tokens.join(connector) +} + +/** + * Split text blocks into a vector of tokens. SEP is a special tokens that + * indicates start of a block. + * + * @deprecated + */ +export function* tokenizeTextBlocks( + blocks: Iterable +): Generator { + let isFirst = true + + for (const block of blocks) { + if (isFirst) { + isFirst = false + } else { + yield SEP + } + + yield* getTextTokens(block) + } +} + +/** + * Yield batches of maxTokens for vector of tokens. + * + * @deprecated + */ +export function* batchTokens( + tokens: Iterable, + maxTokens: number, + ellipsis: string = ELLIPSIS +): Generator { + maxTokens = Math.max(maxTokens, ellipsis ? 3 : 1) + + let batch: string[] = [] + + for (const token of tokens) { + if (token === SEP) { + if (batch.length) { + batch.push(' ') + + yield batch + + batch = [] + } + } else { + batch.push(token) + + if (batch.length === maxTokens) { + let last + + if (ellipsis) { + last = batch.pop() + + batch.push(ellipsis) + } + + yield batch + + batch = [] + + if (ellipsis) { + batch.push(ellipsis) + } + + if (last) { + batch.push(last) + } + } + } + } + + if (batch.length) { + yield batch + } +} + +// --- +// --- +// --- + +/** + * @deprecated + */ +export function trimTextBlockL( + block: string, + ellipsis: string = ELLIPSIS +): string { + if ( + ellipsis && + block !== ellipsis && + block[0] === ellipsis[0] && + block.slice(1, ellipsis.length + 1) === ellipsis + ) { + block = block.slice(1) + } + + return block +} + +/** + * @deprecated + */ +export function trimTextBlockR( + block: string, + ellipsis: string = ELLIPSIS +): string { + if ( + ellipsis && + block !== ellipsis && + block[block.length - 1] === ellipsis[ellipsis.length - 1] && + block.slice(-ellipsis.length - 1, -1) === ellipsis + ) { + block = block.slice(0, -1) + } + + return block +} + +/** + * @deprecated + */ +export function trimTextBlock( + block: string, + ellipsis: string = ELLIPSIS +): string { + return trimTextBlockL(trimTextBlockR(block, ellipsis), ellipsis) +} + +/** + * @deprecated use split instead + */ +export function* splitTextBlocks( + text: string, + maxTokens: number, + overlapTokens: number, + separators: string[] = [], + ellipsis: string = ELLIPSIS +): Generator { + let prevBatch: string[] = [] + + const itr = batchTokens( + tokenizeTextBlocks(separateText(text, separators)), + maxTokens, + ellipsis + ) + + for (const thisBatch of itr) { + if (prevBatch.length) { + const block = adjoinTokens( + [], + prevBatch, + thisBatch, + maxTokens, + overlapTokens, + ellipsis + ).join('') + + yield trimTextBlock(block) + } + + prevBatch = adjoinTokens( + prevBatch, + thisBatch, + [], + maxTokens, + overlapTokens, + ellipsis + ) + } + + if (prevBatch.length) { + const block = prevBatch.join('') + + yield trimTextBlock(block) + } +} + +// --- +// --- +// --- + +/** + * Slices the given string from start to stop token. This function has the same + * behavior as the Array.slice method but for text tokens. + */ +export function slice( + input: string, + startToken: number, + stopToken: number = Infinity, + options: { + toTextTokens: (input: string) => string[] + } = { toTextTokens: getTextTokens } +): string { + const { toTextTokens } = options + + const tokens = toTextTokens(input) + + const sliced = tokens.slice(startToken, stopToken) + + const result = sliced.join('') + + return result +} + +/** + * Splits the text of maxToken sizes with overlap tokens. + */ +export function split( + input: string, + maxTokens: number, + overlapTokens: number = 0, + options: { + toTextTokens: (input: string) => string[] + } = { toTextTokens: getTextTokens } +): string[] { + assert(overlapTokens >= 0, 'overlapTokens must be greater than or equal to 0') + assert(overlapTokens < maxTokens, 'overlapTokens must be less than maxTokens') + + const { toTextTokens } = options + + const chunks: string[] = [] + + const tokens = toTextTokens(input) + + const stepSize = maxTokens - overlapTokens + + for (let i = 0; i < tokens.length; i += stepSize) { + const chunk = tokens.slice(i, i + maxTokens).join('') + + if (chunks.length === 0 || !chunks[chunks.length - 1]?.endsWith(chunk)) { + chunks.push(chunk) + } + } + + return chunks +} diff --git a/packages/gpt/src/text-splitter.test.ts b/packages/gpt/src/text-splitter.test.ts new file mode 100644 index 0000000..cbe1f00 --- /dev/null +++ b/packages/gpt/src/text-splitter.test.ts @@ -0,0 +1,1195 @@ +import { getTextTokensLength } from './index' +import { + DEFAULT_SEPARATORS, + splitTextRecursive, + splitTextRecursiveByTokens, +} from './text-splitter' + +// --- +// --- +// --- + +// @note helper ported from LangChain.js test suite to generate lines of +// repeated characters for chunk boundary testing + +function textLineGenerator(char: string, length: number): string { + const line = new Array(length).join(char) + + return `${line}\n` +} + +// --- +// --- +// --- + +describe('splitTextRecursive', () => { + describe('basic splitting', () => { + it('returns single chunk for short text', () => { + const output = splitTextRecursive('Hello world', { + chunkSize: 100, + chunkOverlap: 0, + }) + + expect(output).toEqual(['Hello world']) + }) + + it('returns empty array for empty text', () => { + const output = splitTextRecursive('', { + chunkSize: 100, + chunkOverlap: 0, + }) + + expect(output).toEqual([]) + }) + + it('splits text on double newlines by default', () => { + const text = 'Hello\n\nWorld' + + const output = splitTextRecursive(text, { + chunkSize: 10, + chunkOverlap: 0, + }) + + expect(output).toEqual(['Hello', 'World']) + }) + + it('splits text on single newlines when no double newlines present', () => { + const text = 'Hello\nWorld' + + const output = splitTextRecursive(text, { + chunkSize: 6, + chunkOverlap: 0, + }) + + expect(output).toEqual(['Hello', 'World']) + }) + + it('splits text on spaces as last resort before characters', () => { + const text = 'foo bar baz' + + const output = splitTextRecursive(text, { + chunkSize: 5, + chunkOverlap: 0, + }) + + expect(output).toEqual(['foo', 'bar', 'baz']) + }) + + it('splits text into individual characters as final fallback', () => { + const text = 'abcde' + + const output = splitTextRecursive(text, { + chunkSize: 2, + chunkOverlap: 0, + }) + + expect(output).toEqual(['ab', 'cd', 'e']) + }) + }) + + // --- + // --- + // --- + + describe('overlap', () => { + it('creates overlapping chunks', () => { + const text = 'foo bar baz 123' + + const output = splitTextRecursive(text, { + chunkSize: 7, + chunkOverlap: 3, + keepSeparator: false, + }) + + expect(output).toEqual(['foo bar', 'bar baz', 'baz 123']) + }) + + it('handles overlap with single character separators', () => { + const text = 'aa ab ac ba bb' + + const output = splitTextRecursive(text, { + keepSeparator: false, + chunkSize: 7, + chunkOverlap: 3, + }) + + expect(output).toEqual(['aa ab', 'ab ac', 'ac ba', 'ba bb']) + }) + }) + + // --- + // --- + // --- + + describe('recursive behavior', () => { + it('uses double newline first then falls back to single newline', () => { + const text = + "Hi.\n\nI'm Harrison.\n\nHow? Are? You?\nOkay then f f f f.\nThis is a weird text to write, but gotta test the splittingggg some how.\n\nBye!\n\n-H." + + const output = splitTextRecursive(text, { + chunkSize: 10, + chunkOverlap: 1, + }) + + expect(output).toEqual([ + 'Hi.', + "I'm", + 'Harrison.', + 'How? Are?', + 'You?', + 'Okay then', + 'f f f f.', + 'This is a', + 'weird', + 'text to', + 'write,', + 'but gotta', + 'test the', + 'splitting', + 'gggg', + 'some how.', + 'Bye!', + '-H.', + ]) + }) + + it('handles multi-level separator fallback', () => { + const text = + 'Part A first paragraph.\n\nPart A second paragraph.\n\nPart B section.\nLine 1\nLine 2' + + const output = splitTextRecursive(text, { + chunkSize: 30, + chunkOverlap: 0, + }) + + expect(output.length).toBeGreaterThan(1) + + for (const chunk of output) { + expect(chunk.length).toBeLessThanOrEqual(30) + } + }) + }) + + // --- + // --- + // --- + + describe('keepSeparator', () => { + it('keeps separator when keepSeparator is true (default)', () => { + const text = 'Hello\n\nWorld' + + const output = splitTextRecursive(text, { + chunkSize: 8, + chunkOverlap: 0, + keepSeparator: true, + }) + + expect(output).toEqual(['Hello', 'World']) + }) + + it('removes separator when keepSeparator is false', () => { + const text = 'foo bar baz' + + const output = splitTextRecursive(text, { + chunkSize: 7, + chunkOverlap: 3, + keepSeparator: false, + }) + + expect(output).toEqual(['foo bar', 'bar baz']) + }) + }) + + // --- + // --- + // --- + + describe('custom separators', () => { + it('uses custom separators', () => { + const text = 'part1|part2|part3' + + const output = splitTextRecursive(text, { + chunkSize: 6, + chunkOverlap: 0, + separators: ['|', ''], + keepSeparator: false, + }) + + expect(output).toEqual(['part1', 'part2', 'part3']) + }) + + it('falls through custom separators in order', () => { + const text = 'section1##subsection1.1##subsection1.2\n\nsection2' + + const output = splitTextRecursive(text, { + chunkSize: 20, + chunkOverlap: 0, + separators: ['\n\n', '##', ' ', ''], + keepSeparator: false, + }) + + expect(output.length).toBeGreaterThan(1) + + for (const chunk of output) { + expect(chunk.length).toBeLessThanOrEqual(20) + } + }) + }) + + // --- + // --- + // --- + + describe('custom length function', () => { + it('uses a custom length function', () => { + // @note word count length function + const wordCount = (text: string) => + text.split(/\s+/).filter((w) => w.length > 0).length + + const text = 'one two three four five six seven eight' + + const output = splitTextRecursive(text, { + chunkSize: 3, + chunkOverlap: 0, + lengthFunction: wordCount, + }) + + expect(output.length).toBeGreaterThan(1) + }) + }) + + // --- + // --- + // --- + + describe('validation', () => { + it('throws when chunkOverlap >= chunkSize', () => { + expect(() => { + splitTextRecursive('test', { chunkSize: 2, chunkOverlap: 4 }) + }).toThrow('Cannot have chunkOverlap >= chunkSize') + + expect(() => { + splitTextRecursive('test', { chunkSize: 2, chunkOverlap: 2 }) + }).toThrow('Cannot have chunkOverlap >= chunkSize') + }) + }) + + // --- + // --- + // --- + + describe('DEFAULT_SEPARATORS', () => { + it('has the correct default separators', () => { + expect(DEFAULT_SEPARATORS).toEqual(['\n\n', '\n', ' ', '']) + }) + }) + + // --- + // --- + // --- + + describe('edge cases', () => { + it('handles text with only separators', () => { + const output = splitTextRecursive('\n\n\n\n', { + chunkSize: 5, + chunkOverlap: 0, + }) + + expect(output).toEqual([]) + }) + + it('handles single character text', () => { + const output = splitTextRecursive('a', { + chunkSize: 10, + chunkOverlap: 0, + }) + + expect(output).toEqual(['a']) + }) + + it('handles text with trailing whitespace', () => { + const output = splitTextRecursive('hello ', { + chunkSize: 10, + chunkOverlap: 0, + }) + + expect(output).toEqual(['hello']) + }) + + it('handles large chunk size', () => { + const text = 'This is a test string that should fit in one chunk.' + + const output = splitTextRecursive(text, { + chunkSize: 1000, + chunkOverlap: 0, + }) + + expect(output).toEqual([text]) + }) + + it('handles text with multiple consecutive separators', () => { + const text = 'Hello\n\n\n\nWorld' + + const output = splitTextRecursive(text, { + chunkSize: 10, + chunkOverlap: 0, + }) + + expect(output).toEqual(['Hello', 'World']) + }) + }) +}) + +// --- +// --- +// --- + +// @note the following tests are ported from langchain-ai/langchainjs +// libs/langchain-textsplitters/src/tests/text_splitter.test.ts to verify +// behavioral parity between our implementation and LangChain's +// RecursiveCharacterTextSplitter + +describe('langchain parity: CharacterTextSplitter equivalents', () => { + // @note LangChain's CharacterTextSplitter uses a single separator and then + // calls mergeSplits. We replicate this using separators: [sep] with + // keepSeparator: false. + + it('splits by character count with overlap', () => { + const output = splitTextRecursive('foo bar baz 123', { + separators: [' '], + chunkSize: 7, + chunkOverlap: 3, + keepSeparator: false, + }) + + expect(output).toEqual(['foo bar', 'bar baz', 'baz 123']) + }) + + it('does not create empty documents from double spaces', () => { + // @note LangChain's CharacterTextSplitter is non-recursive with a single + // separator. We replicate this with separators: [' ', ''] so the recursive + // splitter has a fallback. + const output = splitTextRecursive('foo bar', { + separators: [' ', ''], + chunkSize: 4, + chunkOverlap: 0, + keepSeparator: false, + }) + + expect(output).toEqual(['foo', 'bar']) + }) + + it('handles long words that exceed chunk size', () => { + const output = splitTextRecursive('foo bar baz a a', { + separators: [' ', ''], + chunkSize: 3, + chunkOverlap: 1, + keepSeparator: false, + }) + + expect(output).toEqual(['foo', 'bar', 'baz', 'a a']) + }) + + it('handles shorter words first then long words', () => { + const output = splitTextRecursive('a a foo bar baz', { + separators: [' ', ''], + chunkSize: 3, + chunkOverlap: 1, + keepSeparator: false, + }) + + expect(output).toEqual(['a a', 'foo', 'bar', 'baz']) + }) + + it('splits into characters with tiny chunk size', () => { + // @note unlike LangChain's CharacterTextSplitter (which leaves oversized + // splits intact), the recursive splitter falls through to '' separator + // and splits into individual characters when chunkSize < word length + const output = splitTextRecursive('foo bar baz', { + separators: [' ', ''], + chunkSize: 1, + chunkOverlap: 0, + keepSeparator: false, + }) + + expect(output).toEqual(['f', 'o', 'o', 'b', 'a', 'r', 'b', 'a', 'z']) + }) + + it('handles exhausted separators gracefully', () => { + // @note when separators list has no '' fallback, long words are emitted + // as-is rather than crashing + const output = splitTextRecursive('foo bar baz', { + separators: [' '], + chunkSize: 2, + chunkOverlap: 0, + keepSeparator: false, + }) + + expect(output).toEqual(['foo', 'bar', 'baz']) + }) +}) + +// --- +// --- +// --- + +// @note the following tests are ported from langchain-ai/langchain (Python) +// libs/text-splitters/tests/unit_tests/test_text_splitters.py to verify +// behavioral parity between our implementation and LangChain Python's +// RecursiveCharacterTextSplitter + +describe('langchain parity (python): CharacterTextSplitter equivalents', () => { + it('splits edge separator into small chunks', () => { + // @note Python: test_character_text_splitter_separtor_empty_doc + // "f b" with separator " ", chunk_size=2 → ["f", "b"] + const output = splitTextRecursive('f b', { + separators: [' ', ''], + chunkSize: 2, + chunkOverlap: 0, + keepSeparator: false, + }) + + expect(output).toEqual(['f', 'b']) + }) + + it('handles text with no matching separator', () => { + // @note Python: test_character_text_splitter_no_separator_in_text + // Single word with no separator present returns it as-is + const output = splitTextRecursive('singleword', { + separators: [' ', ''], + chunkSize: 10, + chunkOverlap: 0, + keepSeparator: false, + }) + + expect(output).toEqual(['singleword']) + }) + + it('returns empty array for whitespace-only input', () => { + // @note Python: test_character_text_splitter_whitespace_only + const output = splitTextRecursive(' ', { + separators: [' ', ''], + chunkSize: 5, + chunkOverlap: 0, + keepSeparator: false, + }) + + expect(output).toEqual([]) + }) + + it('merges splits respecting chunk size and overlap', () => { + // @note Python: test_merge_splits + // ["foo", "bar", "baz"] with separator " ", chunk_size=9, overlap=2 + // → "foo bar" (len 7 ≤ 9), then "baz" (can't merge further) + const output = splitTextRecursive('foo bar baz', { + separators: [' '], + chunkSize: 9, + chunkOverlap: 2, + keepSeparator: false, + }) + + expect(output).toEqual(['foo bar', 'baz']) + }) +}) + +// --- +// --- +// --- + +describe('langchain parity (python): RecursiveCharacterTextSplitter', () => { + it('splits with keepSeparator true using custom separators', () => { + // @note Python: test_iterative_text_splitter_keep_separator + // Text "....5X..3Y...4X....5Y..." with separators ["X", "Y"], + // keepSeparator=true, chunkSize=6 (5 + 1 for separator) + const output = splitTextRecursive('....5X..3Y...4X....5Y...', { + separators: ['X', 'Y'], + chunkSize: 6, + chunkOverlap: 0, + keepSeparator: true, + }) + + expect(output).toEqual(['....5', 'X..3', 'Y...4', 'X....5', 'Y...']) + }) + + it('splits with keepSeparator false using custom separators', () => { + // @note Python: test_iterative_text_splitter_discard_separator + // Same text but keepSeparator=false and chunkSize=5 + const output = splitTextRecursive('....5X..3Y...4X....5Y...', { + separators: ['X', 'Y'], + chunkSize: 5, + chunkOverlap: 0, + keepSeparator: false, + }) + + expect(output).toEqual(['....5', '..3', '...4', '....5', '...']) + }) + + it('validates that chunk overlap must be less than chunk size', () => { + // @note Python: test_character_text_splitting_args + // Python only rejects overlap > size, but JS LangChain and our + // implementation also reject overlap == size. This is a known difference. + expect(() => { + splitTextRecursive('test', { chunkSize: 2, chunkOverlap: 4 }) + }).toThrow('Cannot have chunkOverlap >= chunkSize') + }) + + it('rejects zero and negative chunk sizes', () => { + // @note Python: test_character_text_splitting_args validates + // chunk_size > 0 and chunk_overlap >= 0. Our implementation uses + // defaults when not provided, so this validates the throw path. + expect(() => { + splitTextRecursive('test', { chunkSize: 0, chunkOverlap: 0 }) + }).toThrow() + }) + + it('splits with keepSeparator=true (start) matching Python start behavior', () => { + // @note Python: test_recursive_character_text_splitter_keep_separators + // Python supports keep_separator="start" which prepends separator to + // the following chunk. Our keepSeparator=true uses regex lookahead + // which produces the same "start" behavior. + const output = splitTextRecursive('Apple,banana,orange and tomato.', { + separators: [',', '.'], + chunkSize: 10, + chunkOverlap: 0, + keepSeparator: true, + }) + + expect(output).toEqual(['Apple', ',banana', ',orange and tomato', '.']) + }) + + // @note Python also supports keep_separator="end" which appends the + // separator to the preceding chunk: ["Apple,", "banana,", "orange and tomato."] + // Our implementation only supports boolean keepSeparator (matching JS + // LangChain). "end" mode is a known Python-only feature difference. +}) + +// --- +// --- +// --- + +describe('langchain parity: RecursiveCharacterTextSplitter', () => { + it('produces one unique chunk for short content', () => { + const content = textLineGenerator('A', 70) + + const output = splitTextRecursive(content, { + chunkSize: 100, + chunkOverlap: 0, + }) + + expect(output).toEqual([content.trim()]) + }) + + it('splits two lines into separate chunks', () => { + const line1 = textLineGenerator('A', 70) + const line2 = textLineGenerator('B', 70) + const content = line1 + line2 + + const output = splitTextRecursive(content, { + chunkSize: 100, + chunkOverlap: 0, + }) + + expect(output).toEqual([line1.trim(), line2.trim()]) + }) + + it('splits identical lines into separate chunks', () => { + const line = textLineGenerator('A', 70) + const content = line + line + + const output = splitTextRecursive(content, { + chunkSize: 100, + chunkOverlap: 0, + }) + + expect(output).toEqual([line.trim(), line.trim()]) + }) + + it('handles content starting with newlines', () => { + const line1 = textLineGenerator('\n', 2) + const line2 = textLineGenerator('A', 70) + const line3 = textLineGenerator('\n', 4) + const line4 = textLineGenerator('B', 70) + + const content = line1 + line2 + line3 + line4 + + const output = splitTextRecursive(content, { + chunkSize: 100, + chunkOverlap: 0, + }) + + expect(output).toEqual([line2.trim(), line4.trim()]) + }) + + it('creates overlapping chunks from generated lines', () => { + const line1 = textLineGenerator('A', 70) + const line2 = textLineGenerator('B', 20) + const line3 = textLineGenerator('C', 70) + const content = line1 + line2 + line3 + + const output = splitTextRecursive(content, { + chunkSize: 100, + chunkOverlap: 30, + }) + + // @note first chunk contains line1 + line2, second contains line2 + line3 + expect(output).toEqual([(line1 + line2).trim(), (line2 + line3).trim()]) + }) + + it('handles overlap spanning multiple short lines', () => { + const line1 = textLineGenerator('A', 70) + const line2 = textLineGenerator('B', 10) + const line3 = textLineGenerator('C', 10) + const line4 = textLineGenerator('D', 70) + const content = line1 + line2 + line3 + line4 + + const output = splitTextRecursive(content, { + chunkSize: 100, + chunkOverlap: 30, + }) + + expect(output).toEqual([ + (line1 + line2 + line3).trim(), + (line2 + line3 + line4).trim(), + ]) + }) + + it('handles the iterative text splitter test case', () => { + const text = `Hi.\n\nI'm Harrison.\n\nHow? Are? You?\nOkay then f f f f.\nThis is a weird text to write, but gotta test the splittingggg some how.\n\nBye!\n\n-H.` + + const output = splitTextRecursive(text, { + chunkSize: 10, + chunkOverlap: 1, + }) + + expect(output).toEqual([ + 'Hi.', + "I'm", + 'Harrison.', + 'How? Are?', + 'You?', + 'Okay then', + 'f f f f.', + 'This is a', + 'weird', + 'text to', + 'write,', + 'but gotta', + 'test the', + 'splitting', + 'gggg', + 'some how.', + 'Bye!', + '-H.', + ]) + }) + + it('considers separator length correctly for chunk size', () => { + const output = splitTextRecursive('aa ab ac ba bb', { + keepSeparator: false, + chunkSize: 7, + chunkOverlap: 3, + }) + + expect(output).toEqual(['aa ab', 'ab ac', 'ac ba', 'ba bb']) + }) +}) + +// --- +// --- +// --- + +describe('langchain parity: language-specific separators', () => { + it('splits markdown content using markdown separators', () => { + const text = + '# 🦜️🔗 LangChain\n' + + '\n' + + '⚡ Building applications with LLMs through composability ⚡\n' + + '\n' + + '## Quick Install\n' + + '\n' + + '```bash\n' + + "# Hopefully this code block isn't split\n" + + 'pip install langchain\n' + + '```\n' + + '\n' + + 'As an open source project in a rapidly developing field, we are extremely open to contributions.' + + // @note these are the separators from + // RecursiveCharacterTextSplitter.getSeparatorsForLanguage('markdown') + const markdownSeparators = [ + '\n## ', + '\n### ', + '\n#### ', + '\n##### ', + '\n###### ', + '```\n\n', + '\n\n***\n\n', + '\n\n---\n\n', + '\n\n___\n\n', + '\n\n', + '\n', + ' ', + '', + ] + + const output = splitTextRecursive(text, { + separators: markdownSeparators, + chunkSize: 100, + chunkOverlap: 0, + keepSeparator: true, + }) + + expect(output).toEqual([ + '# 🦜️🔗 LangChain\n\n⚡ Building applications with LLMs through composability ⚡', + "## Quick Install\n\n```bash\n# Hopefully this code block isn't split\npip install langchain", + '```', + 'As an open source project in a rapidly developing field, we are extremely open to contributions.', + ]) + }) + + it('splits LaTeX content using LaTeX separators', () => { + const text = [ + '\\begin{document}', + '\\title{🦜️🔗 LangChain}', + '⚡ Building applications with LLMs through composability ⚡', + '', + '\\section{Quick Install}', + '', + '\\begin{verbatim}', + "Hopefully this code block isn't split", + 'pnpm install langchain', + '\\end{verbatim}', + '', + 'As an open source project in a rapidly developing field, we are extremely open to contributions.', + '', + '\\end{document}', + ].join('\n') + + // @note these are the separators from + // RecursiveCharacterTextSplitter.getSeparatorsForLanguage('latex') + const latexSeparators = [ + '\n\\chapter{', + '\n\\section{', + '\n\\subsection{', + '\n\\subsubsection{', + '\n\\begin{enumerate}', + '\n\\begin{itemize}', + '\n\\begin{description}', + '\n\\begin{list}', + '\n\\begin{quote}', + '\n\\begin{quotation}', + '\n\\begin{verse}', + '\n\\begin{verbatim}', + '\n\\begin{align}', + '$$', + '$', + '\n\n', + '\n', + ' ', + '', + ] + + const output = splitTextRecursive(text, { + separators: latexSeparators, + chunkSize: 100, + chunkOverlap: 0, + keepSeparator: true, + }) + + expect(output).toEqual([ + '\\begin{document}\n\\title{🦜️🔗 LangChain}\n⚡ Building applications with LLMs through composability ⚡', + '\\section{Quick Install}', + "\\begin{verbatim}\nHopefully this code block isn't split\npnpm install langchain\n\\end{verbatim}", + 'As an open source project in a rapidly developing field, we are extremely open to contributions.', + '\\end{document}', + ]) + }) + + it('splits HTML content using HTML separators', () => { + const text = [ + '', + '', + ' ', + ' 🦜️🔗 LangChain', + ' ', + ' ', + ' ', + '
', + '

🦜️🔗 LangChain

', + '

⚡ Building applications with LLMs through composability ⚡

', + '
', + '
', + ' As an open source project in a rapidly developing field, we are extremely open to contributions.', + '
', + ' ', + '', + ].join('\n') + + // @note these are the separators from + // RecursiveCharacterTextSplitter.getSeparatorsForLanguage('html') + const htmlSeparators = [ + '', + '
', + '

', + '
', + '

  • ', + '

    ', + '

    ', + '

    ', + '

    ', + '

    ', + '
    ', + '', + '', + '', + '
    ', + '', + '
      ', + '
        ', + '
        ', + '