@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.
- package/LICENSE +201 -0
- package/README.md +66 -4
- package/lib/AiProxy.d.ts +89 -0
- package/lib/AiProxy.d.ts.map +1 -0
- package/lib/AiProxy.js +125 -0
- package/lib/AiProxy.js.map +1 -0
- package/lib/ApiKey.d.ts +105 -0
- package/lib/ApiKey.d.ts.map +1 -0
- package/lib/ApiKey.js +134 -0
- package/lib/ApiKey.js.map +1 -0
- package/lib/AuthProvider.d.ts +71 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +322 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/BoatBindings.d.ts +52 -0
- package/lib/BoatBindings.d.ts.map +1 -0
- package/lib/BoatBindings.js +104 -0
- package/lib/BoatBindings.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +28 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +56 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +3 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/ConnectSandboxHttp.d.ts +21 -0
- package/lib/ConnectSandboxHttp.d.ts.map +1 -0
- package/lib/ConnectSandboxHttp.js +46 -0
- package/lib/ConnectSandboxHttp.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateSandbox.d.ts +134 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +3 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/CreateSandboxHttp.d.ts +21 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +93 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CredentialProfile.d.ts +157 -0
- package/lib/CredentialProfile.d.ts.map +1 -0
- package/lib/CredentialProfile.js +245 -0
- package/lib/CredentialProfile.js.map +1 -0
- package/lib/Credentials.d.ts +15 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +23 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Environment.d.ts +219 -0
- package/lib/Environment.d.ts.map +1 -0
- package/lib/Environment.js +273 -0
- package/lib/Environment.js.map +1 -0
- package/lib/Errors.d.ts +50 -0
- package/lib/Errors.d.ts.map +1 -0
- package/lib/Errors.js +17 -0
- package/lib/Errors.js.map +1 -0
- package/lib/Providers.d.ts +33 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +47 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Sandbox.d.ts +205 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +368 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +159 -0
- package/lib/SandboxClient.d.ts.map +1 -0
- package/lib/SandboxClient.js +1 -0
- package/lib/SandboxClient.js.map +1 -0
- package/lib/SandboxEvents.d.ts +111 -0
- package/lib/SandboxEvents.d.ts.map +1 -0
- package/lib/SandboxEvents.js +60 -0
- package/lib/SandboxEvents.js.map +1 -0
- package/lib/SandboxEventsLive.d.ts +13 -0
- package/lib/SandboxEventsLive.d.ts.map +1 -0
- package/lib/SandboxEventsLive.js +63 -0
- package/lib/SandboxEventsLive.js.map +1 -0
- package/lib/Snapshot.d.ts +123 -0
- package/lib/Snapshot.d.ts.map +1 -0
- package/lib/Snapshot.js +186 -0
- package/lib/Snapshot.js.map +1 -0
- package/lib/Webhook.d.ts +56 -0
- package/lib/Webhook.d.ts.map +1 -0
- package/lib/Webhook.js +98 -0
- package/lib/Webhook.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/Declared.d.ts +34 -0
- package/lib/internal/Declared.d.ts.map +1 -0
- package/lib/internal/Declared.js +38 -0
- package/lib/internal/Declared.js.map +1 -0
- package/lib/internal/Org.d.ts +22 -0
- package/lib/internal/Org.d.ts.map +1 -0
- package/lib/internal/Org.js +23 -0
- package/lib/internal/Org.js.map +1 -0
- package/lib/internal/Ownership.d.ts +29 -0
- package/lib/internal/Ownership.d.ts.map +1 -0
- package/lib/internal/Ownership.js +50 -0
- package/lib/internal/Ownership.js.map +1 -0
- package/lib/internal/Program.d.ts +69 -0
- package/lib/internal/Program.d.ts.map +1 -0
- package/lib/internal/Program.js +178 -0
- package/lib/internal/Program.js.map +1 -0
- package/lib/internal/Ref.d.ts +14 -0
- package/lib/internal/Ref.d.ts.map +1 -0
- package/lib/internal/Ref.js +7 -0
- package/lib/internal/Ref.js.map +1 -0
- package/lib/internal/SandboxHandle.d.ts +17 -0
- package/lib/internal/SandboxHandle.d.ts.map +1 -0
- package/lib/internal/SandboxHandle.js +249 -0
- package/lib/internal/SandboxHandle.js.map +1 -0
- package/lib/internal/WebhookDelivery.d.ts +16 -0
- package/lib/internal/WebhookDelivery.d.ts.map +1 -0
- package/lib/internal/WebhookDelivery.js +109 -0
- package/lib/internal/WebhookDelivery.js.map +1 -0
- package/package.json +67 -3
- package/src/AiProxy.ts +171 -0
- package/src/ApiKey.ts +250 -0
- package/src/AuthProvider.ts +472 -0
- package/src/BoatBindings.ts +136 -0
- package/src/Bootstrap.ts +55 -0
- package/src/ConnectSandbox.ts +60 -0
- package/src/ConnectSandboxHttp.ts +59 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateSandbox.ts +143 -0
- package/src/CreateSandboxHttp.ts +115 -0
- package/src/CredentialProfile.ts +350 -0
- package/src/Credentials.ts +33 -0
- package/src/Environment.ts +416 -0
- package/src/Errors.ts +34 -0
- package/src/Providers.ts +71 -0
- package/src/Sandbox.ts +647 -0
- package/src/SandboxClient.ts +199 -0
- package/src/SandboxEvents.ts +125 -0
- package/src/SandboxEventsLive.ts +88 -0
- package/src/Snapshot.ts +284 -0
- package/src/Webhook.ts +163 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +18 -0
- package/src/internal/Declared.ts +87 -0
- package/src/internal/Org.ts +31 -0
- package/src/internal/Ownership.ts +75 -0
- package/src/internal/Program.ts +250 -0
- package/src/internal/Ref.ts +15 -0
- package/src/internal/SandboxHandle.ts +425 -0
- 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
|
+
);
|