@homeflare/alchemy 0.33.0 → 0.34.0

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 (50) hide show
  1. package/dist/grafana/alerting-provenance.d.ts +80 -0
  2. package/dist/grafana/alerting-provenance.d.ts.map +1 -0
  3. package/dist/grafana/contact-point-fixtures.d.ts +28 -0
  4. package/dist/grafana/contact-point-fixtures.d.ts.map +1 -0
  5. package/dist/grafana/contact-point.d.ts +99 -0
  6. package/dist/grafana/contact-point.d.ts.map +1 -0
  7. package/dist/grafana/dashboard-model.d.ts +8 -38
  8. package/dist/grafana/dashboard-model.d.ts.map +1 -1
  9. package/dist/grafana/datasource.d.ts +2 -9
  10. package/dist/grafana/datasource.d.ts.map +1 -1
  11. package/dist/grafana/index.d.ts +10 -4
  12. package/dist/grafana/index.d.ts.map +1 -1
  13. package/dist/grafana/index.js +198 -41
  14. package/dist/grafana/index.js.map +12 -6
  15. package/dist/grafana/message-template.d.ts +70 -0
  16. package/dist/grafana/message-template.d.ts.map +1 -0
  17. package/dist/grafana/mute-timing.d.ts +112 -0
  18. package/dist/grafana/mute-timing.d.ts.map +1 -0
  19. package/dist/grafana/providers.d.ts +1 -1
  20. package/dist/grafana/providers.d.ts.map +1 -1
  21. package/dist/grafana/secret-refs.d.ts +19 -0
  22. package/dist/grafana/secret-refs.d.ts.map +1 -0
  23. package/dist/grafana/subset-match.d.ts +2 -0
  24. package/dist/grafana/subset-match.d.ts.map +1 -0
  25. package/dist/linux/index.d.ts +1 -1
  26. package/dist/linux/index.d.ts.map +1 -1
  27. package/dist/linux/index.js +3 -1
  28. package/dist/linux/index.js.map +1 -1
  29. package/docs/grafana-alerting.md +145 -0
  30. package/docs/grafana.md +4 -3
  31. package/docs/upstream-conformance.md +26 -4
  32. package/package.json +1 -1
  33. package/src/grafana/alerting-provenance.ts +109 -0
  34. package/src/grafana/contact-point-fixtures.ts +25 -0
  35. package/src/grafana/contact-point-provenance.test.ts +113 -0
  36. package/src/grafana/contact-point-secrets.test.ts +117 -0
  37. package/src/grafana/contact-point.test.ts +123 -0
  38. package/src/grafana/contact-point.ts +179 -0
  39. package/src/grafana/dashboard-model.ts +8 -57
  40. package/src/grafana/datasource.ts +5 -27
  41. package/src/grafana/index.ts +24 -4
  42. package/src/grafana/message-template.test.ts +212 -0
  43. package/src/grafana/message-template.ts +114 -0
  44. package/src/grafana/mute-timing.test.ts +227 -0
  45. package/src/grafana/mute-timing.ts +160 -0
  46. package/src/grafana/providers.ts +6 -0
  47. package/src/grafana/secret-refs.ts +48 -0
  48. package/src/grafana/subset-match.ts +71 -0
  49. package/src/linux/index-export.test.ts +25 -0
  50. package/src/linux/index.ts +15 -1
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The shared refusal for a foreign-provenance ALERTING-PROVISIONING object — `Grafana.ContactPoint`,
3
+ * `Grafana.MessageTemplate`, `Grafana.AlertRuleGroup` and `Grafana.NotificationPolicy` each read a
4
+ * `provenance` string off what Grafana returns (`EmbeddedContactPoint.provenance`,
5
+ * `NotificationTemplate.provenance`, `ProvisionedAlertRule.provenance` per rule, `Route.provenance`
6
+ * on the policy tree — all measured against `@distilled.cloud/grafana`'s generated types,
7
+ * `packages/distilled-grafana/src/services/grafana.ts`) and refuse a write when it disagrees with
8
+ * what this family is allowed to own. Sibling to `provisioned.ts`, not a rename of it — the two
9
+ * refusals guard different fields with different value shapes (`managedBy`/`provisioned` there,
10
+ * `provenance` here) for two different Grafana API surfaces (classic folder/dashboard vs. alerting
11
+ * provisioning), but both end in the SAME tagged error, reused from `provisioned.ts` rather than
12
+ * declared twice: "a write was refused because something else owns this object" is one condition
13
+ * across the whole family, the spirit PR 245's provisioned refusal already established.
14
+ *
15
+ * ★ THE ALLOWLIST, MEASURED AGAINST GRAFANA'S OWN SOURCE (not this SDK, which only echoes the
16
+ * value back on read — never re-fetched live in this PR): Grafana's `pkg/services/ngalert/
17
+ * models` package defines a `Provenance` type with `ProvenanceNone = ""` (nothing has claimed
18
+ * the object — fresh, or written through the classic non-provisioning path), `ProvenanceAPI =
19
+ * "api"` (created or last written through THIS provisioning API — what this family's own
20
+ * create/update leaves behind), `ProvenanceFile = "file"` (Grafana's file-based provisioning),
21
+ * and `ProvenanceConvertedPrometheus = "converted_prometheus"` (the Prometheus-rules importer).
22
+ * Corroborated by Grafana's own docs (grafana.com/docs/grafana/v13.1/alerting/set-up/
23
+ * provision-alerting-resources/http-api-provisioning/), whose example payloads show
24
+ * `"provenance": "api"` on an alert rule this API created and `"provenance": "file"` on a
25
+ * file-provisioned mute timing and template.
26
+ *
27
+ * `WRITABLE_PROVENANCES` is therefore an ALLOWLIST of `{"", "api"}`, not a denylist of the two
28
+ * named foreign values — a provenance source not yet named above (a plugin importer, a future
29
+ * Grafana release) refuses by default instead of silently being treated as writable, the safer
30
+ * failure direction for a control-plane write.
31
+ *
32
+ * ⛔ A MISSING `provenance` FIELD IS TREATED AS FOREIGN, NEVER AS `ProvenanceNone` — fixed after an
33
+ * adversarial review of this PR found the first version failed OPEN: every resource mapped
34
+ * `live.provenance ?? ''`, and `''` (`ProvenanceNone`, writable) is indistinguishable from a
35
+ * field the response simply never included once both collapse through `??`. Every `provenance`
36
+ * field in this family is optional on the wire (`EmbeddedContactPoint.provenance?: string`, etc.
37
+ * — genuinely absent is a real, typed possibility, not a hypothetical), and `Grafana.MuteTiming`'s
38
+ * depends on an unmodeled-key passthrough this family has never observed against a real Grafana
39
+ * response (see its own file header) — a version, edge case or bug in that decode path that drops
40
+ * the field would, under the old `?? ''` rule, silently make a file-provisioned object look
41
+ * writable. `isForeignProvenance`/`refuseIfForeignProvenance` below keep `undefined` as its own
42
+ * value all the way through: ONLY a field Grafana explicitly reported as `""` or `"api"` is
43
+ * writable; missing entirely refuses, with a message that says so rather than naming a fabricated
44
+ * provenance value.
45
+ *
46
+ * ⚠️ CLIENT-SIDE DEFENSE, NOT THE ONLY GUARD — AND NOT RE-MEASURED LIVE IN THIS PR. Grafana's own
47
+ * alerting provisioning API server ALSO enforces this (unlike the classic folder/dashboard write
48
+ * API `provisioned.ts` guards, which has no such server-side check at all): grafana/grafana#103597
49
+ * ("Unable to delete file provisioned alert rules") reports exactly this block firing for a
50
+ * `file`-provenance object, and the `X-Disable-Provenance` header's own documented purpose is "by
51
+ * default, you cannot edit API-provisioned alerting resources in Grafana" (same doc page). This
52
+ * function exists anyway, for the same reason `provisioned.ts` does: a clear, typed, house error
53
+ * instead of whatever 400-with-a-string-message Grafana's server happens to return this version —
54
+ * and as a defense against the known server-side bug that same issue documents
55
+ * (`X-Disable-Provenance` failing to actually unlock a `file`-provenance object), which could
56
+ * otherwise leave a write silently rejected with only a generic `BadRequest` and no explanation
57
+ * of why.
58
+ *
59
+ * ⚠️ `Grafana.MuteTiming` ALSO USES THIS FUNCTION, BUT THROUGH A WIDENED LIVE TYPE — the SDK's
60
+ * generated `MuteTimeInterval` interface (the TS type for both `GET .../mute-timings` and
61
+ * `GET .../mute-timings/{name}`) declares no `provenance` field at all, unlike every other type
62
+ * named above. A first pass at this family assumed that meant the DATA was gone; it is not —
63
+ * MEASURED (see mute-timing.ts's own file header for the full account): `@distilled.cloud/core`'s
64
+ * response decoding never runs a strict schema decode, only `JSON.parse` + a key-rename pass that
65
+ * passes any key a type doesn't model through verbatim, so `provenance` survives on the actual
66
+ * runtime value even though the TS type doesn't know about it. `mute-timing.ts` reads it through
67
+ * a small local `& { provenance?: string }` intersection type rather than the SDK's own.
68
+ */
69
+ import * as Effect from 'effect/Effect';
70
+ import { GrafanaProvisionedObjectError } from './provisioned.ts';
71
+ export { GrafanaProvisionedObjectError };
72
+ /**
73
+ * True when `provenance` names an owner other than "this API" — INCLUDING when Grafana did not
74
+ * report the field at all (`undefined`). `undefined` is deliberately NEVER folded into `''`
75
+ * (`ProvenanceNone`) here — see the file header's "fails open" note. Only a value Grafana
76
+ * explicitly reported as `""` or `"api"` is writable.
77
+ */
78
+ export declare const isForeignProvenance: (provenance: string | undefined) => boolean;
79
+ export declare const refuseIfForeignProvenance: (kind: 'Grafana.ContactPoint' | 'Grafana.MessageTemplate' | 'Grafana.MuteTiming' | 'Grafana.AlertRuleGroup' | 'Grafana.NotificationPolicy', id: string, provenance: string | undefined) => Effect.Effect<never, GrafanaProvisionedObjectError>;
80
+ //# sourceMappingURL=alerting-provenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alerting-provenance.d.ts","sourceRoot":"","sources":["../../src/grafana/alerting-provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAEjE,OAAO,EAAE,6BAA6B,EAAE,CAAC;AAIzC;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,eAAgB,MAAM,GAAG,SAAS,KAAG,OACF,CAAC;AAEpE,eAAO,MAAM,yBAAyB,SAEhC,sBAAsB,GACtB,yBAAyB,GACzB,oBAAoB,GACpB,wBAAwB,GACxB,4BAA4B,MAC5B,MAAM,cACE,MAAM,GAAG,SAAS,KAC7B,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,6BAA6B,CAepD,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared fixtures for `contact-point.test.ts`/`contact-point-secrets.test.ts`/
3
+ * `contact-point-provenance.test.ts` — split across three files to keep each under the house's
4
+ * 250-line cap. Deliberately NOT named `*.test.ts`: importing one test file from another would
5
+ * make `bun test`'s glob discovery execute its `describe` blocks twice (once directly, once as an
6
+ * import side effect of the importing file) — this file has none to run, only plain data.
7
+ */
8
+ export declare const LIST_PATH = "/api/v1/provisioning/contact-points";
9
+ export declare const ITEM_PATH = "/api/v1/provisioning/contact-points/slack-oncall";
10
+ export declare const liveJson: {
11
+ disableResolveMessage: boolean;
12
+ name: string;
13
+ provenance: string;
14
+ settings: {
15
+ recipient: string;
16
+ };
17
+ type: string;
18
+ uid: string;
19
+ };
20
+ export declare const props: {
21
+ name: string;
22
+ settings: {
23
+ recipient: string;
24
+ };
25
+ type: string;
26
+ uid: string;
27
+ };
28
+ //# sourceMappingURL=contact-point-fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contact-point-fixtures.d.ts","sourceRoot":"","sources":["../../src/grafana/contact-point-fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,wCAAwC,CAAC;AAC/D,eAAO,MAAM,SAAS,qDAAqD,CAAC;AAE5E,eAAO,MAAM,QAAQ;IACnB,qBAAqB;IACrB,IAAI;IACJ,UAAU;IACV,QAAQ;QAAI,SAAS;;IACrB,IAAI;IACJ,GAAG;CACJ,CAAC;AAEF,eAAO,MAAM,KAAK;IAChB,IAAI;IACJ,QAAQ;QAAI,SAAS;;IACrB,IAAI;IACJ,GAAG;CACJ,CAAC"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `Grafana.ContactPoint` — one Grafana alerting contact point, keyed by `uid`.
3
+ *
4
+ * ⚠️ NO GET-BY-UID ROUTE EXISTS — MEASURED against the SDK's generated operations (grep across
5
+ * `services/grafana.ts` for `routeGet.*[Cc]ontactpoint`: only `routeGetContactpoints` (list,
6
+ * with an optional `name` filter) and `routeGetContactpointsExport`). Unlike
7
+ * `Grafana.Datasource`/`Grafana.Folder`/`Grafana.Dashboard`, `fetchLive` cannot key its read
8
+ * directly on `uid`. Filtering server-side by `name` is NOT a substitute: `name` is a grouping
9
+ * label a declaration can rename (`uid` is the only field this resource keys on, and Grafana's
10
+ * own doc calls `name` "used as grouping key in the UI" — several contact points can share one),
11
+ * so narrowing the GET to the DECLARED name would miss a live object still filed under an OLDER
12
+ * name mid-rename, reporting it absent and attempting a duplicate `create`. `fetchLive` instead
13
+ * lists every contact point (`routeGetContactpoints({})`, no filter) and matches `uid`
14
+ * client-side — the same ambiguity `resource.ts`'s header and netbox's `soleMatch` name, resolved
15
+ * the same way: requiring the one field the API is actually keyed on.
16
+ *
17
+ * ⛔ SECRET SETTINGS NEVER APPEAR AS A LITERAL PROP (S25) — same shape as `Grafana.Datasource`'s
18
+ * `secureJsonDataRefs` (`secret-refs.ts`, shared). Grafana does not use a separate top-level
19
+ * "secure settings" field for contact points the way it does for datasources; a secure key (a
20
+ * webhook URL with an embedded token, an API key, …) lives INSIDE `settings` on write, and comes
21
+ * back inside `settings` REDACTED on read — Grafana's own docs for the contact-points EXPORT
22
+ * route say as much explicitly ("Redacted settings will contain RedactedValue instead"; the
23
+ * plain, non-export GET this resource calls has no `decrypt` option at all, so it is redacted
24
+ * unconditionally). `secureSettingsRefs` therefore names which keys of `settings` are secret —
25
+ * value resolved from an env var NAME fresh inside `create`/`update`'s own effect (S24), merged
26
+ * into `settings` only at that point, never stored and never diffed (see `matches` below).
27
+ *
28
+ * ⛔ A FOREIGN-PROVENANCE CONTACT POINT REFUSES EVERY UPDATE AND DESTROY — `alerting-provenance.ts`,
29
+ * shared with `Grafana.MessageTemplate`/`Grafana.AlertRuleGroup`/`Grafana.NotificationPolicy`.
30
+ * See that file's header for the full reasoning and citations. `create` is never at risk: it only
31
+ * runs when `fetchLive` already found nothing with this `uid`.
32
+ *
33
+ * ⚠️ `X-Disable-Provenance` IS NEVER SET, BY DESIGN. Every create/update through this resource
34
+ * therefore leaves the contact point with `provenance: "api"` (Grafana's own default for an
35
+ * object this API just wrote) — which the docs describe as blocking that same object from being
36
+ * edited in the Grafana UI afterward. That is the intended outcome, not an oversight: an object
37
+ * this family declares is meant to be owned by the declaration, the same "one owner per resource"
38
+ * posture `provisioned.ts` establishes for folders/dashboards — handing it back to UI-editability
39
+ * by default would silently create the two-owner situation this whole file exists to prevent.
40
+ */
41
+ import { Resource } from 'alchemy';
42
+ import * as Provider from 'alchemy/Provider';
43
+ import * as grafana from '@distilled.cloud/grafana';
44
+ import * as Effect from 'effect/Effect';
45
+ import { type GrafanaProvisionedObjectError } from './alerting-provenance.ts';
46
+ import { type GrafanaSpec } from './resource.ts';
47
+ import { GrafanaSecretRefUnsetError } from './secret-refs.ts';
48
+ export { GrafanaSecretRefUnsetError };
49
+ export interface ContactPointProps {
50
+ uid: string;
51
+ name: string;
52
+ /** Grafana integration id — `slack`, `webhook`, `email`, `pagerduty`, `opsgenie`, … */
53
+ type: string;
54
+ /** Non-secret settings — shape depends on `type`. Never put a secret VALUE here — declare it
55
+ * in `secureSettingsRefs` instead; see the file header. */
56
+ settings: Record<string, unknown>;
57
+ /** Secure field name (e.g. `url`, `token`, `password`) -> env var NAME holding its value. */
58
+ secureSettingsRefs?: Record<string, string>;
59
+ disableResolveMessage?: boolean;
60
+ }
61
+ export interface ContactPointAttributes {
62
+ uid: string;
63
+ name: string;
64
+ type: string;
65
+ /** As Grafana returns it — any secure key is a redaction placeholder, never the real value. */
66
+ settings: Record<string, unknown>;
67
+ disableResolveMessage: boolean;
68
+ /** `""`/`"api"` (this family may write); anything else, INCLUDING `undefined` (Grafana didn't
69
+ * report the field), refuses — see alerting-provenance.ts's "fails open" note. */
70
+ provenance: string | undefined;
71
+ }
72
+ export interface GrafanaContactPoint extends Resource<'Grafana.ContactPoint', ContactPointProps, ContactPointAttributes, never> {
73
+ }
74
+ export declare const GrafanaContactPoint: import("alchemy").ResourceClass<GrafanaContactPoint>;
75
+ export declare const spec: GrafanaSpec<ContactPointProps, grafana.EmbeddedContactPoint, ContactPointAttributes, grafana.RouteGetContactpointsError | grafana.RoutePostContactpointsError | grafana.RoutePutContactpointError | grafana.RouteDeleteContactpointsError | GrafanaProvisionedObjectError | GrafanaSecretRefUnsetError>;
76
+ export declare const handlers: {
77
+ list: () => Effect.Effect<never[], never, never>;
78
+ read: ({ olds }: {
79
+ olds: ContactPointProps;
80
+ }) => Effect.Effect<ContactPointAttributes | undefined, grafana.BadGateway | import("@distilled.cloud/caddy").BadRequest | grafana.BadRequest | grafana.ConfigError | import("@distilled.cloud/caddy").Conflict | grafana.Conflict | import("@distilled.cloud/caddy").Forbidden | grafana.Forbidden | grafana.GatewayTimeout | GrafanaProvisionedObjectError | GrafanaSecretRefUnsetError | import("effect/unstable/http/HttpClientError").HttpClientError | grafana.InternalServerError | grafana.Locked | import("@distilled.cloud/caddy").NotFound | grafana.ServiceUnavailable | grafana.TooManyRequests | grafana.Unauthorized | grafana.UnknownGrafanaError | import("@distilled.cloud/caddy").UnprocessableEntity, grafana.GrafanaOpContext>;
81
+ diff: ({ news, output }: {
82
+ news: import("alchemy").Input<ContactPointProps>;
83
+ output: ContactPointAttributes | undefined;
84
+ }) => Effect.Effect<{
85
+ readonly action: 'update';
86
+ } | {
87
+ readonly action: 'noop';
88
+ } | {
89
+ readonly action: 'replace';
90
+ } | undefined, grafana.BadGateway | import("@distilled.cloud/caddy").BadRequest | grafana.BadRequest | grafana.ConfigError | import("@distilled.cloud/caddy").Conflict | grafana.Conflict | import("@distilled.cloud/caddy").Forbidden | grafana.Forbidden | grafana.GatewayTimeout | GrafanaProvisionedObjectError | GrafanaSecretRefUnsetError | import("effect/unstable/http/HttpClientError").HttpClientError | grafana.InternalServerError | grafana.Locked | import("@distilled.cloud/caddy").NotFound | grafana.ServiceUnavailable | grafana.TooManyRequests | grafana.Unauthorized | grafana.UnknownGrafanaError | import("@distilled.cloud/caddy").UnprocessableEntity, grafana.GrafanaOpContext>;
91
+ reconcile: ({ news }: {
92
+ news: ContactPointProps;
93
+ }) => Effect.Effect<ContactPointAttributes, grafana.BadGateway | import("@distilled.cloud/caddy").BadRequest | grafana.BadRequest | grafana.ConfigError | import("@distilled.cloud/caddy").Conflict | grafana.Conflict | import("@distilled.cloud/caddy").Forbidden | grafana.Forbidden | grafana.GatewayTimeout | GrafanaProvisionedObjectError | GrafanaSecretRefUnsetError | import("effect/unstable/http/HttpClientError").HttpClientError | grafana.InternalServerError | grafana.Locked | import("@distilled.cloud/caddy").NotFound | grafana.ServiceUnavailable | grafana.TooManyRequests | grafana.Unauthorized | grafana.UnknownGrafanaError | import("@distilled.cloud/caddy").UnprocessableEntity, grafana.GrafanaOpContext>;
94
+ delete: ({ olds }: {
95
+ olds: ContactPointProps;
96
+ }) => Effect.Effect<void, grafana.BadGateway | import("@distilled.cloud/caddy").BadRequest | grafana.BadRequest | grafana.ConfigError | import("@distilled.cloud/caddy").Conflict | grafana.Conflict | import("@distilled.cloud/caddy").Forbidden | grafana.Forbidden | grafana.GatewayTimeout | GrafanaProvisionedObjectError | GrafanaSecretRefUnsetError | import("effect/unstable/http/HttpClientError").HttpClientError | grafana.InternalServerError | grafana.Locked | import("@distilled.cloud/caddy").NotFound | grafana.ServiceUnavailable | grafana.TooManyRequests | grafana.Unauthorized | grafana.UnknownGrafanaError | import("@distilled.cloud/caddy").UnprocessableEntity, grafana.GrafanaOpContext>;
97
+ };
98
+ export declare const GrafanaContactPointProvider: () => import("effect/Layer").Layer<Provider.Provider<GrafanaContactPoint>, never, grafana.GrafanaOpContext>;
99
+ //# sourceMappingURL=contact-point.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contact-point.d.ts","sourceRoot":"","sources":["../../src/grafana/contact-point.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,OAAO,MAAM,0BAA0B,CAAC;AACpD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,6BAA6B,EAGnC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,eAAe,CAAC;AAClE,OAAO,EAAE,0BAA0B,EAAqB,MAAM,kBAAkB,CAAC;AAGjF,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAEtC,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb;gEAC4D;IAC5D,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,6FAA6F;IAC7F,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,+FAA+F;IAC/F,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,qBAAqB,EAAE,OAAO,CAAC;IAC/B;uFACmF;IACnF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,mBAAoB,SAAQ,QAAQ,CACnD,sBAAsB,EACtB,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,CACN;CAAG;AAEJ,eAAO,MAAM,mBAAmB,sDAAwD,CAAC;AAoBzF,eAAO,MAAM,IAAI,EAAE,WAAW,CAC5B,iBAAiB,EACjB,OAAO,CAAC,oBAAoB,EAC5B,sBAAsB,EACpB,OAAO,CAAC,0BAA0B,GAClC,OAAO,CAAC,2BAA2B,GACnC,OAAO,CAAC,yBAAyB,GACjC,OAAO,CAAC,6BAA6B,GACrC,6BAA6B,GAC7B,0BAA0B,CAyD7B,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;CAAwB,CAAC;AAE9C,eAAO,MAAM,2BAA2B,6GACyD,CAAC"}
@@ -35,43 +35,13 @@ export declare const normalizeModel: (model: DashboardModel, uid: string) => Das
35
35
  * normalized model is, in dashboard.ts's `matches`, so a title-only mismatch still shows `update`. */
36
36
  export declare const modelTitle: (model: DashboardModel) => string;
37
37
  /**
38
- * ★ `declared` NEED ONLY BE A SUBSET OF `live` — an adversarial review of this PR (2026-09-24)
39
- * found that plain structural equality made an ordinary, never-edited dashboard show `update`
40
- * FOREVER: Grafana's own dashboard-save schema migration rewrites any panel or target whose
41
- * `datasource` is absent into an explicit `{type, uid}` reference to the resolved default
42
- * datasource — documented Grafana schema-migration behavior (not re-measured against a live
43
- * instance in this PR — no live calls). A declared panel that never sets `datasource` (the
44
- * common case: hand-authored JSON, or anything relying on the dashboard-level default) would,
45
- * after the FIRST create, read back with that field populated; plain `deepEqual` (this file used
46
- * `alchemy/Diff`'s before this fix) then disagreed with the declaration on every subsequent plan,
47
- * `update` sent the undecorated model back, and Grafana redecorated it again on the next read —
48
- * non-convergent.
49
- *
50
- * This function instead asks only "does every field the DECLARATION mentions, at any depth, still
51
- * match what is live" — `live` may carry additional fields, at any depth, that `declared` never
52
- * mentioned (the datasource auto-decoration above, or any future Grafana-injected default,
53
- * `fieldConfig` defaults and `pluginVersion` included) without being compared at all. A field
54
- * `declared` DOES mention that is missing, or different, in `live` still fails the match — real
55
- * content drift is still caught exactly as before; only fields the declaration is silent about are
56
- * now tolerated rather than compared.
57
- *
58
- * ⚠️ THE TRADE-OFF THIS ACCEPTS, DELIBERATELY: removing a field from the declaration (rather than
59
- * changing its value) can no longer force Grafana to drop it. If a panel once declared an
60
- * explicit `datasource` and a later declaration deletes that key entirely (rather than setting a
61
- * different one), `live` keeps whatever the last write set — the declaration is silent about that
62
- * key now, so nothing compares it, so no `update` ever fires to change it. This is the same
63
- * "undeclared means don't care, never means force-to-default" convention already used everywhere
64
- * else in this kit for a plain optional prop (e.g. `datasource.ts`'s `matches` never treats an
65
- * undeclared `access` as "must be unset"); it now also applies inside the JSON model, not just at
66
- * the props level. Declare an explicit replacement value to change a field; omission never clears
67
- * one that was set by a previous declaration or by Grafana itself.
68
- *
69
- * ⚠️ ARRAYS COMPARE BY LENGTH, THEN ELEMENT-WISE SUBSET — never by a looser "declared has fewer
70
- * items" rule. `panels`/`targets` are position-significant (see `normalizeModel`'s header); an
71
- * array Grafana pads or reorders on its own would still need normalizing here explicitly, the
72
- * same as any other newly-discovered auto-decoration — this function does not paper over that.
73
- * ⚠️ `null` ON EITHER SIDE OF A LEAF NEVER MISMATCHES (mirrors `alchemy/Diff`'s `deepEqual` with
74
- * `stripNullish: true`, which this function replaces for the dashboard model specifically).
38
+ * `declared` need only be a subset of `live` — dashboard.ts's `matches` runs `normalizeModel`'s
39
+ * output through this. Moved to `subset-match.ts` once `Grafana.ContactPoint`'s `settings` needed
40
+ * the identical "declaration silent about a field means don't compare it" rule; re-exported here
41
+ * under its original name so this file's own callers and dashboard-model.test.ts are unchanged.
42
+ * See subset-match.ts for the full reasoning, including the regression this exists to fix
43
+ * (Grafana's dashboard-save schema migration auto-decorating a panel's `datasource`) and the
44
+ * trade-off it accepts (an omitted field can no longer force-clear a previously-set one).
75
45
  */
76
- export declare const declaredContentMatches: (declared: unknown, live: unknown) => boolean;
46
+ export { declaredContentMatches } from './subset-match.ts';
77
47
  //# sourceMappingURL=dashboard-model.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-model.d.ts","sourceRoot":"","sources":["../../src/grafana/dashboard-model.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAKrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,cAAc,UAAW,cAAc,OAAO,MAAM,KAAG,cASnE,CAAC;AAEF;uGACuG;AACvG,eAAO,MAAM,UAAU,UAAW,cAAc,KAAG,MACC,CAAC;AAKrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,sBAAsB,aAAc,OAAO,QAAQ,OAAO,KAAG,OAgBzE,CAAC"}
1
+ {"version":3,"file":"dashboard-model.d.ts","sourceRoot":"","sources":["../../src/grafana/dashboard-model.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAKrD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,cAAc,UAAW,cAAc,OAAO,MAAM,KAAG,cASnE,CAAC;AAEF;uGACuG;AACvG,eAAO,MAAM,UAAU,UAAW,cAAc,KAAG,MACC,CAAC;AAErD;;;;;;;;GAQG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -22,6 +22,8 @@ import * as Provider from 'alchemy/Provider';
22
22
  import * as grafana from '@distilled.cloud/grafana';
23
23
  import * as Effect from 'effect/Effect';
24
24
  import { type GrafanaSpec } from './resource.ts';
25
+ import { GrafanaSecretRefUnsetError } from './secret-refs.ts';
26
+ export { GrafanaSecretRefUnsetError };
25
27
  export interface DatasourceProps {
26
28
  uid: string;
27
29
  name: string;
@@ -54,14 +56,6 @@ export interface DatasourceAttributes {
54
56
  export interface GrafanaDatasource extends Resource<'Grafana.Datasource', DatasourceProps, DatasourceAttributes, never> {
55
57
  }
56
58
  export declare const GrafanaDatasource: import("alchemy").ResourceClass<GrafanaDatasource>;
57
- declare const GrafanaSecretRefUnsetError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
58
- readonly _tag: "GrafanaSecretRefUnsetError";
59
- } & Readonly<A>;
60
- /** A named secure-field env var is unset — a domain refusal, not a distilled error. */
61
- export declare class GrafanaSecretRefUnsetError extends GrafanaSecretRefUnsetError_base<{
62
- readonly message: string;
63
- }> {
64
- }
65
59
  export declare const spec: GrafanaSpec<DatasourceProps, grafana.DataSource, DatasourceAttributes, grafana.AddDataSourceError | grafana.GetDataSourceByUIDError | grafana.UpdateDataSourceByUIDError | grafana.DeleteDataSourceByUIDError | GrafanaSecretRefUnsetError>;
66
60
  export declare const handlers: {
67
61
  list: () => Effect.Effect<never[], never, never>;
@@ -86,5 +80,4 @@ export declare const handlers: {
86
80
  }) => Effect.Effect<void, grafana.BadGateway | import("@distilled.cloud/caddy").BadRequest | grafana.BadRequest | grafana.ConfigError | import("@distilled.cloud/caddy").Conflict | grafana.Conflict | import("@distilled.cloud/caddy").Forbidden | grafana.Forbidden | grafana.GatewayTimeout | GrafanaSecretRefUnsetError | import("effect/unstable/http/HttpClientError").HttpClientError | grafana.InternalServerError | grafana.Locked | import("@distilled.cloud/caddy").NotFound | grafana.NotFound | grafana.ServiceUnavailable | grafana.TooManyRequests | grafana.Unauthorized | grafana.UnknownGrafanaError | import("@distilled.cloud/caddy").UnprocessableEntity, grafana.GrafanaOpContext>;
87
81
  };
88
82
  export declare const GrafanaDatasourceProvider: () => import("effect/Layer").Layer<Provider.Provider<GrafanaDatasource>, never, grafana.GrafanaOpContext>;
89
- export {};
90
83
  //# sourceMappingURL=datasource.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"datasource.d.ts","sourceRoot":"","sources":["../../src/grafana/datasource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,OAAO,MAAM,0BAA0B,CAAC;AAEpD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,eAAe,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,8FAA8F;IAC9F,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAkB,SAAQ,QAAQ,CACjD,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,KAAK,CACN;CAAG;AAEJ,eAAO,MAAM,iBAAiB,oDAAoD,CAAC;;;;AAEnF,uFAAuF;AACvF,qBAAa,0BAA2B,SAAQ,gCAA+C;IAC7F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;AAwCL,eAAO,MAAM,IAAI,EAAE,WAAW,CAC5B,eAAe,EACf,OAAO,CAAC,UAAU,EAClB,oBAAoB,EAClB,OAAO,CAAC,kBAAkB,GAC1B,OAAO,CAAC,uBAAuB,GAC/B,OAAO,CAAC,0BAA0B,GAClC,OAAO,CAAC,0BAA0B,GAClC,0BAA0B,CAgD7B,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;CAAwB,CAAC;AAE9C,eAAO,MAAM,yBAAyB,2GACuD,CAAC"}
1
+ {"version":3,"file":"datasource.d.ts","sourceRoot":"","sources":["../../src/grafana/datasource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,OAAO,MAAM,0BAA0B,CAAC;AACpD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,eAAe,CAAC;AAClE,OAAO,EAAE,0BAA0B,EAAqB,MAAM,kBAAkB,CAAC;AAEjF,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAEtC,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,8FAA8F;IAC9F,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAkB,SAAQ,QAAQ,CACjD,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,KAAK,CACN;CAAG;AAEJ,eAAO,MAAM,iBAAiB,oDAAoD,CAAC;AAqBnF,eAAO,MAAM,IAAI,EAAE,WAAW,CAC5B,eAAe,EACf,OAAO,CAAC,UAAU,EAClB,oBAAoB,EAClB,OAAO,CAAC,kBAAkB,GAC1B,OAAO,CAAC,uBAAuB,GAC/B,OAAO,CAAC,0BAA0B,GAClC,OAAO,CAAC,0BAA0B,GAClC,0BAA0B,CAgD7B,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;CAAwB,CAAC;AAE9C,eAAO,MAAM,yBAAyB,2GACuD,CAAC"}
@@ -6,14 +6,20 @@
6
6
  * dashboard-model.ts, fake-grafana.ts) are internals a provider needs but a consumer should not
7
7
  * depend on.
8
8
  *
9
- * `Grafana.Datasource`, `Grafana.Folder` and `Grafana.Dashboard` ship here. `Grafana.AlertRule`
10
- * and friends are still follow-up work on the same now-available SDK operations — see
11
- * docs/grafana.md and docs/grafana-folder-dashboard.md for what shipped and what is still open.
9
+ * `Grafana.Datasource`, `Grafana.Folder`, `Grafana.Dashboard`, `Grafana.ContactPoint`,
10
+ * `Grafana.MuteTiming` and `Grafana.MessageTemplate` ship here. `Grafana.AlertRuleGroup` and
11
+ * `Grafana.NotificationPolicy` are stacked follow-up PRs on the same SDK operations — see
12
+ * docs/grafana.md, docs/grafana-folder-dashboard.md and docs/grafana-alerting.md for what shipped
13
+ * and what is still open.
12
14
  */
13
- export { type DatasourceAttributes, type DatasourceProps, GrafanaDatasource, GrafanaDatasourceProvider, GrafanaSecretRefUnsetError, } from './datasource.ts';
15
+ export { type DatasourceAttributes, type DatasourceProps, GrafanaDatasource, GrafanaDatasourceProvider, } from './datasource.ts';
14
16
  export { type DashboardAttributes, type DashboardModel, type DashboardProps, GrafanaDashboard, GrafanaDashboardProvider, } from './dashboard.ts';
15
17
  export { type FolderAttributes, type FolderProps, GrafanaFolder, GrafanaFolderProvider, GrafanaFolderReparentError, } from './folder.ts';
18
+ export { type ContactPointAttributes, type ContactPointProps, GrafanaContactPoint, GrafanaContactPointProvider, } from './contact-point.ts';
19
+ export { type MessageTemplateAttributes, type MessageTemplateProps, GrafanaMessageTemplate, GrafanaMessageTemplateProvider, } from './message-template.ts';
20
+ export { type MuteTimingAttributes, type MuteTimingProps, GrafanaMuteTiming, GrafanaMuteTimingProvider, } from './mute-timing.ts';
16
21
  export { GrafanaProvisionedObjectError } from './provisioned.ts';
22
+ export { GrafanaSecretRefUnsetError } from './secret-refs.ts';
17
23
  export { type GrafanaTarget, grafanaCredentials } from './credentials.ts';
18
24
  export { grafanaProviders } from './providers.ts';
19
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/grafana/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,0BAA0B,GAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,aAAa,EACb,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,aAAa,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/grafana/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,aAAa,EACb,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,KAAK,aAAa,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}