import * as Axiom from "@distilled.cloud/axiom"; import * as Effect from "effect/Effect"; import * as Redacted from "effect/Redacted"; import { deepEqual, isResolved } from "../Diff.ts"; import * as Provider from "../Provider.ts"; import { Resource } from "../Resource.ts"; import type { Providers } from "./Providers.ts"; export type ApiTokenProps = Omit; export type ApiToken = Resource< "Axiom.ApiToken", ApiTokenProps, Omit & { /** * The bearer token. Returned only by `create` (and `regenerate`); Axiom * does not return it on subsequent reads. Persisted in resource state via * `Redacted` — handle with care. */ token: Redacted.Redacted; }, never, Providers >; /** * An Axiom API token — a scoped bearer token used to authenticate API * requests (ingest, query, admin). Capabilities are pinned at creation time; * changing any field triggers a **replacement** because Axiom does not * expose an update endpoint. * * The raw token value is returned only by `create`. After that, Axiom * never echoes it back, so it is captured into `output.token` (as a * {@link Redacted}) on initial create and persisted in resource state. * Treat resource state as sensitive — anyone with read access can recover * the token. Pair with a secret store for downstream consumption. * @see https://axiom.co/docs/reference/tokens * * ### Creating an API Token * **Example:** Ingest-only token scoped to one dataset * ```typescript * const ingest = yield* Axiom.ApiToken("ingest", { * name: "prod-ingest", * description: "OTEL collector ingest", * datasetCapabilities: { * "my-app-traces": { ingest: ["create"] }, * }, * }); * ``` * * **Example:** Read-only query token * ```typescript * yield* Axiom.ApiToken("query", { * name: "grafana-reader", * datasetCapabilities: { * "my-app-traces": { query: ["read"] }, * "my-app-logs": { query: ["read"] }, * }, * }); * ``` * * ### Consuming the Token * **Example:** Forward the token via Cloudflare Secrets * ```typescript * const secret = yield* Cloudflare.SecretsStore.Secret("axiom-token", { * value: ingest.token, * }); * ``` * * @resource */ export const ApiToken = Resource("Axiom.ApiToken"); export const ApiTokenProvider = () => Provider.effect( ApiToken, Effect.gen(function* () { const create = yield* Axiom.createAPIToken; const get = yield* Axiom.getAPIToken; const del = yield* Axiom.deleteAPIToken; const listTokens = yield* Axiom.getAPITokens; return { stables: ["id", "token"], // Enumerate every API token in the org. Axiom exposes a single // account-wide `GET /v2/tokens` collection op (no pagination), so we // fetch it once and hydrate each row into the exact `read` Attributes // shape. The bearer secret is returned by Axiom only at creation and is // never echoed back on enumeration, so — matching `read`, which sources // the secret from cached state — we surface an empty Redacted token // here rather than the real value. list: () => Effect.gen(function* () { const tokens = yield* listTokens({}); return tokens.map((token) => ({ ...token, token: Redacted.make(""), })); }), diff: Effect.fn(function* ({ olds, news, output }) { if (!isResolved(news)) return undefined; // First create — let the engine create normally. if (output == null) return undefined; // Axiom has no update endpoint for tokens, so any actual change // to the inputs forces a replacement (which mints a new bearer // value). If nothing changed, fall through to `noop` so we don't // needlessly rotate the token on every deploy. if (deepEqual(olds, news)) return undefined; return { action: "replace" } as const; }), reconcile: Effect.fn(function* ({ news, output }) { // Observe — Axiom does not expose an update endpoint for tokens, // so any actual change to the inputs is forced to a replacement // by `diff` above. That means by the time `reconcile` runs we // are either (a) creating fresh, or (b) re-reconciling identical // inputs against an existing token. if (output !== undefined) { // Identical inputs — Axiom does not echo the bearer back, so // the cached `output` (with the persisted Redacted token) is // the authoritative current state. return output; } // Ensure — mint a new token. Axiom returns the bearer exactly // once; capture it into Redacted state for downstream consumers. const result = yield* create(news); if (!result.token) { return yield* Effect.die( new Error("Axiom did not return a token on create"), ); } return { ...result, token: Redacted.make(result.token), }; }), delete: Effect.fn(function* ({ output }) { yield* del({ id: output.id }).pipe( Effect.catchTag("NotFound", () => Effect.void), ); }), read: Effect.fn(function* ({ output }) { if (!output?.id) return undefined; return yield* get({ id: output.id }).pipe( Effect.map((current) => ({ ...current, token: output.token })), Effect.catchTag("NotFound", () => Effect.succeed(undefined)), ); }), }; }), );