alchemy 2.0.0-beta.18 → 2.0.0-beta.19

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.
Files changed (128) hide show
  1. package/bin/alchemy.js +428 -80
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/commands/_shared.ts +34 -0
  5. package/bin/commands/bootstrap.ts +10 -3
  6. package/bin/commands/deploy.ts +23 -3
  7. package/bin/commands/login.ts +18 -3
  8. package/bin/commands/logs.ts +11 -2
  9. package/bin/commands/profile.ts +11 -5
  10. package/bin/commands/state.ts +21 -13
  11. package/bin/commands/tail.ts +10 -2
  12. package/lib/Agent/lsp/client.d.ts +2 -2
  13. package/lib/Apply.d.ts.map +1 -1
  14. package/lib/Apply.js +41 -9
  15. package/lib/Apply.js.map +1 -1
  16. package/lib/Axiom/Annotation.d.ts +49 -0
  17. package/lib/Axiom/Annotation.d.ts.map +1 -0
  18. package/lib/Axiom/Annotation.js +74 -0
  19. package/lib/Axiom/Annotation.js.map +1 -0
  20. package/lib/Axiom/ApiToken.d.ts +62 -0
  21. package/lib/Axiom/ApiToken.d.ts.map +1 -0
  22. package/lib/Axiom/ApiToken.js +98 -0
  23. package/lib/Axiom/ApiToken.js.map +1 -0
  24. package/lib/Axiom/AuthProvider.d.ts +48 -0
  25. package/lib/Axiom/AuthProvider.d.ts.map +1 -0
  26. package/lib/Axiom/AuthProvider.js +177 -0
  27. package/lib/Axiom/AuthProvider.js.map +1 -0
  28. package/lib/Axiom/Chart.d.ts +72 -0
  29. package/lib/Axiom/Chart.d.ts.map +1 -0
  30. package/lib/Axiom/Chart.js +14 -0
  31. package/lib/Axiom/Chart.js.map +1 -0
  32. package/lib/Axiom/Credentials.d.ts +12 -0
  33. package/lib/Axiom/Credentials.d.ts.map +1 -0
  34. package/lib/Axiom/Credentials.js +34 -0
  35. package/lib/Axiom/Credentials.js.map +1 -0
  36. package/lib/Axiom/Dashboard.d.ts +124 -0
  37. package/lib/Axiom/Dashboard.d.ts.map +1 -0
  38. package/lib/Axiom/Dashboard.js +140 -0
  39. package/lib/Axiom/Dashboard.js.map +1 -0
  40. package/lib/Axiom/Dataset.d.ts +101 -0
  41. package/lib/Axiom/Dataset.d.ts.map +1 -0
  42. package/lib/Axiom/Dataset.js +183 -0
  43. package/lib/Axiom/Dataset.js.map +1 -0
  44. package/lib/Axiom/Monitor.d.ts +74 -0
  45. package/lib/Axiom/Monitor.d.ts.map +1 -0
  46. package/lib/Axiom/Monitor.js +103 -0
  47. package/lib/Axiom/Monitor.js.map +1 -0
  48. package/lib/Axiom/Notifier.d.ts +63 -0
  49. package/lib/Axiom/Notifier.d.ts.map +1 -0
  50. package/lib/Axiom/Notifier.js +83 -0
  51. package/lib/Axiom/Notifier.js.map +1 -0
  52. package/lib/Axiom/Providers.d.ts +15 -0
  53. package/lib/Axiom/Providers.d.ts.map +1 -0
  54. package/lib/Axiom/Providers.js +32 -0
  55. package/lib/Axiom/Providers.js.map +1 -0
  56. package/lib/Axiom/View.d.ts +55 -0
  57. package/lib/Axiom/View.d.ts.map +1 -0
  58. package/lib/Axiom/View.js +79 -0
  59. package/lib/Axiom/View.js.map +1 -0
  60. package/lib/Axiom/VirtualField.d.ts +42 -0
  61. package/lib/Axiom/VirtualField.d.ts.map +1 -0
  62. package/lib/Axiom/VirtualField.js +71 -0
  63. package/lib/Axiom/VirtualField.js.map +1 -0
  64. package/lib/Axiom/index.d.ts +13 -0
  65. package/lib/Axiom/index.d.ts.map +1 -0
  66. package/lib/Axiom/index.js +13 -0
  67. package/lib/Axiom/index.js.map +1 -0
  68. package/lib/Bundle/Bundle.d.ts +20 -2
  69. package/lib/Bundle/Bundle.d.ts.map +1 -1
  70. package/lib/Bundle/Bundle.js +18 -3
  71. package/lib/Bundle/Bundle.js.map +1 -1
  72. package/lib/Bundle/PurePlugin.d.ts +109 -0
  73. package/lib/Bundle/PurePlugin.d.ts.map +1 -0
  74. package/lib/Bundle/PurePlugin.js +469 -0
  75. package/lib/Bundle/PurePlugin.js.map +1 -0
  76. package/lib/Bundle/index.d.ts +1 -0
  77. package/lib/Bundle/index.d.ts.map +1 -1
  78. package/lib/Bundle/index.js +1 -0
  79. package/lib/Bundle/index.js.map +1 -1
  80. package/lib/Cloudflare/Workers/Worker.d.ts +12 -0
  81. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  82. package/lib/Cloudflare/Workers/Worker.js +19 -4
  83. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  84. package/lib/Output.d.ts.map +1 -1
  85. package/lib/Output.js +1 -1
  86. package/lib/Output.js.map +1 -1
  87. package/lib/Plan.d.ts.map +1 -1
  88. package/lib/Plan.js +10 -4
  89. package/lib/Plan.js.map +1 -1
  90. package/lib/Resource.d.ts +2 -2
  91. package/lib/Resource.d.ts.map +1 -1
  92. package/lib/Telemetry/Attributes.d.ts +43 -0
  93. package/lib/Telemetry/Attributes.d.ts.map +1 -0
  94. package/lib/Telemetry/Attributes.js +144 -0
  95. package/lib/Telemetry/Attributes.js.map +1 -0
  96. package/lib/Telemetry/Layer.d.ts +14 -0
  97. package/lib/Telemetry/Layer.d.ts.map +1 -0
  98. package/lib/Telemetry/Layer.js +50 -0
  99. package/lib/Telemetry/Layer.js.map +1 -0
  100. package/lib/Telemetry/Metrics.d.ts +34 -0
  101. package/lib/Telemetry/Metrics.d.ts.map +1 -0
  102. package/lib/Telemetry/Metrics.js +57 -0
  103. package/lib/Telemetry/Metrics.js.map +1 -0
  104. package/package.json +12 -1
  105. package/src/Apply.ts +118 -9
  106. package/src/Axiom/Annotation.ts +95 -0
  107. package/src/Axiom/ApiToken.ts +125 -0
  108. package/src/Axiom/AuthProvider.ts +341 -0
  109. package/src/Axiom/Chart.ts +94 -0
  110. package/src/Axiom/Credentials.ts +70 -0
  111. package/src/Axiom/Dashboard.ts +192 -0
  112. package/src/Axiom/Dataset.ts +291 -0
  113. package/src/Axiom/Monitor.ts +122 -0
  114. package/src/Axiom/Notifier.ts +104 -0
  115. package/src/Axiom/Providers.ts +58 -0
  116. package/src/Axiom/View.ts +106 -0
  117. package/src/Axiom/VirtualField.ts +90 -0
  118. package/src/Axiom/index.ts +12 -0
  119. package/src/Bundle/Bundle.ts +39 -1
  120. package/src/Bundle/PurePlugin.ts +556 -0
  121. package/src/Bundle/index.ts +1 -0
  122. package/src/Cloudflare/Workers/Worker.ts +37 -6
  123. package/src/Output.ts +2 -1
  124. package/src/Plan.ts +13 -4
  125. package/src/Resource.ts +2 -2
  126. package/src/Telemetry/Attributes.ts +216 -0
  127. package/src/Telemetry/Layer.ts +65 -0
  128. package/src/Telemetry/Metrics.ts +101 -0
@@ -0,0 +1,95 @@
1
+ import * as Operations from "@distilled.cloud/axiom/Operations";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Provider from "../Provider.ts";
4
+ import { Resource } from "../Resource.ts";
5
+ import type { Providers } from "./Providers.ts";
6
+
7
+ export type AnnotationProps = Operations.CreateAnnotationInput;
8
+
9
+ export type Annotation = Resource<
10
+ "Axiom.Annotation",
11
+ AnnotationProps,
12
+ Operations.CreateAnnotationOutput,
13
+ never,
14
+ Providers
15
+ >;
16
+
17
+ /**
18
+ * An Axiom annotation — a vertical marker overlaid on charts to flag a
19
+ * deploy, incident, feature flag flip, or any other point/range event you
20
+ * want correlated with telemetry.
21
+ *
22
+ * Annotations are scoped to one or more `datasets`. Use a single `time` for
23
+ * a point marker or `time` + `endTime` for a range. The `type` (e.g.
24
+ * `"deploy"`, `"incident"`) groups markers visually in the UI.
25
+ *
26
+ * Although typically created at deploy/release time (out-of-band of
27
+ * regular IaC), modelling them as resources makes per-environment
28
+ * annotation history reproducible.
29
+ *
30
+ * @see https://axiom.co/docs/query-data/annotate-charts
31
+ *
32
+ * @section Creating an Annotation
33
+ * @example Point-in-time deploy marker
34
+ * ```typescript
35
+ * yield* Axiom.Annotation("deploy-1.2.3", {
36
+ * type: "deploy",
37
+ * title: "Release 1.2.3",
38
+ * description: "https://github.com/acme/app/releases/tag/v1.2.3",
39
+ * datasets: ["my-app-traces", "my-app-logs"],
40
+ * time: new Date().toISOString(),
41
+ * url: "https://github.com/acme/app/releases/tag/v1.2.3",
42
+ * });
43
+ * ```
44
+ *
45
+ * @example Incident time-range
46
+ * ```typescript
47
+ * yield* Axiom.Annotation("inc-2026-04-27", {
48
+ * type: "incident",
49
+ * title: "Database failover",
50
+ * datasets: ["my-app-traces"],
51
+ * time: "2026-04-27T18:05:00Z",
52
+ * endTime: "2026-04-27T18:32:00Z",
53
+ * url: "https://incident.io/incidents/abc123",
54
+ * });
55
+ * ```
56
+ */
57
+ export const Annotation = Resource<Annotation>("Axiom.Annotation");
58
+
59
+ export const AnnotationProvider = () =>
60
+ Provider.effect(
61
+ Annotation,
62
+ Effect.gen(function* () {
63
+ const create = yield* Operations.createAnnotation;
64
+ const update = yield* Operations.updateAnnotation;
65
+ const get = yield* Operations.getAnnotation;
66
+ const del = yield* Operations.deleteAnnotation;
67
+
68
+ return {
69
+ stables: ["id"],
70
+ create: Effect.fn(function* ({ news }) {
71
+ return yield* create(news);
72
+ }),
73
+ update: Effect.fn(function* ({ news, output }) {
74
+ const result = yield* update({ ...news, id: output.id });
75
+ return { ...result, id: output.id, time: result.time ?? output.time };
76
+ }),
77
+ delete: Effect.fn(function* ({ output }) {
78
+ yield* del({ id: output.id }).pipe(
79
+ Effect.catchTag("NotFound", () => Effect.void),
80
+ );
81
+ }),
82
+ read: Effect.fn(function* ({ output }) {
83
+ if (!output?.id) return undefined;
84
+ return yield* get({ id: output.id }).pipe(
85
+ Effect.map((current) => ({
86
+ ...current,
87
+ id: current.id ?? output.id,
88
+ time: current.time ?? output.time,
89
+ })),
90
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
91
+ );
92
+ }),
93
+ };
94
+ }),
95
+ );
@@ -0,0 +1,125 @@
1
+ import * as Operations from "@distilled.cloud/axiom/Operations";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Redacted from "effect/Redacted";
4
+ import { deepEqual, isResolved } from "../Diff.ts";
5
+ import * as Provider from "../Provider.ts";
6
+ import { Resource } from "../Resource.ts";
7
+ import type { Providers } from "./Providers.ts";
8
+
9
+ export type ApiTokenProps = Omit<Operations.CreateAPITokenInput, never>;
10
+
11
+ export type ApiToken = Resource<
12
+ "Axiom.ApiToken",
13
+ ApiTokenProps,
14
+ Omit<Operations.CreateAPITokenOutput, "token"> & {
15
+ /**
16
+ * The bearer token. Returned only by `create` (and `regenerate`); Axiom
17
+ * does not return it on subsequent reads. Persisted in resource state via
18
+ * `Redacted` — handle with care.
19
+ */
20
+ token: Redacted.Redacted<string>;
21
+ },
22
+ never,
23
+ Providers
24
+ >;
25
+
26
+ /**
27
+ * An Axiom API token — a scoped bearer token used to authenticate API
28
+ * requests (ingest, query, admin). Capabilities are pinned at creation time;
29
+ * changing any field triggers a **replacement** because Axiom does not
30
+ * expose an update endpoint.
31
+ *
32
+ * The raw token value is returned only by `create`. After that, Axiom
33
+ * never echoes it back, so it is captured into `output.token` (as a
34
+ * {@link Redacted}) on initial create and persisted in resource state.
35
+ * Treat resource state as sensitive — anyone with read access can recover
36
+ * the token. Pair with a secret store for downstream consumption.
37
+ *
38
+ * @see https://axiom.co/docs/reference/tokens
39
+ *
40
+ * @section Creating an API Token
41
+ * @example Ingest-only token scoped to one dataset
42
+ * ```typescript
43
+ * const ingest = yield* Axiom.ApiToken("ingest", {
44
+ * name: "prod-ingest",
45
+ * description: "OTEL collector ingest",
46
+ * datasetCapabilities: {
47
+ * "my-app-traces": { ingest: ["create"] },
48
+ * },
49
+ * });
50
+ * ```
51
+ *
52
+ * @example Read-only query token
53
+ * ```typescript
54
+ * yield* Axiom.ApiToken("query", {
55
+ * name: "grafana-reader",
56
+ * datasetCapabilities: {
57
+ * "my-app-traces": { query: ["read"] },
58
+ * "my-app-logs": { query: ["read"] },
59
+ * },
60
+ * });
61
+ * ```
62
+ *
63
+ * @section Consuming the Token
64
+ * @example Forward the token via Cloudflare Secrets
65
+ * ```typescript
66
+ * const secret = yield* Cloudflare.Secret("axiom-token", {
67
+ * value: ingest.token,
68
+ * });
69
+ * ```
70
+ */
71
+ export const ApiToken = Resource<ApiToken>("Axiom.ApiToken");
72
+
73
+ export const ApiTokenProvider = () =>
74
+ Provider.effect(
75
+ ApiToken,
76
+ Effect.gen(function* () {
77
+ const create = yield* Operations.createAPIToken;
78
+ const get = yield* Operations.getAPIToken;
79
+ const del = yield* Operations.deleteAPIToken;
80
+
81
+ return {
82
+ stables: ["id", "token"],
83
+ diff: Effect.fn(function* ({ olds, news, output }) {
84
+ if (!isResolved(news)) return undefined;
85
+ // First create — let the engine create normally.
86
+ if (output == null) return undefined;
87
+ // Axiom has no update endpoint for tokens, so any actual change
88
+ // to the inputs forces a replacement (which mints a new bearer
89
+ // value). If nothing changed, fall through to `noop` so we don't
90
+ // needlessly rotate the token on every deploy.
91
+ if (deepEqual(olds, news)) return undefined;
92
+ return { action: "replace" } as const;
93
+ }),
94
+ create: Effect.fn(function* ({ news }) {
95
+ const result = yield* create(news);
96
+ if (!result.token) {
97
+ return yield* Effect.die(
98
+ new Error("Axiom did not return a token on create"),
99
+ );
100
+ }
101
+ return {
102
+ ...result,
103
+ token: Redacted.make(result.token),
104
+ };
105
+ }),
106
+ update: Effect.fn(function* ({ output }) {
107
+ // Diff returns "replace" for every change, so update should never
108
+ // run. If invoked, just return current state unchanged.
109
+ return output;
110
+ }),
111
+ delete: Effect.fn(function* ({ output }) {
112
+ yield* del({ id: output.id }).pipe(
113
+ Effect.catchTag("NotFound", () => Effect.void),
114
+ );
115
+ }),
116
+ read: Effect.fn(function* ({ output }) {
117
+ if (!output?.id) return undefined;
118
+ return yield* get({ id: output.id }).pipe(
119
+ Effect.map((current) => ({ ...current, token: output.token })),
120
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
121
+ );
122
+ }),
123
+ };
124
+ }),
125
+ );
@@ -0,0 +1,341 @@
1
+ import * as Console from "effect/Console";
2
+ import * as Effect from "effect/Effect";
3
+ import * as FileSystem from "effect/FileSystem";
4
+ import * as Match from "effect/Match";
5
+ import * as Redacted from "effect/Redacted";
6
+ import {
7
+ AuthError,
8
+ AuthProviderLayer,
9
+ type ConfigureContext,
10
+ } from "../Auth/AuthProvider.ts";
11
+ import {
12
+ deleteCredentials,
13
+ displayRedacted,
14
+ readCredentials,
15
+ writeCredentials,
16
+ } from "../Auth/Credentials.ts";
17
+ import { getEnv, getEnvRedacted, retryOnce } from "../Auth/Env.ts";
18
+ import * as Clank from "../Util/Clank.ts";
19
+ import { DEFAULT_API_BASE_URL } from "@distilled.cloud/axiom/Credentials";
20
+
21
+ const STORAGE_KEY = "axiom-stored";
22
+
23
+ const options: Array<{
24
+ value: AxiomAuthConfig["method"];
25
+ label: string;
26
+ hint?: string;
27
+ }> = [
28
+ {
29
+ value: "env",
30
+ label: "Environment Variables",
31
+ hint: "AXIOM_TOKEN (or AXIOM_API_KEY) + optional AXIOM_ORG_ID, AXIOM_URL",
32
+ },
33
+ {
34
+ value: "stored",
35
+ label: "API Token or Personal Access Token",
36
+ hint: "enter credentials interactively, stored in ~/.alchemy/credentials",
37
+ },
38
+ ];
39
+
40
+ export type AxiomAuthConfig =
41
+ | { method: "env" }
42
+ | { method: "stored"; credentialType: "apiToken" }
43
+ | { method: "stored"; credentialType: "pat" };
44
+
45
+ export type AxiomStoredCredentials =
46
+ | { type: "apiToken"; apiToken: string; apiBaseUrl?: string; orgId?: string }
47
+ | { type: "pat"; apiToken: string; apiBaseUrl?: string; orgId: string };
48
+
49
+ export type AxiomResolvedCredentials =
50
+ | {
51
+ type: "apiToken";
52
+ apiToken: Redacted.Redacted<string>;
53
+ apiBaseUrl: string;
54
+ orgId?: string;
55
+ source: { type: AxiomAuthConfig["method"]; details?: string };
56
+ }
57
+ | {
58
+ type: "pat";
59
+ apiToken: Redacted.Redacted<string>;
60
+ apiBaseUrl: string;
61
+ orgId: string;
62
+ source: { type: AxiomAuthConfig["method"]; details?: string };
63
+ };
64
+
65
+ export const AXIOM_AUTH_PROVIDER_NAME = "Axiom";
66
+
67
+ /**
68
+ * Layer that registers the Axiom {@link AuthProvider} into the
69
+ * {@link AuthProviders} registry when built. Include this in the Axiom
70
+ * `providers()` layer so `alchemy login` can discover it.
71
+ */
72
+ export const AxiomAuth = AuthProviderLayer<
73
+ AxiomAuthConfig,
74
+ AxiomResolvedCredentials
75
+ >()(AXIOM_AUTH_PROVIDER_NAME, {
76
+ configure: (profileName, ctx) => configureCredentials(profileName, ctx),
77
+ logout: (profileName, config) => logout(profileName, config),
78
+ login: (profileName, config) => login(profileName, config),
79
+ prettyPrint: (profileName, config) => prettyPrint(profileName, config),
80
+ read: (profileName, config) => resolveCredentials(profileName, config),
81
+ });
82
+
83
+ const resolveCredentials = (
84
+ profileName: string,
85
+ config: AxiomAuthConfig,
86
+ ): Effect.Effect<
87
+ AxiomResolvedCredentials,
88
+ AuthError,
89
+ FileSystem.FileSystem
90
+ > =>
91
+ Match.value(config).pipe(
92
+ Match.when(
93
+ { method: "env" },
94
+ Effect.fnUntraced(function* () {
95
+ const apiToken =
96
+ (yield* getEnvRedacted("AXIOM_TOKEN")) ??
97
+ (yield* getEnvRedacted("AXIOM_API_KEY"));
98
+ if (!apiToken) {
99
+ return yield* new AuthError({
100
+ message:
101
+ "Axiom env credentials not found. Set AXIOM_TOKEN (or AXIOM_API_KEY).",
102
+ });
103
+ }
104
+ const apiBaseUrl =
105
+ (yield* getEnv("AXIOM_URL")) ?? DEFAULT_API_BASE_URL;
106
+ const orgId = yield* getEnv("AXIOM_ORG_ID");
107
+ if (orgId) {
108
+ return {
109
+ type: "pat" as const,
110
+ apiToken,
111
+ apiBaseUrl,
112
+ orgId,
113
+ source: { type: "env" as const },
114
+ };
115
+ }
116
+ return {
117
+ type: "apiToken" as const,
118
+ apiToken,
119
+ apiBaseUrl,
120
+ source: { type: "env" as const },
121
+ };
122
+ }),
123
+ ),
124
+ Match.when({ method: "stored" }, () =>
125
+ readCredentials<AxiomStoredCredentials>(profileName, STORAGE_KEY).pipe(
126
+ Effect.flatMap((creds) =>
127
+ creds == null
128
+ ? Effect.fail(
129
+ new AuthError({
130
+ message:
131
+ "Axiom stored credentials not found. Run: alchemy login --configure",
132
+ }),
133
+ )
134
+ : Effect.succeed(
135
+ Match.value(creds).pipe(
136
+ Match.when({ type: "apiToken" }, (c) => ({
137
+ type: "apiToken" as const,
138
+ apiToken: Redacted.make(c.apiToken),
139
+ apiBaseUrl: c.apiBaseUrl ?? DEFAULT_API_BASE_URL,
140
+ orgId: c.orgId,
141
+ source: { type: "stored" as const },
142
+ })),
143
+ Match.when({ type: "pat" }, (c) => ({
144
+ type: "pat" as const,
145
+ apiToken: Redacted.make(c.apiToken),
146
+ apiBaseUrl: c.apiBaseUrl ?? DEFAULT_API_BASE_URL,
147
+ orgId: c.orgId,
148
+ source: { type: "stored" as const },
149
+ })),
150
+ Match.exhaustive,
151
+ ),
152
+ ),
153
+ ),
154
+ ),
155
+ ),
156
+ Match.exhaustive,
157
+ );
158
+
159
+ const logout = (profileName: string, config: AxiomAuthConfig) =>
160
+ Match.value(config).pipe(
161
+ Match.when({ method: "env" }, () => Effect.void),
162
+ Match.when({ method: "stored" }, () =>
163
+ deleteCredentials(profileName, STORAGE_KEY).pipe(
164
+ Effect.andThen(Clank.success("Axiom: stored credentials removed")),
165
+ ),
166
+ ),
167
+ Match.exhaustive,
168
+ );
169
+
170
+ const login = (profileName: string, config: AxiomAuthConfig) =>
171
+ Match.value(config)
172
+ .pipe(
173
+ Match.when({ method: "env" }, () => Effect.void),
174
+ Match.when({ method: "stored" }, () =>
175
+ readCredentials<AxiomStoredCredentials>(profileName, STORAGE_KEY).pipe(
176
+ Effect.flatMap((creds) =>
177
+ creds == null ? loginStored(profileName) : Effect.void,
178
+ ),
179
+ ),
180
+ ),
181
+ Match.exhaustive,
182
+ )
183
+ .pipe(
184
+ Effect.mapError(
185
+ (e) => new AuthError({ message: "login failed", cause: e }),
186
+ ),
187
+ );
188
+
189
+ const configureCredentials = (profileName: string, ctx: ConfigureContext) =>
190
+ Effect.gen(function* () {
191
+ if (ctx.ci) {
192
+ return { method: "env" as const };
193
+ }
194
+ return yield* configureInteractive(profileName);
195
+ }).pipe(
196
+ Effect.mapError(
197
+ (e) =>
198
+ new AuthError({
199
+ message: "failed to configure credentials",
200
+ cause: e,
201
+ }),
202
+ ),
203
+ );
204
+
205
+ const configureInteractive = (profileName: string) =>
206
+ Clank.select({
207
+ message: "Axiom authentication method",
208
+ options,
209
+ }).pipe(
210
+ Effect.flatMap((method) =>
211
+ Match.value(method).pipe(
212
+ Match.when("env", () => Effect.succeed({ method: "env" as const })),
213
+ Match.when("stored", () => loginStored(profileName)),
214
+ Match.exhaustive,
215
+ ),
216
+ ),
217
+ );
218
+
219
+ const loginStored = Effect.fnUntraced(function* (profileName: string) {
220
+ const credentialType = yield* Clank.select({
221
+ message: "Axiom credential type",
222
+ options: [
223
+ {
224
+ value: "apiToken" as const,
225
+ label: "API Token",
226
+ hint: "recommended — scoped to specific datasets/actions",
227
+ },
228
+ {
229
+ value: "pat" as const,
230
+ label: "Personal Access Token",
231
+ hint: "full account control, requires Org ID",
232
+ },
233
+ ],
234
+ }).pipe(retryOnce);
235
+
236
+ const apiBaseUrlPrompt = yield* Clank.text({
237
+ message: "Axiom API Base URL (Enter for default)",
238
+ placeholder: DEFAULT_API_BASE_URL,
239
+ defaultValue: DEFAULT_API_BASE_URL,
240
+ }).pipe(retryOnce);
241
+ const apiBaseUrl =
242
+ apiBaseUrlPrompt && apiBaseUrlPrompt.length > 0
243
+ ? apiBaseUrlPrompt
244
+ : undefined;
245
+
246
+ return yield* Match.value(credentialType).pipe(
247
+ Match.when("apiToken", () =>
248
+ Effect.gen(function* () {
249
+ const apiToken = yield* Clank.password({
250
+ message: "Axiom API Token",
251
+ validate: (v) => (v.length === 0 ? "Required" : undefined),
252
+ }).pipe(retryOnce);
253
+ const orgId = yield* promptOrgId(false);
254
+
255
+ yield* writeCredentials<AxiomStoredCredentials>(
256
+ profileName,
257
+ STORAGE_KEY,
258
+ {
259
+ type: "apiToken",
260
+ apiToken,
261
+ apiBaseUrl,
262
+ orgId: orgId && orgId.length > 0 ? orgId : undefined,
263
+ },
264
+ );
265
+ yield* Clank.success("Axiom: credentials saved.");
266
+ return {
267
+ method: "stored" as const,
268
+ credentialType: "apiToken" as const,
269
+ };
270
+ }),
271
+ ),
272
+ Match.when("pat", () =>
273
+ Effect.gen(function* () {
274
+ const apiToken = yield* Clank.password({
275
+ message: "Axiom Personal Access Token",
276
+ validate: (v) => (v.length === 0 ? "Required" : undefined),
277
+ }).pipe(retryOnce);
278
+ const orgId = yield* promptOrgId(true);
279
+
280
+ yield* writeCredentials<AxiomStoredCredentials>(
281
+ profileName,
282
+ STORAGE_KEY,
283
+ { type: "pat", apiToken, apiBaseUrl, orgId },
284
+ );
285
+ yield* Clank.success("Axiom: credentials saved.");
286
+ return {
287
+ method: "stored" as const,
288
+ credentialType: "pat" as const,
289
+ };
290
+ }),
291
+ ),
292
+ Match.exhaustive,
293
+ );
294
+ });
295
+
296
+ const promptOrgId = (required: boolean) =>
297
+ getEnv("AXIOM_ORG_ID").pipe(
298
+ Effect.flatMap((envOrgId) =>
299
+ Clank.text({
300
+ message: required
301
+ ? "Axiom Org ID (required for PAT)"
302
+ : "Axiom Org ID (Enter to skip)",
303
+ placeholder: envOrgId ?? "",
304
+ defaultValue: envOrgId ?? "",
305
+ validate: required
306
+ ? (v) => (v.length === 0 ? "Required" : undefined)
307
+ : undefined,
308
+ }).pipe(retryOnce),
309
+ ),
310
+ );
311
+
312
+ const prettyPrint = (profileName: string, config: AxiomAuthConfig) =>
313
+ resolveCredentials(profileName, config).pipe(
314
+ Effect.tap((creds) => {
315
+ const sourceStr = creds.source.details
316
+ ? `${creds.source.type} - ${creds.source.details}`
317
+ : creds.source.type;
318
+ return Match.value(creds).pipe(
319
+ Match.when({ type: "apiToken" }, (c) =>
320
+ Effect.all([
321
+ Console.log(` apiToken: ${displayRedacted(c.apiToken, 9)}`),
322
+ Console.log(` apiBaseUrl: ${c.apiBaseUrl}`),
323
+ Console.log(` orgId: ${c.orgId ?? "(none)"}`),
324
+ Console.log(` source: ${sourceStr}`),
325
+ ]),
326
+ ),
327
+ Match.when({ type: "pat" }, (c) =>
328
+ Effect.all([
329
+ Console.log(` apiToken: ${displayRedacted(c.apiToken, 9)}`),
330
+ Console.log(` apiBaseUrl: ${c.apiBaseUrl}`),
331
+ Console.log(` orgId: ${c.orgId}`),
332
+ Console.log(` source: ${sourceStr}`),
333
+ ]),
334
+ ),
335
+ Match.exhaustive,
336
+ );
337
+ }),
338
+ Effect.catch((e) =>
339
+ Console.error(` Failed to retrieve credentials: ${e}`),
340
+ ),
341
+ );
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Typed view of the Axiom dashboard chart-JSON subset.
3
+ *
4
+ * Axiom's `CreateDashboardInput` declares
5
+ * `dashboard.charts: ReadonlyArray<unknown>` (see
6
+ * `@distilled.cloud/axiom/.../v2/createDashboard.ts`); these
7
+ * interfaces give callers compile-time safety when building the
8
+ * payload by hand.
9
+ *
10
+ * `chart.id` is a free-form string the author picks — Axiom
11
+ * doesn't validate the format. It joins to the matching
12
+ * `LayoutCell.i` in `dashboard.layout`.
13
+ */
14
+
15
+ /**
16
+ * Axiom-validated chart kinds. Discovered by probing
17
+ * `POST /v2/dashboards`: anything else returns
18
+ * `dashboard validation failed at [charts N type]: Invalid input`.
19
+ */
20
+ export type ChartKind =
21
+ | "TimeSeries"
22
+ | "Table"
23
+ | "Pie"
24
+ | "Statistic"
25
+ | "Heatmap"
26
+ | "LogStream"
27
+ | "Note";
28
+
29
+ /**
30
+ * Minimum chart payload Axiom's `POST /v2/dashboards` accepts.
31
+ * Any unknown keys (e.g. `dataset`, `description`) trigger
32
+ * `Unrecognized keys: "<name>"`, so this struct is intentionally
33
+ * narrow. The dataset is implicit via the APL query.
34
+ */
35
+ export interface BaseChart {
36
+ readonly id: string;
37
+ readonly name: string;
38
+ readonly type: ChartKind;
39
+ readonly query: { readonly apl: string };
40
+ }
41
+
42
+ export interface TimeSeriesChart extends BaseChart {
43
+ readonly type: "TimeSeries";
44
+ }
45
+
46
+ export interface TableChart extends BaseChart {
47
+ readonly type: "Table";
48
+ }
49
+
50
+ export interface PieChart extends BaseChart {
51
+ readonly type: "Pie";
52
+ }
53
+
54
+ export interface StatisticChart extends BaseChart {
55
+ readonly type: "Statistic";
56
+ }
57
+
58
+ export interface HeatmapChart extends BaseChart {
59
+ readonly type: "Heatmap";
60
+ }
61
+
62
+ export interface LogStreamChart extends BaseChart {
63
+ readonly type: "LogStream";
64
+ }
65
+
66
+ export interface NoteChart extends BaseChart {
67
+ readonly type: "Note";
68
+ }
69
+
70
+ export type Chart =
71
+ | TimeSeriesChart
72
+ | TableChart
73
+ | PieChart
74
+ | StatisticChart
75
+ | HeatmapChart
76
+ | LogStreamChart
77
+ | NoteChart;
78
+
79
+ /**
80
+ * Mirrors Axiom's `CreateDashboardInput.dashboard.layout` cell schema.
81
+ * `i` references a chart by its `id`.
82
+ */
83
+ export interface LayoutCell {
84
+ readonly i: string;
85
+ readonly x: number;
86
+ readonly y: number | null;
87
+ readonly w: number;
88
+ readonly h: number;
89
+ readonly minW?: number;
90
+ readonly minH?: number;
91
+ readonly maxW?: number;
92
+ readonly maxH?: number;
93
+ readonly static?: boolean;
94
+ }