@homeflare/alchemy 0.51.0 → 0.53.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 (73) hide show
  1. package/dist/grafana/folder-permission.d.ts +76 -0
  2. package/dist/grafana/folder-permission.d.ts.map +1 -0
  3. package/dist/grafana/index.d.ts +3 -1
  4. package/dist/grafana/index.d.ts.map +1 -1
  5. package/dist/grafana/index.js +133 -46
  6. package/dist/grafana/index.js.map +7 -5
  7. package/dist/grafana/providers.d.ts +1 -1
  8. package/dist/grafana/providers.d.ts.map +1 -1
  9. package/dist/grafana/provisioned.d.ts +1 -1
  10. package/dist/grafana/provisioned.d.ts.map +1 -1
  11. package/dist/grafana/team.d.ts +64 -0
  12. package/dist/grafana/team.d.ts.map +1 -0
  13. package/dist/linux/container-env-quote.d.ts +39 -0
  14. package/dist/linux/container-env-quote.d.ts.map +1 -0
  15. package/dist/linux/container-form.d.ts +14 -13
  16. package/dist/linux/container-form.d.ts.map +1 -1
  17. package/dist/linux/container-paths.d.ts +17 -0
  18. package/dist/linux/container-paths.d.ts.map +1 -0
  19. package/dist/linux/index.js +41 -8
  20. package/dist/linux/index.js.map +6 -4
  21. package/dist/litellm/fake-marketplace-litellm.d.ts +33 -0
  22. package/dist/litellm/fake-marketplace-litellm.d.ts.map +1 -0
  23. package/dist/litellm/index.d.ts +1 -0
  24. package/dist/litellm/index.d.ts.map +1 -1
  25. package/dist/litellm/index.js +530 -329
  26. package/dist/litellm/index.js.map +7 -4
  27. package/dist/litellm/marketplace-form.d.ts +21 -0
  28. package/dist/litellm/marketplace-form.d.ts.map +1 -0
  29. package/dist/litellm/marketplace-operations.d.ts +27 -0
  30. package/dist/litellm/marketplace-operations.d.ts.map +1 -0
  31. package/dist/litellm/marketplace-plugin.d.ts +57 -0
  32. package/dist/litellm/marketplace-plugin.d.ts.map +1 -0
  33. package/dist/litellm/marketplace-types.d.ts +55 -0
  34. package/dist/litellm/marketplace-types.d.ts.map +1 -0
  35. package/dist/litellm/providers.d.ts +1 -1
  36. package/dist/litellm/providers.d.ts.map +1 -1
  37. package/dist/unifi/index.js.map +2 -2
  38. package/dist/unifi/network.d.ts +2 -1
  39. package/dist/unifi/network.d.ts.map +1 -1
  40. package/docs/litellm-marketplace.md +59 -0
  41. package/docs/litellm.md +1 -0
  42. package/docs/unifi.md +11 -10
  43. package/package.json +1 -1
  44. package/src/grafana/folder-permission.test.ts +107 -0
  45. package/src/grafana/folder-permission.ts +128 -0
  46. package/src/grafana/index.ts +10 -1
  47. package/src/grafana/providers.ts +4 -0
  48. package/src/grafana/provisioned.ts +1 -1
  49. package/src/grafana/team.test.ts +108 -0
  50. package/src/grafana/team.ts +105 -0
  51. package/src/linux/container-env-chain.test.ts +135 -0
  52. package/src/linux/container-env-live.test.ts +115 -0
  53. package/src/linux/container-env-oracle.ts +109 -0
  54. package/src/linux/container-env-quadlet-oracle.ts +174 -0
  55. package/src/linux/container-env-quote.test.ts +131 -0
  56. package/src/linux/container-env-quote.ts +126 -0
  57. package/src/linux/container-fixture.test.ts +3 -1
  58. package/src/linux/container-form.ts +32 -28
  59. package/src/linux/container-paths.ts +21 -0
  60. package/src/litellm/fake-marketplace-litellm.test.ts +51 -0
  61. package/src/litellm/fake-marketplace-litellm.ts +134 -0
  62. package/src/litellm/index.ts +8 -0
  63. package/src/litellm/marketplace-form.ts +160 -0
  64. package/src/litellm/marketplace-operations.ts +76 -0
  65. package/src/litellm/marketplace-plugin-replace.test.ts +68 -0
  66. package/src/litellm/marketplace-plugin.test.ts +181 -0
  67. package/src/litellm/marketplace-plugin.ts +156 -0
  68. package/src/litellm/marketplace-types.ts +53 -0
  69. package/src/litellm/providers.ts +2 -0
  70. package/src/litellm/registry-exports.test.ts +6 -0
  71. package/src/unifi/network-scope.test.ts +21 -0
  72. package/src/unifi/network.ts +2 -1
  73. package/src/unifi/write-op-reference.test.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `Grafana.FolderPermission` against a fake Grafana: only DIRECT team entries count, writes go through
3
+ * the per-team endpoint (never the whole-list replace), missing team fails loudly, delete sets "".
4
+ */
5
+ import { describe, expect, test } from 'bun:test';
6
+ import type { GrafanaOpContext } from '@distilled.cloud/grafana/Protocol';
7
+ import * as Effect from 'effect/Effect';
8
+ import { fakeFailure, fakeGrafana, fakeGrafanaLayer } from './fake-grafana.ts';
9
+ import { spec } from './folder-permission.ts';
10
+ import { grafanaOperations } from './resource.ts';
11
+
12
+ const TEAMS = '/api/teams/search';
13
+ const PERMS = '/api/access-control/folders/village-grafana';
14
+ const SET = '/api/access-control/folders/village-grafana/teams/7';
15
+ const teamRow = {
16
+ id: 7,
17
+ uid: 'aex7',
18
+ name: 'village-grafana',
19
+ isProvisioned: false,
20
+ memberCount: 0,
21
+ orgId: 1,
22
+ };
23
+ const props = {
24
+ folderUid: 'village-grafana',
25
+ teamName: 'village-grafana',
26
+ permission: 'Edit' as const,
27
+ };
28
+ const run = <A, E>(
29
+ fake: ReturnType<typeof fakeGrafana>,
30
+ effect: Effect.Effect<A, E, GrafanaOpContext>,
31
+ ): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))));
32
+
33
+ const grafanaWith = (entries: () => unknown[], teams = [teamRow]) =>
34
+ fakeGrafana((m, url) => {
35
+ if (m === 'GET' && url.pathname === TEAMS)
36
+ return Response.json({ teams, totalCount: teams.length });
37
+ if (m === 'GET' && url.pathname === PERMS) return Response.json(entries());
38
+ if (m === 'POST' && url.pathname === SET)
39
+ return Response.json({ message: 'Permission updated' });
40
+ return fakeFailure(404, `unexpected ${m} ${url.pathname}`);
41
+ });
42
+
43
+ describe('Grafana.FolderPermission', () => {
44
+ test('an inherited entry for the team does not count as live', async () => {
45
+ const fake = grafanaWith(() => [
46
+ { teamId: 7, permission: 'Edit', isInherited: true },
47
+ { builtInRole: 'Admin', permission: 'Admin' },
48
+ ]);
49
+ expect(await run(fake, spec.fetchLive(props))).toBeUndefined();
50
+ });
51
+
52
+ test('a matching direct entry adopts with no write', async () => {
53
+ const fake = grafanaWith(() => [
54
+ { builtInRole: 'Viewer', permission: 'View', isInherited: true },
55
+ { teamId: 7, team: 'village-grafana', permission: 'Edit', isInherited: false },
56
+ ]);
57
+ const out = await run(fake, grafanaOperations(spec).reconcile(props));
58
+ expect(out).toEqual({
59
+ folderUid: 'village-grafana',
60
+ teamId: 7,
61
+ teamName: 'village-grafana',
62
+ permission: 'Edit',
63
+ });
64
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
65
+ });
66
+
67
+ test('create writes only through the per-team endpoint', async () => {
68
+ const state = { set: false };
69
+ const fake = fakeGrafana((m, url) => {
70
+ if (m === 'GET' && url.pathname === TEAMS)
71
+ return Response.json({ teams: [teamRow], totalCount: 1 });
72
+ if (m === 'GET' && url.pathname === PERMS)
73
+ return Response.json(
74
+ state.set ? [{ teamId: 7, permission: 'Edit', isInherited: false }] : [],
75
+ );
76
+ if (m === 'POST' && url.pathname === SET) {
77
+ state.set = true;
78
+ return Response.json({ message: 'Permission updated' });
79
+ }
80
+ return fakeFailure(404, `unexpected ${m} ${url.pathname}`);
81
+ });
82
+ await run(fake, grafanaOperations(spec).reconcile(props));
83
+ expect(fake.seen.filter((s) => s.method === 'POST')).toEqual([{ method: 'POST', path: SET }]);
84
+ expect(fake.bodies.filter((b) => b !== undefined)).toEqual([{ permission: 'Edit' }]);
85
+ });
86
+
87
+ test('a permission drift (View live, Edit declared) updates', async () => {
88
+ const fake = grafanaWith(() => [{ teamId: 7, permission: 'View', isInherited: false }]);
89
+ await run(fake, Effect.flip(grafanaOperations(spec).reconcile(props))).catch(() => undefined);
90
+ expect(fake.seen.some((s) => s.method === 'POST' && s.path === SET)).toBe(true);
91
+ });
92
+
93
+ test('a missing team fails loudly, never folds to absent', async () => {
94
+ const fake = grafanaWith(() => [], []);
95
+ const failure = await run(fake, Effect.flip(spec.fetchLive(props)));
96
+ expect((failure as { _tag: string })._tag).toBe('GrafanaTeamNotFoundError');
97
+ });
98
+
99
+ test('delete sets the empty permission, and is a no-op when already absent', async () => {
100
+ const live = grafanaWith(() => [{ teamId: 7, permission: 'Edit', isInherited: false }]);
101
+ await run(live, grafanaOperations(spec).destroy(props));
102
+ expect(live.bodies.filter((b) => b !== undefined)).toEqual([{ permission: '' }]);
103
+ const gone = grafanaWith(() => []);
104
+ await run(gone, grafanaOperations(spec).destroy(props));
105
+ expect(gone.seen.every((s) => s.method === 'GET')).toBe(true);
106
+ });
107
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `Grafana.FolderPermission` — ONE team's DIRECT permission on ONE folder.
3
+ *
4
+ * Walked against `@distilled.cloud/grafana` 0.2.1 (`@homeflare/distilled-grafana`):
5
+ * `getResourcePermissions` (`GET /access-control/folders/{uid}`) and `setResourcePermissionsForTeam`
6
+ * (`POST /access-control/folders/{uid}/teams/{teamId}`).
7
+ *
8
+ * ★ ADDITIVE BY CONSTRUCTION. The per-team endpoint touches only that team's managed entry; the
9
+ * folder's other entries (built-in roles, admin users, other teams, anything inherited from a parent
10
+ * folder) are never read into a write and never changed. The whole-list `setResourcePermissions`
11
+ * would replace them all — deliberately not used.
12
+ * ★ ONLY DIRECT ENTRIES COUNT. An entry inherited from a parent folder (`isInherited`) is not this
13
+ * resource's object: it can't be changed here, so it never satisfies a declaration either.
14
+ * ★ The team is named, not numbered. Team ids differ per instance, so `teamName` is resolved through
15
+ * `findTeamByName` on every read and write; a missing team fails loudly with
16
+ * `GrafanaTeamNotFoundError` (declare the `Grafana.Team` first) instead of folding to "absent".
17
+ * ★ delete = set the team's permission to "" (Grafana's documented way to remove a managed entry),
18
+ * idempotent: an already-absent entry is a no-op (S11).
19
+ */
20
+ import { Resource } from 'alchemy';
21
+ import * as Provider from 'alchemy/Provider';
22
+ import * as grafana from '@distilled.cloud/grafana';
23
+ import * as Data from 'effect/Data';
24
+ import * as Effect from 'effect/Effect';
25
+ import { type GrafanaSpec, grafanaHandlers } from './resource.ts';
26
+ import { findTeamByName } from './team.ts';
27
+
28
+ export type FolderPermissionLevel = 'View' | 'Edit' | 'Admin';
29
+
30
+ export interface FolderPermissionProps {
31
+ folderUid: string;
32
+ teamName: string;
33
+ permission: FolderPermissionLevel;
34
+ }
35
+
36
+ export interface FolderPermissionAttributes {
37
+ folderUid: string;
38
+ teamId: number;
39
+ teamName: string;
40
+ permission: string;
41
+ }
42
+
43
+ export interface GrafanaFolderPermission extends Resource<
44
+ 'Grafana.FolderPermission',
45
+ FolderPermissionProps,
46
+ FolderPermissionAttributes,
47
+ never
48
+ > {}
49
+
50
+ export const GrafanaFolderPermission = Resource<GrafanaFolderPermission>(
51
+ 'Grafana.FolderPermission',
52
+ );
53
+
54
+ export class GrafanaTeamNotFoundError extends Data.TaggedError('GrafanaTeamNotFoundError')<{
55
+ readonly message: string;
56
+ }> {}
57
+
58
+ interface LivePermission {
59
+ readonly teamId: number;
60
+ readonly permission: string;
61
+ }
62
+
63
+ const resolveTeamId = (teamName: string) =>
64
+ findTeamByName(teamName).pipe(
65
+ Effect.flatMap((team) =>
66
+ team === undefined
67
+ ? Effect.fail(
68
+ new GrafanaTeamNotFoundError({
69
+ message: `Grafana.FolderPermission: team '${teamName}' does not exist. Declare it with Grafana.Team first.`,
70
+ }),
71
+ )
72
+ : Effect.succeed(team.id),
73
+ ),
74
+ );
75
+
76
+ const setTeam = (folderUid: string, teamId: number, permission: string) =>
77
+ grafana.setResourcePermissionsForTeam({
78
+ resource: 'folders',
79
+ resourceID: folderUid,
80
+ teamID: teamId,
81
+ permission,
82
+ });
83
+
84
+ export const spec: GrafanaSpec<
85
+ FolderPermissionProps,
86
+ LivePermission,
87
+ FolderPermissionAttributes,
88
+ | grafana.SearchTeamsError
89
+ | grafana.GetResourcePermissionsError
90
+ | grafana.SetResourcePermissionsForTeamError
91
+ | GrafanaTeamNotFoundError
92
+ > = {
93
+ attributes: (live, props) => ({
94
+ folderUid: props.folderUid,
95
+ permission: live.permission,
96
+ teamId: live.teamId,
97
+ teamName: props.teamName,
98
+ }),
99
+ create: (props) =>
100
+ resolveTeamId(props.teamName).pipe(
101
+ Effect.flatMap((teamId) => setTeam(props.folderUid, teamId, props.permission)),
102
+ ),
103
+ destroy: (props, live) => setTeam(props.folderUid, live.teamId, ''),
104
+ fetchLive: (props) =>
105
+ Effect.gen(function* () {
106
+ const teamId = yield* resolveTeamId(props.teamName);
107
+ const entries = yield* grafana.getResourcePermissions({
108
+ resource: 'folders',
109
+ resourceID: props.folderUid,
110
+ });
111
+ const direct = (entries ?? []).find(
112
+ (entry) => entry.teamId === teamId && entry.isInherited !== true,
113
+ );
114
+ return direct === undefined
115
+ ? undefined
116
+ : { permission: direct.permission ?? '', teamId: teamId };
117
+ }),
118
+ matches: (attributes, props) => attributes.permission === props.permission,
119
+ update: (props, live) => setTeam(props.folderUid, live.teamId, props.permission),
120
+ };
121
+
122
+ export const handlers = grafanaHandlers(spec);
123
+
124
+ export const GrafanaFolderPermissionProvider = () =>
125
+ Provider.effect(
126
+ GrafanaFolderPermission,
127
+ Effect.succeed(GrafanaFolderPermission.Provider.of(handlers)),
128
+ );
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * `Grafana.Datasource`, `Grafana.Folder`, `Grafana.Dashboard`, `Grafana.ContactPoint`,
10
10
  * `Grafana.MuteTiming`, `Grafana.MessageTemplate`, `Grafana.AlertRuleGroup` and
11
- * `Grafana.NotificationPolicy` all ship here — see docs/grafana.md, docs/grafana-folder-dashboard.md,
11
+ * `Grafana.NotificationPolicy`, `Grafana.Team` and `Grafana.FolderPermission` all ship here — see docs/grafana.md, docs/grafana-folder-dashboard.md,
12
12
  * docs/grafana-alerting.md, docs/grafana-alerting-rules.md and
13
13
  * docs/grafana-notification-policy.md for what shipped.
14
14
  */
@@ -46,6 +46,15 @@ export {
46
46
  GrafanaFolderProvider,
47
47
  GrafanaFolderReparentError,
48
48
  } from './folder.ts';
49
+ export { type TeamAttributes, type TeamProps, GrafanaTeam, GrafanaTeamProvider } from './team.ts';
50
+ export {
51
+ type FolderPermissionAttributes,
52
+ type FolderPermissionLevel,
53
+ type FolderPermissionProps,
54
+ GrafanaFolderPermission,
55
+ GrafanaFolderPermissionProvider,
56
+ GrafanaTeamNotFoundError,
57
+ } from './folder-permission.ts';
49
58
  export {
50
59
  type ContactPointAttributes,
51
60
  type ContactPointProps,
@@ -18,6 +18,8 @@ import { type GrafanaTarget, grafanaCredentials } from './credentials.ts';
18
18
  import { GrafanaDashboardProvider } from './dashboard.ts';
19
19
  import { GrafanaDatasourceProvider } from './datasource.ts';
20
20
  import { GrafanaFolderProvider } from './folder.ts';
21
+ import { GrafanaFolderPermissionProvider } from './folder-permission.ts';
22
+ import { GrafanaTeamProvider } from './team.ts';
21
23
  import { GrafanaMessageTemplateProvider } from './message-template.ts';
22
24
  import { GrafanaMuteTimingProvider } from './mute-timing.ts';
23
25
  import { GrafanaNotificationPolicyProvider } from './notification-policy.ts';
@@ -26,6 +28,8 @@ export const grafanaProviders = (target: GrafanaTarget) =>
26
28
  Layer.mergeAll(
27
29
  GrafanaDatasourceProvider(),
28
30
  GrafanaFolderProvider(),
31
+ GrafanaTeamProvider(),
32
+ GrafanaFolderPermissionProvider(),
29
33
  GrafanaDashboardProvider(),
30
34
  GrafanaContactPointProvider(),
31
35
  GrafanaMuteTimingProvider(),
@@ -29,7 +29,7 @@ export class GrafanaProvisionedObjectError extends Data.TaggedError(
29
29
  }> {}
30
30
 
31
31
  export const refuseIfProvisioned = (
32
- kind: 'Grafana.Folder' | 'Grafana.Dashboard',
32
+ kind: 'Grafana.Folder' | 'Grafana.Dashboard' | 'Grafana.Team',
33
33
  uid: string,
34
34
  owner: string | undefined,
35
35
  ): Effect.Effect<never, GrafanaProvisionedObjectError> => {
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `Grafana.Team` against a fake Grafana through the real `@distilled.cloud/grafana` protocol:
3
+ * exact-name observe (adopt), create body, update only on drift, provisioned refusal, idempotent delete.
4
+ */
5
+ import { describe, expect, test } from 'bun:test';
6
+ import type { GrafanaOpContext } from '@distilled.cloud/grafana/Protocol';
7
+ import * as Effect from 'effect/Effect';
8
+ import { fakeFailure, fakeGrafana, fakeGrafanaLayer } from './fake-grafana.ts';
9
+ import { grafanaOperations } from './resource.ts';
10
+ import { spec } from './team.ts';
11
+
12
+ const team = (over: Record<string, unknown> = {}) => ({
13
+ id: 7,
14
+ uid: 'aex7',
15
+ name: 'village-grafana',
16
+ email: '',
17
+ isProvisioned: false,
18
+ memberCount: 0,
19
+ orgId: 1,
20
+ ...over,
21
+ });
22
+ const search = (teams: unknown[]) =>
23
+ Response.json({ teams, totalCount: teams.length, page: 1, perPage: 100 });
24
+ const run = <A, E>(
25
+ fake: ReturnType<typeof fakeGrafana>,
26
+ effect: Effect.Effect<A, E, GrafanaOpContext>,
27
+ ): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(fakeGrafanaLayer(fake.fetch))));
28
+ const props = { name: 'village-grafana' };
29
+
30
+ describe('Grafana.Team', () => {
31
+ test('fetchLive keeps only an exact name match, GET without a body', async () => {
32
+ const fake = fakeGrafana((m, url) =>
33
+ m === 'GET' && url.pathname === '/api/teams/search'
34
+ ? search([team({ name: 'village-grafana-old', id: 9 }), team()])
35
+ : fakeFailure(404, 'x'),
36
+ );
37
+ const live = await run(fake, spec.fetchLive(props));
38
+ expect(live?.id).toBe(7);
39
+ expect(fake.bodies[0]).toBeUndefined();
40
+ });
41
+
42
+ test('absent team folds to undefined; 403 propagates', async () => {
43
+ expect(
44
+ await run(
45
+ fakeGrafana(() => search([])),
46
+ spec.fetchLive(props),
47
+ ),
48
+ ).toBeUndefined();
49
+ const failure = await run(
50
+ fakeGrafana(() => fakeFailure(403, 'forbidden')),
51
+ Effect.flip(spec.fetchLive(props)),
52
+ );
53
+ expect((failure as { _tag: string })._tag).toBe('Forbidden');
54
+ });
55
+
56
+ test('reconcile adopts a matching live team with no write', async () => {
57
+ const fake = fakeGrafana((m) =>
58
+ m === 'GET' ? search([team()]) : fakeFailure(500, 'no write expected'),
59
+ );
60
+ const out = await run(fake, grafanaOperations(spec).reconcile(props));
61
+ expect(out).toMatchObject({ id: 7, name: 'village-grafana' });
62
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
63
+ });
64
+
65
+ test('reconcile creates when absent, sending only declared fields', async () => {
66
+ let created = false;
67
+ const fake = fakeGrafana((m) => {
68
+ if (m === 'POST') {
69
+ created = true;
70
+ return Response.json({ teamId: 7, uid: 'aex7', message: 'Team created' });
71
+ }
72
+ return search(created ? [team()] : []);
73
+ });
74
+ await run(fake, grafanaOperations(spec).reconcile(props));
75
+ expect(fake.bodies.filter((b) => b !== undefined)).toEqual([{ name: 'village-grafana' }]);
76
+ });
77
+
78
+ test('email drift updates by team id', async () => {
79
+ let updated = false;
80
+ const fake = fakeGrafana((m) => {
81
+ if (m === 'PUT') {
82
+ updated = true;
83
+ return Response.json({ message: 'Team updated' });
84
+ }
85
+ return search([team({ email: updated ? 'ops@example.com' : '' })]);
86
+ });
87
+ await run(fake, grafanaOperations(spec).reconcile({ ...props, email: 'ops@example.com' }));
88
+ expect(fake.seen.some((s) => s.method === 'PUT' && s.path === '/api/teams/7')).toBe(true);
89
+ });
90
+
91
+ test('a provisioned team refuses update and delete without writing', async () => {
92
+ const fake = fakeGrafana(() => search([team({ isProvisioned: true, email: 'a@b' })]));
93
+ const failure = await run(
94
+ fake,
95
+ Effect.flip(grafanaOperations(spec).reconcile({ ...props, email: 'ops@example.com' })),
96
+ );
97
+ expect((failure as { _tag: string })._tag).toBe('GrafanaProvisionedObjectError');
98
+ const del = await run(fake, Effect.flip(grafanaOperations(spec).destroy(props)));
99
+ expect((del as { _tag: string })._tag).toBe('GrafanaProvisionedObjectError');
100
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
101
+ });
102
+
103
+ test('delete is idempotent when the team is already gone', async () => {
104
+ const fake = fakeGrafana(() => search([]));
105
+ await run(fake, grafanaOperations(spec).destroy(props));
106
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
107
+ });
108
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `Grafana.Team` — a Grafana team, keyed by NAME.
3
+ *
4
+ * Walked against `@distilled.cloud/grafana` 0.2.1 (`@homeflare/distilled-grafana`): `searchTeams`,
5
+ * `createTeam`, `updateTeam`, `deleteTeam`. Upstream Alchemy ships no Grafana family (S1); distilled
6
+ * ships the SDK, so every call goes through it (S23).
7
+ *
8
+ * ★ NAME IS THE KEY, NOT `uid`. `CreateTeamRequest` takes only `name`/`email` — there is no way to
9
+ * choose a team uid, and Grafana enforces unique team names per org. `fetchLive` therefore runs
10
+ * `GET /teams/search?name=<name>` (an exact-name filter) and keeps only an exact match; adoption of a
11
+ * team created by hand or by a script is a plain observe (S9/S10).
12
+ * ★ `email` is optional and compared only when declared ("undeclared means don't care", the kit's
13
+ * convention for optional props).
14
+ * ⛔ A provisioned team (`isProvisioned`, e.g. synced from SCIM/LDAP) is refused for update and delete.
15
+ */
16
+ import { Resource } from 'alchemy';
17
+ import * as Provider from 'alchemy/Provider';
18
+ import * as grafana from '@distilled.cloud/grafana';
19
+ import * as Effect from 'effect/Effect';
20
+ import { type GrafanaProvisionedObjectError, refuseIfProvisioned } from './provisioned.ts';
21
+ import { type GrafanaSpec, grafanaHandlers } from './resource.ts';
22
+
23
+ export interface TeamProps {
24
+ /** Team name — unique per org, and this resource's identity. */
25
+ name: string;
26
+ /** Optional team email; compared only when declared. */
27
+ email?: string;
28
+ }
29
+
30
+ export interface TeamAttributes {
31
+ id: number;
32
+ uid: string;
33
+ name: string;
34
+ email: string;
35
+ isProvisioned: boolean;
36
+ }
37
+
38
+ export interface GrafanaTeam extends Resource<'Grafana.Team', TeamProps, TeamAttributes, never> {}
39
+
40
+ export const GrafanaTeam = Resource<GrafanaTeam>('Grafana.Team');
41
+
42
+ /** Exact-name lookup. `/teams/search?name=` is already exact in Grafana; the filter keeps that true. */
43
+ export const findTeamByName = (name: string) =>
44
+ grafana
45
+ .searchTeams({ name, perpage: 100 })
46
+ .pipe(Effect.map((result) => (result.teams ?? []).find((team) => team.name === name)));
47
+
48
+ export const spec: GrafanaSpec<
49
+ TeamProps,
50
+ grafana.TeamDTO,
51
+ TeamAttributes,
52
+ | grafana.SearchTeamsError
53
+ | grafana.CreateTeamError
54
+ | grafana.UpdateTeamError
55
+ | grafana.DeleteTeamError
56
+ | GrafanaProvisionedObjectError
57
+ > = {
58
+ attributes: (live) => ({
59
+ email: live.email ?? '',
60
+ id: live.id,
61
+ isProvisioned: live.isProvisioned,
62
+ name: live.name,
63
+ uid: live.uid,
64
+ }),
65
+ create: (props) =>
66
+ grafana.createTeam({
67
+ name: props.name,
68
+ ...(props.email === undefined ? {} : { email: props.email }),
69
+ }),
70
+ destroy: (props, live) =>
71
+ Effect.gen(function* () {
72
+ if (live.isProvisioned) {
73
+ return yield* refuseIfProvisioned(
74
+ 'Grafana.Team',
75
+ props.name,
76
+ 'an external team sync (isProvisioned)',
77
+ );
78
+ }
79
+ yield* grafana.deleteTeam({ team_id: String(live.id) });
80
+ }),
81
+ fetchLive: (props) => findTeamByName(props.name),
82
+ matches: (attributes, props) =>
83
+ attributes.name === props.name &&
84
+ (props.email === undefined || attributes.email === props.email),
85
+ update: (props, live) =>
86
+ Effect.gen(function* () {
87
+ if (live.isProvisioned) {
88
+ return yield* refuseIfProvisioned(
89
+ 'Grafana.Team',
90
+ props.name,
91
+ 'an external team sync (isProvisioned)',
92
+ );
93
+ }
94
+ return yield* grafana.updateTeam({
95
+ team_id: String(live.id),
96
+ name: props.name,
97
+ ...(props.email === undefined ? {} : { email: props.email }),
98
+ });
99
+ }),
100
+ };
101
+
102
+ export const handlers = grafanaHandlers(spec);
103
+
104
+ export const GrafanaTeamProvider = () =>
105
+ Provider.effect(GrafanaTeam, Effect.succeed(GrafanaTeam.Provider.of(handlers)));
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The `Environment=` quoting contract, proven against a port of the three parsers a value crosses —
3
+ * container-env-oracle.ts, whose every rule was reproduced with the LIVE podman 5.4.2 system
4
+ * generator and a live systemd 257 unit (2026-10-11/12, `QUADLET_UNIT_DIRS=<dir>
5
+ * /usr/lib/systemd/system-generators/podman-system-generator --dryrun`; artifacts in
6
+ * /tmp/opencode/quadlet-quote/) — not inferred. The port's own header cites each measurement.
7
+ *
8
+ * The TEXT-level contract of the renderer itself (what we emit) is container-env-quote.test.ts;
9
+ * this file is the parser-level proof that what we emit SURVIVES, and that what shipped before
10
+ * did not. container-env-live.test.ts re-validates the port against the real generator wherever
11
+ * one is installed.
12
+ */
13
+ import { describe, expect, test } from 'bun:test';
14
+ import { environmentLines } from './container-env-quote.ts';
15
+ import { renderContainerFile } from './container-form.ts';
16
+ import { deployed } from './container-env-oracle.ts';
17
+
18
+ // ── The real values this row exists for (homeflare-ct100 commit 53b84c1, grafana.container) ──
19
+
20
+ const VAR = 'GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH';
21
+ const ROLE_PATH =
22
+ "(contains(groups[*], 'Admin') || contains(email, 'tim@schenanigans.com')) && 'GrafanaAdmin'";
23
+ const oracleLines = (value: string): string[] => [
24
+ 'Environment=SCOPES=openid',
25
+ `Environment=${VAR}=${value}`,
26
+ ];
27
+
28
+ describe('the unquoted renderer (what shipped before) corrupts values through the real chain', () => {
29
+ test('the deployed Grafana expression is truncated at its first space', () => {
30
+ const { env } = deployed(oracleLines(ROLE_PATH));
31
+ expect(env[VAR]).toBe('(contains(groups[*],');
32
+ // Everything after the first space produced no variable of its own here — it is simply gone.
33
+ expect(Object.keys(env).sort()).toEqual([VAR, 'SCOPES']);
34
+ });
35
+
36
+ test('a quoted fragment holding `=` becomes a bogus second variable (measured oracle output)', () => {
37
+ const { env } = deployed(
38
+ oracleLines("contains(groups[*], 'cn=admins,ou=people') && is_member('')"),
39
+ );
40
+ expect(env).toEqual({
41
+ [VAR]: 'contains(groups[*],',
42
+ cn: 'admins,ou=people)',
43
+ SCOPES: 'openid',
44
+ });
45
+ });
46
+
47
+ test('an unknown escape drops every assignment of its line, silently', () => {
48
+ const { env } = deployed(oracleLines('A=1 B=2\\d C=3'));
49
+ expect(env).toEqual({ SCOPES: 'openid' });
50
+ });
51
+
52
+ test('a bare % is consumed as a specifier, never literal data', () => {
53
+ const { env, consumed } = deployed(oracleLines('PCT=50%off'));
54
+ expect(consumed).toContain('%o');
55
+ // ⛔ Assert the VALUE the chain produced, not what it is not: `oracleLines` puts the text
56
+ // under VAR, so an `env.PCT` assertion compares `undefined` and can never fail.
57
+ expect(env[VAR]).toBe('PCT=50ff');
58
+ });
59
+
60
+ test('systemd halves $$: the shipped renderer lets `pa$$word` arrive as `pa$word`', () => {
61
+ // Measured 2026-10-12, live systemd 257 unit: the ExecStart word `pa$$word` (no `:` prefix,
62
+ // so variables ARE substituted) folds to `pa$word` — systemd.service(5) §Command lines.
63
+ const { env, consumed } = deployed(oracleLines('pa$$word'));
64
+ expect(env[VAR]).toBe('pa$word');
65
+ expect(consumed).toEqual([]);
66
+ });
67
+
68
+ test('systemd erases braced variables: the shipped renderer lets a braced HOME ref arrive as `ab`', () => {
69
+ // oxlint-disable-next-line no-template-curly-in-string -- `${HOME}` is the measured test data, not a placeholder
70
+ const { env, consumed } = deployed(oracleLines('a${HOME}b'));
71
+ expect(env[VAR]).toBe('ab');
72
+ // oxlint-disable-next-line no-template-curly-in-string -- the consumed record is the literal reference systemd erased
73
+ expect(consumed).toEqual(['${HOME}']);
74
+ });
75
+ });
76
+
77
+ describe('the quoted renderer survives the same chain intact', () => {
78
+ test('the deployed Grafana expression round-trips exactly', () => {
79
+ const lines = environmentLines({
80
+ [VAR]: ROLE_PATH,
81
+ GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT: 'true',
82
+ }).map(([key, value]) => `${key}=${value}`);
83
+ const { env, consumed } = deployed(lines);
84
+ expect(env).toEqual({
85
+ [VAR]: ROLE_PATH,
86
+ GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT: 'true',
87
+ });
88
+ expect(consumed).toEqual([]);
89
+ });
90
+
91
+ test('escapes, quotes and percent signs each round-trip to the measured generator output', () => {
92
+ const lines = environmentLines({
93
+ BACKSLASH: 'C:\\path',
94
+ PCT: '50%off',
95
+ QUOTED: 'say "hi"',
96
+ }).map(([key, value]) => `${key}=${value}`);
97
+ const { env, consumed } = deployed(lines);
98
+ expect(env).toEqual({ BACKSLASH: 'C:\\path', PCT: '50%off', QUOTED: 'say "hi"' });
99
+ expect(consumed).toEqual([]);
100
+ });
101
+
102
+ test('dollars round-trip exactly: bcrypt hashes, braced references and $$ stay data', () => {
103
+ // The live-systemd counterparts of these three arrived corrupted under the shipped renderer
104
+ // (the two tests above); each `$` doubled to `$$` — systemd's literal-dollar escape — makes
105
+ // the fold restore exactly the declared bytes. `$2y$10$abc` is a bcrypt hash prefix: the
106
+ // common real-world `$` value this round trip must hold.
107
+ // oxlint-disable-next-line no-template-curly-in-string -- `${HOME}` is the measured test data, not a placeholder
108
+ const declared = { B: 'a${HOME}b', C: '$2y$10$abc', D: 'pa$$word' };
109
+ const lines = environmentLines(declared).map(([key, value]) => `${key}=${value}`);
110
+ const { env, consumed } = deployed(lines);
111
+ expect(env).toEqual(declared);
112
+ expect(consumed).toEqual([]);
113
+ });
114
+ });
115
+
116
+ describe('renderContainerFile wires the quoting in — end to end through the chain', () => {
117
+ const props = {
118
+ name: 'grafana-primary',
119
+ container: {
120
+ environmentFile: ['/etc/grafana-primary/env'],
121
+ environment: { [VAR]: ROLE_PATH, GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT: 'true' },
122
+ image: 'docker.io/grafana/grafana:12.2.1',
123
+ network: 'host',
124
+ },
125
+ };
126
+
127
+ test('what the container would receive equals what was declared', () => {
128
+ const envLines = renderContainerFile(props)
129
+ .split('\n')
130
+ .filter((line) => line.startsWith('Environment='));
131
+ const { env, consumed } = deployed(envLines);
132
+ expect(env).toEqual(props.container.environment);
133
+ expect(consumed).toEqual([]);
134
+ });
135
+ });