@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
package/src/ApiKey.ts ADDED
@@ -0,0 +1,250 @@
1
+ import * as boat from "@distilled.cloud/boat-dev/boat";
2
+ import { Unowned } from "alchemy/AdoptPolicy";
3
+ import { 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 * as Redacted from "effect/Redacted";
8
+ import { boatName, isGeneratedName } from "./internal/Ownership.ts";
9
+ import type { Providers } from "./Providers.ts";
10
+
11
+ /**
12
+ * What a key may do. `read-only` inspects; `full-sandbox` operates
13
+ * sandboxes without creating, resuming, forking or deleting them; `ci` adds
14
+ * create, resume, fork, and delete of the sandboxes the key created;
15
+ * `admin` is everything.
16
+ */
17
+ export type ApiKeyPreset = "read-only" | "full-sandbox" | "ci" | "admin";
18
+
19
+ /**
20
+ * One permission. `sandbox.delete-own` deletes only the sandboxes the key
21
+ * created; `host` hosts a port; `account.admin` covers billing, teams,
22
+ * webhooks and keys.
23
+ * @see https://docs.boat.dev/api-keys/scopes
24
+ */
25
+ export type ApiKeyAction =
26
+ | "sandbox.create"
27
+ | "sandbox.read"
28
+ | "sandbox.update"
29
+ | "sandbox.stop"
30
+ | "sandbox.resume"
31
+ | "sandbox.fork"
32
+ | "sandbox.delete"
33
+ | "sandbox.delete-own"
34
+ | "agent.prompt"
35
+ | "exec"
36
+ | "file.read"
37
+ | "file.write"
38
+ | "ssh"
39
+ | "desktop"
40
+ | "host"
41
+ | "snapshot.read"
42
+ | "snapshot.write"
43
+ | "environment.read"
44
+ | "environment.write"
45
+ | "account.read"
46
+ | "account.admin"
47
+ | "*";
48
+
49
+ export interface ApiKeyProps {
50
+ /**
51
+ * Key name. If omitted, a unique name is generated from the stack, stage
52
+ * and logical ID. Changing it replaces the key.
53
+ */
54
+ name?: string;
55
+ /**
56
+ * A preset set of actions. Use either `preset` or `actions`; with
57
+ * neither, the key is an `admin` key. Changing it replaces the key.
58
+ */
59
+ preset?: ApiKeyPreset;
60
+ /** The actions the key may take. Changing them replaces the key. */
61
+ actions?: ApiKeyAction[];
62
+ /**
63
+ * The only sandboxes the key reaches, by id. Omit to reach every
64
+ * sandbox. Changing them replaces the key.
65
+ */
66
+ sandboxIds?: string[];
67
+ /**
68
+ * Environments whose sandboxes the key reaches, by id. Changing them
69
+ * replaces the key.
70
+ */
71
+ environmentIds?: string[];
72
+ /**
73
+ * How long the key works: `90d`, `24h`, a number of seconds, up to
74
+ * `365d`, or `never` for a key that works until it is revoked. A key
75
+ * can't outlive the key that creates it: omitted, the key never expires
76
+ * when the deployer's key doesn't, and otherwise expires with it.
77
+ * Changing it replaces the key.
78
+ */
79
+ ttl?: string;
80
+ }
81
+
82
+ export interface ApiKeyAttributes {
83
+ /** Boat's key id. */
84
+ apiKeyId: string;
85
+ name: string;
86
+ /** The key (`boat_…`), sent as a bearer token. Boat shows it once; Alchemy keeps it in state. */
87
+ key: Redacted.Redacted<string>;
88
+ /** The key's first characters, as the dashboard shows them. */
89
+ keyPrefix: string;
90
+ /** The key's last four characters. */
91
+ keyLastFour: string;
92
+ /** When the key stops working, or `undefined` for a key that never expires. */
93
+ expiresAt: string | undefined;
94
+ }
95
+
96
+ export interface ApiKey extends Resource<
97
+ "Boat.ApiKey",
98
+ ApiKeyProps,
99
+ ApiKeyAttributes,
100
+ never,
101
+ Providers
102
+ > {}
103
+
104
+ /**
105
+ * A scoped Boat API key: the actions it may take, and the sandboxes or
106
+ * environments it reaches. Give each app or CI system its own key with
107
+ * only what it needs.
108
+ *
109
+ * Boat shows a key once, when it is created, so Alchemy keeps it in state
110
+ * as a `Redacted` value. Changing any prop creates a new key and revokes
111
+ * the old one; destroying the resource revokes it.
112
+ *
113
+ * Creating a key needs a key with `account.admin`, such as the one
114
+ * `alchemy profile edit --add Boat` signs in with. A new key cannot have
115
+ * more actions, more sandboxes or a later expiry than the key creating it.
116
+ * The key a browser sign-in gets lasts a year, so keys it creates expire
117
+ * with it. An expired key is minted again on the next deploy.
118
+ *
119
+ * ### Creating an API key
120
+ * **Example:** A key for CI that manages the sandboxes it creates
121
+ * ```typescript
122
+ * export const Ci = Boat.ApiKey("Ci", { preset: "ci", ttl: "90d" });
123
+ * ```
124
+ *
125
+ * **Example:** A key that only runs commands in one sandbox
126
+ * ```typescript
127
+ * const box = yield* Boat.Sandbox("Box", {});
128
+ * const key = yield* Boat.ApiKey("BoxExec", {
129
+ * actions: ["sandbox.read", "exec", "file.read", "file.write"],
130
+ * sandboxIds: [box.sandboxId],
131
+ * });
132
+ * ```
133
+ *
134
+ * @resource
135
+ * @product API key
136
+ */
137
+ export const ApiKey = Resource<ApiKey>("Boat.ApiKey");
138
+
139
+ const redact = (value: string | Redacted.Redacted<string>) =>
140
+ Redacted.isRedacted(value) ? value : Redacted.make(value);
141
+
142
+ const findById = (apiKeyId: string) =>
143
+ boat
144
+ .listApiKeys({})
145
+ .pipe(Effect.map((listed) => listed.apiKeys.find((key) => key.id === apiKeyId)));
146
+
147
+ /** The props that decide what a key is; Boat can't change any of them on a key. */
148
+ const scopeOf = (props: ApiKeyProps) =>
149
+ JSON.stringify({
150
+ name: props.name,
151
+ preset: props.preset,
152
+ actions: props.actions === undefined ? undefined : [...props.actions].sort(),
153
+ sandboxIds: props.sandboxIds === undefined ? undefined : [...props.sandboxIds].sort(),
154
+ environmentIds:
155
+ props.environmentIds === undefined ? undefined : [...props.environmentIds].sort(),
156
+ ttl: props.ttl ?? "never",
157
+ });
158
+
159
+ const attributesOf = (key: boat.ApiKey, secret: Redacted.Redacted<string>): ApiKeyAttributes => ({
160
+ apiKeyId: key.id,
161
+ name: key.name,
162
+ key: secret,
163
+ keyPrefix: key.keyPrefix,
164
+ keyLastFour: key.keyLastFour,
165
+ expiresAt: key.expiresAt ?? undefined,
166
+ });
167
+
168
+ /**
169
+ * Seconds until the signing key expires, from Boat's refusal ("…signed
170
+ * with an API key that expires 2027-11-07T16:21:12.757Z…"), less a minute;
171
+ * Boat's 90-day default when the message names no date.
172
+ */
173
+ const ttlUntilSignerExpires = (message: string | undefined) => {
174
+ const expires = /expires (\S+?Z)/.exec(message ?? "")?.[1];
175
+ const seconds =
176
+ expires === undefined ? Number.NaN : (Date.parse(expires) - Date.now()) / 1000 - 60;
177
+ return Number.isFinite(seconds) && seconds > 60 ? String(Math.floor(seconds)) : "90d";
178
+ };
179
+
180
+ /** Revoke a key; revoking one that is gone succeeds. */
181
+ export const revokeApiKey = (apiKeyId: string) =>
182
+ boat.revokeApiKey({ apiKeyId }).pipe(
183
+ Effect.asVoid,
184
+ Effect.catchTag("NotFound", () => Effect.void),
185
+ );
186
+
187
+ export const ApiKeyProvider = () =>
188
+ Provider.effect(
189
+ ApiKey,
190
+ Effect.gen(function* () {
191
+ return {
192
+ stables: ["apiKeyId", "key", "keyPrefix", "keyLastFour"],
193
+ diff: Effect.fn(function* ({ news, olds }) {
194
+ if (!isResolved(news) || olds === undefined) return undefined;
195
+ if (scopeOf(news) !== scopeOf(olds)) return { action: "replace" } as const;
196
+ return undefined;
197
+ }),
198
+ read: Effect.fn(function* ({ olds, output }) {
199
+ // Boat never returns a key's value after creation, so a key is
200
+ // only usable through state.
201
+ if (output === undefined) return undefined;
202
+ const key = yield* findById(output.apiKeyId);
203
+ if (key === undefined || key.expired === true) return undefined;
204
+ const attrs = attributesOf(key, output.key);
205
+ return olds !== undefined || isGeneratedName(key.name) ? attrs : Unowned(attrs);
206
+ }),
207
+ reconcile: Effect.fn(function* ({ id, news, output, session }) {
208
+ const name = yield* boatName(id, news.name ?? output?.name);
209
+
210
+ // Observe — a key is usable only while its value is in state.
211
+ const observed = output === undefined ? undefined : yield* findById(output.apiKeyId);
212
+ if (output !== undefined && observed !== undefined && observed.expired !== true) {
213
+ return attributesOf(observed, output.key);
214
+ }
215
+
216
+ // Ensure
217
+ yield* session.note(`Creating API key ${name}...`);
218
+ const create = (ttl: string) =>
219
+ boat.createScopedApiKey({
220
+ name,
221
+ ttl,
222
+ ...(news.preset === undefined ? {} : { preset: news.preset }),
223
+ ...(news.actions === undefined ? {} : { actions: news.actions }),
224
+ ...(news.sandboxIds === undefined ? {} : { sandboxIds: news.sandboxIds }),
225
+ ...(news.environmentIds === undefined ? {} : { environmentIds: news.environmentIds }),
226
+ });
227
+ const created = yield* create(news.ttl ?? "never").pipe(
228
+ // Without a `ttl`, the longest Boat allows: as long as the key
229
+ // signing the request, whose expiry Boat names in the refusal.
230
+ Effect.catchTag("ApiKeyOutlivesSigner", (error) =>
231
+ news.ttl === undefined
232
+ ? Effect.suspend(() => create(ttlUntilSignerExpires(error.message)))
233
+ : Effect.fail(error),
234
+ ),
235
+ );
236
+ return attributesOf(created.apiKey, redact(created.secret));
237
+ }),
238
+ delete: Effect.fn(function* ({ output }) {
239
+ yield* revokeApiKey(output.apiKeyId);
240
+ }),
241
+ // For nuke. Listed keys carry no value; delete needs only the id.
242
+ list: Effect.fn(function* () {
243
+ const listed = yield* boat.listApiKeys({});
244
+ return listed.apiKeys
245
+ .filter((key) => isGeneratedName(key.name))
246
+ .map((key) => attributesOf(key, Redacted.make("")));
247
+ }),
248
+ };
249
+ }),
250
+ );