Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
34 kB · 1034 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035import { parseInstallationConfig } from "../configuration/customer.ts";import type { UpgradeBaseline } from "./operation.ts";import { digest } from "./artifact.ts";import type { Artifact } from "./artifact-types.ts";import type { Installation, ReleaseIdentity } from "./installation-metadata.ts";import { fail } from "./deployment-errors.ts";
export type DeploymentNetwork = typeof fetch;type Binding = { type: string; name: string; class_name?: string; script_name?: string; namespace_id?: string; text?: string; json?: string;};type Settings = { bindings: Binding[]; [key: string]: unknown };export type Application = { id: string; name: string; configuration: { image: string; instance_type: string }; max_instances: number; scheduling_policy: string; durable_objects: { namespace_id: string };};export type Rollout = { id: string; description: string; target_configuration: { image: string; instance_type: string }; status?: string;};const id = (value: unknown): value is string => typeof value === "string" && /^(?:[a-f0-9]{32}|[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12})$/.test(value);const object = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);const token = (value: unknown): value is string => typeof value === "string" && value.length > 0 && value.length <= 16_384 && /^[A-Za-z0-9_.-]+$/.test(value);function normalizedConfiguration(value: unknown) { if (!object(value) || typeof value.image !== "string") fail("resource_conflict"); const config = { ...value }; const lite = config.vcpu === 0.0625 && config.memory_mib === 256 && object(config.disk) && config.disk.size_mb === 2000; if (lite) { config.instance_type = "lite"; for (const key of ["vcpu", "memory", "memory_mib"]) delete config[key]; if ( Object.keys(config.disk as Record<string, unknown>).every( (key) => key === "size_mb", ) ) delete config.disk; } if (typeof config.instance_type !== "string") fail("resource_conflict"); return config;}export const eq = (a: unknown, b: unknown): boolean => { if (a === b) return true; if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => eq(v, b[i])); if (!object(a) || !object(b)) return false; const keys = Object.keys(a); return ( keys.length === Object.keys(b).length && keys.every((k) => eq(a[k], b[k])) );};export const fingerprint = (value: unknown): Promise<string> => { const canonical = (v: any): any => Array.isArray(v) ? v.map(canonical) : object(v) ? Object.fromEntries( Object.keys(v) .sort() .map((k) => [k, canonical(v[k])]), ) : v; return digest( new TextEncoder().encode(JSON.stringify(canonical(value))) .buffer as ArrayBuffer, );};async function boundedJson(response: Response) { const reader = response.body?.getReader(); if (!reader) fail("temporarily_unavailable"); const chunks: Uint8Array[] = []; let size = 0; for (;;) { const { value, done } = await reader.read(); if (done) break; size += value.length; if (size > 2 * 1024 * 1024) { await reader.cancel(); fail("temporarily_unavailable"); } chunks.push(value); } const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; } try { return JSON.parse(new TextDecoder().decode(bytes)); } catch { fail("temporarily_unavailable"); }}// Fixed native v4 endpoint adapter. No arbitrary host/path from browser input,// automatic mutation retries, redirect following, or provider error propagation.export class DeploymentAPI { constructor( private readonly accessToken: string, private readonly record: Installation, private readonly network: DeploymentNetwork = fetch, ) {} private async request( path: string, method = "GET", body?: unknown, notFound = false, authorization = this.accessToken, ): Promise<any> { let response: Response; // Native Workers fetch rejects an arbitrary object as its receiver. const network = this.network; try { response = await network( `https://api.cloudflare.com/client/v4/accounts/${this.record.accountId}/${path}`, { method, redirect: "manual", signal: AbortSignal.timeout(60_000), headers: { Authorization: `Bearer ${authorization}`, ...(body instanceof FormData ? {} : body === undefined ? {} : { "Content-Type": "application/json" }), }, ...(body === undefined ? {} : { body: body instanceof FormData ? body : JSON.stringify(body) }), }, ); } catch { return fail("temporarily_unavailable"); } if (response.status === 401) fail("reauthorization_required"); if (response.status === 403) fail("account_denied"); if (response.status === 404 && notFound) return null; const envelope = await boundedJson(response); if ( !response.ok || !object(envelope) || envelope.success !== true || !("result" in envelope) ) fail("temporarily_unavailable"); return envelope.result; } private get script() { return `workers/scripts/${this.record.resources.workerName}`; } async ensureModelGateway() { const gatewayId = "default"; const gatewayPath = `ai-gateway/gateways/${gatewayId}`; let gateway = await this.request(gatewayPath, "GET", undefined, true); if (gateway === null) { await this.request("ai-gateway/gateways", "POST", { id: gatewayId, cache_invalidate_on_update: true, cache_ttl: 0, collect_logs: false, rate_limiting_interval: 0, rate_limiting_limit: 0, authentication: true, }); gateway = await this.request(gatewayPath); } if (gateway?.id !== gatewayId) fail("resource_conflict"); // Gateways are account-shared. Preserve existing billing, logging, limits // and authentication settings rather than replacing them with our defaults. } async enableOpenRouter() { await this.ensureModelGateway(); } async origin() { const result = await this.request("workers/subdomain"); if ( !object(result) || typeof result.subdomain !== "string" || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(result.subdomain) ) fail("setup_required"); return `https://${this.record.resources.workerName}.${result.subdomain}.workers.dev`; } async settings(): Promise<Settings | null> { const result = await this.request( `${this.script}/settings`, "GET", undefined, 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", ) ) fail("resource_conflict"); return result as Settings; } verifyWorker(settings: Settings, installationConfig: unknown) { const bindings = settings.bindings; if (new Set(bindings.map((b) => b.name)).size !== bindings.length) fail("resource_conflict"); const byName = (name: string, type: string) => { const binding = bindings.find((b) => b.name === name); if (binding?.type !== type) fail("resource_conflict"); return binding; }; let installed: unknown; try { installed = JSON.parse( byName("FLAREBOT_INSTALLATION", "plain_text").text!, ); } catch { fail("resource_conflict"); } if ( !eq(installed, installationConfig) || byName("FLAREBOT_MODE", "plain_text").text !== "customer-runtime" || byName("FLAREBOT_ENV", "plain_text").text !== "production" ) fail("resource_conflict"); 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) ) fail("resource_conflict"); } } configuration(settings: Settings) { try { return parseInstallationConfig( JSON.parse( settings.bindings.find((b) => b.name === "FLAREBOT_INSTALLATION") ?.text ?? "null", ), ); } catch { return fail("resource_conflict"); } } async activeDeployment() { const deployments = await this.request(`${this.script}/deployments`); const latest = deployments?.deployments?.[0]; if ( !id(latest?.id) || latest.versions?.length !== 1 || latest.versions[0].percentage !== 100 || !id(latest.versions[0].version_id) ) fail("resource_conflict"); return { deploymentId: latest.id as string, versionId: latest.versions[0].version_id as string, }; } async latestVersion() { // The API returns newest uploads first, including undeployed versions. const versions = await this.request( `${this.script}/versions?page=1&per_page=1`, ); if ( !Array.isArray(versions?.items) || versions.items.length !== 1 || !id(versions.items[0]?.id) ) fail("resource_conflict"); return versions.items[0].id as string; } async endpoint() { const current = await this.request(`${this.script}/subdomain`); if (current?.enabled !== true || current?.previews_enabled !== false) fail("resource_conflict"); } async observe( artifact: Artifact, deployedOperationId: string, expectedConfigDigest: string | null, sourceCodeHash?: string | null, ) { const deployment = await this.activeDeployment(); if ((await this.latestVersion()) !== deployment.versionId) fail("resource_conflict"); const settings = await this.settings(); if (!settings) fail("resource_conflict"); const config = this.configuration(settings); if ( config.installationId !== this.record.installationId || config.ownerSubject !== this.record.ownerSubject || config.runtimeOrigin !== this.record.resources.runtimeOrigin || config.release?.operationId !== deployedOperationId || config.release?.artifactDigest !== artifact.identity.artifactDigest || config.release?.version !== artifact.identity.version ) fail("resource_conflict"); const configDigest = await fingerprint(config); if (expectedConfigDigest && configDigest !== expectedConfigDigest) fail("resource_conflict"); this.verifyWorker(settings, config); const codeHash = await this.verifyContent( sourceCodeHash === undefined ? artifact : undefined, ); if (sourceCodeHash && codeHash !== sourceCodeHash) fail("resource_conflict"); const namespaces = await this.namespaces( config, artifact, sourceCodeHash !== undefined, ); if ( namespaces.personalAgentNamespaceId !== this.record.resources.personalAgentNamespaceId || namespaces.sandboxNamespaceId !== this.record.resources.sandboxNamespaceId ) fail("resource_conflict"); const version = await this.request( `${this.script}/versions/${deployment.versionId}`, ); const runtime = version?.resources?.script_runtime; // The version API reports the normalized defaults of assets.config: {}. // These and the container link are deployment-owned, not inherited settings. if ( !object(runtime) || !eq(runtime.assets, { serve_directly: true, raw_run_worker_first: false, }) || !eq(runtime.containers, [ { name: this.record.resources.sandboxApplicationName, class_name: "Sandbox", }, ]) ) fail("resource_conflict"); const metadata: Record<string, unknown> = {}; const tags = settings.tags === undefined ? [] : settings.tags; if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string")) fail("resource_conflict"); metadata.tags = tags; if (settings.annotations !== undefined) { if (!object(settings.annotations)) fail("resource_conflict"); const annotations: Record<string, string> = {}; for (const [key, value] of Object.entries(settings.annotations)) { if (typeof value !== "string") fail("resource_conflict"); // Cloudflare regenerates this read-only provenance on each upload. if (key === "workers/triggered_by") continue; const limit = key === "workers/message" ? 1000 : key === "workers/tag" ? 100 : 0; if (!limit || new TextEncoder().encode(value).length > limit) fail("resource_conflict"); annotations[key] = value; } if (Object.keys(annotations).length) metadata.annotations = annotations; } // Preserve native upload fields. Unknown settings fail closed before assets // staging instead of relying on omission to retain customer configuration. const preserved = [ "limits", "placement", "observability", "tail_consumers", "logpush", "usage_model", ]; for (const key of Object.keys(settings)) { if ( [ "bindings", "compatibility_date", "compatibility_flags", "created_on", "modified_on", "etag", "has_assets", "last_deployed_from", "tags", "annotations", ].includes(key) ) continue; if (!preserved.includes(key)) fail("resource_conflict"); metadata[key] = settings[key]; } for (const key of Object.keys(runtime)) { if ( [ "compatibility_date", "compatibility_flags", "exports", "assets", "containers", ].includes(key) ) continue; if (!preserved.includes(key)) fail("resource_conflict"); metadata[key] = runtime[key]; } const app = await this.findApplication(); if (!app || app.id !== this.record.resources.sandboxApplicationId) fail("resource_conflict"); await this.endpoint(); if (!eq(deployment, await this.activeDeployment())) fail("resource_conflict"); return { ...deployment, settings, config, configDigest, codeHash, metadata, app, fingerprint: await fingerprint({ bindings: settings.bindings.filter( (b) => b.name !== "FLAREBOT_INSTALLATION", ), metadata, namespaces, }), containerFingerprint: await this.containerFingerprint(app), }; } containerFingerprint(app: Application) { return fingerprint({ id: app.id, name: app.name, namespaceId: app.durable_objects.namespace_id, configuration: app.configuration, max_instances: app.max_instances, scheduling_policy: app.scheduling_policy, }); } private matchesSupportedContainer(app: Application) { let configuration: Record<string, unknown>; try { configuration = normalizedConfiguration(app.configuration); } catch { return false; } return ( app.id === this.record.resources.sandboxApplicationId && app.name === this.record.resources.sandboxApplicationName && app.durable_objects.namespace_id === this.record.resources.sandboxNamespaceId && typeof configuration.image === "string" && /^docker\.io\/cloudflare\/sandbox:[A-Za-z0-9._-]+@sha256:[a-f0-9]{64}$/.test( configuration.image, ) && configuration.instance_type === "lite" && app.max_instances === 4 && app.scheduling_policy === "default" ); } async baseline( source: ReleaseIdentity, target: Artifact, deployedOperationId: string, configDigest: string | null, ): Promise<UpgradeBaseline> { const observed = await this.observe( { ...target, identity: source }, deployedOperationId, configDigest, null, ); // The current contract supports only this durable-object identity and // customer-owned Sandbox shape. It does not assert that old code equals an // archived publisher bundle; the observed hash is pinned for race checks. if (!this.matchesSupportedContainer(observed.app)) fail("resource_conflict"); return { fromRelease: { version: source.version, sourceRevision: source.sourceRevision, artifactDigest: source.artifactDigest, }, toRelease: target.identity, deployedOperationId, configDigest: observed.configDigest, versionId: observed.versionId, deploymentId: observed.deploymentId, sourceCodeHash: observed.codeHash, fingerprint: observed.fingerprint, containerFingerprint: observed.containerFingerprint, targetContainerFingerprint: await this.containerFingerprint({ ...observed.app, ...this.desiredContainer(target), configuration: { ...observed.app.configuration, ...this.desiredContainer(target).configuration, }, }), }; } async upload( artifact: Artifact, installationConfig: unknown, bootstrapSecret: string | null, beforeUpload: () => Promise<void>, inherited?: { versionId: string; bindings: Binding[]; metadata: Record<string, unknown>; }, ) { 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 = await this.request( `${this.script}/assets-upload-session`, "POST", { manifest }, ); if ( !object(session) || !token(session.jwt) || !Array.isArray(session.buckets) || session.buckets.length > assets.length ) fail("temporarily_unavailable"); 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 ) fail("temporarily_unavailable"); const form = new FormData(); for (const hash of bucket) { const file = byHash.get(hash); if (!file || seen.has(hash)) fail("temporarily_unavailable"); 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)); form.append(hash, new Blob([btoa(binary)], { type: file.mime }), hash); } const result = await this.request( "workers/assets/upload?base64=true", "POST", form, false, session.jwt, ); if (!object(result)) fail("temporarily_unavailable"); if (result.jwt !== undefined) { if (!token(result.jwt)) fail("temporarily_unavailable"); completion = result.jwt; completed = true; } } if (!completed) fail("temporarily_unavailable"); const d = artifact.deployment; let bindings: Record<string, unknown>[] = [ { 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: bootstrapSecret, }, ]; if (inherited) { // Only release config and Assets change. Native inheritance preserves every // checked binding, including unreadable secrets, pinned to the old version. bindings = [ { type: "assets", name: "ASSETS" }, { type: "plain_text", name: "FLAREBOT_INSTALLATION", text: JSON.stringify(installationConfig), }, ...inherited.bindings .filter((b) => !["ASSETS", "FLAREBOT_INSTALLATION"].includes(b.name)) .map((b) => ({ name: b.name, type: "inherit", version_id: "latest", })), ]; } else if (!bootstrapSecret) fail("reauthorization_required"); 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, ...inherited?.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, ); } await beforeUpload(); // Script PUT only accepts the literal latest for inheritance. Confirm that // it still identifies the verified source immediately before writing. if (inherited && (await this.latestVersion()) !== inherited.versionId) fail("resource_conflict"); await this.request( this.script + (inherited ? "?bindings_inherit=strict" : ""), "PUT", form, ); } async verifyContent(artifact?: Artifact) { 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; let response: Response; const network = this.network; try { response = await network( `https://api.cloudflare.com/client/v4/accounts/${this.record.accountId}/${this.script}/content/v2`, { redirect: "manual", signal: AbortSignal.timeout(60_000), headers: { Authorization: `Bearer ${this.accessToken}` }, }, ); } catch { fail("temporarily_unavailable"); } if (response.status === 401) fail("reauthorization_required"); if (response.status === 403) fail("account_denied"); if ( !response.ok || !response.headers.get("Content-Type")?.startsWith("multipart/") || response.headers.get("cf-entrypoint") !== "index.js" || !response.body ) fail("resource_conflict"); const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let size = 0; for (;;) { const { value, done } = await reader.read(); if (done) break; size += value.length; if (size > limit) { await reader.cancel(); fail("resource_conflict"); } chunks.push(value); } const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; } const form = await new Response(bytes, { headers: { "Content-Type": response.headers.get("Content-Type")! }, }).formData(); const keys = [...form.keys()]; if (artifact && keys.length !== files.length) fail("resource_conflict"); 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 === "."), )) ) fail("resource_conflict"); const observed: [string, string][] = []; for (const key of keys.sort()) { const parts = form.getAll(key); if (parts.length !== 1) fail("resource_conflict"); const value = parts[0]; const content = typeof value === "string" ? (new TextEncoder().encode(value).buffer as ArrayBuffer) : await value.arrayBuffer(); observed.push([key, await digest(content)]); } for (const file of files) { const parts = form.getAll(file.path.slice("worker/".length)); if (parts.length !== 1) fail("resource_conflict"); const value = parts[0]; const content = typeof value === "string" ? (new TextEncoder().encode(value).buffer as ArrayBuffer) : await value.arrayBuffer(); if ( content.byteLength !== file.size || (await digest(content)) !== file.sha256 ) fail("resource_conflict"); } return fingerprint(observed); } async namespaces( installationConfig: unknown, artifact: Artifact, supportedSource = false, ) { const deployments = await this.request(`${this.script}/deployments`); const latest = deployments?.deployments?.[0]; if ( !Array.isArray(latest?.versions) || latest.versions.length !== 1 || latest.versions[0].percentage !== 100 || !id(latest.versions[0].version_id) ) fail("resource_conflict"); const version = await this.request( `${this.script}/versions/${latest.versions[0].version_id}`, ); const runtime = version?.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) ) fail("resource_conflict"); 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)) ) fail("resource_conflict"); } if ( Object.entries(runtime.exports).some( ([name, value]) => object(value) && value.type === "durable-object" && name !== "PersonalAgent" && name !== "Sandbox", ) ) fail("resource_conflict"); const bindings = version?.resources?.bindings; if (Array.isArray(bindings)) this.verifyWorker({ bindings }, installationConfig); if (!Array.isArray(bindings)) fail("temporarily_unavailable"); const namespace = (name: string) => { 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)) fail("resource_conflict"); return matches[0].namespace_id as string; }; return { personalAgentNamespaceId: namespace("PersonalAgent"), sandboxNamespaceId: namespace("Sandbox"), }; } private application(value: unknown): Application { if ( !object(value) || !id(value.id) || value.name !== this.record.resources.sandboxApplicationName || !object(value.durable_objects) || value.durable_objects.namespace_id !== this.record.resources.sandboxNamespaceId || !object(value.configuration) || typeof value.configuration.image !== "string" || false ) fail("resource_conflict"); return { ...value, configuration: normalizedConfiguration(value.configuration), } as unknown as Application; } async findApplication(): Promise<Application | null> { const known = this.record.resources.sandboxApplicationId; if (known) return this.application( await this.request(`containers/applications/${known}`), ); const list = await this.request( `containers/applications?name=${this.record.resources.sandboxApplicationName}`, ); if (!Array.isArray(list) || list.length > 100) fail("temporarily_unavailable"); const matches = list.filter( (app) => app?.name === this.record.resources.sandboxApplicationName, ); if (matches.length > 1) fail("resource_conflict"); return matches.length ? this.application(matches[0]) : null; } desiredContainer(artifact: Artifact) { const c = artifact.deployment.containers[0]; return { configuration: { image: c.image, instance_type: c.instance_type }, max_instances: c.max_instances, scheduling_policy: "default", }; } matchesContainer(app: Application, artifact: Artifact) { const d = this.desiredContainer(artifact); return ( app.configuration.image === d.configuration.image && app.configuration.instance_type === d.configuration.instance_type && app.max_instances === d.max_instances && app.scheduling_policy === d.scheduling_policy ); } async createApplication(artifact: Artifact) { return this.application( await this.request("containers/applications", "POST", { name: this.record.resources.sandboxApplicationName, instances: 0, ...this.desiredContainer(artifact), durable_objects: { namespace_id: this.record.resources.sandboxNamespaceId, }, }), ); } async patchApplication(app: Application, artifact: Artifact) { await this.request(`containers/applications/${app.id}`, "PATCH", { ...this.desiredContainer(artifact), configuration: { ...app.configuration, ...this.desiredContainer(artifact).configuration, }, }); } async rollout( app: Application, artifact: Artifact, operationId: string, allowCreate: boolean, knownId: string | null = null, remember: (id: string) => Promise<void> = async () => {}, ) { const description = `Flarebot ${operationId}`; const target = { ...app.configuration, ...this.desiredContainer(artifact).configuration, }; const list: any[] = []; if (knownId) list.push( await this.request( `containers/applications/${app.id}/rollouts/${knownId}`, ), ); else { let last: string | null = null; for (let page = 0; page < 10; page++) { const batch = await this.request( `containers/applications/${app.id}/rollouts?limit=100${last ? `&last=${last}` : ""}`, ); if (!Array.isArray(batch) || batch.length > 100) fail("temporarily_unavailable"); list.push(...batch); if (batch.length < 100) break; const next = batch.at(-1)?.id; if (!id(next) || next === last || page === 9) fail("temporarily_unavailable"); last = next; } } const matches = list.filter((r) => r?.description === description); if (matches.length > 1) fail("resource_conflict"); if (matches.length) { if ( !eq( normalizedConfiguration(matches[0].target_configuration), normalizedConfiguration(target), ) || !id(matches[0].id) ) fail("resource_conflict"); if (!knownId) await remember(matches[0].id); if ( matches[0].status === "pending" || matches[0].status === "progressing" ) fail("temporarily_unavailable"); if (matches[0].status !== "completed") fail("resource_conflict"); return; } if (!allowCreate) fail("recovery_required"); const created = await this.request( `containers/applications/${app.id}/rollouts`, "POST", { description, strategy: "rolling", kind: "full_auto", step_percentage: 100, target_configuration: target, }, ); if (id(created?.id)) await remember(created.id); if ( !id(created?.id) || created?.status !== "completed" || created?.description !== description || !eq( normalizedConfiguration(created?.target_configuration), normalizedConfiguration(target), ) ) fail("temporarily_unavailable"); } async publish() { const current = await this.request(`${this.script}/subdomain`); if (current?.enabled === true && current?.previews_enabled === false) return; await this.request(`${this.script}/subdomain`, "POST", { enabled: true, previews_enabled: false, }); }}