@alchemy.run/daytona 0.0.0-stage → 2.0.0-beta.86
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/LICENSE +201 -0
- package/README.md +80 -2
- package/lib/ApiKey.d.ts +99 -0
- package/lib/ApiKey.d.ts.map +1 -0
- package/lib/ApiKey.js +152 -0
- package/lib/ApiKey.js.map +1 -0
- package/lib/AuthProvider.d.ts +55 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +280 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +28 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +72 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +3 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/ConnectSandboxHttp.d.ts +5 -0
- package/lib/ConnectSandboxHttp.d.ts.map +1 -0
- package/lib/ConnectSandboxHttp.js +22 -0
- package/lib/ConnectSandboxHttp.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateSandbox.d.ts +86 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +3 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/CreateSandboxHttp.d.ts +5 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +65 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CreateSecret.d.ts +56 -0
- package/lib/CreateSecret.d.ts.map +1 -0
- package/lib/CreateSecret.js +3 -0
- package/lib/CreateSecret.js.map +1 -0
- package/lib/CreateSecretHttp.d.ts +5 -0
- package/lib/CreateSecretHttp.d.ts.map +1 -0
- package/lib/CreateSecretHttp.js +39 -0
- package/lib/CreateSecretHttp.js.map +1 -0
- package/lib/CreateSnapshot.d.ts +80 -0
- package/lib/CreateSnapshot.d.ts.map +1 -0
- package/lib/CreateSnapshot.js +3 -0
- package/lib/CreateSnapshot.js.map +1 -0
- package/lib/CreateSnapshotHttp.d.ts +5 -0
- package/lib/CreateSnapshotHttp.d.ts.map +1 -0
- package/lib/CreateSnapshotHttp.js +47 -0
- package/lib/CreateSnapshotHttp.js.map +1 -0
- package/lib/CreateVolume.d.ts +81 -0
- package/lib/CreateVolume.d.ts.map +1 -0
- package/lib/CreateVolume.js +10 -0
- package/lib/CreateVolume.js.map +1 -0
- package/lib/CreateVolumeHttp.d.ts +5 -0
- package/lib/CreateVolumeHttp.d.ts.map +1 -0
- package/lib/CreateVolumeHttp.js +31 -0
- package/lib/CreateVolumeHttp.js.map +1 -0
- package/lib/Credentials.d.ts +9 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +18 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/DaytonaBindings.d.ts +51 -0
- package/lib/DaytonaBindings.d.ts.map +1 -0
- package/lib/DaytonaBindings.js +61 -0
- package/lib/DaytonaBindings.js.map +1 -0
- package/lib/IdentityProvider.d.ts +80 -0
- package/lib/IdentityProvider.d.ts.map +1 -0
- package/lib/IdentityProvider.js +111 -0
- package/lib/IdentityProvider.js.map +1 -0
- package/lib/MountVolume.d.ts +86 -0
- package/lib/MountVolume.d.ts.map +1 -0
- package/lib/MountVolume.js +27 -0
- package/lib/MountVolume.js.map +1 -0
- package/lib/OAuthClient.d.ts +14 -0
- package/lib/OAuthClient.d.ts.map +1 -0
- package/lib/OAuthClient.js +23 -0
- package/lib/OAuthClient.js.map +1 -0
- package/lib/Organization.d.ts +40 -0
- package/lib/Organization.d.ts.map +1 -0
- package/lib/Organization.js +80 -0
- package/lib/Organization.js.map +1 -0
- package/lib/OrganizationInvitation.d.ts +70 -0
- package/lib/OrganizationInvitation.d.ts.map +1 -0
- package/lib/OrganizationInvitation.js +109 -0
- package/lib/OrganizationInvitation.js.map +1 -0
- package/lib/OrganizationMember.d.ts +86 -0
- package/lib/OrganizationMember.d.ts.map +1 -0
- package/lib/OrganizationMember.js +109 -0
- package/lib/OrganizationMember.js.map +1 -0
- package/lib/OrganizationOtelConfig.d.ts +59 -0
- package/lib/OrganizationOtelConfig.d.ts.map +1 -0
- package/lib/OrganizationOtelConfig.js +87 -0
- package/lib/OrganizationOtelConfig.js.map +1 -0
- package/lib/OrganizationRole.d.ts +66 -0
- package/lib/OrganizationRole.d.ts.map +1 -0
- package/lib/OrganizationRole.js +106 -0
- package/lib/OrganizationRole.js.map +1 -0
- package/lib/OrganizationSettings.d.ts +76 -0
- package/lib/OrganizationSettings.d.ts.map +1 -0
- package/lib/OrganizationSettings.js +107 -0
- package/lib/OrganizationSettings.js.map +1 -0
- package/lib/Providers.d.ts +14 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +53 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadAuditLogs.d.ts +30 -0
- package/lib/ReadAuditLogs.d.ts.map +1 -0
- package/lib/ReadAuditLogs.js +17 -0
- package/lib/ReadAuditLogs.js.map +1 -0
- package/lib/ReadOrganization.d.ts +39 -0
- package/lib/ReadOrganization.d.ts.map +1 -0
- package/lib/ReadOrganization.js +39 -0
- package/lib/ReadOrganization.js.map +1 -0
- package/lib/ReadUsage.d.ts +31 -0
- package/lib/ReadUsage.d.ts.map +1 -0
- package/lib/ReadUsage.js +17 -0
- package/lib/ReadUsage.js.map +1 -0
- package/lib/Region.d.ts +78 -0
- package/lib/Region.d.ts.map +1 -0
- package/lib/Region.js +169 -0
- package/lib/Region.js.map +1 -0
- package/lib/Registry.d.ts +84 -0
- package/lib/Registry.d.ts.map +1 -0
- package/lib/Registry.js +97 -0
- package/lib/Registry.js.map +1 -0
- package/lib/Runner.d.ts +66 -0
- package/lib/Runner.d.ts.map +1 -0
- package/lib/Runner.js +116 -0
- package/lib/Runner.js.map +1 -0
- package/lib/Sandbox.d.ts +376 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +565 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +258 -0
- package/lib/SandboxClient.d.ts.map +1 -0
- package/lib/SandboxClient.js +233 -0
- package/lib/SandboxClient.js.map +1 -0
- package/lib/Secret.d.ts +93 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +130 -0
- package/lib/Secret.js.map +1 -0
- package/lib/Snapshot.d.ts +327 -0
- package/lib/Snapshot.d.ts.map +1 -0
- package/lib/Snapshot.js +447 -0
- package/lib/Snapshot.js.map +1 -0
- package/lib/Volume.d.ts +99 -0
- package/lib/Volume.d.ts.map +1 -0
- package/lib/Volume.js +152 -0
- package/lib/Volume.js.map +1 -0
- package/lib/WarmPool.d.ts +61 -0
- package/lib/WarmPool.d.ts.map +1 -0
- package/lib/WarmPool.js +81 -0
- package/lib/WarmPool.js.map +1 -0
- package/lib/Webhooks.d.ts +58 -0
- package/lib/Webhooks.d.ts.map +1 -0
- package/lib/Webhooks.js +75 -0
- package/lib/Webhooks.js.map +1 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +36 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/BuildContext.d.ts +16 -0
- package/lib/internal/BuildContext.d.ts.map +1 -0
- package/lib/internal/BuildContext.js +50 -0
- package/lib/internal/BuildContext.js.map +1 -0
- package/lib/internal/Declared.d.ts +29 -0
- package/lib/internal/Declared.d.ts.map +1 -0
- package/lib/internal/Declared.js +31 -0
- package/lib/internal/Declared.js.map +1 -0
- package/lib/internal/Derived.d.ts +40 -0
- package/lib/internal/Derived.d.ts.map +1 -0
- package/lib/internal/Derived.js +64 -0
- package/lib/internal/Derived.js.map +1 -0
- package/lib/internal/Errors.d.ts +4 -0
- package/lib/internal/Errors.d.ts.map +1 -0
- package/lib/internal/Errors.js +1 -0
- package/lib/internal/Errors.js.map +1 -0
- package/lib/internal/Labels.d.ts +20 -0
- package/lib/internal/Labels.d.ts.map +1 -0
- package/lib/internal/Labels.js +22 -0
- package/lib/internal/Labels.js.map +1 -0
- package/lib/internal/Names.d.ts +18 -0
- package/lib/internal/Names.d.ts.map +1 -0
- package/lib/internal/Names.js +25 -0
- package/lib/internal/Names.js.map +1 -0
- package/lib/internal/Organization.d.ts +27 -0
- package/lib/internal/Organization.d.ts.map +1 -0
- package/lib/internal/Organization.js +32 -0
- package/lib/internal/Organization.js.map +1 -0
- package/lib/internal/Process.d.ts +14 -0
- package/lib/internal/Process.d.ts.map +1 -0
- package/lib/internal/Process.js +15 -0
- package/lib/internal/Process.js.map +1 -0
- package/lib/internal/Program.d.ts +83 -0
- package/lib/internal/Program.d.ts.map +1 -0
- package/lib/internal/Program.js +169 -0
- package/lib/internal/Program.js.map +1 -0
- package/lib/internal/Ref.d.ts +14 -0
- package/lib/internal/Ref.d.ts.map +1 -0
- package/lib/internal/Ref.js +7 -0
- package/lib/internal/Ref.js.map +1 -0
- package/lib/internal/Scoped.d.ts +31 -0
- package/lib/internal/Scoped.d.ts.map +1 -0
- package/lib/internal/Scoped.js +48 -0
- package/lib/internal/Scoped.js.map +1 -0
- package/lib/internal/Tar.d.ts +13 -0
- package/lib/internal/Tar.d.ts.map +1 -0
- package/lib/internal/Tar.js +62 -0
- package/lib/internal/Tar.js.map +1 -0
- package/lib/internal/Toolbox.d.ts +11 -0
- package/lib/internal/Toolbox.d.ts.map +1 -0
- package/lib/internal/Toolbox.js +20 -0
- package/lib/internal/Toolbox.js.map +1 -0
- package/lib/internal/Wait.d.ts +49 -0
- package/lib/internal/Wait.d.ts.map +1 -0
- package/lib/internal/Wait.js +57 -0
- package/lib/internal/Wait.js.map +1 -0
- package/package.json +79 -3
- package/src/ApiKey.ts +221 -0
- package/src/AuthProvider.ts +406 -0
- package/src/Bootstrap.ts +55 -0
- package/src/ConnectSandbox.ts +76 -0
- package/src/ConnectSandboxHttp.ts +26 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateSandbox.ts +107 -0
- package/src/CreateSandboxHttp.ts +97 -0
- package/src/CreateSecret.ts +73 -0
- package/src/CreateSecretHttp.ts +58 -0
- package/src/CreateSnapshot.ts +133 -0
- package/src/CreateSnapshotHttp.ts +77 -0
- package/src/CreateVolume.ts +104 -0
- package/src/CreateVolumeHttp.ts +50 -0
- package/src/Credentials.ts +37 -0
- package/src/DaytonaBindings.ts +111 -0
- package/src/IdentityProvider.ts +181 -0
- package/src/MountVolume.ts +122 -0
- package/src/OAuthClient.ts +27 -0
- package/src/Organization.ts +121 -0
- package/src/OrganizationInvitation.ts +174 -0
- package/src/OrganizationMember.ts +174 -0
- package/src/OrganizationOtelConfig.ts +138 -0
- package/src/OrganizationRole.ts +167 -0
- package/src/OrganizationSettings.ts +187 -0
- package/src/Providers.ts +93 -0
- package/src/ReadAuditLogs.ts +50 -0
- package/src/ReadOrganization.ts +83 -0
- package/src/ReadUsage.ts +51 -0
- package/src/Region.ts +226 -0
- package/src/Registry.ts +164 -0
- package/src/Runner.ts +166 -0
- package/src/Sandbox.ts +927 -0
- package/src/SandboxClient.ts +625 -0
- package/src/Secret.ts +203 -0
- package/src/Snapshot.ts +685 -0
- package/src/Volume.ts +229 -0
- package/src/WarmPool.ts +128 -0
- package/src/Webhooks.ts +121 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +46 -0
- package/src/internal/BuildContext.ts +62 -0
- package/src/internal/Declared.ts +80 -0
- package/src/internal/Derived.ts +97 -0
- package/src/internal/Errors.ts +4 -0
- package/src/internal/Labels.ts +39 -0
- package/src/internal/Names.ts +37 -0
- package/src/internal/Organization.ts +40 -0
- package/src/internal/Process.ts +25 -0
- package/src/internal/Program.ts +239 -0
- package/src/internal/Ref.ts +15 -0
- package/src/internal/Scoped.ts +102 -0
- package/src/internal/Tar.ts +72 -0
- package/src/internal/Toolbox.ts +27 -0
- package/src/internal/Wait.ts +105 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as organizations from "@distilled.cloud/daytona/organizations";
|
|
2
|
+
import { deepEqual, isResolved } from "alchemy/Diff";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import { declarable } from "./internal/Declared.ts";
|
|
7
|
+
import {
|
|
8
|
+
currentOrganizationId,
|
|
9
|
+
movesOrganization,
|
|
10
|
+
organizationIdFor,
|
|
11
|
+
type OrganizationRef,
|
|
12
|
+
} from "./internal/Organization.ts";
|
|
13
|
+
import { resolved, type Ref } from "./internal/Ref.ts";
|
|
14
|
+
import type { Providers } from "./Providers.ts";
|
|
15
|
+
|
|
16
|
+
/** A role, by id. Accepts a `Daytona.OrganizationRole` or `{ roleId }`. */
|
|
17
|
+
export type RoleRef = { readonly roleId: string };
|
|
18
|
+
|
|
19
|
+
/** `owner` manages the organization; `member` gets only the assigned roles. */
|
|
20
|
+
export type MemberRole = "owner" | "member";
|
|
21
|
+
|
|
22
|
+
export interface OrganizationInvitationProps {
|
|
23
|
+
/**
|
|
24
|
+
* Organization to act on, e.g. a `Daytona.Organization`. Defaults to the
|
|
25
|
+
* credentials' organization. Changing it replaces the resource.
|
|
26
|
+
*/
|
|
27
|
+
organization?: Ref<OrganizationRef>;
|
|
28
|
+
/** Email to invite. Changing it replaces the invitation. */
|
|
29
|
+
email: string;
|
|
30
|
+
/** Base role. @default "member" */
|
|
31
|
+
role?: MemberRole;
|
|
32
|
+
/** Custom roles to assign. Updated in place. */
|
|
33
|
+
roles?: Ref<RoleRef>[];
|
|
34
|
+
/** When the invitation expires, as an ISO 8601 timestamp. */
|
|
35
|
+
expiresAt?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface OrganizationInvitationAttributes {
|
|
39
|
+
/** Invitation ID. */
|
|
40
|
+
invitationId: string;
|
|
41
|
+
/** Organization ID. */
|
|
42
|
+
organizationId: string;
|
|
43
|
+
/** Invited email. */
|
|
44
|
+
email: string;
|
|
45
|
+
/** `pending`, `accepted`, `declined`, `cancelled`. */
|
|
46
|
+
status: string;
|
|
47
|
+
/** When it expires. */
|
|
48
|
+
expiresAt: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type OrganizationInvitation = Resource<
|
|
52
|
+
"Daytona.OrganizationInvitation",
|
|
53
|
+
OrganizationInvitationProps,
|
|
54
|
+
OrganizationInvitationAttributes,
|
|
55
|
+
never,
|
|
56
|
+
Providers
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Invite someone to the Daytona organization by email, with a base role
|
|
61
|
+
* and custom {@link OrganizationRole}s. Destroying it cancels the
|
|
62
|
+
* invitation if it hasn't been accepted; an accepted invitation leaves the
|
|
63
|
+
* member in place.
|
|
64
|
+
*
|
|
65
|
+
* ### Inviting a Developer
|
|
66
|
+
* **Example:** A member with a custom role
|
|
67
|
+
* ```typescript
|
|
68
|
+
* export const Alice = Daytona.OrganizationInvitation("Alice", {
|
|
69
|
+
* email: "alice@example.com",
|
|
70
|
+
* roles: [Developer],
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @resource
|
|
75
|
+
* @product Organization
|
|
76
|
+
*/
|
|
77
|
+
export const OrganizationInvitation = declarable<OrganizationInvitation>()(
|
|
78
|
+
Resource<OrganizationInvitation>("Daytona.OrganizationInvitation"),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const resolveRoles = (roles: OrganizationInvitationProps["roles"]) =>
|
|
82
|
+
Effect.succeed((roles ?? []).map((r) => resolved(r).roleId));
|
|
83
|
+
|
|
84
|
+
const toAttributes = (
|
|
85
|
+
i: organizations.OrganizationInvitation,
|
|
86
|
+
): OrganizationInvitationAttributes => ({
|
|
87
|
+
invitationId: i.id,
|
|
88
|
+
organizationId: i.organizationId,
|
|
89
|
+
email: i.email,
|
|
90
|
+
status: i.status,
|
|
91
|
+
expiresAt: i.expiresAt,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const OrganizationInvitationProvider = () =>
|
|
95
|
+
Provider.effect(
|
|
96
|
+
OrganizationInvitation,
|
|
97
|
+
Effect.succeed({
|
|
98
|
+
stables: ["invitationId", "organizationId"],
|
|
99
|
+
list: Effect.fn(function* () {
|
|
100
|
+
const organizationId = yield* currentOrganizationId;
|
|
101
|
+
return (yield* organizations.listOrganizationInvitations({ organizationId })).map(
|
|
102
|
+
toAttributes,
|
|
103
|
+
);
|
|
104
|
+
}),
|
|
105
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
106
|
+
if (!isResolved(news) || output === undefined) return undefined;
|
|
107
|
+
if (yield* movesOrganization(news.organization, output))
|
|
108
|
+
return { action: "replace" } as const;
|
|
109
|
+
if (news.email !== output.email) return { action: "replace" } as const;
|
|
110
|
+
}),
|
|
111
|
+
read: Effect.fn(function* ({ output }) {
|
|
112
|
+
if (output === undefined) return undefined;
|
|
113
|
+
const all = yield* organizations.listOrganizationInvitations({
|
|
114
|
+
organizationId: output.organizationId,
|
|
115
|
+
});
|
|
116
|
+
const found = all.find((i) => i.id === output.invitationId);
|
|
117
|
+
return found ? toAttributes(found) : undefined;
|
|
118
|
+
}),
|
|
119
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
120
|
+
const organizationId = yield* organizationIdFor(news.organization);
|
|
121
|
+
const assignedRoleIds = yield* resolveRoles(news.roles);
|
|
122
|
+
const role = news.role ?? "member";
|
|
123
|
+
// Observe the pending invitation for this email.
|
|
124
|
+
const all = yield* organizations.listOrganizationInvitations({ organizationId });
|
|
125
|
+
const current =
|
|
126
|
+
all.find((i) => i.id === output?.invitationId) ??
|
|
127
|
+
all.find((i) => i.email === news.email && i.status === "pending");
|
|
128
|
+
// Ensure
|
|
129
|
+
if (current === undefined || current.status !== "pending") {
|
|
130
|
+
if (current?.status === "accepted") return toAttributes(current);
|
|
131
|
+
return toAttributes(
|
|
132
|
+
yield* organizations.createOrganizationInvitation({
|
|
133
|
+
organizationId,
|
|
134
|
+
email: news.email,
|
|
135
|
+
role,
|
|
136
|
+
assignedRoleIds,
|
|
137
|
+
expiresAt: news.expiresAt,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
// Sync
|
|
142
|
+
const observedRoles = current.assignedRoles.map((r) => r.id).sort();
|
|
143
|
+
if (
|
|
144
|
+
current.role === role &&
|
|
145
|
+
deepEqual(observedRoles, [...assignedRoleIds].sort()) &&
|
|
146
|
+
(news.expiresAt === undefined || news.expiresAt === current.expiresAt)
|
|
147
|
+
) {
|
|
148
|
+
return toAttributes(current);
|
|
149
|
+
}
|
|
150
|
+
return toAttributes(
|
|
151
|
+
yield* organizations.updateOrganizationInvitation({
|
|
152
|
+
organizationId,
|
|
153
|
+
invitationId: current.id,
|
|
154
|
+
role,
|
|
155
|
+
assignedRoleIds,
|
|
156
|
+
expiresAt: news.expiresAt,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}),
|
|
160
|
+
delete: Effect.fn(function* ({ output }) {
|
|
161
|
+
const all = yield* organizations.listOrganizationInvitations({
|
|
162
|
+
organizationId: output.organizationId,
|
|
163
|
+
});
|
|
164
|
+
const found = all.find((i) => i.id === output.invitationId);
|
|
165
|
+
if (found?.status !== "pending") return;
|
|
166
|
+
yield* organizations
|
|
167
|
+
.cancelOrganizationInvitation({
|
|
168
|
+
organizationId: output.organizationId,
|
|
169
|
+
invitationId: output.invitationId,
|
|
170
|
+
})
|
|
171
|
+
.pipe(Effect.catchTag("OrganizationInvitationNotFound", () => Effect.void));
|
|
172
|
+
}),
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as organizations from "@distilled.cloud/daytona/organizations";
|
|
2
|
+
import { deepEqual, isResolved } from "alchemy/Diff";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import { declarable } from "./internal/Declared.ts";
|
|
8
|
+
import {
|
|
9
|
+
movesOrganization,
|
|
10
|
+
organizationIdFor,
|
|
11
|
+
type OrganizationRef,
|
|
12
|
+
} from "./internal/Organization.ts";
|
|
13
|
+
import { resolved, type Ref } from "./internal/Ref.ts";
|
|
14
|
+
import type { MemberRole, RoleRef } from "./OrganizationInvitation.ts";
|
|
15
|
+
import type { Providers } from "./Providers.ts";
|
|
16
|
+
|
|
17
|
+
export interface OrganizationMemberProps {
|
|
18
|
+
/**
|
|
19
|
+
* Organization to act on, e.g. a `Daytona.Organization`. Defaults to the
|
|
20
|
+
* credentials' organization. Changing it replaces the resource.
|
|
21
|
+
*/
|
|
22
|
+
organization?: Ref<OrganizationRef>;
|
|
23
|
+
/** The member, by user ID or email. Changing it replaces the resource. */
|
|
24
|
+
user: string;
|
|
25
|
+
/** Base role. */
|
|
26
|
+
role: MemberRole;
|
|
27
|
+
/** Custom roles. Updated in place. */
|
|
28
|
+
roles?: Ref<RoleRef>[];
|
|
29
|
+
/**
|
|
30
|
+
* Remove the member from the organization on destroy. By default destroy
|
|
31
|
+
* only puts back the access the member had before alchemy changed it.
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
removeOnDestroy?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OrganizationMemberAttributes {
|
|
38
|
+
/** Organization ID. */
|
|
39
|
+
organizationId: string;
|
|
40
|
+
/** User ID. */
|
|
41
|
+
userId: string;
|
|
42
|
+
/** Email. */
|
|
43
|
+
email: string;
|
|
44
|
+
/** Base role. */
|
|
45
|
+
role: string;
|
|
46
|
+
/** Assigned custom role IDs. */
|
|
47
|
+
roleIds: string[];
|
|
48
|
+
/** Access before alchemy first changed it; restored on destroy. */
|
|
49
|
+
original: { role: string; roleIds: string[] };
|
|
50
|
+
/** Whether destroy removes the member. */
|
|
51
|
+
removeOnDestroy: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type OrganizationMember = Resource<
|
|
55
|
+
"Daytona.OrganizationMember",
|
|
56
|
+
OrganizationMemberProps,
|
|
57
|
+
OrganizationMemberAttributes,
|
|
58
|
+
never,
|
|
59
|
+
Providers
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
/** No member of the organization has that user ID or email. */
|
|
63
|
+
export class MemberNotInOrganization extends Data.TaggedError("Daytona.MemberNotInOrganization")<{
|
|
64
|
+
readonly user: string;
|
|
65
|
+
}> {
|
|
66
|
+
override get message() {
|
|
67
|
+
return `${this.user} is not a member of the Daytona organization; invite them with Daytona.OrganizationInvitation`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The access of an existing member of the Daytona organization: their
|
|
73
|
+
* base role and custom {@link OrganizationRole}s. Use it to manage who can
|
|
74
|
+
* do what as code. To add someone, use {@link OrganizationInvitation}.
|
|
75
|
+
*
|
|
76
|
+
* ### Granting a Role
|
|
77
|
+
* **Example:** Make a member a developer
|
|
78
|
+
* ```typescript
|
|
79
|
+
* export const Bob = Daytona.OrganizationMember("Bob", {
|
|
80
|
+
* user: "bob@example.com",
|
|
81
|
+
* role: "member",
|
|
82
|
+
* roles: [Developer],
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @resource
|
|
87
|
+
* @product Organization
|
|
88
|
+
*/
|
|
89
|
+
export const OrganizationMember = declarable<OrganizationMember>()(
|
|
90
|
+
Resource<OrganizationMember>("Daytona.OrganizationMember"),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const findMember = Effect.fn(function* (organizationId: string, user: string) {
|
|
94
|
+
const members = yield* organizations.listOrganizationMembers({ organizationId });
|
|
95
|
+
return members.find((m) => m.userId === user || m.email === user);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const toAttributes = (
|
|
99
|
+
m: organizations.OrganizationUser,
|
|
100
|
+
original: OrganizationMemberAttributes["original"],
|
|
101
|
+
removeOnDestroy: boolean,
|
|
102
|
+
): OrganizationMemberAttributes => ({
|
|
103
|
+
organizationId: m.organizationId,
|
|
104
|
+
userId: m.userId,
|
|
105
|
+
email: m.email,
|
|
106
|
+
role: m.role,
|
|
107
|
+
roleIds: m.assignedRoles.map((r) => r.id).sort(),
|
|
108
|
+
original,
|
|
109
|
+
removeOnDestroy,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const OrganizationMemberProvider = () =>
|
|
113
|
+
Provider.effect(
|
|
114
|
+
OrganizationMember,
|
|
115
|
+
Effect.succeed({
|
|
116
|
+
stables: ["organizationId", "userId", "original"],
|
|
117
|
+
nuke: { skip: true },
|
|
118
|
+
list: () => Effect.succeed([]),
|
|
119
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
120
|
+
if (!isResolved(news) || output === undefined) return undefined;
|
|
121
|
+
if (yield* movesOrganization(news.organization, output))
|
|
122
|
+
return { action: "replace" } as const;
|
|
123
|
+
if (news.user !== output.userId && news.user !== output.email)
|
|
124
|
+
return { action: "replace" } as const;
|
|
125
|
+
}),
|
|
126
|
+
read: Effect.fn(function* ({ output }) {
|
|
127
|
+
if (output === undefined) return undefined;
|
|
128
|
+
const found = yield* findMember(output.organizationId, output.userId);
|
|
129
|
+
return found ? toAttributes(found, output.original, output.removeOnDestroy) : undefined;
|
|
130
|
+
}),
|
|
131
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
132
|
+
const organizationId = yield* organizationIdFor(news.organization);
|
|
133
|
+
const member = yield* findMember(organizationId, output?.userId ?? news.user);
|
|
134
|
+
if (member === undefined) return yield* new MemberNotInOrganization({ user: news.user });
|
|
135
|
+
const original = output?.original ?? {
|
|
136
|
+
role: member.role,
|
|
137
|
+
roleIds: member.assignedRoles.map((r) => r.id).sort(),
|
|
138
|
+
};
|
|
139
|
+
const roleIds = (news.roles ?? []).map((r) => resolved(r).roleId).sort();
|
|
140
|
+
if (
|
|
141
|
+
member.role !== news.role ||
|
|
142
|
+
!deepEqual(member.assignedRoles.map((r) => r.id).sort(), roleIds)
|
|
143
|
+
) {
|
|
144
|
+
yield* organizations.updateAccessForOrganizationMember({
|
|
145
|
+
organizationId,
|
|
146
|
+
userId: member.userId,
|
|
147
|
+
role: news.role,
|
|
148
|
+
assignedRoleIds: roleIds,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const updated = yield* findMember(organizationId, member.userId);
|
|
152
|
+
return toAttributes(updated ?? member, original, news.removeOnDestroy ?? false);
|
|
153
|
+
}),
|
|
154
|
+
delete: Effect.fn(function* ({ output }) {
|
|
155
|
+
if (output.removeOnDestroy) {
|
|
156
|
+
yield* organizations
|
|
157
|
+
.deleteOrganizationMember({
|
|
158
|
+
organizationId: output.organizationId,
|
|
159
|
+
userId: output.userId,
|
|
160
|
+
})
|
|
161
|
+
.pipe(Effect.catchTag("OrganizationMemberNotFound", () => Effect.void));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const member = yield* findMember(output.organizationId, output.userId);
|
|
165
|
+
if (member === undefined) return;
|
|
166
|
+
yield* organizations.updateAccessForOrganizationMember({
|
|
167
|
+
organizationId: output.organizationId,
|
|
168
|
+
userId: output.userId,
|
|
169
|
+
role: output.original.role,
|
|
170
|
+
assignedRoleIds: output.original.roleIds,
|
|
171
|
+
});
|
|
172
|
+
}),
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as organizations from "@distilled.cloud/daytona/organizations";
|
|
2
|
+
import { deepEqual, isResolved } from "alchemy/Diff";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Redacted from "effect/Redacted";
|
|
7
|
+
import { declarable } from "./internal/Declared.ts";
|
|
8
|
+
import {
|
|
9
|
+
currentOrganizationId,
|
|
10
|
+
movesOrganization,
|
|
11
|
+
organizationIdFor,
|
|
12
|
+
type OrganizationRef,
|
|
13
|
+
} from "./internal/Organization.ts";
|
|
14
|
+
import type { Ref } from "./internal/Ref.ts";
|
|
15
|
+
import type { Providers } from "./Providers.ts";
|
|
16
|
+
|
|
17
|
+
export interface OrganizationOtelConfigProps {
|
|
18
|
+
/**
|
|
19
|
+
* Organization to act on, e.g. a `Daytona.Organization`. Defaults to the
|
|
20
|
+
* credentials' organization. Changing it replaces the resource.
|
|
21
|
+
*/
|
|
22
|
+
organization?: Ref<OrganizationRef>;
|
|
23
|
+
/** OTLP/HTTP endpoint sandboxes export telemetry to. */
|
|
24
|
+
endpoint: string;
|
|
25
|
+
/** Headers sent with every export, e.g. an API key. */
|
|
26
|
+
headers?: Record<string, string | Redacted.Redacted<string>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OrganizationOtelConfigAttributes {
|
|
30
|
+
/** Organization ID. */
|
|
31
|
+
organizationId: string;
|
|
32
|
+
/** OTLP endpoint. */
|
|
33
|
+
endpoint: string;
|
|
34
|
+
/** Header names configured (values are not returned). */
|
|
35
|
+
headerNames: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type OrganizationOtelConfig = Resource<
|
|
39
|
+
"Daytona.OrganizationOtelConfig",
|
|
40
|
+
OrganizationOtelConfigProps,
|
|
41
|
+
OrganizationOtelConfigAttributes,
|
|
42
|
+
never,
|
|
43
|
+
Providers
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Export the organization's sandbox telemetry (logs, traces, metrics) to
|
|
48
|
+
* your own OpenTelemetry backend. One per organization; destroying it
|
|
49
|
+
* stops the export.
|
|
50
|
+
*
|
|
51
|
+
* ### Exporting Telemetry
|
|
52
|
+
* **Example:** Send sandbox telemetry to Axiom
|
|
53
|
+
* ```typescript
|
|
54
|
+
* export const Telemetry = Daytona.OrganizationOtelConfig("Telemetry", {
|
|
55
|
+
* endpoint: "https://api.axiom.co",
|
|
56
|
+
* headers: {
|
|
57
|
+
* Authorization: Config.Redacted("AXIOM_TOKEN"),
|
|
58
|
+
* "X-Axiom-Dataset": "daytona",
|
|
59
|
+
* },
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @resource
|
|
64
|
+
* @product Organization
|
|
65
|
+
*/
|
|
66
|
+
export const OrganizationOtelConfig = declarable<OrganizationOtelConfig>()(
|
|
67
|
+
Resource<OrganizationOtelConfig>("Daytona.OrganizationOtelConfig"),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const plainHeaders = (headers: OrganizationOtelConfigProps["headers"]) =>
|
|
71
|
+
headers === undefined
|
|
72
|
+
? undefined
|
|
73
|
+
: Object.fromEntries(
|
|
74
|
+
Object.entries(headers).map(([k, v]) => [
|
|
75
|
+
k,
|
|
76
|
+
Redacted.isRedacted(v) ? Redacted.value(v) : v,
|
|
77
|
+
]),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Daytona stores the endpoint normalized (`https://host` -> `https://host/`).
|
|
81
|
+
const sameEndpoint = (observed: string | undefined, desired: string) =>
|
|
82
|
+
observed !== undefined && URL.canParse(observed) && URL.canParse(desired)
|
|
83
|
+
? new URL(observed).href === new URL(desired).href
|
|
84
|
+
: observed === desired;
|
|
85
|
+
|
|
86
|
+
// getOrganizationOtelConfig is for Daytona's runners and proxy (users get
|
|
87
|
+
// 401); the organization itself carries the config.
|
|
88
|
+
const observeOtelConfig = (organizationId: string) =>
|
|
89
|
+
Effect.map(organizations.getOrganization({ organizationId }), (o) => o.otelConfig);
|
|
90
|
+
|
|
91
|
+
export const OrganizationOtelConfigProvider = () =>
|
|
92
|
+
Provider.effect(
|
|
93
|
+
OrganizationOtelConfig,
|
|
94
|
+
Effect.succeed({
|
|
95
|
+
stables: ["organizationId"],
|
|
96
|
+
nuke: { singleton: true },
|
|
97
|
+
list: () => Effect.succeed([]),
|
|
98
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
99
|
+
if (!isResolved(news) || output === undefined) return undefined;
|
|
100
|
+
if (yield* movesOrganization(news.organization, output))
|
|
101
|
+
return { action: "replace" } as const;
|
|
102
|
+
}),
|
|
103
|
+
read: Effect.fn(function* ({ output }) {
|
|
104
|
+
const organizationId = output?.organizationId ?? (yield* currentOrganizationId);
|
|
105
|
+
const config = yield* observeOtelConfig(organizationId);
|
|
106
|
+
if (!config?.endpoint) return undefined;
|
|
107
|
+
return {
|
|
108
|
+
organizationId,
|
|
109
|
+
endpoint: config.endpoint,
|
|
110
|
+
headerNames: Object.keys(config.headers ?? {}),
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
reconcile: Effect.fn(function* ({ news, olds }) {
|
|
114
|
+
const organizationId = yield* organizationIdFor(news.organization);
|
|
115
|
+
// Observe — header values aren't returned, so they are written
|
|
116
|
+
// whenever they changed since the last deploy (or on first deploy).
|
|
117
|
+
const current = yield* observeOtelConfig(organizationId);
|
|
118
|
+
const headers = plainHeaders(news.headers);
|
|
119
|
+
if (
|
|
120
|
+
!sameEndpoint(current?.endpoint, news.endpoint) ||
|
|
121
|
+
olds === undefined ||
|
|
122
|
+
!deepEqual(plainHeaders(olds.headers), headers)
|
|
123
|
+
) {
|
|
124
|
+
yield* organizations.updateOrganizationOtelConfig({
|
|
125
|
+
organizationId,
|
|
126
|
+
endpoint: news.endpoint,
|
|
127
|
+
headers,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { organizationId, endpoint: news.endpoint, headerNames: Object.keys(headers ?? {}) };
|
|
131
|
+
}),
|
|
132
|
+
delete: Effect.fn(function* ({ output }) {
|
|
133
|
+
yield* organizations
|
|
134
|
+
.deleteOrganizationOtelConfig({ organizationId: output.organizationId })
|
|
135
|
+
.pipe(Effect.catchTag("OrganizationNotFound", () => Effect.void));
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as organizations from "@distilled.cloud/daytona/organizations";
|
|
2
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import { deepEqual, isResolved } from "alchemy/Diff";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { Resource } from "alchemy/Resource";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import type { Permission } from "./ApiKey.ts";
|
|
8
|
+
import { declarable } from "./internal/Declared.ts";
|
|
9
|
+
import { isOwnedName, resolveName } from "./internal/Names.ts";
|
|
10
|
+
import {
|
|
11
|
+
currentOrganizationId,
|
|
12
|
+
movesOrganization,
|
|
13
|
+
organizationIdFor,
|
|
14
|
+
type OrganizationRef,
|
|
15
|
+
} from "./internal/Organization.ts";
|
|
16
|
+
import type { Ref } from "./internal/Ref.ts";
|
|
17
|
+
import type { Providers } from "./Providers.ts";
|
|
18
|
+
|
|
19
|
+
export interface OrganizationRoleProps {
|
|
20
|
+
/**
|
|
21
|
+
* Organization to act on, e.g. a `Daytona.Organization`. Defaults to the
|
|
22
|
+
* credentials' organization. Changing it replaces the resource.
|
|
23
|
+
*/
|
|
24
|
+
organization?: Ref<OrganizationRef>;
|
|
25
|
+
/**
|
|
26
|
+
* Role name. If omitted, a unique name is generated from the stack,
|
|
27
|
+
* stage and logical ID. Renames update in place.
|
|
28
|
+
*/
|
|
29
|
+
name?: string;
|
|
30
|
+
/** What the role is for. */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Permissions members with this role get. Updated in place. */
|
|
33
|
+
permissions: Permission[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OrganizationRoleAttributes {
|
|
37
|
+
/** Role ID; what invitations and member access take. */
|
|
38
|
+
roleId: string;
|
|
39
|
+
/** Organization the role belongs to. */
|
|
40
|
+
organizationId: string;
|
|
41
|
+
/** Role name. */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Description. */
|
|
44
|
+
description: string;
|
|
45
|
+
/** Permissions the role grants. */
|
|
46
|
+
permissions: Permission[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type OrganizationRole = Resource<
|
|
50
|
+
"Daytona.OrganizationRole",
|
|
51
|
+
OrganizationRoleProps,
|
|
52
|
+
OrganizationRoleAttributes,
|
|
53
|
+
never,
|
|
54
|
+
Providers
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A custom role in the Daytona organization: a named set of permissions to
|
|
59
|
+
* give members, through {@link OrganizationInvitation} or
|
|
60
|
+
* {@link OrganizationMember}.
|
|
61
|
+
* @see https://www.daytona.io/docs/en/organizations/
|
|
62
|
+
*
|
|
63
|
+
* ### Creating a Role
|
|
64
|
+
* **Example:** A role that can run sandboxes but not manage images
|
|
65
|
+
* ```typescript
|
|
66
|
+
* export const Developer = Daytona.OrganizationRole("Developer", {
|
|
67
|
+
* description: "Run sandboxes",
|
|
68
|
+
* permissions: ["write:sandboxes", "delete:sandboxes", "read:volumes"],
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @resource
|
|
73
|
+
* @product Organization
|
|
74
|
+
*/
|
|
75
|
+
export const OrganizationRole = declarable<OrganizationRole>()(
|
|
76
|
+
Resource<OrganizationRole>("Daytona.OrganizationRole"),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const toAttributes = (
|
|
80
|
+
organizationId: string,
|
|
81
|
+
r: organizations.OrganizationRole,
|
|
82
|
+
): OrganizationRoleAttributes => ({
|
|
83
|
+
roleId: r.id,
|
|
84
|
+
organizationId,
|
|
85
|
+
name: r.name,
|
|
86
|
+
description: r.description,
|
|
87
|
+
permissions: r.permissions,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const listRoles = (organizationId: string) =>
|
|
91
|
+
organizations.listOrganizationRoles({ organizationId });
|
|
92
|
+
|
|
93
|
+
const sorted = (permissions: readonly string[]) => [...permissions].sort();
|
|
94
|
+
|
|
95
|
+
export const OrganizationRoleProvider = () =>
|
|
96
|
+
Provider.effect(
|
|
97
|
+
OrganizationRole,
|
|
98
|
+
Effect.succeed({
|
|
99
|
+
stables: ["roleId", "organizationId"],
|
|
100
|
+
list: Effect.fn(function* () {
|
|
101
|
+
const organizationId = yield* currentOrganizationId;
|
|
102
|
+
const roles = yield* listRoles(organizationId);
|
|
103
|
+
return roles.filter((r) => !r.isGlobal).map((r) => toAttributes(organizationId, r));
|
|
104
|
+
}),
|
|
105
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
106
|
+
if (!isResolved(news) || output === undefined) return undefined;
|
|
107
|
+
if (yield* movesOrganization(news.organization, output))
|
|
108
|
+
return { action: "replace" } as const;
|
|
109
|
+
}),
|
|
110
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
111
|
+
const organizationId = output?.organizationId ?? (yield* currentOrganizationId);
|
|
112
|
+
const name = yield* resolveName(id, olds?.name, output?.name);
|
|
113
|
+
const roles = yield* listRoles(organizationId);
|
|
114
|
+
const found =
|
|
115
|
+
roles.find((r) => r.id === output?.roleId) ?? roles.find((r) => r.name === name);
|
|
116
|
+
if (found === undefined) return undefined;
|
|
117
|
+
const attrs = toAttributes(organizationId, found);
|
|
118
|
+
return (yield* isOwnedName(id, found.name, output !== undefined)) ? attrs : Unowned(attrs);
|
|
119
|
+
}),
|
|
120
|
+
reconcile: Effect.fn(function* ({ id, news, output }) {
|
|
121
|
+
const organizationId = yield* organizationIdFor(news.organization);
|
|
122
|
+
const name = yield* resolveName(id, news.name, output?.name);
|
|
123
|
+
const description = news.description ?? "";
|
|
124
|
+
// Observe
|
|
125
|
+
const roles = yield* listRoles(organizationId);
|
|
126
|
+
const current =
|
|
127
|
+
roles.find((r) => r.id === output?.roleId) ?? roles.find((r) => r.name === name);
|
|
128
|
+
// Ensure
|
|
129
|
+
if (current === undefined) {
|
|
130
|
+
const created = yield* organizations
|
|
131
|
+
.createOrganizationRole({
|
|
132
|
+
organizationId,
|
|
133
|
+
name,
|
|
134
|
+
description,
|
|
135
|
+
permissions: news.permissions,
|
|
136
|
+
})
|
|
137
|
+
.pipe(Effect.catchTag("OrganizationRoleAlreadyExists", () => Effect.undefined));
|
|
138
|
+
if (created) return toAttributes(organizationId, created);
|
|
139
|
+
}
|
|
140
|
+
const existing = current ?? (yield* listRoles(organizationId)).find((r) => r.name === name);
|
|
141
|
+
if (existing === undefined) {
|
|
142
|
+
return yield* Effect.die(`Daytona role ${name} vanished during create`);
|
|
143
|
+
}
|
|
144
|
+
// Sync against observed state.
|
|
145
|
+
if (
|
|
146
|
+
existing.name === name &&
|
|
147
|
+
existing.description === description &&
|
|
148
|
+
deepEqual(sorted(existing.permissions), sorted(news.permissions))
|
|
149
|
+
) {
|
|
150
|
+
return toAttributes(organizationId, existing);
|
|
151
|
+
}
|
|
152
|
+
const updated = yield* organizations.updateOrganizationRole({
|
|
153
|
+
organizationId,
|
|
154
|
+
roleId: existing.id,
|
|
155
|
+
name,
|
|
156
|
+
description,
|
|
157
|
+
permissions: news.permissions,
|
|
158
|
+
});
|
|
159
|
+
return toAttributes(organizationId, updated);
|
|
160
|
+
}),
|
|
161
|
+
delete: Effect.fn(function* ({ output }) {
|
|
162
|
+
yield* organizations
|
|
163
|
+
.deleteOrganizationRole({ organizationId: output.organizationId, roleId: output.roleId })
|
|
164
|
+
.pipe(Effect.catchTag("OrganizationRoleNotFound", () => Effect.void));
|
|
165
|
+
}),
|
|
166
|
+
}),
|
|
167
|
+
);
|