@alchemy.run/axiom 0.0.0 → 2.0.0-beta.85
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/LICENSE +201 -0
- package/README.md +54 -4
- package/lib/Annotation.d.ts +51 -0
- package/lib/Annotation.d.ts.map +1 -0
- package/lib/Annotation.js +97 -0
- package/lib/Annotation.js.map +1 -0
- package/lib/ApiToken.d.ts +74 -0
- package/lib/ApiToken.d.ts.map +1 -0
- package/lib/ApiToken.js +130 -0
- package/lib/ApiToken.js.map +1 -0
- package/lib/AuthProvider.d.ts +40 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +99 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Chart.d.ts +130 -0
- package/lib/Chart.d.ts.map +1 -0
- package/lib/Chart.js +14 -0
- package/lib/Chart.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/Credentials.d.ts +10 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +17 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Dashboard.d.ts +112 -0
- package/lib/Dashboard.d.ts.map +1 -0
- package/lib/Dashboard.js +157 -0
- package/lib/Dashboard.js.map +1 -0
- package/lib/Dataset.d.ts +129 -0
- package/lib/Dataset.d.ts.map +1 -0
- package/lib/Dataset.js +221 -0
- package/lib/Dataset.js.map +1 -0
- package/lib/Monitor.d.ts +76 -0
- package/lib/Monitor.d.ts.map +1 -0
- package/lib/Monitor.js +126 -0
- package/lib/Monitor.js.map +1 -0
- package/lib/Notifier.d.ts +65 -0
- package/lib/Notifier.d.ts.map +1 -0
- package/lib/Notifier.js +112 -0
- package/lib/Notifier.js.map +1 -0
- package/lib/OtelHeaders.d.ts +9 -0
- package/lib/OtelHeaders.d.ts.map +1 -0
- package/lib/OtelHeaders.js +9 -0
- package/lib/OtelHeaders.js.map +1 -0
- package/lib/Providers.d.ts +15 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +34 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Telemetry.d.ts +66 -0
- package/lib/Telemetry.d.ts.map +1 -0
- package/lib/Telemetry.js +68 -0
- package/lib/Telemetry.js.map +1 -0
- package/lib/View.d.ts +57 -0
- package/lib/View.d.ts.map +1 -0
- package/lib/View.js +100 -0
- package/lib/View.js.map +1 -0
- package/lib/VirtualField.d.ts +44 -0
- package/lib/VirtualField.d.ts.map +1 -0
- package/lib/VirtualField.js +97 -0
- package/lib/VirtualField.js.map +1 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/package.json +53 -3
- package/src/Annotation.ts +121 -0
- package/src/ApiToken.ts +158 -0
- package/src/AuthProvider.ts +135 -0
- package/src/Chart.ts +156 -0
- package/src/Contributions.ts +9 -0
- package/src/Credentials.ts +26 -0
- package/src/Dashboard.ts +192 -0
- package/src/Dataset.ts +333 -0
- package/src/Monitor.ts +148 -0
- package/src/Notifier.ts +136 -0
- package/src/OtelHeaders.ts +9 -0
- package/src/Providers.ts +60 -0
- package/src/Telemetry.ts +116 -0
- package/src/View.ts +130 -0
- package/src/VirtualField.ts +123 -0
- package/src/index.ts +13 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as Axiom from "@distilled.cloud/axiom";
|
|
2
|
+
import { isResolved } from "alchemy/Diff";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
/**
|
|
7
|
+
* An Axiom virtual field — a saved APL expression that appears as a derived
|
|
8
|
+
* column on a dataset at query time. Use these to standardise common
|
|
9
|
+
* computations (status classes, latency buckets, parsed JSON paths) so
|
|
10
|
+
* dashboards and monitors don't have to redefine them.
|
|
11
|
+
*
|
|
12
|
+
* Bound to a single `dataset`; changing the dataset triggers a replacement.
|
|
13
|
+
* @see https://axiom.co/docs/query-data/virtual-fields
|
|
14
|
+
*
|
|
15
|
+
* ### Creating a Virtual Field
|
|
16
|
+
* **Example:** HTTP status class (e.g. 200 → "2xx")
|
|
17
|
+
* ```typescript
|
|
18
|
+
* yield* Axiom.VirtualField("status-class", {
|
|
19
|
+
* dataset: "my-app-traces",
|
|
20
|
+
* name: "status_class",
|
|
21
|
+
* description: "HTTP response class bucket",
|
|
22
|
+
* expression: 'strcat(tostring(toint(status / 100)), "xx")',
|
|
23
|
+
* type: "string",
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* **Example:** Latency bucket in seconds
|
|
28
|
+
* ```typescript
|
|
29
|
+
* yield* Axiom.VirtualField("latency-bucket", {
|
|
30
|
+
* dataset: "my-app-traces",
|
|
31
|
+
* name: "latency_bucket_s",
|
|
32
|
+
* expression: "bin(duration_ms / 1000.0, 0.5)",
|
|
33
|
+
* type: "number",
|
|
34
|
+
* unit: "s",
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @resource
|
|
39
|
+
* @product Dataset
|
|
40
|
+
*/
|
|
41
|
+
export const VirtualField = Resource("Axiom.VirtualField");
|
|
42
|
+
export const VirtualFieldProvider = () => Provider.effect(VirtualField, Effect.gen(function* () {
|
|
43
|
+
const create = yield* Axiom.createVirtualField;
|
|
44
|
+
const update = yield* Axiom.updateVirtualField;
|
|
45
|
+
const get = yield* Axiom.getVirtualField;
|
|
46
|
+
const del = yield* Axiom.deleteVirtualField;
|
|
47
|
+
const listVirtualFields = yield* Axiom.getVirtualFields;
|
|
48
|
+
const listDatasets = yield* Axiom.getDatasets;
|
|
49
|
+
return {
|
|
50
|
+
stables: ["id"],
|
|
51
|
+
// Axiom only exposes a per-dataset virtual-field enumeration
|
|
52
|
+
// (`GET /v2/vfields?dataset=...`); there is no account-wide list. So
|
|
53
|
+
// enumerate every dataset (`GET /v2/datasets`), fan out the per-dataset
|
|
54
|
+
// vfields list with bounded concurrency, and flatten. Each row already
|
|
55
|
+
// matches the `read` Attributes shape (`CreateVirtualFieldOutput`), so
|
|
56
|
+
// it's directly usable by `delete` with no follow-up read.
|
|
57
|
+
list: () => Effect.gen(function* () {
|
|
58
|
+
const datasets = yield* listDatasets({});
|
|
59
|
+
const perDataset = yield* Effect.forEach(datasets, (ds) => listVirtualFields({ dataset: ds.name }), { concurrency: 10 });
|
|
60
|
+
return perDataset.flat().map((vf) => ({ ...vf }));
|
|
61
|
+
}),
|
|
62
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
63
|
+
if (!isResolved(news))
|
|
64
|
+
return undefined;
|
|
65
|
+
if (output && news.dataset !== output.dataset) {
|
|
66
|
+
return { action: "replace" };
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}),
|
|
70
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
71
|
+
// Observe — Axiom assigns the virtual-field id server-side, so
|
|
72
|
+
// the only handle to a previously-created field is the cached
|
|
73
|
+
// `output.id`. Probe for live state with that id; treat NotFound
|
|
74
|
+
// (deleted out-of-band) as "no observed state" so we converge by
|
|
75
|
+
// re-creating.
|
|
76
|
+
const observed = output?.id
|
|
77
|
+
? yield* get({ id: output.id }).pipe(Effect.catchTag("NotFound", () => Effect.undefined))
|
|
78
|
+
: undefined;
|
|
79
|
+
// Ensure — POST mints a new virtual field with a fresh id.
|
|
80
|
+
if (observed === undefined) {
|
|
81
|
+
return yield* create(news);
|
|
82
|
+
}
|
|
83
|
+
// Sync — the field exists; PUT against its id with the desired
|
|
84
|
+
// props. `dataset` is replacement-only (handled in diff).
|
|
85
|
+
return yield* update({ ...news, id: observed.id });
|
|
86
|
+
}),
|
|
87
|
+
delete: Effect.fn(function* ({ output }) {
|
|
88
|
+
yield* del({ id: output.id }).pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
89
|
+
}),
|
|
90
|
+
read: Effect.fn(function* ({ output }) {
|
|
91
|
+
if (!output?.id)
|
|
92
|
+
return undefined;
|
|
93
|
+
return yield* get({ id: output.id }).pipe(Effect.catchTag("NotFound", () => Effect.undefined));
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}));
|
|
97
|
+
//# sourceMappingURL=VirtualField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VirtualField.js","sourceRoot":"","sources":["../src/VirtualField.ts"],"names":[],"mappings":"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"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./Annotation.ts";
|
|
2
|
+
export * from "./ApiToken.ts";
|
|
3
|
+
export * from "./AuthProvider.ts";
|
|
4
|
+
export * from "./Chart.ts";
|
|
5
|
+
export * from "./Credentials.ts";
|
|
6
|
+
export * from "./Dashboard.ts";
|
|
7
|
+
export * from "./Dataset.ts";
|
|
8
|
+
export * from "./Monitor.ts";
|
|
9
|
+
export * from "./Notifier.ts";
|
|
10
|
+
export * from "./Telemetry.ts";
|
|
11
|
+
export * from "./Providers.ts";
|
|
12
|
+
export * from "./View.ts";
|
|
13
|
+
export * from "./VirtualField.ts";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./Annotation.js";
|
|
2
|
+
export * from "./ApiToken.js";
|
|
3
|
+
export * from "./AuthProvider.js";
|
|
4
|
+
export * from "./Chart.js";
|
|
5
|
+
export * from "./Credentials.js";
|
|
6
|
+
export * from "./Dashboard.js";
|
|
7
|
+
export * from "./Dataset.js";
|
|
8
|
+
export * from "./Monitor.js";
|
|
9
|
+
export * from "./Notifier.js";
|
|
10
|
+
export * from "./Telemetry.js";
|
|
11
|
+
export * from "./Providers.js";
|
|
12
|
+
export * from "./View.js";
|
|
13
|
+
export * from "./VirtualField.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alchemy.run/axiom",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "2.0.0-beta.85",
|
|
4
4
|
"description": "Axiom datasets, dashboards, monitors and telemetry export for Alchemy.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -10,12 +10,62 @@
|
|
|
10
10
|
"observability",
|
|
11
11
|
"opentelemetry"
|
|
12
12
|
],
|
|
13
|
+
"homepage": "https://alchemy.run",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/alchemy-run/alchemy/issues"
|
|
16
|
+
},
|
|
13
17
|
"license": "Apache-2.0",
|
|
14
18
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
15
|
-
"homepage": "https://alchemy.run",
|
|
16
19
|
"repository": {
|
|
17
20
|
"type": "git",
|
|
18
21
|
"url": "git+https://github.com/alchemy-run/alchemy.git",
|
|
19
22
|
"directory": "packages/axiom"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib",
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./lib/index.d.ts",
|
|
33
|
+
"bun": "./src/index.ts",
|
|
34
|
+
"worker": "./src/index.ts",
|
|
35
|
+
"default": "./lib/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./*": {
|
|
38
|
+
"types": "./lib/*.d.ts",
|
|
39
|
+
"bun": "./src/*.ts",
|
|
40
|
+
"worker": "./src/*.ts",
|
|
41
|
+
"default": "./lib/*.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@distilled.cloud/axiom": "1.0.0-rc.18",
|
|
50
|
+
"@distilled.cloud/core": "1.0.0-rc.18"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "1.4.3",
|
|
54
|
+
"alchemy": "2.0.0-beta.85",
|
|
55
|
+
"effect": "^4.1.0",
|
|
56
|
+
"typescript": "^7.0.2"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"alchemy": "2.0.0-beta.85",
|
|
60
|
+
"effect": "^4.1.0"
|
|
61
|
+
},
|
|
62
|
+
"alchemy": {
|
|
63
|
+
"contributes": [
|
|
64
|
+
"@alchemy.run/axiom/Contributions"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "pnpm -r --filter \"{.}...\" run build:package",
|
|
69
|
+
"build:package": "node scripts/build.ts"
|
|
20
70
|
}
|
|
21
|
-
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as Axiom from "@distilled.cloud/axiom";
|
|
2
|
+
import * as Provider from "alchemy/Provider";
|
|
3
|
+
import { Resource } from "alchemy/Resource";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import type { Providers } from "./Providers.ts";
|
|
6
|
+
|
|
7
|
+
export type AnnotationProps = Axiom.CreateAnnotationRequest;
|
|
8
|
+
|
|
9
|
+
export type Annotation = Resource<
|
|
10
|
+
"Axiom.Annotation",
|
|
11
|
+
AnnotationProps,
|
|
12
|
+
Axiom.Annotation,
|
|
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
|
+
* @see https://axiom.co/docs/query-data/annotate-charts
|
|
30
|
+
*
|
|
31
|
+
* ### Creating an Annotation
|
|
32
|
+
* **Example:** Point-in-time deploy marker
|
|
33
|
+
* ```typescript
|
|
34
|
+
* yield* Axiom.Annotation("deploy-1.2.3", {
|
|
35
|
+
* type: "deploy",
|
|
36
|
+
* title: "Release 1.2.3",
|
|
37
|
+
* description: "https://github.com/acme/app/releases/tag/v1.2.3",
|
|
38
|
+
* datasets: ["my-app-traces", "my-app-logs"],
|
|
39
|
+
* time: new Date().toISOString(),
|
|
40
|
+
* url: "https://github.com/acme/app/releases/tag/v1.2.3",
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* **Example:** Incident time-range
|
|
45
|
+
* ```typescript
|
|
46
|
+
* yield* Axiom.Annotation("inc-2026-04-27", {
|
|
47
|
+
* type: "incident",
|
|
48
|
+
* title: "Database failover",
|
|
49
|
+
* datasets: ["my-app-traces"],
|
|
50
|
+
* time: "2026-04-27T18:05:00Z",
|
|
51
|
+
* endTime: "2026-04-27T18:32:00Z",
|
|
52
|
+
* url: "https://incident.io/incidents/abc123",
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @resource
|
|
57
|
+
* @product Dataset
|
|
58
|
+
*/
|
|
59
|
+
export const Annotation = Resource<Annotation>("Axiom.Annotation");
|
|
60
|
+
|
|
61
|
+
export const AnnotationProvider = () =>
|
|
62
|
+
Provider.effect(
|
|
63
|
+
Annotation,
|
|
64
|
+
Effect.gen(function* () {
|
|
65
|
+
const create = yield* Axiom.createAnnotation;
|
|
66
|
+
const update = yield* Axiom.updateAnnotation;
|
|
67
|
+
const get = yield* Axiom.getAnnotation;
|
|
68
|
+
const del = yield* Axiom.deleteAnnotation;
|
|
69
|
+
const listAnnotations = yield* Axiom.getAnnotations;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
stables: ["id"],
|
|
73
|
+
// Axiom exposes a flat, account-wide `GET /v2/annotations` list that
|
|
74
|
+
// returns every annotation's full record in one (non-paginated)
|
|
75
|
+
// response — the exact same shape `get`/`read` produce. Hand each
|
|
76
|
+
// record back directly as the resource's Attributes.
|
|
77
|
+
list: () =>
|
|
78
|
+
listAnnotations({}).pipe(Effect.map((annotations) => annotations.map((a) => ({ ...a })))),
|
|
79
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
80
|
+
// Observe — Axiom assigns the annotation id server-side, so the
|
|
81
|
+
// only handle to a previously-created annotation is the cached
|
|
82
|
+
// `output.id`. Probe for live state with that id; treat NotFound
|
|
83
|
+
// (deleted out-of-band, or never created) as "no observed state".
|
|
84
|
+
const observed = output?.id
|
|
85
|
+
? yield* get({ id: output.id }).pipe(
|
|
86
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
87
|
+
)
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
90
|
+
// Ensure — when no observed annotation exists, POST creates one
|
|
91
|
+
// and Axiom assigns the id.
|
|
92
|
+
if (observed === undefined) {
|
|
93
|
+
return yield* create(news);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Sync — the annotation exists; apply desired props with PUT and
|
|
97
|
+
// preserve the stable id and original time as fallbacks.
|
|
98
|
+
const result = yield* update({ ...news, id: observed.id! });
|
|
99
|
+
return {
|
|
100
|
+
...result,
|
|
101
|
+
id: observed.id!,
|
|
102
|
+
time: result.time ?? output?.time ?? news.time,
|
|
103
|
+
};
|
|
104
|
+
}),
|
|
105
|
+
delete: Effect.fn(function* ({ output }) {
|
|
106
|
+
yield* del({ id: output.id }).pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
107
|
+
}),
|
|
108
|
+
read: Effect.fn(function* ({ output }) {
|
|
109
|
+
if (!output?.id) return undefined;
|
|
110
|
+
return yield* get({ id: output.id }).pipe(
|
|
111
|
+
Effect.map((current) => ({
|
|
112
|
+
...current,
|
|
113
|
+
id: current.id ?? output.id,
|
|
114
|
+
time: current.time ?? output.time,
|
|
115
|
+
})),
|
|
116
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
117
|
+
);
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
}),
|
|
121
|
+
);
|
package/src/ApiToken.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as Axiom from "@distilled.cloud/axiom";
|
|
2
|
+
import { deepEqual, isResolved } from "alchemy/Diff";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Redacted from "effect/Redacted";
|
|
8
|
+
import type { Providers } from "./Providers.ts";
|
|
9
|
+
|
|
10
|
+
export type ApiTokenProps = Omit<Axiom.CreateAPITokenRequest, never>;
|
|
11
|
+
|
|
12
|
+
export type ApiToken = Resource<
|
|
13
|
+
"Axiom.ApiToken",
|
|
14
|
+
ApiTokenProps,
|
|
15
|
+
Omit<Axiom.CreateAPITokenResponse, "token"> & {
|
|
16
|
+
/**
|
|
17
|
+
* The bearer token. Returned only by `create` (and `regenerate`); Axiom
|
|
18
|
+
* does not return it on subsequent reads. Persisted in resource state via
|
|
19
|
+
* `Redacted` — handle with care.
|
|
20
|
+
*/
|
|
21
|
+
token: Redacted.Redacted<string>;
|
|
22
|
+
},
|
|
23
|
+
never,
|
|
24
|
+
Providers
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An Axiom API token — a scoped bearer token used to authenticate API
|
|
29
|
+
* requests (ingest, query, admin). Capabilities are pinned at creation time;
|
|
30
|
+
* changing any field triggers a **replacement** because Axiom does not
|
|
31
|
+
* expose an update endpoint.
|
|
32
|
+
*
|
|
33
|
+
* The raw token value is returned only by `create`. After that, Axiom
|
|
34
|
+
* never echoes it back, so it is captured into `output.token` (as a
|
|
35
|
+
* {@link Redacted}) on initial create and persisted in resource state.
|
|
36
|
+
* Treat resource state as sensitive — anyone with read access can recover
|
|
37
|
+
* the token. Pair with a secret store for downstream consumption.
|
|
38
|
+
* @see https://axiom.co/docs/reference/tokens
|
|
39
|
+
*
|
|
40
|
+
* ### 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
|
+
* ### Consuming the Token
|
|
64
|
+
* **Example:** Forward the token via Cloudflare Secrets
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const secret = yield* Cloudflare.SecretsStore.Secret("axiom-token", {
|
|
67
|
+
* value: ingest.token,
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @resource
|
|
72
|
+
* @product API Token
|
|
73
|
+
*/
|
|
74
|
+
export const ApiToken = Resource<ApiToken>("Axiom.ApiToken");
|
|
75
|
+
|
|
76
|
+
/** Axiom created a token but returned no bearer value. */
|
|
77
|
+
export class MissingApiTokenValue extends Data.TaggedError("MissingApiTokenValue")<{
|
|
78
|
+
readonly id: string;
|
|
79
|
+
}> {
|
|
80
|
+
override get message() {
|
|
81
|
+
return `Axiom did not return a value for the new API token ${this.id}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const ApiTokenProvider = () =>
|
|
86
|
+
Provider.effect(
|
|
87
|
+
ApiToken,
|
|
88
|
+
Effect.gen(function* () {
|
|
89
|
+
const create = yield* Axiom.createAPIToken;
|
|
90
|
+
const get = yield* Axiom.getAPIToken;
|
|
91
|
+
const del = yield* Axiom.deleteAPIToken;
|
|
92
|
+
const listTokens = yield* Axiom.getAPITokens;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
stables: ["id", "token"],
|
|
96
|
+
// Enumerate every API token in the org. Axiom exposes a single
|
|
97
|
+
// account-wide `GET /v2/tokens` collection op (no pagination), so we
|
|
98
|
+
// fetch it once and hydrate each row into the exact `read` Attributes
|
|
99
|
+
// shape. The bearer secret is returned by Axiom only at creation and is
|
|
100
|
+
// never echoed back on enumeration, so — matching `read`, which sources
|
|
101
|
+
// the secret from cached state — we surface an empty Redacted token
|
|
102
|
+
// here rather than the real value.
|
|
103
|
+
list: () =>
|
|
104
|
+
Effect.gen(function* () {
|
|
105
|
+
const tokens = yield* listTokens({});
|
|
106
|
+
return tokens.map((token) => ({
|
|
107
|
+
...token,
|
|
108
|
+
token: Redacted.make(""),
|
|
109
|
+
}));
|
|
110
|
+
}),
|
|
111
|
+
diff: Effect.fn(function* ({ olds, news, output }) {
|
|
112
|
+
if (!isResolved(news)) return undefined;
|
|
113
|
+
// First create — let the engine create normally.
|
|
114
|
+
if (output == null) return undefined;
|
|
115
|
+
// Axiom has no update endpoint for tokens, so any actual change
|
|
116
|
+
// to the inputs forces a replacement (which mints a new bearer
|
|
117
|
+
// value). If nothing changed, fall through to `noop` so we don't
|
|
118
|
+
// needlessly rotate the token on every deploy.
|
|
119
|
+
if (deepEqual(olds, news)) return undefined;
|
|
120
|
+
return { action: "replace" } as const;
|
|
121
|
+
}),
|
|
122
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
123
|
+
// Observe — Axiom does not expose an update endpoint for tokens,
|
|
124
|
+
// so any actual change to the inputs is forced to a replacement
|
|
125
|
+
// by `diff` above. That means by the time `reconcile` runs we
|
|
126
|
+
// are either (a) creating fresh, or (b) re-reconciling identical
|
|
127
|
+
// inputs against an existing token.
|
|
128
|
+
if (output !== undefined) {
|
|
129
|
+
// Identical inputs — Axiom does not echo the bearer back, so
|
|
130
|
+
// the cached `output` (with the persisted Redacted token) is
|
|
131
|
+
// the authoritative current state.
|
|
132
|
+
return output;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ensure — mint a new token. Axiom returns the bearer exactly
|
|
136
|
+
// once; capture it into Redacted state for downstream consumers.
|
|
137
|
+
const result = yield* create(news);
|
|
138
|
+
if (!result.token) {
|
|
139
|
+
return yield* Effect.die(new MissingApiTokenValue({ id: result.id }));
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
...result,
|
|
143
|
+
token: Redacted.isRedacted(result.token) ? result.token : Redacted.make(result.token),
|
|
144
|
+
};
|
|
145
|
+
}),
|
|
146
|
+
delete: Effect.fn(function* ({ output }) {
|
|
147
|
+
yield* del({ id: output.id }).pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
148
|
+
}),
|
|
149
|
+
read: Effect.fn(function* ({ output }) {
|
|
150
|
+
if (!output?.id) return undefined;
|
|
151
|
+
return yield* get({ id: output.id }).pipe(
|
|
152
|
+
Effect.map((current) => ({ ...current, token: output.token })),
|
|
153
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
154
|
+
);
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from "@distilled.cloud/axiom/Credentials";
|
|
2
|
+
import { AuthError } from "alchemy/Auth/AuthProvider";
|
|
3
|
+
import { getEnv, getEnvRedacted } from "alchemy/Auth/Env";
|
|
4
|
+
import {
|
|
5
|
+
makeStoredAuthProvider,
|
|
6
|
+
storedSecret,
|
|
7
|
+
storedValueText,
|
|
8
|
+
type StoredAuthConfig,
|
|
9
|
+
} from "alchemy/Auth/StoredAuthProvider";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Redacted from "effect/Redacted";
|
|
12
|
+
|
|
13
|
+
export const AXIOM_AUTH_PROVIDER_NAME = "Axiom";
|
|
14
|
+
|
|
15
|
+
export 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
|
+
*/
|
|
24
|
+
export 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
|
+
|
|
40
|
+
const readEnvironment = Effect.gen(function* () {
|
|
41
|
+
const apiToken =
|
|
42
|
+
(yield* getEnvRedacted("AXIOM_TOKEN")) ?? (yield* getEnvRedacted("AXIOM_API_KEY"));
|
|
43
|
+
if (!apiToken) {
|
|
44
|
+
return yield* AuthError.make({
|
|
45
|
+
message: "Axiom CI credentials not found. Set AXIOM_TOKEN or AXIOM_API_KEY.",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const apiBaseUrl = (yield* getEnv("AXIOM_URL")) ?? DEFAULT_API_BASE_URL;
|
|
49
|
+
const orgId = yield* getEnv("AXIOM_ORG_ID");
|
|
50
|
+
return orgId
|
|
51
|
+
? {
|
|
52
|
+
type: "pat" as const,
|
|
53
|
+
apiToken,
|
|
54
|
+
apiBaseUrl,
|
|
55
|
+
orgId,
|
|
56
|
+
source: { type: "env" as const },
|
|
57
|
+
}
|
|
58
|
+
: {
|
|
59
|
+
type: "apiToken" as const,
|
|
60
|
+
apiToken,
|
|
61
|
+
apiBaseUrl,
|
|
62
|
+
source: { type: "env" as const },
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const axiomAuth = makeStoredAuthProvider<AxiomResolvedCredentials>({
|
|
67
|
+
provider: AXIOM_AUTH_PROVIDER_NAME,
|
|
68
|
+
fields: [
|
|
69
|
+
{
|
|
70
|
+
name: "token",
|
|
71
|
+
label: "Axiom API Token or Personal Access Token",
|
|
72
|
+
secret: true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "orgId",
|
|
76
|
+
label: "Axiom Org ID (required for personal access tokens)",
|
|
77
|
+
optional: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "apiBaseUrl",
|
|
81
|
+
label: "Axiom API Base URL",
|
|
82
|
+
optional: true,
|
|
83
|
+
placeholder: DEFAULT_API_BASE_URL,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
toResolved: (values, source) => {
|
|
87
|
+
const apiToken = storedSecret(values.token) ?? Redacted.make("");
|
|
88
|
+
const apiBaseUrl = storedValueText(values.apiBaseUrl) ?? DEFAULT_API_BASE_URL;
|
|
89
|
+
return values.orgId !== undefined
|
|
90
|
+
? {
|
|
91
|
+
type: "pat",
|
|
92
|
+
apiToken,
|
|
93
|
+
apiBaseUrl,
|
|
94
|
+
orgId: storedValueText(values.orgId) ?? "",
|
|
95
|
+
source: { type: source },
|
|
96
|
+
}
|
|
97
|
+
: {
|
|
98
|
+
type: "apiToken",
|
|
99
|
+
apiToken,
|
|
100
|
+
apiBaseUrl,
|
|
101
|
+
source: { type: source },
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
readEnvironment,
|
|
105
|
+
environment: [
|
|
106
|
+
{
|
|
107
|
+
name: "AXIOM_TOKEN",
|
|
108
|
+
required: true,
|
|
109
|
+
secret: true,
|
|
110
|
+
alternatives: ["AXIOM_API_KEY"],
|
|
111
|
+
description: "Personal access token or API token.",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "AXIOM_URL",
|
|
115
|
+
required: false,
|
|
116
|
+
description: "API base URL for self-hosted or regional deployments.",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "AXIOM_ORG_ID",
|
|
120
|
+
required: false,
|
|
121
|
+
description: "Organization id; required when the token is a personal access token.",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Layer that registers the Axiom {@link AuthProvider} into the
|
|
128
|
+
* {@link AuthProviders} registry when built. Include this in the Axiom
|
|
129
|
+
* `providers()` layer so the alchemy CLI can discover it.
|
|
130
|
+
*/
|
|
131
|
+
export const AxiomAuth = axiomAuth.layer;
|
|
132
|
+
|
|
133
|
+
/** Schema of Axiom's inline static-token values. */
|
|
134
|
+
export const AxiomStoredCredentials = axiomAuth.storedSchema;
|
|
135
|
+
export type AxiomStoredCredentials = typeof AxiomStoredCredentials.Type;
|