1import { DEFAULT_API_BASE_URL } from "@distilled.cloud/axiom/Credentials";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import { AuthError } from "../Auth/AuthProvider.ts";
5import { getEnv, getEnvRedacted } from "../Auth/Env.ts";
6import {
7 makeStoredAuthProvider,
8 storedSecret,
9 storedValueText,
10 type StoredAuthConfig,
11} from "../Auth/StoredAuthProvider.ts";
12
13export const AXIOM_AUTH_PROVIDER_NAME = "Axiom";
14
15export type AxiomAuthConfig = StoredAuthConfig;
16
17/**
18 * Resolved Axiom credentials. The provider values are a flat record
19 * (token + optional orgId/apiBaseUrl); the `apiToken`/`pat` distinction is
20 * derived at resolution time from orgId presence — an org id means the token
21 * is treated as a personal access token, mirroring how the CI environment
22 * resolution has always classified `AXIOM_ORG_ID`.
23 */
24export type AxiomResolvedCredentials =
25 | {
26 type: "apiToken";
27 apiToken: Redacted.Redacted<string>;
28 apiBaseUrl: string;
29 orgId?: string;
30 source: { type: AxiomAuthConfig["method"] | "env"; details?: string };
31 }
32 | {
33 type: "pat";
34 apiToken: Redacted.Redacted<string>;
35 apiBaseUrl: string;
36 orgId: string;
37 source: { type: AxiomAuthConfig["method"] | "env"; details?: string };
38 };
39
40const readEnvironment = Effect.gen(function* () {
41 const apiToken =
42 (yield* getEnvRedacted("AXIOM_TOKEN")) ??
43 (yield* getEnvRedacted("AXIOM_API_KEY"));
44 if (!apiToken) {
45 return yield* new AuthError({
46 message:
47 "Axiom CI credentials not found. Set AXIOM_TOKEN or AXIOM_API_KEY.",
48 });
49 }
50 const apiBaseUrl = (yield* getEnv("AXIOM_URL")) ?? DEFAULT_API_BASE_URL;
51 const orgId = yield* getEnv("AXIOM_ORG_ID");
52 return orgId
53 ? {
54 type: "pat" as const,
55 apiToken,
56 apiBaseUrl,
57 orgId,
58 source: { type: "env" as const },
59 }
60 : {
61 type: "apiToken" as const,
62 apiToken,
63 apiBaseUrl,
64 source: { type: "env" as const },
65 };
66});
67
68const axiomAuth = makeStoredAuthProvider<AxiomResolvedCredentials>({
69 provider: AXIOM_AUTH_PROVIDER_NAME,
70 fields: [
71 {
72 name: "token",
73 label: "Axiom API Token or Personal Access Token",
74 secret: true,
75 },
76 {
77 name: "orgId",
78 label: "Axiom Org ID (required for personal access tokens)",
79 optional: true,
80 },
81 {
82 name: "apiBaseUrl",
83 label: "Axiom API Base URL",
84 optional: true,
85 placeholder: DEFAULT_API_BASE_URL,
86 },
87 ],
88 toResolved: (values, source) => {
89 const apiToken = storedSecret(values.token) ?? Redacted.make("");
90 const apiBaseUrl =
91 storedValueText(values.apiBaseUrl) ?? DEFAULT_API_BASE_URL;
92 return values.orgId !== undefined
93 ? {
94 type: "pat",
95 apiToken,
96 apiBaseUrl,
97 orgId: storedValueText(values.orgId) ?? "",
98 source: { type: source },
99 }
100 : {
101 type: "apiToken",
102 apiToken,
103 apiBaseUrl,
104 source: { type: source },
105 };
106 },
107 readEnvironment,
108 environment: [
109 {
110 name: "AXIOM_TOKEN",
111 required: true,
112 secret: true,
113 alternatives: ["AXIOM_API_KEY"],
114 description: "Personal access token or API token.",
115 },
116 {
117 name: "AXIOM_URL",
118 required: false,
119 description: "API base URL for self-hosted or regional deployments.",
120 },
121 {
122 name: "AXIOM_ORG_ID",
123 required: false,
124 description:
125 "Organization id; required when the token is a personal access token.",
126 },
127 ],
128});
129
130/**
131 * Layer that registers the Axiom {@link AuthProvider} into the
132 * {@link AuthProviders} registry when built. Include this in the Axiom
133 * `providers()` layer so the alchemy CLI can discover it.
134 */
135export const AxiomAuth = axiomAuth.layer;
136
137/** Schema of Axiom's inline static-token values. */
138export const AxiomStoredCredentials = axiomAuth.storedSchema;
139export type AxiomStoredCredentials = typeof AxiomStoredCredentials.Type;