@app-vitals/agent-admin 0.0.1 → 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 +62 -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
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/anthropic-identity-client.ts
|
|
3
|
+
* AnthropicIdentityClient interface and HttpAnthropicIdentityClient
|
|
4
|
+
* implementation (PAI-1.5).
|
|
5
|
+
*
|
|
6
|
+
* Manages the Anthropic-side identity of a Shipwright agent through the
|
|
7
|
+
* Anthropic Admin API, using the typed resources of @anthropic-ai/sdk:
|
|
8
|
+
*
|
|
9
|
+
* createServiceAccount POST /v1/organizations/service_accounts
|
|
10
|
+
* addServiceAccountToWorkspace POST /v1/organizations/service_accounts/{id}/workspaces
|
|
11
|
+
* createFederationRule POST /v1/organizations/federation_rules
|
|
12
|
+
* archiveFederationRule POST /v1/organizations/federation_rules/{id}/archive
|
|
13
|
+
* archiveServiceAccount POST /v1/organizations/service_accounts/{id}/archive
|
|
14
|
+
* findServiceAccountByName GET /v1/organizations/service_accounts (paged)
|
|
15
|
+
* findFederationRuleByName GET /v1/organizations/federation_rules (paged)
|
|
16
|
+
*
|
|
17
|
+
* What the issue calls a "rule" is the spec's federation rule. The list
|
|
18
|
+
* endpoints have no name filter, so the find-by-name fallbacks follow the
|
|
19
|
+
* `next_page` cursor until it is exhausted.
|
|
20
|
+
*
|
|
21
|
+
* Auth: the admin pod signs in by federation (FederatedTokenProvider); the
|
|
22
|
+
* exchanged access token is sent as a bearer token. The SDK is built with the
|
|
23
|
+
* injected `fetchFn`, so tests replay recorded fixtures with no global
|
|
24
|
+
* override.
|
|
25
|
+
*
|
|
26
|
+
* Trust boundary: every response body is parsed with zod before any field is
|
|
27
|
+
* used. Secrets: no token or Authorization header is ever logged or placed in
|
|
28
|
+
* an error message (SDK logging is off and SDK errors are re-mapped to
|
|
29
|
+
* status / error type / request id only).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import {
|
|
35
|
+
type AnthropicAccessTokenProvider,
|
|
36
|
+
DEFAULT_ANTHROPIC_BASE_URL,
|
|
37
|
+
OAUTH_API_BETA,
|
|
38
|
+
} from "./anthropic-federated-token-provider.ts";
|
|
39
|
+
|
|
40
|
+
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface AnthropicServiceAccount {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
/** ISO timestamp, or null while the service account is live. */
|
|
46
|
+
archivedAt: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AnthropicFederationRule {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
archivedAt: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AnthropicWorkspaceMember {
|
|
56
|
+
serviceAccountId: string;
|
|
57
|
+
workspaceId: string;
|
|
58
|
+
workspaceRole: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type AnthropicWorkspaceRole =
|
|
62
|
+
| "workspace_admin"
|
|
63
|
+
| "workspace_developer"
|
|
64
|
+
| "workspace_restricted_developer"
|
|
65
|
+
| "workspace_user";
|
|
66
|
+
|
|
67
|
+
export interface CreateServiceAccountInput {
|
|
68
|
+
name: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AddServiceAccountToWorkspaceInput {
|
|
73
|
+
serviceAccountId: string;
|
|
74
|
+
workspaceId: string;
|
|
75
|
+
/** Defaults to workspace_developer. */
|
|
76
|
+
workspaceRole?: AnthropicWorkspaceRole;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CreateFederationRuleInput {
|
|
80
|
+
name: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
/** Federation issuer (`fdis_...`) the rule matches tokens from. */
|
|
83
|
+
issuerId: string;
|
|
84
|
+
/** Service account (`svac_...`) the rule mints tokens for. */
|
|
85
|
+
serviceAccountId: string;
|
|
86
|
+
workspaceId?: string;
|
|
87
|
+
/** Defaults to workspace:developer. */
|
|
88
|
+
oauthScope?: string;
|
|
89
|
+
match: {
|
|
90
|
+
subjectPrefix?: string;
|
|
91
|
+
audience?: string;
|
|
92
|
+
condition?: string;
|
|
93
|
+
};
|
|
94
|
+
tokenLifetimeSeconds?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface AnthropicIdentityClient {
|
|
98
|
+
createServiceAccount(
|
|
99
|
+
input: CreateServiceAccountInput,
|
|
100
|
+
): Promise<AnthropicServiceAccount>;
|
|
101
|
+
addServiceAccountToWorkspace(
|
|
102
|
+
input: AddServiceAccountToWorkspaceInput,
|
|
103
|
+
): Promise<AnthropicWorkspaceMember>;
|
|
104
|
+
createFederationRule(
|
|
105
|
+
input: CreateFederationRuleInput,
|
|
106
|
+
): Promise<AnthropicFederationRule>;
|
|
107
|
+
archiveFederationRule(ruleId: string): Promise<AnthropicFederationRule>;
|
|
108
|
+
archiveServiceAccount(
|
|
109
|
+
serviceAccountId: string,
|
|
110
|
+
): Promise<AnthropicServiceAccount>;
|
|
111
|
+
/** Live (non-archived) service account with exactly this name, or null. */
|
|
112
|
+
findServiceAccountByName(
|
|
113
|
+
name: string,
|
|
114
|
+
): Promise<AnthropicServiceAccount | null>;
|
|
115
|
+
/** Live (non-archived) federation rule with exactly this name, or null. */
|
|
116
|
+
findFederationRuleByName(
|
|
117
|
+
name: string,
|
|
118
|
+
): Promise<AnthropicFederationRule | null>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Errors ───────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/** An Anthropic API call failed. Carries no body text, only safe metadata. */
|
|
124
|
+
export class AnthropicApiError extends Error {
|
|
125
|
+
constructor(
|
|
126
|
+
readonly operation: string,
|
|
127
|
+
readonly status: number | undefined,
|
|
128
|
+
readonly errorType?: string,
|
|
129
|
+
readonly requestId?: string,
|
|
130
|
+
) {
|
|
131
|
+
super(
|
|
132
|
+
`Anthropic ${operation} failed${status ? ` with status ${status}` : " (network error)"}${
|
|
133
|
+
errorType ? ` (${errorType})` : ""
|
|
134
|
+
}${requestId ? ` (request-id ${requestId})` : ""}`,
|
|
135
|
+
);
|
|
136
|
+
this.name = "AnthropicApiError";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 409: a resource with that name already exists. Fall back to find-by-name. */
|
|
141
|
+
export class AnthropicConflictError extends AnthropicApiError {
|
|
142
|
+
constructor(operation: string, errorType?: string, requestId?: string) {
|
|
143
|
+
super(operation, 409, errorType, requestId);
|
|
144
|
+
this.name = "AnthropicConflictError";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 401 that persisted after one token re-exchange. */
|
|
149
|
+
export class AnthropicAuthError extends AnthropicApiError {
|
|
150
|
+
constructor(operation: string, errorType?: string, requestId?: string) {
|
|
151
|
+
super(operation, 401, errorType, requestId);
|
|
152
|
+
this.name = "AnthropicAuthError";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A response body did not match the expected schema (body is never echoed). */
|
|
157
|
+
export class AnthropicResponseValidationError extends Error {
|
|
158
|
+
constructor(
|
|
159
|
+
readonly operation: string,
|
|
160
|
+
detail: string,
|
|
161
|
+
) {
|
|
162
|
+
super(`Anthropic ${operation} returned an invalid response: ${detail}`);
|
|
163
|
+
this.name = "AnthropicResponseValidationError";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Response schemas (trust boundary) ────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/** Service accounts and federation rules share this shape. */
|
|
170
|
+
const NamedResourceSchema = z.object({
|
|
171
|
+
id: z.string().min(1),
|
|
172
|
+
name: z.string(),
|
|
173
|
+
archived_at: z.string().nullable(),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const WorkspaceMemberSchema = z.object({
|
|
177
|
+
service_account_id: z.string().min(1),
|
|
178
|
+
workspace_id: z.string().min(1),
|
|
179
|
+
workspace_role: z.string(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const pageOf = <T extends z.ZodType>(item: T) =>
|
|
183
|
+
z.object({ data: z.array(item), next_page: z.string().nullable() });
|
|
184
|
+
|
|
185
|
+
const ErrorBodySchema = z.object({
|
|
186
|
+
request_id: z.string().nullish(),
|
|
187
|
+
error: z.object({ type: z.string() }),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const toNamedResource = (
|
|
191
|
+
r: z.infer<typeof NamedResourceSchema>,
|
|
192
|
+
): AnthropicServiceAccount & AnthropicFederationRule => ({
|
|
193
|
+
id: r.id,
|
|
194
|
+
name: r.name,
|
|
195
|
+
archivedAt: r.archived_at,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
const PAGE_SIZE = 100;
|
|
201
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
202
|
+
|
|
203
|
+
export interface HttpAnthropicIdentityClientOptions {
|
|
204
|
+
/** Injected fetch; used for every API call (tests replay recorded fixtures). */
|
|
205
|
+
fetchFn: typeof fetch;
|
|
206
|
+
tokenProvider: AnthropicAccessTokenProvider;
|
|
207
|
+
baseUrl?: string;
|
|
208
|
+
timeoutMs?: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
type RawCall = (sdk: Anthropic) => { asResponse(): Promise<Response> };
|
|
212
|
+
|
|
213
|
+
export class HttpAnthropicIdentityClient implements AnthropicIdentityClient {
|
|
214
|
+
private readonly baseUrl: string;
|
|
215
|
+
private readonly timeoutMs: number;
|
|
216
|
+
private sdkCache: { token: string; sdk: Anthropic } | null = null;
|
|
217
|
+
|
|
218
|
+
constructor(private readonly opts: HttpAnthropicIdentityClientOptions) {
|
|
219
|
+
this.baseUrl = opts.baseUrl ?? DEFAULT_ANTHROPIC_BASE_URL;
|
|
220
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Interface ───────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
async createServiceAccount(
|
|
226
|
+
input: CreateServiceAccountInput,
|
|
227
|
+
): Promise<AnthropicServiceAccount> {
|
|
228
|
+
const sa = await this.request(
|
|
229
|
+
"createServiceAccount",
|
|
230
|
+
NamedResourceSchema,
|
|
231
|
+
(sdk) =>
|
|
232
|
+
sdk.organization.serviceAccounts.create({
|
|
233
|
+
name: input.name,
|
|
234
|
+
description: input.description,
|
|
235
|
+
// A workload may only create developer-role service accounts.
|
|
236
|
+
organization_role: "developer",
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
return toNamedResource(sa);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async addServiceAccountToWorkspace(
|
|
243
|
+
input: AddServiceAccountToWorkspaceInput,
|
|
244
|
+
): Promise<AnthropicWorkspaceMember> {
|
|
245
|
+
const m = await this.request(
|
|
246
|
+
"addServiceAccountToWorkspace",
|
|
247
|
+
WorkspaceMemberSchema,
|
|
248
|
+
(sdk) =>
|
|
249
|
+
sdk.organization.serviceAccounts.workspaces.add(
|
|
250
|
+
input.serviceAccountId,
|
|
251
|
+
{
|
|
252
|
+
workspace_id: input.workspaceId,
|
|
253
|
+
workspace_role: input.workspaceRole ?? "workspace_developer",
|
|
254
|
+
},
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
return {
|
|
258
|
+
serviceAccountId: m.service_account_id,
|
|
259
|
+
workspaceId: m.workspace_id,
|
|
260
|
+
workspaceRole: m.workspace_role,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async createFederationRule(
|
|
265
|
+
input: CreateFederationRuleInput,
|
|
266
|
+
): Promise<AnthropicFederationRule> {
|
|
267
|
+
const rule = await this.request(
|
|
268
|
+
"createFederationRule",
|
|
269
|
+
NamedResourceSchema,
|
|
270
|
+
(sdk) =>
|
|
271
|
+
sdk.organization.federation.rules.create({
|
|
272
|
+
// Undefined optionals are dropped by JSON serialization.
|
|
273
|
+
name: input.name,
|
|
274
|
+
description: input.description,
|
|
275
|
+
issuer_id: input.issuerId,
|
|
276
|
+
match: {
|
|
277
|
+
subject_prefix: input.match.subjectPrefix,
|
|
278
|
+
audience: input.match.audience,
|
|
279
|
+
condition: input.match.condition,
|
|
280
|
+
},
|
|
281
|
+
oauth_scope: input.oauthScope ?? "workspace:developer",
|
|
282
|
+
target: {
|
|
283
|
+
type: "service_account",
|
|
284
|
+
service_account_id: input.serviceAccountId,
|
|
285
|
+
},
|
|
286
|
+
workspace_id: input.workspaceId,
|
|
287
|
+
token_lifetime_seconds: input.tokenLifetimeSeconds,
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
return toNamedResource(rule);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async archiveFederationRule(
|
|
294
|
+
ruleId: string,
|
|
295
|
+
): Promise<AnthropicFederationRule> {
|
|
296
|
+
const rule = await this.request(
|
|
297
|
+
"archiveFederationRule",
|
|
298
|
+
NamedResourceSchema,
|
|
299
|
+
(sdk) => sdk.organization.federation.rules.archive(ruleId),
|
|
300
|
+
);
|
|
301
|
+
return toNamedResource(rule);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async archiveServiceAccount(
|
|
305
|
+
serviceAccountId: string,
|
|
306
|
+
): Promise<AnthropicServiceAccount> {
|
|
307
|
+
const sa = await this.request(
|
|
308
|
+
"archiveServiceAccount",
|
|
309
|
+
NamedResourceSchema,
|
|
310
|
+
(sdk) => sdk.organization.serviceAccounts.archive(serviceAccountId),
|
|
311
|
+
);
|
|
312
|
+
return toNamedResource(sa);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async findServiceAccountByName(
|
|
316
|
+
name: string,
|
|
317
|
+
): Promise<AnthropicServiceAccount | null> {
|
|
318
|
+
const found = await this.findByName(
|
|
319
|
+
"findServiceAccountByName",
|
|
320
|
+
NamedResourceSchema,
|
|
321
|
+
name,
|
|
322
|
+
(sdk, page) =>
|
|
323
|
+
sdk.organization.serviceAccounts.list({ limit: PAGE_SIZE, page }),
|
|
324
|
+
);
|
|
325
|
+
return found ? toNamedResource(found) : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async findFederationRuleByName(
|
|
329
|
+
name: string,
|
|
330
|
+
): Promise<AnthropicFederationRule | null> {
|
|
331
|
+
const found = await this.findByName(
|
|
332
|
+
"findFederationRuleByName",
|
|
333
|
+
NamedResourceSchema,
|
|
334
|
+
name,
|
|
335
|
+
(sdk, page) =>
|
|
336
|
+
sdk.organization.federation.rules.list({ limit: PAGE_SIZE, page }),
|
|
337
|
+
);
|
|
338
|
+
return found ? toNamedResource(found) : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Internals ───────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The list endpoints have no name filter: walk every page via `next_page`
|
|
345
|
+
* until the name is found or the cursor is exhausted. Archived entries are
|
|
346
|
+
* skipped (they are excluded by default but checked defensively).
|
|
347
|
+
*/
|
|
348
|
+
private async findByName<
|
|
349
|
+
T extends z.ZodType<{ name: string; archived_at: string | null }>,
|
|
350
|
+
>(
|
|
351
|
+
operation: string,
|
|
352
|
+
itemSchema: T,
|
|
353
|
+
name: string,
|
|
354
|
+
list: (sdk: Anthropic, page: string | undefined) => ReturnType<RawCall>,
|
|
355
|
+
): Promise<z.infer<T> | null> {
|
|
356
|
+
const schema = pageOf(itemSchema);
|
|
357
|
+
const seen = new Set<string>();
|
|
358
|
+
let page: string | undefined;
|
|
359
|
+
for (;;) {
|
|
360
|
+
const cursor = page;
|
|
361
|
+
const result = await this.request(operation, schema, (sdk) =>
|
|
362
|
+
list(sdk, cursor),
|
|
363
|
+
);
|
|
364
|
+
const hit = result.data.find(
|
|
365
|
+
(item) => item.name === name && item.archived_at === null,
|
|
366
|
+
);
|
|
367
|
+
if (hit) return hit;
|
|
368
|
+
if (result.next_page === null) return null;
|
|
369
|
+
if (seen.has(result.next_page)) {
|
|
370
|
+
throw new AnthropicResponseValidationError(
|
|
371
|
+
operation,
|
|
372
|
+
"pagination cursor repeated",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
seen.add(result.next_page);
|
|
376
|
+
page = result.next_page;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The SDK client is rebuilt only when the access token changes. */
|
|
381
|
+
private async sdk(): Promise<Anthropic> {
|
|
382
|
+
const token = await this.opts.tokenProvider.getAccessToken();
|
|
383
|
+
if (this.sdkCache?.token === token) return this.sdkCache.sdk;
|
|
384
|
+
const sdk = new Anthropic({
|
|
385
|
+
// Explicit null stops the SDK reading ANTHROPIC_API_KEY from the env,
|
|
386
|
+
// which would outrank the federated bearer token.
|
|
387
|
+
apiKey: null,
|
|
388
|
+
authToken: token,
|
|
389
|
+
baseURL: this.baseUrl,
|
|
390
|
+
fetch: this.opts.fetchFn as unknown as NonNullable<
|
|
391
|
+
ConstructorParameters<typeof Anthropic>[0]
|
|
392
|
+
>["fetch"],
|
|
393
|
+
// Retrying 409 would defeat duplicate-name detection; 401 is retried
|
|
394
|
+
// here after a token re-exchange instead.
|
|
395
|
+
maxRetries: 0,
|
|
396
|
+
timeout: this.timeoutMs,
|
|
397
|
+
logLevel: "off",
|
|
398
|
+
defaultHeaders: { "anthropic-beta": OAUTH_API_BETA },
|
|
399
|
+
});
|
|
400
|
+
this.sdkCache = { token, sdk };
|
|
401
|
+
return sdk;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Run one SDK call, parse the raw JSON body with zod, and map failures to
|
|
406
|
+
* typed errors. A 401 triggers exactly one token re-exchange and retry.
|
|
407
|
+
*/
|
|
408
|
+
private async request<T extends z.ZodType>(
|
|
409
|
+
operation: string,
|
|
410
|
+
schema: T,
|
|
411
|
+
call: RawCall,
|
|
412
|
+
): Promise<z.infer<T>> {
|
|
413
|
+
for (let attempt = 0; ; attempt++) {
|
|
414
|
+
const sdk = await this.sdk();
|
|
415
|
+
let text: string;
|
|
416
|
+
try {
|
|
417
|
+
text = await (await call(sdk).asResponse()).text();
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const mapped = mapError(operation, err);
|
|
420
|
+
if (mapped instanceof AnthropicAuthError && attempt === 0) {
|
|
421
|
+
this.opts.tokenProvider.invalidate();
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
throw mapped;
|
|
425
|
+
}
|
|
426
|
+
return parseBody(operation, schema, text);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function parseBody<T extends z.ZodType>(
|
|
432
|
+
operation: string,
|
|
433
|
+
schema: T,
|
|
434
|
+
text: string,
|
|
435
|
+
): z.infer<T> {
|
|
436
|
+
let json: unknown;
|
|
437
|
+
try {
|
|
438
|
+
json = JSON.parse(text);
|
|
439
|
+
} catch {
|
|
440
|
+
throw new AnthropicResponseValidationError(operation, "body is not JSON");
|
|
441
|
+
}
|
|
442
|
+
const parsed = schema.safeParse(json);
|
|
443
|
+
if (!parsed.success) {
|
|
444
|
+
// Paths only: zod messages for values could echo response content.
|
|
445
|
+
const paths = parsed.error.issues
|
|
446
|
+
.map((i) => i.path.join(".") || "(root)")
|
|
447
|
+
.join(", ");
|
|
448
|
+
throw new AnthropicResponseValidationError(
|
|
449
|
+
operation,
|
|
450
|
+
`schema mismatch at ${paths}`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return parsed.data;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function mapError(operation: string, err: unknown): Error {
|
|
457
|
+
if (!(err instanceof Anthropic.APIError)) {
|
|
458
|
+
// Connection failures and timeouts: never surface the SDK message, which
|
|
459
|
+
// can include the request URL and cause chain.
|
|
460
|
+
return new AnthropicApiError(operation, undefined);
|
|
461
|
+
}
|
|
462
|
+
const body = ErrorBodySchema.safeParse(err.error);
|
|
463
|
+
const errorType = body.success ? body.data.error.type : undefined;
|
|
464
|
+
const bodyRequestId = body.success ? body.data.request_id : undefined;
|
|
465
|
+
const requestId = bodyRequestId ?? err.requestID ?? undefined;
|
|
466
|
+
if (err.status === 409) {
|
|
467
|
+
return new AnthropicConflictError(operation, errorType, requestId);
|
|
468
|
+
}
|
|
469
|
+
if (err.status === 401) {
|
|
470
|
+
return new AnthropicAuthError(operation, errorType, requestId);
|
|
471
|
+
}
|
|
472
|
+
return new AnthropicApiError(operation, err.status, errorType, requestId);
|
|
473
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/anthropic-identity-fixture-spec.integration.test.ts
|
|
3
|
+
* Offline contract test: every request and response in the Anthropic recorded
|
|
4
|
+
* fixture doubles must satisfy the Anthropic OpenAPI 3.1 spec copied into the
|
|
5
|
+
* repo (pinned to the @anthropic-ai/sdk version in admin/package.json). A
|
|
6
|
+
* recorded fixture that drifts from the spec fails here.
|
|
7
|
+
*
|
|
8
|
+
* Known gap: POST /v1/oauth/token is not in the spec, so the token exchange is
|
|
9
|
+
* not covered by this test (see anthropic-identity-client.fixtures.ts).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "bun:test";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import {
|
|
15
|
+
type AnthropicRecordedFixtures,
|
|
16
|
+
loadRecordedFixtures,
|
|
17
|
+
} from "./anthropic-identity-client.fixtures.ts";
|
|
18
|
+
import {
|
|
19
|
+
loadSpec,
|
|
20
|
+
readSpecSdkVersion,
|
|
21
|
+
validateRecordedFixturesAgainstSpec,
|
|
22
|
+
} from "./anthropic-openapi-contract.fixtures.ts";
|
|
23
|
+
|
|
24
|
+
const spec = loadSpec();
|
|
25
|
+
|
|
26
|
+
function clone(): AnthropicRecordedFixtures {
|
|
27
|
+
return structuredClone(loadRecordedFixtures());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("pinned Anthropic OpenAPI spec", () => {
|
|
31
|
+
it("is OpenAPI 3.1", () => {
|
|
32
|
+
expect(spec.openapi).toStartWith("3.1");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("is pinned to exactly the @anthropic-ai/sdk version in admin/package.json", () => {
|
|
36
|
+
const pkg = JSON.parse(
|
|
37
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
|
|
38
|
+
);
|
|
39
|
+
const declared: string = pkg.dependencies["@anthropic-ai/sdk"];
|
|
40
|
+
expect(declared).toMatch(/^\d+\.\d+\.\d+$/);
|
|
41
|
+
expect(readSpecSdkVersion()).toBe(declared);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("Anthropic identity recorded fixtures vs spec", () => {
|
|
46
|
+
it("every recorded request and response conforms to the spec", () => {
|
|
47
|
+
const { problems } = validateRecordedFixturesAgainstSpec(
|
|
48
|
+
loadRecordedFixtures(),
|
|
49
|
+
spec,
|
|
50
|
+
);
|
|
51
|
+
expect(problems).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("covers every operation the client uses", () => {
|
|
55
|
+
const { operations } = validateRecordedFixturesAgainstSpec(
|
|
56
|
+
loadRecordedFixtures(),
|
|
57
|
+
spec,
|
|
58
|
+
);
|
|
59
|
+
const used = new Set(operations.map((o) => `${o.method} ${o.template}`));
|
|
60
|
+
for (const op of [
|
|
61
|
+
"POST /v1/organizations/service_accounts",
|
|
62
|
+
"GET /v1/organizations/service_accounts",
|
|
63
|
+
"POST /v1/organizations/service_accounts/{service_account_id}/archive",
|
|
64
|
+
"POST /v1/organizations/service_accounts/{service_account_id}/workspaces",
|
|
65
|
+
"POST /v1/organizations/federation_rules",
|
|
66
|
+
"GET /v1/organizations/federation_rules",
|
|
67
|
+
"POST /v1/organizations/federation_rules/{federation_rule_id}/archive",
|
|
68
|
+
]) {
|
|
69
|
+
expect(used).toContain(op);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("fails when a recorded fixture drifts from the spec", () => {
|
|
74
|
+
const first = (c: AnthropicRecordedFixtures, scenario: string) =>
|
|
75
|
+
c.scenarios[scenario][0];
|
|
76
|
+
|
|
77
|
+
it("response is missing a required field", () => {
|
|
78
|
+
const c = clone();
|
|
79
|
+
// biome-ignore lint/suspicious/noExplicitAny: mutate recorded JSON
|
|
80
|
+
delete (first(c, "createServiceAccount_success").response.body as any).id;
|
|
81
|
+
expect(
|
|
82
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.join("\n"),
|
|
83
|
+
).toMatch(/required property 'id'/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("response has a wrong type", () => {
|
|
87
|
+
const c = clone();
|
|
88
|
+
// biome-ignore lint/suspicious/noExplicitAny: mutate recorded JSON
|
|
89
|
+
(first(c, "createServiceAccount_success").response.body as any).name = 7;
|
|
90
|
+
expect(
|
|
91
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.length,
|
|
92
|
+
).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("request carries a property the spec does not allow", () => {
|
|
96
|
+
const c = clone();
|
|
97
|
+
// biome-ignore lint/suspicious/noExplicitAny: mutate recorded JSON
|
|
98
|
+
(first(c, "createServiceAccount_success").request.body as any).bogus = 1;
|
|
99
|
+
expect(
|
|
100
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.join("\n"),
|
|
101
|
+
).toMatch(/additional properties/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("request is missing a required property", () => {
|
|
105
|
+
const c = clone();
|
|
106
|
+
// biome-ignore lint/suspicious/noExplicitAny: mutate recorded JSON
|
|
107
|
+
delete (first(c, "createServiceAccount_success").request.body as any)
|
|
108
|
+
.name;
|
|
109
|
+
expect(
|
|
110
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.join("\n"),
|
|
111
|
+
).toMatch(/required property 'name'/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("path is not in the spec", () => {
|
|
115
|
+
const c = clone();
|
|
116
|
+
first(c, "createServiceAccount_success").request.path =
|
|
117
|
+
"/v1/organizations/service_accountz";
|
|
118
|
+
expect(
|
|
119
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.join("\n"),
|
|
120
|
+
).toMatch(/path is not in the spec/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("method is not defined for the path", () => {
|
|
124
|
+
const c = clone();
|
|
125
|
+
first(c, "createServiceAccount_success").request.method = "DELETE";
|
|
126
|
+
expect(
|
|
127
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.join("\n"),
|
|
128
|
+
).toMatch(/method not defined/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("query parameter is unknown", () => {
|
|
132
|
+
const c = clone();
|
|
133
|
+
first(c, "findServiceAccountByName_empty").request.query = {
|
|
134
|
+
limitt: "1",
|
|
135
|
+
};
|
|
136
|
+
expect(
|
|
137
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.join("\n"),
|
|
138
|
+
).toMatch(/query parameter "limitt"/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("query value violates the parameter schema", () => {
|
|
142
|
+
const c = clone();
|
|
143
|
+
first(c, "findServiceAccountByName_empty").request.query = {
|
|
144
|
+
limit: "1000",
|
|
145
|
+
};
|
|
146
|
+
expect(
|
|
147
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.join("\n"),
|
|
148
|
+
).toMatch(/query "limit"/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("status code is not documented", () => {
|
|
152
|
+
const c = clone();
|
|
153
|
+
first(c, "createServiceAccount_success").response.status = 418;
|
|
154
|
+
expect(
|
|
155
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.join("\n"),
|
|
156
|
+
).toMatch(/status 418 is not documented/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("error body does not match ErrorResponse", () => {
|
|
160
|
+
const c = clone();
|
|
161
|
+
first(c, "createServiceAccount_duplicate_409").response.body = {
|
|
162
|
+
message: "nope",
|
|
163
|
+
};
|
|
164
|
+
expect(
|
|
165
|
+
validateRecordedFixturesAgainstSpec(c, spec).problems.length,
|
|
166
|
+
).toBeGreaterThan(0);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|