@app-vitals/agent-admin 0.0.0-stage → 0.1.0
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/Dockerfile +43 -0
- package/README.md +72 -2
- package/e2e/agents-page.e2e.ts +338 -0
- package/e2e/chat-degraded.e2e.ts +153 -0
- package/e2e/chat-page.e2e.ts +169 -0
- package/e2e/chat-progress.e2e.ts +206 -0
- package/e2e/chat-thread-page.e2e.ts +444 -0
- package/e2e/login-page.e2e.ts +148 -0
- package/e2e/test-server.ts +427 -0
- package/openapi.json +6098 -0
- package/openapi.platform.json +5553 -0
- package/package.json +64 -4
- package/playwright.config.ts +25 -0
- package/prisma/client/browser.ts +134 -0
- package/prisma/client/client.ts +158 -0
- package/prisma/client/commonInputTypes.ts +578 -0
- package/prisma/client/enums.ts +15 -0
- package/prisma/client/internal/class.ts +424 -0
- package/prisma/client/internal/prismaNamespace.ts +2898 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
- package/prisma/client/models/Account.ts +1774 -0
- package/prisma/client/models/AccountInvite.ts +1386 -0
- package/prisma/client/models/AccountMember.ts +1351 -0
- package/prisma/client/models/Agent.ts +4554 -0
- package/prisma/client/models/AgentAsk.ts +1373 -0
- package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
- package/prisma/client/models/AgentCronJob.ts +2416 -0
- package/prisma/client/models/AgentCronRun.ts +2858 -0
- package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
- package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
- package/prisma/client/models/AgentEnv.ts +1421 -0
- package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
- package/prisma/client/models/AgentMember.ts +1316 -0
- package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
- package/prisma/client/models/AgentPlugin.ts +1421 -0
- package/prisma/client/models/AgentToken.ts +1380 -0
- package/prisma/client/models/AgentTool.ts +1351 -0
- package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
- package/prisma/client/models/ChatThreadWatch.ts +1183 -0
- package/prisma/client/models/PushSubscription.ts +1233 -0
- package/prisma/client/models/SessionAlertState.ts +1187 -0
- package/prisma/client/models/SessionFollow.ts +1187 -0
- package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
- package/prisma/client/models.ts +34 -0
- package/prisma/migrations/20260608000000_init/migration.sql +112 -0
- package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
- package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
- package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
- package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
- package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
- package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
- package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
- package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
- package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
- package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
- package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
- package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
- package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
- package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
- package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
- package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
- package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
- package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
- package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
- package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
- package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
- package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
- package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
- package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
- package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
- package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
- package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
- package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
- package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
- package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
- package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
- package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
- package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
- package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
- package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
- package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
- package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
- package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
- package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema/coding.prisma +85 -0
- package/prisma/schema/schema.prisma +506 -0
- package/prisma.config.ts +34 -0
- package/pwa-assets/icons/apple-touch-icon.png +0 -0
- package/pwa-assets/icons/favicon-32.png +0 -0
- package/pwa-assets/icons/icon-192.png +0 -0
- package/pwa-assets/icons/icon-512.png +0 -0
- package/pwa-assets/icons/icon-maskable-192.png +0 -0
- package/pwa-assets/icons/icon-maskable-512.png +0 -0
- package/src/account-agent-create.ts +42 -0
- package/src/account-agent-create.unit.test.ts +28 -0
- package/src/account-created-notifier.ts +71 -0
- package/src/account-created-notifier.unit.test.ts +70 -0
- package/src/account-email.ts +9 -0
- package/src/account-email.unit.test.ts +21 -0
- package/src/account-invites.ts +70 -0
- package/src/account-lifecycle.integration.test.ts +329 -0
- package/src/account-lifecycle.ts +206 -0
- package/src/account-lifecycle.unit.test.ts +108 -0
- package/src/account-members.ts +128 -0
- package/src/account-onboarding.integration.test.ts +101 -0
- package/src/account-onboarding.ts +95 -0
- package/src/account-trial-expiry-sweeper.ts +85 -0
- package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
- package/src/account-trial-warning-sweeper.ts +216 -0
- package/src/accounts-api.integration.test.ts +196 -0
- package/src/accounts-api.smoke.test.ts +292 -0
- package/src/accounts-api.ts +289 -0
- package/src/accounts-api.unit.test.ts +93 -0
- package/src/accounts.integration.test.ts +178 -0
- package/src/accounts.ts +208 -0
- package/src/admin-boot-config.ts +574 -0
- package/src/admin-boot-config.unit.test.ts +28 -0
- package/src/admin-boot.boot-order.unit.test.ts +91 -0
- package/src/admin-boot.ts +690 -0
- package/src/admin-dev-login.smoke.test.ts +317 -0
- package/src/admin-extension.ts +432 -0
- package/src/admin-extension.unit.test.ts +532 -0
- package/src/admin-local-agent.smoke.test.ts +1643 -0
- package/src/admin-spec.smoke.test.ts +96 -0
- package/src/admin-spec.ts +186 -0
- package/src/admin-ui-account-nav.ts +20 -0
- package/src/admin-ui-account-pages.ts +131 -0
- package/src/admin-ui-account-pages.unit.test.ts +112 -0
- package/src/admin-ui-account.smoke.test.ts +286 -0
- package/src/admin-ui-account.ts +185 -0
- package/src/admin-ui-accounts-pages.ts +202 -0
- package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
- package/src/admin-ui-accounts.smoke.test.ts +434 -0
- package/src/admin-ui-accounts.ts +266 -0
- package/src/admin-ui-layout.ts +60 -0
- package/src/admin-ui-layout.unit.test.ts +140 -0
- package/src/admin-ui-pages.ts +3489 -0
- package/src/admin-ui-pages.unit.test.ts +4355 -0
- package/src/admin-ui-push-test.smoke.test.ts +125 -0
- package/src/admin-ui-push-test.ts +102 -0
- package/src/admin-ui-styles.ts +656 -0
- package/src/admin-ui-styles.unit.test.ts +103 -0
- package/src/admin-ui.fixtures.ts +457 -0
- package/src/admin-ui.smoke.test.ts +7227 -0
- package/src/admin-ui.ts +3824 -0
- package/src/agent-account-assignment.ts +49 -0
- package/src/agent-account-assignment.unit.test.ts +99 -0
- package/src/agent-anthropic-identity.integration.test.ts +99 -0
- package/src/agent-anthropic-identity.ts +78 -0
- package/src/agent-anthropic-identity.unit.test.ts +77 -0
- package/src/agent-ask-config.ts +59 -0
- package/src/agent-ask-config.unit.test.ts +70 -0
- package/src/agent-ask.integration.test.ts +175 -0
- package/src/agent-ask.smoke.test.ts +457 -0
- package/src/agent-ask.ts +378 -0
- package/src/agent-chat-tokens.integration.test.ts +420 -0
- package/src/agent-chat-tokens.smoke.test.ts +551 -0
- package/src/agent-chat-tokens.ts +259 -0
- package/src/agent-cron-jobs.integration.test.ts +582 -0
- package/src/agent-cron-jobs.ts +615 -0
- package/src/agent-cron-jobs.unit.test.ts +420 -0
- package/src/agent-cron-run-stats.integration.test.ts +1551 -0
- package/src/agent-cron-run-stats.smoke.test.ts +953 -0
- package/src/agent-cron-run-stats.ts +752 -0
- package/src/agent-cron-runs.integration.test.ts +1491 -0
- package/src/agent-cron-runs.ts +489 -0
- package/src/agent-deletion-checklist.ts +128 -0
- package/src/agent-deletion-checklist.unit.test.ts +201 -0
- package/src/agent-deletion-per-agent.integration.test.ts +323 -0
- package/src/agent-deletion.integration.test.ts +323 -0
- package/src/agent-deletion.ts +329 -0
- package/src/agent-deletion.unit.test.ts +412 -0
- package/src/agent-envs.integration.test.ts +323 -0
- package/src/agent-envs.ts +244 -0
- package/src/agent-github-installations.integration.test.ts +115 -0
- package/src/agent-github-installations.ts +52 -0
- package/src/agent-manifest.ts +685 -0
- package/src/agent-manifest.unit.test.ts +870 -0
- package/src/agent-members.ts +69 -0
- package/src/agent-members.unit.test.ts +250 -0
- package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
- package/src/agent-plugins.integration.test.ts +157 -0
- package/src/agent-plugins.ts +95 -0
- package/src/agent-plugins.unit.test.ts +43 -0
- package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
- package/src/agent-provisioner.integration.test.ts +1245 -0
- package/src/agent-provisioner.ts +1650 -0
- package/src/agent-provisioner.unit.test.ts +1852 -0
- package/src/agent-tokens.integration.test.ts +150 -0
- package/src/agent-tokens.ts +112 -0
- package/src/agent-tools.integration.test.ts +129 -0
- package/src/agent-tools.ts +111 -0
- package/src/agents-api.integration.test.ts +705 -0
- package/src/agents-api.sentry.smoke.test.ts +478 -0
- package/src/agents-api.smoke.test.ts +5986 -0
- package/src/agents-api.ts +2826 -0
- package/src/agents.integration.test.ts +512 -0
- package/src/agents.ts +1097 -0
- package/src/agents.unit.test.ts +1736 -0
- package/src/anthropic-credential-guard.ts +44 -0
- package/src/anthropic-credential-guard.unit.test.ts +88 -0
- package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
- package/src/anthropic-federated-token-provider.ts +249 -0
- package/src/anthropic-identity-client.fixtures.ts +237 -0
- package/src/anthropic-identity-client.integration.test.ts +342 -0
- package/src/anthropic-identity-client.ts +473 -0
- package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
- package/src/anthropic-identity-name.integration.test.ts +83 -0
- package/src/anthropic-identity-name.ts +163 -0
- package/src/anthropic-identity-name.unit.test.ts +345 -0
- package/src/anthropic-openapi-contract.fixtures.ts +192 -0
- package/src/api-auth.ts +383 -0
- package/src/api-auth.unit.test.ts +837 -0
- package/src/api.smoke.test.ts +1225 -0
- package/src/api.ts +376 -0
- package/src/attachment-validation.ts +124 -0
- package/src/attachment-validation.unit.test.ts +218 -0
- package/src/caller-scope.ts +131 -0
- package/src/caller-scope.unit.test.ts +116 -0
- package/src/chat-credential-config.ts +22 -0
- package/src/chat-credentials-logging.unit.test.ts +92 -0
- package/src/chat-markers.ts +114 -0
- package/src/chat-markers.unit.test.ts +116 -0
- package/src/chat-service-provisioning-client.integration.test.ts +412 -0
- package/src/chat-service-provisioning-client.ts +221 -0
- package/src/chat.smoke.test.ts +1875 -0
- package/src/clock.ts +47 -0
- package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
- package/src/coding-task-store-provisioning-client.ts +156 -0
- package/src/crypto.ts +83 -0
- package/src/dev-auth-guard.ts +29 -0
- package/src/dev-auth-guard.unit.test.ts +51 -0
- package/src/errors.ts +77 -0
- package/src/errors.unit.test.ts +52 -0
- package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
- package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
- package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
- package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
- package/src/fixtures/github-app-manifest-cassette.json +44 -0
- package/src/fixtures/google-auth-cassette.json +36 -0
- package/src/fixtures/http-chat-client-cassette.json +226 -0
- package/src/fixtures/k8s-cassette.json +470 -0
- package/src/fixtures/okta-auth-cassette.json +36 -0
- package/src/fixtures/slack-provision-cassette.json +12 -0
- package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
- package/src/fixtures/task-store-provisioning-cassette.json +85 -0
- package/src/github-app-provisioning-client.integration.test.ts +149 -0
- package/src/github-app-provisioning-client.ts +161 -0
- package/src/github-app-provisioning-client.unit.test.ts +114 -0
- package/src/github-provisioning-service.ts +569 -0
- package/src/github-provisioning-service.unit.test.ts +123 -0
- package/src/google-auth-client.integration.test.ts +195 -0
- package/src/google-auth-client.ts +121 -0
- package/src/http-chat-client.integration.test.ts +554 -0
- package/src/http-chat-client.ts +533 -0
- package/src/http-chat-client.unit.test.ts +119 -0
- package/src/import-boundary.unit.test.ts +240 -0
- package/src/kubernetes-client.integration.test.ts +697 -0
- package/src/kubernetes-client.ts +1027 -0
- package/src/kubernetes-client.unit.test.ts +726 -0
- package/src/main.smoke.test.ts +91 -0
- package/src/main.ts +21 -0
- package/src/main.unit.test.ts +801 -0
- package/src/okta-auth-client.integration.test.ts +263 -0
- package/src/okta-auth-client.ts +152 -0
- package/src/openapi-schemas.ts +1310 -0
- package/src/openapi-schemas.unit.test.ts +348 -0
- package/src/per-agent-identity.fixtures.ts +305 -0
- package/src/platform-doc-route.ts +40 -0
- package/src/prisma-client.integration.test.ts +57 -0
- package/src/prisma-client.ts +187 -0
- package/src/prisma-client.unit.test.ts +178 -0
- package/src/prisma-tx.ts +23 -0
- package/src/push-content.ts +177 -0
- package/src/push-content.unit.test.ts +213 -0
- package/src/push-crypto.ts +135 -0
- package/src/push-crypto.unit.test.ts +66 -0
- package/src/push-sender.ts +169 -0
- package/src/push-sender.unit.test.ts +107 -0
- package/src/push-service.ts +332 -0
- package/src/push-service.unit.test.ts +679 -0
- package/src/push-toggle.ts +135 -0
- package/src/push-toggle.unit.test.ts +78 -0
- package/src/pwa-https-gating.smoke.test.ts +229 -0
- package/src/pwa-icons.unit.test.ts +41 -0
- package/src/pwa-routes.smoke.test.ts +276 -0
- package/src/pwa.ts +380 -0
- package/src/pwa.unit.test.ts +321 -0
- package/src/schema.unit.test.ts +122 -0
- package/src/self-serve-config.ts +52 -0
- package/src/self-serve-config.unit.test.ts +79 -0
- package/src/server.smoke.test.ts +273 -0
- package/src/server.ts +50 -0
- package/src/service-integration.ts +67 -0
- package/src/service-integration.unit.test.ts +129 -0
- package/src/slack-provisioning-client.integration.test.ts +308 -0
- package/src/slack-provisioning-client.ts +411 -0
- package/src/slack-provisioning-client.unit.test.ts +80 -0
- package/src/slack-provisioning-service.integration.test.ts +637 -0
- package/src/slack-provisioning-service.ts +451 -0
- package/src/slack-provisioning.integration.test.ts +395 -0
- package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
- package/src/specialty-manifest-loader.ts +255 -0
- package/src/specialty-manifest-loader.unit.test.ts +301 -0
- package/src/specialty-registry.schema.unit.test.ts +35 -0
- package/src/specialty-registry.ts +236 -0
- package/src/specialty-registry.unit.test.ts +272 -0
- package/src/startup-policy-reconcile.ts +77 -0
- package/src/startup-policy-reconcile.unit.test.ts +251 -0
- package/src/theme-tokens.json +12 -0
- package/src/token-crypto.ts +60 -0
- package/src/token-crypto.unit.test.ts +182 -0
- package/src/trial-expiry-sweeper.integration.test.ts +318 -0
- package/src/trial-expiry-sweeper.ts +132 -0
- package/src/trial-expiry-sweeper.unit.test.ts +391 -0
- package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
- package/src/trial-expiry-warning-sweeper.ts +447 -0
- package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
- package/src/voice-contract.unit.test.ts +102 -0
- package/tsconfig.json +9 -0
package/src/api.ts
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/api.ts
|
|
3
|
+
* Hono runtime API — GET /:id/config and GET /:id/crons.
|
|
4
|
+
*
|
|
5
|
+
* Mounted at /agents/* via root.route("/agents", runtimeApp).
|
|
6
|
+
* Hono v4's .route() strips the prefix before dispatching — routes must be
|
|
7
|
+
* registered without the /agents prefix so they resolve correctly at
|
|
8
|
+
* GET /agents/:id/config and GET /agents/:id/crons from the root.
|
|
9
|
+
*
|
|
10
|
+
* Auth: same admin-key / per-agent-token / session-cookie middleware as the
|
|
11
|
+
* CRUD routes (SHIPWRIGHT_INTERNAL_API_KEY removed in UNI-1.2).
|
|
12
|
+
* This is the endpoint the harness polls every 60s.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: Auth middleware is scoped per-route (not global app.use("*")).
|
|
15
|
+
* Using app.use("*") in a sub-app mounted via root.route("/agents", runtimeApp)
|
|
16
|
+
* causes Hono v4 to hoist the middleware as a /agents/* guard in root — which
|
|
17
|
+
* blocks all admin CRUD requests (POST/PATCH/DELETE /agents/:id/*) before they
|
|
18
|
+
* reach the admin handlers. Per-route middleware confines the check to only the
|
|
19
|
+
* two routes that need it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
|
23
|
+
import type { AgentCronJob } from "./agent-cron-jobs.ts";
|
|
24
|
+
import type { AgentEnvBundle } from "./agent-envs.ts";
|
|
25
|
+
import type { AgentTokenService } from "./agent-tokens.ts";
|
|
26
|
+
import { anthropicCredentialKeysIn } from "./anthropic-credential-guard.ts";
|
|
27
|
+
import { type AdminApiKey, createAdminAuthMiddleware } from "./api-auth.ts";
|
|
28
|
+
import {
|
|
29
|
+
AGENT_PHASES,
|
|
30
|
+
AgentConfigResponseSchema,
|
|
31
|
+
AgentCronJobSchema,
|
|
32
|
+
AgentIdParamSchema,
|
|
33
|
+
RuntimeErrorSchema,
|
|
34
|
+
} from "./openapi-schemas.ts";
|
|
35
|
+
|
|
36
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export interface AgentPlugin {
|
|
39
|
+
marketplace: string;
|
|
40
|
+
plugin: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AgentConfigResponse {
|
|
44
|
+
env: Record<string, string>;
|
|
45
|
+
allowedTools: string[];
|
|
46
|
+
plugins: AgentPlugin[];
|
|
47
|
+
repos: string[];
|
|
48
|
+
reviewAuthorAllowlist: string[];
|
|
49
|
+
patchAuthorAllowlist: string[];
|
|
50
|
+
restrictSlackToMembers: boolean;
|
|
51
|
+
memberEmails: string[];
|
|
52
|
+
/**
|
|
53
|
+
* ATE-3.1: ISO timestamp of trial expiry, or null/absent when no trial is
|
|
54
|
+
* configured. Optional (rather than a required nullable field) so existing
|
|
55
|
+
* fixtures/doubles built against this interface before ATE-3.1 keep
|
|
56
|
+
* compiling unchanged. Read by agent/src/index.ts's syncConfig() to sync
|
|
57
|
+
* agent-trial-expiry-ref.ts's live ref, which agent/src/runtime/slack.ts's
|
|
58
|
+
* isTrialExpired() gate reads to block Slack access post-expiry.
|
|
59
|
+
*/
|
|
60
|
+
trialExpiresAt?: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* SSP-8.3: status of the agent's owning Account ('active' | 'suspended' |
|
|
63
|
+
* 'trial_expired'), or null/absent when the agent has no account. Optional
|
|
64
|
+
* for the same fixture-compat reason as trialExpiresAt. Read by
|
|
65
|
+
* agent/src/index.ts's syncConfig() to drive slack.ts's isAccountPaused().
|
|
66
|
+
*/
|
|
67
|
+
accountStatus?: string | null;
|
|
68
|
+
/**
|
|
69
|
+
* ARD-6.1: the agent's Specialty name (e.g. "coding"). Additive and
|
|
70
|
+
* optional — fixtures/doubles built before this field keep compiling, and
|
|
71
|
+
* the agent runtime treats an absent value as "coding".
|
|
72
|
+
*/
|
|
73
|
+
typeName?: string;
|
|
74
|
+
/**
|
|
75
|
+
* PMC-1.1: phase -> subagentType (or null) map, covering all six pipeline
|
|
76
|
+
* phases regardless of whether the agent has an explicit override row for
|
|
77
|
+
* each — phases without a row default to null. The live handler always
|
|
78
|
+
* populates this (see AgentConfigResponseSchema, which requires it), but
|
|
79
|
+
* the TS interface field stays optional — mirroring trialExpiresAt above —
|
|
80
|
+
* so existing fixtures/doubles built against this interface before PMC-1.1
|
|
81
|
+
* (and lib/admin-types.ts's openapi-fetch `paths` type, regenerated
|
|
82
|
+
* separately from admin/openapi.json) keep compiling unchanged.
|
|
83
|
+
*/
|
|
84
|
+
phaseMethodology?: Record<string, string | null>;
|
|
85
|
+
/**
|
|
86
|
+
* APM-1.5: direct passthrough of the 6 agent-policy fields APM-1.1 added
|
|
87
|
+
* to the Agent row. Required fields — no prior-fixture-compat concern
|
|
88
|
+
* since they didn't exist before this task.
|
|
89
|
+
*/
|
|
90
|
+
autoPostReviews: boolean;
|
|
91
|
+
allowSelfReview: boolean;
|
|
92
|
+
minConfidence: number;
|
|
93
|
+
maxFindings: number;
|
|
94
|
+
cleanupMergedWorktrees: boolean;
|
|
95
|
+
cleanupAfterDays: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface AgentEnvServiceLike {
|
|
99
|
+
getConfigBundle(agentId: string): Promise<AgentEnvBundle | null>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface AgentCronJobServiceLike {
|
|
103
|
+
list(agentId: string): Promise<AgentCronJob[]>;
|
|
104
|
+
listWithRunSummary?(agentId: string): Promise<
|
|
105
|
+
Array<
|
|
106
|
+
AgentCronJob & {
|
|
107
|
+
lastRun: null | {
|
|
108
|
+
startedAt: Date;
|
|
109
|
+
completedAt: Date | null;
|
|
110
|
+
skipped: boolean;
|
|
111
|
+
outcome: string | null;
|
|
112
|
+
};
|
|
113
|
+
runCountToday: number;
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface AgentServiceLike {
|
|
120
|
+
getById(agentId: string): Promise<{
|
|
121
|
+
id: string;
|
|
122
|
+
repos: string[];
|
|
123
|
+
reviewAuthorAllowlist: string[];
|
|
124
|
+
patchAuthorAllowlist: string[];
|
|
125
|
+
restrictSlackToMembers: boolean;
|
|
126
|
+
memberEmails: string[];
|
|
127
|
+
/** ATE-3.1: optional so pre-existing test doubles built against this interface keep compiling unchanged. */
|
|
128
|
+
trialExpiresAt?: Date | null;
|
|
129
|
+
/** SSP-8.3: owning account status; optional for pre-existing test doubles. */
|
|
130
|
+
accountStatus?: string | null;
|
|
131
|
+
typeName?: string;
|
|
132
|
+
/** WIF-2.1: optional so pre-existing test doubles keep compiling; absent is treated as not self-hosted. */
|
|
133
|
+
selfHosted?: boolean;
|
|
134
|
+
/** APM-1.5: the 6 agent-policy fields, direct-passthrough into the response. */
|
|
135
|
+
autoPostReviews: boolean;
|
|
136
|
+
allowSelfReview: boolean;
|
|
137
|
+
minConfidence: number;
|
|
138
|
+
maxFindings: number;
|
|
139
|
+
cleanupMergedWorktrees: boolean;
|
|
140
|
+
cleanupAfterDays: number;
|
|
141
|
+
} | null>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface AgentPluginServiceLike {
|
|
145
|
+
listEnabled(agentId: string): Promise<Array<{ name: string }>>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface AgentPhaseMethodologyServiceLike {
|
|
149
|
+
list(
|
|
150
|
+
agentId: string,
|
|
151
|
+
): Promise<Array<{ phase: string; subagentType: string | null }>>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface AgentRuntimeDeps {
|
|
155
|
+
agentEnvService: AgentEnvServiceLike;
|
|
156
|
+
agentCronJobService: AgentCronJobServiceLike;
|
|
157
|
+
agentService: AgentServiceLike;
|
|
158
|
+
agentPluginService: AgentPluginServiceLike;
|
|
159
|
+
/**
|
|
160
|
+
* Optional: supplied by an AdminExtension (the coding extension). Absent →
|
|
161
|
+
* GET /agents/:id/config omits `phaseMethodology`.
|
|
162
|
+
*/
|
|
163
|
+
agentPhaseMethodologyService?: AgentPhaseMethodologyServiceLike;
|
|
164
|
+
/** Session secret for cookie auth (SHIPWRIGHT_SESSION_SECRET). */
|
|
165
|
+
sessionSecret: string;
|
|
166
|
+
/** Parsed SHIPWRIGHT_ADMIN_API_KEYS — optional; absent means env key auth is disabled. */
|
|
167
|
+
adminApiKeys?: Map<string, AdminApiKey>;
|
|
168
|
+
/** Token service for per-agent bearer token validation. */
|
|
169
|
+
agentTokenService: Pick<AgentTokenService, "validate">;
|
|
170
|
+
/** WIF-2.1: Anthropic federation is enabled; defaults to false. */
|
|
171
|
+
anthropicFederationEnabled?: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Route definitions ────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
const getConfigRoute = createRoute({
|
|
177
|
+
method: "get",
|
|
178
|
+
path: "/:id/config",
|
|
179
|
+
tags: ["runtime"],
|
|
180
|
+
summary: "Get agent config bundle",
|
|
181
|
+
description:
|
|
182
|
+
"Returns the agent's full runtime config bundle — decrypted env vars, allowed-tools patterns, installed plugins (with derived marketplace), scoped repos, review/patch author allowlists, and Slack membership-restriction settings. Polled by the agent harness on startup and during its config sync loop. Returns 404 if the agent doesn't exist.",
|
|
183
|
+
security: [{ bearerAuth: [] }],
|
|
184
|
+
request: {
|
|
185
|
+
params: AgentIdParamSchema,
|
|
186
|
+
},
|
|
187
|
+
responses: {
|
|
188
|
+
200: {
|
|
189
|
+
description: "Agent config bundle",
|
|
190
|
+
content: { "application/json": { schema: AgentConfigResponseSchema } },
|
|
191
|
+
},
|
|
192
|
+
401: {
|
|
193
|
+
description: "Unauthorized",
|
|
194
|
+
content: { "application/json": { schema: RuntimeErrorSchema } },
|
|
195
|
+
},
|
|
196
|
+
404: {
|
|
197
|
+
description: "Agent not found",
|
|
198
|
+
content: { "application/json": { schema: RuntimeErrorSchema } },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const getCronsRoute = createRoute({
|
|
204
|
+
method: "get",
|
|
205
|
+
path: "/:id/crons",
|
|
206
|
+
tags: ["runtime"],
|
|
207
|
+
summary: "List agent cron jobs",
|
|
208
|
+
description:
|
|
209
|
+
"Returns the agent's cron jobs as a plain array (not wrapped), used by the agent harness's scheduler. Returns 404 if the agent doesn't exist.",
|
|
210
|
+
security: [{ bearerAuth: [] }],
|
|
211
|
+
request: {
|
|
212
|
+
params: AgentIdParamSchema,
|
|
213
|
+
},
|
|
214
|
+
responses: {
|
|
215
|
+
200: {
|
|
216
|
+
description: "Array of cron jobs",
|
|
217
|
+
content: {
|
|
218
|
+
"application/json": { schema: z.array(AgentCronJobSchema) },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
401: {
|
|
222
|
+
description: "Unauthorized",
|
|
223
|
+
content: { "application/json": { schema: RuntimeErrorSchema } },
|
|
224
|
+
},
|
|
225
|
+
404: {
|
|
226
|
+
description: "Agent not found",
|
|
227
|
+
content: { "application/json": { schema: RuntimeErrorSchema } },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Creates the OpenAPIHono runtime API app.
|
|
236
|
+
*
|
|
237
|
+
* Inject real services for production; inject mocks for tests.
|
|
238
|
+
*/
|
|
239
|
+
export function createAgentRuntimeApp(deps: AgentRuntimeDeps): OpenAPIHono {
|
|
240
|
+
const {
|
|
241
|
+
agentEnvService,
|
|
242
|
+
agentCronJobService,
|
|
243
|
+
agentService,
|
|
244
|
+
agentPluginService,
|
|
245
|
+
agentPhaseMethodologyService,
|
|
246
|
+
} = deps;
|
|
247
|
+
|
|
248
|
+
const app = new OpenAPIHono();
|
|
249
|
+
|
|
250
|
+
// Auth middleware — applied per-route, NOT as app.use("*").
|
|
251
|
+
// See file-level comment for why global middleware is avoided here.
|
|
252
|
+
const requireAuth = createAdminAuthMiddleware({
|
|
253
|
+
sessionSecret: deps.sessionSecret,
|
|
254
|
+
adminApiKeys: deps.adminApiKeys,
|
|
255
|
+
agentTokenService: deps.agentTokenService,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─── GET /:id/config ─────────────────────────────────────────────────────
|
|
259
|
+
// Reachable from root as GET /agents/:id/config (Hono v4 strips prefix)
|
|
260
|
+
|
|
261
|
+
app.use("/:id/config", requireAuth);
|
|
262
|
+
|
|
263
|
+
app.openapi(getConfigRoute, async (c) => {
|
|
264
|
+
const { id } = c.req.valid("param");
|
|
265
|
+
|
|
266
|
+
// Check agent existence
|
|
267
|
+
const agent = await agentService.getById(id);
|
|
268
|
+
if (!agent) {
|
|
269
|
+
return c.json({ error: "Not found" }, 404);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fetch env bundle, plugins, and phase-methodology overrides in parallel
|
|
273
|
+
const [bundle, plugins, phaseMethodologyRows] = await Promise.all([
|
|
274
|
+
agentEnvService.getConfigBundle(id),
|
|
275
|
+
agentPluginService.listEnabled(id),
|
|
276
|
+
agentPhaseMethodologyService?.list(id),
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
// WIF-2.1: with federation enabled, a non-self-hosted agent must never be
|
|
280
|
+
// handed Anthropic credentials, even ones already stored in its bundle
|
|
281
|
+
// (the agent re-reads this endpoint every 60s). Log names only.
|
|
282
|
+
let env = bundle?.env ?? {};
|
|
283
|
+
if (deps.anthropicFederationEnabled && !agent.selfHosted) {
|
|
284
|
+
const blocked = anthropicCredentialKeysIn(Object.keys(env));
|
|
285
|
+
if (blocked.length > 0) {
|
|
286
|
+
console.warn(
|
|
287
|
+
`[api] stripped Anthropic credentials from agent ${id} config under federation: ${blocked.join(", ")}`,
|
|
288
|
+
);
|
|
289
|
+
env = Object.fromEntries(
|
|
290
|
+
Object.entries(env).filter(([k]) => !blocked.includes(k)),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Every phase defaults to null; override rows (if any) fill in the rest.
|
|
296
|
+
const phaseMethodology: Record<string, string | null> = Object.fromEntries(
|
|
297
|
+
AGENT_PHASES.map((phase) => [phase, null]),
|
|
298
|
+
);
|
|
299
|
+
for (const row of phaseMethodologyRows ?? []) {
|
|
300
|
+
phaseMethodology[row.phase] = row.subagentType;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Not annotated as `: AgentConfigResponse` — that interface's
|
|
304
|
+
// phaseMethodology field is deliberately optional (see its doc comment)
|
|
305
|
+
// for downstream consumer/fixture compatibility, but this handler always
|
|
306
|
+
// populates every field, so leaving the literal un-annotated lets it
|
|
307
|
+
// infer the fully-required shape the route's Zod response schema (which
|
|
308
|
+
// requires phaseMethodology) expects. `c.json()` below still structurally
|
|
309
|
+
// checks this literal against that schema-derived type.
|
|
310
|
+
const response = {
|
|
311
|
+
env,
|
|
312
|
+
allowedTools: bundle?.allowedTools ?? [],
|
|
313
|
+
plugins: plugins.map((p) => {
|
|
314
|
+
// The stored name is the canonical Claude plugin spec — exactly what
|
|
315
|
+
// you'd pass to `claude plugin install`: "<plugin>@<marketplace>"
|
|
316
|
+
// (e.g. "shipwright@shipwright"), or a bare "<plugin>" that defaults to
|
|
317
|
+
// the bundled "shipwright" marketplace. The harness reassembles
|
|
318
|
+
// "<plugin>@<marketplace>" from these two fields.
|
|
319
|
+
const at = p.name.indexOf("@");
|
|
320
|
+
return at === -1
|
|
321
|
+
? { marketplace: "shipwright", plugin: p.name }
|
|
322
|
+
: { plugin: p.name.slice(0, at), marketplace: p.name.slice(at + 1) };
|
|
323
|
+
}),
|
|
324
|
+
repos: agent.repos,
|
|
325
|
+
reviewAuthorAllowlist: agent.reviewAuthorAllowlist,
|
|
326
|
+
patchAuthorAllowlist: agent.patchAuthorAllowlist,
|
|
327
|
+
restrictSlackToMembers: agent.restrictSlackToMembers ?? false,
|
|
328
|
+
memberEmails: agent.memberEmails ?? [],
|
|
329
|
+
trialExpiresAt: agent.trialExpiresAt
|
|
330
|
+
? agent.trialExpiresAt.toISOString()
|
|
331
|
+
: null,
|
|
332
|
+
accountStatus: agent.accountStatus ?? null,
|
|
333
|
+
typeName: agent.typeName,
|
|
334
|
+
// Omitted entirely when no phase-methodology service was supplied
|
|
335
|
+
// (platform-only admin, no coding extension).
|
|
336
|
+
...(agentPhaseMethodologyService ? { phaseMethodology } : {}),
|
|
337
|
+
// APM-1.5: direct passthrough — the DB column always has a value
|
|
338
|
+
// (Prisma schema default), so no `??` fallback is needed.
|
|
339
|
+
autoPostReviews: agent.autoPostReviews,
|
|
340
|
+
allowSelfReview: agent.allowSelfReview,
|
|
341
|
+
minConfidence: agent.minConfidence,
|
|
342
|
+
maxFindings: agent.maxFindings,
|
|
343
|
+
cleanupMergedWorktrees: agent.cleanupMergedWorktrees,
|
|
344
|
+
cleanupAfterDays: agent.cleanupAfterDays,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// The route's Zod schema requires phaseMethodology; a platform-only admin
|
|
348
|
+
// omits it, so the response is asserted to the schema-derived shape.
|
|
349
|
+
return c.json(
|
|
350
|
+
response as typeof response & {
|
|
351
|
+
phaseMethodology: Record<string, string | null>;
|
|
352
|
+
},
|
|
353
|
+
200,
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ─── GET /:id/crons ──────────────────────────────────────────────────────
|
|
358
|
+
// Reachable from root as GET /agents/:id/crons (Hono v4 strips prefix)
|
|
359
|
+
|
|
360
|
+
app.use("/:id/crons", requireAuth);
|
|
361
|
+
|
|
362
|
+
app.openapi(getCronsRoute, async (c) => {
|
|
363
|
+
const { id } = c.req.valid("param");
|
|
364
|
+
|
|
365
|
+
// Check agent existence
|
|
366
|
+
const agent = await agentService.getById(id);
|
|
367
|
+
if (!agent) {
|
|
368
|
+
return c.json({ error: "Not found" }, 404);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const crons = await agentCronJobService.list(id);
|
|
372
|
+
return c.json(crons, 200);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return app;
|
|
376
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/attachment-validation.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure, I/O-free validation for chat message attachments. Enforces the size cap
|
|
5
|
+
* and an allowlist of MIME types before an upload is ever handed to the chat
|
|
6
|
+
* service. Kept side-effect-free so it is trivially unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Maximum allowed attachment size (10 MB) — mirrors the chat service guard. */
|
|
10
|
+
export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
/** MIME type prefixes accepted wholesale (e.g. any text/* file). */
|
|
13
|
+
const ALLOWED_MIME_PREFIXES = ["text/"];
|
|
14
|
+
|
|
15
|
+
/** Exact MIME types accepted beyond the prefix list. */
|
|
16
|
+
const ALLOWED_MIME_EXACT = new Set([
|
|
17
|
+
"image/jpeg",
|
|
18
|
+
"image/png",
|
|
19
|
+
"image/gif",
|
|
20
|
+
"image/webp",
|
|
21
|
+
"image/svg+xml",
|
|
22
|
+
"image/heic",
|
|
23
|
+
"image/heif",
|
|
24
|
+
"application/pdf",
|
|
25
|
+
"application/json",
|
|
26
|
+
// VM-3.1: mic-recorded blobs are submitted through this same upload path —
|
|
27
|
+
// these are the exact MIME strings Chrome/Firefox's MediaRecorder emits
|
|
28
|
+
// (browser/codec dependent, so all three are allowed).
|
|
29
|
+
"audio/webm",
|
|
30
|
+
"audio/webm;codecs=opus",
|
|
31
|
+
"audio/ogg",
|
|
32
|
+
// Safari/iOS MediaRecorder output, plus common TTS/upload audio formats.
|
|
33
|
+
"audio/mp4",
|
|
34
|
+
"audio/mpeg",
|
|
35
|
+
"audio/wav",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Filename extensions rendered as an inline <audio> player instead of a
|
|
40
|
+
* plain filename badge (VM-3.1). No MIME type is persisted end-to-end on a
|
|
41
|
+
* ChatMessage — the chat service only stores attachmentFilename/attachmentSize
|
|
42
|
+
* — so audio-ness is inferred from the filename extension. Covers both
|
|
43
|
+
* MediaRecorder-authored user attachments (.webm/.ogg, this task) and
|
|
44
|
+
* TTS-authored assistant attachments (.wav/.mp3, from VM-2.1's chat-poller).
|
|
45
|
+
*/
|
|
46
|
+
const AUDIO_FILE_EXTENSIONS = [".wav", ".mp3", ".webm", ".ogg", ".m4a"];
|
|
47
|
+
|
|
48
|
+
const AUDIO_CONTENT_TYPES: Record<string, string> = {
|
|
49
|
+
".wav": "audio/wav",
|
|
50
|
+
".mp3": "audio/mpeg",
|
|
51
|
+
".webm": "audio/webm",
|
|
52
|
+
".ogg": "audio/ogg",
|
|
53
|
+
".m4a": "audio/mp4",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Map an audio filename's extension to a real `audio/*` Content-Type, for
|
|
58
|
+
* the admin attachment-proxy route to serve. The chat service's own
|
|
59
|
+
* GET /:id/attachment always streams `application/octet-stream`
|
|
60
|
+
* (chat/src/routes/messages.ts), which most browsers refuse to play inline
|
|
61
|
+
* in an <audio> element — the proxy route re-labels the response using this
|
|
62
|
+
* mapping instead. Returns null for a non-audio (or unrecognized) filename.
|
|
63
|
+
*/
|
|
64
|
+
export function audioContentTypeForFilename(
|
|
65
|
+
filename: string | null | undefined,
|
|
66
|
+
): string | null {
|
|
67
|
+
if (!filename) return null;
|
|
68
|
+
const lower = filename.toLowerCase();
|
|
69
|
+
const ext = AUDIO_FILE_EXTENSIONS.find((e) => lower.endsWith(e));
|
|
70
|
+
return ext ? (AUDIO_CONTENT_TYPES[ext] ?? null) : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether an attachment's filename indicates audio content. */
|
|
74
|
+
export function isAudioFilename(filename: string | null | undefined): boolean {
|
|
75
|
+
return audioContentTypeForFilename(filename) !== null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type AttachmentValidationResult =
|
|
79
|
+
| { ok: false; error: string; status: 413 | 415 }
|
|
80
|
+
| { ok: true; filename: string; size: number };
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate an attachment by filename, byte size, and MIME type.
|
|
84
|
+
* Returns a discriminated result: on failure it carries a clear error message
|
|
85
|
+
* and the appropriate HTTP status (413 too large, 415 unsupported type).
|
|
86
|
+
*/
|
|
87
|
+
export function validateAttachment(
|
|
88
|
+
filename: string,
|
|
89
|
+
size: number,
|
|
90
|
+
mimeType: string,
|
|
91
|
+
): AttachmentValidationResult {
|
|
92
|
+
if (size > MAX_ATTACHMENT_BYTES) {
|
|
93
|
+
const mb = Math.round((size / 1024 / 1024) * 10) / 10;
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
status: 413,
|
|
97
|
+
error: `Attachment exceeds the 10 MB limit (received ~${mb} MB)`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Strip parameters (";codecs=opus") and case before matching; the exact set
|
|
102
|
+
// still lists the codecs variant for backward compatibility.
|
|
103
|
+
const baseType = mimeType.split(";")[0].trim().toLowerCase();
|
|
104
|
+
// Chromium reports audio-only MediaRecorder output as video/webm — accept it
|
|
105
|
+
// only for .webm filenames so arbitrary video uploads stay rejected.
|
|
106
|
+
const isWebmRecording =
|
|
107
|
+
baseType === "video/webm" && filename.toLowerCase().endsWith(".webm");
|
|
108
|
+
|
|
109
|
+
const isAllowed =
|
|
110
|
+
ALLOWED_MIME_EXACT.has(mimeType) ||
|
|
111
|
+
ALLOWED_MIME_EXACT.has(baseType) ||
|
|
112
|
+
isWebmRecording ||
|
|
113
|
+
ALLOWED_MIME_PREFIXES.some((prefix) => baseType.startsWith(prefix));
|
|
114
|
+
|
|
115
|
+
if (!isAllowed) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
status: 415,
|
|
119
|
+
error: `Attachment type "${mimeType || "unknown"}" is not allowed`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { ok: true, filename, size };
|
|
124
|
+
}
|