@alchemy.run/boat 0.0.0 → 2.0.0-beta.87

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 (154) 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 +173 -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 +238 -0
  114. package/lib/internal/SandboxHandle.js.map +1 -0
  115. package/lib/internal/SavedSnapshot.d.ts +10 -0
  116. package/lib/internal/SavedSnapshot.d.ts.map +1 -0
  117. package/lib/internal/SavedSnapshot.js +22 -0
  118. package/lib/internal/SavedSnapshot.js.map +1 -0
  119. package/lib/internal/WebhookDelivery.d.ts +16 -0
  120. package/lib/internal/WebhookDelivery.d.ts.map +1 -0
  121. package/lib/internal/WebhookDelivery.js +109 -0
  122. package/lib/internal/WebhookDelivery.js.map +1 -0
  123. package/package.json +67 -3
  124. package/src/AiProxy.ts +171 -0
  125. package/src/ApiKey.ts +250 -0
  126. package/src/AuthProvider.ts +472 -0
  127. package/src/BoatBindings.ts +136 -0
  128. package/src/Bootstrap.ts +55 -0
  129. package/src/ConnectSandbox.ts +60 -0
  130. package/src/ConnectSandboxHttp.ts +59 -0
  131. package/src/Contributions.ts +9 -0
  132. package/src/CreateSandbox.ts +143 -0
  133. package/src/CreateSandboxHttp.ts +115 -0
  134. package/src/CredentialProfile.ts +350 -0
  135. package/src/Credentials.ts +33 -0
  136. package/src/Environment.ts +416 -0
  137. package/src/Errors.ts +34 -0
  138. package/src/Providers.ts +71 -0
  139. package/src/Sandbox.ts +647 -0
  140. package/src/SandboxClient.ts +200 -0
  141. package/src/SandboxEvents.ts +125 -0
  142. package/src/SandboxEventsLive.ts +88 -0
  143. package/src/Snapshot.ts +264 -0
  144. package/src/Webhook.ts +163 -0
  145. package/src/globals.d.ts +2 -0
  146. package/src/index.ts +18 -0
  147. package/src/internal/Declared.ts +87 -0
  148. package/src/internal/Org.ts +31 -0
  149. package/src/internal/Ownership.ts +75 -0
  150. package/src/internal/Program.ts +250 -0
  151. package/src/internal/Ref.ts +15 -0
  152. package/src/internal/SandboxHandle.ts +407 -0
  153. package/src/internal/SavedSnapshot.ts +34 -0
  154. package/src/internal/WebhookDelivery.ts +179 -0
@@ -0,0 +1,22 @@
1
+ import * as boat from "@distilled.cloud/boat-dev/boat";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schedule from "effect/Schedule";
4
+ import { SnapshotFailed } from "../Errors.js";
5
+ /**
6
+ * How long to wait for a save. A save runs in the background, and a running
7
+ * sandbox takes a fresh capture first, which can take minutes.
8
+ */
9
+ const SAVE_TIMEOUT = "10 minutes";
10
+ const poll = { schedule: Schedule.spaced("5 seconds"), times: 120 };
11
+ /** Polls until Boat reports the snapshot `ready` (succeeds) or `failed` (fails). */
12
+ export const awaitSaved = (name) => boat.getSavedSnapshot({ name }).pipe(Effect.map((got) => got.snapshot), Effect.repeat({ ...poll, until: (snapshot) => snapshot.status !== "saving" }), Effect.flatMap((snapshot) => snapshot.status === "ready"
13
+ ? Effect.succeed(snapshot)
14
+ : new SnapshotFailed({
15
+ snapshot: name,
16
+ message: snapshot.status === "failed"
17
+ ? `Boat could not save snapshot ${name}: ${snapshot.error ?? "unknown error"}`
18
+ : `Boat snapshot ${name} was still saving after ${SAVE_TIMEOUT}`,
19
+ })));
20
+ /** Retries a saved-snapshot write while an earlier save under the same name is running. */
21
+ export const whileSaving = (effect) => effect.pipe(Effect.retry({ ...poll, while: (error) => error._tag === "SaveInProgress" }));
22
+ //# sourceMappingURL=SavedSnapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SavedSnapshot.js","sources":["../../src/internal/SavedSnapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,gCAAgC,CAAC;AACvD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;GAGG;AACH,MAAM,YAAY,GAAG,YAAY,CAAC;AAClC,MAAM,IAAI,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,GAAG,EAAW,CAAC;AAE7E,oFAAoF;AACpF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE,CACzC,IAAI,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAClC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,EACjC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC,EAC7E,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAC1B,QAAQ,CAAC,MAAM,KAAK,OAAO;IACzB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;IAC1B,CAAC,CAAC,IAAI,cAAc,CAAC;QACjB,QAAQ,EAAE,IAAI;QACd,OAAO,EACL,QAAQ,CAAC,MAAM,KAAK,QAAQ;YAC1B,CAAC,CAAC,gCAAgC,IAAI,KAAK,QAAQ,CAAC,KAAK,IAAI,eAAe,EAAE;YAC9E,CAAC,CAAC,iBAAiB,IAAI,2BAA2B,YAAY,EAAE;KACrE,CAAC,CACP,CACF,CAAC;AAEJ,2FAA2F;AAC3F,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,MAA8B,EAC9B,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type RuntimeContext } from "alchemy/RuntimeContext";
2
+ import * as Effect from "effect/Effect";
3
+ import type { BoatAuth } from "../BoatBindings.ts";
4
+ import type { SandboxEvent } from "../SandboxEvents.ts";
5
+ /** How old a delivery's timestamp may be before it is refused. */
6
+ export declare const MAX_AGE_SECONDS = 300;
7
+ /** The largest delivery accepted. */
8
+ export declare const MAX_BODY_BYTES: number;
9
+ export interface Scope {
10
+ readonly auth: BoatAuth;
11
+ readonly owner: Effect.Effect<Readonly<Record<string, string>>>;
12
+ }
13
+ export declare const handleDelivery: <Req>(request: Request, secret: Effect.Effect<string | undefined>, types: ReadonlySet<string>, scope: Scope | undefined, process: (event: SandboxEvent) => Effect.Effect<void, never, Req>) => Effect.Effect<Response, never, Req | RuntimeContext>;
14
+ /** The bound secret as a string: a `Redacted`, its serialized form, or a plain string. */
15
+ export declare const asSecret: (raw: unknown) => string | undefined;
16
+ //# sourceMappingURL=WebhookDelivery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebhookDelivery.d.ts","sources":["../../src/internal/WebhookDelivery.ts"],"names":[],"mappings":"AAKA,OAAO,EAAoB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE/E,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,kEAAkE;AAClE,eAAO,MAAM,eAAe,MAAM,CAAC;AAEnC,qCAAqC;AACrC,eAAO,MAAM,cAAc,QAAc,CAAC;AAE1C,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CACjE;AAED,eAAO,MAAM,cAAc,GAAI,GAAG,WACvB,OAAO,UACR,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC,SAClC,WAAW,CAAC,MAAM,CAAC,SACnB,KAAK,GAAG,SAAS,WACf,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,KAChE,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,cAAc,CAyElD,CAAC;AAEL,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,QAAS,OAAO,KAAG,MAAM,GAAG,SAUhD,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * INTERNAL — handles one delivery of Boat's webhook on a host: checks the
3
+ * signature, the timestamp and the payload, then scopes and dispatches it.
4
+ */
5
+ import * as boat from "@distilled.cloud/boat-dev/boat";
6
+ import { isRedactedMarker } from "alchemy/RuntimeContext";
7
+ import * as Clock from "effect/Clock";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Redacted from "effect/Redacted";
10
+ import { ALCHEMY_LABEL_PREFIX } from "./Ownership.js";
11
+ /** How old a delivery's timestamp may be before it is refused. */
12
+ export const MAX_AGE_SECONDS = 300;
13
+ /** The largest delivery accepted. */
14
+ export const MAX_BODY_BYTES = 1024 * 1024;
15
+ export const handleDelivery = (request, secret, types, scope, process) => Effect.gen(function* () {
16
+ if (request.method !== "POST")
17
+ return new Response("method not allowed", { status: 405 });
18
+ // Boat's events are a few hundred bytes; refuse anything large before reading it.
19
+ if (Number(request.headers.get("content-length") ?? "0") > MAX_BODY_BYTES) {
20
+ return new Response("payload too large", { status: 413 });
21
+ }
22
+ const body = yield* Effect.tryPromise(() => request.arrayBuffer()).pipe(Effect.option);
23
+ if (body._tag === "None")
24
+ return new Response("unreadable body", { status: 400 });
25
+ if (body.value.byteLength > MAX_BODY_BYTES) {
26
+ return new Response("payload too large", { status: 413 });
27
+ }
28
+ // The signature covers the delivery id, the timestamp and the raw body
29
+ // bytes; a stale timestamp is refused, so a captured delivery can't be
30
+ // replayed later.
31
+ const delivery = request.headers.get("x-ascii-delivery") ?? "";
32
+ const timestamp = request.headers.get("x-ascii-timestamp") ?? "";
33
+ const signature = (request.headers.get("x-ascii-signature") ?? "").replace(/^v1=/, "");
34
+ const now = yield* Clock.currentTimeMillis;
35
+ const fresh = /^\d{1,12}$/.test(timestamp) &&
36
+ Math.abs(Number(now) / 1000 - Number(timestamp)) <= MAX_AGE_SECONDS;
37
+ const valid = delivery !== "" &&
38
+ fresh &&
39
+ (yield* verifySignature((yield* secret) ?? "", `${delivery}.${timestamp}.`, body.value, signature));
40
+ if (!valid)
41
+ return new Response("invalid signature", { status: 401 });
42
+ const parsed = yield* Effect.try(() => JSON.parse(new TextDecoder().decode(body.value))).pipe(Effect.orElseSucceed(() => undefined));
43
+ if (parsed === undefined ||
44
+ parsed.id !== delivery ||
45
+ typeof parsed.type !== "string" ||
46
+ typeof parsed.data?.sandbox?.id !== "string") {
47
+ return new Response("invalid payload", { status: 400 });
48
+ }
49
+ // An event this host didn't subscribe to: acknowledged, not handled.
50
+ if (!types.has(parsed.type))
51
+ return new Response(null, { status: 202 });
52
+ let event = {
53
+ ...parsed,
54
+ attempt: Number(request.headers.get("x-ascii-attempt") ?? "1") || 1,
55
+ };
56
+ if (scope !== undefined) {
57
+ const lookup = yield* labelsOf(scope.auth, parsed.data.sandbox.id);
58
+ // Boat couldn't be asked: answer 503 so it delivers again, rather than
59
+ // drop an event that may be ours.
60
+ if (lookup._tag === "Unavailable")
61
+ return new Response("try again", { status: 503 });
62
+ const labels = lookup._tag === "Found" ? lookup.labels : undefined;
63
+ const owner = yield* scope.owner;
64
+ const ours = labels !== undefined &&
65
+ ["stack", "stage"].every((key) => owner[`${ALCHEMY_LABEL_PREFIX}${key}`] !== undefined &&
66
+ labels[`${ALCHEMY_LABEL_PREFIX}${key}`] === owner[`${ALCHEMY_LABEL_PREFIX}${key}`]);
67
+ // Another stack's sandbox, or one already deleted: acknowledged, not handled.
68
+ if (!ours)
69
+ return new Response(null, { status: 202 });
70
+ event = { ...event, data: { ...event.data, sandbox: { ...event.data.sandbox, labels } } };
71
+ }
72
+ yield* process(event);
73
+ return new Response(null, { status: 202 });
74
+ });
75
+ /** The bound secret as a string: a `Redacted`, its serialized form, or a plain string. */
76
+ export const asSecret = (raw) => {
77
+ if (raw === undefined || raw === null || raw === "")
78
+ return undefined;
79
+ if (Redacted.isRedacted(raw)) {
80
+ const value = Redacted.value(raw);
81
+ return typeof value === "string" && value.length > 0 ? value : undefined;
82
+ }
83
+ if (isRedactedMarker(raw) && typeof raw.value === "string") {
84
+ return raw.value.length > 0 ? raw.value : undefined;
85
+ }
86
+ return typeof raw === "string" ? raw : undefined;
87
+ };
88
+ /** A sandbox's labels; `Gone` when Boat no longer has it, `Unavailable` when Boat couldn't be asked. */
89
+ const labelsOf = (auth, sandboxId) => auth.authorize(boat.getSandbox({ sandboxId })).pipe(Effect.map((got) => ({
90
+ _tag: "Found",
91
+ labels: Object.fromEntries(Object.entries(got.sandbox?.labels ?? {}).filter((entry) => typeof entry[1] === "string")),
92
+ })), Effect.catchTag("NotFound", () => Effect.succeed({ _tag: "Gone" })), Effect.orElseSucceed(() => ({ _tag: "Unavailable" })));
93
+ const fromHex = (hex) => Uint8Array.from(hex.match(/../g) ?? [], (byte) => Number.parseInt(byte, 16));
94
+ /**
95
+ * Boat's signature: hex HMAC-SHA256 of `<delivery>.<timestamp>.` followed
96
+ * by the raw body bytes. Web Crypto checks it (in constant time), so it runs
97
+ * on Workers without `nodejs_compat`.
98
+ */
99
+ const verifySignature = (secret, prefix, body, signature) => Effect.gen(function* () {
100
+ if (secret === "" || !/^[a-f0-9]{64}$/i.test(signature))
101
+ return false;
102
+ const head = new TextEncoder().encode(prefix);
103
+ const signed = new Uint8Array(head.byteLength + body.byteLength);
104
+ signed.set(head, 0);
105
+ signed.set(new Uint8Array(body), head.byteLength);
106
+ const valid = yield* Effect.tryPromise(() => crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"])).pipe(Effect.flatMap((key) => Effect.tryPromise(() => crypto.subtle.verify("HMAC", key, fromHex(signature.toLowerCase()), signed))), Effect.orElseSucceed(() => false));
107
+ return valid;
108
+ });
109
+ //# sourceMappingURL=WebhookDelivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebhookDelivery.js","sources":["../../src/internal/WebhookDelivery.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alchemy.run/boat",
3
- "version": "0.0.0",
3
+ "version": "2.0.0-beta.87",
4
4
  "description": "Boat sandboxes for Alchemy.",
5
5
  "keywords": [
6
6
  "alchemy",
@@ -9,12 +9,76 @@
9
9
  "infrastructure-as-code",
10
10
  "sandbox"
11
11
  ],
12
+ "homepage": "https://alchemy.run",
13
+ "bugs": {
14
+ "url": "https://github.com/alchemy-run/alchemy/issues"
15
+ },
12
16
  "license": "Apache-2.0",
13
17
  "author": "Sam Goodwin <sam@alchemy.run>",
14
- "homepage": "https://alchemy.run",
15
18
  "repository": {
16
19
  "type": "git",
17
20
  "url": "git+https://github.com/alchemy-run/alchemy.git",
18
21
  "directory": "packages/boat"
22
+ },
23
+ "files": [
24
+ "lib",
25
+ "src"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "exports": {
30
+ ".": {
31
+ "types": "./lib/index.d.ts",
32
+ "bun": "./src/index.ts",
33
+ "worker": "./src/index.ts",
34
+ "default": "./lib/index.js"
35
+ },
36
+ "./Bootstrap": {
37
+ "types": "./src/Bootstrap.ts",
38
+ "bun": "./src/Bootstrap.ts",
39
+ "default": "./src/Bootstrap.ts"
40
+ },
41
+ "./internal/*": null,
42
+ "./*": {
43
+ "types": "./lib/*.d.ts",
44
+ "bun": "./src/*.ts",
45
+ "worker": "./src/*.ts",
46
+ "default": "./lib/*.js"
47
+ },
48
+ "./package.json": "./package.json"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "dependencies": {
54
+ "@distilled.cloud/boat-dev": "1.0.0-rc.20",
55
+ "@distilled.cloud/core": "1.0.0-rc.20",
56
+ "rolldown": "1.2.16"
57
+ },
58
+ "devDependencies": {
59
+ "@effect/platform-node": "^4.1.4",
60
+ "@types/bun": "1.4.3",
61
+ "alchemy": "2.0.0-beta.87",
62
+ "effect": "^4.1.4",
63
+ "typescript": "^7.1.0-dev.20261105.1"
64
+ },
65
+ "peerDependencies": {
66
+ "@effect/platform-node": "^4.1.4",
67
+ "alchemy": "2.0.0-beta.87",
68
+ "effect": "^4.1.4"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@effect/platform-node": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "alchemy": {
76
+ "contributes": [
77
+ "@alchemy.run/boat/Contributions"
78
+ ]
79
+ },
80
+ "scripts": {
81
+ "build": "pnpm -r --filter \"{.}...\" run build:package",
82
+ "build:package": "node scripts/build.ts"
19
83
  }
20
- }
84
+ }
package/src/AiProxy.ts ADDED
@@ -0,0 +1,171 @@
1
+ import * as boat from "@distilled.cloud/boat-dev/boat";
2
+ import * as Provider from "alchemy/Provider";
3
+ import { Resource } from "alchemy/Resource";
4
+ import * as Effect from "effect/Effect";
5
+ import { sameOrg, stackOrg } from "./internal/Org.ts";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ export interface AiProxyProps {
9
+ /**
10
+ * Whether the account's AI credentials proxy is on. While it is on, Boat
11
+ * bills the account $1 a day, and $1 per 1,000 proxied requests to the
12
+ * wallet each sandbox bills.
13
+ * @default true
14
+ */
15
+ enabled?: boolean;
16
+ }
17
+
18
+ export interface AiProxyAttributes {
19
+ /** The account the switch belongs to: you, or an organization. */
20
+ accountId: string;
21
+ /**
22
+ * The organization whose switch this is, as the stack's `org` named it;
23
+ * `undefined` for your own.
24
+ */
25
+ org: string | undefined;
26
+ /** Whether the proxy is on. */
27
+ enabled: boolean;
28
+ /** Where sandboxes send proxied requests. */
29
+ proxyUrl: string;
30
+ /**
31
+ * What the switch was before Alchemy first changed it. Destroying the
32
+ * resource puts it back.
33
+ */
34
+ initialEnabled: boolean;
35
+ }
36
+
37
+ export interface AiProxy extends Resource<
38
+ "Boat.AiProxy",
39
+ AiProxyProps,
40
+ AiProxyAttributes,
41
+ never,
42
+ Providers
43
+ > {}
44
+
45
+ /**
46
+ * The AI credentials proxy switch of a Boat account.
47
+ *
48
+ * With the proxy on, a sandbox gets placeholders in place of the keys of
49
+ * its credential profile, and the proxy's URL in place of each base URL.
50
+ * Boat swaps in the real key as a request passes through, so the key never
51
+ * enters the sandbox, and a copied placeholder stops working when the
52
+ * sandbox stops.
53
+ *
54
+ * :::caution
55
+ * The proxy is billed: $1 for each day it is on, and $1 per 1,000 proxied
56
+ * requests. It is one switch for a whole account, so turning it on changes
57
+ * every sandbox that uses one of the account's credential profiles, at its
58
+ * next start.
59
+ * :::
60
+ *
61
+ * The switch is yours, or an organization's. With an `org` on the stack
62
+ * (`Boat.providers({ org })`, or the profile's) it is the organization's,
63
+ * which only an owner can change. That is the same account that owns the
64
+ * stack's `Boat.CredentialProfile`s.
65
+ *
66
+ * The switch always exists, so nothing is created or deleted. Destroying
67
+ * the resource puts the switch back the way it was found.
68
+ *
69
+ * ### Turning the proxy on
70
+ * **Example:** Proxy the credentials of sandboxes you hand to users
71
+ * ```typescript
72
+ * export const MyProfile = Boat.CredentialProfile("MyProfile", {
73
+ * anthropic: Config.Redacted("ANTHROPIC_API_KEY"),
74
+ * });
75
+ *
76
+ * export const Proxy = Boat.AiProxy("Proxy", { enabled: true });
77
+ *
78
+ * export const Tenants = Boat.Environment("Tenants", {
79
+ * safeForThirdParties: true,
80
+ * credentialProfile: MyProfile,
81
+ * proxyAiCredentials: Proxy,
82
+ * });
83
+ * ```
84
+ *
85
+ * `proxyAiCredentials` takes the proxy itself, so the environment is updated
86
+ * after the switch, and proxies only while it is on. Boat rejects
87
+ * `proxyAiCredentials: true` while the proxy is off.
88
+ *
89
+ * ### Turning the proxy off
90
+ * **Example:** Keep it off
91
+ * ```typescript
92
+ * export const Proxy = Boat.AiProxy("Proxy", { enabled: false });
93
+ * ```
94
+ *
95
+ * @resource
96
+ * @product AI Proxy
97
+ */
98
+ export const AiProxy = Resource<AiProxy>("Boat.AiProxy");
99
+
100
+ /**
101
+ * The account a call addresses. Without an `org`, Boat answers for the
102
+ * active wallet, which may be an organization's, so your own switch is
103
+ * named explicitly.
104
+ */
105
+ const account = (org: string | undefined) => ({ org: org ?? "personal" });
106
+
107
+ export const AiProxyProvider = () =>
108
+ Provider.effect(
109
+ AiProxy,
110
+ Effect.gen(function* () {
111
+ return {
112
+ nuke: { singleton: true },
113
+ stables: ["accountId", "org", "initialEnabled"],
114
+ // Each account has its own switch.
115
+ diff: Effect.fn(function* ({ output }) {
116
+ if (output !== undefined && !sameOrg(output.org, yield* stackOrg)) {
117
+ return { action: "replace" } as const;
118
+ }
119
+ }),
120
+ // The switch always exists and belongs to nobody, so a cold read
121
+ // adopts it; what it shows then is what destroy restores.
122
+ read: Effect.fn(function* ({ output }) {
123
+ const org = output ? output.org : yield* stackOrg;
124
+ const observed = yield* boat.getAiProxy(account(org));
125
+ return {
126
+ accountId: observed.accountId,
127
+ org,
128
+ enabled: observed.enabled,
129
+ proxyUrl: observed.proxyUrl,
130
+ initialEnabled: output !== undefined ? output.initialEnabled : observed.enabled,
131
+ };
132
+ }),
133
+ reconcile: Effect.fn(function* ({ news, output, session }) {
134
+ const org = yield* stackOrg;
135
+ const desired = news.enabled ?? true;
136
+
137
+ // Observe.
138
+ const observed = yield* boat.getAiProxy(account(org));
139
+
140
+ // Capture what it was before the first change, for destroy.
141
+ const initialEnabled = output !== undefined ? output.initialEnabled : observed.enabled;
142
+
143
+ // Sync — only when it differs.
144
+ if (observed.enabled === desired) {
145
+ return {
146
+ accountId: observed.accountId,
147
+ org,
148
+ enabled: observed.enabled,
149
+ proxyUrl: observed.proxyUrl,
150
+ initialEnabled,
151
+ };
152
+ }
153
+ yield* session.note(`Turning the AI credentials proxy ${desired ? "on" : "off"}...`);
154
+ const updated = yield* boat.updateAiProxy({ ...account(org), enabled: desired });
155
+ return {
156
+ accountId: updated.accountId,
157
+ org,
158
+ enabled: updated.enabled,
159
+ proxyUrl: updated.proxyUrl,
160
+ initialEnabled,
161
+ };
162
+ }),
163
+ // Put the switch back the way it was found.
164
+ delete: Effect.fn(function* ({ output }) {
165
+ const observed = yield* boat.getAiProxy(account(output.org));
166
+ if (observed.enabled === output.initialEnabled) return;
167
+ yield* boat.updateAiProxy({ ...account(output.org), enabled: output.initialEnabled });
168
+ }),
169
+ };
170
+ }),
171
+ );