@alchemy.run/boat 0.0.0 → 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 (149) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +66 -4
  3. package/lib/AiProxy.d.ts +89 -0
  4. package/lib/AiProxy.d.ts.map +1 -0
  5. package/lib/AiProxy.js +125 -0
  6. package/lib/AiProxy.js.map +1 -0
  7. package/lib/ApiKey.d.ts +105 -0
  8. package/lib/ApiKey.d.ts.map +1 -0
  9. package/lib/ApiKey.js +134 -0
  10. package/lib/ApiKey.js.map +1 -0
  11. package/lib/AuthProvider.d.ts +71 -0
  12. package/lib/AuthProvider.d.ts.map +1 -0
  13. package/lib/AuthProvider.js +322 -0
  14. package/lib/AuthProvider.js.map +1 -0
  15. package/lib/BoatBindings.d.ts +52 -0
  16. package/lib/BoatBindings.d.ts.map +1 -0
  17. package/lib/BoatBindings.js +104 -0
  18. package/lib/BoatBindings.js.map +1 -0
  19. package/lib/Bootstrap.d.ts +8 -0
  20. package/lib/Bootstrap.d.ts.map +1 -0
  21. package/lib/Bootstrap.js +28 -0
  22. package/lib/Bootstrap.js.map +1 -0
  23. package/lib/ConnectSandbox.d.ts +56 -0
  24. package/lib/ConnectSandbox.d.ts.map +1 -0
  25. package/lib/ConnectSandbox.js +3 -0
  26. package/lib/ConnectSandbox.js.map +1 -0
  27. package/lib/ConnectSandboxHttp.d.ts +21 -0
  28. package/lib/ConnectSandboxHttp.d.ts.map +1 -0
  29. package/lib/ConnectSandboxHttp.js +46 -0
  30. package/lib/ConnectSandboxHttp.js.map +1 -0
  31. package/lib/Contributions.d.ts +7 -0
  32. package/lib/Contributions.d.ts.map +1 -0
  33. package/lib/Contributions.js +6 -0
  34. package/lib/Contributions.js.map +1 -0
  35. package/lib/CreateSandbox.d.ts +134 -0
  36. package/lib/CreateSandbox.d.ts.map +1 -0
  37. package/lib/CreateSandbox.js +3 -0
  38. package/lib/CreateSandbox.js.map +1 -0
  39. package/lib/CreateSandboxHttp.d.ts +21 -0
  40. package/lib/CreateSandboxHttp.d.ts.map +1 -0
  41. package/lib/CreateSandboxHttp.js +93 -0
  42. package/lib/CreateSandboxHttp.js.map +1 -0
  43. package/lib/CredentialProfile.d.ts +157 -0
  44. package/lib/CredentialProfile.d.ts.map +1 -0
  45. package/lib/CredentialProfile.js +245 -0
  46. package/lib/CredentialProfile.js.map +1 -0
  47. package/lib/Credentials.d.ts +15 -0
  48. package/lib/Credentials.d.ts.map +1 -0
  49. package/lib/Credentials.js +23 -0
  50. package/lib/Credentials.js.map +1 -0
  51. package/lib/Environment.d.ts +219 -0
  52. package/lib/Environment.d.ts.map +1 -0
  53. package/lib/Environment.js +273 -0
  54. package/lib/Environment.js.map +1 -0
  55. package/lib/Errors.d.ts +50 -0
  56. package/lib/Errors.d.ts.map +1 -0
  57. package/lib/Errors.js +17 -0
  58. package/lib/Errors.js.map +1 -0
  59. package/lib/Providers.d.ts +33 -0
  60. package/lib/Providers.d.ts.map +1 -0
  61. package/lib/Providers.js +47 -0
  62. package/lib/Providers.js.map +1 -0
  63. package/lib/Sandbox.d.ts +205 -0
  64. package/lib/Sandbox.d.ts.map +1 -0
  65. package/lib/Sandbox.js +368 -0
  66. package/lib/Sandbox.js.map +1 -0
  67. package/lib/SandboxClient.d.ts +159 -0
  68. package/lib/SandboxClient.d.ts.map +1 -0
  69. package/lib/SandboxClient.js +1 -0
  70. package/lib/SandboxClient.js.map +1 -0
  71. package/lib/SandboxEvents.d.ts +111 -0
  72. package/lib/SandboxEvents.d.ts.map +1 -0
  73. package/lib/SandboxEvents.js +60 -0
  74. package/lib/SandboxEvents.js.map +1 -0
  75. package/lib/SandboxEventsLive.d.ts +13 -0
  76. package/lib/SandboxEventsLive.d.ts.map +1 -0
  77. package/lib/SandboxEventsLive.js +63 -0
  78. package/lib/SandboxEventsLive.js.map +1 -0
  79. package/lib/Snapshot.d.ts +123 -0
  80. package/lib/Snapshot.d.ts.map +1 -0
  81. package/lib/Snapshot.js +186 -0
  82. package/lib/Snapshot.js.map +1 -0
  83. package/lib/Webhook.d.ts +56 -0
  84. package/lib/Webhook.d.ts.map +1 -0
  85. package/lib/Webhook.js +98 -0
  86. package/lib/Webhook.js.map +1 -0
  87. package/lib/index.d.ts +19 -0
  88. package/lib/index.d.ts.map +1 -0
  89. package/lib/index.js +19 -0
  90. package/lib/index.js.map +1 -0
  91. package/lib/internal/Declared.d.ts +34 -0
  92. package/lib/internal/Declared.d.ts.map +1 -0
  93. package/lib/internal/Declared.js +38 -0
  94. package/lib/internal/Declared.js.map +1 -0
  95. package/lib/internal/Org.d.ts +22 -0
  96. package/lib/internal/Org.d.ts.map +1 -0
  97. package/lib/internal/Org.js +23 -0
  98. package/lib/internal/Org.js.map +1 -0
  99. package/lib/internal/Ownership.d.ts +29 -0
  100. package/lib/internal/Ownership.d.ts.map +1 -0
  101. package/lib/internal/Ownership.js +50 -0
  102. package/lib/internal/Ownership.js.map +1 -0
  103. package/lib/internal/Program.d.ts +69 -0
  104. package/lib/internal/Program.d.ts.map +1 -0
  105. package/lib/internal/Program.js +178 -0
  106. package/lib/internal/Program.js.map +1 -0
  107. package/lib/internal/Ref.d.ts +14 -0
  108. package/lib/internal/Ref.d.ts.map +1 -0
  109. package/lib/internal/Ref.js +7 -0
  110. package/lib/internal/Ref.js.map +1 -0
  111. package/lib/internal/SandboxHandle.d.ts +17 -0
  112. package/lib/internal/SandboxHandle.d.ts.map +1 -0
  113. package/lib/internal/SandboxHandle.js +249 -0
  114. package/lib/internal/SandboxHandle.js.map +1 -0
  115. package/lib/internal/WebhookDelivery.d.ts +16 -0
  116. package/lib/internal/WebhookDelivery.d.ts.map +1 -0
  117. package/lib/internal/WebhookDelivery.js +109 -0
  118. package/lib/internal/WebhookDelivery.js.map +1 -0
  119. package/package.json +67 -3
  120. package/src/AiProxy.ts +171 -0
  121. package/src/ApiKey.ts +250 -0
  122. package/src/AuthProvider.ts +472 -0
  123. package/src/BoatBindings.ts +136 -0
  124. package/src/Bootstrap.ts +55 -0
  125. package/src/ConnectSandbox.ts +60 -0
  126. package/src/ConnectSandboxHttp.ts +59 -0
  127. package/src/Contributions.ts +9 -0
  128. package/src/CreateSandbox.ts +143 -0
  129. package/src/CreateSandboxHttp.ts +115 -0
  130. package/src/CredentialProfile.ts +350 -0
  131. package/src/Credentials.ts +33 -0
  132. package/src/Environment.ts +416 -0
  133. package/src/Errors.ts +34 -0
  134. package/src/Providers.ts +71 -0
  135. package/src/Sandbox.ts +647 -0
  136. package/src/SandboxClient.ts +199 -0
  137. package/src/SandboxEvents.ts +125 -0
  138. package/src/SandboxEventsLive.ts +88 -0
  139. package/src/Snapshot.ts +284 -0
  140. package/src/Webhook.ts +163 -0
  141. package/src/globals.d.ts +2 -0
  142. package/src/index.ts +18 -0
  143. package/src/internal/Declared.ts +87 -0
  144. package/src/internal/Org.ts +31 -0
  145. package/src/internal/Ownership.ts +75 -0
  146. package/src/internal/Program.ts +250 -0
  147. package/src/internal/Ref.ts +15 -0
  148. package/src/internal/SandboxHandle.ts +425 -0
  149. package/src/internal/WebhookDelivery.ts +179 -0
@@ -0,0 +1,47 @@
1
+ import { CredentialsStoreLive } from "alchemy/Auth/Credentials";
2
+ import { ProfileStoreLive } from "alchemy/Auth/Profile";
3
+ import * as Provider from "alchemy/Provider";
4
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
5
+ import * as Layer from "effect/Layer";
6
+ import { AiProxy, AiProxyProvider } from "./AiProxy.js";
7
+ import { ApiKey, ApiKeyProvider } from "./ApiKey.js";
8
+ import { BoatAuth } from "./AuthProvider.js";
9
+ import { CredentialProfile, CredentialProfileProvider } from "./CredentialProfile.js";
10
+ import * as Credentials from "./Credentials.js";
11
+ import { Environment, EnvironmentProvider } from "./Environment.js";
12
+ import { Sandbox, SandboxProvider } from "./Sandbox.js";
13
+ import { Snapshot, SnapshotProvider } from "./Snapshot.js";
14
+ import { Webhook, WebhookProvider } from "./Webhook.js";
15
+ /**
16
+ * Service tag bundling all Boat providers + auth + credentials. Use
17
+ * `Boat.providers()` to materialize the full layer.
18
+ */
19
+ export class Providers extends Provider.ProviderCollection()("Boat") {
20
+ }
21
+ /**
22
+ * Build the complete Boat providers Layer: every Boat resource, the Boat
23
+ * AuthProvider registration (so `alchemy profile edit` can configure it),
24
+ * and credential resolution.
25
+ *
26
+ * `org` picks the organization (wallet) the stack's resources bill and the
27
+ * one whose shared environments `Boat.Environment` manages: an org id
28
+ * (`team_…`), an org name, or `personal`. It overrides the profile's `org`
29
+ * (or `BOAT_ORG`); with neither, Boat uses the account's active wallet and
30
+ * environments are your personal ones.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * Alchemy.Stack("App", { providers: Boat.providers() }, ...)
35
+ * Alchemy.Stack("App", { providers: Boat.providers({ org: "team_abc" }) }, ...)
36
+ * ```
37
+ */
38
+ export const providers = (options) => Layer.effect(Providers, Provider.collection([
39
+ AiProxy,
40
+ ApiKey,
41
+ CredentialProfile,
42
+ Environment,
43
+ Sandbox,
44
+ Snapshot,
45
+ Webhook,
46
+ ])).pipe(Layer.provide(Layer.mergeAll(AiProxyProvider(), ApiKeyProvider(), CredentialProfileProvider(), EnvironmentProvider(), SandboxProvider(), SnapshotProvider(), WebhookProvider())), Layer.provideMerge(Credentials.fromAuthProvider(options)), Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(BoatAuth), Layer.provideMerge(ProfileStoreLive), Layer.provideMerge(CredentialsStoreLive), Layer.orDie);
47
+ //# sourceMappingURL=Providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Providers.js","sources":["../src/Providers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,eAAe,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACtF,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAExD;;;GAGG;AACH,MAAM,OAAO,SAAU,SAAQ,QAAQ,CAAC,kBAAkB,EAAa,CAAC,MAAM,CAAC;CAAG;AAIlF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAmC,EAAE,EAAE,CAC/D,KAAK,CAAC,MAAM,CACV,SAAS,EACT,QAAQ,CAAC,UAAU,CAAC;IAClB,OAAO;IACP,MAAM;IACN,iBAAiB;IACjB,WAAW;IACX,OAAO;IACP,QAAQ;IACR,OAAO;CACR,CAAC,CACH,CAAC,IAAI,CACJ,KAAK,CAAC,OAAO,CACX,KAAK,CAAC,QAAQ,CACZ,eAAe,EAAE,EACjB,cAAc,EAAE,EAChB,yBAAyB,EAAE,EAC3B,mBAAmB,EAAE,EACrB,eAAe,EAAE,EACjB,gBAAgB,EAAE,EAClB,eAAe,EAAE,CAClB,CACF,EACD,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,EACzD,KAAK,CAAC,YAAY,CAAC,eAAe,CAAC,KAAK,CAAC,EACzC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,EAC5B,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,EACpC,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,EACxC,KAAK,CAAC,KAAK,CACZ,CAAC"}
@@ -0,0 +1,205 @@
1
+ import * as boat from "@distilled.cloud/boat-dev/boat";
2
+ import type * as Bundle from "alchemy/Bundle/Bundle";
3
+ import { Platform, type Main, type PlatformProps } from "alchemy/Platform";
4
+ import * as Provider from "alchemy/Provider";
5
+ import { Resource } from "alchemy/Resource";
6
+ import { type HostRuntimeContext, type ServerHost } from "alchemy/Runtime/Process";
7
+ import { Stack } from "alchemy/Stack";
8
+ import * as Duration from "effect/Duration";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Redacted from "effect/Redacted";
11
+ import { type ProgramState } from "./internal/Program.ts";
12
+ import { type Ref } from "./internal/Ref.ts";
13
+ import type { Providers } from "./Providers.ts";
14
+ import type { SnapshotBase } from "./Snapshot.ts";
15
+ /**
16
+ * Machine size. `small` runs at half the default rate and `large` at
17
+ * twice; `xlarge` needs the $100/month plan or higher.
18
+ */
19
+ export type SandboxType = "small" | "default" | "large" | "xlarge";
20
+ /** Where a sandbox is in its life. `archived` is stopped, with its disk saved. */
21
+ export type SandboxState = "init" | "provisioning" | "provisioned" | "cloning" | "ready" | "idle" | "running" | "archiving" | "archived" | "error" | "cancelled";
22
+ /** How the setup script went: `pending`, `running`, `done` or `failed`. */
23
+ export type SandboxSetupStatus = "pending" | "running" | "done" | "failed";
24
+ /** An environment, by name. Accepts a `Boat.Environment` or `{ name }`. */
25
+ export interface SandboxEnvironmentRef {
26
+ name: string;
27
+ }
28
+ /** What bindings attach to a sandbox that runs a program: its process's environment. */
29
+ export interface SandboxBinding {
30
+ env?: Record<string, any>;
31
+ }
32
+ export interface SandboxProps extends PlatformProps {
33
+ /**
34
+ * Display name. If omitted, a unique name is generated from the stack,
35
+ * stage and logical ID. Renamed in place.
36
+ */
37
+ name?: string;
38
+ /**
39
+ * Machine size. Changing it replaces the sandbox.
40
+ * @default "default"
41
+ */
42
+ type?: SandboxType;
43
+ /**
44
+ * How long the sandbox runs before Boat stops it, saving its disk;
45
+ * `false` never stops it. Updated in place: the next stop is then
46
+ * `autoStop` from the deploy.
47
+ * @default "1 hour", or `false` for a sandbox with `main`
48
+ */
49
+ autoStop?: Duration.Input | false;
50
+ /**
51
+ * Environment variables. On a sandbox without `main` they are the
52
+ * sandbox's, on top of its environment's, and changing them replaces it.
53
+ * On a sandbox with `main` they are the program's, and changing them
54
+ * restarts it.
55
+ */
56
+ env?: Record<string, string | Redacted.Redacted<string>>;
57
+ /**
58
+ * The `Boat.Environment` to attach (or `{ name }` of one): its
59
+ * variables, secret files and credential settings. Omit for the account's
60
+ * default environment (`base`). Changing it replaces the sandbox.
61
+ */
62
+ environment?: Ref<SandboxEnvironmentRef>;
63
+ /**
64
+ * Attach none of the account's secrets (no environment variables, secret
65
+ * files or credentials), and confine the sandbox so it can't act on the
66
+ * account. For sandboxes you give to your own users. Changing it replaces
67
+ * the sandbox.
68
+ * @default false
69
+ */
70
+ noEnv?: boolean;
71
+ /**
72
+ * `false` never snapshots the sandbox: stopping it erases its disk, and it
73
+ * can't be resumed, forked or saved. Changing it replaces the sandbox.
74
+ * @default true
75
+ */
76
+ snapshots?: boolean;
77
+ /**
78
+ * `false` keeps no command output, process list or metrics for the
79
+ * sandbox. Changing it replaces the sandbox.
80
+ * @default true
81
+ */
82
+ monitoring?: boolean;
83
+ /**
84
+ * A shell script run once, in the background, after the sandbox starts.
85
+ * Its outcome is `setupStatus`. Changing it replaces the sandbox.
86
+ */
87
+ setupScript?: string;
88
+ /**
89
+ * The `Boat.Snapshot` to start from (or `{ name }` of one saved
90
+ * elsewhere, or a share code `bsh_...`). Changing or rebuilding it
91
+ * replaces the sandbox, so it starts from the new disk.
92
+ */
93
+ snapshot?: Ref<SnapshotBase>;
94
+ /**
95
+ * Your own labels, at most 17 (Alchemy adds three). Boat sets labels
96
+ * only at create, so changing them replaces the sandbox.
97
+ */
98
+ labels?: Record<string, string>;
99
+ /**
100
+ * The sandbox's subdomain under `on.boat.dev`. Port `p` answers at
101
+ * `https://<subdomain>-<p>.on.boat.dev`. Renamed in place.
102
+ */
103
+ subdomain?: string;
104
+ /**
105
+ * Module entrypoint of a program to run in the sandbox, typically
106
+ * `import.meta.url` in the class form. Alchemy bundles it, uploads it
107
+ * into the sandbox and runs it with Node 24; a code change uploads it
108
+ * again and restarts the process, without replacing the sandbox.
109
+ */
110
+ main?: string;
111
+ /**
112
+ * Port the program listens on. Written to `PORT`; Boat serves it at
113
+ * `url`.
114
+ * @default 3000
115
+ */
116
+ port?: number;
117
+ /**
118
+ * Serve the program's port to anyone. Otherwise requests need `urlToken`
119
+ * (as the `_token` query parameter, or the `_port_auth` cookie).
120
+ * Updated in place.
121
+ * @default false
122
+ */
123
+ public?: boolean;
124
+ /**
125
+ * Named export to load from `main`.
126
+ * @default "default"
127
+ */
128
+ handler?: string;
129
+ /** Bundler configuration for `main`. */
130
+ build?: Bundle.BundleConfig;
131
+ /**
132
+ * Who in the organization that pays for the sandbox can use it:
133
+ * `private`, only you; `shared`, every member. Omitted, the organization's
134
+ * default for new sandboxes applies. Updated in place. Sharing a sandbox
135
+ * that holds your logins (GitHub, Claude and Codex logins, environment
136
+ * secrets) wipes them at its next start and never pushes them again, so
137
+ * share one with `noEnv` or an environment that passes nothing. A
138
+ * personal sandbox is always private, and Boat refuses `shared` for one.
139
+ */
140
+ visibility?: "shared" | "private";
141
+ }
142
+ export interface SandboxAttributes {
143
+ /** Boat's sandbox id (`bx_…`). */
144
+ sandboxId: string;
145
+ name: string;
146
+ /** `ready`, `running`, `idle`, `archived` (stopped), … */
147
+ state: SandboxState;
148
+ type: SandboxType | undefined;
149
+ /** vCPUs and RAM (GB) the machine size guarantees. */
150
+ vcpu: number | undefined;
151
+ memoryGB: number | undefined;
152
+ /**
153
+ * The subdomain its ports are served under: port `p` answers at
154
+ * `https://<subdomain>-<p>.on.boat.dev`.
155
+ */
156
+ subdomain: string | undefined;
157
+ /** The machine's URL. */
158
+ machineUrl: string | undefined;
159
+ /** The environment the sandbox runs, and its version. */
160
+ environment: string | undefined;
161
+ environmentVersion: number | undefined;
162
+ /** Seconds it runs before stopping, or `undefined` when it never stops. */
163
+ ttlSeconds: number | undefined;
164
+ /** Your labels, without Alchemy's. */
165
+ labels: Record<string, string>;
166
+ /** The setup script's outcome: `pending`, `running`, `done` or `failed`. */
167
+ setupStatus: SandboxSetupStatus | undefined;
168
+ /** The program's URL, for a sandbox with `main`. */
169
+ url: string | undefined;
170
+ /**
171
+ * The token a request to `url` needs unless the sandbox is `public`: the
172
+ * `_token` query parameter, or the `_port_auth` cookie.
173
+ */
174
+ urlToken: Redacted.Redacted<string> | undefined;
175
+ /** The program the sandbox runs, for a sandbox with `main`. */
176
+ program: ProgramState | undefined;
177
+ /**
178
+ * The organization that pays for it, fixed when it was created;
179
+ * `undefined` when you do.
180
+ */
181
+ org: {
182
+ id: string;
183
+ name: string;
184
+ } | undefined;
185
+ /** `shared` or `private` for an organization's sandbox; `undefined` for yours. */
186
+ visibility: "shared" | "private" | undefined;
187
+ }
188
+ export type Sandbox = Resource<"Boat.Sandbox", SandboxProps, SandboxAttributes, SandboxBinding, Providers, {
189
+ sandboxId: string;
190
+ }>;
191
+ export type SandboxServices = ServerHost;
192
+ /** A program's shape: a long-running server returning `{ fetch }`, or `host.run` loops. */
193
+ export type SandboxShape = Main<SandboxServices>;
194
+ export type SandboxRuntimeContext = HostRuntimeContext;
195
+ export declare const Sandbox: Platform<Sandbox, SandboxServices, SandboxShape, SandboxRuntimeContext> & {
196
+ /** References a sandbox the stack doesn't manage, by id. */
197
+ readonly lookup: ReturnType<typeof Resource<Sandbox>>["lookup"];
198
+ };
199
+ /** A sandbox, or `undefined` when Boat no longer has it. */
200
+ export declare const observeSandbox: (sandboxId: string) => Effect.Effect<boat.Sandbox | undefined, import("@distilled.cloud/boat-dev").BadGateway | import("@distilled.cloud/core/errors").BadRequest | import("@distilled.cloud/boat-dev").BoatParseError | import("@distilled.cloud/boat-dev").ConfigError | import("@distilled.cloud/core/errors").Conflict | import("@distilled.cloud/core/errors").Forbidden | import("@distilled.cloud/boat-dev").GatewayTimeout | import("effect/http/HttpClientError").HttpClientError | import("@distilled.cloud/boat-dev").InternalServerError | import("@distilled.cloud/core/protocol-http").InvalidInput | import("@distilled.cloud/boat-dev").Locked | import("@distilled.cloud/core/sdk-error").SdkError | import("@distilled.cloud/boat-dev").ServiceUnavailable | import("@distilled.cloud/boat-dev").TooManyRequests | import("@distilled.cloud/boat-dev").Unauthorized | import("@distilled.cloud/boat-dev").UnknownBoatError | import("@distilled.cloud/boat-dev").UnprocessableEntity, boat.BoatOpContext>;
201
+ /** Delete a sandbox; deleting one that is gone succeeds. */
202
+ export declare const deleteSandbox: (sandboxId: string) => Effect.Effect<void, import("@distilled.cloud/boat-dev").BadGateway | import("@distilled.cloud/core/errors").BadRequest | import("@distilled.cloud/boat-dev").BoatParseError | import("@distilled.cloud/boat-dev").ConfigError | import("@distilled.cloud/core/errors").Conflict | boat.Conflict | import("@distilled.cloud/core/errors").Forbidden | boat.Forbidden | import("@distilled.cloud/boat-dev").GatewayTimeout | import("effect/http/HttpClientError").HttpClientError | import("@distilled.cloud/boat-dev").InternalServerError | import("@distilled.cloud/core/protocol-http").InvalidInput | import("@distilled.cloud/boat-dev").Locked | import("@distilled.cloud/core/sdk-error").SdkError | import("@distilled.cloud/boat-dev").ServiceUnavailable | import("@distilled.cloud/boat-dev").TooManyRequests | import("@distilled.cloud/boat-dev").Unauthorized | import("@distilled.cloud/boat-dev").UnknownBoatError | import("@distilled.cloud/boat-dev").UnprocessableEntity, boat.BoatOpContext>;
203
+ export declare const toAttributes: (sandbox: boat.Sandbox) => SandboxAttributes;
204
+ export declare const SandboxProvider: () => import("effect/Layer").Layer<Provider.Provider<Sandbox>, never, import("effect/FileSystem").FileSystem | import("effect/Path").Path | Stack | import("alchemy/Stage").Stage | boat.BoatOpContext>;
205
+ //# sourceMappingURL=Sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sandbox.d.ts","sources":["../src/Sandbox.ts"],"names":[],"mappings":"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"}
package/lib/Sandbox.js ADDED
@@ -0,0 +1,368 @@
1
+ import * as boat from "@distilled.cloud/boat-dev/boat";
2
+ import { Unowned } from "alchemy/AdoptPolicy";
3
+ import { isResolved } from "alchemy/Diff";
4
+ import { Platform } from "alchemy/Platform";
5
+ import * as Provider from "alchemy/Provider";
6
+ import { Resource } from "alchemy/Resource";
7
+ import { createHostRuntimeContext, } from "alchemy/Runtime/Process";
8
+ import { Stack } from "alchemy/Stack";
9
+ import * as Duration from "effect/Duration";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Redacted from "effect/Redacted";
12
+ import * as Stream from "effect/Stream";
13
+ import { isDeclaration } from "./internal/Declared.js";
14
+ import { boatName, hasAnyAlchemyLabel, hasOwnershipLabels, ownershipLabels, userLabels, } from "./internal/Ownership.js";
15
+ import { bundleProgram, DEFAULT_PORT, deployProgram, isProgramRunning, } from "./internal/Program.js";
16
+ import { resolved } from "./internal/Ref.js";
17
+ /**
18
+ * A Boat sandbox the stack owns: a Linux machine with Docker, passwordless
19
+ * `sudo` and a desktop, that stops itself after `autoStop` and keeps its
20
+ * disk until it is resumed.
21
+ *
22
+ * Use it for a workspace that outlives requests (an agent's home, a dev
23
+ * box, a preview environment). Bindings create short-lived sandboxes at
24
+ * run time with `Boat.CreateSandbox`.
25
+ *
26
+ * Alchemy labels the sandbox with its stack, stage and logical ID
27
+ * (`alchemy.stack`, `alchemy.stage`, `alchemy.id`) to recognise the
28
+ * sandboxes it owns.
29
+ *
30
+ * The sandbox bills the stack's organization (`Boat.providers({ org })`,
31
+ * or the profile's), else your active wallet. Boat fixes the wallet at
32
+ * create, so changing the stack's `org` later leaves the sandbox where it
33
+ * is; `org` says which.
34
+ *
35
+ * :::caution
36
+ * Replacing the sandbox (a new `type`, `env`, `environment`, `noEnv`,
37
+ * `snapshots`, `monitoring`, `setupScript`, `labels`, or a new or rebuilt
38
+ * `snapshot`) creates a fresh sandbox and deletes the old one with its disk.
39
+ * :::
40
+ *
41
+ * ### Creating a Sandbox
42
+ * **Example:** A dev box that stops after an idle hour
43
+ * ```typescript
44
+ * import * as Boat from "@alchemy.run/boat";
45
+ *
46
+ * export const DevBox = Boat.Sandbox("DevBox", {
47
+ * type: "large",
48
+ * autoStop: "1 hour",
49
+ * setupScript: "git clone https://github.com/alchemy-run/alchemy ~/alchemy",
50
+ * });
51
+ * ```
52
+ *
53
+ * **Example:** A sandbox with none of the account's secrets
54
+ * ```typescript
55
+ * export const Untrusted = Boat.Sandbox("Untrusted", {
56
+ * noEnv: true,
57
+ * env: { TASK: "lint" },
58
+ * });
59
+ * ```
60
+ *
61
+ * ### Running a Program
62
+ * The class form runs an Effect program in the sandbox: give it `main` and
63
+ * an implementation, and Alchemy bundles the program, uploads it and runs
64
+ * it with Node, served at `url`. A code change uploads the new bundle and
65
+ * restarts the process; the sandbox and its URL stay the same. A sandbox
66
+ * with `main` never stops on its own unless `autoStop` says so; if it
67
+ * stopped anyway, the next deploy resumes it and starts the program.
68
+ *
69
+ * **Example:** An HTTP server
70
+ * ```typescript
71
+ * export default class Api extends Boat.Sandbox<Api>()(
72
+ * "Api",
73
+ * { main: import.meta.url, public: true },
74
+ * Effect.gen(function* () {
75
+ * return {
76
+ * fetch: Effect.gen(function* () {
77
+ * return yield* HttpServerResponse.json({ ok: true });
78
+ * }),
79
+ * };
80
+ * }),
81
+ * ) {}
82
+ * ```
83
+ *
84
+ * ### Starting from a snapshot and an environment
85
+ * **Example:** A sandbox with a project set up and its secrets
86
+ * ```typescript
87
+ * export const Golden = Boat.Snapshot("Golden", { run: ["npm i -g pnpm"] });
88
+ * export const MyEnv = Boat.Environment("MyEnv", {
89
+ * env: { API_TOKEN: Config.Redacted("API_TOKEN") },
90
+ * });
91
+ *
92
+ * export const Box = Boat.Sandbox("Box", {
93
+ * snapshot: Golden,
94
+ * environment: MyEnv,
95
+ * autoStop: "30 minutes",
96
+ * });
97
+ * ```
98
+ *
99
+ * ### Sharing with an organization
100
+ * **Example:** A sandbox every member of the team can use
101
+ * ```typescript
102
+ * // with Boat.providers({ org: "acme" }) on the stack
103
+ * export const TeamBox = Boat.Sandbox("TeamBox", {
104
+ * noEnv: true,
105
+ * visibility: "shared",
106
+ * });
107
+ * ```
108
+ *
109
+ * ### Using it at runtime
110
+ * **Example:** Run a command in it from a Worker
111
+ * ```typescript
112
+ * const devBox = yield* Boat.ConnectSandbox(DevBox);
113
+ * // in fetch:
114
+ * yield* devBox.resume();
115
+ * const result = yield* devBox.exec("git pull && npm test", { cwd: "/home/user/repo" });
116
+ * ```
117
+ *
118
+ * ### Referencing a sandbox the stack doesn't manage
119
+ * **Example:** Look one up by id
120
+ * ```typescript
121
+ * const shared = Boat.Sandbox.lookup({ sandboxId: "bx_abc123" });
122
+ * const box = yield* Boat.ConnectSandbox(shared);
123
+ * ```
124
+ *
125
+ * @resource
126
+ * @product Sandbox
127
+ */
128
+ /** The plain resource behind {@link Sandbox}, which carries `lookup`. */
129
+ const SandboxResource = Resource("Boat.Sandbox");
130
+ export const Sandbox = Object.assign(Platform("Boat.Sandbox", {
131
+ createRuntimeContext: createHostRuntimeContext("Boat.Sandbox"),
132
+ // A declaration passed as a prop (`snapshot: Golden`) is an Effect until
133
+ // yielded. Yield it here so it is registered and the prop resolves to
134
+ // its attributes, as with `yield* Golden` inside Effect.gen.
135
+ transformProps: (_id, props) => Effect.gen(function* () {
136
+ if (globalThis.__ALCHEMY_RUNTIME__)
137
+ return props;
138
+ // A lookup (`Boat.Environment.lookup({ name })`) is an Output, which
139
+ // the engine resolves; only a declaration is yielded.
140
+ const ref = (value) => isDeclaration(value)
141
+ ? value
142
+ : Effect.succeed(value);
143
+ return {
144
+ ...props,
145
+ snapshot: props.snapshot === undefined ? undefined : yield* ref(props.snapshot),
146
+ environment: props.environment === undefined ? undefined : yield* ref(props.environment),
147
+ };
148
+ }),
149
+ }), { lookup: (props) => SandboxResource.lookup(props) });
150
+ /** A sandbox, or `undefined` when Boat no longer has it. */
151
+ export const observeSandbox = (sandboxId) => boat.getSandbox({ sandboxId }).pipe(Effect.map((got) => got.sandbox === undefined || got.sandbox.state === "cancelled" ? undefined : got.sandbox), Effect.catchTag("NotFound", () => Effect.undefined));
152
+ /** Delete a sandbox; deleting one that is gone succeeds. */
153
+ export const deleteSandbox = (sandboxId) => boat.deleteSandbox({ sandboxId, xAsciiConfirmDelete: sandboxId }).pipe(Effect.asVoid, Effect.catchTag("NotFound", () => Effect.void));
154
+ const plainEnv = (env) => env === undefined
155
+ ? undefined
156
+ : Object.fromEntries(Object.entries(env).map(([key, value]) => [
157
+ key,
158
+ Redacted.isRedacted(value) ? Redacted.value(value) : value,
159
+ ]));
160
+ export const toAttributes = (sandbox) => ({
161
+ sandboxId: sandbox.id,
162
+ name: sandbox.name,
163
+ state: sandbox.state,
164
+ type: sandbox.type,
165
+ vcpu: sandbox.vcpu,
166
+ memoryGB: sandbox.memoryGB,
167
+ subdomain: sandbox.subdomain ?? undefined,
168
+ machineUrl: sandbox.url ?? undefined,
169
+ environment: sandbox.environment ?? undefined,
170
+ environmentVersion: sandbox.environmentVersion ?? undefined,
171
+ ttlSeconds: sandbox.ttlSeconds ?? undefined,
172
+ labels: userLabels(sandbox.labels),
173
+ setupStatus: sandbox.setupStatus ?? undefined,
174
+ url: undefined,
175
+ urlToken: undefined,
176
+ program: undefined,
177
+ org: sandbox.team ? { id: sandbox.team.id, name: sandbox.team.name } : undefined,
178
+ visibility: sandbox.visibility ?? undefined,
179
+ });
180
+ /** The bindings still attached (a removed binding arrives as a `delete`). */
181
+ const activeBindings = (bindings) => bindings.filter((binding) => binding.action !== "delete");
182
+ /** The program's environment from its bindings. */
183
+ const bindingEnv = (bindings) => activeBindings(bindings).reduce((acc, binding) => ({ ...acc, ...binding.data?.env }), {});
184
+ /**
185
+ * Boat's `ttlSeconds` for `autoStop`: `null` never stops. Without
186
+ * `autoStop`, a program sandbox never stops and others keep Boat's default.
187
+ */
188
+ const ttlSecondsOf = (autoStop, program) => autoStop === false
189
+ ? null
190
+ : autoStop === undefined
191
+ ? program
192
+ ? null
193
+ : undefined
194
+ : Math.max(1, Math.round(Duration.toMillis(Duration.fromInputUnsafe(autoStop)) / 1000));
195
+ /** Props whose change means a new sandbox (Boat can't change them on a live one). */
196
+ const replacingProps = (props) => JSON.stringify({
197
+ type: props.type ?? "default",
198
+ // A program's env is its process's: changing it restarts the program.
199
+ env: props.main === undefined ? (plainEnv(props.env) ?? {}) : undefined,
200
+ program: props.main !== undefined,
201
+ environment: props.environment === undefined ? undefined : resolved(props.environment).name,
202
+ noEnv: props.noEnv ?? false,
203
+ snapshots: props.snapshots ?? true,
204
+ monitoring: props.monitoring ?? true,
205
+ setupScript: props.setupScript,
206
+ snapshot: props.snapshot === undefined
207
+ ? undefined
208
+ : [resolved(props.snapshot).name, resolved(props.snapshot).setupHash ?? null],
209
+ labels: props.labels ?? {},
210
+ });
211
+ export const SandboxProvider = () => Provider.effect(Sandbox, Effect.gen(function* () {
212
+ const stack = yield* Stack;
213
+ // The bundled program's bootstrap reads these to rebuild the Stack at runtime.
214
+ const alchemyEnv = {
215
+ ALCHEMY_STACK_NAME: stack.name,
216
+ ALCHEMY_STAGE: stack.stage,
217
+ ALCHEMY_PHASE: "runtime",
218
+ HOST: "0.0.0.0",
219
+ };
220
+ return {
221
+ stables: ["sandboxId"],
222
+ diff: Effect.fn(function* ({ news, olds, output }) {
223
+ if (olds === undefined)
224
+ return undefined;
225
+ // A program's props are partly unresolved at plan (its bindings'
226
+ // env holds Outputs); the replacing ones are compared once known.
227
+ if (isResolved(news) && replacingProps(olds) !== replacingProps(news)) {
228
+ return { action: "replace" };
229
+ }
230
+ // The bundle depends only on `main`, `handler` and `build`, which
231
+ // are plain. A code change, or a process that ended (the sandbox
232
+ // stopped or rebooted), redeploys the program in place.
233
+ const props = news;
234
+ if (typeof props.main === "string" && output !== undefined) {
235
+ const { hash } = yield* bundleProgram({ ...props, main: props.main });
236
+ if (output.program === undefined || hash !== output.program.codeHash) {
237
+ return { action: "update" };
238
+ }
239
+ if (!(yield* isProgramRunning(output.sandboxId, output.program))) {
240
+ return { action: "update" };
241
+ }
242
+ }
243
+ return undefined;
244
+ }),
245
+ read: Effect.fn(function* ({ id, output }) {
246
+ if (output === undefined)
247
+ return undefined;
248
+ const sandbox = yield* observeSandbox(output.sandboxId);
249
+ if (sandbox === undefined)
250
+ return undefined;
251
+ // The program isn't visible on the sandbox; keep what the last deploy recorded.
252
+ const attrs = {
253
+ ...toAttributes(sandbox),
254
+ url: output.url,
255
+ urlToken: output.urlToken,
256
+ program: output.program,
257
+ };
258
+ return (yield* hasOwnershipLabels(id, sandbox.labels)) ? attrs : Unowned(attrs);
259
+ }),
260
+ lookup: Effect.fn(function* ({ sandboxId }) {
261
+ const sandbox = yield* observeSandbox(sandboxId);
262
+ return sandbox === undefined ? undefined : toAttributes(sandbox);
263
+ }),
264
+ reconcile: Effect.fn(function* ({ id, news, output, bindings, session }) {
265
+ const name = yield* boatName(id, news.name ?? output?.name, 120);
266
+ const owner = yield* ownershipLabels(id);
267
+ const program = news.main !== undefined;
268
+ // A program serves for as long as the sandbox runs: don't stop it on a timer.
269
+ const ttlSeconds = ttlSecondsOf(news.autoStop, program);
270
+ // Observe — `output` caches the id; the sandbox may be gone.
271
+ let sandbox = output === undefined ? undefined : yield* observeSandbox(output.sandboxId);
272
+ // Ensure
273
+ if (sandbox === undefined) {
274
+ yield* session.note(`Creating sandbox ${name}...`);
275
+ const created = yield* boat.createSandbox({
276
+ ...(news.type === undefined ? {} : { type: news.type }),
277
+ ...(ttlSeconds === undefined ? {} : { ttlSeconds }),
278
+ // A program's env is its process's, written at deploy, not the sandbox's.
279
+ ...(news.env === undefined || program ? {} : { env: plainEnv(news.env) }),
280
+ ...(news.environment === undefined
281
+ ? {}
282
+ : { environment: resolved(news.environment).name }),
283
+ ...(news.noEnv === undefined ? {} : { noEnv: news.noEnv }),
284
+ ...(news.snapshots === undefined ? {} : { snapshots: news.snapshots }),
285
+ ...(news.monitoring === undefined ? {} : { monitoring: news.monitoring }),
286
+ ...(news.setupScript === undefined ? {} : { setupScript: news.setupScript }),
287
+ ...(news.snapshot === undefined ? {} : { from: resolved(news.snapshot).name }),
288
+ ...(news.visibility === undefined ? {} : { visibility: news.visibility }),
289
+ labels: { ...news.labels, ...owner },
290
+ });
291
+ sandbox = created.sandbox;
292
+ }
293
+ // Sync — a stopped program sandbox is resumed, so the program can run.
294
+ if (program && sandbox.state === "archived") {
295
+ yield* session.note(`Resuming sandbox ${name}...`);
296
+ const resumed = yield* boat.resumeSandbox({ sandboxId: sandbox.id });
297
+ sandbox = resumed.sandbox ?? sandbox;
298
+ }
299
+ // Sync — name, subdomain and auto-stop, against what Boat reports.
300
+ const ttlChanged = ttlSeconds !== undefined && (sandbox.ttlSeconds ?? null) !== ttlSeconds;
301
+ const subdomainChanged = news.subdomain !== undefined && sandbox.subdomain !== news.subdomain;
302
+ if (sandbox.name !== name || ttlChanged || subdomainChanged) {
303
+ const updated = yield* boat.updateSandbox({
304
+ sandboxId: sandbox.id,
305
+ ...(sandbox.name !== name ? { name } : {}),
306
+ ...(ttlChanged ? { ttlSeconds } : {}),
307
+ ...(subdomainChanged ? { subdomain: news.subdomain } : {}),
308
+ });
309
+ sandbox = updated.sandbox ?? sandbox;
310
+ }
311
+ // Sync — visibility, against what Boat reports.
312
+ if (news.visibility !== undefined &&
313
+ (sandbox.visibility ?? "private") !== news.visibility) {
314
+ yield* session.note(`Making sandbox ${name} ${news.visibility === "shared" ? "shared" : "private"}...`);
315
+ if (news.visibility === "shared") {
316
+ const shared = yield* boat.shareSandbox({ sandboxId: sandbox.id });
317
+ if (shared.restartRequired) {
318
+ yield* session.note(`Sandbox ${name} is shared from its next start: stop and resume it to share it now.`);
319
+ }
320
+ }
321
+ else {
322
+ yield* boat.unshareSandbox({ sandboxId: sandbox.id });
323
+ }
324
+ sandbox = (yield* observeSandbox(sandbox.id)) ?? sandbox;
325
+ }
326
+ const attrs = toAttributes(sandbox);
327
+ if (news.main === undefined)
328
+ return attrs;
329
+ // Deploy the program into the running sandbox, then host its port.
330
+ const port = news.port ?? DEFAULT_PORT;
331
+ const state = yield* deployProgram({
332
+ id,
333
+ sandboxId: sandbox.id,
334
+ source: { ...news, main: news.main },
335
+ port,
336
+ env: { ...bindingEnv(bindings), ...alchemyEnv, PORT: String(port), ...news.env },
337
+ // A replaced sandbox runs nothing yet.
338
+ previous: output?.sandboxId === sandbox.id ? output.program : undefined,
339
+ note: session.note,
340
+ });
341
+ const hosted = yield* boat.hostPort({
342
+ sandboxId: sandbox.id,
343
+ port,
344
+ public: news.public ?? false,
345
+ });
346
+ const parsed = hosted.url === undefined ? undefined : new URL(hosted.url);
347
+ const token = parsed?.searchParams.get("_token") ?? undefined;
348
+ return {
349
+ ...attrs,
350
+ url: parsed?.origin,
351
+ urlToken: token === undefined ? undefined : Redacted.make(token),
352
+ program: state,
353
+ };
354
+ }),
355
+ delete: Effect.fn(function* ({ output }) {
356
+ yield* deleteSandbox(output.sandboxId);
357
+ }),
358
+ // Declared sandboxes and the ones bindings create at run time both
359
+ // carry `alchemy.stack`, so `nuke` finds both.
360
+ list: Effect.fn(function* () {
361
+ const all = yield* Stream.runCollect(boat.listSandboxes.items({ limit: 100 }));
362
+ return Array.from(all)
363
+ .filter((sandbox) => sandbox.state !== "cancelled" && hasAnyAlchemyLabel(sandbox.labels))
364
+ .map(toAttributes);
365
+ }),
366
+ };
367
+ }));
368
+ //# sourceMappingURL=Sandbox.js.map