happy2 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/package.json +74 -0
- package/packages/happy2-server/Dockerfile +18 -0
- package/packages/happy2-server/README.md +418 -0
- package/packages/happy2-server/bin/happy2.js +3 -0
- package/packages/happy2-server/dist/cli.d.ts +3 -0
- package/packages/happy2-server/dist/cli.d.ts.map +1 -0
- package/packages/happy2-server/dist/cli.js +44 -0
- package/packages/happy2-server/dist/cli.js.map +1 -0
- package/packages/happy2-server/dist/index.d.ts +24 -0
- package/packages/happy2-server/dist/index.d.ts.map +1 -0
- package/packages/happy2-server/dist/index.js +22 -0
- package/packages/happy2-server/dist/index.js.map +1 -0
- package/packages/happy2-server/dist/modules/agents/builtin-images.d.ts +8 -0
- package/packages/happy2-server/dist/modules/agents/builtin-images.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/agents/builtin-images.js +283 -0
- package/packages/happy2-server/dist/modules/agents/builtin-images.js.map +1 -0
- package/packages/happy2-server/dist/modules/agents/command.d.ts +3 -0
- package/packages/happy2-server/dist/modules/agents/command.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/agents/command.js +7 -0
- package/packages/happy2-server/dist/modules/agents/command.js.map +1 -0
- package/packages/happy2-server/dist/modules/agents/daemon.d.ts +106 -0
- package/packages/happy2-server/dist/modules/agents/daemon.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/agents/daemon.js +522 -0
- package/packages/happy2-server/dist/modules/agents/daemon.js.map +1 -0
- package/packages/happy2-server/dist/modules/agents/docker.d.ts +51 -0
- package/packages/happy2-server/dist/modules/agents/docker.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/agents/docker.js +256 -0
- package/packages/happy2-server/dist/modules/agents/docker.js.map +1 -0
- package/packages/happy2-server/dist/modules/agents/docker.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/agents/docker.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/agents/docker.test.js +155 -0
- package/packages/happy2-server/dist/modules/agents/docker.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/agents/index.d.ts +5 -0
- package/packages/happy2-server/dist/modules/agents/index.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/agents/index.js +4 -0
- package/packages/happy2-server/dist/modules/agents/index.js.map +1 -0
- package/packages/happy2-server/dist/modules/agents/service.d.ts +140 -0
- package/packages/happy2-server/dist/modules/agents/service.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/agents/service.js +1283 -0
- package/packages/happy2-server/dist/modules/agents/service.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/cloudflare-access.d.ts +15 -0
- package/packages/happy2-server/dist/modules/auth/cloudflare-access.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/cloudflare-access.js +49 -0
- package/packages/happy2-server/dist/modules/auth/cloudflare-access.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/crypto.d.ts +4 -0
- package/packages/happy2-server/dist/modules/auth/crypto.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/crypto.js +31 -0
- package/packages/happy2-server/dist/modules/auth/crypto.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/crypto.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/auth/crypto.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/crypto.test.js +11 -0
- package/packages/happy2-server/dist/modules/auth/crypto.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/email.d.ts +3 -0
- package/packages/happy2-server/dist/modules/auth/email.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/email.js +22 -0
- package/packages/happy2-server/dist/modules/auth/email.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/metadata.d.ts +5 -0
- package/packages/happy2-server/dist/modules/auth/metadata.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/metadata.js +28 -0
- package/packages/happy2-server/dist/modules/auth/metadata.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/metadata.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/auth/metadata.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/metadata.test.js +10 -0
- package/packages/happy2-server/dist/modules/auth/metadata.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/methods.d.ts +10 -0
- package/packages/happy2-server/dist/modules/auth/methods.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/methods.js +21 -0
- package/packages/happy2-server/dist/modules/auth/methods.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/methods.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/auth/methods.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/methods.test.js +16 -0
- package/packages/happy2-server/dist/modules/auth/methods.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/oidc.d.ts +9 -0
- package/packages/happy2-server/dist/modules/auth/oidc.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/oidc.js +68 -0
- package/packages/happy2-server/dist/modules/auth/oidc.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/service.d.ts +44 -0
- package/packages/happy2-server/dist/modules/auth/service.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/service.js +208 -0
- package/packages/happy2-server/dist/modules/auth/service.js.map +1 -0
- package/packages/happy2-server/dist/modules/auth/tokens.d.ts +21 -0
- package/packages/happy2-server/dist/modules/auth/tokens.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/auth/tokens.js +79 -0
- package/packages/happy2-server/dist/modules/auth/tokens.js.map +1 -0
- package/packages/happy2-server/dist/modules/automation/repository.d.ts +127 -0
- package/packages/happy2-server/dist/modules/automation/repository.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/automation/repository.js +821 -0
- package/packages/happy2-server/dist/modules/automation/repository.js.map +1 -0
- package/packages/happy2-server/dist/modules/collaboration/repository.d.ts +839 -0
- package/packages/happy2-server/dist/modules/collaboration/repository.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/collaboration/repository.js +5795 -0
- package/packages/happy2-server/dist/modules/collaboration/repository.js.map +1 -0
- package/packages/happy2-server/dist/modules/collaboration/repository.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/collaboration/repository.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/collaboration/repository.test.js +620 -0
- package/packages/happy2-server/dist/modules/collaboration/repository.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/collaboration/types.d.ts +243 -0
- package/packages/happy2-server/dist/modules/collaboration/types.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/collaboration/types.js +8 -0
- package/packages/happy2-server/dist/modules/collaboration/types.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/defaults.d.ts +8 -0
- package/packages/happy2-server/dist/modules/config/defaults.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/defaults.js +70 -0
- package/packages/happy2-server/dist/modules/config/defaults.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/defaults.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/config/defaults.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/defaults.test.js +27 -0
- package/packages/happy2-server/dist/modules/config/defaults.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/environment.d.ts +4 -0
- package/packages/happy2-server/dist/modules/config/environment.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/environment.js +92 -0
- package/packages/happy2-server/dist/modules/config/environment.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/loader.d.ts +5 -0
- package/packages/happy2-server/dist/modules/config/loader.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/loader.js +251 -0
- package/packages/happy2-server/dist/modules/config/loader.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/loader.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/config/loader.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/loader.test.js +108 -0
- package/packages/happy2-server/dist/modules/config/loader.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/paths.d.ts +7 -0
- package/packages/happy2-server/dist/modules/config/paths.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/paths.js +15 -0
- package/packages/happy2-server/dist/modules/config/paths.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/paths.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/config/paths.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/paths.test.js +22 -0
- package/packages/happy2-server/dist/modules/config/paths.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/runtime.d.ts +6 -0
- package/packages/happy2-server/dist/modules/config/runtime.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/runtime.js +14 -0
- package/packages/happy2-server/dist/modules/config/runtime.js.map +1 -0
- package/packages/happy2-server/dist/modules/config/type.d.ts +88 -0
- package/packages/happy2-server/dist/modules/config/type.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/config/type.js +2 -0
- package/packages/happy2-server/dist/modules/config/type.js.map +1 -0
- package/packages/happy2-server/dist/modules/database.d.ts +105 -0
- package/packages/happy2-server/dist/modules/database.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/database.js +583 -0
- package/packages/happy2-server/dist/modules/database.js.map +1 -0
- package/packages/happy2-server/dist/modules/drizzle.d.ts +11 -0
- package/packages/happy2-server/dist/modules/drizzle.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/drizzle.js +31 -0
- package/packages/happy2-server/dist/modules/drizzle.js.map +1 -0
- package/packages/happy2-server/dist/modules/files/lock.d.ts +3 -0
- package/packages/happy2-server/dist/modules/files/lock.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/files/lock.js +83 -0
- package/packages/happy2-server/dist/modules/files/lock.js.map +1 -0
- package/packages/happy2-server/dist/modules/files/media.d.ts +18 -0
- package/packages/happy2-server/dist/modules/files/media.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/files/media.js +352 -0
- package/packages/happy2-server/dist/modules/files/media.js.map +1 -0
- package/packages/happy2-server/dist/modules/files/provider.d.ts +130 -0
- package/packages/happy2-server/dist/modules/files/provider.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/files/provider.js +448 -0
- package/packages/happy2-server/dist/modules/files/provider.js.map +1 -0
- package/packages/happy2-server/dist/modules/files/quota.d.ts +58 -0
- package/packages/happy2-server/dist/modules/files/quota.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/files/quota.js +180 -0
- package/packages/happy2-server/dist/modules/files/quota.js.map +1 -0
- package/packages/happy2-server/dist/modules/files/routes.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/files/routes.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/files/routes.test.js +163 -0
- package/packages/happy2-server/dist/modules/files/routes.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/files/scanner.d.ts +27 -0
- package/packages/happy2-server/dist/modules/files/scanner.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/files/scanner.js +66 -0
- package/packages/happy2-server/dist/modules/files/scanner.js.map +1 -0
- package/packages/happy2-server/dist/modules/files/storage.d.ts +95 -0
- package/packages/happy2-server/dist/modules/files/storage.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/files/storage.js +855 -0
- package/packages/happy2-server/dist/modules/files/storage.js.map +1 -0
- package/packages/happy2-server/dist/modules/files/storage.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/files/storage.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/files/storage.test.js +170 -0
- package/packages/happy2-server/dist/modules/files/storage.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/index.d.ts +6 -0
- package/packages/happy2-server/dist/modules/integrations/index.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/index.js +6 -0
- package/packages/happy2-server/dist/modules/integrations/index.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/repository.d.ts +136 -0
- package/packages/happy2-server/dist/modules/integrations/repository.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/repository.js +1229 -0
- package/packages/happy2-server/dist/modules/integrations/repository.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/repository.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/integrations/repository.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/repository.test.js +345 -0
- package/packages/happy2-server/dist/modules/integrations/repository.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/routes.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/integrations/routes.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/routes.test.js +161 -0
- package/packages/happy2-server/dist/modules/integrations/routes.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/secrets.d.ts +19 -0
- package/packages/happy2-server/dist/modules/integrations/secrets.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/secrets.js +69 -0
- package/packages/happy2-server/dist/modules/integrations/secrets.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/ssrf.d.ts +32 -0
- package/packages/happy2-server/dist/modules/integrations/ssrf.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/ssrf.js +185 -0
- package/packages/happy2-server/dist/modules/integrations/ssrf.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/ssrf.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/integrations/ssrf.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/ssrf.test.js +56 -0
- package/packages/happy2-server/dist/modules/integrations/ssrf.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/transport.d.ts +21 -0
- package/packages/happy2-server/dist/modules/integrations/transport.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/transport.js +108 -0
- package/packages/happy2-server/dist/modules/integrations/transport.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/transport.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/integrations/transport.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/transport.test.js +66 -0
- package/packages/happy2-server/dist/modules/integrations/transport.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/types.d.ts +155 -0
- package/packages/happy2-server/dist/modules/integrations/types.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/integrations/types.js +26 -0
- package/packages/happy2-server/dist/modules/integrations/types.js.map +1 -0
- package/packages/happy2-server/dist/modules/operations/export-worker.d.ts +16 -0
- package/packages/happy2-server/dist/modules/operations/export-worker.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/operations/export-worker.js +56 -0
- package/packages/happy2-server/dist/modules/operations/export-worker.js.map +1 -0
- package/packages/happy2-server/dist/modules/operations/repository.d.ts +215 -0
- package/packages/happy2-server/dist/modules/operations/repository.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/operations/repository.js +2011 -0
- package/packages/happy2-server/dist/modules/operations/repository.js.map +1 -0
- package/packages/happy2-server/dist/modules/operations/repository.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/operations/repository.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/operations/repository.test.js +610 -0
- package/packages/happy2-server/dist/modules/operations/repository.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/operations/types.d.ts +144 -0
- package/packages/happy2-server/dist/modules/operations/types.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/operations/types.js +9 -0
- package/packages/happy2-server/dist/modules/operations/types.js.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/events.d.ts +99 -0
- package/packages/happy2-server/dist/modules/realtime/events.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/events.js +173 -0
- package/packages/happy2-server/dist/modules/realtime/events.js.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/index.d.ts +4 -0
- package/packages/happy2-server/dist/modules/realtime/index.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/index.js +4 -0
- package/packages/happy2-server/dist/modules/realtime/index.js.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/local.d.ts +37 -0
- package/packages/happy2-server/dist/modules/realtime/local.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/local.js +181 -0
- package/packages/happy2-server/dist/modules/realtime/local.js.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/local.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/realtime/local.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/local.test.js +186 -0
- package/packages/happy2-server/dist/modules/realtime/local.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/pubsub.d.ts +26 -0
- package/packages/happy2-server/dist/modules/realtime/pubsub.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/realtime/pubsub.js +7 -0
- package/packages/happy2-server/dist/modules/realtime/pubsub.js.map +1 -0
- package/packages/happy2-server/dist/modules/request/database-idempotency.d.ts +15 -0
- package/packages/happy2-server/dist/modules/request/database-idempotency.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/request/database-idempotency.js +141 -0
- package/packages/happy2-server/dist/modules/request/database-idempotency.js.map +1 -0
- package/packages/happy2-server/dist/modules/request/idempotency-hook.d.ts +12 -0
- package/packages/happy2-server/dist/modules/request/idempotency-hook.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/request/idempotency-hook.js +98 -0
- package/packages/happy2-server/dist/modules/request/idempotency-hook.js.map +1 -0
- package/packages/happy2-server/dist/modules/request/idempotency.d.ts +146 -0
- package/packages/happy2-server/dist/modules/request/idempotency.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/request/idempotency.js +312 -0
- package/packages/happy2-server/dist/modules/request/idempotency.js.map +1 -0
- package/packages/happy2-server/dist/modules/request/idempotency.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/request/idempotency.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/request/idempotency.test.js +191 -0
- package/packages/happy2-server/dist/modules/request/idempotency.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/request/index.d.ts +5 -0
- package/packages/happy2-server/dist/modules/request/index.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/request/index.js +5 -0
- package/packages/happy2-server/dist/modules/request/index.js.map +1 -0
- package/packages/happy2-server/dist/modules/request/rate-limit.d.ts +100 -0
- package/packages/happy2-server/dist/modules/request/rate-limit.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/request/rate-limit.js +239 -0
- package/packages/happy2-server/dist/modules/request/rate-limit.js.map +1 -0
- package/packages/happy2-server/dist/modules/request/rate-limit.test.d.ts +2 -0
- package/packages/happy2-server/dist/modules/request/rate-limit.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/request/rate-limit.test.js +123 -0
- package/packages/happy2-server/dist/modules/request/rate-limit.test.js.map +1 -0
- package/packages/happy2-server/dist/modules/schema.d.ts +31454 -0
- package/packages/happy2-server/dist/modules/schema.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/schema.js +1081 -0
- package/packages/happy2-server/dist/modules/schema.js.map +1 -0
- package/packages/happy2-server/dist/modules/workspace/index.d.ts +3 -0
- package/packages/happy2-server/dist/modules/workspace/index.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/workspace/index.js +3 -0
- package/packages/happy2-server/dist/modules/workspace/index.js.map +1 -0
- package/packages/happy2-server/dist/modules/workspace/service.d.ts +54 -0
- package/packages/happy2-server/dist/modules/workspace/service.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/workspace/service.js +997 -0
- package/packages/happy2-server/dist/modules/workspace/service.js.map +1 -0
- package/packages/happy2-server/dist/modules/workspace/types.d.ts +58 -0
- package/packages/happy2-server/dist/modules/workspace/types.d.ts.map +1 -0
- package/packages/happy2-server/dist/modules/workspace/types.js +11 -0
- package/packages/happy2-server/dist/modules/workspace/types.js.map +1 -0
- package/packages/happy2-server/dist/routes/agents.d.ts +5 -0
- package/packages/happy2-server/dist/routes/agents.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/agents.js +225 -0
- package/packages/happy2-server/dist/routes/agents.js.map +1 -0
- package/packages/happy2-server/dist/routes/auth.d.ts +5 -0
- package/packages/happy2-server/dist/routes/auth.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/auth.js +128 -0
- package/packages/happy2-server/dist/routes/auth.js.map +1 -0
- package/packages/happy2-server/dist/routes/automation.d.ts +6 -0
- package/packages/happy2-server/dist/routes/automation.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/automation.js +315 -0
- package/packages/happy2-server/dist/routes/automation.js.map +1 -0
- package/packages/happy2-server/dist/routes/basic.d.ts +3 -0
- package/packages/happy2-server/dist/routes/basic.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/basic.js +5 -0
- package/packages/happy2-server/dist/routes/basic.js.map +1 -0
- package/packages/happy2-server/dist/routes/collaboration.d.ts +7 -0
- package/packages/happy2-server/dist/routes/collaboration.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/collaboration.js +1285 -0
- package/packages/happy2-server/dist/routes/collaboration.js.map +1 -0
- package/packages/happy2-server/dist/routes/files.d.ts +9 -0
- package/packages/happy2-server/dist/routes/files.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/files.js +397 -0
- package/packages/happy2-server/dist/routes/files.js.map +1 -0
- package/packages/happy2-server/dist/routes/integrations.d.ts +6 -0
- package/packages/happy2-server/dist/routes/integrations.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/integrations.js +486 -0
- package/packages/happy2-server/dist/routes/integrations.js.map +1 -0
- package/packages/happy2-server/dist/routes/operations.d.ts +5 -0
- package/packages/happy2-server/dist/routes/operations.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/operations.js +588 -0
- package/packages/happy2-server/dist/routes/operations.js.map +1 -0
- package/packages/happy2-server/dist/routes/operations.test.d.ts +2 -0
- package/packages/happy2-server/dist/routes/operations.test.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/operations.test.js +106 -0
- package/packages/happy2-server/dist/routes/operations.test.js.map +1 -0
- package/packages/happy2-server/dist/routes/sync.d.ts +6 -0
- package/packages/happy2-server/dist/routes/sync.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/sync.js +549 -0
- package/packages/happy2-server/dist/routes/sync.js.map +1 -0
- package/packages/happy2-server/dist/routes/workspace.d.ts +5 -0
- package/packages/happy2-server/dist/routes/workspace.d.ts.map +1 -0
- package/packages/happy2-server/dist/routes/workspace.js +240 -0
- package/packages/happy2-server/dist/routes/workspace.js.map +1 -0
- package/packages/happy2-server/dist/runner.d.ts +3 -0
- package/packages/happy2-server/dist/runner.d.ts.map +1 -0
- package/packages/happy2-server/dist/runner.js +30 -0
- package/packages/happy2-server/dist/runner.js.map +1 -0
- package/packages/happy2-server/dist/server.d.ts +32 -0
- package/packages/happy2-server/dist/server.d.ts.map +1 -0
- package/packages/happy2-server/dist/server.js +284 -0
- package/packages/happy2-server/dist/server.js.map +1 -0
- package/packages/happy2-server/dist/standalone.d.ts +13 -0
- package/packages/happy2-server/dist/standalone.d.ts.map +1 -0
- package/packages/happy2-server/dist/standalone.js +99 -0
- package/packages/happy2-server/dist/standalone.js.map +1 -0
- package/packages/happy2-server/dist/web/assets/figtree-latin-wght-normal-D_ZTVpCC.woff2 +0 -0
- package/packages/happy2-server/dist/web/assets/happy-otter-Q2O_g-DT.png +0 -0
- package/packages/happy2-server/dist/web/assets/index-COvbX8JP.css +2 -0
- package/packages/happy2-server/dist/web/assets/index-DdpugxMO.js +57 -0
- package/packages/happy2-server/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/packages/happy2-server/dist/web/assets/onboarding-CRUpq9xl.png +0 -0
- package/packages/happy2-server/dist/web/index.html +13 -0
- package/packages/happy2-server/drizzle/0000_initial.sql +13 -0
- package/packages/happy2-server/drizzle/0001_accounts_profiles_files.sql +42 -0
- package/packages/happy2-server/drizzle/0002_file_visibility_and_uploader.sql +5 -0
- package/packages/happy2-server/drizzle/0003_collaboration.sql +565 -0
- package/packages/happy2-server/drizzle/0004_collaboration_expansion.sql +1099 -0
- package/packages/happy2-server/drizzle/0005_ai_agents.sql +85 -0
- package/packages/happy2-server/drizzle/0006_rig_global_event_queue.sql +10 -0
- package/packages/happy2-server/drizzle/0007_rename_server_identity.sql +3 -0
- package/packages/happy2-server/drizzle/0008_agent_message_streaming.sql +47 -0
- package/packages/happy2-server/drizzle/0009_chat_workspace_binding_lookup.sql +2 -0
- package/packages/happy2-server/drizzle/0010_rig_secret_assignments.sql +21 -0
- package/packages/happy2-server/drizzle/meta/_journal.json +83 -0
- package/packages/happy2-server/happy2.example.toml +104 -0
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import { createId } from "@paralleldrive/cuid2";
|
|
3
|
+
import { createClient } from "@libsql/client";
|
|
4
|
+
import { and, asc, desc, eq, gt, isNull, lte, or, sql } from "drizzle-orm";
|
|
5
|
+
import { createDatabase, retrySqliteBusy, } from "../drizzle.js";
|
|
6
|
+
import { accounts, apiCredentials, auditLogEntries, botIdentities, chatMembers, chats, files, integrations, serverSyncState, slashCommands, syncEvents, users, webhookDeliveries, webhookSubscriptions, } from "../schema.js";
|
|
7
|
+
import { generateApiToken, generateIncomingWebhookToken, generateSigningSecret, hashesEqual, secretHash, tokenPrefix, } from "./secrets.js";
|
|
8
|
+
import { StrictWebhookUrlPolicy } from "./ssrf.js";
|
|
9
|
+
import { IntegrationError, integrationKinds, integrationScopes, } from "./types.js";
|
|
10
|
+
const MAX_DELIVERY_RESPONSE = 16_000;
|
|
11
|
+
const MAX_EVENT_PAYLOAD = 1_000_000;
|
|
12
|
+
const MAX_DELIVERY_ATTEMPTS = 8;
|
|
13
|
+
export class IntegrationRepository {
|
|
14
|
+
client;
|
|
15
|
+
db;
|
|
16
|
+
ownsClient;
|
|
17
|
+
protector;
|
|
18
|
+
urlPolicy;
|
|
19
|
+
now;
|
|
20
|
+
constructor(source, options) {
|
|
21
|
+
this.ownsClient = typeof source === "string";
|
|
22
|
+
this.client =
|
|
23
|
+
typeof source === "string"
|
|
24
|
+
? createClient({ url: source, authToken: options.authToken })
|
|
25
|
+
: source;
|
|
26
|
+
this.protector = options.secretProtector;
|
|
27
|
+
this.urlPolicy = options.urlPolicy ?? new StrictWebhookUrlPolicy();
|
|
28
|
+
this.now = options.now ?? (() => new Date());
|
|
29
|
+
this.db = createDatabase(this.client);
|
|
30
|
+
}
|
|
31
|
+
close() {
|
|
32
|
+
if (this.ownsClient)
|
|
33
|
+
this.client.close();
|
|
34
|
+
}
|
|
35
|
+
async listBots(actorUserId) {
|
|
36
|
+
await this.requireAdminDb(this.db, actorUserId);
|
|
37
|
+
const rows = await this.db
|
|
38
|
+
.select({
|
|
39
|
+
id: botIdentities.id,
|
|
40
|
+
name: botIdentities.name,
|
|
41
|
+
username: botIdentities.username,
|
|
42
|
+
description: botIdentities.description,
|
|
43
|
+
photo_file_id: botIdentities.photoFileId,
|
|
44
|
+
owner_user_id: botIdentities.ownerUserId,
|
|
45
|
+
active: botIdentities.active,
|
|
46
|
+
created_at: botIdentities.createdAt,
|
|
47
|
+
updated_at: botIdentities.updatedAt,
|
|
48
|
+
})
|
|
49
|
+
.from(botIdentities)
|
|
50
|
+
.orderBy(desc(botIdentities.createdAt), desc(botIdentities.id));
|
|
51
|
+
return rows.map(asBot);
|
|
52
|
+
}
|
|
53
|
+
async createBot(input) {
|
|
54
|
+
const name = requiredTrimmed(input.name, "Bot name", 200);
|
|
55
|
+
const username = normalizedUsername(input.username);
|
|
56
|
+
const description = optionalTrimmed(input.description, "Bot description", 2_000);
|
|
57
|
+
return this.writeDb(async (tx) => {
|
|
58
|
+
await this.requireAdminDb(tx, input.actorUserId);
|
|
59
|
+
if (input.ownerUserId)
|
|
60
|
+
await this.requireActiveUserDb(tx, input.ownerUserId);
|
|
61
|
+
if (input.photoFileId)
|
|
62
|
+
await this.requireFileDb(tx, input.photoFileId);
|
|
63
|
+
const id = createId();
|
|
64
|
+
try {
|
|
65
|
+
await tx.insert(botIdentities).values({
|
|
66
|
+
id,
|
|
67
|
+
name,
|
|
68
|
+
username,
|
|
69
|
+
description: description ?? null,
|
|
70
|
+
photoFileId: input.photoFileId ?? null,
|
|
71
|
+
ownerUserId: input.ownerUserId ?? null,
|
|
72
|
+
createdByUserId: input.actorUserId,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw constraintConflict(error, "Bot username is already in use");
|
|
77
|
+
}
|
|
78
|
+
const change = await this.recordChangeDb(tx, input.actorUserId, "bot.created", id);
|
|
79
|
+
await this.appendAuditDb(tx, input.actorUserId, "bot.created", "bot", id);
|
|
80
|
+
await tx
|
|
81
|
+
.update(botIdentities)
|
|
82
|
+
.set({ syncSequence: Number(change.sequence) })
|
|
83
|
+
.where(eq(botIdentities.id, id));
|
|
84
|
+
return { value: await this.getBotDb(tx, id), change };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async updateBot(input) {
|
|
88
|
+
if (input.name === undefined &&
|
|
89
|
+
input.username === undefined &&
|
|
90
|
+
input.description === undefined &&
|
|
91
|
+
input.photoFileId === undefined &&
|
|
92
|
+
input.ownerUserId === undefined)
|
|
93
|
+
throw new IntegrationError("invalid", "At least one bot field is required");
|
|
94
|
+
const name = input.name === undefined ? undefined : requiredTrimmed(input.name, "Bot name", 200);
|
|
95
|
+
const username = input.username === undefined ? undefined : normalizedUsername(input.username);
|
|
96
|
+
const description = input.description === undefined || input.description === null
|
|
97
|
+
? input.description
|
|
98
|
+
: optionalTrimmed(input.description, "Bot description", 2_000);
|
|
99
|
+
return this.writeDb(async (tx) => {
|
|
100
|
+
await this.requireAdminDb(tx, input.actorUserId);
|
|
101
|
+
await this.getBotDb(tx, input.botId, true);
|
|
102
|
+
if (input.ownerUserId)
|
|
103
|
+
await this.requireActiveUserDb(tx, input.ownerUserId);
|
|
104
|
+
if (input.photoFileId)
|
|
105
|
+
await this.requireFileDb(tx, input.photoFileId);
|
|
106
|
+
try {
|
|
107
|
+
await tx
|
|
108
|
+
.update(botIdentities)
|
|
109
|
+
.set({
|
|
110
|
+
...(name === undefined ? {} : { name }),
|
|
111
|
+
...(username === undefined ? {} : { username }),
|
|
112
|
+
...(input.description === undefined
|
|
113
|
+
? {}
|
|
114
|
+
: { description: description ?? null }),
|
|
115
|
+
...(input.photoFileId === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { photoFileId: input.photoFileId }),
|
|
118
|
+
...(input.ownerUserId === undefined
|
|
119
|
+
? {}
|
|
120
|
+
: { ownerUserId: input.ownerUserId }),
|
|
121
|
+
updatedAt: sql `CURRENT_TIMESTAMP`,
|
|
122
|
+
})
|
|
123
|
+
.where(and(eq(botIdentities.id, input.botId), isNull(botIdentities.deletedAt)));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw constraintConflict(error, "Bot username is already in use");
|
|
127
|
+
}
|
|
128
|
+
const change = await this.recordChangeDb(tx, input.actorUserId, "bot.updated", input.botId);
|
|
129
|
+
await this.appendAuditDb(tx, input.actorUserId, "bot.updated", "bot", input.botId);
|
|
130
|
+
await tx
|
|
131
|
+
.update(botIdentities)
|
|
132
|
+
.set({ syncSequence: Number(change.sequence) })
|
|
133
|
+
.where(eq(botIdentities.id, input.botId));
|
|
134
|
+
return { value: await this.getBotDb(tx, input.botId), change };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async revokeBot(actorUserId, botId) {
|
|
138
|
+
return this.writeDb(async (tx) => {
|
|
139
|
+
await this.requireAdminDb(tx, actorUserId);
|
|
140
|
+
const changed = await tx
|
|
141
|
+
.update(botIdentities)
|
|
142
|
+
.set({
|
|
143
|
+
active: 0,
|
|
144
|
+
deletedAt: sql `CURRENT_TIMESTAMP`,
|
|
145
|
+
updatedAt: sql `CURRENT_TIMESTAMP`,
|
|
146
|
+
})
|
|
147
|
+
.where(and(eq(botIdentities.id, botId), isNull(botIdentities.deletedAt)))
|
|
148
|
+
.returning({ id: botIdentities.id });
|
|
149
|
+
if (changed.length === 0)
|
|
150
|
+
throw new IntegrationError("not_found", "Bot was not found");
|
|
151
|
+
const integrationIds = tx
|
|
152
|
+
.select({ id: integrations.id })
|
|
153
|
+
.from(integrations)
|
|
154
|
+
.where(eq(integrations.botId, botId));
|
|
155
|
+
await tx
|
|
156
|
+
.update(apiCredentials)
|
|
157
|
+
.set({ revokedAt: sql `coalesce(${apiCredentials.revokedAt}, CURRENT_TIMESTAMP)` })
|
|
158
|
+
.where(or(eq(apiCredentials.botId, botId), sql `${apiCredentials.integrationId} in (${integrationIds})`));
|
|
159
|
+
await tx
|
|
160
|
+
.update(integrations)
|
|
161
|
+
.set({
|
|
162
|
+
active: 0,
|
|
163
|
+
deletedAt: sql `coalesce(${integrations.deletedAt}, CURRENT_TIMESTAMP)`,
|
|
164
|
+
updatedAt: sql `CURRENT_TIMESTAMP`,
|
|
165
|
+
})
|
|
166
|
+
.where(eq(integrations.botId, botId));
|
|
167
|
+
const change = await this.recordChangeDb(tx, actorUserId, "bot.revoked", botId);
|
|
168
|
+
await this.appendAuditDb(tx, actorUserId, "bot.revoked", "bot", botId);
|
|
169
|
+
return change;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async listIntegrations(actorUserId) {
|
|
173
|
+
await this.requireAdminDb(this.db, actorUserId);
|
|
174
|
+
const rows = await this.db
|
|
175
|
+
.select(integrationSelection)
|
|
176
|
+
.from(integrations)
|
|
177
|
+
.orderBy(desc(integrations.createdAt), desc(integrations.id));
|
|
178
|
+
return rows.map(asIntegration);
|
|
179
|
+
}
|
|
180
|
+
async createIntegration(input) {
|
|
181
|
+
return this.createIntegrationRecord(input);
|
|
182
|
+
}
|
|
183
|
+
async revokeIntegration(actorUserId, integrationId) {
|
|
184
|
+
return this.writeDb(async (tx) => {
|
|
185
|
+
await this.requireAdminDb(tx, actorUserId);
|
|
186
|
+
const changed = await tx
|
|
187
|
+
.update(integrations)
|
|
188
|
+
.set({
|
|
189
|
+
active: 0,
|
|
190
|
+
deletedAt: sql `CURRENT_TIMESTAMP`,
|
|
191
|
+
updatedAt: sql `CURRENT_TIMESTAMP`,
|
|
192
|
+
})
|
|
193
|
+
.where(and(eq(integrations.id, integrationId), isNull(integrations.deletedAt)))
|
|
194
|
+
.returning({ id: integrations.id });
|
|
195
|
+
if (changed.length === 0)
|
|
196
|
+
throw new IntegrationError("not_found", "Integration was not found");
|
|
197
|
+
await tx
|
|
198
|
+
.update(apiCredentials)
|
|
199
|
+
.set({ revokedAt: sql `coalesce(${apiCredentials.revokedAt}, CURRENT_TIMESTAMP)` })
|
|
200
|
+
.where(eq(apiCredentials.integrationId, integrationId));
|
|
201
|
+
await tx
|
|
202
|
+
.update(webhookSubscriptions)
|
|
203
|
+
.set({ active: 0, updatedAt: sql `CURRENT_TIMESTAMP` })
|
|
204
|
+
.where(eq(webhookSubscriptions.integrationId, integrationId));
|
|
205
|
+
await tx
|
|
206
|
+
.update(slashCommands)
|
|
207
|
+
.set({ active: 0, updatedAt: sql `CURRENT_TIMESTAMP` })
|
|
208
|
+
.where(eq(slashCommands.integrationId, integrationId));
|
|
209
|
+
const change = await this.recordChangeDb(tx, actorUserId, "integration.revoked", integrationId);
|
|
210
|
+
await this.appendAuditDb(tx, actorUserId, "integration.revoked", "integration", integrationId);
|
|
211
|
+
return change;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async listApiCredentials(actorUserId, integrationId) {
|
|
215
|
+
await this.requireAdminDb(this.db, actorUserId);
|
|
216
|
+
await this.requireIntegrationDb(this.db, integrationId, false);
|
|
217
|
+
const rows = await this.db
|
|
218
|
+
.select(credentialSelection)
|
|
219
|
+
.from(apiCredentials)
|
|
220
|
+
.where(eq(apiCredentials.integrationId, integrationId))
|
|
221
|
+
.orderBy(desc(apiCredentials.createdAt), desc(apiCredentials.id));
|
|
222
|
+
return rows.map(asCredential);
|
|
223
|
+
}
|
|
224
|
+
async createApiCredential(input) {
|
|
225
|
+
const name = requiredTrimmed(input.name, "Credential name", 200);
|
|
226
|
+
const expiresAt = input.expiresAt ? futureDate(input.expiresAt, this.now()) : undefined;
|
|
227
|
+
const token = generateApiToken();
|
|
228
|
+
return this.writeDb(async (tx) => {
|
|
229
|
+
await this.requireAdminDb(tx, input.actorUserId);
|
|
230
|
+
const integration = await this.requireIntegrationDb(tx, input.integrationId, true);
|
|
231
|
+
const scopes = input.scopes ? normalizeScopes(input.scopes) : integration.scopes;
|
|
232
|
+
requireScopeSubset(scopes, integration.scopes);
|
|
233
|
+
const id = createId();
|
|
234
|
+
await tx.insert(apiCredentials).values({
|
|
235
|
+
id,
|
|
236
|
+
integrationId: input.integrationId,
|
|
237
|
+
name,
|
|
238
|
+
tokenPrefix: tokenPrefix(token),
|
|
239
|
+
tokenHash: secretHash(token),
|
|
240
|
+
scopesJson: JSON.stringify(scopes),
|
|
241
|
+
createdByUserId: input.actorUserId,
|
|
242
|
+
expiresAt: expiresAt ?? null,
|
|
243
|
+
});
|
|
244
|
+
await this.appendAuditDb(tx, input.actorUserId, "integration.credential_created", "api_credential", id, { integrationId: input.integrationId, scopes });
|
|
245
|
+
const [row] = await tx
|
|
246
|
+
.select(credentialSelection)
|
|
247
|
+
.from(apiCredentials)
|
|
248
|
+
.where(eq(apiCredentials.id, id));
|
|
249
|
+
if (!row)
|
|
250
|
+
throw new Error("API credential was not created");
|
|
251
|
+
return { credential: asCredential(row), token };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async revokeApiCredential(actorUserId, credentialId) {
|
|
255
|
+
await this.writeDb(async (tx) => {
|
|
256
|
+
await this.requireAdminDb(tx, actorUserId);
|
|
257
|
+
const changed = await tx
|
|
258
|
+
.update(apiCredentials)
|
|
259
|
+
.set({ revokedAt: sql `CURRENT_TIMESTAMP` })
|
|
260
|
+
.where(and(eq(apiCredentials.id, credentialId), isNull(apiCredentials.revokedAt)))
|
|
261
|
+
.returning({ id: apiCredentials.id });
|
|
262
|
+
if (changed.length === 0)
|
|
263
|
+
throw new IntegrationError("not_found", "API credential was not found");
|
|
264
|
+
await this.appendAuditDb(tx, actorUserId, "integration.credential_revoked", "api_credential", credentialId);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async authenticateApiCredential(token, requiredScopes = []) {
|
|
268
|
+
if (!token.startsWith("happy2_api_") || token.length > 256)
|
|
269
|
+
return undefined;
|
|
270
|
+
const requested = normalizeScopes(requiredScopes);
|
|
271
|
+
const candidates = await this.db
|
|
272
|
+
.select({
|
|
273
|
+
id: apiCredentials.id,
|
|
274
|
+
integrationId: apiCredentials.integrationId,
|
|
275
|
+
tokenHash: apiCredentials.tokenHash,
|
|
276
|
+
credentialScopesJson: apiCredentials.scopesJson,
|
|
277
|
+
integrationScopesJson: integrations.scopesJson,
|
|
278
|
+
botId: integrations.botId,
|
|
279
|
+
createdByUserId: integrations.createdByUserId,
|
|
280
|
+
})
|
|
281
|
+
.from(apiCredentials)
|
|
282
|
+
.innerJoin(integrations, eq(integrations.id, apiCredentials.integrationId))
|
|
283
|
+
.innerJoin(users, eq(users.id, integrations.createdByUserId))
|
|
284
|
+
.innerJoin(accounts, eq(accounts.id, users.accountId))
|
|
285
|
+
.where(and(eq(apiCredentials.tokenPrefix, tokenPrefix(token)), isNull(apiCredentials.revokedAt), or(isNull(apiCredentials.expiresAt), gt(sql `datetime(${apiCredentials.expiresAt})`, sql `CURRENT_TIMESTAMP`)), eq(integrations.active, 1), isNull(integrations.deletedAt), eq(users.role, "admin"), isNull(users.deletedAt), eq(accounts.active, 1), isNull(accounts.bannedAt), isNull(accounts.deletedAt)));
|
|
286
|
+
const digest = secretHash(token);
|
|
287
|
+
const row = candidates.find((candidate) => hashesEqual(candidate.tokenHash, digest));
|
|
288
|
+
if (!row)
|
|
289
|
+
return undefined;
|
|
290
|
+
const credentialScopes = parseScopes(row.credentialScopesJson);
|
|
291
|
+
const integrationScopeValues = parseScopes(row.integrationScopesJson);
|
|
292
|
+
const effective = credentialScopes.filter((scope) => integrationScopeValues.includes(scope));
|
|
293
|
+
if (requested.some((scope) => !effective.includes(scope)))
|
|
294
|
+
return undefined;
|
|
295
|
+
await this.db
|
|
296
|
+
.update(apiCredentials)
|
|
297
|
+
.set({ lastUsedAt: sql `CURRENT_TIMESTAMP` })
|
|
298
|
+
.where(and(eq(apiCredentials.id, row.id), isNull(apiCredentials.revokedAt)));
|
|
299
|
+
return {
|
|
300
|
+
credentialId: row.id,
|
|
301
|
+
integrationId: row.integrationId,
|
|
302
|
+
actorUserId: row.createdByUserId,
|
|
303
|
+
botId: row.botId ?? undefined,
|
|
304
|
+
scopes: effective,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async createIncomingWebhook(input) {
|
|
308
|
+
const token = generateIncomingWebhookToken();
|
|
309
|
+
return this.writeDb(async (tx) => {
|
|
310
|
+
await this.requireAdminDb(tx, input.actorUserId);
|
|
311
|
+
await this.requireBotDb(tx, input.botId);
|
|
312
|
+
await this.requireChatDb(tx, input.chatId);
|
|
313
|
+
const integration = await this.insertIntegrationDb(tx, {
|
|
314
|
+
actorUserId: input.actorUserId,
|
|
315
|
+
kind: "incoming_webhook",
|
|
316
|
+
name: input.name,
|
|
317
|
+
description: input.description,
|
|
318
|
+
botId: input.botId,
|
|
319
|
+
scopes: ["messages:write"],
|
|
320
|
+
});
|
|
321
|
+
const subscriptionId = createId();
|
|
322
|
+
await tx.insert(webhookSubscriptions).values({
|
|
323
|
+
id: subscriptionId,
|
|
324
|
+
integrationId: integration.id,
|
|
325
|
+
direction: "incoming",
|
|
326
|
+
chatId: input.chatId,
|
|
327
|
+
tokenHash: secretHash(token),
|
|
328
|
+
eventTypesJson: "[]",
|
|
329
|
+
});
|
|
330
|
+
const change = await this.finishIntegrationChangeDb(tx, input.actorUserId, "integration.created", integration.id);
|
|
331
|
+
return {
|
|
332
|
+
value: {
|
|
333
|
+
integration: await this.getIntegrationDb(tx, integration.id),
|
|
334
|
+
subscription: await this.getSubscriptionDb(tx, subscriptionId),
|
|
335
|
+
token,
|
|
336
|
+
},
|
|
337
|
+
change,
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async invokeIncomingWebhook(token, textValue, sink, idempotencyKey) {
|
|
342
|
+
if (!token.startsWith("happy2_hook_") || token.length > 256)
|
|
343
|
+
throw new IntegrationError("unauthorized", "Incoming webhook token is invalid");
|
|
344
|
+
const textBody = requiredText(textValue, "Webhook message", 40_000);
|
|
345
|
+
if (idempotencyKey !== undefined &&
|
|
346
|
+
(idempotencyKey.length === 0 ||
|
|
347
|
+
idempotencyKey.length > 200 ||
|
|
348
|
+
!/^[\x21-\x7e]+$/.test(idempotencyKey)))
|
|
349
|
+
throw new IntegrationError("invalid", "Idempotency key is invalid");
|
|
350
|
+
const [row] = await this.db
|
|
351
|
+
.select({
|
|
352
|
+
id: webhookSubscriptions.id,
|
|
353
|
+
chatId: webhookSubscriptions.chatId,
|
|
354
|
+
integrationId: integrations.id,
|
|
355
|
+
botId: integrations.botId,
|
|
356
|
+
scopesJson: integrations.scopesJson,
|
|
357
|
+
createdByUserId: integrations.createdByUserId,
|
|
358
|
+
})
|
|
359
|
+
.from(webhookSubscriptions)
|
|
360
|
+
.innerJoin(integrations, eq(integrations.id, webhookSubscriptions.integrationId))
|
|
361
|
+
.innerJoin(users, eq(users.id, integrations.createdByUserId))
|
|
362
|
+
.innerJoin(accounts, eq(accounts.id, users.accountId))
|
|
363
|
+
.where(and(eq(webhookSubscriptions.direction, "incoming"), eq(webhookSubscriptions.tokenHash, secretHash(token)), eq(webhookSubscriptions.active, 1), eq(integrations.kind, "incoming_webhook"), eq(integrations.active, 1), isNull(integrations.deletedAt), eq(users.role, "admin"), isNull(users.deletedAt), eq(accounts.active, 1), isNull(accounts.bannedAt), isNull(accounts.deletedAt)));
|
|
364
|
+
if (!row || !parseScopes(row.scopesJson).includes("messages:write"))
|
|
365
|
+
throw new IntegrationError("unauthorized", "Incoming webhook token is invalid");
|
|
366
|
+
const chatId = row.chatId ?? undefined;
|
|
367
|
+
const botId = row.botId ?? undefined;
|
|
368
|
+
const actorUserId = row.createdByUserId ?? undefined;
|
|
369
|
+
if (!chatId || !botId || !actorUserId)
|
|
370
|
+
throw new IntegrationError("forbidden", "Incoming webhook is no longer configured");
|
|
371
|
+
return sink.sendMessage({
|
|
372
|
+
actorUserId,
|
|
373
|
+
integrationId: row.integrationId,
|
|
374
|
+
subscriptionId: row.id,
|
|
375
|
+
botId,
|
|
376
|
+
chatId,
|
|
377
|
+
text: textBody,
|
|
378
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
async createOutgoingWebhook(input) {
|
|
382
|
+
const url = this.urlPolicy.validateForStorage(input.url);
|
|
383
|
+
const eventTypes = normalizeEventTypes(input.eventTypes);
|
|
384
|
+
const signingSecret = generateSigningSecret();
|
|
385
|
+
const ciphertext = await this.protector.protect(signingSecret);
|
|
386
|
+
return this.writeDb(async (tx) => {
|
|
387
|
+
await this.requireAdminDb(tx, input.actorUserId);
|
|
388
|
+
if (input.chatId)
|
|
389
|
+
await this.requireChatDb(tx, input.chatId);
|
|
390
|
+
const integration = await this.insertIntegrationDb(tx, {
|
|
391
|
+
actorUserId: input.actorUserId,
|
|
392
|
+
kind: "outgoing_webhook",
|
|
393
|
+
name: input.name,
|
|
394
|
+
description: input.description,
|
|
395
|
+
scopes: ["events:read"],
|
|
396
|
+
});
|
|
397
|
+
const subscriptionId = createId();
|
|
398
|
+
await tx.insert(webhookSubscriptions).values({
|
|
399
|
+
id: subscriptionId,
|
|
400
|
+
integrationId: integration.id,
|
|
401
|
+
direction: "outgoing",
|
|
402
|
+
chatId: input.chatId ?? null,
|
|
403
|
+
url,
|
|
404
|
+
signingSecretCiphertext: ciphertext,
|
|
405
|
+
eventTypesJson: JSON.stringify(eventTypes),
|
|
406
|
+
});
|
|
407
|
+
const change = await this.finishIntegrationChangeDb(tx, input.actorUserId, "integration.created", integration.id);
|
|
408
|
+
return {
|
|
409
|
+
value: {
|
|
410
|
+
integration: await this.getIntegrationDb(tx, integration.id),
|
|
411
|
+
subscription: await this.getSubscriptionDb(tx, subscriptionId),
|
|
412
|
+
signingSecret,
|
|
413
|
+
},
|
|
414
|
+
change,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async listWebhookSubscriptions(actorUserId, integrationId) {
|
|
419
|
+
await this.requireAdminDb(this.db, actorUserId);
|
|
420
|
+
await this.requireIntegrationDb(this.db, integrationId, false);
|
|
421
|
+
const rows = await this.db
|
|
422
|
+
.select(subscriptionSelection)
|
|
423
|
+
.from(webhookSubscriptions)
|
|
424
|
+
.where(eq(webhookSubscriptions.integrationId, integrationId))
|
|
425
|
+
.orderBy(desc(webhookSubscriptions.createdAt), desc(webhookSubscriptions.id));
|
|
426
|
+
return rows.map(asSubscription);
|
|
427
|
+
}
|
|
428
|
+
async enqueueOutgoingEvent(input) {
|
|
429
|
+
boundedIdentifier(input.eventId, "Event id");
|
|
430
|
+
const eventType = normalizedEventType(input.eventType);
|
|
431
|
+
const payloadJson = serializedPayload({
|
|
432
|
+
eventId: input.eventId,
|
|
433
|
+
eventType,
|
|
434
|
+
occurredAt: this.now().toISOString(),
|
|
435
|
+
payload: input.payload,
|
|
436
|
+
});
|
|
437
|
+
if ((await outgoingSubscriptionIds(this.db, eventType, input.chatId)).length === 0)
|
|
438
|
+
return [];
|
|
439
|
+
return this.writeDb(async (tx) => {
|
|
440
|
+
const subscriptions = await outgoingSubscriptionIds(tx, eventType, input.chatId);
|
|
441
|
+
const deliveries = [];
|
|
442
|
+
for (const row of subscriptions) {
|
|
443
|
+
const subscriptionId = row.id;
|
|
444
|
+
const id = createId();
|
|
445
|
+
await tx
|
|
446
|
+
.insert(webhookDeliveries)
|
|
447
|
+
.values({
|
|
448
|
+
id,
|
|
449
|
+
subscriptionId,
|
|
450
|
+
eventId: input.eventId,
|
|
451
|
+
eventType,
|
|
452
|
+
payloadJson,
|
|
453
|
+
nextAttemptAt: this.now().toISOString(),
|
|
454
|
+
})
|
|
455
|
+
.onConflictDoNothing();
|
|
456
|
+
const [delivery] = await tx
|
|
457
|
+
.select(deliverySelection)
|
|
458
|
+
.from(webhookDeliveries)
|
|
459
|
+
.where(and(eq(webhookDeliveries.subscriptionId, subscriptionId), eq(webhookDeliveries.eventId, input.eventId)));
|
|
460
|
+
if (delivery)
|
|
461
|
+
deliveries.push(asDelivery(delivery));
|
|
462
|
+
}
|
|
463
|
+
return deliveries;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Converts durable sync rows into idempotently queued outgoing events. Calling
|
|
468
|
+
* this again after a crash is safe because each delivery uses the sync row id.
|
|
469
|
+
*/
|
|
470
|
+
async enqueueSyncSequence(sequence) {
|
|
471
|
+
if (!/^\d+$/.test(sequence))
|
|
472
|
+
throw new IntegrationError("invalid", "Sync sequence is invalid");
|
|
473
|
+
const events = await this.db
|
|
474
|
+
.select()
|
|
475
|
+
.from(syncEvents)
|
|
476
|
+
.where(eq(syncEvents.sequence, Number(sequence)))
|
|
477
|
+
.orderBy(asc(syncEvents.id));
|
|
478
|
+
const deliveries = [];
|
|
479
|
+
for (const event of events) {
|
|
480
|
+
deliveries.push(...(await this.enqueueOutgoingEvent({
|
|
481
|
+
eventId: `sync:${event.id}`,
|
|
482
|
+
eventType: event.kind,
|
|
483
|
+
chatId: event.chatId ?? undefined,
|
|
484
|
+
payload: {
|
|
485
|
+
syncEventId: String(event.id),
|
|
486
|
+
sequence: String(event.sequence),
|
|
487
|
+
chatId: event.chatId ?? undefined,
|
|
488
|
+
chatPts: event.chatPts === null ? undefined : String(event.chatPts),
|
|
489
|
+
entityId: event.entityId ?? undefined,
|
|
490
|
+
actorUserId: event.actorUserId ?? undefined,
|
|
491
|
+
targetUserId: event.targetUserId ?? undefined,
|
|
492
|
+
createdAt: event.createdAt,
|
|
493
|
+
},
|
|
494
|
+
})));
|
|
495
|
+
}
|
|
496
|
+
return deliveries;
|
|
497
|
+
}
|
|
498
|
+
/** Recovers sync rows missed by ephemeral pubsub delivery, without a local cursor. */
|
|
499
|
+
async enqueuePendingSyncEvents(limit = 100) {
|
|
500
|
+
positiveLimit(limit, 1_000);
|
|
501
|
+
const alreadyQueued = this.db
|
|
502
|
+
.select({ id: webhookDeliveries.id })
|
|
503
|
+
.from(webhookDeliveries)
|
|
504
|
+
.where(and(eq(webhookDeliveries.subscriptionId, webhookSubscriptions.id), eq(webhookDeliveries.eventId, sql `'sync:' || ${syncEvents.id}`)));
|
|
505
|
+
const eligibleSubscription = this.db
|
|
506
|
+
.select({ id: webhookSubscriptions.id })
|
|
507
|
+
.from(webhookSubscriptions)
|
|
508
|
+
.innerJoin(integrations, eq(integrations.id, webhookSubscriptions.integrationId))
|
|
509
|
+
.where(and(eq(webhookSubscriptions.direction, "outgoing"), eq(webhookSubscriptions.active, 1), eq(integrations.active, 1), isNull(integrations.deletedAt), sql `julianday(${syncEvents.createdAt}) >= julianday(${webhookSubscriptions.createdAt})`, or(isNull(webhookSubscriptions.chatId), eq(webhookSubscriptions.chatId, syncEvents.chatId)), sql `exists (select 1 from json_each(${webhookSubscriptions.eventTypesJson}) where value = ${syncEvents.kind})`, sql `exists (select 1 from json_each(${integrations.scopesJson}) where value = 'events:read')`, sql `not exists ${alreadyQueued}`));
|
|
510
|
+
const firstEventId = sql `min(${syncEvents.id})`;
|
|
511
|
+
const sequences = await this.db
|
|
512
|
+
.select({ sequence: syncEvents.sequence, firstEventId })
|
|
513
|
+
.from(syncEvents)
|
|
514
|
+
.where(sql `exists ${eligibleSubscription}`)
|
|
515
|
+
.groupBy(syncEvents.sequence)
|
|
516
|
+
.orderBy(firstEventId)
|
|
517
|
+
.limit(limit);
|
|
518
|
+
const deliveries = [];
|
|
519
|
+
for (const row of sequences)
|
|
520
|
+
deliveries.push(...(await this.enqueueSyncSequence(String(row.sequence))));
|
|
521
|
+
return deliveries;
|
|
522
|
+
}
|
|
523
|
+
async listWebhookDeliveries(actorUserId, integrationId, limit = 100) {
|
|
524
|
+
await this.requireAdminDb(this.db, actorUserId);
|
|
525
|
+
positiveLimit(limit, 200);
|
|
526
|
+
await this.requireIntegrationDb(this.db, integrationId, false);
|
|
527
|
+
const rows = await this.db
|
|
528
|
+
.select(deliverySelection)
|
|
529
|
+
.from(webhookDeliveries)
|
|
530
|
+
.innerJoin(webhookSubscriptions, eq(webhookSubscriptions.id, webhookDeliveries.subscriptionId))
|
|
531
|
+
.where(eq(webhookSubscriptions.integrationId, integrationId))
|
|
532
|
+
.orderBy(desc(webhookDeliveries.createdAt), desc(webhookDeliveries.id))
|
|
533
|
+
.limit(limit);
|
|
534
|
+
return rows.map(asDelivery);
|
|
535
|
+
}
|
|
536
|
+
async createSlashCommand(input) {
|
|
537
|
+
const command = normalizedCommand(input.command);
|
|
538
|
+
const handlerUrl = this.urlPolicy.validateForStorage(input.handlerUrl);
|
|
539
|
+
const signingSecret = generateSigningSecret();
|
|
540
|
+
const ciphertext = await this.protector.protect(signingSecret);
|
|
541
|
+
return this.writeDb(async (tx) => {
|
|
542
|
+
await this.requireAdminDb(tx, input.actorUserId);
|
|
543
|
+
if (input.botId)
|
|
544
|
+
await this.requireBotDb(tx, input.botId);
|
|
545
|
+
const integration = await this.insertIntegrationDb(tx, {
|
|
546
|
+
actorUserId: input.actorUserId,
|
|
547
|
+
kind: "slash_command",
|
|
548
|
+
name: input.name,
|
|
549
|
+
description: input.description,
|
|
550
|
+
botId: input.botId,
|
|
551
|
+
scopes: ["commands:receive"],
|
|
552
|
+
});
|
|
553
|
+
const commandId = createId();
|
|
554
|
+
try {
|
|
555
|
+
await tx.insert(slashCommands).values({
|
|
556
|
+
id: commandId,
|
|
557
|
+
integrationId: integration.id,
|
|
558
|
+
command,
|
|
559
|
+
description: optionalTrimmed(input.description, "Command description", 500) ?? null,
|
|
560
|
+
usageHint: optionalTrimmed(input.usageHint, "Usage hint", 500) ?? null,
|
|
561
|
+
handlerUrl,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
throw constraintConflict(error, "Slash command is already registered");
|
|
566
|
+
}
|
|
567
|
+
await tx.insert(webhookSubscriptions).values({
|
|
568
|
+
id: createId(),
|
|
569
|
+
integrationId: integration.id,
|
|
570
|
+
direction: "outgoing",
|
|
571
|
+
url: handlerUrl,
|
|
572
|
+
signingSecretCiphertext: ciphertext,
|
|
573
|
+
eventTypesJson: JSON.stringify([slashEventType(commandId)]),
|
|
574
|
+
});
|
|
575
|
+
const change = await this.finishIntegrationChangeDb(tx, input.actorUserId, "integration.created", integration.id);
|
|
576
|
+
return {
|
|
577
|
+
value: {
|
|
578
|
+
integration: await this.getIntegrationDb(tx, integration.id),
|
|
579
|
+
command: await this.getSlashCommandDb(tx, commandId),
|
|
580
|
+
signingSecret,
|
|
581
|
+
},
|
|
582
|
+
change,
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
async listSlashCommands(actorUserId) {
|
|
587
|
+
await this.requireActiveUserDb(this.db, actorUserId);
|
|
588
|
+
const rows = await this.db
|
|
589
|
+
.select(slashCommandSelection)
|
|
590
|
+
.from(slashCommands)
|
|
591
|
+
.innerJoin(integrations, eq(integrations.id, slashCommands.integrationId))
|
|
592
|
+
.where(and(eq(slashCommands.active, 1), eq(integrations.active, 1), isNull(integrations.deletedAt)))
|
|
593
|
+
.orderBy(asc(slashCommands.command));
|
|
594
|
+
return rows.map(asSlashCommand);
|
|
595
|
+
}
|
|
596
|
+
async invokeSlashCommand(input) {
|
|
597
|
+
const command = normalizedCommand(input.command);
|
|
598
|
+
const commandText = optionalTextBody(input.text, "Command text", 20_000) ?? "";
|
|
599
|
+
return this.writeDb(async (tx) => {
|
|
600
|
+
await this.requireChatMemberDb(tx, input.actorUserId, input.chatId);
|
|
601
|
+
const commandRow = await this.findSlashSubscriptionDb(tx, command);
|
|
602
|
+
if (!commandRow)
|
|
603
|
+
throw new IntegrationError("not_found", "Slash command was not found");
|
|
604
|
+
const eventId = `slash:${createId()}`;
|
|
605
|
+
const eventType = slashEventType(commandRow.id);
|
|
606
|
+
const payload = serializedPayload({
|
|
607
|
+
eventId,
|
|
608
|
+
eventType,
|
|
609
|
+
occurredAt: this.now().toISOString(),
|
|
610
|
+
payload: {
|
|
611
|
+
command,
|
|
612
|
+
text: commandText,
|
|
613
|
+
chatId: input.chatId,
|
|
614
|
+
actorUserId: input.actorUserId,
|
|
615
|
+
integrationId: commandRow.integrationId,
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
const deliveryId = createId();
|
|
619
|
+
await tx.insert(webhookDeliveries).values({
|
|
620
|
+
id: deliveryId,
|
|
621
|
+
subscriptionId: commandRow.subscriptionId,
|
|
622
|
+
eventId,
|
|
623
|
+
eventType,
|
|
624
|
+
payloadJson: payload,
|
|
625
|
+
nextAttemptAt: this.now().toISOString(),
|
|
626
|
+
});
|
|
627
|
+
const [delivery] = await tx
|
|
628
|
+
.select(deliverySelection)
|
|
629
|
+
.from(webhookDeliveries)
|
|
630
|
+
.where(eq(webhookDeliveries.id, deliveryId));
|
|
631
|
+
if (!delivery)
|
|
632
|
+
throw new Error("Slash command invocation was not queued");
|
|
633
|
+
return asDelivery(delivery);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
async dispatchDueWebhooks(transport, options = {}) {
|
|
637
|
+
const limit = positiveLimit(options.limit ?? 25, 100);
|
|
638
|
+
const leaseMs = positiveLimit(options.leaseMs ?? 30_000, 300_000);
|
|
639
|
+
const maxAttempts = positiveLimit(options.maxAttempts ?? MAX_DELIVERY_ATTEMPTS, 20);
|
|
640
|
+
const claimed = await this.claimDueDeliveries(limit, leaseMs, maxAttempts);
|
|
641
|
+
let delivered = 0;
|
|
642
|
+
let failed = 0;
|
|
643
|
+
for (const delivery of claimed) {
|
|
644
|
+
try {
|
|
645
|
+
const target = await this.urlPolicy.resolveForDelivery(delivery.url);
|
|
646
|
+
const secret = await this.protector.reveal(delivery.signingSecretCiphertext);
|
|
647
|
+
const timestamp = Math.floor(this.now().getTime() / 1_000).toString();
|
|
648
|
+
const signature = `v1=${createHmac("sha256", secret)
|
|
649
|
+
.update(`${timestamp}.${delivery.payloadJson}`, "utf8")
|
|
650
|
+
.digest("hex")}`;
|
|
651
|
+
const response = await transport.deliver({
|
|
652
|
+
deliveryId: delivery.id,
|
|
653
|
+
eventId: delivery.eventId,
|
|
654
|
+
eventType: delivery.eventType,
|
|
655
|
+
url: target.url,
|
|
656
|
+
allowedAddresses: target.addresses,
|
|
657
|
+
body: delivery.payloadJson,
|
|
658
|
+
headers: {
|
|
659
|
+
"content-type": "application/json",
|
|
660
|
+
"x-happy2-event-id": delivery.eventId,
|
|
661
|
+
"x-happy2-signature": signature,
|
|
662
|
+
"x-happy2-timestamp": timestamp,
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
if (response.statusCode < 200 || response.statusCode >= 300)
|
|
666
|
+
throw new DeliveryHttpError(response.statusCode, response.body);
|
|
667
|
+
await this.completeDelivery(delivery, response.statusCode, response.body);
|
|
668
|
+
delivered += 1;
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
await this.failDelivery(delivery, error, maxAttempts);
|
|
672
|
+
failed += 1;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return { delivered, failed };
|
|
676
|
+
}
|
|
677
|
+
async createIntegrationRecord(input) {
|
|
678
|
+
return this.writeDb(async (tx) => {
|
|
679
|
+
await this.requireAdminDb(tx, input.actorUserId);
|
|
680
|
+
if (input.botId)
|
|
681
|
+
await this.requireBotDb(tx, input.botId);
|
|
682
|
+
const integration = await this.insertIntegrationDb(tx, input);
|
|
683
|
+
const change = await this.finishIntegrationChangeDb(tx, input.actorUserId, "integration.created", integration.id);
|
|
684
|
+
return { value: await this.getIntegrationDb(tx, integration.id), change };
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
async claimDueDeliveries(limit, leaseMs, maxAttempts) {
|
|
688
|
+
const now = this.now();
|
|
689
|
+
const nowIso = now.toISOString();
|
|
690
|
+
const leaseUntil = new Date(now.getTime() + leaseMs).toISOString();
|
|
691
|
+
const dueCondition = and(sql `${webhookDeliveries.attempts} < ${maxAttempts}`, or(sql `${webhookDeliveries.status} in ('pending', 'failed')`, and(eq(webhookDeliveries.status, "delivering"), lte(sql `julianday(${webhookDeliveries.nextAttemptAt})`, sql `julianday(${nowIso})`))), lte(sql `julianday(${webhookDeliveries.nextAttemptAt})`, sql `julianday(${nowIso})`), eq(webhookSubscriptions.active, 1), eq(webhookSubscriptions.direction, "outgoing"), eq(integrations.active, 1), isNull(integrations.deletedAt));
|
|
692
|
+
const [candidate] = await this.db
|
|
693
|
+
.select({ id: webhookDeliveries.id })
|
|
694
|
+
.from(webhookDeliveries)
|
|
695
|
+
.innerJoin(webhookSubscriptions, eq(webhookSubscriptions.id, webhookDeliveries.subscriptionId))
|
|
696
|
+
.innerJoin(integrations, eq(integrations.id, webhookSubscriptions.integrationId))
|
|
697
|
+
.where(dueCondition)
|
|
698
|
+
.limit(1);
|
|
699
|
+
if (!candidate)
|
|
700
|
+
return [];
|
|
701
|
+
return this.writeDb(async (tx) => {
|
|
702
|
+
const due = await tx
|
|
703
|
+
.select({ id: webhookDeliveries.id })
|
|
704
|
+
.from(webhookDeliveries)
|
|
705
|
+
.innerJoin(webhookSubscriptions, eq(webhookSubscriptions.id, webhookDeliveries.subscriptionId))
|
|
706
|
+
.innerJoin(integrations, eq(integrations.id, webhookSubscriptions.integrationId))
|
|
707
|
+
.where(dueCondition)
|
|
708
|
+
.orderBy(asc(webhookDeliveries.nextAttemptAt), asc(webhookDeliveries.id))
|
|
709
|
+
.limit(limit);
|
|
710
|
+
const claimed = [];
|
|
711
|
+
for (const candidate of due) {
|
|
712
|
+
const id = candidate.id;
|
|
713
|
+
const changed = await tx
|
|
714
|
+
.update(webhookDeliveries)
|
|
715
|
+
.set({
|
|
716
|
+
status: "delivering",
|
|
717
|
+
attempts: sql `${webhookDeliveries.attempts} + 1`,
|
|
718
|
+
nextAttemptAt: leaseUntil,
|
|
719
|
+
})
|
|
720
|
+
.where(and(eq(webhookDeliveries.id, id), sql `${webhookDeliveries.attempts} < ${maxAttempts}`, or(sql `${webhookDeliveries.status} in ('pending', 'failed')`, and(eq(webhookDeliveries.status, "delivering"), lte(sql `julianday(${webhookDeliveries.nextAttemptAt})`, sql `julianday(${now.toISOString()})`)))))
|
|
721
|
+
.returning({ id: webhookDeliveries.id });
|
|
722
|
+
if (changed.length === 0)
|
|
723
|
+
continue;
|
|
724
|
+
const [row] = await tx
|
|
725
|
+
.select({
|
|
726
|
+
...deliverySelection,
|
|
727
|
+
payload_json: webhookDeliveries.payloadJson,
|
|
728
|
+
url: webhookSubscriptions.url,
|
|
729
|
+
signing_secret_ciphertext: webhookSubscriptions.signingSecretCiphertext,
|
|
730
|
+
})
|
|
731
|
+
.from(webhookDeliveries)
|
|
732
|
+
.innerJoin(webhookSubscriptions, eq(webhookSubscriptions.id, webhookDeliveries.subscriptionId))
|
|
733
|
+
.where(eq(webhookDeliveries.id, id));
|
|
734
|
+
if (row)
|
|
735
|
+
claimed.push(asClaimedDelivery(row));
|
|
736
|
+
}
|
|
737
|
+
return claimed;
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
async completeDelivery(delivery, responseStatus, responseBody) {
|
|
741
|
+
await this.db
|
|
742
|
+
.update(webhookDeliveries)
|
|
743
|
+
.set({
|
|
744
|
+
status: "delivered",
|
|
745
|
+
responseStatus,
|
|
746
|
+
responseBody: truncate(responseBody, MAX_DELIVERY_RESPONSE) ?? null,
|
|
747
|
+
lastError: null,
|
|
748
|
+
deliveredAt: sql `CURRENT_TIMESTAMP`,
|
|
749
|
+
})
|
|
750
|
+
.where(and(eq(webhookDeliveries.id, delivery.id), eq(webhookDeliveries.status, "delivering"), eq(webhookDeliveries.nextAttemptAt, delivery.nextAttemptAt)));
|
|
751
|
+
}
|
|
752
|
+
async failDelivery(delivery, error, maxAttempts) {
|
|
753
|
+
const response = error instanceof DeliveryHttpError ? error : undefined;
|
|
754
|
+
const exhausted = delivery.attempts >= maxAttempts;
|
|
755
|
+
const nextAttemptAt = exhausted
|
|
756
|
+
? this.now().toISOString()
|
|
757
|
+
: new Date(this.now().getTime() + retryDelay(delivery.id, delivery.attempts)).toISOString();
|
|
758
|
+
await this.db
|
|
759
|
+
.update(webhookDeliveries)
|
|
760
|
+
.set({
|
|
761
|
+
status: "failed",
|
|
762
|
+
nextAttemptAt,
|
|
763
|
+
responseStatus: response?.statusCode ?? null,
|
|
764
|
+
responseBody: truncate(response?.responseBody, MAX_DELIVERY_RESPONSE) ?? null,
|
|
765
|
+
lastError: truncate(errorMessage(error), 2_000),
|
|
766
|
+
})
|
|
767
|
+
.where(and(eq(webhookDeliveries.id, delivery.id), eq(webhookDeliveries.status, "delivering"), eq(webhookDeliveries.nextAttemptAt, delivery.nextAttemptAt)));
|
|
768
|
+
}
|
|
769
|
+
async requireAdminDb(executor, userId) {
|
|
770
|
+
const [row] = await executor
|
|
771
|
+
.select({ id: users.id })
|
|
772
|
+
.from(users)
|
|
773
|
+
.innerJoin(accounts, eq(accounts.id, users.accountId))
|
|
774
|
+
.where(and(eq(users.id, userId), eq(users.role, "admin"), isNull(users.deletedAt), eq(accounts.active, 1), isNull(accounts.bannedAt), isNull(accounts.deletedAt)));
|
|
775
|
+
if (!row)
|
|
776
|
+
throw new IntegrationError("forbidden", "Server admin permission is required");
|
|
777
|
+
}
|
|
778
|
+
async requireActiveUserDb(executor, userId) {
|
|
779
|
+
const [row] = await executor
|
|
780
|
+
.select({ id: users.id })
|
|
781
|
+
.from(users)
|
|
782
|
+
.innerJoin(accounts, eq(accounts.id, users.accountId))
|
|
783
|
+
.where(and(eq(users.id, userId), isNull(users.deletedAt), eq(accounts.active, 1), isNull(accounts.bannedAt), isNull(accounts.deletedAt)));
|
|
784
|
+
if (!row)
|
|
785
|
+
throw new IntegrationError("not_found", "User was not found");
|
|
786
|
+
}
|
|
787
|
+
async requireChatMemberDb(executor, userId, chatId) {
|
|
788
|
+
const [row] = await executor
|
|
789
|
+
.select({ id: chatMembers.chatId })
|
|
790
|
+
.from(chatMembers)
|
|
791
|
+
.innerJoin(chats, eq(chats.id, chatMembers.chatId))
|
|
792
|
+
.innerJoin(users, eq(users.id, chatMembers.userId))
|
|
793
|
+
.innerJoin(accounts, eq(accounts.id, users.accountId))
|
|
794
|
+
.where(and(eq(chatMembers.chatId, chatId), eq(chatMembers.userId, userId), isNull(chatMembers.leftAt), isNull(chats.deletedAt), isNull(chats.archivedAt), isNull(users.deletedAt), eq(accounts.active, 1), isNull(accounts.bannedAt), isNull(accounts.deletedAt)));
|
|
795
|
+
if (!row)
|
|
796
|
+
throw new IntegrationError("not_found", "Chat was not found");
|
|
797
|
+
}
|
|
798
|
+
async requireChatDb(executor, chatId) {
|
|
799
|
+
const [row] = await executor
|
|
800
|
+
.select({ id: chats.id })
|
|
801
|
+
.from(chats)
|
|
802
|
+
.where(and(eq(chats.id, chatId), isNull(chats.deletedAt)));
|
|
803
|
+
if (!row)
|
|
804
|
+
throw new IntegrationError("not_found", "Chat was not found");
|
|
805
|
+
}
|
|
806
|
+
async requireFileDb(executor, fileId) {
|
|
807
|
+
const [row] = await executor
|
|
808
|
+
.select({ id: files.id })
|
|
809
|
+
.from(files)
|
|
810
|
+
.where(and(eq(files.id, fileId), isNull(files.deletedAt), eq(files.uploadStatus, "complete"), sql `${files.scanStatus} != 'infected'`));
|
|
811
|
+
if (!row)
|
|
812
|
+
throw new IntegrationError("not_found", "File was not found");
|
|
813
|
+
}
|
|
814
|
+
async requireBotDb(executor, botId) {
|
|
815
|
+
await this.getBotDb(executor, botId, true);
|
|
816
|
+
}
|
|
817
|
+
async requireIntegrationDb(executor, integrationId, active) {
|
|
818
|
+
const row = await this.getIntegrationDb(executor, integrationId);
|
|
819
|
+
if (active && !row.active)
|
|
820
|
+
throw new IntegrationError("not_found", "Integration was not found");
|
|
821
|
+
return row;
|
|
822
|
+
}
|
|
823
|
+
async getBotDb(executor, botId, active = false) {
|
|
824
|
+
const [row] = await executor
|
|
825
|
+
.select(botSelection)
|
|
826
|
+
.from(botIdentities)
|
|
827
|
+
.where(eq(botIdentities.id, botId));
|
|
828
|
+
if (!row || (active && (row.active !== 1 || row.deleted_at !== null)))
|
|
829
|
+
throw new IntegrationError("not_found", "Bot was not found");
|
|
830
|
+
return asBot(row);
|
|
831
|
+
}
|
|
832
|
+
async getIntegrationDb(executor, integrationId) {
|
|
833
|
+
const [row] = await executor
|
|
834
|
+
.select(integrationSelection)
|
|
835
|
+
.from(integrations)
|
|
836
|
+
.where(eq(integrations.id, integrationId));
|
|
837
|
+
if (!row)
|
|
838
|
+
throw new IntegrationError("not_found", "Integration was not found");
|
|
839
|
+
return asIntegration(row);
|
|
840
|
+
}
|
|
841
|
+
async getSubscriptionDb(executor, subscriptionId) {
|
|
842
|
+
const [row] = await executor
|
|
843
|
+
.select(subscriptionSelection)
|
|
844
|
+
.from(webhookSubscriptions)
|
|
845
|
+
.where(eq(webhookSubscriptions.id, subscriptionId));
|
|
846
|
+
if (!row)
|
|
847
|
+
throw new Error("Webhook subscription was not created");
|
|
848
|
+
return asSubscription(row);
|
|
849
|
+
}
|
|
850
|
+
async getSlashCommandDb(executor, commandId) {
|
|
851
|
+
const [row] = await executor
|
|
852
|
+
.select(slashCommandSelection)
|
|
853
|
+
.from(slashCommands)
|
|
854
|
+
.where(eq(slashCommands.id, commandId));
|
|
855
|
+
if (!row)
|
|
856
|
+
throw new Error("Slash command was not created");
|
|
857
|
+
return asSlashCommand(row);
|
|
858
|
+
}
|
|
859
|
+
async findSlashSubscriptionDb(executor, command) {
|
|
860
|
+
const [row] = await executor
|
|
861
|
+
.select({
|
|
862
|
+
id: slashCommands.id,
|
|
863
|
+
integrationId: slashCommands.integrationId,
|
|
864
|
+
subscriptionId: webhookSubscriptions.id,
|
|
865
|
+
})
|
|
866
|
+
.from(slashCommands)
|
|
867
|
+
.innerJoin(integrations, eq(integrations.id, slashCommands.integrationId))
|
|
868
|
+
.innerJoin(webhookSubscriptions, eq(webhookSubscriptions.integrationId, integrations.id))
|
|
869
|
+
.where(and(sql `${slashCommands.command} = ${command} collate nocase`, eq(slashCommands.active, 1), eq(integrations.active, 1), isNull(integrations.deletedAt), sql `exists (select 1 from json_each(${integrations.scopesJson}) where value = 'commands:receive')`, eq(webhookSubscriptions.direction, "outgoing"), eq(webhookSubscriptions.active, 1), sql `exists (select 1 from json_each(${webhookSubscriptions.eventTypesJson}) where value = 'slash_command:' || ${slashCommands.id})`))
|
|
870
|
+
.limit(1);
|
|
871
|
+
return row;
|
|
872
|
+
}
|
|
873
|
+
async insertIntegrationDb(tx, input) {
|
|
874
|
+
if (!integrationKinds.includes(input.kind))
|
|
875
|
+
throw new IntegrationError("invalid", "Integration kind is invalid");
|
|
876
|
+
const id = createId();
|
|
877
|
+
await tx.insert(integrations).values({
|
|
878
|
+
id,
|
|
879
|
+
kind: input.kind,
|
|
880
|
+
name: requiredTrimmed(input.name, "Integration name", 200),
|
|
881
|
+
description: optionalTrimmed(input.description, "Integration description", 2_000) ?? null,
|
|
882
|
+
botId: input.botId ?? null,
|
|
883
|
+
createdByUserId: input.actorUserId,
|
|
884
|
+
scopesJson: JSON.stringify(normalizeScopes(input.scopes)),
|
|
885
|
+
});
|
|
886
|
+
return { id };
|
|
887
|
+
}
|
|
888
|
+
async finishIntegrationChangeDb(tx, actorUserId, kind, integrationId) {
|
|
889
|
+
const change = await this.recordChangeDb(tx, actorUserId, kind, integrationId);
|
|
890
|
+
await this.appendAuditDb(tx, actorUserId, kind, "integration", integrationId);
|
|
891
|
+
await tx
|
|
892
|
+
.update(integrations)
|
|
893
|
+
.set({ syncSequence: Number(change.sequence) })
|
|
894
|
+
.where(eq(integrations.id, integrationId));
|
|
895
|
+
return change;
|
|
896
|
+
}
|
|
897
|
+
async recordChangeDb(tx, actorUserId, kind, entityId) {
|
|
898
|
+
const [state] = await tx
|
|
899
|
+
.update(serverSyncState)
|
|
900
|
+
.set({ sequence: sql `${serverSyncState.sequence} + 1` })
|
|
901
|
+
.where(eq(serverSyncState.id, 1))
|
|
902
|
+
.returning({ sequence: serverSyncState.sequence });
|
|
903
|
+
if (!state)
|
|
904
|
+
throw new Error("Server sync state is not initialized");
|
|
905
|
+
await tx.insert(syncEvents).values({
|
|
906
|
+
sequence: state.sequence,
|
|
907
|
+
kind,
|
|
908
|
+
entityId,
|
|
909
|
+
actorUserId,
|
|
910
|
+
});
|
|
911
|
+
return { sequence: String(state.sequence), kind, entityId };
|
|
912
|
+
}
|
|
913
|
+
async appendAuditDb(tx, actorUserId, action, targetType, targetId, metadata) {
|
|
914
|
+
await tx.insert(auditLogEntries).values({
|
|
915
|
+
id: createId(),
|
|
916
|
+
actorUserId,
|
|
917
|
+
action,
|
|
918
|
+
targetType,
|
|
919
|
+
targetId,
|
|
920
|
+
metadataJson: metadata ? JSON.stringify(metadata) : null,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
writeDb(operation) {
|
|
924
|
+
return retrySqliteBusy(() => this.db.transaction(operation));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function outgoingSubscriptionIds(executor, eventType, chatId) {
|
|
928
|
+
return executor
|
|
929
|
+
.select({ id: webhookSubscriptions.id })
|
|
930
|
+
.from(webhookSubscriptions)
|
|
931
|
+
.innerJoin(integrations, eq(integrations.id, webhookSubscriptions.integrationId))
|
|
932
|
+
.where(and(eq(webhookSubscriptions.direction, "outgoing"), eq(webhookSubscriptions.active, 1), eq(integrations.active, 1), isNull(integrations.deletedAt), or(isNull(webhookSubscriptions.chatId), eq(webhookSubscriptions.chatId, chatId ?? "")), sql `exists (select 1 from json_each(${webhookSubscriptions.eventTypesJson}) where value = ${eventType})`, sql `exists (select 1 from json_each(${integrations.scopesJson}) where value = 'events:read')`))
|
|
933
|
+
.orderBy(asc(webhookSubscriptions.id));
|
|
934
|
+
}
|
|
935
|
+
const botSelection = {
|
|
936
|
+
id: botIdentities.id,
|
|
937
|
+
name: botIdentities.name,
|
|
938
|
+
username: botIdentities.username,
|
|
939
|
+
description: botIdentities.description,
|
|
940
|
+
photo_file_id: botIdentities.photoFileId,
|
|
941
|
+
owner_user_id: botIdentities.ownerUserId,
|
|
942
|
+
active: botIdentities.active,
|
|
943
|
+
created_at: botIdentities.createdAt,
|
|
944
|
+
updated_at: botIdentities.updatedAt,
|
|
945
|
+
deleted_at: botIdentities.deletedAt,
|
|
946
|
+
};
|
|
947
|
+
const integrationSelection = {
|
|
948
|
+
id: integrations.id,
|
|
949
|
+
kind: integrations.kind,
|
|
950
|
+
name: integrations.name,
|
|
951
|
+
description: integrations.description,
|
|
952
|
+
bot_id: integrations.botId,
|
|
953
|
+
scopes_json: integrations.scopesJson,
|
|
954
|
+
active: integrations.active,
|
|
955
|
+
created_at: integrations.createdAt,
|
|
956
|
+
updated_at: integrations.updatedAt,
|
|
957
|
+
};
|
|
958
|
+
const credentialSelection = {
|
|
959
|
+
id: apiCredentials.id,
|
|
960
|
+
integration_id: apiCredentials.integrationId,
|
|
961
|
+
name: apiCredentials.name,
|
|
962
|
+
token_prefix: apiCredentials.tokenPrefix,
|
|
963
|
+
scopes_json: apiCredentials.scopesJson,
|
|
964
|
+
expires_at: apiCredentials.expiresAt,
|
|
965
|
+
last_used_at: apiCredentials.lastUsedAt,
|
|
966
|
+
revoked_at: apiCredentials.revokedAt,
|
|
967
|
+
created_at: apiCredentials.createdAt,
|
|
968
|
+
};
|
|
969
|
+
const subscriptionSelection = {
|
|
970
|
+
id: webhookSubscriptions.id,
|
|
971
|
+
integration_id: webhookSubscriptions.integrationId,
|
|
972
|
+
direction: webhookSubscriptions.direction,
|
|
973
|
+
chat_id: webhookSubscriptions.chatId,
|
|
974
|
+
url: webhookSubscriptions.url,
|
|
975
|
+
event_types_json: webhookSubscriptions.eventTypesJson,
|
|
976
|
+
active: webhookSubscriptions.active,
|
|
977
|
+
created_at: webhookSubscriptions.createdAt,
|
|
978
|
+
updated_at: webhookSubscriptions.updatedAt,
|
|
979
|
+
};
|
|
980
|
+
const slashCommandSelection = {
|
|
981
|
+
id: slashCommands.id,
|
|
982
|
+
integration_id: slashCommands.integrationId,
|
|
983
|
+
command: slashCommands.command,
|
|
984
|
+
description: slashCommands.description,
|
|
985
|
+
usage_hint: slashCommands.usageHint,
|
|
986
|
+
active: slashCommands.active,
|
|
987
|
+
created_at: slashCommands.createdAt,
|
|
988
|
+
updated_at: slashCommands.updatedAt,
|
|
989
|
+
};
|
|
990
|
+
const deliverySelection = {
|
|
991
|
+
id: webhookDeliveries.id,
|
|
992
|
+
subscription_id: webhookDeliveries.subscriptionId,
|
|
993
|
+
event_id: webhookDeliveries.eventId,
|
|
994
|
+
event_type: webhookDeliveries.eventType,
|
|
995
|
+
status: webhookDeliveries.status,
|
|
996
|
+
attempts: webhookDeliveries.attempts,
|
|
997
|
+
next_attempt_at: webhookDeliveries.nextAttemptAt,
|
|
998
|
+
created_at: webhookDeliveries.createdAt,
|
|
999
|
+
};
|
|
1000
|
+
function asBot(row) {
|
|
1001
|
+
return {
|
|
1002
|
+
id: text(row.id),
|
|
1003
|
+
name: text(row.name),
|
|
1004
|
+
username: text(row.username),
|
|
1005
|
+
description: optionalText(row.description),
|
|
1006
|
+
photoFileId: optionalText(row.photo_file_id),
|
|
1007
|
+
ownerUserId: optionalText(row.owner_user_id),
|
|
1008
|
+
active: number(row.active) === 1,
|
|
1009
|
+
createdAt: text(row.created_at),
|
|
1010
|
+
updatedAt: text(row.updated_at),
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function asIntegration(row) {
|
|
1014
|
+
return {
|
|
1015
|
+
id: text(row.id),
|
|
1016
|
+
kind: text(row.kind),
|
|
1017
|
+
name: text(row.name),
|
|
1018
|
+
description: optionalText(row.description),
|
|
1019
|
+
botId: optionalText(row.bot_id),
|
|
1020
|
+
scopes: parseScopes(row.scopes_json),
|
|
1021
|
+
active: number(row.active) === 1,
|
|
1022
|
+
createdAt: text(row.created_at),
|
|
1023
|
+
updatedAt: text(row.updated_at),
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function asCredential(row) {
|
|
1027
|
+
return {
|
|
1028
|
+
id: text(row.id),
|
|
1029
|
+
integrationId: text(row.integration_id),
|
|
1030
|
+
name: text(row.name),
|
|
1031
|
+
tokenPrefix: text(row.token_prefix),
|
|
1032
|
+
scopes: parseScopes(row.scopes_json),
|
|
1033
|
+
expiresAt: optionalText(row.expires_at),
|
|
1034
|
+
lastUsedAt: optionalText(row.last_used_at),
|
|
1035
|
+
revokedAt: optionalText(row.revoked_at),
|
|
1036
|
+
createdAt: text(row.created_at),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function asSubscription(row) {
|
|
1040
|
+
return {
|
|
1041
|
+
id: text(row.id),
|
|
1042
|
+
integrationId: text(row.integration_id),
|
|
1043
|
+
direction: text(row.direction),
|
|
1044
|
+
chatId: optionalText(row.chat_id),
|
|
1045
|
+
url: optionalText(row.url),
|
|
1046
|
+
eventTypes: stringArray(row.event_types_json),
|
|
1047
|
+
active: number(row.active) === 1,
|
|
1048
|
+
createdAt: text(row.created_at),
|
|
1049
|
+
updatedAt: text(row.updated_at),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function asSlashCommand(row) {
|
|
1053
|
+
return {
|
|
1054
|
+
id: text(row.id),
|
|
1055
|
+
integrationId: text(row.integration_id),
|
|
1056
|
+
command: text(row.command),
|
|
1057
|
+
description: optionalText(row.description),
|
|
1058
|
+
usageHint: optionalText(row.usage_hint),
|
|
1059
|
+
active: number(row.active) === 1,
|
|
1060
|
+
createdAt: text(row.created_at),
|
|
1061
|
+
updatedAt: text(row.updated_at),
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function asDelivery(row) {
|
|
1065
|
+
return {
|
|
1066
|
+
id: text(row.id),
|
|
1067
|
+
subscriptionId: text(row.subscription_id),
|
|
1068
|
+
eventId: text(row.event_id),
|
|
1069
|
+
eventType: text(row.event_type),
|
|
1070
|
+
status: text(row.status),
|
|
1071
|
+
attempts: number(row.attempts),
|
|
1072
|
+
nextAttemptAt: text(row.next_attempt_at),
|
|
1073
|
+
createdAt: text(row.created_at),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function asClaimedDelivery(row) {
|
|
1077
|
+
return {
|
|
1078
|
+
...asDelivery(row),
|
|
1079
|
+
url: text(row.url),
|
|
1080
|
+
signingSecretCiphertext: text(row.signing_secret_ciphertext),
|
|
1081
|
+
payloadJson: text(row.payload_json),
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function normalizeScopes(values) {
|
|
1085
|
+
const unique = [...new Set(values)];
|
|
1086
|
+
if (unique.some((scope) => !integrationScopes.includes(scope)))
|
|
1087
|
+
throw new IntegrationError("invalid", "Integration scope is invalid");
|
|
1088
|
+
return unique.sort();
|
|
1089
|
+
}
|
|
1090
|
+
function parseScopes(value) {
|
|
1091
|
+
const parsed = stringArray(value);
|
|
1092
|
+
if (parsed.some((scope) => !integrationScopes.includes(scope)))
|
|
1093
|
+
throw new Error("Database contains an invalid integration scope");
|
|
1094
|
+
return parsed;
|
|
1095
|
+
}
|
|
1096
|
+
function requireScopeSubset(requested, allowed) {
|
|
1097
|
+
if (requested.some((scope) => !allowed.includes(scope)))
|
|
1098
|
+
throw new IntegrationError("forbidden", "Credential scope exceeds its integration");
|
|
1099
|
+
}
|
|
1100
|
+
function normalizeEventTypes(values) {
|
|
1101
|
+
if (values.length === 0 || values.length > 50)
|
|
1102
|
+
throw new IntegrationError("invalid", "Outgoing webhook requires 1-50 event types");
|
|
1103
|
+
return [...new Set(values.map(normalizedEventType))].sort();
|
|
1104
|
+
}
|
|
1105
|
+
function normalizedEventType(value) {
|
|
1106
|
+
const normalized = value.trim();
|
|
1107
|
+
if (!/^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/.test(normalized))
|
|
1108
|
+
throw new IntegrationError("invalid", "Webhook event type is invalid");
|
|
1109
|
+
return normalized;
|
|
1110
|
+
}
|
|
1111
|
+
function normalizedCommand(value) {
|
|
1112
|
+
const normalized = value.trim().toLowerCase();
|
|
1113
|
+
if (!/^\/[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized))
|
|
1114
|
+
throw new IntegrationError("invalid", "Slash command is invalid");
|
|
1115
|
+
return normalized;
|
|
1116
|
+
}
|
|
1117
|
+
function slashEventType(commandId) {
|
|
1118
|
+
return `slash_command:${commandId}`;
|
|
1119
|
+
}
|
|
1120
|
+
function normalizedUsername(value) {
|
|
1121
|
+
const normalized = value.trim().toLowerCase();
|
|
1122
|
+
if (!/^[a-z0-9][a-z0-9_.-]{1,31}$/.test(normalized))
|
|
1123
|
+
throw new IntegrationError("invalid", "Bot username must contain 2-32 safe characters");
|
|
1124
|
+
return normalized;
|
|
1125
|
+
}
|
|
1126
|
+
function requiredTrimmed(value, name, maximum) {
|
|
1127
|
+
const normalized = value.trim();
|
|
1128
|
+
if (!normalized || normalized.length > maximum)
|
|
1129
|
+
throw new IntegrationError("invalid", `${name} must contain 1-${maximum} characters`);
|
|
1130
|
+
return normalized;
|
|
1131
|
+
}
|
|
1132
|
+
function optionalTrimmed(value, name, maximum) {
|
|
1133
|
+
if (value === undefined)
|
|
1134
|
+
return undefined;
|
|
1135
|
+
const normalized = value.trim();
|
|
1136
|
+
if (!normalized || normalized.length > maximum)
|
|
1137
|
+
throw new IntegrationError("invalid", `${name} must contain 1-${maximum} characters`);
|
|
1138
|
+
return normalized;
|
|
1139
|
+
}
|
|
1140
|
+
function requiredText(value, name, maximum) {
|
|
1141
|
+
if (!value.trim() || value.length > maximum)
|
|
1142
|
+
throw new IntegrationError("invalid", `${name} must contain 1-${maximum} characters`);
|
|
1143
|
+
return value;
|
|
1144
|
+
}
|
|
1145
|
+
function optionalTextBody(value, name, maximum) {
|
|
1146
|
+
if (value === undefined)
|
|
1147
|
+
return undefined;
|
|
1148
|
+
if (value.length > maximum)
|
|
1149
|
+
throw new IntegrationError("invalid", `${name} must contain at most ${maximum} characters`);
|
|
1150
|
+
return value;
|
|
1151
|
+
}
|
|
1152
|
+
function futureDate(value, now) {
|
|
1153
|
+
const timestamp = Date.parse(value);
|
|
1154
|
+
if (!Number.isFinite(timestamp) || timestamp <= now.getTime())
|
|
1155
|
+
throw new IntegrationError("invalid", "Credential expiry must be in the future");
|
|
1156
|
+
return new Date(timestamp).toISOString();
|
|
1157
|
+
}
|
|
1158
|
+
function serializedPayload(value) {
|
|
1159
|
+
let serialized;
|
|
1160
|
+
try {
|
|
1161
|
+
serialized = JSON.stringify(value);
|
|
1162
|
+
}
|
|
1163
|
+
catch {
|
|
1164
|
+
throw new IntegrationError("invalid", "Webhook payload must be JSON serializable");
|
|
1165
|
+
}
|
|
1166
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_EVENT_PAYLOAD)
|
|
1167
|
+
throw new IntegrationError("invalid", "Webhook payload is too large");
|
|
1168
|
+
return serialized;
|
|
1169
|
+
}
|
|
1170
|
+
function boundedIdentifier(value, name) {
|
|
1171
|
+
if (!value || value.length > 256 || value.trim() !== value)
|
|
1172
|
+
throw new IntegrationError("invalid", `${name} is invalid`);
|
|
1173
|
+
}
|
|
1174
|
+
function positiveLimit(value, maximum) {
|
|
1175
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum)
|
|
1176
|
+
throw new IntegrationError("invalid", `Value must be between 1 and ${maximum}`);
|
|
1177
|
+
return value;
|
|
1178
|
+
}
|
|
1179
|
+
function retryDelay(deliveryId, attempts) {
|
|
1180
|
+
const base = Math.min(60 * 60_000, 1_000 * 2 ** Math.max(0, attempts - 1));
|
|
1181
|
+
const digest = createHash("sha256").update(`${deliveryId}:${attempts}`).digest();
|
|
1182
|
+
const jitter = 0.8 + (digest.readUInt16BE(0) / 65_535) * 0.4;
|
|
1183
|
+
return Math.round(base * jitter);
|
|
1184
|
+
}
|
|
1185
|
+
function constraintConflict(error, message) {
|
|
1186
|
+
const code = error.code;
|
|
1187
|
+
return code?.includes("CONSTRAINT") ? new IntegrationError("conflict", message) : error;
|
|
1188
|
+
}
|
|
1189
|
+
function truncate(value, maximum) {
|
|
1190
|
+
return value === undefined ? undefined : value.slice(0, maximum);
|
|
1191
|
+
}
|
|
1192
|
+
function errorMessage(error) {
|
|
1193
|
+
return error instanceof Error ? error.message : String(error);
|
|
1194
|
+
}
|
|
1195
|
+
function stringArray(value) {
|
|
1196
|
+
const parsed = JSON.parse(text(value));
|
|
1197
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string"))
|
|
1198
|
+
throw new Error("Expected JSON string array");
|
|
1199
|
+
return parsed;
|
|
1200
|
+
}
|
|
1201
|
+
function text(value) {
|
|
1202
|
+
if (typeof value === "string")
|
|
1203
|
+
return value;
|
|
1204
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
1205
|
+
return String(value);
|
|
1206
|
+
throw new Error("Expected database text value");
|
|
1207
|
+
}
|
|
1208
|
+
function optionalText(value) {
|
|
1209
|
+
return value === null || value === undefined ? undefined : text(value);
|
|
1210
|
+
}
|
|
1211
|
+
function number(value) {
|
|
1212
|
+
if (typeof value === "number")
|
|
1213
|
+
return value;
|
|
1214
|
+
if (typeof value === "bigint")
|
|
1215
|
+
return Number(value);
|
|
1216
|
+
if (typeof value === "string" && /^-?\d+$/.test(value))
|
|
1217
|
+
return Number(value);
|
|
1218
|
+
throw new Error("Expected database integer value");
|
|
1219
|
+
}
|
|
1220
|
+
class DeliveryHttpError extends Error {
|
|
1221
|
+
statusCode;
|
|
1222
|
+
responseBody;
|
|
1223
|
+
constructor(statusCode, responseBody) {
|
|
1224
|
+
super(`Webhook returned HTTP ${statusCode}`);
|
|
1225
|
+
this.statusCode = statusCode;
|
|
1226
|
+
this.responseBody = responseBody;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
//# sourceMappingURL=repository.js.map
|