@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
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/okta-auth-client.integration.test.ts
|
|
3
|
+
* Integration tests for HttpOktaAuthClient against recorded Okta OIDC
|
|
4
|
+
* API fixtures.
|
|
5
|
+
*
|
|
6
|
+
* Drives the client through an INJECTED fetchFn that replays canned Responses
|
|
7
|
+
* from a cassette keyed by scenario — no live API server, no global.fetch
|
|
8
|
+
* override, no mock.module(). Mirrors the pattern in
|
|
9
|
+
* google-auth-client.integration.test.ts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "bun:test";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { HttpOktaAuthClient } from "./okta-auth-client.ts";
|
|
15
|
+
|
|
16
|
+
// ─── Cassette ───────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
interface CassetteEntry {
|
|
19
|
+
status: number;
|
|
20
|
+
body: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CASSETTE_PATH = new URL(
|
|
24
|
+
"./fixtures/okta-auth-cassette.json",
|
|
25
|
+
import.meta.url,
|
|
26
|
+
).pathname;
|
|
27
|
+
|
|
28
|
+
const cassette: Record<string, CassetteEntry> = JSON.parse(
|
|
29
|
+
readFileSync(CASSETTE_PATH, "utf-8"),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
interface RecordedRequest {
|
|
33
|
+
url: string;
|
|
34
|
+
method: string;
|
|
35
|
+
headers: Headers;
|
|
36
|
+
body?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build an injected fetchFn that returns the cassette entry for `key`.
|
|
41
|
+
* Records the last request so tests can assert URL/method/headers/body.
|
|
42
|
+
*/
|
|
43
|
+
function cassetteFetch(key: string): {
|
|
44
|
+
fetchFn: typeof fetch;
|
|
45
|
+
lastRequest: () => RecordedRequest;
|
|
46
|
+
} {
|
|
47
|
+
let last: RecordedRequest | undefined;
|
|
48
|
+
const entry = cassette[key];
|
|
49
|
+
if (!entry) throw new Error(`cassette key not found: ${key}`);
|
|
50
|
+
|
|
51
|
+
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
52
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
53
|
+
last = {
|
|
54
|
+
url,
|
|
55
|
+
method: init?.method ?? "GET",
|
|
56
|
+
headers: new Headers(init?.headers),
|
|
57
|
+
body:
|
|
58
|
+
typeof init?.body === "string"
|
|
59
|
+
? init.body
|
|
60
|
+
: init?.body instanceof URLSearchParams
|
|
61
|
+
? init.body.toString()
|
|
62
|
+
: undefined,
|
|
63
|
+
};
|
|
64
|
+
// A string body is replayed verbatim (e.g. a raw error payload) so the
|
|
65
|
+
// client sees genuinely non-JSON-object bytes when relevant; here all
|
|
66
|
+
// cassette bodies are either objects (JSON-encoded) or raw strings.
|
|
67
|
+
const isRaw = typeof entry.body === "string";
|
|
68
|
+
return new Response(
|
|
69
|
+
isRaw ? (entry.body as string) : JSON.stringify(entry.body),
|
|
70
|
+
{
|
|
71
|
+
status: entry.status,
|
|
72
|
+
headers: { "Content-Type": isRaw ? "text/plain" : "application/json" },
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
}) as typeof fetch;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
fetchFn,
|
|
79
|
+
lastRequest: () => {
|
|
80
|
+
if (!last) throw new Error("fetchFn was not called");
|
|
81
|
+
return last;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const TEST_ISSUER = "https://example.okta.com/oauth2/default";
|
|
87
|
+
|
|
88
|
+
function makeClient(
|
|
89
|
+
key: string,
|
|
90
|
+
issuer: string = TEST_ISSUER,
|
|
91
|
+
): {
|
|
92
|
+
client: HttpOktaAuthClient;
|
|
93
|
+
lastRequest: () => RecordedRequest;
|
|
94
|
+
} {
|
|
95
|
+
const { fetchFn, lastRequest } = cassetteFetch(key);
|
|
96
|
+
const client = new HttpOktaAuthClient({ issuer, fetchFn });
|
|
97
|
+
return { client, lastRequest };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── getAuthorizationUrl ─────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
describe("HttpOktaAuthClient — getAuthorizationUrl", () => {
|
|
103
|
+
it("builds the authorize URL with standard OIDC query params (pure, no network call)", () => {
|
|
104
|
+
const client = new HttpOktaAuthClient({ issuer: TEST_ISSUER });
|
|
105
|
+
const url = client.getAuthorizationUrl({
|
|
106
|
+
clientId: "client-id-abc",
|
|
107
|
+
redirectUri: "https://example.com/callback",
|
|
108
|
+
state: "state-xyz",
|
|
109
|
+
scope: "openid profile email",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const parsed = new URL(url);
|
|
113
|
+
expect(`${parsed.origin}${parsed.pathname}`).toBe(
|
|
114
|
+
`${TEST_ISSUER}/v1/authorize`,
|
|
115
|
+
);
|
|
116
|
+
expect(parsed.searchParams.get("client_id")).toBe("client-id-abc");
|
|
117
|
+
expect(parsed.searchParams.get("redirect_uri")).toBe(
|
|
118
|
+
"https://example.com/callback",
|
|
119
|
+
);
|
|
120
|
+
expect(parsed.searchParams.get("response_type")).toBe("code");
|
|
121
|
+
expect(parsed.searchParams.get("scope")).toBe("openid profile email");
|
|
122
|
+
expect(parsed.searchParams.get("state")).toBe("state-xyz");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("defaults scope to 'openid profile email' when not provided", () => {
|
|
126
|
+
const client = new HttpOktaAuthClient({ issuer: TEST_ISSUER });
|
|
127
|
+
const url = client.getAuthorizationUrl({
|
|
128
|
+
clientId: "client-id-abc",
|
|
129
|
+
redirectUri: "https://example.com/callback",
|
|
130
|
+
state: "state-xyz",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const parsed = new URL(url);
|
|
134
|
+
expect(parsed.searchParams.get("scope")).toBe("openid profile email");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("normalizes an issuer with a trailing slash to avoid a double slash", () => {
|
|
138
|
+
const client = new HttpOktaAuthClient({
|
|
139
|
+
issuer: "https://example.okta.com/oauth2/default/",
|
|
140
|
+
});
|
|
141
|
+
const url = client.getAuthorizationUrl({
|
|
142
|
+
clientId: "client-id-abc",
|
|
143
|
+
redirectUri: "https://example.com/callback",
|
|
144
|
+
state: "state-xyz",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(url.startsWith(`${TEST_ISSUER}/v1/authorize?`)).toBe(true);
|
|
148
|
+
expect(url.includes("//v1/authorize")).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─── exchangeCode ────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe("HttpOktaAuthClient — exchangeCode", () => {
|
|
155
|
+
it("POSTs form-encoded params to the Okta token endpoint and maps the response", async () => {
|
|
156
|
+
const { client, lastRequest } = makeClient("exchangeCode_success");
|
|
157
|
+
const result = await client.exchangeCode({
|
|
158
|
+
code: "auth-code-123",
|
|
159
|
+
clientId: "client-id-abc",
|
|
160
|
+
clientSecret: "client-secret-xyz",
|
|
161
|
+
redirectUri: "https://example.com/callback",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const req = lastRequest();
|
|
165
|
+
expect(req.method).toBe("POST");
|
|
166
|
+
expect(req.url).toBe(`${TEST_ISSUER}/v1/token`);
|
|
167
|
+
expect(req.headers.get("content-type")).toBe(
|
|
168
|
+
"application/x-www-form-urlencoded",
|
|
169
|
+
);
|
|
170
|
+
const params = new URLSearchParams(req.body);
|
|
171
|
+
expect(params.get("code")).toBe("auth-code-123");
|
|
172
|
+
expect(params.get("client_id")).toBe("client-id-abc");
|
|
173
|
+
expect(params.get("client_secret")).toBe("client-secret-xyz");
|
|
174
|
+
expect(params.get("redirect_uri")).toBe("https://example.com/callback");
|
|
175
|
+
expect(params.get("grant_type")).toBe("authorization_code");
|
|
176
|
+
|
|
177
|
+
expect(result).toEqual({
|
|
178
|
+
accessToken: "okta.test-access-token",
|
|
179
|
+
refreshToken: "okta.test-refresh-token",
|
|
180
|
+
idToken: "okta.test-id-token-jwt",
|
|
181
|
+
expiresIn: 3600,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("maps a response missing optional fields (no refresh/id token)", async () => {
|
|
186
|
+
const { client } = makeClient("exchangeCode_success_no_optional_fields");
|
|
187
|
+
const result = await client.exchangeCode({
|
|
188
|
+
code: "auth-code-123",
|
|
189
|
+
clientId: "client-id-abc",
|
|
190
|
+
clientSecret: "client-secret-xyz",
|
|
191
|
+
redirectUri: "https://example.com/callback",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(result.accessToken).toBe("okta.test-access-token-minimal");
|
|
195
|
+
expect(result.refreshToken).toBeUndefined();
|
|
196
|
+
expect(result.idToken).toBeUndefined();
|
|
197
|
+
expect(result.expiresIn).toBe(3600);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("throws on a non-ok response, including the status and body text", async () => {
|
|
201
|
+
const { client } = makeClient("exchangeCode_400");
|
|
202
|
+
await expect(
|
|
203
|
+
client.exchangeCode({
|
|
204
|
+
code: "bad-code",
|
|
205
|
+
clientId: "client-id-abc",
|
|
206
|
+
clientSecret: "client-secret-xyz",
|
|
207
|
+
redirectUri: "https://example.com/callback",
|
|
208
|
+
}),
|
|
209
|
+
).rejects.toThrow(/Token exchange failed: 400/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("normalizes an issuer with a trailing slash for the token endpoint", async () => {
|
|
213
|
+
const { client, lastRequest } = makeClient(
|
|
214
|
+
"exchangeCode_success",
|
|
215
|
+
"https://example.okta.com/oauth2/default/",
|
|
216
|
+
);
|
|
217
|
+
await client.exchangeCode({
|
|
218
|
+
code: "auth-code-123",
|
|
219
|
+
clientId: "client-id-abc",
|
|
220
|
+
clientSecret: "client-secret-xyz",
|
|
221
|
+
redirectUri: "https://example.com/callback",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(lastRequest().url).toBe(`${TEST_ISSUER}/v1/token`);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ─── getUserInfo ─────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
describe("HttpOktaAuthClient — getUserInfo", () => {
|
|
231
|
+
it("GETs the userinfo endpoint with a Bearer token and returns the profile", async () => {
|
|
232
|
+
const { client, lastRequest } = makeClient("getUserInfo_success");
|
|
233
|
+
const result = await client.getUserInfo("test-access-token");
|
|
234
|
+
|
|
235
|
+
const req = lastRequest();
|
|
236
|
+
expect(req.method).toBe("GET");
|
|
237
|
+
expect(req.url).toBe(`${TEST_ISSUER}/v1/userinfo`);
|
|
238
|
+
expect(req.headers.get("authorization")).toBe("Bearer test-access-token");
|
|
239
|
+
|
|
240
|
+
expect(result).toEqual({
|
|
241
|
+
sub: "00u1a2b3c4d5e6f7g8h9",
|
|
242
|
+
email: "user@example.com",
|
|
243
|
+
email_verified: true,
|
|
244
|
+
name: "Test User",
|
|
245
|
+
picture: "https://example.com/photo.jpg",
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("throws on a non-ok response", async () => {
|
|
250
|
+
const { client } = makeClient("getUserInfo_401");
|
|
251
|
+
await expect(client.getUserInfo("bad-token")).rejects.toThrow(
|
|
252
|
+
/Userinfo fetch failed: 401/,
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ─── Default construction ───────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
describe("HttpOktaAuthClient — default construction", () => {
|
|
260
|
+
it("can be constructed with just an issuer (defaults fetchFn to global fetch, no network call)", () => {
|
|
261
|
+
expect(() => new HttpOktaAuthClient({ issuer: TEST_ISSUER })).not.toThrow();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/okta-auth-client.ts
|
|
3
|
+
* Typed client for Okta OIDC authorization, token exchange, and user profile
|
|
4
|
+
* lookup.
|
|
5
|
+
*
|
|
6
|
+
* Interface + HTTP implementation following the project's client DI pattern
|
|
7
|
+
* (mirrors google-auth-client.ts). Endpoints are derived from a
|
|
8
|
+
* caller-supplied issuer URL using Okta's well-known endpoint conventions
|
|
9
|
+
* ({issuer}/v1/authorize, /v1/token, /v1/userinfo) — no live
|
|
10
|
+
* `Issuer.discover()` `.well-known` network call happens, so construction is
|
|
11
|
+
* fully synchronous and side-effect free. Tests inject a mock fetchFn;
|
|
12
|
+
* production uses HttpOktaAuthClient with the real global fetch.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface OktaTokenResponse {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
refreshToken?: string;
|
|
20
|
+
idToken?: string;
|
|
21
|
+
expiresIn: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OktaUserInfo {
|
|
25
|
+
sub: string;
|
|
26
|
+
email?: string;
|
|
27
|
+
email_verified?: boolean;
|
|
28
|
+
name: string;
|
|
29
|
+
picture?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Interface ──────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export interface OktaAuthClient {
|
|
35
|
+
getAuthorizationUrl(params: {
|
|
36
|
+
clientId: string;
|
|
37
|
+
redirectUri: string;
|
|
38
|
+
state: string;
|
|
39
|
+
scope?: string;
|
|
40
|
+
}): string;
|
|
41
|
+
|
|
42
|
+
exchangeCode(params: {
|
|
43
|
+
code: string;
|
|
44
|
+
clientId: string;
|
|
45
|
+
clientSecret: string;
|
|
46
|
+
redirectUri: string;
|
|
47
|
+
}): Promise<OktaTokenResponse>;
|
|
48
|
+
|
|
49
|
+
getUserInfo(accessToken: string): Promise<OktaUserInfo>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Error ──────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
class OktaAuthClientError extends Error {
|
|
55
|
+
constructor(
|
|
56
|
+
message: string,
|
|
57
|
+
public readonly statusCode?: number,
|
|
58
|
+
) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "OktaAuthClientError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── HTTP implementation ────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const DEFAULT_SCOPE = "openid profile email";
|
|
67
|
+
|
|
68
|
+
export class HttpOktaAuthClient implements OktaAuthClient {
|
|
69
|
+
private readonly issuer: string;
|
|
70
|
+
private readonly fetchFn: typeof fetch;
|
|
71
|
+
|
|
72
|
+
constructor(opts: { issuer: string; fetchFn?: typeof fetch }) {
|
|
73
|
+
// Normalize away a trailing slash so `${issuer}/v1/...` never produces a
|
|
74
|
+
// double slash, regardless of how the issuer was configured.
|
|
75
|
+
this.issuer = opts.issuer.replace(/\/+$/, "");
|
|
76
|
+
this.fetchFn = opts.fetchFn ?? fetch;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getAuthorizationUrl(params: {
|
|
80
|
+
clientId: string;
|
|
81
|
+
redirectUri: string;
|
|
82
|
+
state: string;
|
|
83
|
+
scope?: string;
|
|
84
|
+
}): string {
|
|
85
|
+
const query = new URLSearchParams({
|
|
86
|
+
client_id: params.clientId,
|
|
87
|
+
redirect_uri: params.redirectUri,
|
|
88
|
+
response_type: "code",
|
|
89
|
+
scope: params.scope ?? DEFAULT_SCOPE,
|
|
90
|
+
state: params.state,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return `${this.issuer}/v1/authorize?${query.toString()}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async exchangeCode(params: {
|
|
97
|
+
code: string;
|
|
98
|
+
clientId: string;
|
|
99
|
+
clientSecret: string;
|
|
100
|
+
redirectUri: string;
|
|
101
|
+
}): Promise<OktaTokenResponse> {
|
|
102
|
+
const res = await this.fetchFn(`${this.issuer}/v1/token`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
105
|
+
body: new URLSearchParams({
|
|
106
|
+
code: params.code,
|
|
107
|
+
client_id: params.clientId,
|
|
108
|
+
client_secret: params.clientSecret,
|
|
109
|
+
redirect_uri: params.redirectUri,
|
|
110
|
+
grant_type: "authorization_code",
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const body = await res.text();
|
|
116
|
+
throw new OktaAuthClientError(
|
|
117
|
+
`Token exchange failed: ${res.status} ${body}`,
|
|
118
|
+
res.status,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = (await res.json()) as {
|
|
123
|
+
access_token: string;
|
|
124
|
+
refresh_token?: string;
|
|
125
|
+
id_token?: string;
|
|
126
|
+
expires_in: number;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
accessToken: data.access_token,
|
|
131
|
+
refreshToken: data.refresh_token,
|
|
132
|
+
idToken: data.id_token,
|
|
133
|
+
expiresIn: data.expires_in,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getUserInfo(accessToken: string): Promise<OktaUserInfo> {
|
|
138
|
+
const res = await this.fetchFn(`${this.issuer}/v1/userinfo`, {
|
|
139
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
const body = await res.text();
|
|
144
|
+
throw new OktaAuthClientError(
|
|
145
|
+
`Userinfo fetch failed: ${res.status} ${body}`,
|
|
146
|
+
res.status,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (await res.json()) as OktaUserInfo;
|
|
151
|
+
}
|
|
152
|
+
}
|