@microagi/alchemy-gcp 0.9.0 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to `@microagi/alchemy-gcp`. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this package
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.10.0 — 2026-07-02
8
+
9
+ ### Added
10
+
11
+ - **`GCP.StorageBucketIamMember`** — standalone resource managing a
12
+ single `(bucket, role, member)` IAM entry on an **existing** GCS
13
+ bucket by raw name (a bucket the stack does not create, adopt, or
14
+ delete). Unlike the `storageBucketIamMember` binding helper — which
15
+ requires a stack-owned `StorageBucket` target and is additive-only —
16
+ this resource **revokes the member on delete** (dropping the binding
17
+ if emptied), so grants disappear when the declaring resource leaves
18
+ the stack. Member-scoped read-modify-write preserves all foreign
19
+ roles/members verbatim; the get→set cycle retries with backoff for
20
+ etag races; a missing bucket fails fast as `ConfigError`. `diff`
21
+ replaces on any change to the triple (persisted `output` checked
22
+ first, `||`-fallback so empty persisted fields are never treated as
23
+ a prior identity); `read` reports member absence as drift so the
24
+ engine re-grants. First consumer: research-infra's collab-reconciler
25
+ per-group bucket read grants.
26
+
27
+ ## 0.9.0 — 2026-06-30
28
+
29
+ ### Added
30
+
31
+ - **`GCP.HelmRelease`** — Alchemy resource for managing Helm chart
32
+ releases on GKE clusters (#8). _(Entry backfilled — 0.9.0 shipped
33
+ without a changelog entry.)_
34
+
7
35
  ## 0.8.0 — 2026-06-17
8
36
 
9
37
  ### Added
@@ -1 +1 @@
1
- {"version":3,"file":"Providers.d.ts","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;;AA+DtC,qBAAa,SAAU,SAAQ,cAA+C;CAAG;AAEjF,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,gaAqEnB,CAAC"}
1
+ {"version":3,"file":"Providers.d.ts","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;;AAmEtC,qBAAa,SAAU,SAAQ,cAA+C;CAAG;AAEjF,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,gaAuEnB,CAAC"}
package/lib/Providers.js CHANGED
@@ -24,6 +24,7 @@ import { Job, JobProvider } from "./Run/Job.js";
24
24
  import { Service, ServiceProvider } from "./Run/Service.js";
25
25
  import { PsaConnection, PsaConnectionProvider, } from "./ServiceNetworking/PsaConnection.js";
26
26
  import { StorageBucket, StorageBucketProvider, } from "./Storage/Bucket.js";
27
+ import { StorageBucketIamMember, StorageBucketIamMemberProvider, } from "./Storage/StorageBucketIamMember.js";
27
28
  import { ApiEnable, ApiEnableProvider } from "./ServiceUsage/ApiEnable.js";
28
29
  import { SqlDatabase, SqlDatabaseProvider } from "./Sqladmin/Database.js";
29
30
  import { SqlInstance, SqlInstanceProvider } from "./Sqladmin/Instance.js";
@@ -61,6 +62,7 @@ export const providers = () => Layer.effect(Providers, Provider.collection([
61
62
  PsaConnection,
62
63
  ManagedLustreInstance,
63
64
  StorageBucket,
65
+ StorageBucketIamMember,
64
66
  Service,
65
67
  Job,
66
68
  SqlInstance,
@@ -72,7 +74,7 @@ export const providers = () => Layer.effect(Providers, Provider.collection([
72
74
  KubernetesSecret,
73
75
  KubernetesManifest,
74
76
  HelmRelease,
75
- ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), ClusterProvider(), NodePoolProvider(), ApiEnableProvider(), NetworkProvider(), SubnetworkProvider(), FirewallProvider(), AddressProvider(), ForwardingRuleProvider(), GlobalAddressProvider(), SharedVpcHostProvider(), SharedVpcServiceProjectProvider(), PsaConnectionProvider(), ManagedLustreInstanceProvider(), StorageBucketProvider(), ServiceProvider(), JobProvider(), SqlInstanceProvider(), SqlDatabaseProvider(), SqlUserProvider(), ArtifactRegistryRepositoryProvider(), ServiceAccountProvider(), ServiceAccountKeyProvider(), KubernetesSecretProvider(), KubernetesManifestProvider(), HelmReleaseProvider())), Layer.provideMerge(fromAuthProvider()), Layer.provideMerge(GCPAuth),
77
+ ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), ClusterProvider(), NodePoolProvider(), ApiEnableProvider(), NetworkProvider(), SubnetworkProvider(), FirewallProvider(), AddressProvider(), ForwardingRuleProvider(), GlobalAddressProvider(), SharedVpcHostProvider(), SharedVpcServiceProjectProvider(), PsaConnectionProvider(), ManagedLustreInstanceProvider(), StorageBucketProvider(), StorageBucketIamMemberProvider(), ServiceProvider(), JobProvider(), SqlInstanceProvider(), SqlDatabaseProvider(), SqlUserProvider(), ArtifactRegistryRepositoryProvider(), ServiceAccountProvider(), ServiceAccountKeyProvider(), KubernetesSecretProvider(), KubernetesManifestProvider(), HelmReleaseProvider())), Layer.provideMerge(fromAuthProvider()), Layer.provideMerge(GCPAuth),
76
78
  // Auth/config failures (missing ADC, malformed profile) become
77
79
  // unrecoverable defects rather than typed errors — mirrors
78
80
  // `AWS/Providers.ts` and `Cloudflare/Providers.ts`. Resource lifecycle
@@ -1 +1 @@
1
- {"version":3,"file":"Providers.js","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Providers.js","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,118 @@
1
+ import { Resource } from "alchemy";
2
+ import * as Provider from "alchemy/Provider";
3
+ import type * as GCP from "../Providers.ts";
4
+ /**
5
+ * A single `(role, member)` IAM binding entry on an **existing** GCS
6
+ * bucket that this stack does not manage. Contrast with
7
+ * {@link storageBucketIamMember}, the target-side binding helper: that
8
+ * one requires a stack-owned {@link StorageBucket} and is additive-only
9
+ * (bindings written into the bucket's binding bag are merged on
10
+ * reconcile but never pruned). This standalone resource takes the raw
11
+ * bucket name, adds the member on reconcile, and — the part the helper
12
+ * cannot do — **removes it again on delete**, so grants revoke when the
13
+ * declaring resource leaves the stack.
14
+ *
15
+ * Identity is the `(bucket, role, member, condition.expression)` tuple;
16
+ * all are immutable — changing any replaces (the old grant is revoked,
17
+ * the new one granted).
18
+ *
19
+ * With a `condition`, the member lands in the binding whose condition
20
+ * **expression** matches (CEL, compared byte-for-byte) — the binding is
21
+ * created with the given title/description if absent. Title and
22
+ * description are create-time metadata only: an existing binding with
23
+ * the same expression but a different title is adopted as-is, never
24
+ * retitled (that lets hand-applied conditional grants converge into the
25
+ * stack without a duplicate binding). Conditional bindings require the
26
+ * bucket to have uniform bucket-level access (GCS rejects conditions
27
+ * otherwise) and policy version 3 — the read-modify-write here always
28
+ * requests/writes version 3.
29
+ *
30
+ * The read-modify-write on the shared bucket policy is member-scoped:
31
+ * exactly one member is added/removed, every foreign role and member is
32
+ * preserved verbatim, and the whole cycle retries with backoff so
33
+ * concurrent writers on the same bucket compose safely (GCS rejects a
34
+ * stale-etag set with `Conflict`/`PreconditionFailed`).
35
+ *
36
+ * There is nothing GCP-side to carry alchemy ownership labels on an IAM
37
+ * binding, so `read` has no `Unowned` adoption gate — presence of the
38
+ * exact member under the exact role IS the resource.
39
+ *
40
+ * @section Granting on an unmanaged bucket
41
+ * @example Read access for a collaborator group's namespace identity
42
+ * ```typescript
43
+ * yield* GCP.StorageBucketIamMember("SrlPackagingRead", {
44
+ * bucket: "srl_200hr_packaging",
45
+ * role: "roles/storage.objectViewer",
46
+ * member:
47
+ * "principal://iam.googleapis.com/projects/864268824536/locations/global/workloadIdentityPools/micro-research-cluster-a.svc.id.goog/subject/ns/collab-srl/sa/default",
48
+ * });
49
+ * ```
50
+ *
51
+ * @section Prefix-scoped read on a customer bucket
52
+ * @example Conditional grant limited to one object prefix (+ bucket-level list)
53
+ * ```typescript
54
+ * yield* GCP.StorageBucketIamMember("QaDatasetRead", {
55
+ * bucket: "golden-zone-prod-770927",
56
+ * role: "roles/storage.objectViewer",
57
+ * member: "principal://iam.googleapis.com/…/subject/ns/collab-jeffrey-coworking/sa/default",
58
+ * condition: {
59
+ * title: "collab-jeffrey-coworking",
60
+ * expression:
61
+ * 'resource.name.startsWith("projects/_/buckets/golden-zone-prod-770927/objects/qa/") || resource.name == "projects/_/buckets/golden-zone-prod-770927"',
62
+ * },
63
+ * });
64
+ * ```
65
+ */
66
+ export type StorageBucketIamMemberCondition = {
67
+ /**
68
+ * Binding title — create-time metadata only. NOT identity: a live
69
+ * binding with the same expression but another title is adopted, not
70
+ * retitled/replaced.
71
+ */
72
+ title: string;
73
+ /** Optional human note on the binding. Create-time metadata only. */
74
+ description?: string;
75
+ /**
76
+ * CEL condition expression. Part of the resource identity — compared
77
+ * byte-for-byte against live bindings' expressions, so keep the
78
+ * rendering deterministic. Immutable — replace.
79
+ */
80
+ expression: string;
81
+ };
82
+ export type StorageBucketIamMemberProps = {
83
+ /**
84
+ * Name of the (pre-existing) GCS bucket to grant on. The bucket is
85
+ * NOT created, adopted, or deleted by this resource. Immutable —
86
+ * replace.
87
+ */
88
+ bucket: string;
89
+ /** IAM role, e.g. `roles/storage.objectViewer`. Immutable — replace. */
90
+ role: string;
91
+ /**
92
+ * IAM member string (`serviceAccount:…`, `principal://…`,
93
+ * `group:…`, …). Immutable — replace.
94
+ */
95
+ member: string;
96
+ /**
97
+ * Optional IAM Condition scoping the grant (e.g. to an object
98
+ * prefix). Absent → the member joins the role's UNCONDITIONAL
99
+ * binding. The condition **expression** is identity (immutable —
100
+ * replace); title/description are create-time metadata. Requires
101
+ * uniform bucket-level access on the bucket.
102
+ */
103
+ condition?: StorageBucketIamMemberCondition;
104
+ };
105
+ export type StorageBucketIamMemberAttributes = {
106
+ /** Bucket name, threaded through from props. */
107
+ bucket: string;
108
+ /** IAM role, threaded through from props. */
109
+ role: string;
110
+ /** IAM member, threaded through from props. */
111
+ member: string;
112
+ /** IAM Condition, threaded through from props (absent = unconditional). */
113
+ condition?: StorageBucketIamMemberCondition;
114
+ };
115
+ export type StorageBucketIamMember = Resource<"GCP.StorageBucketIamMember", StorageBucketIamMemberProps, StorageBucketIamMemberAttributes, never, GCP.Providers>;
116
+ export declare const StorageBucketIamMember: import("alchemy").ResourceClass<StorageBucketIamMember>;
117
+ export declare const StorageBucketIamMemberProvider: () => import("effect/Layer").Layer<Provider.Provider<StorageBucketIamMember>, never, import("@distilled.cloud/gcp").Credentials | import("effect/unstable/http/HttpClient").HttpClient>;
118
+ //# sourceMappingURL=StorageBucketIamMember.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StorageBucketIamMember.d.ts","sourceRoot":"","sources":["../../src/Storage/StorageBucketIamMember.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,MAAM,MAAM,+BAA+B,GAAG;IAC5C;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,+BAA+B,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,SAAS,CAAC,EAAE,+BAA+B,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAC3C,4BAA4B,EAC5B,2BAA2B,EAC3B,gCAAgC,EAChC,KAAK,EACL,GAAG,CAAC,SAAS,CACd,CAAC;AAEF,eAAO,MAAM,sBAAsB,yDAElC,CAAC;AAEF,eAAO,MAAM,8BAA8B,yLA0NxC,CAAC"}
@@ -0,0 +1,176 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as storage from "@distilled.cloud/gcp/storage-v1";
3
+ import { Resource } from "alchemy";
4
+ import { isResolved } from "alchemy/Diff";
5
+ import * as Provider from "alchemy/Provider";
6
+ import * as Duration from "effect/Duration";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Schedule from "effect/Schedule";
9
+ export const StorageBucketIamMember = Resource("GCP.StorageBucketIamMember");
10
+ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucketIamMember, Effect.gen(function* () {
11
+ const getIamPolicyBuckets = yield* storage.getIamPolicyBuckets;
12
+ const setIamPolicyBuckets = yield* storage.setIamPolicyBuckets;
13
+ // Same backoff as StorageBucket's syncIam: the get→set cycle races
14
+ // other writers on the bucket policy (including sibling instances
15
+ // of this resource); a stale etag fails the set, the retry re-reads.
16
+ // ConfigError (bucket doesn't exist) is a terminal misconfiguration
17
+ // — fail fast instead of burning the ~8-minute backoff on it.
18
+ const etagRetry = (eff) => eff.pipe(Effect.retry({
19
+ while: (e) => e?._tag !== "ConfigError",
20
+ schedule: Schedule.exponential(Duration.seconds(2)).pipe(Schedule.both(Schedule.recurs(8))),
21
+ }));
22
+ // Binding selector: with no condition, ONLY the unconditional
23
+ // binding for `role` (conditional bindings are someone else's);
24
+ // with a condition, the binding whose expression matches
25
+ // byte-for-byte — title/description deliberately ignored so
26
+ // hand-applied grants with the same expression are adopted.
27
+ const bindingMatches = (b, role, condition) => b.role === role &&
28
+ (condition
29
+ ? b.condition?.expression === condition.expression
30
+ : !b.condition);
31
+ const hasMember = (policy, role, member, condition) => (policy.bindings ?? []).some((b) => bindingMatches(b, role, condition) &&
32
+ (b.members ?? []).includes(member));
33
+ return {
34
+ // Identity IS the tuple; all fields are static values.
35
+ stables: ["bucket", "role", "member", "condition"],
36
+ diff: Effect.fn(function* ({ news, olds = {}, output }) {
37
+ if (!isResolved(news))
38
+ return undefined;
39
+ // Prior identity from persisted attributes first, then olds —
40
+ // mirrors Bucket.ts (`output?.name || olds.name`). `||`, not
41
+ // `??`: an empty string in a partially-persisted attribute is
42
+ // NOT a prior identity — treating it as one would force a
43
+ // spurious replace whose delete targets the wrong (empty)
44
+ // identity. If only `output` survives (e.g. props were lost),
45
+ // a changed triple must still replace, or the old member is
46
+ // never revoked.
47
+ const priorBucket = output?.bucket || olds.bucket;
48
+ const priorRole = output?.role || olds.role;
49
+ const priorMember = output?.member || olds.member;
50
+ if ((priorBucket && priorBucket !== news.bucket) ||
51
+ (priorRole && priorRole !== news.role) ||
52
+ (priorMember && priorMember !== news.member)) {
53
+ return { action: "replace" };
54
+ }
55
+ // Condition expression is identity too — but only when a prior
56
+ // identity exists at all (a partially-persisted record with no
57
+ // bucket names no grant to revoke). Absent condition compares
58
+ // as "" so unconditional↔conditional flips replace, while a
59
+ // 0.10.x record (no condition attribute) vs. unconditional
60
+ // props stays a no-op.
61
+ const priorCond = output?.condition?.expression ?? olds.condition?.expression ?? "";
62
+ const newsCond = news.condition?.expression ?? "";
63
+ if (priorBucket && priorCond !== newsCond) {
64
+ return { action: "replace" };
65
+ }
66
+ return undefined;
67
+ }),
68
+ reconcile: Effect.fn(function* ({ news }) {
69
+ yield* etagRetry(Effect.gen(function* () {
70
+ const current = yield* getIamPolicyBuckets({
71
+ bucket: news.bucket,
72
+ optionsRequestedPolicyVersion: 3,
73
+ }).pipe(Effect.catchTag("NotFound", () => Effect.fail(new ConfigError({
74
+ message: `Bucket ${news.bucket} does not exist — StorageBucketIamMember grants on pre-existing buckets only.`,
75
+ }))));
76
+ if (hasMember(current, news.role, news.member, news.condition)) {
77
+ return;
78
+ }
79
+ const bindings = (current.bindings ?? []).map((b) => ({
80
+ ...b,
81
+ members: [...(b.members ?? [])],
82
+ }));
83
+ const existing = bindings.find((b) => bindingMatches(b, news.role, news.condition));
84
+ if (existing) {
85
+ existing.members = [...(existing.members ?? []), news.member];
86
+ }
87
+ else {
88
+ bindings.push({
89
+ role: news.role,
90
+ members: [news.member],
91
+ ...(news.condition
92
+ ? {
93
+ condition: {
94
+ title: news.condition.title,
95
+ ...(news.condition.description
96
+ ? { description: news.condition.description }
97
+ : {}),
98
+ expression: news.condition.expression,
99
+ },
100
+ }
101
+ : {}),
102
+ });
103
+ }
104
+ yield* setIamPolicyBuckets({
105
+ bucket: news.bucket,
106
+ body: { ...current, bindings, version: 3 },
107
+ });
108
+ }));
109
+ return {
110
+ bucket: news.bucket,
111
+ role: news.role,
112
+ member: news.member,
113
+ ...(news.condition ? { condition: news.condition } : {}),
114
+ };
115
+ }),
116
+ delete: Effect.fn(function* ({ output }) {
117
+ // Partially-persisted attributes (empty fields) name no real
118
+ // grant — nothing to revoke, and getIamPolicy("") would 4xx.
119
+ if (!output.bucket || !output.role || !output.member)
120
+ return;
121
+ yield* etagRetry(Effect.gen(function* () {
122
+ const current = yield* getIamPolicyBuckets({
123
+ bucket: output.bucket,
124
+ optionsRequestedPolicyVersion: 3,
125
+ }).pipe(
126
+ // Bucket gone (or unreadable because gone) — the grant
127
+ // went with it; idempotent success.
128
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
129
+ if (!current ||
130
+ !hasMember(current, output.role, output.member, output.condition)) {
131
+ return;
132
+ }
133
+ // Remove exactly our member from the matched binding;
134
+ // drop the binding entirely if that empties it (GCS rejects
135
+ // bindings with zero members).
136
+ const bindings = (current.bindings ?? [])
137
+ .map((b) => bindingMatches(b, output.role, output.condition)
138
+ ? {
139
+ ...b,
140
+ members: (b.members ?? []).filter((m) => m !== output.member),
141
+ }
142
+ : { ...b, members: [...(b.members ?? [])] })
143
+ .filter((b) => (b.members?.length ?? 0) > 0);
144
+ yield* setIamPolicyBuckets({
145
+ bucket: output.bucket,
146
+ body: { ...current, bindings, version: 3 },
147
+ }).pipe(Effect.catchTag("NotFound", () => Effect.void));
148
+ }));
149
+ }),
150
+ read: Effect.fn(function* ({ output, olds }) {
151
+ const bucket = output?.bucket ?? olds?.bucket;
152
+ const role = output?.role ?? olds?.role;
153
+ const member = output?.member ?? olds?.member;
154
+ const condition = output?.condition ?? olds?.condition;
155
+ if (!bucket || !role || !member)
156
+ return undefined;
157
+ const current = yield* getIamPolicyBuckets({
158
+ bucket,
159
+ optionsRequestedPolicyVersion: 3,
160
+ }).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)), Effect.catchTag("Forbidden", () => Effect.succeed(undefined)));
161
+ if (!current)
162
+ return undefined;
163
+ // Absent member → undefined: the engine treats that as drift
164
+ // and the next plan re-grants.
165
+ if (!hasMember(current, role, member, condition))
166
+ return undefined;
167
+ return {
168
+ bucket,
169
+ role,
170
+ member,
171
+ ...(condition ? { condition } : {}),
172
+ };
173
+ }),
174
+ };
175
+ }));
176
+ //# sourceMappingURL=StorageBucketIamMember.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StorageBucketIamMember.js","sourceRoot":"","sources":["../../src/Storage/StorageBucketIamMember.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,OAAO,MAAM,iCAAiC,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AA6H5C,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAC5C,4BAA4B,CAC7B,CAAC;AAEF,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,EAAE,CACjD,QAAQ,CAAC,MAAM,CACb,sBAAsB,EACtB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,mBAAmB,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC;IAC/D,MAAM,mBAAmB,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC;IAE/D,mEAAmE;IACnE,kEAAkE;IAClE,qEAAqE;IACrE,oEAAoE;IACpE,8DAA8D;IAC9D,MAAM,SAAS,GAAG,CAAU,GAA2B,EAAE,EAAE,CACzD,GAAG,CAAC,IAAI,CACN,MAAM,CAAC,KAAK,CAAC;QACX,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAE,CAAuB,EAAE,IAAI,KAAK,aAAa;QAC9D,QAAQ,EAAE,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CACtD,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAClC;KACF,CAAC,CACH,CAAC;IAEJ,8DAA8D;IAC9D,gEAAgE;IAChE,yDAAyD;IACzD,4DAA4D;IAC5D,4DAA4D;IAC5D,MAAM,cAAc,GAAG,CACrB,CAAyD,EACzD,IAAY,EACZ,SAA2C,EAC3C,EAAE,CACF,CAAC,CAAC,IAAI,KAAK,IAAI;QACf,CAAC,SAAS;YACR,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,UAAU,KAAK,SAAS,CAAC,UAAU;YAClD,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAEpB,MAAM,SAAS,GAAG,CAChB,MAAsB,EACtB,IAAY,EACZ,MAAc,EACd,SAA2C,EAC3C,EAAE,CACF,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,CAC1B,CAAC,CAAC,EAAE,EAAE,CACJ,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC;QAClC,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CACrC,CAAC;IAEJ,OAAO;QACL,uDAAuD;QACvD,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC;QAClD,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,EAAE,MAAM,EAAE;YACpD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,OAAO,SAAS,CAAC;YACxC,8DAA8D;YAC9D,6DAA6D;YAC7D,8DAA8D;YAC9D,0DAA0D;YAC1D,0DAA0D;YAC1D,8DAA8D;YAC9D,4DAA4D;YAC5D,iBAAiB;YACjB,MAAM,WAAW,GAAG,MAAM,EAAE,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;YAClD,MAAM,SAAS,GAAG,MAAM,EAAE,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC;YAC5C,MAAM,WAAW,GAAG,MAAM,EAAE,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;YAClD,IACE,CAAC,WAAW,IAAI,WAAW,KAAK,IAAI,CAAC,MAAM,CAAC;gBAC5C,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC,IAAI,CAAC;gBACtC,CAAC,WAAW,IAAI,WAAW,KAAK,IAAI,CAAC,MAAM,CAAC,EAC5C,CAAC;gBACD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAW,CAAC;YACxC,CAAC;YACD,+DAA+D;YAC/D,+DAA+D;YAC/D,8DAA8D;YAC9D,4DAA4D;YAC5D,2DAA2D;YAC3D,uBAAuB;YACvB,MAAM,SAAS,GACb,MAAM,EAAE,SAAS,EAAE,UAAU,IAAI,IAAI,CAAC,SAAS,EAAE,UAAU,IAAI,EAAE,CAAC;YACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,IAAI,EAAE,CAAC;YAClD,IAAI,WAAW,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;gBAC1C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAW,CAAC;YACxC,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC,CAAC;QACF,SAAS,EAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE;YACtC,KAAK,CAAC,CAAC,SAAS,CACd,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAClB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,mBAAmB,CAAC;oBACzC,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,6BAA6B,EAAE,CAAC;iBACjC,CAAC,CAAC,IAAI,CACL,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE,CAC/B,MAAM,CAAC,IAAI,CACT,IAAI,WAAW,CAAC;oBACd,OAAO,EAAE,UAAU,IAAI,CAAC,MAAM,+EAA+E;iBAC9G,CAAC,CACH,CACF,CACF,CAAC;gBACF,IAAI,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;oBAC/D,OAAO;gBACT,CAAC;gBAED,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;oBACpD,GAAG,CAAC;oBACJ,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;iBAChC,CAAC,CAAC,CAAC;gBACJ,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CACnC,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAC7C,CAAC;gBACF,IAAI,QAAQ,EAAE,CAAC;oBACb,QAAQ,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;gBAChE,CAAC;qBAAM,CAAC;oBACN,QAAQ,CAAC,IAAI,CAAC;wBACZ,IAAI,EAAE,IAAI,CAAC,IAAI;wBACf,OAAO,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC;wBACtB,GAAG,CAAC,IAAI,CAAC,SAAS;4BAChB,CAAC,CAAC;gCACE,SAAS,EAAE;oCACT,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK;oCAC3B,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW;wCAC5B,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;wCAC7C,CAAC,CAAC,EAAE,CAAC;oCACP,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU;iCACtC;6BACF;4BACH,CAAC,CAAC,EAAE,CAAC;qBACR,CAAC,CAAC;gBACL,CAAC;gBACD,KAAK,CAAC,CAAC,mBAAmB,CAAC;oBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,EAAE;iBAC3C,CAAC,CAAC;YACL,CAAC,CAAC,CACH,CAAC;YACF,OAAO;gBACL,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACzD,CAAC;QACJ,CAAC,CAAC;QACF,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE;YACrC,6DAA6D;YAC7D,6DAA6D;YAC7D,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM;gBAAE,OAAO;YAC7D,KAAK,CAAC,CAAC,SAAS,CACd,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAClB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,mBAAmB,CAAC;oBACzC,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,6BAA6B,EAAE,CAAC;iBACjC,CAAC,CAAC,IAAI;gBACL,uDAAuD;gBACvD,oCAAoC;gBACpC,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE,CAC/B,MAAM,CAAC,OAAO,CAAC,SAAuC,CAAC,CACxD,CACF,CAAC;gBACF,IACE,CAAC,OAAO;oBACR,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,EACjE,CAAC;oBACD,OAAO;gBACT,CAAC;gBACD,sDAAsD;gBACtD,4DAA4D;gBAC5D,+BAA+B;gBAC/B,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;qBACtC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACT,cAAc,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC;oBAC9C,CAAC,CAAC;wBACE,GAAG,CAAC;wBACJ,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAC/B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAC3B;qBACF;oBACH,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,EAAE,CAC9C;qBACA,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC/C,KAAK,CAAC,CAAC,mBAAmB,CAAC;oBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,EAAE;iBAC3C,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAC1D,CAAC,CAAC,CACH,CAAC;QACJ,CAAC,CAAC;QACF,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;YACzC,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,IAAI,EAAE,MAAM,CAAC;YAC9C,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,CAAC;YACxC,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,IAAI,EAAE,MAAM,CAAC;YAC9C,MAAM,SAAS,GAAG,MAAM,EAAE,SAAS,IAAI,IAAI,EAAE,SAAS,CAAC;YACvD,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM;gBAAE,OAAO,SAAS,CAAC;YAClD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,mBAAmB,CAAC;gBACzC,MAAM;gBACN,6BAA6B,EAAE,CAAC;aACjC,CAAC,CAAC,IAAI,CACL,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE,CAC/B,MAAM,CAAC,OAAO,CAAC,SAAuC,CAAC,CACxD,EACD,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE,CAChC,MAAM,CAAC,OAAO,CAAC,SAAuC,CAAC,CACxD,CACF,CAAC;YACF,IAAI,CAAC,OAAO;gBAAE,OAAO,SAAS,CAAC;YAC/B,6DAA6D;YAC7D,+BAA+B;YAC/B,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAC;YACnE,OAAO;gBACL,MAAM;gBACN,IAAI;gBACJ,MAAM;gBACN,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACpC,CAAC;QACJ,CAAC,CAAC;KACH,CAAC;AACJ,CAAC,CAAC,CACH,CAAC"}
@@ -1,4 +1,6 @@
1
1
  export { StorageBucket, StorageBucketProvider, } from "./Bucket.ts";
2
2
  export type { StorageBucketProps, StorageBucketIamBinding, StorageBucketBindingContract, } from "./Bucket.ts";
3
3
  export * from "./BucketIamMember.ts";
4
+ export { StorageBucketIamMember, StorageBucketIamMemberProvider, } from "./StorageBucketIamMember.ts";
5
+ export type { StorageBucketIamMemberProps, StorageBucketIamMemberAttributes, StorageBucketIamMemberCondition, } from "./StorageBucketIamMember.ts";
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AACrB,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AACrB,cAAc,sBAAsB,CAAC;AACrC,OAAO,EACL,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,2BAA2B,EAC3B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,6BAA6B,CAAC"}
@@ -1,3 +1,4 @@
1
1
  export { StorageBucket, StorageBucketProvider, } from "./Bucket.js";
2
2
  export * from "./BucketIamMember.js";
3
+ export { StorageBucketIamMember, StorageBucketIamMemberProvider, } from "./StorageBucketIamMember.js";
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/Storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAMrB,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/Storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAMrB,cAAc,sBAAsB,CAAC;AACrC,OAAO,EACL,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microagi/alchemy-gcp",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "GCP provider for Alchemy v2 (Effect-based IaC). Resources for Project, ServiceAccount, Cluster, NodePool, Compute, ManagedLustre, ServiceUsage, ServiceNetworking, Cloud Run (Service + Job + IAM), Cloud SQL (Instance + Database + User with IAM database authn), Artifact Registry (Docker repositories, standard + remote pull-through caches), Kubernetes (typed Secret + generic SSA Manifest + Helm release), with ADC-backed credentials and target-side IAM bindings.",
5
5
  "keywords": [
6
6
  "alchemy",
package/src/Providers.ts CHANGED
@@ -57,6 +57,10 @@ import {
57
57
  StorageBucket,
58
58
  StorageBucketProvider,
59
59
  } from "./Storage/Bucket.ts";
60
+ import {
61
+ StorageBucketIamMember,
62
+ StorageBucketIamMemberProvider,
63
+ } from "./Storage/StorageBucketIamMember.ts";
60
64
  import { ApiEnable, ApiEnableProvider } from "./ServiceUsage/ApiEnable.ts";
61
65
  import { SqlDatabase, SqlDatabaseProvider } from "./Sqladmin/Database.ts";
62
66
  import { SqlInstance, SqlInstanceProvider } from "./Sqladmin/Instance.ts";
@@ -100,6 +104,7 @@ export const providers = () =>
100
104
  PsaConnection,
101
105
  ManagedLustreInstance,
102
106
  StorageBucket,
107
+ StorageBucketIamMember,
103
108
  Service,
104
109
  Job,
105
110
  SqlInstance,
@@ -130,6 +135,7 @@ export const providers = () =>
130
135
  PsaConnectionProvider(),
131
136
  ManagedLustreInstanceProvider(),
132
137
  StorageBucketProvider(),
138
+ StorageBucketIamMemberProvider(),
133
139
  ServiceProvider(),
134
140
  JobProvider(),
135
141
  SqlInstanceProvider(),
@@ -0,0 +1,355 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as storage from "@distilled.cloud/gcp/storage-v1";
3
+ import { Resource } from "alchemy";
4
+ import { isResolved } from "alchemy/Diff";
5
+ import * as Provider from "alchemy/Provider";
6
+ import * as Duration from "effect/Duration";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Schedule from "effect/Schedule";
9
+ import type * as GCP from "../Providers.ts";
10
+
11
+ /**
12
+ * A single `(role, member)` IAM binding entry on an **existing** GCS
13
+ * bucket that this stack does not manage. Contrast with
14
+ * {@link storageBucketIamMember}, the target-side binding helper: that
15
+ * one requires a stack-owned {@link StorageBucket} and is additive-only
16
+ * (bindings written into the bucket's binding bag are merged on
17
+ * reconcile but never pruned). This standalone resource takes the raw
18
+ * bucket name, adds the member on reconcile, and — the part the helper
19
+ * cannot do — **removes it again on delete**, so grants revoke when the
20
+ * declaring resource leaves the stack.
21
+ *
22
+ * Identity is the `(bucket, role, member, condition.expression)` tuple;
23
+ * all are immutable — changing any replaces (the old grant is revoked,
24
+ * the new one granted).
25
+ *
26
+ * With a `condition`, the member lands in the binding whose condition
27
+ * **expression** matches (CEL, compared byte-for-byte) — the binding is
28
+ * created with the given title/description if absent. Title and
29
+ * description are create-time metadata only: an existing binding with
30
+ * the same expression but a different title is adopted as-is, never
31
+ * retitled (that lets hand-applied conditional grants converge into the
32
+ * stack without a duplicate binding). Conditional bindings require the
33
+ * bucket to have uniform bucket-level access (GCS rejects conditions
34
+ * otherwise) and policy version 3 — the read-modify-write here always
35
+ * requests/writes version 3.
36
+ *
37
+ * The read-modify-write on the shared bucket policy is member-scoped:
38
+ * exactly one member is added/removed, every foreign role and member is
39
+ * preserved verbatim, and the whole cycle retries with backoff so
40
+ * concurrent writers on the same bucket compose safely (GCS rejects a
41
+ * stale-etag set with `Conflict`/`PreconditionFailed`).
42
+ *
43
+ * There is nothing GCP-side to carry alchemy ownership labels on an IAM
44
+ * binding, so `read` has no `Unowned` adoption gate — presence of the
45
+ * exact member under the exact role IS the resource.
46
+ *
47
+ * @section Granting on an unmanaged bucket
48
+ * @example Read access for a collaborator group's namespace identity
49
+ * ```typescript
50
+ * yield* GCP.StorageBucketIamMember("SrlPackagingRead", {
51
+ * bucket: "srl_200hr_packaging",
52
+ * role: "roles/storage.objectViewer",
53
+ * member:
54
+ * "principal://iam.googleapis.com/projects/864268824536/locations/global/workloadIdentityPools/micro-research-cluster-a.svc.id.goog/subject/ns/collab-srl/sa/default",
55
+ * });
56
+ * ```
57
+ *
58
+ * @section Prefix-scoped read on a customer bucket
59
+ * @example Conditional grant limited to one object prefix (+ bucket-level list)
60
+ * ```typescript
61
+ * yield* GCP.StorageBucketIamMember("QaDatasetRead", {
62
+ * bucket: "golden-zone-prod-770927",
63
+ * role: "roles/storage.objectViewer",
64
+ * member: "principal://iam.googleapis.com/…/subject/ns/collab-jeffrey-coworking/sa/default",
65
+ * condition: {
66
+ * title: "collab-jeffrey-coworking",
67
+ * expression:
68
+ * 'resource.name.startsWith("projects/_/buckets/golden-zone-prod-770927/objects/qa/") || resource.name == "projects/_/buckets/golden-zone-prod-770927"',
69
+ * },
70
+ * });
71
+ * ```
72
+ */
73
+ export type StorageBucketIamMemberCondition = {
74
+ /**
75
+ * Binding title — create-time metadata only. NOT identity: a live
76
+ * binding with the same expression but another title is adopted, not
77
+ * retitled/replaced.
78
+ */
79
+ title: string;
80
+ /** Optional human note on the binding. Create-time metadata only. */
81
+ description?: string;
82
+ /**
83
+ * CEL condition expression. Part of the resource identity — compared
84
+ * byte-for-byte against live bindings' expressions, so keep the
85
+ * rendering deterministic. Immutable — replace.
86
+ */
87
+ expression: string;
88
+ };
89
+
90
+ export type StorageBucketIamMemberProps = {
91
+ /**
92
+ * Name of the (pre-existing) GCS bucket to grant on. The bucket is
93
+ * NOT created, adopted, or deleted by this resource. Immutable —
94
+ * replace.
95
+ */
96
+ bucket: string;
97
+ /** IAM role, e.g. `roles/storage.objectViewer`. Immutable — replace. */
98
+ role: string;
99
+ /**
100
+ * IAM member string (`serviceAccount:…`, `principal://…`,
101
+ * `group:…`, …). Immutable — replace.
102
+ */
103
+ member: string;
104
+ /**
105
+ * Optional IAM Condition scoping the grant (e.g. to an object
106
+ * prefix). Absent → the member joins the role's UNCONDITIONAL
107
+ * binding. The condition **expression** is identity (immutable —
108
+ * replace); title/description are create-time metadata. Requires
109
+ * uniform bucket-level access on the bucket.
110
+ */
111
+ condition?: StorageBucketIamMemberCondition;
112
+ };
113
+
114
+ export type StorageBucketIamMemberAttributes = {
115
+ /** Bucket name, threaded through from props. */
116
+ bucket: string;
117
+ /** IAM role, threaded through from props. */
118
+ role: string;
119
+ /** IAM member, threaded through from props. */
120
+ member: string;
121
+ /** IAM Condition, threaded through from props (absent = unconditional). */
122
+ condition?: StorageBucketIamMemberCondition;
123
+ };
124
+
125
+ export type StorageBucketIamMember = Resource<
126
+ "GCP.StorageBucketIamMember",
127
+ StorageBucketIamMemberProps,
128
+ StorageBucketIamMemberAttributes,
129
+ never,
130
+ GCP.Providers
131
+ >;
132
+
133
+ export const StorageBucketIamMember = Resource<StorageBucketIamMember>(
134
+ "GCP.StorageBucketIamMember",
135
+ );
136
+
137
+ export const StorageBucketIamMemberProvider = () =>
138
+ Provider.effect(
139
+ StorageBucketIamMember,
140
+ Effect.gen(function* () {
141
+ const getIamPolicyBuckets = yield* storage.getIamPolicyBuckets;
142
+ const setIamPolicyBuckets = yield* storage.setIamPolicyBuckets;
143
+
144
+ // Same backoff as StorageBucket's syncIam: the get→set cycle races
145
+ // other writers on the bucket policy (including sibling instances
146
+ // of this resource); a stale etag fails the set, the retry re-reads.
147
+ // ConfigError (bucket doesn't exist) is a terminal misconfiguration
148
+ // — fail fast instead of burning the ~8-minute backoff on it.
149
+ const etagRetry = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
150
+ eff.pipe(
151
+ Effect.retry({
152
+ while: (e) => (e as { _tag?: string })?._tag !== "ConfigError",
153
+ schedule: Schedule.exponential(Duration.seconds(2)).pipe(
154
+ Schedule.both(Schedule.recurs(8)),
155
+ ),
156
+ }),
157
+ );
158
+
159
+ // Binding selector: with no condition, ONLY the unconditional
160
+ // binding for `role` (conditional bindings are someone else's);
161
+ // with a condition, the binding whose expression matches
162
+ // byte-for-byte — title/description deliberately ignored so
163
+ // hand-applied grants with the same expression are adopted.
164
+ const bindingMatches = (
165
+ b: { role?: string; condition?: { expression?: string } },
166
+ role: string,
167
+ condition?: StorageBucketIamMemberCondition,
168
+ ) =>
169
+ b.role === role &&
170
+ (condition
171
+ ? b.condition?.expression === condition.expression
172
+ : !b.condition);
173
+
174
+ const hasMember = (
175
+ policy: storage.Policy,
176
+ role: string,
177
+ member: string,
178
+ condition?: StorageBucketIamMemberCondition,
179
+ ) =>
180
+ (policy.bindings ?? []).some(
181
+ (b) =>
182
+ bindingMatches(b, role, condition) &&
183
+ (b.members ?? []).includes(member),
184
+ );
185
+
186
+ return {
187
+ // Identity IS the tuple; all fields are static values.
188
+ stables: ["bucket", "role", "member", "condition"],
189
+ diff: Effect.fn(function* ({ news, olds = {}, output }) {
190
+ if (!isResolved(news)) return undefined;
191
+ // Prior identity from persisted attributes first, then olds —
192
+ // mirrors Bucket.ts (`output?.name || olds.name`). `||`, not
193
+ // `??`: an empty string in a partially-persisted attribute is
194
+ // NOT a prior identity — treating it as one would force a
195
+ // spurious replace whose delete targets the wrong (empty)
196
+ // identity. If only `output` survives (e.g. props were lost),
197
+ // a changed triple must still replace, or the old member is
198
+ // never revoked.
199
+ const priorBucket = output?.bucket || olds.bucket;
200
+ const priorRole = output?.role || olds.role;
201
+ const priorMember = output?.member || olds.member;
202
+ if (
203
+ (priorBucket && priorBucket !== news.bucket) ||
204
+ (priorRole && priorRole !== news.role) ||
205
+ (priorMember && priorMember !== news.member)
206
+ ) {
207
+ return { action: "replace" } as const;
208
+ }
209
+ // Condition expression is identity too — but only when a prior
210
+ // identity exists at all (a partially-persisted record with no
211
+ // bucket names no grant to revoke). Absent condition compares
212
+ // as "" so unconditional↔conditional flips replace, while a
213
+ // 0.10.x record (no condition attribute) vs. unconditional
214
+ // props stays a no-op.
215
+ const priorCond =
216
+ output?.condition?.expression ?? olds.condition?.expression ?? "";
217
+ const newsCond = news.condition?.expression ?? "";
218
+ if (priorBucket && priorCond !== newsCond) {
219
+ return { action: "replace" } as const;
220
+ }
221
+ return undefined;
222
+ }),
223
+ reconcile: Effect.fn(function* ({ news }) {
224
+ yield* etagRetry(
225
+ Effect.gen(function* () {
226
+ const current = yield* getIamPolicyBuckets({
227
+ bucket: news.bucket,
228
+ optionsRequestedPolicyVersion: 3,
229
+ }).pipe(
230
+ Effect.catchTag("NotFound", () =>
231
+ Effect.fail(
232
+ new ConfigError({
233
+ message: `Bucket ${news.bucket} does not exist — StorageBucketIamMember grants on pre-existing buckets only.`,
234
+ }),
235
+ ),
236
+ ),
237
+ );
238
+ if (hasMember(current, news.role, news.member, news.condition)) {
239
+ return;
240
+ }
241
+
242
+ const bindings = (current.bindings ?? []).map((b) => ({
243
+ ...b,
244
+ members: [...(b.members ?? [])],
245
+ }));
246
+ const existing = bindings.find((b) =>
247
+ bindingMatches(b, news.role, news.condition),
248
+ );
249
+ if (existing) {
250
+ existing.members = [...(existing.members ?? []), news.member];
251
+ } else {
252
+ bindings.push({
253
+ role: news.role,
254
+ members: [news.member],
255
+ ...(news.condition
256
+ ? {
257
+ condition: {
258
+ title: news.condition.title,
259
+ ...(news.condition.description
260
+ ? { description: news.condition.description }
261
+ : {}),
262
+ expression: news.condition.expression,
263
+ },
264
+ }
265
+ : {}),
266
+ });
267
+ }
268
+ yield* setIamPolicyBuckets({
269
+ bucket: news.bucket,
270
+ body: { ...current, bindings, version: 3 },
271
+ });
272
+ }),
273
+ );
274
+ return {
275
+ bucket: news.bucket,
276
+ role: news.role,
277
+ member: news.member,
278
+ ...(news.condition ? { condition: news.condition } : {}),
279
+ };
280
+ }),
281
+ delete: Effect.fn(function* ({ output }) {
282
+ // Partially-persisted attributes (empty fields) name no real
283
+ // grant — nothing to revoke, and getIamPolicy("") would 4xx.
284
+ if (!output.bucket || !output.role || !output.member) return;
285
+ yield* etagRetry(
286
+ Effect.gen(function* () {
287
+ const current = yield* getIamPolicyBuckets({
288
+ bucket: output.bucket,
289
+ optionsRequestedPolicyVersion: 3,
290
+ }).pipe(
291
+ // Bucket gone (or unreadable because gone) — the grant
292
+ // went with it; idempotent success.
293
+ Effect.catchTag("NotFound", () =>
294
+ Effect.succeed(undefined as storage.Policy | undefined),
295
+ ),
296
+ );
297
+ if (
298
+ !current ||
299
+ !hasMember(current, output.role, output.member, output.condition)
300
+ ) {
301
+ return;
302
+ }
303
+ // Remove exactly our member from the matched binding;
304
+ // drop the binding entirely if that empties it (GCS rejects
305
+ // bindings with zero members).
306
+ const bindings = (current.bindings ?? [])
307
+ .map((b) =>
308
+ bindingMatches(b, output.role, output.condition)
309
+ ? {
310
+ ...b,
311
+ members: (b.members ?? []).filter(
312
+ (m) => m !== output.member,
313
+ ),
314
+ }
315
+ : { ...b, members: [...(b.members ?? [])] },
316
+ )
317
+ .filter((b) => (b.members?.length ?? 0) > 0);
318
+ yield* setIamPolicyBuckets({
319
+ bucket: output.bucket,
320
+ body: { ...current, bindings, version: 3 },
321
+ }).pipe(Effect.catchTag("NotFound", () => Effect.void));
322
+ }),
323
+ );
324
+ }),
325
+ read: Effect.fn(function* ({ output, olds }) {
326
+ const bucket = output?.bucket ?? olds?.bucket;
327
+ const role = output?.role ?? olds?.role;
328
+ const member = output?.member ?? olds?.member;
329
+ const condition = output?.condition ?? olds?.condition;
330
+ if (!bucket || !role || !member) return undefined;
331
+ const current = yield* getIamPolicyBuckets({
332
+ bucket,
333
+ optionsRequestedPolicyVersion: 3,
334
+ }).pipe(
335
+ Effect.catchTag("NotFound", () =>
336
+ Effect.succeed(undefined as storage.Policy | undefined),
337
+ ),
338
+ Effect.catchTag("Forbidden", () =>
339
+ Effect.succeed(undefined as storage.Policy | undefined),
340
+ ),
341
+ );
342
+ if (!current) return undefined;
343
+ // Absent member → undefined: the engine treats that as drift
344
+ // and the next plan re-grants.
345
+ if (!hasMember(current, role, member, condition)) return undefined;
346
+ return {
347
+ bucket,
348
+ role,
349
+ member,
350
+ ...(condition ? { condition } : {}),
351
+ };
352
+ }),
353
+ };
354
+ }),
355
+ );
@@ -8,3 +8,12 @@ export type {
8
8
  StorageBucketBindingContract,
9
9
  } from "./Bucket.ts";
10
10
  export * from "./BucketIamMember.ts";
11
+ export {
12
+ StorageBucketIamMember,
13
+ StorageBucketIamMemberProvider,
14
+ } from "./StorageBucketIamMember.ts";
15
+ export type {
16
+ StorageBucketIamMemberProps,
17
+ StorageBucketIamMemberAttributes,
18
+ StorageBucketIamMemberCondition,
19
+ } from "./StorageBucketIamMember.ts";