| 1 | import * as Effect from "effect/Effect"; |
| 2 | import { Stack } from "../../Stack.ts"; |
| 3 | import { Stage } from "../../Stage.ts"; |
| 4 | |
| 5 | type NamespaceTypeId = typeof NamespaceTypeId; |
| 6 | const NamespaceTypeId = "Cloudflare.Artifacts.Namespace" as const; |
| 7 | |
| 8 | /** |
| 9 | * Cloudflare validation: 3–63 chars, lowercase alphanumeric and hyphens, must |
| 10 | * start and end with a lowercase alphanumeric character. |
| 11 | */ |
| 12 | const ARTIFACTS_NAMESPACE_REGEX = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])?$/; |
| 13 | |
| 14 | export class InvalidNamespaceError extends Error { |
| 15 | readonly _tag = "InvalidNamespaceError" as const; |
| 16 | constructor(public readonly namespace: string) { |
| 17 | super( |
| 18 | `Invalid artifacts namespace name '${namespace}'. Must be 3-63 characters, start and end with a lowercase alphanumeric character, and contain only lowercase alphanumeric characters and hyphens.`, |
| 19 | ); |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | export type NamespaceProps = { |
| 24 | /** |
| 25 | * Cloudflare namespace name. Namespaces are implicit on Cloudflare — the |
| 26 | * first repo created against this name conjures the namespace. |
| 27 | * |
| 28 | * Must be 3–63 lowercase alphanumeric characters or hyphens, and must start |
| 29 | * and end with a lowercase alphanumeric character. If omitted, a unique |
| 30 | * physical name is generated from the resource's logical id, app, and stage. |
| 31 | */ |
| 32 | namespace?: string; |
| 33 | }; |
| 34 | |
| 35 | /** |
| 36 | * Marker for a Cloudflare Artifacts namespace binding. |
| 37 | * |
| 38 | * Artifacts namespaces are implicit (created on first repo write) and require |
| 39 | * no deploy-time provisioning, so this is a pure binding marker rather than |
| 40 | * a full Resource. The Worker provider sees this object in `bindings: { ... }` |
| 41 | * and emits the corresponding `{ type: "artifacts", name, namespace }` binding |
| 42 | * to the script. |
| 43 | */ |
| 44 | export type Namespace = { |
| 45 | kind: NamespaceTypeId; |
| 46 | name: string; |
| 47 | namespace: string; |
| 48 | }; |
| 49 | |
| 50 | export const isNamespace = (value: unknown): value is Namespace => |
| 51 | typeof value === "object" && |
| 52 | value !== null && |
| 53 | "kind" in value && |
| 54 | (value as Namespace).kind === NamespaceTypeId; |
| 55 | |
| 56 | /** |
| 57 | * A Cloudflare Artifacts namespace — the top-level container for Git-compatible |
| 58 | * versioned repositories. See the |
| 59 | * {@link https://blog.cloudflare.com/artifacts-git-for-agents-beta/ | Artifacts launch post} |
| 60 | * and {@link https://developers.cloudflare.com/artifacts/concepts/namespaces/ | Namespaces docs}. |
| 61 | * |
| 62 | * Namespaces on Cloudflare are **implicit**: there is no `POST /namespaces` |
| 63 | * endpoint. The namespace is conjured the first time a repo is created against |
| 64 | * it (either via the REST API or the Worker binding). Because of that, the |
| 65 | * Alchemy "resource" is a thin binding marker — there is nothing to provision |
| 66 | * at deploy time. Repos themselves are typically created at runtime through |
| 67 | * the bound `Artifacts` API. |
| 68 | * |
| 69 | * Unlike the other Worker-only bindings, an Artifacts namespace does **not** |
| 70 | * auto-bind when yielded — it always requires an explicit access level via |
| 71 | * {@link ReadNamespace} / {@link WriteNamespace} / {@link ReadWriteNamespace}. |
| 72 | * |
| 73 | * ### Declaring a Namespace |
| 74 | * **Example:** Default namespace (a unique physical name is generated) |
| 75 | * ```typescript |
| 76 | * const Repos = Cloudflare.Artifacts.Namespace("Repos"); |
| 77 | * ``` |
| 78 | * |
| 79 | * **Example:** Override the namespace name (must be lowercase, 3–63 chars) |
| 80 | * ```typescript |
| 81 | * const Repos = Cloudflare.Artifacts.Namespace("Repos", { namespace: "starter-repos" }); |
| 82 | * ``` |
| 83 | * |
| 84 | * ### Binding to a Worker |
| 85 | * **Example:** Wiring it into a Worker |
| 86 | * ```typescript |
| 87 | * export const Worker = Cloudflare.Worker("Worker", { |
| 88 | * main: "./src/worker.ts", |
| 89 | * bindings: { Repos }, |
| 90 | * }); |
| 91 | * |
| 92 | * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>; |
| 93 | * // { Repos: Artifacts } |
| 94 | * ``` |
| 95 | * |
| 96 | * **Example:** Async-style worker |
| 97 | * ```typescript |
| 98 | * export default { |
| 99 | * async fetch(request: Request, env: WorkerEnv) { |
| 100 | * const repo = await env.Repos.create("starter-repo"); |
| 101 | * return Response.json({ remote: repo.remote, token: repo.token }); |
| 102 | * }, |
| 103 | * }; |
| 104 | * ``` |
| 105 | * |
| 106 | * **Example:** Effect-style worker (explicit access level) |
| 107 | * ```typescript |
| 108 | * const artifacts = yield* Cloudflare.Artifacts.ReadWriteNamespace(Repos); |
| 109 | * const repo = yield* artifacts.create("starter-repo", { |
| 110 | * setDefaultBranch: "main", |
| 111 | * }); |
| 112 | * ``` |
| 113 | * |
| 114 | * @binding |
| 115 | * @product Artifacts |
| 116 | * @category Developer Platform |
| 117 | */ |
| 118 | export const Namespace: ( |
| 119 | name: string, |
| 120 | props?: NamespaceProps, |
| 121 | ) => Effect.Effect<Namespace, never, Stack | Stage> = Effect.fn(function* ( |
| 122 | name: string, |
| 123 | props?: NamespaceProps, |
| 124 | ) { |
| 125 | const namespace = props?.namespace |
| 126 | ? props.namespace |
| 127 | : name.toLocaleLowerCase(); |
| 128 | if (!ARTIFACTS_NAMESPACE_REGEX.test(namespace)) { |
| 129 | return yield* Effect.die(new InvalidNamespaceError(namespace)); |
| 130 | } |
| 131 | return { |
| 132 | kind: NamespaceTypeId, |
| 133 | name, |
| 134 | namespace, |
| 135 | } satisfies Namespace; |
| 136 | }); |