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.
- package/bin/alchemy.js +428 -80
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +2 -0
- package/bin/commands/_shared.ts +34 -0
- package/bin/commands/bootstrap.ts +10 -3
- package/bin/commands/deploy.ts +23 -3
- package/bin/commands/login.ts +18 -3
- package/bin/commands/logs.ts +11 -2
- package/bin/commands/profile.ts +11 -5
- package/bin/commands/state.ts +21 -13
- package/bin/commands/tail.ts +10 -2
- package/lib/Agent/lsp/client.d.ts +2 -2
- package/lib/Apply.d.ts.map +1 -1
- package/lib/Apply.js +41 -9
- package/lib/Apply.js.map +1 -1
- package/lib/Axiom/Annotation.d.ts +49 -0
- package/lib/Axiom/Annotation.d.ts.map +1 -0
- package/lib/Axiom/Annotation.js +74 -0
- package/lib/Axiom/Annotation.js.map +1 -0
- package/lib/Axiom/ApiToken.d.ts +62 -0
- package/lib/Axiom/ApiToken.d.ts.map +1 -0
- package/lib/Axiom/ApiToken.js +98 -0
- package/lib/Axiom/ApiToken.js.map +1 -0
- package/lib/Axiom/AuthProvider.d.ts +48 -0
- package/lib/Axiom/AuthProvider.d.ts.map +1 -0
- package/lib/Axiom/AuthProvider.js +177 -0
- package/lib/Axiom/AuthProvider.js.map +1 -0
- package/lib/Axiom/Chart.d.ts +72 -0
- package/lib/Axiom/Chart.d.ts.map +1 -0
- package/lib/Axiom/Chart.js +14 -0
- package/lib/Axiom/Chart.js.map +1 -0
- package/lib/Axiom/Credentials.d.ts +12 -0
- package/lib/Axiom/Credentials.d.ts.map +1 -0
- package/lib/Axiom/Credentials.js +34 -0
- package/lib/Axiom/Credentials.js.map +1 -0
- package/lib/Axiom/Dashboard.d.ts +124 -0
- package/lib/Axiom/Dashboard.d.ts.map +1 -0
- package/lib/Axiom/Dashboard.js +140 -0
- package/lib/Axiom/Dashboard.js.map +1 -0
- package/lib/Axiom/Dataset.d.ts +101 -0
- package/lib/Axiom/Dataset.d.ts.map +1 -0
- package/lib/Axiom/Dataset.js +183 -0
- package/lib/Axiom/Dataset.js.map +1 -0
- package/lib/Axiom/Monitor.d.ts +74 -0
- package/lib/Axiom/Monitor.d.ts.map +1 -0
- package/lib/Axiom/Monitor.js +103 -0
- package/lib/Axiom/Monitor.js.map +1 -0
- package/lib/Axiom/Notifier.d.ts +63 -0
- package/lib/Axiom/Notifier.d.ts.map +1 -0
- package/lib/Axiom/Notifier.js +83 -0
- package/lib/Axiom/Notifier.js.map +1 -0
- package/lib/Axiom/Providers.d.ts +15 -0
- package/lib/Axiom/Providers.d.ts.map +1 -0
- package/lib/Axiom/Providers.js +32 -0
- package/lib/Axiom/Providers.js.map +1 -0
- package/lib/Axiom/View.d.ts +55 -0
- package/lib/Axiom/View.d.ts.map +1 -0
- package/lib/Axiom/View.js +79 -0
- package/lib/Axiom/View.js.map +1 -0
- package/lib/Axiom/VirtualField.d.ts +42 -0
- package/lib/Axiom/VirtualField.d.ts.map +1 -0
- package/lib/Axiom/VirtualField.js +71 -0
- package/lib/Axiom/VirtualField.js.map +1 -0
- package/lib/Axiom/index.d.ts +13 -0
- package/lib/Axiom/index.d.ts.map +1 -0
- package/lib/Axiom/index.js +13 -0
- package/lib/Axiom/index.js.map +1 -0
- package/lib/Bundle/Bundle.d.ts +20 -2
- package/lib/Bundle/Bundle.d.ts.map +1 -1
- package/lib/Bundle/Bundle.js +18 -3
- package/lib/Bundle/Bundle.js.map +1 -1
- package/lib/Bundle/PurePlugin.d.ts +109 -0
- package/lib/Bundle/PurePlugin.d.ts.map +1 -0
- package/lib/Bundle/PurePlugin.js +469 -0
- package/lib/Bundle/PurePlugin.js.map +1 -0
- package/lib/Bundle/index.d.ts +1 -0
- package/lib/Bundle/index.d.ts.map +1 -1
- package/lib/Bundle/index.js +1 -0
- package/lib/Bundle/index.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +12 -0
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +19 -4
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +1 -1
- package/lib/Output.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +10 -4
- package/lib/Plan.js.map +1 -1
- package/lib/Resource.d.ts +2 -2
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Telemetry/Attributes.d.ts +43 -0
- package/lib/Telemetry/Attributes.d.ts.map +1 -0
- package/lib/Telemetry/Attributes.js +144 -0
- package/lib/Telemetry/Attributes.js.map +1 -0
- package/lib/Telemetry/Layer.d.ts +14 -0
- package/lib/Telemetry/Layer.d.ts.map +1 -0
- package/lib/Telemetry/Layer.js +50 -0
- package/lib/Telemetry/Layer.js.map +1 -0
- package/lib/Telemetry/Metrics.d.ts +34 -0
- package/lib/Telemetry/Metrics.d.ts.map +1 -0
- package/lib/Telemetry/Metrics.js +57 -0
- package/lib/Telemetry/Metrics.js.map +1 -0
- package/package.json +12 -1
- package/src/Apply.ts +118 -9
- package/src/Axiom/Annotation.ts +95 -0
- package/src/Axiom/ApiToken.ts +125 -0
- package/src/Axiom/AuthProvider.ts +341 -0
- package/src/Axiom/Chart.ts +94 -0
- package/src/Axiom/Credentials.ts +70 -0
- package/src/Axiom/Dashboard.ts +192 -0
- package/src/Axiom/Dataset.ts +291 -0
- package/src/Axiom/Monitor.ts +122 -0
- package/src/Axiom/Notifier.ts +104 -0
- package/src/Axiom/Providers.ts +58 -0
- package/src/Axiom/View.ts +106 -0
- package/src/Axiom/VirtualField.ts +90 -0
- package/src/Axiom/index.ts +12 -0
- package/src/Bundle/Bundle.ts +39 -1
- package/src/Bundle/PurePlugin.ts +556 -0
- package/src/Bundle/index.ts +1 -0
- package/src/Cloudflare/Workers/Worker.ts +37 -6
- package/src/Output.ts +2 -1
- package/src/Plan.ts +13 -4
- package/src/Resource.ts +2 -2
- package/src/Telemetry/Attributes.ts +216 -0
- package/src/Telemetry/Layer.ts +65 -0
- 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
|
+
}
|