Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
22 kB · 624 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625import { measureDeployment } from "./deployment-timing.ts";import { ensureAttachmentBucket } from "./attachment-storage.ts";import { createAssetUpload, createScriptAssetUpload, createScriptSubdomain, getScriptSubdomain, getSubdomain, listScriptDeployments, listScriptVersions,} from "@distilled.cloud/cloudflare/workers";import * as Effect from "effect/Effect";import type { InstallationConfig } from "../configuration/customer.ts";import { parseInstallationConfig } from "../configuration/customer.ts";import type { Artifact } from "./artifact-types.ts";import { digest } from "./artifact.ts";import { CloudflareAccount } from "./cloudflare-account.ts";import { ReauthorizationRequired, ResourceConflict, SetupRequired, TemporarilyUnavailable,} from "./deployment-errors.ts";import { eq, fingerprint, id, object } from "./deployment-values.ts";import type { Installation } from "./installation-metadata.ts";import { bodyBytes } from "../server/http.ts";export type Binding = { type: string; name: string; class_name?: string; script_name?: string; namespace_id?: string; bucket_name?: string; text?: string; json?: string;};export type Settings = { bindings: Binding[]; [key: string]: unknown };
const token = (value: unknown): value is string => typeof value === "string" && value.length > 0 && value.length <= 16_384 && /^[A-Za-z0-9_.-]+$/.test(value);
export type WorkerUploadBindings = | { kind: "bootstrap"; secret: string } | { kind: "inherit"; versionId: string; bindings: Binding[]; metadata: Record<string, unknown>; };
export class WorkerAPI { constructor( private readonly client: CloudflareAccount, private readonly record: Installation, ) {} private get worker() { return { accountId: this.record.accountId, scriptName: this.record.resources.workerName, }; } private get script() { return `workers/scripts/${this.record.resources.workerName}`; } origin() { return Effect.gen({ self: this }, function* () { const result = yield* this.client.sdk( getSubdomain({ accountId: this.record.accountId }), ); if ( !object(result) || typeof result.subdomain !== "string" || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(result.subdomain) ) return yield* new SetupRequired(); return `https://${this.record.resources.workerName}.${result.subdomain}.workers.dev`; }); } settings() { return Effect.gen({ self: this }, function* () { const result = yield* this.client.request(`${this.script}/settings`, { allowNotFound: true, }); if (result === null) return null; if ( !object(result) || !Array.isArray(result.bindings) || result.bindings.length > 256 || result.bindings.some( (b) => !object(b) || typeof b.type !== "string" || typeof b.name !== "string", ) ) return yield* new ResourceConflict(); return result as Settings; }); } verifyWorker(settings: Settings, installationConfig: InstallationConfig) { return Effect.try({ try: () => { const bindings = settings.bindings; if (new Set(bindings.map((b) => b.name)).size !== bindings.length) throw new ResourceConflict(); const byName = (name: string, type: string) => { const binding = bindings.find((b) => b.name === name); if (binding?.type !== type) throw new ResourceConflict(); return binding; }; const installed: unknown = JSON.parse( byName("FLAREBOT_INSTALLATION", "plain_text").text!, ); if ( !eq(installed, installationConfig) || byName("FLAREBOT_MODE", "plain_text").text !== "customer-runtime" || byName("FLAREBOT_ENV", "plain_text").text !== "production" ) throw new ResourceConflict(); for (const [name, type] of [ ["ASSETS", "assets"], ["AI", "ai"], ["BROWSER", "browser"], ["LOADER", "worker_loader"], ["FLAREBOT_SESSION_SECRET", "secret_text"], ]) byName(name, type); for (const name of ["PersonalAgent", "Sandbox"]) { const b = byName(name, "durable_object_namespace"); if ( b.class_name !== name || (b.script_name !== undefined && b.script_name !== this.record.resources.workerName) ) throw new ResourceConflict(); } }, catch: () => new ResourceConflict(), }); } configuration(settings: Settings) { return Effect.try({ try: () => { return parseInstallationConfig( JSON.parse( settings.bindings.find((b) => b.name === "FLAREBOT_INSTALLATION") ?.text ?? "null", ), ); }, catch: () => new ResourceConflict(), }); } activeDeployment() { return Effect.gen({ self: this }, function* () { const deployments = yield* this.client.sdk( listScriptDeployments(this.worker), ); const latest = object(deployments) && Array.isArray(deployments.deployments) ? deployments.deployments[0] : undefined; if ( !object(latest) || !id(latest.id) || !Array.isArray(latest.versions) || latest.versions.length !== 1 || !object(latest.versions[0]) || latest.versions[0].percentage !== 100 || !id(latest.versions[0].versionId) ) return yield* new ResourceConflict(); return { deploymentId: latest.id, versionId: latest.versions[0].versionId, }; }); } latestVersion() { return Effect.gen({ self: this }, function* () { // The API returns newest uploads first, including undeployed versions. const versions = yield* this.client.sdk( listScriptVersions({ ...this.worker, page: 1, perPage: 1 }), ); if ( !object(versions) || !Array.isArray(versions.items) || versions.items.length !== 1 || !object(versions.items[0]) || !id(versions.items[0].id) ) return yield* new ResourceConflict(); return versions.items[0].id; }); } endpoint() { return Effect.gen({ self: this }, function* () { const current = yield* this.client.sdk(getScriptSubdomain(this.worker)); if ( !object(current) || current.enabled !== true || current.previewsEnabled !== false ) return yield* new ResourceConflict(); }); } upload<E>( artifact: Artifact, installationConfig: InstallationConfig, source: WorkerUploadBindings, beforeUpload: Effect.Effect<void, E>, ) { return Effect.gen({ self: this }, function* () { const completion = yield* measureDeployment( "asset upload", this.record, Effect.gen({ self: this }, function* () { const assets = artifact.files.filter((f) => f.assetHash); const manifest = Object.fromEntries( assets.map((f) => [ `/${f.path.slice("assets/".length)}`, { hash: f.assetHash!, size: f.size }, ]), ); const session = yield* this.client.sdk( createScriptAssetUpload({ ...this.worker, manifest }), ); if ( !object(session) || !token(session.jwt) || !Array.isArray(session.buckets) || session.buckets.length > assets.length ) return yield* new TemporarilyUnavailable(); const byHash = new Map(assets.map((f) => [f.assetHash!, f])); const seen = new Set(); let completion = session.jwt; let completed = session.buckets.length === 0; for (const bucket of session.buckets) { if ( !Array.isArray(bucket) || !bucket.length || bucket.length > assets.length ) return yield* new TemporarilyUnavailable(); const parts: Record<string, File> = {}; for (const hash of bucket) { const file = byHash.get(hash); if (!file || seen.has(hash)) return yield* new TemporarilyUnavailable(); seen.add(hash); // Base64 encoding is transient per bucket; source bytes remain opaque. const bytes = new Uint8Array(artifact.bytes[file.path]); let binary = ""; for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode(...bytes.subarray(i, i + 8192)); parts[hash] = new File([btoa(binary)], hash, { type: file.mime }); } const result = yield* this.client.sdk( createAssetUpload({ accountId: this.record.accountId, base64: true, jwtToken: session.jwt, body: parts, }), ); if (!object(result)) return yield* new TemporarilyUnavailable(); if (result.jwt !== undefined) { if (!token(result.jwt)) return yield* new TemporarilyUnavailable(); completion = result.jwt; completed = true; } } if (!completed) return yield* new TemporarilyUnavailable(); return completion; }), ); const d = artifact.deployment; const bucketName = `flarebot-attachments-${this.record.installationId}`; if (d.r2_buckets) { const existing = source.kind === "inherit" ? source.bindings.find((b) => b.name === "ATTACHMENTS") : undefined; if ( existing && (existing.type !== "r2_bucket" || existing.bucket_name !== bucketName) ) return yield* new ResourceConflict(); yield* ensureAttachmentBucket(this.client, bucketName); } // Inheritance retains unreadable secrets and every verified customer binding. const bindings: Record<string, unknown>[] = source.kind === "inherit" ? [ { type: "assets", name: "ASSETS" }, { type: "plain_text", name: "FLAREBOT_INSTALLATION", text: JSON.stringify(installationConfig), }, ...source.bindings .filter( (b) => !["ASSETS", "FLAREBOT_INSTALLATION"].includes(b.name), ) .map((b) => ({ name: b.name, type: "inherit", version_id: "latest", })), ] : [ { type: "assets", name: "ASSETS" }, ...d.durable_objects.bindings.map((b) => ({ type: "durable_object_namespace", ...b, })), { type: "ai", name: "AI" }, { type: "browser", name: "BROWSER" }, { type: "worker_loader", name: "LOADER" }, { type: "plain_text", name: "FLAREBOT_MODE", text: "customer-runtime", }, { type: "plain_text", name: "FLAREBOT_ENV", text: "production" }, { type: "plain_text", name: "FLAREBOT_INSTALLATION", text: JSON.stringify(installationConfig), }, { type: "secret_text", name: "FLAREBOT_SESSION_SECRET", text: source.secret, }, ]; if (d.r2_buckets && !bindings.some((b) => b.name === "ATTACHMENTS")) bindings.push({ type: "r2_bucket", name: "ATTACHMENTS", bucket_name: bucketName, }); if (source.kind === "bootstrap" && !source.secret) return yield* new ReauthorizationRequired(); const form = new FormData(); form.append( "metadata", new Blob( [ JSON.stringify({ main_module: "index.js", compatibility_date: d.compatibility_date, compatibility_flags: d.compatibility_flags, bindings, exports: d.exports, containers: [ { name: this.record.resources.sandboxApplicationName, class_name: "Sandbox", }, ], keep_bindings: [ "secret_text", "secret_key", "plain_text", "json", ], observability: d.observability, ...(source.kind === "inherit" ? source.metadata : {}), assets: { jwt: completion, config: {} }, }), ], { type: "application/json" }, ), ); for (const file of artifact.files.filter((f) => f.path.startsWith("worker/"), )) { const name = file.path.slice("worker/".length); form.append( name, new Blob([artifact.bytes[file.path]], { type: "application/javascript+module", }), name, ); } yield* beforeUpload; // Script PUT only accepts the literal latest for inheritance. Confirm that // it still identifies the verified source immediately before writing. if ( source.kind === "inherit" && (yield* this.latestVersion()) !== source.versionId ) return yield* new ResourceConflict(); yield* measureDeployment( "Worker PUT", this.record, this.client.request( this.script + (source.kind === "inherit" ? "?bindings_inherit=strict" : ""), { method: "PUT", body: form }, ), ); }); } verifyContent(artifact?: Artifact) { return Effect.gen({ self: this }, function* () { const files = artifact?.files.filter((file) => file.path.startsWith("worker/")) ?? []; const limit = artifact ? files.reduce((sum, file) => sum + file.size, 0) + 65_536 : 16 * 1024 * 1024 + 65_536; return yield* this.client.response( `${this.script}/content/v2`, {}, (response) => Effect.gen(function* () { if ( !response.ok || !response.headers.get("Content-Type")?.startsWith("multipart/") || response.headers.get("cf-entrypoint") !== "index.js" || !response.body ) return yield* new ResourceConflict(); const bytes = yield* bodyBytes( response, limit, new ResourceConflict(), new TemporarilyUnavailable(), ); const form = yield* Effect.tryPromise({ try: () => new Response(bytes, { headers: { "Content-Type": response.headers.get("Content-Type")!, }, }).formData(), catch: () => new TemporarilyUnavailable(), }); const keys = [...form.keys()]; if (artifact && keys.length !== files.length) return yield* new ResourceConflict(); if ( !artifact && (!keys.includes("index.js") || keys.length > 256 || new Set(keys).size !== keys.length || keys.some( (name) => !/^[A-Za-z0-9_./-]+\.js$/.test(name) || name .split("/") .some((part) => !part || part === ".." || part === "."), )) ) return yield* new ResourceConflict(); const observed: [string, string][] = []; for (const key of keys.sort()) { const parts = form.getAll(key); if (parts.length !== 1) return yield* new ResourceConflict(); const value = parts[0]; const content = typeof value === "string" ? (new TextEncoder().encode(value).buffer as ArrayBuffer) : yield* Effect.promise(() => value.arrayBuffer()); observed.push([key, yield* digest(content)]); } for (const file of files) { const parts = form.getAll(file.path.slice("worker/".length)); if (parts.length !== 1) return yield* new ResourceConflict(); const value = parts[0]; const content = typeof value === "string" ? (new TextEncoder().encode(value).buffer as ArrayBuffer) : yield* Effect.promise(() => value.arrayBuffer()); if ( content.byteLength !== file.size || (yield* digest(content)) !== file.sha256 ) return yield* new ResourceConflict(); } return yield* fingerprint(observed); }), ); }); } namespaces( installationConfig: InstallationConfig, artifact: Artifact, supportedSource = false, ) { return Effect.gen({ self: this }, function* () { const deployment = yield* this.activeDeployment(); const version = yield* this.version(deployment.versionId); return yield* this.namespacesFromVersion( version, installationConfig, artifact, supportedSource, ); }); } version(versionId: string) { return this.client.request(`${this.script}/versions/${versionId}`); } namespacesFromVersion( version: unknown, installationConfig: InstallationConfig, artifact: Artifact, supportedSource = false, ) { return Effect.gen({ self: this }, function* () { const resources = object(version) && object(version.resources) ? version.resources : undefined; const runtime = resources?.script_runtime; if ( !object(runtime) || (supportedSource ? typeof runtime.compatibility_date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(runtime.compatibility_date) || (!eq(runtime.compatibility_flags, ["nodejs_compat"]) && !eq(runtime.compatibility_flags, [ "nodejs_compat", "global_fetch_strictly_public", ])) : runtime.compatibility_date !== artifact.deployment.compatibility_date || !eq( runtime.compatibility_flags, artifact.deployment.compatibility_flags, )) || !object(runtime.exports) ) return yield* new ResourceConflict(); for (const name of ["PersonalAgent", "Sandbox"]) { const exported = runtime.exports[name]; if ( !object(exported) || exported.type !== "durable-object" || exported.storage !== "sqlite" || (exported.state !== undefined && exported.state !== "created") || (exported.container !== undefined && (name !== "Sandbox" || exported.container !== this.record.resources.sandboxApplicationName)) ) return yield* new ResourceConflict(); } if ( Object.entries(runtime.exports).some( ([name, value]) => object(value) && value.type === "durable-object" && name !== "PersonalAgent" && name !== "Sandbox", ) ) return yield* new ResourceConflict(); const bindings = resources?.bindings; if (Array.isArray(bindings)) yield* this.verifyWorker({ bindings }, installationConfig); if (!Array.isArray(bindings)) return yield* new TemporarilyUnavailable(); if (!supportedSource && artifact.deployment.r2_buckets) { const attachmentBinding = bindings.find( (b) => object(b) && b.name === "ATTACHMENTS", ); if ( !object(attachmentBinding) || attachmentBinding.type !== "r2_bucket" || attachmentBinding.bucket_name !== `flarebot-attachments-${this.record.installationId}` ) return yield* new ResourceConflict(); } const namespace = (name: string) => Effect.try({ try: () => { const matches = bindings.filter( (b) => b.type === "durable_object_namespace" && b.name === name && b.class_name === name && (b.script_name === undefined || b.script_name === this.record.resources.workerName), ); if (matches.length !== 1 || !id(matches[0].namespace_id)) throw new ResourceConflict(); return matches[0].namespace_id as string; }, catch: () => new ResourceConflict(), }); return { personalAgentNamespaceId: yield* namespace("PersonalAgent"), sandboxNamespaceId: yield* namespace("Sandbox"), }; }); } publish() { return Effect.gen({ self: this }, function* () { const current = yield* this.client.sdk(getScriptSubdomain(this.worker)); if ( object(current) && current.enabled === true && current.previewsEnabled === false ) return; yield* this.client.sdk( createScriptSubdomain({ ...this.worker, enabled: true, previewsEnabled: false, }), ); }); }}