1import * as Effect from "effect/Effect";
2import { Stack } from "../../Stack.ts";
3import { Stage } from "../../Stage.ts";
4
5type NamespaceTypeId = typeof NamespaceTypeId;
6const 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 */
12const ARTIFACTS_NAMESPACE_REGEX = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])?$/;
13
14export 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
23export 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 */
44export type Namespace = {
45 kind: NamespaceTypeId;
46 name: string;
47 namespace: string;
48};
49
50export 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 */
118export 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});