@homeflare/alchemy 0.29.1 → 0.30.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 (87) hide show
  1. package/README.md +20 -0
  2. package/dist/caddy/config-reconcile.d.ts.map +1 -1
  3. package/dist/caddy/config.d.ts.map +1 -1
  4. package/dist/caddy/format-warnings.d.ts +29 -0
  5. package/dist/caddy/format-warnings.d.ts.map +1 -0
  6. package/dist/caddy/format.d.ts +50 -0
  7. package/dist/caddy/format.d.ts.map +1 -0
  8. package/dist/caddy/index.d.ts +1 -0
  9. package/dist/caddy/index.d.ts.map +1 -1
  10. package/dist/caddy/index.js +78 -10
  11. package/dist/caddy/index.js.map +7 -5
  12. package/dist/google-workspace/credentials.d.ts +54 -0
  13. package/dist/google-workspace/credentials.d.ts.map +1 -0
  14. package/dist/google-workspace/domain-alias.d.ts +74 -0
  15. package/dist/google-workspace/domain-alias.d.ts.map +1 -0
  16. package/dist/google-workspace/fake-google-workspace.d.ts +41 -0
  17. package/dist/google-workspace/fake-google-workspace.d.ts.map +1 -0
  18. package/dist/google-workspace/group-member.d.ts +73 -0
  19. package/dist/google-workspace/group-member.d.ts.map +1 -0
  20. package/dist/google-workspace/group.d.ts +87 -0
  21. package/dist/google-workspace/group.d.ts.map +1 -0
  22. package/dist/google-workspace/index.d.ts +19 -0
  23. package/dist/google-workspace/index.d.ts.map +1 -0
  24. package/dist/google-workspace/index.js +283 -0
  25. package/dist/google-workspace/index.js.map +17 -0
  26. package/dist/google-workspace/org-unit.d.ts +75 -0
  27. package/dist/google-workspace/org-unit.d.ts.map +1 -0
  28. package/dist/google-workspace/providers.d.ts +16 -0
  29. package/dist/google-workspace/providers.d.ts.map +1 -0
  30. package/dist/google-workspace/resource.d.ts +80 -0
  31. package/dist/google-workspace/resource.d.ts.map +1 -0
  32. package/dist/google-workspace/values.d.ts +28 -0
  33. package/dist/google-workspace/values.d.ts.map +1 -0
  34. package/dist/grafana/credentials.d.ts +38 -0
  35. package/dist/grafana/credentials.d.ts.map +1 -0
  36. package/dist/grafana/datasource.d.ts +90 -0
  37. package/dist/grafana/datasource.d.ts.map +1 -0
  38. package/dist/grafana/fake-grafana.d.ts +48 -0
  39. package/dist/grafana/fake-grafana.d.ts.map +1 -0
  40. package/dist/grafana/index.d.ts +17 -0
  41. package/dist/grafana/index.d.ts.map +1 -0
  42. package/dist/grafana/index.js +149 -0
  43. package/dist/grafana/index.js.map +13 -0
  44. package/dist/grafana/providers.d.ts +17 -0
  45. package/dist/grafana/providers.d.ts.map +1 -0
  46. package/dist/grafana/resource.d.ts +68 -0
  47. package/dist/grafana/resource.d.ts.map +1 -0
  48. package/docs/caddy-fmt.md +59 -0
  49. package/docs/caddy.md +7 -0
  50. package/docs/credentials.md +11 -0
  51. package/docs/google-workspace.md +105 -0
  52. package/docs/grafana.md +102 -0
  53. package/docs/peers.md +9 -0
  54. package/docs/upstream-conformance.md +37 -0
  55. package/package.json +9 -1
  56. package/src/caddy/config-format-warning.test.ts +91 -0
  57. package/src/caddy/config-reconcile.ts +6 -0
  58. package/src/caddy/config.ts +8 -1
  59. package/src/caddy/format-warnings.test.ts +50 -0
  60. package/src/caddy/format-warnings.ts +39 -0
  61. package/src/caddy/format.test.ts +105 -0
  62. package/src/caddy/format.ts +121 -0
  63. package/src/caddy/index.ts +7 -0
  64. package/src/caddy/providers.test.ts +8 -0
  65. package/src/google-workspace/credentials.test.ts +40 -0
  66. package/src/google-workspace/credentials.ts +75 -0
  67. package/src/google-workspace/domain-alias.test.ts +69 -0
  68. package/src/google-workspace/domain-alias.ts +116 -0
  69. package/src/google-workspace/fake-google-workspace.ts +54 -0
  70. package/src/google-workspace/group-member.test.ts +91 -0
  71. package/src/google-workspace/group-member.ts +126 -0
  72. package/src/google-workspace/group.test.ts +93 -0
  73. package/src/google-workspace/group.ts +128 -0
  74. package/src/google-workspace/index.ts +54 -0
  75. package/src/google-workspace/org-unit.test.ts +81 -0
  76. package/src/google-workspace/org-unit.ts +143 -0
  77. package/src/google-workspace/providers.ts +26 -0
  78. package/src/google-workspace/resource.test.ts +186 -0
  79. package/src/google-workspace/resource.ts +162 -0
  80. package/src/google-workspace/values.ts +36 -0
  81. package/src/grafana/credentials.ts +65 -0
  82. package/src/grafana/datasource.test.ts +183 -0
  83. package/src/grafana/datasource.ts +171 -0
  84. package/src/grafana/fake-grafana.ts +64 -0
  85. package/src/grafana/index.ts +22 -0
  86. package/src/grafana/providers.ts +19 -0
  87. package/src/grafana/resource.ts +120 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `GoogleWorkspace.Group` — one Admin SDK Directory group (`admin/directory/v1/groups`).
3
+ *
4
+ * ★ WHY A GROUP IS THE FIRST OBJECT CLASS, AND NOT A USER. The estate's own Cloudflare Access
5
+ * policies already reference Workspace groups by name ("Schenanigans - Admin", "Schenanigans -
6
+ * Google" — `docs/zero-trust-access-applications.md` in homeflare-landscape) as the Google IdP
7
+ * groups an Access rule allows. A group is also the one Directory object with no foreign-key
8
+ * prerequisite: `email` is the whole identity, the same reasoning `netbox/prefix.ts` gives for
9
+ * starting with `Prefix` over `Device`.
10
+ *
11
+ * ⛔ USERS ARE OUT OF SCOPE FOR THIS FAMILY. `insertUsers`/`patchUsers`/`updateUsers` all accept a
12
+ * `password` field on the wire `User` schema — modelling `GoogleWorkspace.User` here would put a
13
+ * credential-shaped field one keystroke away from becoming a prop, and Alchemy persists props
14
+ * unencrypted (S25). No stack in this estate declares a Workspace user today (measured
15
+ * 2026-09-24 — see docs/upstream-conformance.md), so there is no house need forcing the
16
+ * question. `GroupMember` (group-member.ts) references a user by email without ever creating,
17
+ * reading or storing one.
18
+ *
19
+ * ★ ADOPT-FRIENDLY BY CONSTRUCTION. `groupKey` accepts an email, alias or id; this family always
20
+ * uses `email` so `fetchLive` binds the live group by the same key a human would type. An
21
+ * instance that already has `admin@schenanigans.com`'s "Schenanigans - Admin" group is BOUND,
22
+ * never duplicated, and `attributes` maps the full live row — the exact-as-live half of
23
+ * `adopt(true)` (resource.ts's own note).
24
+ *
25
+ * ⛔ THIS RESOURCE DOES NOT DELETE BY DEFAULT. `defaultRemovalPolicy: 'retain'` — removing a group
26
+ * an Access policy still names would break sign-in for everyone that rule allows. `delete` stays
27
+ * fully implemented; a caller opts in with `.pipe(RemovalPolicy.destroy())`.
28
+ *
29
+ * ⛔ NO CREDENTIAL IS A PROP. `GOOGLE_ACCESS_TOKEN` is read from the environment at call time,
30
+ * through `@distilled.cloud/google-workspace`'s `CredentialsFromEnv` (resource.ts) — see
31
+ * credentials.ts for where that token comes from.
32
+ */
33
+ import { Resource } from 'alchemy';
34
+ import * as Provider from 'alchemy/Provider';
35
+ import * as directory from '@distilled.cloud/google-workspace/unstable/admin_directory_v1';
36
+ import * as Effect from 'effect/Effect';
37
+ import type { GoogleWorkspaceRequirements, GoogleWorkspaceSpec } from './resource.ts';
38
+ import { googleWorkspaceHandlers } from './resource.ts';
39
+ import { ciEqual, stringArray, text } from './values.ts';
40
+
41
+ export interface GroupProps {
42
+ /** The group's email address — the primary key for locate, create and update alike. */
43
+ email: string;
44
+ /** Display name. @default the local part of `email` */
45
+ name?: string;
46
+ /** Free text, up to 4,096 characters per the Directory API. */
47
+ description?: string;
48
+ }
49
+
50
+ export interface GroupAttributes {
51
+ email: string;
52
+ name: string;
53
+ description: string;
54
+ groupId: string;
55
+ /** Read-only — direct members, as a decimal string on the wire (Directory's own choice). */
56
+ directMembersCount: string;
57
+ /** Read-only. Aliases outside the primary domain/subdomains; edited only via alias endpoints. */
58
+ nonEditableAliases: string[];
59
+ /** Read-only. Aliases within the account's domains; edited only via alias endpoints. */
60
+ aliases: string[];
61
+ adminCreated: boolean;
62
+ }
63
+
64
+ export interface GoogleWorkspaceGroup extends Resource<
65
+ 'GoogleWorkspace.Group',
66
+ GroupProps,
67
+ GroupAttributes,
68
+ never,
69
+ GoogleWorkspaceRequirements
70
+ > {}
71
+
72
+ export const GoogleWorkspaceGroup = Resource<GoogleWorkspaceGroup>('GoogleWorkspace.Group', {
73
+ defaultRemovalPolicy: 'retain',
74
+ });
75
+
76
+ /** Un-declared `name` converges here — deterministic, so an undeclared field never drifts. */
77
+ const defaultName = (props: GroupProps): string =>
78
+ props.name ?? props.email.split('@')[0] ?? props.email;
79
+
80
+ const attributesOf = (live: directory.Group, props: GroupProps): GroupAttributes | undefined => {
81
+ if (live.email === undefined || !ciEqual(live.email, props.email)) return undefined;
82
+ return {
83
+ adminCreated: live.adminCreated === true,
84
+ aliases: stringArray(live.aliases),
85
+ description: text(live.description),
86
+ directMembersCount: text(live.directMembersCount),
87
+ email: live.email,
88
+ groupId: text(live.id),
89
+ name: text(live.name),
90
+ nonEditableAliases: stringArray(live.nonEditableAliases),
91
+ };
92
+ };
93
+
94
+ /** ★ EXPORTED for direct testing with an explicit fake `Credentials` layer — see group.test.ts. */
95
+ export const spec: GoogleWorkspaceSpec<
96
+ GroupProps,
97
+ directory.Group,
98
+ GroupAttributes,
99
+ | directory.GetGroupsError
100
+ | directory.InsertGroupsError
101
+ | directory.PatchGroupsError
102
+ | directory.DeleteGroupsError
103
+ > = {
104
+ attributes: attributesOf,
105
+ create: (props) =>
106
+ directory.insertGroups({
107
+ body: { description: props.description ?? '', email: props.email, name: defaultName(props) },
108
+ }),
109
+ describe: (props) => `groups/${props.email}`,
110
+ destroy: (_props, live) =>
111
+ directory.deleteGroups({ groupKey: text(live.id) || text(live.email) }),
112
+ fetchLive: (props) =>
113
+ directory
114
+ .getGroups({ groupKey: props.email })
115
+ .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
116
+ matches: (attributes, props) =>
117
+ attributes.description === (props.description ?? '') && attributes.name === defaultName(props),
118
+ update: (props, live) =>
119
+ directory.patchGroups({
120
+ body: { description: props.description ?? '', name: defaultName(props) },
121
+ groupKey: text(live.id) || text(live.email),
122
+ }),
123
+ };
124
+
125
+ export const handlers = googleWorkspaceHandlers(spec);
126
+
127
+ export const GoogleWorkspaceGroupProvider = () =>
128
+ Provider.effect(GoogleWorkspaceGroup, Effect.succeed(GoogleWorkspaceGroup.Provider.of(handlers)));
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Google Workspace providers for Alchemy.
3
+ *
4
+ * ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY. These are
5
+ * the symbols a real stack consumes; the rest of the files are internals a provider needs but a
6
+ * consumer should not depend on. An `export *` here would publish every helper as API and make
7
+ * the next refactor a breaking change.
8
+ * ★ `credentials.ts`'s re-exports ARE published, because a stack needs `CredentialsFromEnv` (or
9
+ * `fromAccessToken` in a test) to build a runtime layer, and `GoogleWorkspaceKeyRef` /
10
+ * `describeKeyRef` are the documentation seam credential setup is meant to go through.
11
+ */
12
+ export {
13
+ type GoogleWorkspaceConfig,
14
+ Credentials,
15
+ CredentialsFromEnv,
16
+ describeKeyRef,
17
+ fromAccessToken,
18
+ GOOGLE_ACCESS_TOKEN_ENV,
19
+ GOOGLE_PROJECT_ID_ENV,
20
+ type GoogleWorkspaceKeyRef,
21
+ } from './credentials.ts';
22
+ export {
23
+ GoogleWorkspaceDomainAlias,
24
+ GoogleWorkspaceDomainAliasProvider,
25
+ type DomainAliasAttributes,
26
+ type DomainAliasProps,
27
+ } from './domain-alias.ts';
28
+ export {
29
+ GoogleWorkspaceGroup,
30
+ GoogleWorkspaceGroupProvider,
31
+ type GroupAttributes,
32
+ type GroupProps,
33
+ } from './group.ts';
34
+ export {
35
+ GoogleWorkspaceGroupMember,
36
+ GoogleWorkspaceGroupMemberProvider,
37
+ type DeliverySettings,
38
+ type GroupMemberAttributes,
39
+ type GroupMemberProps,
40
+ type MemberRole,
41
+ } from './group-member.ts';
42
+ export {
43
+ GoogleWorkspaceOrgUnit,
44
+ GoogleWorkspaceOrgUnitProvider,
45
+ type OrgUnitAttributes,
46
+ type OrgUnitProps,
47
+ } from './org-unit.ts';
48
+ export { googleWorkspaceProviders } from './providers.ts';
49
+ export {
50
+ googleWorkspaceHandlers,
51
+ googleWorkspaceOperations,
52
+ type GoogleWorkspaceRequirements,
53
+ type GoogleWorkspaceSpec,
54
+ } from './resource.ts';
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `GoogleWorkspace.OrgUnit`'s `spec` against a fake Directory API — proves the leading-slash
3
+ * asymmetry `values.ts`'s `stripLeadingSlash`/`withLeadingSlash` exist for (org-unit.ts's note).
4
+ */
5
+ import { describe, expect, test } from 'bun:test';
6
+ import * as Effect from 'effect/Effect';
7
+ import {
8
+ fakeFailure,
9
+ fakeGoogleWorkspace,
10
+ fakeGoogleWorkspaceLayer,
11
+ } from './fake-google-workspace.ts';
12
+ import { spec } from './org-unit.ts';
13
+
14
+ const UNIT_PATH = '/admin/directory/v1/customer/my_customer/orgunits/Contractors/2026';
15
+ const props = {
16
+ orgUnitPath: '/Contractors/2026',
17
+ parentOrgUnitPath: '/Contractors',
18
+ description: '2026 cohort',
19
+ };
20
+
21
+ const readThrough = (fetchFn: typeof globalThis.fetch) =>
22
+ Effect.runPromise(
23
+ spec.fetchLive(props).pipe(
24
+ Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))),
25
+ Effect.provide(fakeGoogleWorkspaceLayer(fetchFn)),
26
+ ),
27
+ );
28
+
29
+ describe('GoogleWorkspace.OrgUnit spec.fetchLive + spec.attributes', () => {
30
+ test('the path is sent without its leading slash, and read back with one', async () => {
31
+ const fake = fakeGoogleWorkspace((method, url) =>
32
+ method === 'GET' && url.pathname === UNIT_PATH
33
+ ? Response.json({
34
+ description: '2026 cohort',
35
+ name: '2026',
36
+ orgUnitId: 'id:0123',
37
+ orgUnitPath: '/Contractors/2026',
38
+ parentOrgUnitPath: '/Contractors',
39
+ })
40
+ : fakeFailure(404, 'not found', 'NOT_FOUND'),
41
+ );
42
+ expect(await readThrough(fake.fetch)).toEqual({
43
+ customerId: 'my_customer',
44
+ description: '2026 cohort',
45
+ name: '2026',
46
+ orgUnitId: 'id:0123',
47
+ orgUnitPath: '/Contractors/2026',
48
+ parentOrgUnitPath: '/Contractors',
49
+ });
50
+ expect(fake.seen).toEqual([{ method: 'GET', path: UNIT_PATH }]);
51
+ });
52
+
53
+ test('a 404 folds to undefined', async () => {
54
+ const fake = fakeGoogleWorkspace(() => fakeFailure(404, 'not found', 'NOT_FOUND'));
55
+ expect(await readThrough(fake.fetch)).toBeUndefined();
56
+ });
57
+ });
58
+
59
+ describe('GoogleWorkspace.OrgUnit spec.matches', () => {
60
+ const live = {
61
+ customerId: 'my_customer',
62
+ description: '2026 cohort',
63
+ name: '2026',
64
+ orgUnitId: 'id:0123',
65
+ orgUnitPath: '/Contractors/2026',
66
+ parentOrgUnitPath: '/Contractors',
67
+ };
68
+
69
+ test('matching name, parent and description is no drift', () => {
70
+ expect(spec.matches(live, props)).toBe(true);
71
+ });
72
+
73
+ test('a moved parent is drift', () => {
74
+ expect(spec.matches(live, { ...props, parentOrgUnitPath: '/Employees' })).toBe(false);
75
+ });
76
+
77
+ test('an undeclared parentOrgUnitPath converges to the root, deterministically', () => {
78
+ const { parentOrgUnitPath: _p, ...rootProps } = props;
79
+ expect(spec.matches({ ...live, parentOrgUnitPath: '/' }, rootProps)).toBe(true);
80
+ });
81
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * `GoogleWorkspace.OrgUnit` — one organizational unit (`admin/directory/v1/customer/{id}/orgunits`).
3
+ *
4
+ * ★ `orgUnitPath` IS THE IDENTITY the way `NetboxPrefix.prefix` is. Google's own doc line is
5
+ * copied onto `OrgUnitProps.orgUnitPath` below because it is the one field this resource cannot
6
+ * get wrong: it is DERIVED on read (`parentOrgUnitPath` + `name`) but AUTHORED as a full path on
7
+ * write, and the API's own path-parameter form drops the leading `/` (`GetOrgunitsRequest`'s
8
+ * `orgUnitPath` doc: "the full path … minus the leading `/`"). `values.ts`'s
9
+ * `stripLeadingSlash`/`withLeadingSlash` are the one place that asymmetry is handled.
10
+ *
11
+ * ⛔ THIS RESOURCE DOES NOT REMOVE SUB-UNITS OR MOVE USERS. Deleting an org unit that still has
12
+ * children or members fails server-side (Directory refuses rather than cascading) — nothing
13
+ * here retries around that refusal, matching S11's "observe before deleting", not "force
14
+ * through".
15
+ *
16
+ * ⛔ `defaultRemovalPolicy: 'retain'` — an org unit governs which Workspace services a user can
17
+ * reach (Google's own doc line on `orgUnitPath`); deleting one out from under live users is
18
+ * exactly the kind of hard-to-reverse action this policy exists to require an explicit opt-in
19
+ * for.
20
+ */
21
+ import { Resource } from 'alchemy';
22
+ import * as Provider from 'alchemy/Provider';
23
+ import * as directory from '@distilled.cloud/google-workspace/unstable/admin_directory_v1';
24
+ import * as Effect from 'effect/Effect';
25
+ import type { GoogleWorkspaceRequirements, GoogleWorkspaceSpec } from './resource.ts';
26
+ import { googleWorkspaceHandlers } from './resource.ts';
27
+ import { stripLeadingSlash, text, withLeadingSlash } from './values.ts';
28
+
29
+ export interface OrgUnitProps {
30
+ /** The unique id for the Workspace account, or `my_customer` for the caller's own account. */
31
+ customerId?: string;
32
+ /**
33
+ * The full path to the organizational unit, e.g. `/Contractors/2026`. Required. In order to
34
+ * edit an `orgUnitPath`, either update `name` or `parentOrgUnitPath` — this field is derived on
35
+ * read, not itself patchable, so declaring a different path here plans a `replace`.
36
+ */
37
+ orgUnitPath: string;
38
+ /** The parent's full path, e.g. `/Contractors`. Omit for a top-level unit under `/`. */
39
+ parentOrgUnitPath?: string;
40
+ description?: string;
41
+ }
42
+
43
+ export interface OrgUnitAttributes {
44
+ customerId: string;
45
+ orgUnitPath: string;
46
+ parentOrgUnitPath: string;
47
+ name: string;
48
+ description: string;
49
+ orgUnitId: string;
50
+ }
51
+
52
+ export interface GoogleWorkspaceOrgUnit extends Resource<
53
+ 'GoogleWorkspace.OrgUnit',
54
+ OrgUnitProps,
55
+ OrgUnitAttributes,
56
+ never,
57
+ GoogleWorkspaceRequirements
58
+ > {}
59
+
60
+ export const GoogleWorkspaceOrgUnit = Resource<GoogleWorkspaceOrgUnit>('GoogleWorkspace.OrgUnit', {
61
+ defaultRemovalPolicy: 'retain',
62
+ });
63
+
64
+ const customerOf = (props: OrgUnitProps): string => props.customerId ?? 'my_customer';
65
+ /** The path segment the API wants, split from its own leading-name component for `name`/`insert`. */
66
+ const nameOf = (props: OrgUnitProps): string =>
67
+ props.orgUnitPath.split('/').filter(Boolean).pop() ?? props.orgUnitPath;
68
+ const parentOf = (props: OrgUnitProps): string => props.parentOrgUnitPath ?? '/';
69
+
70
+ const attributesOf = (
71
+ live: directory.OrgUnit,
72
+ props: OrgUnitProps,
73
+ ): OrgUnitAttributes | undefined => {
74
+ const livePath = live.orgUnitPath === undefined ? undefined : withLeadingSlash(live.orgUnitPath);
75
+ if (livePath === undefined || livePath.toLowerCase() !== props.orgUnitPath.toLowerCase()) {
76
+ return undefined;
77
+ }
78
+ return {
79
+ customerId: customerOf(props),
80
+ description: text(live.description),
81
+ name: text(live.name),
82
+ orgUnitId: text(live.orgUnitId),
83
+ orgUnitPath: livePath,
84
+ parentOrgUnitPath: text(live.parentOrgUnitPath),
85
+ };
86
+ };
87
+
88
+ /** ★ EXPORTED for direct testing — see org-unit.test.ts. */
89
+ export const spec: GoogleWorkspaceSpec<
90
+ OrgUnitProps,
91
+ directory.OrgUnit,
92
+ OrgUnitAttributes,
93
+ | directory.GetOrgunitsError
94
+ | directory.InsertOrgunitsError
95
+ | directory.PatchOrgunitsError
96
+ | directory.DeleteOrgunitsError
97
+ > = {
98
+ attributes: attributesOf,
99
+ create: (props) =>
100
+ directory.insertOrgunits({
101
+ body: {
102
+ description: props.description ?? '',
103
+ name: nameOf(props),
104
+ parentOrgUnitPath: parentOf(props),
105
+ },
106
+ customerId: customerOf(props),
107
+ }),
108
+ describe: (props) => `customer/${customerOf(props)}/orgunits${props.orgUnitPath}`,
109
+ destroy: (props, live) =>
110
+ directory.deleteOrgunits({
111
+ customerId: customerOf(props),
112
+ orgUnitPath: stripLeadingSlash(text(live.orgUnitPath) || props.orgUnitPath),
113
+ }),
114
+ fetchLive: (props) =>
115
+ directory
116
+ .getOrgunits({
117
+ customerId: customerOf(props),
118
+ orgUnitPath: stripLeadingSlash(props.orgUnitPath),
119
+ })
120
+ .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
121
+ matches: (attributes, props) =>
122
+ attributes.description === (props.description ?? '') &&
123
+ attributes.parentOrgUnitPath === parentOf(props) &&
124
+ attributes.name === nameOf(props),
125
+ update: (props, live) =>
126
+ directory.patchOrgunits({
127
+ body: {
128
+ description: props.description ?? '',
129
+ name: nameOf(props),
130
+ parentOrgUnitPath: parentOf(props),
131
+ },
132
+ customerId: customerOf(props),
133
+ orgUnitPath: stripLeadingSlash(text(live.orgUnitPath) || props.orgUnitPath),
134
+ }),
135
+ };
136
+
137
+ export const handlers = googleWorkspaceHandlers(spec);
138
+
139
+ export const GoogleWorkspaceOrgUnitProvider = () =>
140
+ Provider.effect(
141
+ GoogleWorkspaceOrgUnit,
142
+ Effect.succeed(GoogleWorkspaceOrgUnit.Provider.of(handlers)),
143
+ );
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Every Google Workspace provider, wired once — the same role `litellmProviders`/`netboxProviders`-
3
+ * style helpers play elsewhere in this kit (S16: "a subpath's `providers()` plays the same role" as
4
+ * upstream's `Providers.ts`).
5
+ *
6
+ * Layer.mergeAll(googleWorkspaceProviders(), …the stack's other providers)
7
+ *
8
+ * ⚠️ CREDENTIALS ARE NOT A PARAMETER HERE. Every handler already closes over
9
+ * `CredentialsFromEnv` inside `resource.ts` (`GOOGLE_ACCESS_TOKEN`, resolved at call time — see
10
+ * credentials.ts), so this layer only wires the four resource providers together. `HttpClient`
11
+ * is NOT provided here either — it is ambient in every Alchemy runtime, the same note
12
+ * `litellm/providers.ts` makes for its own family.
13
+ */
14
+ import * as Layer from 'effect/Layer';
15
+ import { GoogleWorkspaceDomainAliasProvider } from './domain-alias.ts';
16
+ import { GoogleWorkspaceGroupProvider } from './group.ts';
17
+ import { GoogleWorkspaceGroupMemberProvider } from './group-member.ts';
18
+ import { GoogleWorkspaceOrgUnitProvider } from './org-unit.ts';
19
+
20
+ export const googleWorkspaceProviders = () =>
21
+ Layer.mergeAll(
22
+ GoogleWorkspaceGroupProvider(),
23
+ GoogleWorkspaceGroupMemberProvider(),
24
+ GoogleWorkspaceDomainAliasProvider(),
25
+ GoogleWorkspaceOrgUnitProvider(),
26
+ );
@@ -0,0 +1,186 @@
1
+ /**
2
+ * `googleWorkspaceOperations`'s shared `diff`/`reconcile` engine, exercised end-to-end through a
3
+ * fake Directory API — not just through a resource's own `spec.matches`/`fetchLive` in isolation.
4
+ *
5
+ * 🔴 WHY THIS FILE EXISTS. An adversarial review of this family (2026-09-24) found that every
6
+ * other test here calls `spec.fetchLive`/`spec.attributes`/`spec.matches` directly and never
7
+ * `googleWorkspaceOperations(spec).diff`/`.reconcile` — so a bug INSIDE `resource.ts` itself
8
+ * (shared by all four resources) could invert `diff`'s `noop`/`update` branches, or skip the
9
+ * "write returned no error but the object is still absent" die-guard, and no test here would
10
+ * catch it. This file closes that gap: it drives the real engine against real specs (`group.ts`,
11
+ * whose spec HAS `update`, and `domain-alias.ts`, whose spec does NOT), through the same fake
12
+ * HTTP layer every other test in this family uses.
13
+ */
14
+ import { describe, expect, test } from 'bun:test';
15
+ import * as Effect from 'effect/Effect';
16
+ import { spec as aliasSpec } from './domain-alias.ts';
17
+ import {
18
+ fakeFailure,
19
+ fakeGoogleWorkspace,
20
+ fakeGoogleWorkspaceLayer,
21
+ } from './fake-google-workspace.ts';
22
+ import { spec as groupSpec } from './group.ts';
23
+ import { googleWorkspaceOperations } from './resource.ts';
24
+
25
+ const groupOps = googleWorkspaceOperations(groupSpec);
26
+ const groupProps = { email: 'ops@schenanigans.com', name: 'Ops', description: 'ops team' };
27
+ const GROUP_PATH = '/admin/directory/v1/groups/ops%40schenanigans.com';
28
+
29
+ describe('googleWorkspaceOperations(group spec).reconcile', () => {
30
+ test('absent → create, then read back — the happy create path', async () => {
31
+ let created = false;
32
+ const fake = fakeGoogleWorkspace((method, url) => {
33
+ if (method === 'POST' && url.pathname === '/admin/directory/v1/groups') {
34
+ created = true;
35
+ return Response.json({ email: groupProps.email, name: 'Ops' });
36
+ }
37
+ if (method === 'GET' && url.pathname === GROUP_PATH) {
38
+ return created
39
+ ? Response.json({ description: 'ops team', email: groupProps.email, name: 'Ops' })
40
+ : fakeFailure(404, 'not found', 'NOT_FOUND');
41
+ }
42
+ return fakeFailure(404, 'not found', 'NOT_FOUND');
43
+ });
44
+ const attrs = await Effect.runPromise(
45
+ groupOps.reconcile(groupProps).pipe(Effect.provide(fakeGoogleWorkspaceLayer(fake.fetch))),
46
+ );
47
+ expect(created).toBe(true);
48
+ expect(attrs.email).toBe(groupProps.email);
49
+ });
50
+
51
+ test('present and matching → no PATCH is ever sent', async () => {
52
+ const fake = fakeGoogleWorkspace((method, url) =>
53
+ method === 'GET' && url.pathname === GROUP_PATH
54
+ ? Response.json({ description: 'ops team', email: groupProps.email, name: 'Ops' })
55
+ : fakeFailure(404, 'not found', 'NOT_FOUND'),
56
+ );
57
+ await Effect.runPromise(
58
+ groupOps.reconcile(groupProps).pipe(Effect.provide(fakeGoogleWorkspaceLayer(fake.fetch))),
59
+ );
60
+ expect(fake.seen.some((r) => r.method === 'PATCH')).toBe(false);
61
+ });
62
+
63
+ test('present and drifted → PATCH is sent exactly once', async () => {
64
+ let patched = false;
65
+ const fake = fakeGoogleWorkspace((method, url) => {
66
+ if (method === 'PATCH' && url.pathname === GROUP_PATH) {
67
+ patched = true;
68
+ return Response.json({ description: 'ops team', email: groupProps.email, name: 'Ops' });
69
+ }
70
+ if (method === 'GET' && url.pathname === GROUP_PATH) {
71
+ return Response.json({
72
+ description: 'stale description',
73
+ email: groupProps.email,
74
+ name: 'Ops',
75
+ });
76
+ }
77
+ return fakeFailure(404, 'not found', 'NOT_FOUND');
78
+ });
79
+ await Effect.runPromise(
80
+ groupOps.reconcile(groupProps).pipe(Effect.provide(fakeGoogleWorkspaceLayer(fake.fetch))),
81
+ );
82
+ expect(patched).toBe(true);
83
+ });
84
+
85
+ test('a write that reports success but leaves the object absent dies — never lies to the plan', async () => {
86
+ const fake = fakeGoogleWorkspace(
87
+ (method, url) =>
88
+ method === 'POST' && url.pathname === '/admin/directory/v1/groups'
89
+ ? Response.json({})
90
+ : fakeFailure(404, 'not found', 'NOT_FOUND'), // every GET stays 404, even after "create"
91
+ );
92
+ const failed = await Effect.runPromiseExit(
93
+ groupOps.reconcile(groupProps).pipe(Effect.provide(fakeGoogleWorkspaceLayer(fake.fetch))),
94
+ );
95
+ expect(failed._tag).toBe('Failure');
96
+ });
97
+ });
98
+
99
+ describe('googleWorkspaceOperations(group spec).diff', () => {
100
+ const live = {
101
+ adminCreated: false,
102
+ aliases: [],
103
+ description: 'ops team',
104
+ directMembersCount: '0',
105
+ email: groupProps.email,
106
+ groupId: 'x',
107
+ name: 'Ops',
108
+ nonEditableAliases: [],
109
+ };
110
+
111
+ test('output undefined → undefined (engine default: treat as update)', async () => {
112
+ // ⚠️ `diff` short-circuits on `output === undefined` before it ever reads — no request is
113
+ // made — but the Effect's TYPE still carries `GoogleWorkspaceOpContext`, so a layer is
114
+ // still required to satisfy `runPromise`. `fake.seen` below proves the short-circuit.
115
+ const fake = fakeGoogleWorkspace(() => fakeFailure(500, 'must not be called', 'INTERNAL'));
116
+ const result = await Effect.runPromise(
117
+ groupOps
118
+ .diff(groupProps, undefined)
119
+ .pipe(Effect.provide(fakeGoogleWorkspaceLayer(fake.fetch))),
120
+ );
121
+ expect(result).toBeUndefined();
122
+ expect(fake.seen).toEqual([]);
123
+ });
124
+
125
+ test('matching live → noop, read through the real fake', async () => {
126
+ const fake = fakeGoogleWorkspace((method, url) =>
127
+ method === 'GET' && url.pathname === GROUP_PATH
128
+ ? Response.json({ description: 'ops team', email: groupProps.email, name: 'Ops' })
129
+ : fakeFailure(404, 'not found', 'NOT_FOUND'),
130
+ );
131
+ const result = await Effect.runPromise(
132
+ groupOps.diff(groupProps, live).pipe(Effect.provide(fakeGoogleWorkspaceLayer(fake.fetch))),
133
+ );
134
+ expect(result).toEqual({ action: 'noop' });
135
+ });
136
+
137
+ test('drifted live, spec HAS update → action is update, not replace', async () => {
138
+ const fake = fakeGoogleWorkspace((method, url) =>
139
+ method === 'GET' && url.pathname === GROUP_PATH
140
+ ? Response.json({
141
+ description: 'a different description',
142
+ email: groupProps.email,
143
+ name: 'Ops',
144
+ })
145
+ : fakeFailure(404, 'not found', 'NOT_FOUND'),
146
+ );
147
+ const result = await Effect.runPromise(
148
+ groupOps.diff(groupProps, live).pipe(Effect.provide(fakeGoogleWorkspaceLayer(fake.fetch))),
149
+ );
150
+ expect(result).toEqual({ action: 'update' });
151
+ });
152
+ });
153
+
154
+ describe('googleWorkspaceOperations(domain-alias spec).diff — spec has NO update', () => {
155
+ const aliasOps = googleWorkspaceOperations(aliasSpec);
156
+ const aliasProps = {
157
+ domainAliasName: 'alias.schenanigans.com',
158
+ parentDomainName: 'schenanigans.com',
159
+ };
160
+ const ALIAS_PATH =
161
+ '/admin/directory/v1/customer/my_customer/domainaliases/alias.schenanigans.com';
162
+ const liveAttrs = {
163
+ creationTime: '0',
164
+ customer: 'my_customer',
165
+ domainAliasName: aliasProps.domainAliasName,
166
+ parentDomainName: aliasProps.parentDomainName,
167
+ verified: true,
168
+ };
169
+
170
+ test('drifted live, spec has NO update → replace, never update', async () => {
171
+ const fake = fakeGoogleWorkspace((method, url) =>
172
+ method === 'GET' && url.pathname === ALIAS_PATH
173
+ ? Response.json({
174
+ domainAliasName: aliasProps.domainAliasName,
175
+ parentDomainName: 'homeflare.dev',
176
+ })
177
+ : fakeFailure(404, 'not found', 'NOT_FOUND'),
178
+ );
179
+ const result = await Effect.runPromise(
180
+ aliasOps
181
+ .diff(aliasProps, liveAttrs)
182
+ .pipe(Effect.provide(fakeGoogleWorkspaceLayer(fake.fetch))),
183
+ );
184
+ expect(result).toEqual({ action: 'replace' });
185
+ });
186
+ });