@alchemy.run/axiom 0.0.0 → 2.0.0-beta.84

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 (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +54 -4
  3. package/lib/Annotation.d.ts +51 -0
  4. package/lib/Annotation.d.ts.map +1 -0
  5. package/lib/Annotation.js +97 -0
  6. package/lib/Annotation.js.map +1 -0
  7. package/lib/ApiToken.d.ts +64 -0
  8. package/lib/ApiToken.d.ts.map +1 -0
  9. package/lib/ApiToken.js +123 -0
  10. package/lib/ApiToken.js.map +1 -0
  11. package/lib/AuthProvider.d.ts +40 -0
  12. package/lib/AuthProvider.d.ts.map +1 -0
  13. package/lib/AuthProvider.js +99 -0
  14. package/lib/AuthProvider.js.map +1 -0
  15. package/lib/Chart.d.ts +130 -0
  16. package/lib/Chart.d.ts.map +1 -0
  17. package/lib/Chart.js +14 -0
  18. package/lib/Chart.js.map +1 -0
  19. package/lib/Contributions.d.ts +7 -0
  20. package/lib/Contributions.d.ts.map +1 -0
  21. package/lib/Contributions.js +6 -0
  22. package/lib/Contributions.js.map +1 -0
  23. package/lib/Credentials.d.ts +10 -0
  24. package/lib/Credentials.d.ts.map +1 -0
  25. package/lib/Credentials.js +30 -0
  26. package/lib/Credentials.js.map +1 -0
  27. package/lib/Dashboard.d.ts +112 -0
  28. package/lib/Dashboard.d.ts.map +1 -0
  29. package/lib/Dashboard.js +157 -0
  30. package/lib/Dashboard.js.map +1 -0
  31. package/lib/Dataset.d.ts +120 -0
  32. package/lib/Dataset.d.ts.map +1 -0
  33. package/lib/Dataset.js +215 -0
  34. package/lib/Dataset.js.map +1 -0
  35. package/lib/Monitor.d.ts +76 -0
  36. package/lib/Monitor.d.ts.map +1 -0
  37. package/lib/Monitor.js +126 -0
  38. package/lib/Monitor.js.map +1 -0
  39. package/lib/Notifier.d.ts +65 -0
  40. package/lib/Notifier.d.ts.map +1 -0
  41. package/lib/Notifier.js +112 -0
  42. package/lib/Notifier.js.map +1 -0
  43. package/lib/OtelHeaders.d.ts +9 -0
  44. package/lib/OtelHeaders.d.ts.map +1 -0
  45. package/lib/OtelHeaders.js +9 -0
  46. package/lib/OtelHeaders.js.map +1 -0
  47. package/lib/Providers.d.ts +15 -0
  48. package/lib/Providers.d.ts.map +1 -0
  49. package/lib/Providers.js +34 -0
  50. package/lib/Providers.js.map +1 -0
  51. package/lib/Telemetry.d.ts +66 -0
  52. package/lib/Telemetry.d.ts.map +1 -0
  53. package/lib/Telemetry.js +68 -0
  54. package/lib/Telemetry.js.map +1 -0
  55. package/lib/View.d.ts +57 -0
  56. package/lib/View.d.ts.map +1 -0
  57. package/lib/View.js +100 -0
  58. package/lib/View.js.map +1 -0
  59. package/lib/VirtualField.d.ts +44 -0
  60. package/lib/VirtualField.d.ts.map +1 -0
  61. package/lib/VirtualField.js +97 -0
  62. package/lib/VirtualField.js.map +1 -0
  63. package/lib/index.d.ts +14 -0
  64. package/lib/index.d.ts.map +1 -0
  65. package/lib/index.js +14 -0
  66. package/lib/index.js.map +1 -0
  67. package/package.json +53 -3
  68. package/src/Annotation.ts +121 -0
  69. package/src/ApiToken.ts +148 -0
  70. package/src/AuthProvider.ts +135 -0
  71. package/src/Chart.ts +156 -0
  72. package/src/Contributions.ts +9 -0
  73. package/src/Credentials.ts +68 -0
  74. package/src/Dashboard.ts +192 -0
  75. package/src/Dataset.ts +326 -0
  76. package/src/Monitor.ts +148 -0
  77. package/src/Notifier.ts +136 -0
  78. package/src/OtelHeaders.ts +9 -0
  79. package/src/Providers.ts +60 -0
  80. package/src/Telemetry.ts +116 -0
  81. package/src/View.ts +130 -0
  82. package/src/VirtualField.ts +123 -0
  83. 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.succeed(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.succeed(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
@@ -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.0",
3
+ "version": "2.0.0-beta.84",
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.17",
50
+ "@distilled.cloud/core": "1.0.0-rc.17"
51
+ },
52
+ "devDependencies": {
53
+ "@types/bun": "1.4.3",
54
+ "alchemy": "2.0.0-beta.84",
55
+ "effect": "^4.0.5",
56
+ "typescript": "^7.0.2"
57
+ },
58
+ "peerDependencies": {
59
+ "alchemy": "2.0.0-beta.84",
60
+ "effect": "^4.0.5"
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.succeed(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.succeed(undefined)),
117
+ );
118
+ }),
119
+ };
120
+ }),
121
+ );
@@ -0,0 +1,148 @@
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 Effect from "effect/Effect";
6
+ import * as Redacted from "effect/Redacted";
7
+ import type { Providers } from "./Providers.ts";
8
+
9
+ export type ApiTokenProps = Omit<Axiom.CreateAPITokenRequest, never>;
10
+
11
+ export type ApiToken = Resource<
12
+ "Axiom.ApiToken",
13
+ ApiTokenProps,
14
+ Omit<Axiom.CreateAPITokenResponse, "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
+ * @see https://axiom.co/docs/reference/tokens
38
+ *
39
+ * ### Creating an API Token
40
+ * **Example:** Ingest-only token scoped to one dataset
41
+ * ```typescript
42
+ * const ingest = yield* Axiom.ApiToken("ingest", {
43
+ * name: "prod-ingest",
44
+ * description: "OTEL collector ingest",
45
+ * datasetCapabilities: {
46
+ * "my-app-traces": { ingest: ["create"] },
47
+ * },
48
+ * });
49
+ * ```
50
+ *
51
+ * **Example:** Read-only query token
52
+ * ```typescript
53
+ * yield* Axiom.ApiToken("query", {
54
+ * name: "grafana-reader",
55
+ * datasetCapabilities: {
56
+ * "my-app-traces": { query: ["read"] },
57
+ * "my-app-logs": { query: ["read"] },
58
+ * },
59
+ * });
60
+ * ```
61
+ *
62
+ * ### Consuming the Token
63
+ * **Example:** Forward the token via Cloudflare Secrets
64
+ * ```typescript
65
+ * const secret = yield* Cloudflare.SecretsStore.Secret("axiom-token", {
66
+ * value: ingest.token,
67
+ * });
68
+ * ```
69
+ *
70
+ * @resource
71
+ * @product API Token
72
+ */
73
+ export const ApiToken = Resource<ApiToken>("Axiom.ApiToken");
74
+
75
+ export const ApiTokenProvider = () =>
76
+ Provider.effect(
77
+ ApiToken,
78
+ Effect.gen(function* () {
79
+ const create = yield* Axiom.createAPIToken;
80
+ const get = yield* Axiom.getAPIToken;
81
+ const del = yield* Axiom.deleteAPIToken;
82
+ const listTokens = yield* Axiom.getAPITokens;
83
+
84
+ return {
85
+ stables: ["id", "token"],
86
+ // Enumerate every API token in the org. Axiom exposes a single
87
+ // account-wide `GET /v2/tokens` collection op (no pagination), so we
88
+ // fetch it once and hydrate each row into the exact `read` Attributes
89
+ // shape. The bearer secret is returned by Axiom only at creation and is
90
+ // never echoed back on enumeration, so — matching `read`, which sources
91
+ // the secret from cached state — we surface an empty Redacted token
92
+ // here rather than the real value.
93
+ list: () =>
94
+ Effect.gen(function* () {
95
+ const tokens = yield* listTokens({});
96
+ return tokens.map((token) => ({
97
+ ...token,
98
+ token: Redacted.make(""),
99
+ }));
100
+ }),
101
+ diff: Effect.fn(function* ({ olds, news, output }) {
102
+ if (!isResolved(news)) return undefined;
103
+ // First create — let the engine create normally.
104
+ if (output == null) return undefined;
105
+ // Axiom has no update endpoint for tokens, so any actual change
106
+ // to the inputs forces a replacement (which mints a new bearer
107
+ // value). If nothing changed, fall through to `noop` so we don't
108
+ // needlessly rotate the token on every deploy.
109
+ if (deepEqual(olds, news)) return undefined;
110
+ return { action: "replace" } as const;
111
+ }),
112
+ reconcile: Effect.fn(function* ({ news, output }) {
113
+ // Observe — Axiom does not expose an update endpoint for tokens,
114
+ // so any actual change to the inputs is forced to a replacement
115
+ // by `diff` above. That means by the time `reconcile` runs we
116
+ // are either (a) creating fresh, or (b) re-reconciling identical
117
+ // inputs against an existing token.
118
+ if (output !== undefined) {
119
+ // Identical inputs — Axiom does not echo the bearer back, so
120
+ // the cached `output` (with the persisted Redacted token) is
121
+ // the authoritative current state.
122
+ return output;
123
+ }
124
+
125
+ // Ensure — mint a new token. Axiom returns the bearer exactly
126
+ // once; capture it into Redacted state for downstream consumers.
127
+ const result = yield* create(news);
128
+ if (!result.token) {
129
+ return yield* Effect.die(new Error("Axiom did not return a token on create"));
130
+ }
131
+ return {
132
+ ...result,
133
+ token: Redacted.make(result.token),
134
+ };
135
+ }),
136
+ delete: Effect.fn(function* ({ output }) {
137
+ yield* del({ id: output.id }).pipe(Effect.catchTag("NotFound", () => Effect.void));
138
+ }),
139
+ read: Effect.fn(function* ({ output }) {
140
+ if (!output?.id) return undefined;
141
+ return yield* get({ id: output.id }).pipe(
142
+ Effect.map((current) => ({ ...current, token: output.token })),
143
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
144
+ );
145
+ }),
146
+ };
147
+ }),
148
+ );
@@ -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;