@homeflare/alchemy 0.31.2 → 0.33.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 (184) hide show
  1. package/README.md +39 -3
  2. package/dist/grafana/dashboard-model.d.ts +77 -0
  3. package/dist/grafana/dashboard-model.d.ts.map +1 -0
  4. package/dist/grafana/dashboard.d.ts +98 -0
  5. package/dist/grafana/dashboard.d.ts.map +1 -0
  6. package/dist/grafana/folder.d.ts +100 -0
  7. package/dist/grafana/folder.d.ts.map +1 -0
  8. package/dist/grafana/index.d.ts +9 -8
  9. package/dist/grafana/index.d.ts.map +1 -1
  10. package/dist/grafana/index.js +158 -6
  11. package/dist/grafana/index.js.map +8 -4
  12. package/dist/grafana/providers.d.ts +1 -1
  13. package/dist/grafana/providers.d.ts.map +1 -1
  14. package/dist/grafana/provisioned.d.ts +11 -0
  15. package/dist/grafana/provisioned.d.ts.map +1 -0
  16. package/dist/linux/container-form.d.ts +129 -0
  17. package/dist/linux/container-form.d.ts.map +1 -0
  18. package/dist/linux/container-generator.d.ts +74 -0
  19. package/dist/linux/container-generator.d.ts.map +1 -0
  20. package/dist/linux/container-handlers.d.ts +15 -0
  21. package/dist/linux/container-handlers.d.ts.map +1 -0
  22. package/dist/linux/container-lifecycle.d.ts +37 -0
  23. package/dist/linux/container-lifecycle.d.ts.map +1 -0
  24. package/dist/linux/container-preflight.d.ts +34 -0
  25. package/dist/linux/container-preflight.d.ts.map +1 -0
  26. package/dist/linux/container-secrets.d.ts +37 -0
  27. package/dist/linux/container-secrets.d.ts.map +1 -0
  28. package/dist/linux/container-settle.d.ts +34 -0
  29. package/dist/linux/container-settle.d.ts.map +1 -0
  30. package/dist/linux/container.d.ts +30 -0
  31. package/dist/linux/container.d.ts.map +1 -0
  32. package/dist/linux/fake-quadlet.d.ts +25 -0
  33. package/dist/linux/fake-quadlet.d.ts.map +1 -0
  34. package/dist/linux/index.d.ts +6 -3
  35. package/dist/linux/index.d.ts.map +1 -1
  36. package/dist/linux/index.js +641 -268
  37. package/dist/linux/index.js.map +15 -7
  38. package/dist/linux/providers.d.ts +1 -1
  39. package/dist/linux/providers.d.ts.map +1 -1
  40. package/dist/linux/systemctl.d.ts +7 -1
  41. package/dist/linux/systemctl.d.ts.map +1 -1
  42. package/dist/opnsense/alias-form.d.ts +29 -0
  43. package/dist/opnsense/alias-form.d.ts.map +1 -0
  44. package/dist/opnsense/alias.d.ts +81 -0
  45. package/dist/opnsense/alias.d.ts.map +1 -0
  46. package/dist/opnsense/category-form.d.ts +10 -0
  47. package/dist/opnsense/category-form.d.ts.map +1 -0
  48. package/dist/opnsense/category.d.ts +58 -0
  49. package/dist/opnsense/category.d.ts.map +1 -0
  50. package/dist/opnsense/credentials.d.ts +36 -0
  51. package/dist/opnsense/credentials.d.ts.map +1 -0
  52. package/dist/opnsense/fake-opnsense.d.ts +51 -0
  53. package/dist/opnsense/fake-opnsense.d.ts.map +1 -0
  54. package/dist/opnsense/group-form.d.ts +11 -0
  55. package/dist/opnsense/group-form.d.ts.map +1 -0
  56. package/dist/opnsense/group.d.ts +62 -0
  57. package/dist/opnsense/group.d.ts.map +1 -0
  58. package/dist/opnsense/index.d.ts +16 -0
  59. package/dist/opnsense/index.d.ts.map +1 -0
  60. package/dist/opnsense/index.js +211 -0
  61. package/dist/opnsense/index.js.map +20 -0
  62. package/dist/opnsense/policy.d.ts +20 -0
  63. package/dist/opnsense/policy.d.ts.map +1 -0
  64. package/dist/opnsense/providers.d.ts +13 -0
  65. package/dist/opnsense/providers.d.ts.map +1 -0
  66. package/dist/opnsense/resource.d.ts +74 -0
  67. package/dist/opnsense/resource.d.ts.map +1 -0
  68. package/dist/opnsense/wire.d.ts +39 -0
  69. package/dist/opnsense/wire.d.ts.map +1 -0
  70. package/dist/proxmox/index.js +170 -80
  71. package/dist/proxmox/index.js.map +9 -7
  72. package/dist/proxmox/storage-form.d.ts +54 -0
  73. package/dist/proxmox/storage-form.d.ts.map +1 -0
  74. package/dist/proxmox/storage-wire.d.ts +56 -0
  75. package/dist/proxmox/storage-wire.d.ts.map +1 -0
  76. package/dist/proxmox/storage.d.ts +55 -60
  77. package/dist/proxmox/storage.d.ts.map +1 -1
  78. package/dist/proxmox/zfs-pool-form.d.ts +124 -0
  79. package/dist/proxmox/zfs-pool-form.d.ts.map +1 -0
  80. package/dist/proxmox/zfs-pool-wire.d.ts +78 -0
  81. package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -0
  82. package/dist/proxmox/zfs-pool.d.ts +27 -52
  83. package/dist/proxmox/zfs-pool.d.ts.map +1 -1
  84. package/dist/unifi/fake-unifi.d.ts +48 -0
  85. package/dist/unifi/fake-unifi.d.ts.map +1 -0
  86. package/dist/unifi/firewall-zone-form.d.ts +40 -0
  87. package/dist/unifi/firewall-zone-form.d.ts.map +1 -0
  88. package/dist/unifi/firewall-zone.d.ts +50 -0
  89. package/dist/unifi/firewall-zone.d.ts.map +1 -0
  90. package/dist/unifi/index.d.ts +13 -0
  91. package/dist/unifi/index.d.ts.map +1 -0
  92. package/dist/unifi/index.js +190 -0
  93. package/dist/unifi/index.js.map +16 -0
  94. package/dist/unifi/network-form.d.ts +67 -0
  95. package/dist/unifi/network-form.d.ts.map +1 -0
  96. package/dist/unifi/network.d.ts +67 -0
  97. package/dist/unifi/network.d.ts.map +1 -0
  98. package/dist/unifi/policy.d.ts +20 -0
  99. package/dist/unifi/policy.d.ts.map +1 -0
  100. package/dist/unifi/providers.d.ts +13 -0
  101. package/dist/unifi/providers.d.ts.map +1 -0
  102. package/dist/unifi/resource.d.ts +64 -0
  103. package/dist/unifi/resource.d.ts.map +1 -0
  104. package/docs/grafana-folder-dashboard.md +145 -0
  105. package/docs/grafana.md +11 -10
  106. package/docs/opnsense.md +146 -0
  107. package/docs/provider-standard.md +1 -1
  108. package/docs/quadlet-container.md +132 -0
  109. package/docs/unifi.md +93 -0
  110. package/docs/upstream-conformance.md +9 -4
  111. package/package.json +12 -2
  112. package/src/grafana/dashboard-model.test.ts +111 -0
  113. package/src/grafana/dashboard-model.ts +111 -0
  114. package/src/grafana/dashboard.test.ts +232 -0
  115. package/src/grafana/dashboard.ts +161 -0
  116. package/src/grafana/folder.test.ts +212 -0
  117. package/src/grafana/folder.ts +161 -0
  118. package/src/grafana/index.ts +21 -8
  119. package/src/grafana/providers.ts +7 -1
  120. package/src/grafana/provisioned.ts +49 -0
  121. package/src/linux/container-fixture.test.ts +92 -0
  122. package/src/linux/container-form.ts +243 -0
  123. package/src/linux/container-generator.test.ts +195 -0
  124. package/src/linux/container-generator.ts +113 -0
  125. package/src/linux/container-handlers.ts +73 -0
  126. package/src/linux/container-lifecycle.test.ts +209 -0
  127. package/src/linux/container-lifecycle.ts +217 -0
  128. package/src/linux/container-preflight.ts +97 -0
  129. package/src/linux/container-secrets.test.ts +171 -0
  130. package/src/linux/container-secrets.ts +112 -0
  131. package/src/linux/container-settle.ts +97 -0
  132. package/src/linux/container.ts +60 -0
  133. package/src/linux/fake-quadlet.ts +127 -0
  134. package/src/linux/index.ts +11 -3
  135. package/src/linux/providers.ts +2 -0
  136. package/src/linux/systemctl.ts +9 -0
  137. package/src/opnsense/alias-form.test.ts +89 -0
  138. package/src/opnsense/alias-form.ts +53 -0
  139. package/src/opnsense/alias.test.ts +203 -0
  140. package/src/opnsense/alias.ts +117 -0
  141. package/src/opnsense/category-form.test.ts +54 -0
  142. package/src/opnsense/category-form.ts +20 -0
  143. package/src/opnsense/category.test.ts +138 -0
  144. package/src/opnsense/category.ts +82 -0
  145. package/src/opnsense/credentials.ts +41 -0
  146. package/src/opnsense/fake-opnsense.ts +79 -0
  147. package/src/opnsense/group-form.test.ts +75 -0
  148. package/src/opnsense/group-form.ts +25 -0
  149. package/src/opnsense/group.test.ts +141 -0
  150. package/src/opnsense/group.ts +78 -0
  151. package/src/opnsense/index.ts +45 -0
  152. package/src/opnsense/policy.ts +49 -0
  153. package/src/opnsense/providers.ts +21 -0
  154. package/src/opnsense/resource.ts +146 -0
  155. package/src/opnsense/wire.ts +56 -0
  156. package/src/opnsense/write-refusal.test.ts +148 -0
  157. package/src/proxmox/constraints-host-forms.test.ts +1 -1
  158. package/src/proxmox/storage-form.ts +144 -0
  159. package/src/proxmox/storage-read-failure.test.ts +214 -0
  160. package/src/proxmox/storage-wire.ts +124 -0
  161. package/src/proxmox/storage.ts +165 -175
  162. package/src/proxmox/zfs-pool-adopt.test.ts +26 -0
  163. package/src/proxmox/zfs-pool-form.ts +236 -0
  164. package/src/proxmox/zfs-pool-read-failure.test.ts +118 -0
  165. package/src/proxmox/zfs-pool-wire.ts +132 -0
  166. package/src/proxmox/zfs-pool.ts +106 -76
  167. package/src/unifi/fake-unifi.ts +60 -0
  168. package/src/unifi/firewall-zone-form.ts +65 -0
  169. package/src/unifi/firewall-zone.test.ts +88 -0
  170. package/src/unifi/firewall-zone.ts +66 -0
  171. package/src/unifi/index.ts +26 -0
  172. package/src/unifi/network-form.ts +207 -0
  173. package/src/unifi/network.test.ts +209 -0
  174. package/src/unifi/network.ts +83 -0
  175. package/src/unifi/policy.ts +37 -0
  176. package/src/unifi/providers.ts +15 -0
  177. package/src/unifi/resource.test.ts +133 -0
  178. package/src/unifi/resource.ts +131 -0
  179. package/dist/proxmox/zfs-pool-spec.d.ts +0 -27
  180. package/dist/proxmox/zfs-pool-spec.d.ts.map +0 -1
  181. package/dist/proxmox/zfs-pool-write.d.ts +0 -128
  182. package/dist/proxmox/zfs-pool-write.d.ts.map +0 -1
  183. package/src/proxmox/zfs-pool-spec.ts +0 -76
  184. package/src/proxmox/zfs-pool-write.ts +0 -196
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/grafana/datasource.ts", "../src/grafana/resource.ts", "../src/grafana/credentials.ts", "../src/grafana/providers.ts"],
3
+ "sources": ["../src/grafana/datasource.ts", "../src/grafana/resource.ts", "../src/grafana/dashboard.ts", "../src/grafana/dashboard-model.ts", "../src/grafana/provisioned.ts", "../src/grafana/folder.ts", "../src/grafana/credentials.ts", "../src/grafana/providers.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * `Grafana.Datasource` — one Grafana data source, keyed by `uid`.\n *\n * ★ WHY `uid` IS REQUIRED, NOT GENERATED. `getDataSourceByUID`/`updateDataSourceByUID`/\n * `deleteDataSourceByUID` are all UID-keyed, and `getDataSources` (list) takes no filter — so\n * locating a datasource this resource didn't create means listing the whole org and matching by\n * name, the same ambiguity netbox/resource.ts's `soleMatch` exists to catch. Requiring `uid` up\n * front (Grafana accepts a caller-chosen one on create — `AddDataSourceRequest.uid`) sidesteps\n * it entirely: `read`/`reconcile`/`destroy` all key on the one field the API itself is keyed on.\n * `teslamate-grafana`'s live datasource (provisioned from a file, not this resource) has a\n * Grafana-generated uid; the census in the handoff script records it so a first declaration can\n * name it explicitly.\n *\n * ⛔ `secureJsonData` (e.g. a datasource password) NEVER APPEARS AS A LITERAL PROP (S25).\n * `secureJsonDataRefs` takes env var NAMES instead; `resolveSecureJsonData` reads them from\n * `process.env` fresh inside `create`/`update`'s own effect, never at plan time and never\n * returned in `attributes` — Grafana's own read API agrees: `DataSource.secureJsonFields` on a\n * GET is a map of booleans (is-it-set), never the value.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as grafana from '@distilled.cloud/grafana';\nimport * as Data from 'effect/Data';\nimport * as Effect from 'effect/Effect';\nimport { type GrafanaSpec, grafanaHandlers } from './resource.ts';\n\nexport interface DatasourceProps {\n uid: string;\n name: string;\n /** e.g. `postgres`, `prometheus`, `influxdb` — the Grafana plugin id. */\n type: string;\n url?: string;\n access?: 'proxy' | 'direct';\n database?: string;\n user?: string;\n basicAuth?: boolean;\n basicAuthUser?: string;\n isDefault?: boolean;\n withCredentials?: boolean;\n jsonData?: Record<string, unknown>;\n /** Secure field name (e.g. `password`) -> env var NAME holding its value. Never the value. */\n secureJsonDataRefs?: Record<string, string>;\n}\n\nexport interface DatasourceAttributes {\n uid: string;\n id: number;\n name: string;\n type: string;\n url: string;\n access: string;\n orgId: number;\n isDefault: boolean;\n readOnly: boolean;\n version: number;\n}\n\nexport interface GrafanaDatasource extends Resource<\n 'Grafana.Datasource',\n DatasourceProps,\n DatasourceAttributes,\n never\n> {}\n\nexport const GrafanaDatasource = Resource<GrafanaDatasource>('Grafana.Datasource');\n\n/** A named secure-field env var is unset — a domain refusal, not a distilled error. */\nexport class GrafanaSecretRefUnsetError extends Data.TaggedError('GrafanaSecretRefUnsetError')<{\n readonly message: string;\n}> {}\n\n/** Reads every named env var fresh, inside the caller's effect (S24) — never at plan time. */\nconst resolveSecureJsonData = (\n refs: Record<string, string> | undefined,\n): Effect.Effect<Record<string, string> | undefined, GrafanaSecretRefUnsetError> =>\n Effect.gen(function* () {\n if (refs === undefined) return undefined;\n const out: Record<string, string> = {};\n for (const [field, envVar] of Object.entries(refs)) {\n const raw = process.env[envVar];\n if (raw === undefined || raw.trim() === '') {\n return yield* Effect.fail(\n new GrafanaSecretRefUnsetError({\n message:\n `${envVar} is unset. Export the value of secureJsonData.${field} there — ` +\n 'never as an Alchemy prop.',\n }),\n );\n }\n out[field] = raw.trim();\n }\n return out;\n });\n\n/** `exactOptionalPropertyTypes` means an undeclared field must be OMITTED, never sent as `undefined`. */\nconst fields = (props: DatasourceProps) => ({\n name: props.name,\n type: props.type,\n ...(props.access === undefined ? {} : { access: props.access }),\n ...(props.basicAuth === undefined ? {} : { basicAuth: props.basicAuth }),\n ...(props.basicAuthUser === undefined ? {} : { basicAuthUser: props.basicAuthUser }),\n ...(props.database === undefined ? {} : { database: props.database }),\n ...(props.isDefault === undefined ? {} : { isDefault: props.isDefault }),\n ...(props.jsonData === undefined ? {} : { jsonData: props.jsonData }),\n ...(props.url === undefined ? {} : { url: props.url }),\n ...(props.user === undefined ? {} : { user: props.user }),\n ...(props.withCredentials === undefined ? {} : { withCredentials: props.withCredentials }),\n});\n\nexport const spec: GrafanaSpec<\n DatasourceProps,\n grafana.DataSource,\n DatasourceAttributes,\n | grafana.AddDataSourceError\n | grafana.GetDataSourceByUIDError\n | grafana.UpdateDataSourceByUIDError\n | grafana.DeleteDataSourceByUIDError\n | GrafanaSecretRefUnsetError\n> = {\n attributes: (live) => ({\n access: live.access ?? '',\n id: live.id ?? 0,\n isDefault: live.isDefault ?? false,\n name: live.name ?? '',\n orgId: live.orgId ?? 0,\n readOnly: live.readOnly ?? false,\n type: live.type ?? '',\n uid: live.uid ?? '',\n url: live.url ?? '',\n version: live.version ?? 0,\n }),\n create: (props) =>\n Effect.flatMap(resolveSecureJsonData(props.secureJsonDataRefs), (secureJsonData) =>\n grafana.addDataSource({\n ...fields(props),\n ...(secureJsonData === undefined ? {} : { secureJsonData }),\n uid: props.uid,\n }),\n ),\n destroy: (props) => grafana.deleteDataSourceByUID({ uid: props.uid }),\n fetchLive: (props) =>\n grafana\n .getDataSourceByUID({ uid: props.uid })\n .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),\n /**\n * ⛔ AN UNDECLARED FIELD IS NEITHER COMPARED NOR SENT — the same rule every family in this\n * estate follows. `secureJsonDataRefs` is never compared: Grafana never returns the value to\n * diff against, only whether a field is set (`secureJsonFields`), so this resource cannot\n * detect drift in a secret's value — only in whether one is configured at all.\n */\n matches: (attributes, props) =>\n (props.name === undefined || attributes.name === props.name) &&\n (props.type === undefined || attributes.type === props.type) &&\n (props.url === undefined || attributes.url === props.url) &&\n (props.access === undefined || attributes.access === props.access) &&\n (props.isDefault === undefined || attributes.isDefault === props.isDefault),\n update: (props, live) =>\n Effect.flatMap(resolveSecureJsonData(props.secureJsonDataRefs), (secureJsonData) =>\n grafana.updateDataSourceByUID({\n ...fields(props),\n ...(secureJsonData === undefined ? {} : { secureJsonData }),\n ...(live.version === undefined ? {} : { version: live.version }),\n uid: props.uid,\n }),\n ),\n};\n\nexport const handlers = grafanaHandlers(spec);\n\nexport const GrafanaDatasourceProvider = () =>\n Provider.effect(GrafanaDatasource, Effect.succeed(GrafanaDatasource.Provider.of(handlers)));\n",
6
6
  "/**\n * One shape for every Grafana object this family declares — mirroring `../forgejo/resource.ts`\n * and `../netbox/resource.ts` exactly, minus the credentials wrapping those two bake in.\n *\n * ★ CREDENTIALS ARE NOT PROVIDED HERE. Forgejo and NetBox each manage exactly one instance for\n * this house, so their engines close over a hardcoded `CredentialsFromEnv`. Grafana does not —\n * see `credentials.ts`'s note on `grafana.homeflare.dev` vs `teslamate-grafana` — so `Credentials`\n * stays a context requirement on every handler here, and `providers.ts`'s `grafanaProviders`\n * supplies it per target with `Layer.provide`, the same composition `litellm/providers.ts` uses.\n *\n * ⚠️ `NotFound` IS FOLDED TO \"ABSENT\" INSIDE EACH RESOURCE FILE'S OWN `fetchLive`, NOT HERE — the\n * same split forgejo's and netbox's engines use. `Effect.catchTag`'s tag-literal inference needs\n * a concrete error union to resolve `\"NotFound\"` against; a spec's `E` is only concrete at each\n * resource file's own call site.\n */\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport type { GrafanaOpContext } from '@distilled.cloud/grafana/Protocol';\nimport * as Effect from 'effect/Effect';\n\n/**\n * One Grafana object's distilled calls. `Live` is whatever the SDK decodes (a `DataSource`, …) —\n * no untyped `Record<string, unknown>`. `E` is left to each resource file to declare (the union\n * of the distilled operations it actually calls plus any domain refusal, e.g. an unset secret-ref\n * env var), so a call site's precise error union flows straight through instead of being widened\n * by hand here.\n */\nexport type GrafanaSpec<Props extends object, Live, Attributes, E> = {\n /** Already folds the SDK's `NotFound` to `undefined` via its own `catchTag` — see the note above. */\n readonly fetchLive: (props: Props) => Effect.Effect<Live | undefined, E, GrafanaOpContext>;\n readonly attributes: (live: Live, props: Props) => Attributes | undefined;\n readonly matches: (attributes: Attributes, props: Props) => boolean;\n readonly create: (props: Props) => Effect.Effect<unknown, E, GrafanaOpContext>;\n readonly update?: (props: Props, live: Live) => Effect.Effect<unknown, E, GrafanaOpContext>;\n readonly destroy: (props: Props, live: Live) => Effect.Effect<unknown, E, GrafanaOpContext>;\n};\n\nexport const grafanaOperations = <Props extends object, Live, Attributes, E>(\n spec: GrafanaSpec<Props, Live, Attributes, E>,\n) => {\n const read = (props: Props) =>\n spec\n .fetchLive(props)\n .pipe(Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))));\n\n return {\n read,\n\n diff: (news: Input<Props>, output: Attributes | undefined) =>\n Effect.gen(function* () {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* read(news);\n if (live === undefined) return { action: 'update' } as const;\n if (spec.matches(live, news)) return { action: 'noop' } as const;\n return spec.update === undefined\n ? ({ action: 'replace' } as const)\n : ({ action: 'update' } as const);\n }),\n\n /**\n * ★ OBSERVES BEFORE IT WRITES (S9, S10) — THE WHOLE OF \"ADOPT\" FOR THIS FAMILY. A `uid` this\n * Grafana instance already has is bound and, when it already matches the declaration,\n * left alone; only a genuinely absent `uid` is created. Declaring exactly what is live\n * changes nothing on `reconcile`, the same posture netbox/resource.ts documents as\n * \"adopt is the default posture, not a flag\".\n */\n reconcile: (news: Props) =>\n Effect.gen(function* () {\n const before = yield* spec.fetchLive(news);\n if (before === undefined) {\n yield* spec.create(news);\n } else if (spec.update !== undefined) {\n const mapped = spec.attributes(before, news);\n if (mapped === undefined || spec.matches(mapped, news)) {\n /* adoption or already converged — no write */\n } else {\n yield* spec.update(news, before);\n }\n }\n const after = yield* read(news);\n if (after === undefined) {\n return yield* Effect.die(\n new Error(\n 'grafanaOperations: the write returned no error but the object is still absent. ' +\n 'Read back rather than trusting the response status.',\n ),\n );\n }\n return after;\n }),\n\n destroy: (olds: Props) =>\n Effect.gen(function* () {\n const live = yield* spec.fetchLive(olds);\n if (live === undefined) return;\n yield* spec.destroy(olds, live);\n }),\n };\n};\n\n/**\n * The five provider handlers for a spec'd Grafana object, wired once. `Credentials` and\n * `HttpClient.HttpClient` stay ambient context requirements — see the file header.\n *\n * ⚠️ `list` answers empty — an org's full datasource list is not a page this family sweeps for\n * adoption; adoption stays explicit, the same reasoning forgejo's and netbox's engines give.\n */\nexport const grafanaHandlers = <Props extends object, Live, Attributes extends object, E>(\n spec: GrafanaSpec<Props, Live, Attributes, E>,\n) => {\n const ops = grafanaOperations(spec);\n return {\n list: () => Effect.succeed([]),\n read: ({ olds }: { olds: Props }) => ops.read(olds),\n diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>\n ops.diff(news, output),\n reconcile: ({ news }: { news: Props }) => ops.reconcile(news),\n delete: ({ olds }: { olds: Props }) => ops.destroy(olds),\n };\n};\n",
7
+ "/**\n * `Grafana.Dashboard` — one Grafana dashboard, keyed by `uid`. Same uid-required doctrine as\n * `datasource.ts`/`folder.ts` (`getDashboard`/`postDashboard`/`deleteDashboard` are all UID-keyed\n * on the wire — `postDashboard`'s own `uid` travels INSIDE the `dashboard` JSON body, not as a\n * request field, because Grafana's classic dashboard-save API has never had one; see\n * `dashboard-model.ts`'s `normalizeModel`, which is what puts it there).\n *\n * ★ THE DECLARED CONTENT IS THE DASHBOARD JSON MODEL — `DashboardProps.dashboard`, the same shape\n * Grafana's own \"Export as JSON\" produces. `dashboard-model.ts`'s `normalizeModel` strips the\n * volatile fields (`id`, `version`, `iteration`) and forces `uid` to this resource's own value\n * before ANY comparison, so a plan against an unchanged dashboard is a true noop despite those\n * fields moving on every live save — see that file for the full reasoning, including which\n * fields (`panels`, `targets`) are array-ORDER-significant and compared as such.\n *\n * ⛔ `matches` COMPARES \"IS THE DECLARATION A SUBSET OF WHAT IS LIVE\", NOT PLAIN EQUALITY —\n * `dashboard-model.ts`'s `declaredContentMatches`, added after an adversarial review of this PR\n * found plain `deepEqual` made an ordinary dashboard show `update` FOREVER: Grafana's own\n * schema-migration on save decorates any panel/target whose `datasource` is absent with an\n * explicit reference, so a declaration that never sets one would permanently disagree with what\n * it reads back. See that function's own header for the full reasoning and the trade-off it\n * accepts (an omitted field can no longer be used to CLEAR a previously-set one).\n *\n * ⛔ VERSION CONFLICTS (412) FAIL LOUDLY — NEVER OVERWRITTEN BLINDLY. `update` injects the\n * `version` this resource just read (`live.meta.version`, from the SAME `fetchLive` call\n * `resource.ts`'s `reconcile` already made before calling `update`) into the model it sends, and\n * `overwrite` is never set. If Grafana's own version check then still fails — someone else saved\n * in the gap between that read and this write — `postDashboard` answers `412` and\n * `PreconditionFailed` is left IN the error union below, uncaught: the whole reconcile fails, the\n * deploy stops, and nothing is written. The next run's `fetchLive` re-reads the new live version\n * fresh, which is the \"re-read\" half of \"re-read and fail loudly\" — there is no separate retry\n * loop in this file, because the next `plan`/`deploy` invocation already IS that re-read.\n *\n * ⛔ A PROVISIONED DASHBOARD (`meta.provisioned` — see provisioned.ts) REFUSES EVERY UPDATE AND\n * DESTROY, naming `meta.provisionedExternalId` (the file) when Grafana reports one. `matches`\n * still compares structurally regardless, so a mismatched declaration against a provisioned\n * dashboard shows `update` in a plan rather than a silent, permanently-hidden noop.\n *\n * ⚠️ NO RETAIN DEFAULT, UNLIKE `Grafana.Folder`. Deleting one dashboard is a single-object blast\n * radius — the same class `Grafana.Datasource` already accepts without a retain guard — not\n * folder.ts's multi-object cascade (a folder's delete takes its dashboards AND their alert rules\n * with it). See folder.ts's file header for the full contrast.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as grafana from '@distilled.cloud/grafana';\nimport * as Effect from 'effect/Effect';\nimport {\n type DashboardModel,\n declaredContentMatches,\n modelTitle,\n normalizeModel,\n} from './dashboard-model.ts';\nimport { type GrafanaProvisionedObjectError, refuseIfProvisioned } from './provisioned.ts';\nimport { type GrafanaSpec, grafanaHandlers } from './resource.ts';\n\nexport type { DashboardModel };\n\nexport interface DashboardProps {\n uid: string;\n /** Omit for the General/root folder. */\n folderUid?: string;\n /** The dashboard JSON model — see the file header. `id`/`version`/`iteration`/`uid` inside it\n * are ignored; `uid` above is the single source of truth. */\n dashboard: DashboardModel;\n}\n\nexport interface DashboardAttributes {\n uid: string;\n title: string;\n version: number;\n folderUid: string;\n url: string;\n /** True when Grafana reports this dashboard as file-provisioned (read-only — see provisioned.ts). */\n provisioned: boolean;\n provisionedExternalId: string;\n /** The live model, normalized the same way `matches` compares it. */\n dashboard: DashboardModel;\n}\n\nexport interface GrafanaDashboard extends Resource<\n 'Grafana.Dashboard',\n DashboardProps,\n DashboardAttributes,\n never\n> {}\n\nexport const GrafanaDashboard = Resource<GrafanaDashboard>('Grafana.Dashboard');\n\nconst modelOf = (live: grafana.DashboardFullWithMeta): DashboardModel =>\n live.dashboard !== null && typeof live.dashboard === 'object'\n ? (live.dashboard as DashboardModel)\n : {};\n\nexport const spec: GrafanaSpec<\n DashboardProps,\n grafana.DashboardFullWithMeta,\n DashboardAttributes,\n | grafana.GetDashboardError\n | grafana.PostDashboardError\n | grafana.DeleteDashboardError\n | GrafanaProvisionedObjectError\n> = {\n attributes: (live, props) => {\n const model = normalizeModel(modelOf(live), props.uid);\n return {\n dashboard: model,\n folderUid: live.meta?.folderUid ?? '',\n provisioned: live.meta?.provisioned ?? false,\n provisionedExternalId: live.meta?.provisionedExternalId ?? '',\n title: modelTitle(model),\n uid: props.uid,\n url: live.meta?.url ?? '',\n version: live.meta?.version ?? 0,\n };\n },\n create: (props) =>\n grafana.postDashboard({\n dashboard: normalizeModel(props.dashboard, props.uid),\n ...(props.folderUid === undefined ? {} : { folderUid: props.folderUid }),\n }),\n destroy: (props, live) =>\n Effect.gen(function* () {\n if (live.meta?.provisioned) {\n return yield* refuseIfProvisioned(\n 'Grafana.Dashboard',\n props.uid,\n live.meta.provisionedExternalId,\n );\n }\n yield* grafana.deleteDashboard({ uid: props.uid });\n }),\n fetchLive: (props) =>\n grafana\n .getDashboard({ uid: props.uid })\n .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),\n matches: (attributes, props) =>\n (props.folderUid === undefined || attributes.folderUid === props.folderUid) &&\n declaredContentMatches(normalizeModel(props.dashboard, props.uid), attributes.dashboard),\n update: (props, live) =>\n Effect.gen(function* () {\n if (live.meta?.provisioned) {\n return yield* refuseIfProvisioned(\n 'Grafana.Dashboard',\n props.uid,\n live.meta.provisionedExternalId,\n );\n }\n return yield* grafana.postDashboard({\n dashboard: {\n ...normalizeModel(props.dashboard, props.uid),\n ...(live.meta?.version === undefined ? {} : { version: live.meta.version }),\n },\n ...(props.folderUid === undefined ? {} : { folderUid: props.folderUid }),\n });\n }),\n};\n\nexport const handlers = grafanaHandlers(spec);\n\nexport const GrafanaDashboardProvider = () =>\n Provider.effect(GrafanaDashboard, Effect.succeed(GrafanaDashboard.Provider.of(handlers)));\n",
8
+ "/**\n * Pure functions over a dashboard JSON model — no SDK call, no Effect. Split out of dashboard.ts\n * so that file stays under the house's 250-line cap, and so `normalizeModel`'s no-change-is-noop\n * behavior is exercised directly in dashboard-model.test.ts without a fake Grafana in the way.\n */\nexport type DashboardModel = Record<string, unknown>;\n\n/** Grafana injects or increments these on every save; a declared model never sets them meaningfully. */\nconst VOLATILE_FIELDS = ['id', 'version', 'iteration'] as const;\n\n/**\n * ★ NORMALIZES BOTH SIDES THE SAME WAY, so a no-change plan is a true noop\n * (docs/grafana-folder-dashboard.md). Strips `id`/`version`/`iteration` (Grafana-managed, never\n * declared content) and forces `uid` to this resource's OWN canonical value, wherever — or\n * whether — the source object carried one. That second part is \"uid placement\": a dashboard\n * pasted from Grafana's own \"Export as JSON\" always embeds the OLD live uid (or none, for a\n * never-saved dashboard), while the live model this resource reads back always carries the\n * CURRENT one; forcing both to `props.uid` before comparing is what makes those two shapes equal\n * instead of permanently disagreeing on one field neither side is meant to declare independently\n * (`DashboardProps.uid` — see dashboard.ts — is the single source of truth).\n *\n * ⚠️ EVERY OTHER DECLARED FIELD COMPARES STRUCTURALLY, ARRAY ORDER INCLUDED — dashboard.ts's\n * `matches` runs this output through THIS FILE's own `declaredContentMatches` (below), which\n * never reorders an array. That is correct here: `panels` is position-significant (screen layout\n * via its own `gridPos`, but the array order also drives Grafana's internal panel indexing and\n * any `repeat` behavior) and each panel's `targets` is execution/legend order. Nothing in a\n * dashboard model is a genuinely unordered SET the way UniFi's `trustedDhcpServerIpAddresses` is\n * (unifi/network-form.ts's `sortedSet`) — this family has no field that needs one.\n * ⚠️ `schemaVersion` IS DELIBERATELY **NOT** NORMALIZED — NOT MEASURED against a live instance (no\n * live calls in this PR). Community reports say Grafana can rewrite an old dashboard's\n * `schemaVersion` upward on load; IF that also happens on every plain save of an\n * already-current-schema dashboard, every plan against it would show a false `update` forever —\n * but silently normalizing it away on a guess would just as easily hide a genuine schema\n * mismatch. Left comparing until measured: add it to `VOLATILE_FIELDS` with a dated, measured\n * comment the day it is seen live, the same standard every other row in this file meets.\n */\nexport const normalizeModel = (model: DashboardModel, uid: string): DashboardModel => {\n const normalized: DashboardModel = {};\n for (const [key, value] of Object.entries(model)) {\n if (key === 'uid') continue;\n if ((VOLATILE_FIELDS as readonly string[]).includes(key)) continue;\n normalized[key] = value;\n }\n normalized.uid = uid;\n return normalized;\n};\n\n/** The model's own declared title — reporting only. It is never compared on its own; the whole\n * normalized model is, in dashboard.ts's `matches`, so a title-only mismatch still shows `update`. */\nexport const modelTitle = (model: DashboardModel): string =>\n typeof model.title === 'string' ? model.title : '';\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * ★ `declared` NEED ONLY BE A SUBSET OF `live` — an adversarial review of this PR (2026-09-24)\n * found that plain structural equality made an ordinary, never-edited dashboard show `update`\n * FOREVER: Grafana's own dashboard-save schema migration rewrites any panel or target whose\n * `datasource` is absent into an explicit `{type, uid}` reference to the resolved default\n * datasource — documented Grafana schema-migration behavior (not re-measured against a live\n * instance in this PR — no live calls). A declared panel that never sets `datasource` (the\n * common case: hand-authored JSON, or anything relying on the dashboard-level default) would,\n * after the FIRST create, read back with that field populated; plain `deepEqual` (this file used\n * `alchemy/Diff`'s before this fix) then disagreed with the declaration on every subsequent plan,\n * `update` sent the undecorated model back, and Grafana redecorated it again on the next read —\n * non-convergent.\n *\n * This function instead asks only \"does every field the DECLARATION mentions, at any depth, still\n * match what is live\" — `live` may carry additional fields, at any depth, that `declared` never\n * mentioned (the datasource auto-decoration above, or any future Grafana-injected default,\n * `fieldConfig` defaults and `pluginVersion` included) without being compared at all. A field\n * `declared` DOES mention that is missing, or different, in `live` still fails the match — real\n * content drift is still caught exactly as before; only fields the declaration is silent about are\n * now tolerated rather than compared.\n *\n * ⚠️ THE TRADE-OFF THIS ACCEPTS, DELIBERATELY: removing a field from the declaration (rather than\n * changing its value) can no longer force Grafana to drop it. If a panel once declared an\n * explicit `datasource` and a later declaration deletes that key entirely (rather than setting a\n * different one), `live` keeps whatever the last write set — the declaration is silent about that\n * key now, so nothing compares it, so no `update` ever fires to change it. This is the same\n * \"undeclared means don't care, never means force-to-default\" convention already used everywhere\n * else in this kit for a plain optional prop (e.g. `datasource.ts`'s `matches` never treats an\n * undeclared `access` as \"must be unset\"); it now also applies inside the JSON model, not just at\n * the props level. Declare an explicit replacement value to change a field; omission never clears\n * one that was set by a previous declaration or by Grafana itself.\n *\n * ⚠️ ARRAYS COMPARE BY LENGTH, THEN ELEMENT-WISE SUBSET — never by a looser \"declared has fewer\n * items\" rule. `panels`/`targets` are position-significant (see `normalizeModel`'s header); an\n * array Grafana pads or reorders on its own would still need normalizing here explicitly, the\n * same as any other newly-discovered auto-decoration — this function does not paper over that.\n * ⚠️ `null` ON EITHER SIDE OF A LEAF NEVER MISMATCHES (mirrors `alchemy/Diff`'s `deepEqual` with\n * `stripNullish: true`, which this function replaces for the dashboard model specifically).\n */\nexport const declaredContentMatches = (declared: unknown, live: unknown): boolean => {\n if (declared == null) return true;\n if (Array.isArray(declared)) {\n return (\n Array.isArray(live) &&\n declared.length === live.length &&\n declared.every((item, index) => declaredContentMatches(item, live[index]))\n );\n }\n if (isPlainObject(declared)) {\n return (\n isPlainObject(live) &&\n Object.entries(declared).every(([key, value]) => declaredContentMatches(value, live[key]))\n );\n }\n return declared === live;\n};\n",
9
+ "/**\n * The shared refusal for a file-provisioned Grafana object — `Grafana.Folder` and\n * `Grafana.Dashboard` both carry a live signal Grafana itself treats as read-only through the\n * classic write API this family calls: `Folder.managedBy` (non-empty when something other than\n * this API manages the folder — provisioning is the case this house cares about, but the field\n * itself is Grafana's general \"who owns this\" marker) and `DashboardMeta.provisioned` (paired\n * with `provisionedExternalId`, the file path). A declaration must never attempt a write against\n * either — see docs/grafana-folder-dashboard.md's \"provisioned objects\" section.\n *\n * ⚠️ THE EXACT `managedBy` VALUES ARE INFERRED FROM THE SDK's SCHEMA, NOT MEASURED. No live\n * instance in this house currently has a file-provisioned FOLDER to read (docs/grafana.md:\n * `teslamate-grafana` has none at all; the mini's Grafana provisions dashboards, not folders) and\n * this PR makes no live calls. `Folder.managedBy` and `DashboardMeta.provisioned` are exactly\n * the fields the OpenAPI schema types for this purpose — treat any non-empty `managedBy` as\n * provisioned rather than special-casing specific string values that were never observed live.\n *\n * ⛔ ADOPTING A PROVISIONED OBJECT IS STILL FINE. `read`/`reconcile`'s no-write \"already converged\"\n * path (resource.ts) never calls `create`/`update`/`destroy` at all when the declaration already\n * matches what is live — only an actual attempted WRITE against a provisioned object is refused,\n * by the two callers of this function inside folder.ts/dashboard.ts's own `update`/`destroy`.\n */\nimport * as Data from 'effect/Data';\nimport * as Effect from 'effect/Effect';\n\nexport class GrafanaProvisionedObjectError extends Data.TaggedError(\n 'GrafanaProvisionedObjectError',\n)<{\n readonly message: string;\n}> {}\n\nexport const refuseIfProvisioned = (\n kind: 'Grafana.Folder' | 'Grafana.Dashboard',\n uid: string,\n owner: string | undefined,\n): Effect.Effect<never, GrafanaProvisionedObjectError> => {\n const ownerClause =\n owner === undefined || owner === ''\n ? `(this Grafana instance did not report an owning file)`\n : `by ${owner}`;\n return Effect.fail(\n new GrafanaProvisionedObjectError({\n message:\n `${kind} '${uid}' is file-provisioned ${ownerClause} and read-only through the classic ` +\n `write API this family calls. Edit the provisioning file instead — declaring this ` +\n `object read-only (a declaration matching what is live) is safe and will never reach ` +\n `this refusal.`,\n }),\n );\n};\n",
10
+ "/**\n * `Grafana.Folder` — one Grafana folder, keyed by `uid`. Mirrors `datasource.ts`'s shape and\n * doctrine (uid required, not generated — same reasoning, `getFolderByUID`/`updateFolder`/\n * `deleteFolder` are all UID-keyed and `getFolders` (list) has no per-uid filter).\n *\n * ⛔ `description` IS WRITE-ONLY — MEASURED against `@distilled.cloud/grafana`'s generated types\n * (`Folder` in services/grafana.ts has no `description` field at all, though\n * `CreateFolderRequest`/`UpdateFolderRequest` both accept one). Grafana's classic folder GET\n * never returns it, not even as an is-it-set flag the way `DataSource.secureJsonFields` reports\n * for a datasource's secure fields. So `description` is sent on every create/update that\n * declares it but is NEVER part of `matches` — this resource cannot detect drift in a value it\n * is never shown, the same treatment `datasource.ts` gives `secureJsonDataRefs`.\n *\n * ⚠️ NESTING (`parentUid`) IS CREATE-ONLY — MEASURED against the same generated types.\n * `CreateFolderRequest.parentUid` exists; `UpdateFolderRequest` has no `parentUid` field at all\n * (`folder_uid`, `description`, `overwrite`, `title`, `version` — reparenting is simply not a\n * thing `PUT /folders/{uid}` can do). A declaration whose `parentUid` no longer matches the live\n * folder therefore REFUSES with `GrafanaFolderReparentError` rather than either silently doing\n * nothing (which would lie about the declaration taking effect) or deleting and recreating the\n * folder under the new parent (which — see the ⛔ below — takes every dashboard and alert rule in\n * it with it). Move a folder by hand in Grafana, then adjust the declaration to match.\n *\n * ⛔ DELETE DEFAULTS TO `retain` — DELETING A FOLDER DELETES ITS DASHBOARDS AND ALERT RULES IN\n * GRAFANA, per `deleteFolder`'s own operation description (\"along with all dashboards (and their\n * alerts) stored in the folder... cannot be reverted... also deletes all the subfolders\"). That\n * is the same class of multi-object cascade `openbao/mount.ts` guards with\n * `defaultRemovalPolicy: 'retain'` (\"disabling a mount destroys every secret under it\") — VERIFIED\n * there against Alchemy's own `Apply.ts` (not re-verified here; same engine, same option). A\n * stack that explicitly wants the cascade opts in with `.pipe(RemovalPolicy.destroy())` on the\n * declared instance; `destroy` below still implements the real DELETE in full either way (S11).\n * `Grafana.Dashboard` does NOT default to retain — deleting one dashboard is the same\n * single-object blast radius `Grafana.Datasource` already accepts without a retain guard, not\n * this family's multi-object cascade.\n *\n * ⛔ A PROVISIONED FOLDER (`managedBy` set — see provisioned.ts) REFUSES EVERY UPDATE AND DESTROY.\n * `matches` still compares structurally either way, so a mismatched declaration against a\n * provisioned folder shows `update` in a plan (never a silent, permanently-hidden noop) and then\n * fails loudly, by design, when reconcile actually tries to write.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as grafana from '@distilled.cloud/grafana';\nimport * as Data from 'effect/Data';\nimport * as Effect from 'effect/Effect';\nimport { type GrafanaProvisionedObjectError, refuseIfProvisioned } from './provisioned.ts';\nimport { type GrafanaSpec, grafanaHandlers } from './resource.ts';\n\nexport interface FolderProps {\n uid: string;\n title: string;\n /** Write-only — see the file header. Sent on every create/update that declares it; never diffed. */\n description?: string;\n /** Parent folder UID, for nested folders. Create-only — see the file header. */\n parentUid?: string;\n}\n\nexport interface FolderAttributes {\n uid: string;\n id: number;\n title: string;\n parentUid: string;\n /** Non-empty when Grafana reports this folder as externally managed (e.g. provisioning). */\n managedBy: string;\n url: string;\n version: number;\n}\n\nexport interface GrafanaFolder extends Resource<\n 'Grafana.Folder',\n FolderProps,\n FolderAttributes,\n never\n> {}\n\nexport const GrafanaFolder = Resource<GrafanaFolder>('Grafana.Folder', {\n defaultRemovalPolicy: 'retain',\n});\n\n/** A declared `parentUid` disagrees with the live folder's — `updateFolder` cannot reparent. */\nexport class GrafanaFolderReparentError extends Data.TaggedError('GrafanaFolderReparentError')<{\n readonly message: string;\n}> {}\n\nexport const spec: GrafanaSpec<\n FolderProps,\n grafana.Folder,\n FolderAttributes,\n | grafana.CreateFolderError\n | grafana.GetFolderByUIDError\n | grafana.UpdateFolderError\n | grafana.DeleteFolderError\n | GrafanaProvisionedObjectError\n | GrafanaFolderReparentError\n> = {\n attributes: (live) => ({\n id: live.id ?? 0,\n managedBy: live.managedBy ?? '',\n parentUid: live.parentUid ?? '',\n title: live.title ?? '',\n uid: live.uid ?? '',\n url: live.url ?? '',\n version: live.version ?? 0,\n }),\n create: (props) =>\n grafana.createFolder({\n uid: props.uid,\n title: props.title,\n ...(props.description === undefined ? {} : { description: props.description }),\n ...(props.parentUid === undefined ? {} : { parentUid: props.parentUid }),\n }),\n destroy: (props, live) =>\n Effect.gen(function* () {\n if (live.managedBy) {\n return yield* refuseIfProvisioned('Grafana.Folder', props.uid, live.managedBy);\n }\n yield* grafana.deleteFolder({ folder_uid: props.uid });\n }),\n fetchLive: (props) =>\n grafana\n .getFolderByUID({ folder_uid: props.uid })\n .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),\n matches: (attributes, props) =>\n attributes.title === props.title &&\n (props.parentUid === undefined || attributes.parentUid === props.parentUid),\n update: (props, live) =>\n Effect.gen(function* () {\n if (live.managedBy) {\n return yield* refuseIfProvisioned('Grafana.Folder', props.uid, live.managedBy);\n }\n if (props.parentUid !== undefined && props.parentUid !== (live.parentUid ?? undefined)) {\n return yield* Effect.fail(\n new GrafanaFolderReparentError({\n message:\n `Grafana.Folder '${props.uid}' declares parentUid '${props.parentUid}' but is ` +\n `live under '${live.parentUid ?? '(root)'}'. PUT /folders/{uid} cannot reparent a ` +\n `folder — move it by hand in Grafana, then update the declaration to match.`,\n }),\n );\n }\n // ⚠️ UNLIKE DASHBOARD.TS's 412, THIS HAS NO VERIFIED CONFLICT SIGNAL. `version` is sent\n // optimistically, but the SDK's own doc comment on `UpdateFolderRequest.version` says\n // \"only used by the legacy folder implementation\" — on Grafana's newer unified-storage\n // folder backend it is plausibly ignored outright, meaning two concurrent title/description\n // writes could silently last-write-win with no error, unlike a dashboard save. NOT\n // MEASURED against a live instance either way (no live calls in this PR; flagged by an\n // adversarial review) — sending it is harmless (a no-op field on a backend that ignores it)\n // and correct on one that still honors it, so it stays, but do not read its PRESENCE here\n // as proof this resource detects folder update conflicts the way dashboard.ts does.\n return yield* grafana.updateFolder({\n folder_uid: props.uid,\n title: props.title,\n ...(props.description === undefined ? {} : { description: props.description }),\n ...(live.version === undefined ? {} : { version: live.version }),\n });\n }),\n};\n\nexport const handlers = grafanaHandlers(spec);\n\nexport const GrafanaFolderProvider = () =>\n Provider.effect(GrafanaFolder, Effect.succeed(GrafanaFolder.Provider.of(handlers)));\n",
7
11
  "/**\n * Grafana credentials for this family — a URL plus a TOKEN REFERENCE (an env var NAME), never a\n * literal secret. Wraps `@distilled.cloud/grafana`'s own `Credentials` context directly (S23 —\n * reuse the SDK's own client rather than inventing a second one); this file only adds the house's\n * lazy, per-instance resolution on top of it.\n *\n * ★ ONE `GRAFANA_TOKEN`/`GRAFANA_URL` PAIR ISN'T ENOUGH FOR THIS HOUSE. Unlike Forgejo (one\n * instance), the estate runs at least two Grafanas — `grafana.homeflare.dev` and\n * `teslamate-grafana` on CT100, which the unit's own comment says has \"Own Access SaaS OIDC\n * client, never grafana.homeflare.dev's\". The SDK's `CredentialsFromEnv` reads one fixed pair\n * of names, so a stack declaring against more than one instance needs a target per instance —\n * `grafanaCredentials(target)` below, parameterized the way `litellm/credentials.ts` and\n * `proxmox/credentials.ts` already are in this kit.\n *\n * ⛔ THE TOKEN NEVER TOUCHES A PROP OR AN ATTRIBUTE (S25). `tokenEnv` is a NAME, resolved from\n * `process.env` fresh inside the credentials effect on every request (S24) — a layer built once\n * still re-reads the environment each time an operation runs, the same laziness\n * `litellm/credentials.ts` and the SDK's own `forgejo`/`netbox` `CredentialsFromEnv` have.\n *\n * ⚠️ THE SDK's `Credentials` SERVICE VALUE TYPE IS `Effect.Effect<Config, never, never>` — the\n * error channel is `never` (credentials.d.ts). A missing env var can only surface as a defect\n * here, via `Effect.orDie`, the same trade-off `forgejo/resource.ts` and `netbox/resource.ts`\n * already accept for their own `CredentialsFromEnv`; it is not a choice this file introduces.\n */\nimport { type Config, Credentials } from '@distilled.cloud/grafana/Credentials';\nimport { ConfigError } from '@distilled.cloud/grafana/Errors';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\n\nexport interface GrafanaTarget {\n /** Instance origin, e.g. `https://tesla.homeflare.dev/dash` — never `/api`; the SDK appends it. */\n readonly baseUrl: string;\n /** Env var NAME holding a Grafana service-account token. Never the value itself. */\n readonly tokenEnv: string;\n /** Selects the org a call applies to, via `X-Grafana-Org-Id`. Omit for the default org. */\n readonly orgId?: number | string;\n}\n\nconst resolve = (target: GrafanaTarget): Effect.Effect<Config, never, never> =>\n Effect.sync(() => process.env[target.tokenEnv]).pipe(\n Effect.flatMap((raw) =>\n raw === undefined || raw.trim() === ''\n ? Effect.fail(\n new ConfigError({\n message:\n `${target.tokenEnv} is not set. Export the Grafana service-account token there ` +\n `— never as an Alchemy prop — before running a stack that declares a resource ` +\n `against ${target.baseUrl}.`,\n }),\n )\n : Effect.succeed({\n apiBaseUrl: target.baseUrl.replace(/\\/+$/, ''),\n token: Redacted.make(raw.trim()),\n ...(target.orgId === undefined ? {} : { orgId: target.orgId }),\n } satisfies Config),\n ),\n Effect.orDie,\n );\n\n/** The lazy, per-instance layer `grafanaProviders(target)` composes by default. */\nexport const grafanaCredentials = (target: GrafanaTarget): Layer.Layer<Credentials> =>\n Layer.succeed(Credentials, resolve(target));\n\nexport type { Credentials };\n",
8
- "/**\n * The Grafana providers for one instance, with its credentials, as one layer for a stack.\n *\n * Layer.mergeAll(grafanaProviders({ baseUrl: '...', tokenEnv: 'GRAFANA_TOKEN' }), …)\n *\n * ★ ONE CALL PER INSTANCE. A stack declaring resources against both `grafana.homeflare.dev` and\n * `teslamate-grafana` composes `grafanaProviders(targetA)` and `grafanaProviders(targetB)` —\n * each resource type's `Resource` constructor is a single global registration, so the SAME\n * `Grafana.Datasource` type serves both; only the credential layer differs per call.\n * ⚠️ `HttpClient` is NOT provided here. It is ambient in every Alchemy runtime, the same as\n * `litellmProviders`/`netboxProviders`-style families in this kit assume; a stack that already\n * deploys anything over HTTP already has one.\n */\nimport * as Layer from 'effect/Layer';\nimport { type GrafanaTarget, grafanaCredentials } from './credentials.ts';\nimport { GrafanaDatasourceProvider } from './datasource.ts';\n\nexport const grafanaProviders = (target: GrafanaTarget) =>\n GrafanaDatasourceProvider().pipe(Layer.provide(grafanaCredentials(target)));\n"
12
+ "/**\n * The Grafana providers for one instance, with its credentials, as one layer for a stack.\n *\n * Layer.mergeAll(grafanaProviders({ baseUrl: '...', tokenEnv: 'GRAFANA_TOKEN' }), …)\n *\n * ★ ONE CALL PER INSTANCE. A stack declaring resources against both `grafana.homeflare.dev` and\n * `teslamate-grafana` composes `grafanaProviders(targetA)` and `grafanaProviders(targetB)` —\n * each resource type's `Resource` constructor is a single global registration, so the SAME\n * `Grafana.Datasource` type serves both; only the credential layer differs per call.\n * ⚠️ `HttpClient` is NOT provided here. It is ambient in every Alchemy runtime, the same as\n * `litellmProviders`/`netboxProviders`-style families in this kit assume; a stack that already\n * deploys anything over HTTP already has one.\n */\nimport * as Layer from 'effect/Layer';\nimport { type GrafanaTarget, grafanaCredentials } from './credentials.ts';\nimport { GrafanaDashboardProvider } from './dashboard.ts';\nimport { GrafanaDatasourceProvider } from './datasource.ts';\nimport { GrafanaFolderProvider } from './folder.ts';\n\nexport const grafanaProviders = (target: GrafanaTarget) =>\n Layer.mergeAll(\n GrafanaDatasourceProvider(),\n GrafanaFolderProvider(),\n GrafanaDashboardProvider(),\n ).pipe(Layer.provide(grafanaCredentials(target)));\n"
9
13
  ],
10
- "mappings": "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",
11
- "debugId": "DA921F3915B68A6164756E2164756E21",
14
+ "mappings": "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",
15
+ "debugId": "D031E5F2BA23E8BC64756E2164756E21",
12
16
  "names": []
13
17
  }
@@ -13,5 +13,5 @@
13
13
  */
14
14
  import * as Layer from 'effect/Layer';
15
15
  import { type GrafanaTarget } from './credentials.ts';
16
- export declare const grafanaProviders: (target: GrafanaTarget) => Layer.Layer<import("alchemy").Provider<import("./datasource.ts").GrafanaDatasource>, never, import("effect/unstable/http/HttpClient").HttpClient>;
16
+ export declare const grafanaProviders: (target: GrafanaTarget) => Layer.Layer<import("alchemy").Provider<import("./dashboard.ts").GrafanaDashboard> | import("alchemy").Provider<import("./datasource.ts").GrafanaDatasource> | import("alchemy").Provider<import("./folder.ts").GrafanaFolder>, never, import("effect/unstable/http/HttpClient").HttpClient>;
17
17
  //# sourceMappingURL=providers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/grafana/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,kBAAkB,CAAC;AAG1E,eAAO,MAAM,gBAAgB,WAAY,aAAa,sJACuB,CAAC"}
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/grafana/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,kBAAkB,CAAC;AAK1E,eAAO,MAAM,gBAAgB,WAAY,aAAa,gSAKH,CAAC"}
@@ -0,0 +1,11 @@
1
+ import * as Effect from 'effect/Effect';
2
+ declare const GrafanaProvisionedObjectError_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 & {
3
+ readonly _tag: "GrafanaProvisionedObjectError";
4
+ } & Readonly<A>;
5
+ export declare class GrafanaProvisionedObjectError extends GrafanaProvisionedObjectError_base<{
6
+ readonly message: string;
7
+ }> {
8
+ }
9
+ export declare const refuseIfProvisioned: (kind: 'Grafana.Folder' | 'Grafana.Dashboard', uid: string, owner: string | undefined) => Effect.Effect<never, GrafanaProvisionedObjectError>;
10
+ export {};
11
+ //# sourceMappingURL=provisioned.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provisioned.d.ts","sourceRoot":"","sources":["../../src/grafana/provisioned.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;;;;AAExC,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;AAEL,eAAO,MAAM,mBAAmB,SACxB,gBAAgB,GAAG,mBAAmB,OACvC,MAAM,SACJ,MAAM,GAAG,SAAS,KACxB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,6BAA6B,CAcpD,CAAC"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `Podman.Container` props, attributes and the `.container` file renderer — the pure half of the
3
+ * family, mirroring unit-form.ts's split for Systemd.Unit.
4
+ *
5
+ * ★ WHY THE `[Container]` SECTION IS TYPED, UNLIKE `Systemd.Unit`'s SECTIONS. unit-form.ts renders
6
+ * `[Unit]`/`[Service]`/`[Install]` verbatim because there is no machine-readable schema of
7
+ * `systemd.unit(5)` to check a key against. Quadlet's `[Container]` section is different: it is
8
+ * ONE vendor's documented, versioned surface — `podman-systemd.unit(5)`, Podman 5.4 (checked
9
+ * against `containers/podman` tag `v5.4.0`,
10
+ * `docs/source/markdown/podman-systemd.unit.5.md`, read 2026-09-24) — so every key below is
11
+ * checked against that page, not invented. `[Unit]`/`[Service]`/`[Install]` stay close to
12
+ * unit-form.ts's own verbatim-lines approach, because THOSE three sections are still systemd's
13
+ * own directive set, which Quadlet passes straight through (same doc, "All options and tables
14
+ * available in standard systemd unit files are supported").
15
+ * ⛔ `[Container] Environment=` IS NEVER A SECRET — enforced in container-secrets.ts, called from
16
+ * `containerProblems` below. A container that needs a secret takes `EnvironmentFile=` pointing at
17
+ * a host path an out-of-band secret renderer (openbao-agent) maintains; this family never reads
18
+ * or renders that file's contents.
19
+ * ⛔ `[Install]` SUPPORTS ONLY `Alias=`/`WantedBy=`/`RequiredBy=` FOR A `.container` FILE (same doc,
20
+ * "Currently, only the Alias, WantedBy and RequiredBy keys are supported") — NOT `Also=`, which
21
+ * plain `systemd.unit(5)` allows and unit-form.ts's `INSTALL_KEYS` therefore lists. See
22
+ * container-generator.ts's header for why there is no `enabled` prop at all.
23
+ */
24
+ import { configDigest, digestOf } from './unit-form.ts';
25
+ /** ★ Reused, not redefined: same digest and `restartOn`-join contract as Systemd.Unit. */
26
+ export { configDigest, digestOf };
27
+ export type UnitLine = readonly [key: string, value: string];
28
+ /**
29
+ * The `[Container]` keys this family supports — a deliberate SUBSET of the ~80 keys
30
+ * `podman-systemd.unit(5)` documents (task scope: `Image`, `ContainerName`, `Network`, `Volume`,
31
+ * `EnvironmentFile`, `Environment`, `PublishPort`, `Exec`, `User`, `AutoUpdate`, `PodmanArgs`).
32
+ * Adding another key means adding a field here and checking it against the same doc — never a
33
+ * free-text escape hatch, or `Environment=` secret-checking (below) could be bypassed through it.
34
+ */
35
+ export type ContainerSection = {
36
+ /** `Image=`. Required. "recommended to use a fully qualified image name" (doc). */
37
+ readonly image: string;
38
+ /** `ContainerName=`. @default quadlet's own `systemd-<name>` (doc) when omitted. */
39
+ readonly containerName?: string;
40
+ /** `Network=`. MUST support `'host'` — every container this rollout declares runs host netns. */
41
+ readonly network?: string;
42
+ /** `Volume=`, repeatable. */
43
+ readonly volume?: readonly string[];
44
+ /** `EnvironmentFile=`, repeatable. The ONLY way a secret reaches the container — see file header. */
45
+ readonly environmentFile?: readonly string[];
46
+ /** `Environment=`, repeatable `KEY=VALUE`. NEVER a secret — see container-secrets.ts. */
47
+ readonly environment?: Readonly<Record<string, string>>;
48
+ /** `PublishPort=`, repeatable. Refused together with `network: 'host'` — see `containerProblems`. */
49
+ readonly publishPort?: readonly string[];
50
+ /** `Exec=`. Appended after the image's entrypoint, "the same way as ... a Kubernetes pod" (doc). */
51
+ readonly exec?: string;
52
+ /** `User=`. The numeric UID inside the container (doc: "does not need to match the UID on the host"). */
53
+ readonly user?: string;
54
+ /** `AutoUpdate=`. Podman's own two values (doc): `'registry'` or `'local'`. */
55
+ readonly autoUpdate?: 'registry' | 'local';
56
+ /** `PodmanArgs=`, repeatable. Doc: "not recommended" — the generator does not understand it. */
57
+ readonly podmanArgs?: readonly string[];
58
+ };
59
+ export type ContainerProps = {
60
+ /** The Quadlet file's basename, WITHOUT `.container`. Written as `<name>.container`. */
61
+ name: string;
62
+ /** @default QUADLET_DEFAULT_DIRECTORY. Must be one of QUADLET_SEARCH_DIRECTORIES (below). */
63
+ directory?: string;
64
+ /** `[Unit]` — `Description=`/`Documentation=` typed, everything else verbatim (see file header). */
65
+ unit?: {
66
+ description?: string;
67
+ documentation?: string;
68
+ lines?: readonly UnitLine[];
69
+ };
70
+ /** `[Container]` — Quadlet's own, typed and doc-checked. */
71
+ container: ContainerSection;
72
+ /** `[Service]` — `Restart=`/`RestartSec=` typed, everything else verbatim. */
73
+ service?: {
74
+ restart?: string;
75
+ restartSec?: string;
76
+ lines?: readonly UnitLine[];
77
+ };
78
+ /** `[Install]` — the three keys Quadlet honours for a `.container` file (see file header). */
79
+ install?: {
80
+ wantedBy?: readonly string[];
81
+ requiredBy?: readonly string[];
82
+ alias?: readonly string[];
83
+ };
84
+ /** `systemctl start`/`stop` the GENERATED `<name>.service`. @default true. No `enabled` prop —
85
+ * see container-generator.ts's header for why boot-time enablement is not a systemctl call here. */
86
+ started?: boolean;
87
+ /** Same contract as Systemd.Unit's `restartOn`: digests of things read outside the `.container` file. */
88
+ restartOn?: readonly string[];
89
+ };
90
+ export type ContainerAttributes = {
91
+ name: string;
92
+ /** The GENERATED unit's name: `<name>.service`. Never `.container` — systemctl never sees that. */
93
+ serviceName: string;
94
+ containerPath: string;
95
+ /** SHA-256 of the `.container` file this resource wrote. */
96
+ containerSha256: string;
97
+ configSha256: string;
98
+ loadState: string;
99
+ activeState: string;
100
+ unitFileState?: string;
101
+ /** `systemctl show`'s `SourcePath=` on the generated unit — measured on CT100 2026-09-24 to be
102
+ * the `.container` file's own path; used to prove the generated unit is really ours. */
103
+ sourcePath?: string;
104
+ active: boolean;
105
+ };
106
+ /**
107
+ * Podman's OWN rootful search path, most specific first — `podman-systemd.unit(5)`, "Podman Unit
108
+ * Search Path", Podman 5.4, read 2026-09-24: "Quadlet files for the root user can be placed in the
109
+ * following directories ordered in precedence" (`/run` over `/etc` over `/usr/share`).
110
+ * ★ CT100's `caddy.container` lives in the middle one — measured live, `cat` over `ssh ct100`,
111
+ * 2026-09-24: `/etc/containers/systemd/caddy.container`.
112
+ * ⛔ A directory outside this list is invisible to Quadlet: the generator only reads these three, so
113
+ * a file elsewhere is a `.container` file with no `.service` ever generated from it — silently,
114
+ * unlike `Systemd.Unit`'s directory check, which at least writes a unit systemd could load if only
115
+ * it were enabled. See `containerProblems`.
116
+ */
117
+ export declare const QUADLET_SEARCH_DIRECTORIES: readonly ['/run/containers/systemd', '/etc/containers/systemd', '/usr/share/containers/systemd'];
118
+ export declare const QUADLET_DEFAULT_DIRECTORY = "/etc/containers/systemd";
119
+ /** ★ A conservative subset of what Quadlet itself accepts as a unit name: no template `@`, no `/`. */
120
+ export declare const NAME: RegExp;
121
+ export declare const containerPathFor: (props: Pick<ContainerProps, 'name' | 'directory'>) => string;
122
+ /** The unit Quadlet generates FROM the `.container` file — systemctl only ever sees this name. */
123
+ export declare const serviceNameFor: (props: Pick<ContainerProps, 'name'>) => string;
124
+ /** The whole `.container` file. ★ Reuses unit-form.ts's `renderUnit` — same `[Section]` INI shape. */
125
+ export declare const renderContainerFile: (props: ContainerProps) => string;
126
+ export declare const refuseContainer: (name: string, message: string) => Error;
127
+ /** Every refusal at once, so one plan shows the whole list — mirrors `unitProblems`'s shape. */
128
+ export declare const containerProblems: (props: ContainerProps) => string[];
129
+ //# sourceMappingURL=container-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-form.d.ts","sourceRoot":"","sources":["../../src/linux/container-form.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Quadlet is a systemd GENERATOR (`systemd.generator(7)`, tag `v257`, `man/systemd.generator.xml`,
3
+ * read 2026-09-24), not a unit reader — this is the one fact the whole lifecycle is built around.
4
+ *
5
+ * ★ "Generator output is supposed to last only until the next `daemon-reload`... Units written by
6
+ * generators are removed when the configuration is reloaded" (same doc). `daemon-reload` DELETES
7
+ * `/run/systemd/generator` wholesale and reruns every generator from the `.container` files
8
+ * currently on disk. So a `.container` file the generator refuses produces NO generated unit —
9
+ * even one that generated fine a moment ago, if `daemon-reload` runs again over a now-broken file.
10
+ * That is why `container-lifecycle.ts` restores the LAST-KNOWN-GOOD file and reloads again on a
11
+ * failed update, rather than leaving the bad file in place the way `Systemd.Unit` leaves a bad
12
+ * update for the next deploy to retry (unit-lifecycle.ts): here, "the next deploy" might be a
13
+ * reboot, and by then the container this resource already promised running has no systemd unit
14
+ * at all, not merely a stale one.
15
+ * ⛔ THE GENERATED UNIT CANNOT BE `systemctl enable`D — `podman-systemd.unit(5)`, Podman 5.4, "Podman
16
+ * unit files": "The services created by Podman are considered transient by systemd... it is not
17
+ * possible to 'systemctl enable' them... To compensate... the generator manually applies the
18
+ * [Install] section... during generation, in the same way `systemctl enable` does when run
19
+ * later." MEASURED on CT100 2026-09-24 (`ssh ct100`, read-only): `caddy.container` declares
20
+ * `[Install] WantedBy=multi-user.target` and nothing ever ran `systemctl enable caddy`; yet
21
+ * `systemctl is-enabled caddy.service` prints `generated`, `UnitFileState=generated`, and
22
+ * `/run/systemd/generator/multi-user.target.wants/caddy.service` is a symlink the GENERATOR put
23
+ * there. So `Podman.Container` has no `enabled` prop and never calls `enableUnit`/`disableUnit`
24
+ * (systemctl.ts) — declaring `install.wantedBy` IS the enablement, applied by the generator on
25
+ * every `daemon-reload`, exactly the way `Systemd.Unit`'s enable/disable calls are applied by
26
+ * this family's own `settle` step. That is the one place this family's "enable logic" differs.
27
+ * ★ SourcePath TIES THE GENERATED UNIT BACK TO OUR FILE. MEASURED on CT100 2026-09-24:
28
+ * `systemctl show caddy.service -p SourcePath` -> `/etc/containers/systemd/caddy.container`.
29
+ * `verifyGenerated` below uses it the way `unit-preflight.ts`'s `assertUnclaimed` uses a file
30
+ * digest: a generated unit whose SourcePath is NOT our path belongs to a same-named `.container`
31
+ * file earlier in `QUADLET_SEARCH_DIRECTORIES` (container-form.ts) — a takeover, not a success.
32
+ * ⚠️ REASONED, NOT MEASURED — THE ONE GAP THIS VERIFICATION HAS: whether an ALREADY-ACTIVE unit's
33
+ * `LoadState` stays `loaded` (systemd does not garbage-collect a referenced/active unit object
34
+ * merely because a reload failed to reproduce it) even when the UPDATED `.container` file the
35
+ * generator just refused would, on its own, generate nothing. Measuring this needs writing a
36
+ * broken file to a live host, which read-only access forbids. If that is how a live host
37
+ * behaves, `LoadState`/`SourcePath` alone would not catch a failed update to a container that
38
+ * was already running — only a CREATE, or an update to an already-INACTIVE one, is verified with
39
+ * full confidence by this check alone. Restoring the last-known-good file on ANY verification
40
+ * failure (container-lifecycle.ts) does not depend on this gap: it always leaves the host at the
41
+ * file that is known to generate, closing the risk window even where detection is uncertain.
42
+ * ⛔ `needsVerificationReload` (below) is what stops a NEW resource from getting stuck — found on
43
+ * adversarial review. `container-lifecycle.ts`'s `needsReload` used to be `wrote || stale ||
44
+ * preStatus.needDaemonReload`; for an INTERRUPTED CREATE (the write landed, the apply crashed
45
+ * before `daemon-reload` ran) the retry sees `wrote: false` (the file already holds `desired`)
46
+ * and `stale: false` (there is no PRIOR state to disagree with — this is a create), so nothing
47
+ * ever reloaded and `verifyGenerated` threw on the still-missing generated unit, forever: loud,
48
+ * but stuck, since nothing about a plain retry ever changed. The same holds for adopting a file
49
+ * whose generated unit is missing or does not match. `needsVerificationReload` shares
50
+ * `generatedMatches` with `verifyGenerated` itself, so the reload gate and the verification it
51
+ * is gating can never disagree about what "already proven" means.
52
+ */
53
+ import type { UnitStatus } from './systemctl.ts';
54
+ /** MEASURED on CT100 2026-09-24: `systemctl show caddy.service -p FragmentPath`. */
55
+ export declare const GENERATED_UNIT_DIRECTORY = "/run/systemd/generator";
56
+ export declare class QuadletGeneratorError extends Error {
57
+ constructor(name: string, detail: string);
58
+ }
59
+ /**
60
+ * Whether `container-lifecycle.ts` must `daemon-reload` before trusting `status` as proof of
61
+ * anything — true whenever `status` does not already show a matching generation. Called on the
62
+ * PRE-write status, so it costs nothing when `wrote`/`stale`/`NeedDaemonReload` already force a
63
+ * reload; it earns its keep exactly when none of them do but nothing has actually been proven yet
64
+ * (a fresh create or adopt with no prior state to compare against).
65
+ */
66
+ export declare const needsVerificationReload: (containerPath: string, status: UnitStatus) => boolean;
67
+ /**
68
+ * Throws a typed, loud `QuadletGeneratorError` — never lets a generator failure read as "the
69
+ * resource is simply absent", which is what a plain `LoadState=not-found` would otherwise look
70
+ * like to a caller that only knows the `Systemd.Unit` shape (a unit that has genuinely never been
71
+ * declared also reads `not-found`).
72
+ */
73
+ export declare const verifyGenerated: (name: string, containerPath: string, serviceName: string, status: UnitStatus) => void;
74
+ //# sourceMappingURL=container-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-generator.d.ts","sourceRoot":"","sources":["../../src/linux/container-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAGvC;CACF;AAUD;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,kBAAmB,MAAM,UAAU,UAAU,KAAG,OAC1C,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,eAAe,SACpB,MAAM,iBACG,MAAM,eACR,MAAM,UACX,UAAU,KACjB,IAmBF,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Diff } from 'alchemy/Diff';
2
+ import type { Input } from 'alchemy/Input';
3
+ import * as Effect from 'effect/Effect';
4
+ import type { HostRunner } from '../launchd/runner.ts';
5
+ import type { ContainerAttributes, ContainerProps } from './container-form.ts';
6
+ export declare const readHandler: (runner: HostRunner, olds: ContainerProps, output: ContainerAttributes | undefined) => Effect.Effect<ContainerAttributes | undefined, Error>;
7
+ export declare const diffHandler: (runner: HostRunner, instanceId: string, news: Input<ContainerProps>, output: ContainerAttributes | undefined) => Effect.Effect<Diff | undefined, Error>;
8
+ export declare const reconcileHandler: (runner: HostRunner, args: {
9
+ fqn: string;
10
+ instanceId: string;
11
+ news: ContainerProps;
12
+ output: ContainerAttributes | undefined;
13
+ }) => Effect.Effect<ContainerAttributes, Error>;
14
+ export declare const deleteHandler: (runner: HostRunner, output: ContainerAttributes) => Effect.Effect<void, Error>;
15
+ //# sourceMappingURL=container-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-handlers.d.ts","sourceRoot":"","sources":["../../src/linux/container-handlers.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAS/E,eAAO,MAAM,WAAW,WACd,UAAU,QACZ,cAAc,UACZ,mBAAmB,GAAG,SAAS,KACtC,MAAM,CAAC,MAAM,CAAC,mBAAmB,GAAG,SAAS,EAAE,KAAK,CAKnD,CAAC;AAEL,eAAO,MAAM,WAAW,WACd,UAAU,cACN,MAAM,QACZ,KAAK,CAAC,cAAc,CAAC,UACnB,mBAAmB,GAAG,SAAS,KACtC,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,KAAK,CAgBvC,CAAC;AAEF,eAAO,MAAM,gBAAgB,WACnB,UAAU,QACZ;IACJ,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACzC,KACA,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,KAAK,CAGxC,CAAC;AAEJ,eAAO,MAAM,aAAa,WAChB,UAAU,UACV,mBAAmB,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAgD,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `Podman.Container`'s read / diff / reconcile / delete as plain async functions over a
3
+ * HostRunner — same split as `unit-lifecycle.ts`, so the whole lifecycle runs against a fake host
4
+ * (fake-quadlet.ts) with no real systemd or Podman.
5
+ *
6
+ * ⛔ WHAT A DEPLOY DOES NOT DO — same rule `Systemd.Unit` was built around (unit-lifecycle.ts
7
+ * header): it NEVER mass-restarts. A container is restarted only when THIS resource's rendered
8
+ * `.container` file changed, or a digest the declaration listed in `restartOn` changed.
9
+ * ★ THE ORDER IS WRITE, RELOAD, VERIFY THE GENERATOR, THEN START OR RESTART. `daemon-reload` is
10
+ * where Quadlet turns the `.container` file into `<name>.service` (container-generator.ts) —
11
+ * writing it changes nothing on its own, and a write that fails leaves the old file (and its
12
+ * already-generated unit) exactly as they were.
13
+ * ⛔ A GENERATOR FAILURE IS ROLLED BACK, NOT LEFT FOR THE NEXT DEPLOY. `container-generator.ts`'s
14
+ * header explains why: `daemon-reload` deletes ALL generator output before regenerating, so a
15
+ * `.container` file the generator refuses has NO generated unit at all, even if the file it
16
+ * replaced generated one fine. An UPDATE that fails verification restores the previous file and
17
+ * reloads again, so the container this resource already promised running never loses its unit; a
18
+ * CREATE that fails removes the file it wrote, same as `Systemd.Unit`'s undo.
19
+ * ⛔ NO ENABLE/DISABLE — see container-generator.ts and container-settle.ts headers.
20
+ */
21
+ import type { Diff } from 'alchemy/Diff';
22
+ import type { HostRunner } from '../launchd/runner.ts';
23
+ import { type ContainerAttributes, type ContainerProps } from './container-form.ts';
24
+ /** What is on the host now: the `.container` file's digest and the GENERATED unit's status. */
25
+ export declare const readContainer: (runner: HostRunner, props: ContainerProps) => Promise<ContainerAttributes | undefined>;
26
+ export declare const diffContainer: (runner: HostRunner, news: ContainerProps, output: ContainerAttributes) => Promise<Diff>;
27
+ /**
28
+ * `adopt` is whether adoption is on AND this apply is a create or an unfinished generation of our
29
+ * own — same meaning as `Systemd.Unit`'s (unit-lifecycle.ts reconcileUnit).
30
+ */
31
+ export declare const reconcileContainer: (runner: HostRunner, props: ContainerProps, output: ContainerAttributes | undefined, adopt?: boolean) => Promise<ContainerAttributes>;
32
+ /**
33
+ * Stop, remove the `.container` file, reload — clearing the orphaned generated unit. Idempotent.
34
+ * No `disableUnit`: never enabled through systemctl in the first place (container-settle.ts).
35
+ */
36
+ export declare const deleteContainer: (runner: HostRunner, output: ContainerAttributes) => Promise<void>;
37
+ //# sourceMappingURL=container-lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-lifecycle.d.ts","sourceRoot":"","sources":["../../src/linux/container-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAOpB,MAAM,qBAAqB,CAAC;AAY7B,+FAA+F;AAC/F,eAAO,MAAM,aAAa,WAChB,UAAU,SACX,cAAc,KACpB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAczC,CAAC;AAEF,eAAO,MAAM,aAAa,WAChB,UAAU,QACZ,cAAc,UACZ,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAwBd,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,WACrB,UAAU,SACX,cAAc,UACb,mBAAmB,GAAG,SAAS,sBAEtC,OAAO,CAAC,mBAAmB,CAyF7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,WAClB,UAAU,UACV,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAad,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Everything `Podman.Container` checks before it touches the host — split from
3
+ * container-lifecycle.ts so a reconcile and a plan-time REPLACE run the very same checks, exactly
4
+ * the reason unit-preflight.ts exists (see that file's header; this mirrors it).
5
+ *
6
+ * ★ EVERY CHECK HERE IS READ-ONLY: validation, `systemctl show`, a file read and
7
+ * `HostRunner.checkWrite`. Nothing here writes, reloads or drives Podman.
8
+ */
9
+ import { type HostRunner } from '../launchd/runner.ts';
10
+ import { type UnitStatus } from './systemctl.ts';
11
+ import { type ContainerAttributes, type ContainerProps } from './container-form.ts';
12
+ /** Throw every refusal at once, so one plan shows the whole list. */
13
+ export declare const assertValid: (props: ContainerProps) => void;
14
+ /** ⛔ Writing the file and driving systemctl/Podman are root's — refuse up front, not mid-apply. */
15
+ export declare const assertMayWrite: (runner: HostRunner, name: string) => void;
16
+ /**
17
+ * ⛔ A MASKED GENERATED UNIT IS SOMEONE'S DECISION, NOT DRIFT — same rule as `Systemd.Unit`
18
+ * (unit-preflight.ts `assertUsable`). Masking a generated unit is done the ordinary way, by
19
+ * symlinking its name to `/dev/null` in a directory Quadlet's generator output does not win
20
+ * against (`/etc/systemd/system` outranks `/run/systemd/generator`); unmasking here would
21
+ * silently overrule that.
22
+ */
23
+ export declare const assertUsable: (props: Pick<ContainerProps, 'name'>, status: UnitStatus) => void;
24
+ /**
25
+ * ⛔ A `.container` FILE THIS RESOURCE DOES NOT OWN IS NEVER OVERWRITTEN — same rule and same
26
+ * exemption as `unit-preflight.ts`'s `assertUnclaimed`: a file byte-identical to our render is
27
+ * this declaration's own leftover, not someone else's.
28
+ */
29
+ export declare const assertUnclaimed: (runner: HostRunner, props: ContainerProps) => Promise<void>;
30
+ /** The half of a rename check that needs only the NEW NAME — mirrors `assertRenameTarget`. */
31
+ export declare const assertRenameTarget: (runner: HostRunner, old: ContainerAttributes, name: string) => Promise<void>;
32
+ /** A rename, checked at plan time, before the plan may promise a delete-first replace. */
33
+ export declare const assertReplaceable: (runner: HostRunner, next: ContainerProps, old: ContainerAttributes) => Promise<void>;
34
+ //# sourceMappingURL=container-preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-preflight.d.ts","sourceRoot":"","sources":["../../src/linux/container-preflight.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAMpB,MAAM,qBAAqB,CAAC;AAI7B,qEAAqE;AACrE,eAAO,MAAM,WAAW,UAAW,cAAc,KAAG,IAGnD,CAAC;AAEF,mGAAmG;AACnG,eAAO,MAAM,cAAc,WAAY,UAAU,QAAQ,MAAM,KAAG,IAQjE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,UAAW,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,UAAU,UAAU,KAAG,IAQtF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,WAAkB,UAAU,SAAS,cAAc,KAAG,OAAO,CAAC,IAAI,CAW7F,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,kBAAkB,WACrB,UAAU,OACb,mBAAmB,QAClB,MAAM,KACX,OAAO,CAAC,IAAI,CAId,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,iBAAiB,WACpB,UAAU,QACZ,cAAc,OACf,mBAAmB,KACvB,OAAO,CAAC,IAAI,CAKd,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A best-effort refusal of an inline secret ANYWHERE in a rendered `.container` file — checked
3
+ * from `containerProblems` (container-form.ts), so it runs at plan AND apply time, before anything
4
+ * is written.
5
+ *
6
+ * ⛔ THIS IS NOT A SECRET SCANNER. It cannot prove a value is safe, only refuse the OBVIOUS cases: a
7
+ * key whose name says what it holds, or a value shaped like a real vendor token. The actual
8
+ * contract is `EnvironmentFile=` (container-form.ts's header): a secret belongs in a host file an
9
+ * out-of-band renderer (openbao-agent) maintains, never in a prop, because the WHOLE file is
10
+ * rendered into a 0644 file (`unit-form.ts` `UNIT_WRITE`) AND persisted unencrypted in Alchemy
11
+ * state — the same two reasons `Systemd.Unit`'s `content` is never secret. A determined caller can
12
+ * still name a variable `CFG` and put a token in it; this check is a guardrail against the common
13
+ * mistake, not a boundary a hostile caller cannot cross.
14
+ * ★ KEY-NAME FIRST, VALUE SECOND. The name check catches the overwhelming majority of real
15
+ * mistakes (copying a `docker run -e SECRET=...` line verbatim) and has no false-negative risk
16
+ * from an unfamiliar token format; the value check is defense in depth for a handful of
17
+ * widely-recognised vendor prefixes, not an attempt at general entropy detection — an entropy
18
+ * check has enough false positives on ordinary config (long image digests, base64 Caddyfile
19
+ * snippets) that it would train callers to ignore the refusal.
20
+ * ⛔ CHECKED EVERYWHERE, NOT ONLY `container.environment` — found on adversarial review.
21
+ * `container.environment` is the field most likely to carry one BY ACCIDENT, and gets the
22
+ * precise per-key `secretLikeEnvironment` check below with the best error message. But every
23
+ * other free-text field renders into the SAME 0644 file and the SAME state row: `exec`,
24
+ * `podmanArgs` (each entry rendered verbatim), and the escape-hatch `unit.lines`/`service.lines`
25
+ * — a `service.lines` entry can spell a native systemd `Environment=` directly, which never goes
26
+ * through the typed `container.environment` map at all. `secretLikeLines` below runs the same two
27
+ * heuristics — value-shape, and an embedded `KEY=VALUE` whose KEY looks like a secret — over every
28
+ * OTHER rendered line, and `container-form.ts`'s `containerProblems` calls both, so nothing
29
+ * rendered escapes either check.
30
+ */
31
+ export declare const secretLikeEnvironment: (environment: Readonly<Record<string, string>>) => string[];
32
+ /** The same two heuristics as `secretLikeEnvironment`, over any OTHER rendered `[key, value]` line
33
+ * (`Exec=`, `PodmanArgs=`, and the `unit.lines`/`service.lines` escape hatches) — see file header. */
34
+ export declare const secretLikeLines: (lines: readonly (readonly [key: string, value: string])[]) => string[];
35
+ /** `PodmanArgs=` entries that try to set a container env var outright — see `ENV_FLAG`'s comment. */
36
+ export declare const podmanArgsProblems: (args: readonly string[]) => string[];
37
+ //# sourceMappingURL=container-secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-secrets.d.ts","sourceRoot":"","sources":["../../src/linux/container-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA6BH,eAAO,MAAM,qBAAqB,gBAAiB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAG,MAAM,EAkB3F,CAAC;AAEF;uGACuG;AACvG,eAAO,MAAM,eAAe,UACnB,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,EAAE,KACxD,MAAM,EAkBR,CAAC;AAEF,qGAAqG;AACrG,eAAO,MAAM,kBAAkB,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,EAQ9D,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The step that decides whether the GENERATED `<name>.service` actually moves: running state and —
3
+ * only for a real change — a restart. Split out for the same reason unit-settle.ts is split from
4
+ * unit-lifecycle.ts: it is the restart contract, and it should be readable on its own.
5
+ *
6
+ * ⛔ THE RESTART CONDITION IS THE WHOLE CONTRACT, same as `Systemd.Unit`'s (unit-settle.ts):
7
+ * `changed` comes from the caller (container-lifecycle.ts `reconcileContainer`), `configChanged`
8
+ * is a digest the DECLARATION itself listed. Nothing else restarts a container.
9
+ * ★ NO ENABLE/DISABLE HERE — the one place this file is SHORTER than unit-settle.ts, not longer.
10
+ * Quadlet's generator applies `[Install]` itself on every `daemon-reload` (container-generator.ts
11
+ * header); calling `systemctl enable`/`disable` on a generated unit does not work at all (same
12
+ * header, doc-cited). So there is no enablement branch to settle — only started/stopped and the
13
+ * restart-on-change rule.
14
+ */
15
+ import type { HostRunner } from '../launchd/runner.ts';
16
+ import { type UnitStatus } from './systemctl.ts';
17
+ import { type ContainerAttributes, type ContainerProps } from './container-form.ts';
18
+ export declare const attributesOf: (props: ContainerProps, configSha256: string, containerSha256: string, containerPath: string, serviceName: string, status: UnitStatus) => ContainerAttributes;
19
+ export type Settle = {
20
+ readonly before: Uint8Array | undefined;
21
+ /** The `.container` file or systemd's view of it changed in a way that earns a restart. */
22
+ readonly changed: boolean;
23
+ readonly configChanged: boolean;
24
+ readonly desired: string;
25
+ readonly path: string;
26
+ readonly serviceName: string;
27
+ readonly status: UnitStatus;
28
+ };
29
+ /**
30
+ * Bring running state to the declaration, restarting ONLY for a real change. No enable/disable —
31
+ * see file header.
32
+ */
33
+ export declare const settle: (runner: HostRunner, props: ContainerProps, configSha256: string, step: Settle) => Promise<ContainerAttributes>;
34
+ //# sourceMappingURL=container-settle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-settle.d.ts","sourceRoot":"","sources":["../../src/linux/container-settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAMhB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,YAAY,UAChB,cAAc,gBACP,MAAM,mBACH,MAAM,iBACR,MAAM,eACR,MAAM,UACX,UAAU,KACjB,mBAWD,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IACxC,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,WACT,UAAU,SACX,cAAc,gBACP,MAAM,QACd,MAAM,KACX,OAAO,CAAC,mBAAmB,CA0B7B,CAAC"}