@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.
Files changed (274) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +80 -2
  3. package/lib/ApiKey.d.ts +99 -0
  4. package/lib/ApiKey.d.ts.map +1 -0
  5. package/lib/ApiKey.js +152 -0
  6. package/lib/ApiKey.js.map +1 -0
  7. package/lib/AuthProvider.d.ts +55 -0
  8. package/lib/AuthProvider.d.ts.map +1 -0
  9. package/lib/AuthProvider.js +280 -0
  10. package/lib/AuthProvider.js.map +1 -0
  11. package/lib/Bootstrap.d.ts +8 -0
  12. package/lib/Bootstrap.d.ts.map +1 -0
  13. package/lib/Bootstrap.js +28 -0
  14. package/lib/Bootstrap.js.map +1 -0
  15. package/lib/ConnectSandbox.d.ts +72 -0
  16. package/lib/ConnectSandbox.d.ts.map +1 -0
  17. package/lib/ConnectSandbox.js +3 -0
  18. package/lib/ConnectSandbox.js.map +1 -0
  19. package/lib/ConnectSandboxHttp.d.ts +5 -0
  20. package/lib/ConnectSandboxHttp.d.ts.map +1 -0
  21. package/lib/ConnectSandboxHttp.js +22 -0
  22. package/lib/ConnectSandboxHttp.js.map +1 -0
  23. package/lib/Contributions.d.ts +7 -0
  24. package/lib/Contributions.d.ts.map +1 -0
  25. package/lib/Contributions.js +6 -0
  26. package/lib/Contributions.js.map +1 -0
  27. package/lib/CreateSandbox.d.ts +86 -0
  28. package/lib/CreateSandbox.d.ts.map +1 -0
  29. package/lib/CreateSandbox.js +3 -0
  30. package/lib/CreateSandbox.js.map +1 -0
  31. package/lib/CreateSandboxHttp.d.ts +5 -0
  32. package/lib/CreateSandboxHttp.d.ts.map +1 -0
  33. package/lib/CreateSandboxHttp.js +65 -0
  34. package/lib/CreateSandboxHttp.js.map +1 -0
  35. package/lib/CreateSecret.d.ts +56 -0
  36. package/lib/CreateSecret.d.ts.map +1 -0
  37. package/lib/CreateSecret.js +3 -0
  38. package/lib/CreateSecret.js.map +1 -0
  39. package/lib/CreateSecretHttp.d.ts +5 -0
  40. package/lib/CreateSecretHttp.d.ts.map +1 -0
  41. package/lib/CreateSecretHttp.js +39 -0
  42. package/lib/CreateSecretHttp.js.map +1 -0
  43. package/lib/CreateSnapshot.d.ts +80 -0
  44. package/lib/CreateSnapshot.d.ts.map +1 -0
  45. package/lib/CreateSnapshot.js +3 -0
  46. package/lib/CreateSnapshot.js.map +1 -0
  47. package/lib/CreateSnapshotHttp.d.ts +5 -0
  48. package/lib/CreateSnapshotHttp.d.ts.map +1 -0
  49. package/lib/CreateSnapshotHttp.js +47 -0
  50. package/lib/CreateSnapshotHttp.js.map +1 -0
  51. package/lib/CreateVolume.d.ts +81 -0
  52. package/lib/CreateVolume.d.ts.map +1 -0
  53. package/lib/CreateVolume.js +10 -0
  54. package/lib/CreateVolume.js.map +1 -0
  55. package/lib/CreateVolumeHttp.d.ts +5 -0
  56. package/lib/CreateVolumeHttp.d.ts.map +1 -0
  57. package/lib/CreateVolumeHttp.js +31 -0
  58. package/lib/CreateVolumeHttp.js.map +1 -0
  59. package/lib/Credentials.d.ts +9 -0
  60. package/lib/Credentials.d.ts.map +1 -0
  61. package/lib/Credentials.js +18 -0
  62. package/lib/Credentials.js.map +1 -0
  63. package/lib/DaytonaBindings.d.ts +51 -0
  64. package/lib/DaytonaBindings.d.ts.map +1 -0
  65. package/lib/DaytonaBindings.js +61 -0
  66. package/lib/DaytonaBindings.js.map +1 -0
  67. package/lib/IdentityProvider.d.ts +80 -0
  68. package/lib/IdentityProvider.d.ts.map +1 -0
  69. package/lib/IdentityProvider.js +111 -0
  70. package/lib/IdentityProvider.js.map +1 -0
  71. package/lib/MountVolume.d.ts +86 -0
  72. package/lib/MountVolume.d.ts.map +1 -0
  73. package/lib/MountVolume.js +27 -0
  74. package/lib/MountVolume.js.map +1 -0
  75. package/lib/OAuthClient.d.ts +14 -0
  76. package/lib/OAuthClient.d.ts.map +1 -0
  77. package/lib/OAuthClient.js +23 -0
  78. package/lib/OAuthClient.js.map +1 -0
  79. package/lib/Organization.d.ts +40 -0
  80. package/lib/Organization.d.ts.map +1 -0
  81. package/lib/Organization.js +80 -0
  82. package/lib/Organization.js.map +1 -0
  83. package/lib/OrganizationInvitation.d.ts +70 -0
  84. package/lib/OrganizationInvitation.d.ts.map +1 -0
  85. package/lib/OrganizationInvitation.js +109 -0
  86. package/lib/OrganizationInvitation.js.map +1 -0
  87. package/lib/OrganizationMember.d.ts +86 -0
  88. package/lib/OrganizationMember.d.ts.map +1 -0
  89. package/lib/OrganizationMember.js +109 -0
  90. package/lib/OrganizationMember.js.map +1 -0
  91. package/lib/OrganizationOtelConfig.d.ts +59 -0
  92. package/lib/OrganizationOtelConfig.d.ts.map +1 -0
  93. package/lib/OrganizationOtelConfig.js +87 -0
  94. package/lib/OrganizationOtelConfig.js.map +1 -0
  95. package/lib/OrganizationRole.d.ts +66 -0
  96. package/lib/OrganizationRole.d.ts.map +1 -0
  97. package/lib/OrganizationRole.js +106 -0
  98. package/lib/OrganizationRole.js.map +1 -0
  99. package/lib/OrganizationSettings.d.ts +76 -0
  100. package/lib/OrganizationSettings.d.ts.map +1 -0
  101. package/lib/OrganizationSettings.js +107 -0
  102. package/lib/OrganizationSettings.js.map +1 -0
  103. package/lib/Providers.d.ts +14 -0
  104. package/lib/Providers.d.ts.map +1 -0
  105. package/lib/Providers.js +53 -0
  106. package/lib/Providers.js.map +1 -0
  107. package/lib/ReadAuditLogs.d.ts +30 -0
  108. package/lib/ReadAuditLogs.d.ts.map +1 -0
  109. package/lib/ReadAuditLogs.js +17 -0
  110. package/lib/ReadAuditLogs.js.map +1 -0
  111. package/lib/ReadOrganization.d.ts +39 -0
  112. package/lib/ReadOrganization.d.ts.map +1 -0
  113. package/lib/ReadOrganization.js +39 -0
  114. package/lib/ReadOrganization.js.map +1 -0
  115. package/lib/ReadUsage.d.ts +31 -0
  116. package/lib/ReadUsage.d.ts.map +1 -0
  117. package/lib/ReadUsage.js +17 -0
  118. package/lib/ReadUsage.js.map +1 -0
  119. package/lib/Region.d.ts +78 -0
  120. package/lib/Region.d.ts.map +1 -0
  121. package/lib/Region.js +169 -0
  122. package/lib/Region.js.map +1 -0
  123. package/lib/Registry.d.ts +84 -0
  124. package/lib/Registry.d.ts.map +1 -0
  125. package/lib/Registry.js +97 -0
  126. package/lib/Registry.js.map +1 -0
  127. package/lib/Runner.d.ts +66 -0
  128. package/lib/Runner.d.ts.map +1 -0
  129. package/lib/Runner.js +116 -0
  130. package/lib/Runner.js.map +1 -0
  131. package/lib/Sandbox.d.ts +376 -0
  132. package/lib/Sandbox.d.ts.map +1 -0
  133. package/lib/Sandbox.js +565 -0
  134. package/lib/Sandbox.js.map +1 -0
  135. package/lib/SandboxClient.d.ts +258 -0
  136. package/lib/SandboxClient.d.ts.map +1 -0
  137. package/lib/SandboxClient.js +233 -0
  138. package/lib/SandboxClient.js.map +1 -0
  139. package/lib/Secret.d.ts +93 -0
  140. package/lib/Secret.d.ts.map +1 -0
  141. package/lib/Secret.js +130 -0
  142. package/lib/Secret.js.map +1 -0
  143. package/lib/Snapshot.d.ts +327 -0
  144. package/lib/Snapshot.d.ts.map +1 -0
  145. package/lib/Snapshot.js +447 -0
  146. package/lib/Snapshot.js.map +1 -0
  147. package/lib/Volume.d.ts +99 -0
  148. package/lib/Volume.d.ts.map +1 -0
  149. package/lib/Volume.js +152 -0
  150. package/lib/Volume.js.map +1 -0
  151. package/lib/WarmPool.d.ts +61 -0
  152. package/lib/WarmPool.d.ts.map +1 -0
  153. package/lib/WarmPool.js +81 -0
  154. package/lib/WarmPool.js.map +1 -0
  155. package/lib/Webhooks.d.ts +58 -0
  156. package/lib/Webhooks.d.ts.map +1 -0
  157. package/lib/Webhooks.js +75 -0
  158. package/lib/Webhooks.js.map +1 -0
  159. package/lib/index.d.ts +37 -0
  160. package/lib/index.d.ts.map +1 -0
  161. package/lib/index.js +36 -0
  162. package/lib/index.js.map +1 -0
  163. package/lib/internal/BuildContext.d.ts +16 -0
  164. package/lib/internal/BuildContext.d.ts.map +1 -0
  165. package/lib/internal/BuildContext.js +50 -0
  166. package/lib/internal/BuildContext.js.map +1 -0
  167. package/lib/internal/Declared.d.ts +29 -0
  168. package/lib/internal/Declared.d.ts.map +1 -0
  169. package/lib/internal/Declared.js +31 -0
  170. package/lib/internal/Declared.js.map +1 -0
  171. package/lib/internal/Derived.d.ts +40 -0
  172. package/lib/internal/Derived.d.ts.map +1 -0
  173. package/lib/internal/Derived.js +64 -0
  174. package/lib/internal/Derived.js.map +1 -0
  175. package/lib/internal/Errors.d.ts +4 -0
  176. package/lib/internal/Errors.d.ts.map +1 -0
  177. package/lib/internal/Errors.js +1 -0
  178. package/lib/internal/Errors.js.map +1 -0
  179. package/lib/internal/Labels.d.ts +20 -0
  180. package/lib/internal/Labels.d.ts.map +1 -0
  181. package/lib/internal/Labels.js +22 -0
  182. package/lib/internal/Labels.js.map +1 -0
  183. package/lib/internal/Names.d.ts +18 -0
  184. package/lib/internal/Names.d.ts.map +1 -0
  185. package/lib/internal/Names.js +25 -0
  186. package/lib/internal/Names.js.map +1 -0
  187. package/lib/internal/Organization.d.ts +27 -0
  188. package/lib/internal/Organization.d.ts.map +1 -0
  189. package/lib/internal/Organization.js +32 -0
  190. package/lib/internal/Organization.js.map +1 -0
  191. package/lib/internal/Process.d.ts +14 -0
  192. package/lib/internal/Process.d.ts.map +1 -0
  193. package/lib/internal/Process.js +15 -0
  194. package/lib/internal/Process.js.map +1 -0
  195. package/lib/internal/Program.d.ts +83 -0
  196. package/lib/internal/Program.d.ts.map +1 -0
  197. package/lib/internal/Program.js +169 -0
  198. package/lib/internal/Program.js.map +1 -0
  199. package/lib/internal/Ref.d.ts +14 -0
  200. package/lib/internal/Ref.d.ts.map +1 -0
  201. package/lib/internal/Ref.js +7 -0
  202. package/lib/internal/Ref.js.map +1 -0
  203. package/lib/internal/Scoped.d.ts +31 -0
  204. package/lib/internal/Scoped.d.ts.map +1 -0
  205. package/lib/internal/Scoped.js +48 -0
  206. package/lib/internal/Scoped.js.map +1 -0
  207. package/lib/internal/Tar.d.ts +13 -0
  208. package/lib/internal/Tar.d.ts.map +1 -0
  209. package/lib/internal/Tar.js +62 -0
  210. package/lib/internal/Tar.js.map +1 -0
  211. package/lib/internal/Toolbox.d.ts +11 -0
  212. package/lib/internal/Toolbox.d.ts.map +1 -0
  213. package/lib/internal/Toolbox.js +20 -0
  214. package/lib/internal/Toolbox.js.map +1 -0
  215. package/lib/internal/Wait.d.ts +49 -0
  216. package/lib/internal/Wait.d.ts.map +1 -0
  217. package/lib/internal/Wait.js +57 -0
  218. package/lib/internal/Wait.js.map +1 -0
  219. package/package.json +79 -3
  220. package/src/ApiKey.ts +221 -0
  221. package/src/AuthProvider.ts +406 -0
  222. package/src/Bootstrap.ts +55 -0
  223. package/src/ConnectSandbox.ts +76 -0
  224. package/src/ConnectSandboxHttp.ts +26 -0
  225. package/src/Contributions.ts +9 -0
  226. package/src/CreateSandbox.ts +107 -0
  227. package/src/CreateSandboxHttp.ts +97 -0
  228. package/src/CreateSecret.ts +73 -0
  229. package/src/CreateSecretHttp.ts +58 -0
  230. package/src/CreateSnapshot.ts +133 -0
  231. package/src/CreateSnapshotHttp.ts +77 -0
  232. package/src/CreateVolume.ts +104 -0
  233. package/src/CreateVolumeHttp.ts +50 -0
  234. package/src/Credentials.ts +37 -0
  235. package/src/DaytonaBindings.ts +111 -0
  236. package/src/IdentityProvider.ts +181 -0
  237. package/src/MountVolume.ts +122 -0
  238. package/src/OAuthClient.ts +27 -0
  239. package/src/Organization.ts +121 -0
  240. package/src/OrganizationInvitation.ts +174 -0
  241. package/src/OrganizationMember.ts +174 -0
  242. package/src/OrganizationOtelConfig.ts +138 -0
  243. package/src/OrganizationRole.ts +167 -0
  244. package/src/OrganizationSettings.ts +187 -0
  245. package/src/Providers.ts +93 -0
  246. package/src/ReadAuditLogs.ts +50 -0
  247. package/src/ReadOrganization.ts +83 -0
  248. package/src/ReadUsage.ts +51 -0
  249. package/src/Region.ts +226 -0
  250. package/src/Registry.ts +164 -0
  251. package/src/Runner.ts +166 -0
  252. package/src/Sandbox.ts +927 -0
  253. package/src/SandboxClient.ts +625 -0
  254. package/src/Secret.ts +203 -0
  255. package/src/Snapshot.ts +685 -0
  256. package/src/Volume.ts +229 -0
  257. package/src/WarmPool.ts +128 -0
  258. package/src/Webhooks.ts +121 -0
  259. package/src/globals.d.ts +2 -0
  260. package/src/index.ts +46 -0
  261. package/src/internal/BuildContext.ts +62 -0
  262. package/src/internal/Declared.ts +80 -0
  263. package/src/internal/Derived.ts +97 -0
  264. package/src/internal/Errors.ts +4 -0
  265. package/src/internal/Labels.ts +39 -0
  266. package/src/internal/Names.ts +37 -0
  267. package/src/internal/Organization.ts +40 -0
  268. package/src/internal/Process.ts +25 -0
  269. package/src/internal/Program.ts +239 -0
  270. package/src/internal/Ref.ts +15 -0
  271. package/src/internal/Scoped.ts +102 -0
  272. package/src/internal/Tar.ts +72 -0
  273. package/src/internal/Toolbox.ts +27 -0
  274. 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
+ );