1import * as kms from "@distilled.cloud/gcp/cloudkms_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import type { Providers } from "../Providers.ts";
11import { waitForOperation as waitForGcpOperation } from "../Operation.ts";
12
13const MAX_NAME_LENGTH = 63;
14
15export type SingleTenantHsmAddQuorumMember = {
16 /**
17 * PEM-encoded RSA-2048 public key of the 2FA key for the new quorum
18 * member.
19 */
20 twoFactorPublicKeyPem?: string;
21};
22
23export type SingleTenantHsmRemoveQuorumMember = {
24 /**
25 * PEM-encoded RSA-2048 public key of the 2FA key to remove.
26 */
27 twoFactorPublicKeyPem?: string;
28};
29
30export type SingleTenantHsmRegisterTwoFactorAuthKeys = {
31 /**
32 * PEM-encoded RSA-2048 public keys for M-of-N quorum auth.
33 */
34 twoFactorPublicKeyPems?: string[];
35 /**
36 * Required approver count (M). Must be `>= 2` and
37 * `<= totalApproverCount - 1`.
38 */
39 requiredApproverCount?: number;
40};
41
42export type SingleTenantHsmInstanceProposalProps = {
43 /**
44 * Parent SingleTenantHsmInstance. Full name
45 * `projects/{project}/locations/{location}/singleTenantHsmInstances/{instance}`
46 * or the instance id (combined with `location`). Immutable — changing
47 * it replaces the proposal.
48 */
49 singleTenantHsmInstance: string;
50 /**
51 * Cloud KMS location (`us-central1`, `global`, `us`, …). Used when
52 * `singleTenantHsmInstance` is a bare id. Immutable — changing it
53 * replaces the proposal. `US-CENTRAL1` is accepted and normalized to
54 * `us-central1`.
55 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
56 */
57 location?: string;
58 /**
59 * Proposal id (the last path segment). If omitted, a unique name is
60 * generated from the stack, stage, and logical id. Must match
61 * `[a-zA-Z0-9_-]{1,63}`. Immutable — changing it replaces the
62 * proposal.
63 */
64 proposalId?: string;
65 /**
66 * Time-to-live for the proposal (e.g. `"3600s"`). Input-only.
67 * Proposals expire after this duration. Immutable.
68 */
69 ttl?: string;
70 /**
71 * Refresh the instance so it stays active. Mutually exclusive with
72 * the other operation fields. Immutable.
73 */
74 refreshSingleTenantHsmInstance?: boolean;
75 /**
76 * Enable a DISABLED instance. Mutually exclusive with the other
77 * operation fields. Immutable.
78 */
79 enableSingleTenantHsmInstance?: boolean;
80 /**
81 * Disable an ACTIVE instance. Mutually exclusive with the other
82 * operation fields. Immutable.
83 */
84 disableSingleTenantHsmInstance?: boolean;
85 /**
86 * Delete the instance. Mutually exclusive with the other operation
87 * fields. Immutable.
88 */
89 deleteSingleTenantHsmInstance?: boolean;
90 /**
91 * Add a quorum member. Mutually exclusive with the other operation
92 * fields. Immutable.
93 */
94 addQuorumMember?: SingleTenantHsmAddQuorumMember;
95 /**
96 * Remove a quorum member. Mutually exclusive with the other
97 * operation fields. Immutable.
98 */
99 removeQuorumMember?: SingleTenantHsmRemoveQuorumMember;
100 /**
101 * Register 2FA keys to finish instance setup. Required while the
102 * instance is `PENDING_TWO_FACTOR_AUTH_REGISTRATION`. Mutually
103 * exclusive with the other operation fields. Immutable.
104 */
105 registerTwoFactorAuthKeys?: SingleTenantHsmRegisterTwoFactorAuthKeys;
106};
107
108export type SingleTenantHsmInstanceProposalAttrs = {
109 /** Full resource name `projects/.../proposals/{proposal}`. */
110 name: string;
111 /** Proposal id (last path segment). */
112 proposalId: string;
113 /** Parent SingleTenantHsmInstance resource name. */
114 singleTenantHsmInstance: string;
115 /** Location id (`us-central1`, `global`, …). */
116 location: string;
117 /** Project id. */
118 project: string;
119 /** Current proposal state. */
120 state: string | undefined;
121 /** RFC3339 creation timestamp. */
122 createTime: string | undefined;
123 /** RFC3339 expiration timestamp. */
124 expireTime: string | undefined;
125 /** RFC3339 deletion timestamp, if soft-deleted. */
126 deleteTime: string | undefined;
127 /** RFC3339 purge timestamp, if `state` is `DELETED`. */
128 purgeTime: string | undefined;
129 /** Failure reason, if `state` is `FAILED`. */
130 failureReason: string | undefined;
131 /** Whether this proposal refreshes the instance. */
132 refreshSingleTenantHsmInstance: boolean;
133 /** Whether this proposal enables the instance. */
134 enableSingleTenantHsmInstance: boolean;
135 /** Whether this proposal disables the instance. */
136 disableSingleTenantHsmInstance: boolean;
137 /** Whether this proposal deletes the instance. */
138 deleteSingleTenantHsmInstance: boolean;
139 /** Add-quorum-member payload, if any. */
140 addQuorumMember: SingleTenantHsmAddQuorumMember | undefined;
141 /** Remove-quorum-member payload, if any. */
142 removeQuorumMember: SingleTenantHsmRemoveQuorumMember | undefined;
143 /** Register-2FA payload, if any. */
144 registerTwoFactorAuthKeys:
145 | SingleTenantHsmRegisterTwoFactorAuthKeys
146 | undefined;
147};
148
149export type SingleTenantHsmInstanceProposal = Resource<
150 "GCP.KMS.SingleTenantHsmInstanceProposal",
151 SingleTenantHsmInstanceProposalProps,
152 SingleTenantHsmInstanceProposalAttrs,
153 never,
154 Providers
155>;
156
157/**
158 * A Cloud KMS SingleTenantHsmInstanceProposal — a quorum-gated operation
159 * on a single-tenant HSM instance (refresh, enable, disable, delete,
160 * register 2FA keys, add/remove quorum members).
161 *
162 * Parent instance, location, id, TTL, and the chosen operation are
163 * identity (changing them replaces the proposal). Cloud KMS has no
164 * update API. Proposals have no labels, so `list` returns every proposal
165 * under listed instances. Single-tenant HSM is entitlement-gated.
166 *
167 * ### Creating a Proposal
168 * **Example:** Refresh an instance
169 * ```typescript
170 * const proposal = yield* GCP.KMS.SingleTenantHsmInstanceProposal(
171 * "Refresh",
172 * {
173 * singleTenantHsmInstance: instanceName,
174 * refreshSingleTenantHsmInstance: true,
175 * },
176 * );
177 * ```
178 *
179 * **Example:** Register 2FA keys
180 * ```typescript
181 * const proposal = yield* GCP.KMS.SingleTenantHsmInstanceProposal(
182 * "Register",
183 * {
184 * singleTenantHsmInstance: instanceName,
185 * registerTwoFactorAuthKeys: {
186 * twoFactorPublicKeyPems: [pemA, pemB, pemC],
187 * requiredApproverCount: 2,
188 * },
189 * },
190 * );
191 * ```
192 *
193 * @resource
194 * @category KMS
195 */
196export const SingleTenantHsmInstanceProposal =
197 Resource<SingleTenantHsmInstanceProposal>(
198 "GCP.KMS.SingleTenantHsmInstanceProposal",
199 );
200
201export class SingleTenantHsmInstanceProposalNotResolved extends Data.TaggedError(
202 "GCP.KMS.SingleTenantHsmInstanceProposalNotResolved",
203)<{
204 name: string;
205}> {}
206
207export class SingleTenantHsmInstanceProposalOperationFailed extends Data.TaggedError(
208 "GCP.KMS.SingleTenantHsmInstanceProposalOperationFailed",
209)<{
210 operation: string;
211 message: string;
212}> {}
213
214export class SingleTenantHsmInstanceProposalPending extends Data.TaggedError(
215 "GCP.KMS.SingleTenantHsmInstanceProposalPending",
216)<{
217 name: string;
218 state: string;
219}> {}
220
221const lastSegment = (value: string) => {
222 const trimmed = value.replace(/\/+$/, "");
223 const parts = trimmed.split("/");
224 return parts[parts.length - 1] || trimmed;
225};
226
227const normalizeLocation = (location: string | undefined, fallback: string) =>
228 lastSegment(location ?? fallback).toLowerCase();
229
230const parseName = (name: string, fallbackLocation: string) => {
231 const parts = name.split("/").filter((part) => part.length > 0);
232 const proposalsAt = parts.lastIndexOf("proposals");
233 const instancesAt = parts.lastIndexOf("singleTenantHsmInstances");
234 const locationsAt = parts.lastIndexOf("locations");
235 const projectsAt = parts.lastIndexOf("projects");
236 const instance =
237 instancesAt >= 0 ? parts.slice(0, instancesAt + 2).join("/") : "";
238 return {
239 project:
240 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : "",
241 location:
242 locationsAt >= 0 && parts[locationsAt + 1]
243 ? parts[locationsAt + 1]!
244 : fallbackLocation,
245 singleTenantHsmInstance: instance,
246 proposalId:
247 proposalsAt >= 0 && parts[proposalsAt + 1]
248 ? parts[proposalsAt + 1]!
249 : lastSegment(name),
250 };
251};
252
253const resolveParent = (
254 project: string,
255 instance: string,
256 location: string | undefined,
257 region: string,
258) => {
259 if (instance.includes("/")) {
260 const parsed = parseName(
261 instance.includes("/proposals/") ? instance : `${instance}/proposals/_`,
262 region,
263 );
264 return {
265 parent: parsed.singleTenantHsmInstance,
266 location: parsed.location,
267 project: parsed.project || project,
268 };
269 }
270 const loc = normalizeLocation(location, region);
271 return {
272 parent: `projects/${project}/locations/${loc}/singleTenantHsmInstances/${instance}`,
273 location: loc,
274 project,
275 };
276};
277
278const resourceName = (parent: string, proposalId: string) =>
279 `${parent}/proposals/${proposalId}`;
280
281const toId = (id: string, proposalId: string | undefined, existing?: string) =>
282 Effect.gen(function* () {
283 return (
284 proposalId ??
285 existing ??
286 (yield* createPhysicalName({
287 id,
288 maxLength: MAX_NAME_LENGTH,
289 lowercase: true,
290 }))
291 );
292 });
293
294const present = (value: unknown) => value !== undefined && value !== false;
295
296const toAttrs = (
297 proposal: kms.SingleTenantHsmInstanceProposal,
298 project: string,
299 region: string,
300): SingleTenantHsmInstanceProposalAttrs => {
301 const name = proposal.name ?? "";
302 const parsed = parseName(name, region);
303 return {
304 name,
305 proposalId: parsed.proposalId,
306 singleTenantHsmInstance: parsed.singleTenantHsmInstance,
307 location: parsed.location,
308 project: parsed.project || project,
309 state: proposal.state,
310 createTime: proposal.createTime,
311 expireTime: proposal.expireTime,
312 deleteTime: proposal.deleteTime,
313 purgeTime: proposal.purgeTime,
314 failureReason: proposal.failureReason,
315 refreshSingleTenantHsmInstance: present(
316 proposal.refreshSingleTenantHsmInstance,
317 ),
318 enableSingleTenantHsmInstance: present(
319 proposal.enableSingleTenantHsmInstance,
320 ),
321 disableSingleTenantHsmInstance: present(
322 proposal.disableSingleTenantHsmInstance,
323 ),
324 deleteSingleTenantHsmInstance: present(
325 proposal.deleteSingleTenantHsmInstance,
326 ),
327 addQuorumMember: proposal.addQuorumMember,
328 removeQuorumMember: proposal.removeQuorumMember,
329 registerTwoFactorAuthKeys: proposal.registerTwoFactorAuthKeys,
330 };
331};
332
333const toBody = (
334 news: SingleTenantHsmInstanceProposalProps,
335): kms.SingleTenantHsmInstanceProposal => ({
336 ttl: news.ttl,
337 refreshSingleTenantHsmInstance: news.refreshSingleTenantHsmInstance
338 ? {}
339 : undefined,
340 enableSingleTenantHsmInstance: news.enableSingleTenantHsmInstance
341 ? {}
342 : undefined,
343 disableSingleTenantHsmInstance: news.disableSingleTenantHsmInstance
344 ? {}
345 : undefined,
346 deleteSingleTenantHsmInstance: news.deleteSingleTenantHsmInstance
347 ? {}
348 : undefined,
349 addQuorumMember: news.addQuorumMember,
350 removeQuorumMember: news.removeQuorumMember,
351 registerTwoFactorAuthKeys: news.registerTwoFactorAuthKeys,
352});
353
354const operationKey = (props: {
355 refreshSingleTenantHsmInstance?: boolean;
356 enableSingleTenantHsmInstance?: boolean;
357 disableSingleTenantHsmInstance?: boolean;
358 deleteSingleTenantHsmInstance?: boolean;
359 addQuorumMember?: SingleTenantHsmAddQuorumMember;
360 removeQuorumMember?: SingleTenantHsmRemoveQuorumMember;
361 registerTwoFactorAuthKeys?: SingleTenantHsmRegisterTwoFactorAuthKeys;
362}) => {
363 if (props.refreshSingleTenantHsmInstance) return "refresh";
364 if (props.enableSingleTenantHsmInstance) return "enable";
365 if (props.disableSingleTenantHsmInstance) return "disable";
366 if (props.deleteSingleTenantHsmInstance) return "delete";
367 if (props.addQuorumMember !== undefined) {
368 return `add:${props.addQuorumMember.twoFactorPublicKeyPem ?? ""}`;
369 }
370 if (props.removeQuorumMember !== undefined) {
371 return `remove:${props.removeQuorumMember.twoFactorPublicKeyPem ?? ""}`;
372 }
373 if (props.registerTwoFactorAuthKeys !== undefined) {
374 const keys = (props.registerTwoFactorAuthKeys.twoFactorPublicKeyPems ?? [])
375 .slice()
376 .sort()
377 .join(",");
378 return `register:${props.registerTwoFactorAuthKeys.requiredApproverCount ?? ""}:${keys}`;
379 }
380 return "";
381};
382
383const getByName = (name: string) =>
384 kms
385 .getProjectsLocationsSingleTenantHsmInstancesProposals({ name })
386 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
387
388const paginate = <A, E, R>(
389 fetch: (
390 pageToken: string | undefined,
391 ) => Effect.Effect<{ items: A[]; nextPageToken?: string }, E, R>,
392) =>
393 Effect.gen(function* () {
394 const found: A[] = [];
395 let pageToken: string | undefined;
396 for (let page = 0; page < 10; page++) {
397 const response = yield* fetch(pageToken);
398 found.push(...response.items);
399 pageToken = response.nextPageToken;
400 if (pageToken === undefined || pageToken === "") break;
401 }
402 return found;
403 });
404
405const listInstancesAt = (parent: string) =>
406 paginate((pageToken) =>
407 kms
408 .listProjectsLocationsSingleTenantHsmInstances({
409 parent,
410 pageSize: 1000,
411 pageToken,
412 })
413 .pipe(
414 Effect.map((response) => ({
415 items: response.singleTenantHsmInstances ?? [],
416 nextPageToken: response.nextPageToken,
417 })),
418 Effect.catchTag("NotFound", () =>
419 Effect.succeed({
420 items: [] as kms.SingleTenantHsmInstance[],
421 nextPageToken: undefined,
422 }),
423 ),
424 ),
425 );
426
427const listProposalsInInstance = (parent: string) =>
428 paginate((pageToken) =>
429 kms
430 .listProjectsLocationsSingleTenantHsmInstancesProposals({
431 parent,
432 pageSize: 1000,
433 pageToken,
434 })
435 .pipe(
436 Effect.map((response) => ({
437 items: response.singleTenantHsmInstanceProposals ?? [],
438 nextPageToken: response.nextPageToken,
439 })),
440 Effect.catchTag("NotFound", () =>
441 Effect.succeed({
442 items: [] as kms.SingleTenantHsmInstanceProposal[],
443 nextPageToken: undefined,
444 }),
445 ),
446 ),
447 );
448
449const listProposalsAt = (locationParent: string) =>
450 Effect.gen(function* () {
451 const instances = yield* listInstancesAt(locationParent);
452 const pages = yield* Effect.forEach(
453 instances,
454 (instance) =>
455 instance.name
456 ? listProposalsInInstance(instance.name)
457 : Effect.succeed([] as kms.SingleTenantHsmInstanceProposal[]),
458 { concurrency: 4 },
459 );
460 return pages.flat();
461 });
462
463const waitOperation = (operation: kms.Operation) =>
464 waitForGcpOperation(
465 operation,
466 (name) => kms.getProjectsLocationsOperations({ name }),
467 { budget: "10 minutes" },
468 ).pipe(
469 // Re-read the finished operation for its typed response.
470 Effect.flatMap(() =>
471 operation.name === undefined
472 ? Effect.succeed(operation)
473 : kms.getProjectsLocationsOperations({ name: operation.name }),
474 ),
475 );
476
477const waitReady = (
478 name: string,
479): Effect.Effect<
480 kms.SingleTenantHsmInstanceProposal,
481 | SingleTenantHsmInstanceProposalNotResolved
482 | SingleTenantHsmInstanceProposalPending
483 | kms.GetProjectsLocationsSingleTenantHsmInstancesProposalsError,
484 kms.GcpOpContext
485> => {
486 const probe: Effect.Effect<
487 kms.SingleTenantHsmInstanceProposal,
488 | SingleTenantHsmInstanceProposalNotResolved
489 | SingleTenantHsmInstanceProposalPending
490 | kms.GetProjectsLocationsSingleTenantHsmInstancesProposalsError,
491 kms.GcpOpContext
492 > = getByName(name).pipe(
493 Effect.flatMap(
494 (
495 proposal,
496 ): Effect.Effect<
497 kms.SingleTenantHsmInstanceProposal,
498 | SingleTenantHsmInstanceProposalNotResolved
499 | SingleTenantHsmInstanceProposalPending
500 > => {
501 if (proposal === undefined) {
502 return Effect.fail(
503 new SingleTenantHsmInstanceProposalNotResolved({ name }),
504 );
505 }
506 if (proposal.state === "CREATING") {
507 return Effect.fail(
508 new SingleTenantHsmInstanceProposalPending({
509 name,
510 state: proposal.state,
511 }),
512 );
513 }
514 return Effect.succeed(proposal);
515 },
516 ),
517 );
518 return probe.pipe(
519 Effect.retry({
520 while: (error) =>
521 error._tag === "GCP.KMS.SingleTenantHsmInstanceProposalPending",
522 times: 8,
523 schedule: Schedule.spaced("500 millis"),
524 }),
525 );
526};
527
528const nameFromOperation = (operation: kms.Operation) => {
529 const response = operation.response;
530 const value = response?.name;
531 return typeof value === "string" && value.length > 0 ? value : undefined;
532};
533
534export const SingleTenantHsmInstanceProposalProvider = () =>
535 Provider.succeed(SingleTenantHsmInstanceProposal, {
536 stables: [
537 "name",
538 "proposalId",
539 "singleTenantHsmInstance",
540 "location",
541 "project",
542 "createTime",
543 ],
544
545 diff: Effect.fn(function* ({ news, olds, output }) {
546 if (!isResolved(news)) return undefined;
547 const env = yield* GcpEnvironment.current;
548
549 const previousId = olds?.proposalId ?? output?.proposalId;
550 const nextId = news.proposalId ?? previousId;
551 const idChanged =
552 previousId !== undefined &&
553 nextId !== undefined &&
554 nextId !== previousId;
555
556 const previousParent =
557 output?.singleTenantHsmInstance ??
558 (olds?.singleTenantHsmInstance
559 ? resolveParent(
560 "",
561 olds.singleTenantHsmInstance,
562 olds.location,
563 env.region,
564 ).parent
565 : undefined);
566 const nextParent = resolveParent(
567 output?.project ?? "",
568 news.singleTenantHsmInstance,
569 news.location ?? output?.location,
570 env.region,
571 ).parent;
572 const parentChanged =
573 previousParent !== undefined && previousParent !== nextParent;
574
575 const previousOp = operationKey({
576 refreshSingleTenantHsmInstance:
577 olds?.refreshSingleTenantHsmInstance ??
578 output?.refreshSingleTenantHsmInstance,
579 enableSingleTenantHsmInstance:
580 olds?.enableSingleTenantHsmInstance ??
581 output?.enableSingleTenantHsmInstance,
582 disableSingleTenantHsmInstance:
583 olds?.disableSingleTenantHsmInstance ??
584 output?.disableSingleTenantHsmInstance,
585 deleteSingleTenantHsmInstance:
586 olds?.deleteSingleTenantHsmInstance ??
587 output?.deleteSingleTenantHsmInstance,
588 addQuorumMember: olds?.addQuorumMember ?? output?.addQuorumMember,
589 removeQuorumMember:
590 olds?.removeQuorumMember ?? output?.removeQuorumMember,
591 registerTwoFactorAuthKeys:
592 olds?.registerTwoFactorAuthKeys ?? output?.registerTwoFactorAuthKeys,
593 });
594 const nextOp = operationKey(news);
595 const operationChanged =
596 previousOp.length > 0 && nextOp.length > 0 && previousOp !== nextOp;
597
598 if (!idChanged && !parentChanged && !operationChanged) {
599 return undefined;
600 }
601 return { action: "replace" as const, deleteFirst: false };
602 }),
603
604 read: Effect.fn(function* ({ id, olds, output }) {
605 const env = yield* GcpEnvironment.current;
606 const proposalId = yield* toId(id, olds?.proposalId, output?.proposalId);
607 const name =
608 output?.name ??
609 (olds?.singleTenantHsmInstance || output?.singleTenantHsmInstance
610 ? resourceName(
611 resolveParent(
612 env.project,
613 olds?.singleTenantHsmInstance ??
614 output?.singleTenantHsmInstance ??
615 "",
616 olds?.location ?? output?.location,
617 env.region,
618 ).parent,
619 proposalId,
620 )
621 : undefined);
622 if (name === undefined) return undefined;
623 const existing = yield* getByName(name);
624 if (existing === undefined) return undefined;
625 // Proposals have no labels. Existence at the computed name is
626 // ownership.
627 return toAttrs(existing, env.project, env.region);
628 }),
629
630 list: () =>
631 Effect.gen(function* () {
632 const env = yield* GcpEnvironment.current;
633 const found: SingleTenantHsmInstanceProposalAttrs[] = [];
634 let pageToken: string | undefined;
635 for (let page = 0; page < 10; page++) {
636 const response = yield* kms.listProjectsLocations({
637 name: `projects/${env.project}`,
638 pageSize: 100,
639 pageToken,
640 });
641 const parents = (response.locations ?? [])
642 .map((location) => location.name)
643 .filter((name): name is string => !!name);
644 const batches = yield* Effect.forEach(
645 parents,
646 (parent) => listProposalsAt(parent),
647 { concurrency: 4 },
648 );
649 for (const proposals of batches) {
650 for (const proposal of proposals) {
651 found.push(toAttrs(proposal, env.project, env.region));
652 }
653 }
654 pageToken = response.nextPageToken;
655 if (pageToken === undefined || pageToken === "") break;
656 }
657 return found;
658 }),
659
660 reconcile: Effect.fn(function* ({ id, news, output }) {
661 const env = yield* GcpEnvironment.current;
662 const proposalId = yield* toId(id, news.proposalId, output?.proposalId);
663 const parent = resolveParent(
664 env.project,
665 news.singleTenantHsmInstance,
666 news.location ?? output?.location,
667 env.region,
668 );
669 const name = resourceName(parent.parent, proposalId);
670
671 let current = yield* getByName(name);
672
673 if (current === undefined) {
674 const created = yield* kms
675 .createProjectsLocationsSingleTenantHsmInstancesProposals({
676 parent: parent.parent,
677 singleTenantHsmInstanceProposalId: proposalId,
678 body: toBody(news),
679 })
680 .pipe(
681 Effect.flatMap((operation) =>
682 waitOperation(operation).pipe(
683 Effect.map((done) => nameFromOperation(done) ?? name),
684 ),
685 ),
686 Effect.flatMap((resolved) => getByName(resolved)),
687 Effect.catchTag("Conflict", () => getByName(name)),
688 );
689 current = created ?? undefined;
690 }
691
692 if (current === undefined) {
693 return yield* new SingleTenantHsmInstanceProposalNotResolved({
694 name,
695 });
696 }
697
698 if (current.state === "CREATING") {
699 current = yield* waitReady(current.name ?? name);
700 }
701
702 return toAttrs(current, env.project, env.region);
703 }),
704
705 delete: Effect.fn(function* ({ output }) {
706 yield* kms
707 .deleteProjectsLocationsSingleTenantHsmInstancesProposals({
708 name: output.name,
709 })
710 .pipe(
711 Effect.catchTag("NotFound", () => Effect.void),
712 Effect.catchTag("BadRequest", () => Effect.void),
713 Effect.catchTag("Conflict", () => Effect.void),
714 );
715 }),
716 });