alchemy 2.0.0-beta.15 → 2.0.0-beta.17

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 (104) hide show
  1. package/bin/alchemy.js +1661 -710
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/cli.js +9 -7
  5. package/bin/commands/deploy.ts +11 -2
  6. package/bin/commands/login.ts +6 -3
  7. package/bin/commands/profile.ts +106 -0
  8. package/bin/commands/state.ts +91 -1
  9. package/lib/Auth/Credentials.d.ts +6 -0
  10. package/lib/Auth/Credentials.d.ts.map +1 -1
  11. package/lib/Auth/Credentials.js +12 -1
  12. package/lib/Auth/Credentials.js.map +1 -1
  13. package/lib/Auth/Profile.d.ts +5 -0
  14. package/lib/Auth/Profile.d.ts.map +1 -1
  15. package/lib/Auth/Profile.js +11 -0
  16. package/lib/Auth/Profile.js.map +1 -1
  17. package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts +4 -4
  18. package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -1
  19. package/lib/Cloudflare/ApiToken/AccountApiToken.js +16 -11
  20. package/lib/Cloudflare/ApiToken/AccountApiToken.js.map +1 -1
  21. package/lib/Cloudflare/ApiToken/Common.d.ts +11 -12
  22. package/lib/Cloudflare/ApiToken/Common.d.ts.map +1 -1
  23. package/lib/Cloudflare/ApiToken/Common.js +4 -14
  24. package/lib/Cloudflare/ApiToken/Common.js.map +1 -1
  25. package/lib/Cloudflare/ApiToken/UserApiToken.d.ts +2 -3
  26. package/lib/Cloudflare/ApiToken/UserApiToken.d.ts.map +1 -1
  27. package/lib/Cloudflare/ApiToken/UserApiToken.js +4 -4
  28. package/lib/Cloudflare/ApiToken/UserApiToken.js.map +1 -1
  29. package/lib/Cloudflare/Artifacts/Artifacts.d.ts +101 -0
  30. package/lib/Cloudflare/Artifacts/Artifacts.d.ts.map +1 -0
  31. package/lib/Cloudflare/Artifacts/Artifacts.js +92 -0
  32. package/lib/Cloudflare/Artifacts/Artifacts.js.map +1 -0
  33. package/lib/Cloudflare/Artifacts/ArtifactsBinding.d.ts +83 -0
  34. package/lib/Cloudflare/Artifacts/ArtifactsBinding.d.ts.map +1 -0
  35. package/lib/Cloudflare/Artifacts/ArtifactsBinding.js +65 -0
  36. package/lib/Cloudflare/Artifacts/ArtifactsBinding.js.map +1 -0
  37. package/lib/Cloudflare/Artifacts/index.d.ts +3 -0
  38. package/lib/Cloudflare/Artifacts/index.d.ts.map +1 -0
  39. package/lib/Cloudflare/Artifacts/index.js +3 -0
  40. package/lib/Cloudflare/Artifacts/index.js.map +1 -0
  41. package/lib/Cloudflare/Container/ContainerBinding.d.ts +1 -1
  42. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  43. package/lib/Cloudflare/Providers.js +3 -1
  44. package/lib/Cloudflare/Providers.js.map +1 -1
  45. package/lib/Cloudflare/Workers/InferEnv.d.ts +1 -1
  46. package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
  47. package/lib/Cloudflare/Workers/InferEnv.js +0 -1
  48. package/lib/Cloudflare/Workers/InferEnv.js.map +1 -1
  49. package/lib/Cloudflare/Workers/Worker.d.ts +2 -1
  50. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  51. package/lib/Cloudflare/Workers/Worker.js +25 -18
  52. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  53. package/lib/Cloudflare/index.d.ts +1 -0
  54. package/lib/Cloudflare/index.d.ts.map +1 -1
  55. package/lib/Cloudflare/index.js +1 -0
  56. package/lib/Cloudflare/index.js.map +1 -1
  57. package/lib/GitHub/AuthProvider.d.ts +37 -0
  58. package/lib/GitHub/AuthProvider.d.ts.map +1 -0
  59. package/lib/GitHub/AuthProvider.js +154 -0
  60. package/lib/GitHub/AuthProvider.js.map +1 -0
  61. package/lib/GitHub/Comment.d.ts +2 -1
  62. package/lib/GitHub/Comment.d.ts.map +1 -1
  63. package/lib/GitHub/Comment.js +1 -0
  64. package/lib/GitHub/Comment.js.map +1 -1
  65. package/lib/GitHub/Credentials.d.ts +29 -0
  66. package/lib/GitHub/Credentials.d.ts.map +1 -0
  67. package/lib/GitHub/Credentials.js +54 -0
  68. package/lib/GitHub/Credentials.js.map +1 -0
  69. package/lib/GitHub/Providers.d.ts +12 -1
  70. package/lib/GitHub/Providers.d.ts.map +1 -1
  71. package/lib/GitHub/Providers.js +14 -3
  72. package/lib/GitHub/Providers.js.map +1 -1
  73. package/lib/GitHub/Secret.d.ts +8 -10
  74. package/lib/GitHub/Secret.d.ts.map +1 -1
  75. package/lib/GitHub/Secret.js +16 -14
  76. package/lib/GitHub/Secret.js.map +1 -1
  77. package/lib/GitHub/Variable.d.ts +8 -10
  78. package/lib/GitHub/Variable.d.ts.map +1 -1
  79. package/lib/GitHub/Variable.js +12 -13
  80. package/lib/GitHub/Variable.js.map +1 -1
  81. package/lib/GitHub/index.d.ts +2 -0
  82. package/lib/GitHub/index.d.ts.map +1 -1
  83. package/lib/GitHub/index.js +2 -0
  84. package/lib/GitHub/index.js.map +1 -1
  85. package/package.json +4 -4
  86. package/src/Auth/Credentials.ts +17 -1
  87. package/src/Auth/Profile.ts +15 -0
  88. package/src/Cloudflare/ApiToken/AccountApiToken.ts +25 -14
  89. package/src/Cloudflare/ApiToken/Common.ts +12 -19
  90. package/src/Cloudflare/ApiToken/UserApiToken.ts +6 -8
  91. package/src/Cloudflare/Artifacts/Artifacts.ts +139 -0
  92. package/src/Cloudflare/Artifacts/ArtifactsBinding.ts +188 -0
  93. package/src/Cloudflare/Artifacts/index.ts +2 -0
  94. package/src/Cloudflare/Providers.ts +3 -0
  95. package/src/Cloudflare/Workers/InferEnv.ts +5 -4
  96. package/src/Cloudflare/Workers/Worker.ts +12 -1
  97. package/src/Cloudflare/index.ts +1 -0
  98. package/src/GitHub/AuthProvider.ts +309 -0
  99. package/src/GitHub/Comment.ts +4 -1
  100. package/src/GitHub/Credentials.ts +99 -0
  101. package/src/GitHub/Providers.ts +29 -3
  102. package/src/GitHub/Secret.ts +22 -24
  103. package/src/GitHub/Variable.ts +16 -23
  104. package/src/GitHub/index.ts +2 -0
@@ -12,14 +12,12 @@ import {
12
12
  conditionFingerprint,
13
13
  policyFingerprint,
14
14
  resolvePolicies,
15
- type BaseApiTokenProps,
15
+ type ApiTokenProps,
16
16
  } from "./Common.ts";
17
17
 
18
- export type AccountApiTokenProps = BaseApiTokenProps;
19
-
20
18
  export type AccountApiToken = Resource<
21
19
  "Cloudflare.AccountApiToken",
22
- AccountApiTokenProps,
20
+ ApiTokenProps,
23
21
  {
24
22
  tokenId: string;
25
23
  name: string;
@@ -52,6 +50,7 @@ export type AccountApiToken = Resource<
52
50
  * ```typescript
53
51
  * const token = yield* Cloudflare.AccountApiToken("ci-token", {
54
52
  * name: "my-ci-token",
53
+ * accountId,
55
54
  * policies: [
56
55
  * {
57
56
  * effect: "allow",
@@ -59,7 +58,7 @@ export type AccountApiToken = Resource<
59
58
  * "Workers Scripts Write",
60
59
  * "Workers KV Storage Write",
61
60
  * ],
62
- * resources: { "com.cloudflare.api.account": "*" },
61
+ * resources: { [`com.cloudflare.api.account.${accountId}`]: "*" },
63
62
  * },
64
63
  * ],
65
64
  * });
@@ -87,7 +86,7 @@ export const AccountApiTokenProvider = () =>
87
86
  Provider.effect(
88
87
  AccountApiToken,
89
88
  Effect.gen(function* () {
90
- const { accountId } = yield* CloudflareEnvironment;
89
+ const { accountId: defaultAccountId } = yield* CloudflareEnvironment;
91
90
  const createToken = yield* accounts.createToken;
92
91
  const updateToken = yield* accounts.updateToken;
93
92
  const deleteToken = yield* accounts.deleteToken;
@@ -100,6 +99,7 @@ export const AccountApiTokenProvider = () =>
100
99
  status?: "active" | "disabled" | "expired" | null;
101
100
  },
102
101
  value: Redacted.Redacted<string>,
102
+ accountId: string,
103
103
  ): AccountApiTokenAttributes => ({
104
104
  tokenId: tokenData.id ?? "",
105
105
  name: tokenData.name ?? "",
@@ -112,16 +112,19 @@ export const AccountApiTokenProvider = () =>
112
112
  stables: ["tokenId", "accountId"],
113
113
  diff: Effect.fn(function* ({ id, olds, news, output }) {
114
114
  if (!isResolved(news)) return undefined;
115
- if ((output?.accountId ?? accountId) !== accountId) {
115
+ const newAccountId = news.accountId ?? defaultAccountId;
116
+ const oldAccountId =
117
+ output?.accountId ?? olds?.accountId ?? defaultAccountId;
118
+ if (oldAccountId !== newAccountId) {
116
119
  return { action: "replace" } as const;
117
120
  }
118
121
  const oldName = output?.name ?? (yield* resolveName(id, olds?.name));
119
122
  const newName = yield* resolveName(id, news.name);
120
123
  const oldPolicyFp = policyFingerprint(
121
- resolvePolicies(olds?.policies ?? [], accountId),
124
+ resolvePolicies(olds?.policies ?? []),
122
125
  );
123
126
  const newPolicyFp = policyFingerprint(
124
- resolvePolicies(news.policies, accountId),
127
+ resolvePolicies(news.policies),
125
128
  );
126
129
  const oldCondFp = conditionFingerprint(olds?.condition);
127
130
  const newCondFp = conditionFingerprint(news.condition);
@@ -136,8 +139,9 @@ export const AccountApiTokenProvider = () =>
136
139
  }
137
140
  }),
138
141
  create: Effect.fn(function* ({ id, news }) {
142
+ const accountId = news.accountId ?? defaultAccountId;
139
143
  const name = yield* resolveName(id, news.name);
140
- const policies = resolvePolicies(news.policies, accountId);
144
+ const policies = resolvePolicies(news.policies);
141
145
  const result = yield* createToken({
142
146
  accountId,
143
147
  name,
@@ -151,11 +155,16 @@ export const AccountApiTokenProvider = () =>
151
155
  `Cloudflare did not return a value for token "${name}".`,
152
156
  );
153
157
  }
154
- return buildAttributes(result, Redacted.make(result.value));
158
+ return buildAttributes(
159
+ result,
160
+ Redacted.make(result.value),
161
+ accountId,
162
+ );
155
163
  }),
156
164
  update: Effect.fn(function* ({ id, news, output }) {
165
+ const accountId = news.accountId ?? defaultAccountId;
157
166
  const name = yield* resolveName(id, news.name);
158
- const policies = resolvePolicies(news.policies, accountId);
167
+ const policies = resolvePolicies(news.policies);
159
168
  const result = yield* updateToken({
160
169
  accountId,
161
170
  tokenId: output.tokenId,
@@ -167,7 +176,7 @@ export const AccountApiTokenProvider = () =>
167
176
  });
168
177
  // Cloudflare doesn't return the value on update; preserve the
169
178
  // value captured at creation.
170
- return buildAttributes(result, output.value);
179
+ return buildAttributes(result, output.value, accountId);
171
180
  }),
172
181
  delete: Effect.fn(function* ({ output }) {
173
182
  yield* deleteToken({
@@ -187,7 +196,9 @@ export const AccountApiTokenProvider = () =>
187
196
  accountId: output.accountId,
188
197
  tokenId: output.tokenId,
189
198
  }).pipe(
190
- Effect.map((token) => buildAttributes(token, output.value)),
199
+ Effect.map((token) =>
200
+ buildAttributes(token, output.value, output.accountId),
201
+ ),
191
202
  Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
192
203
  Effect.catchTag("TokenNotFound", () => Effect.succeed(undefined)),
193
204
  );
@@ -6,14 +6,13 @@ import {
6
6
  /**
7
7
  * Resource keys recognized by Cloudflare API token policies.
8
8
  *
9
- * The literal `"com.cloudflare.api.account"` is rewritten by the account-scoped
10
- * provider to `"com.cloudflare.api.account.${accountId}"` so callers don't have
11
- * to splice the account ID themselves.
9
+ * Cloudflare requires the account ID to be embedded directly in the resource
10
+ * key (e.g. `com.cloudflare.api.account.<accountId>`); pass the fully-qualified
11
+ * key — no rewriting is performed.
12
12
  *
13
13
  * @see https://developers.cloudflare.com/fundamentals/api/reference/permissions/
14
14
  */
15
15
  export type ApiTokenResourceKey =
16
- | "com.cloudflare.api.account"
17
16
  | `com.cloudflare.api.account.${string}`
18
17
  | `com.cloudflare.api.account.zone.${string}`
19
18
  | `com.cloudflare.edge.r2.bucket.${string}`
@@ -41,12 +40,17 @@ export interface ApiTokenCondition {
41
40
  };
42
41
  }
43
42
 
44
- export type BaseApiTokenProps = {
43
+ export type ApiTokenProps = {
45
44
  /**
46
45
  * Token name. Defaults to a generated physical name based on the
47
46
  * resource's logical id, app name, and stage.
48
47
  */
49
48
  name?: string;
49
+ /**
50
+ * The Cloudflare account ID that owns this token. Defaults to the
51
+ * account ID resolved from the ambient {@link CloudflareEnvironment}.
52
+ */
53
+ accountId?: string;
50
54
  /**
51
55
  * Access policies attached to the token. At least one policy is required
52
56
  * by Cloudflare.
@@ -81,35 +85,24 @@ export const resolvePermissionGroup = (ref: ApiTokenPermissionGroupRef) => {
81
85
  return ref.meta ? { id: ref.id, meta: ref.meta } : { id: ref.id };
82
86
  };
83
87
 
84
- /**
85
- * Rewrite the bare `"com.cloudflare.api.account"` key to the
86
- * account-scoped form. Pass `accountId: undefined` to leave keys unchanged
87
- * (e.g. for user-owned tokens whose policies don't carry an account scope).
88
- */
89
- export const resolveResources = (
88
+ const resolveResources = (
90
89
  resources: ApiTokenPolicy["resources"],
91
- accountId: string | undefined,
92
90
  ): Record<string, string> => {
93
91
  const out: Record<string, string> = {};
94
92
  for (const [key, value] of Object.entries(resources)) {
95
93
  if (value === undefined) continue;
96
- if (key === "com.cloudflare.api.account" && accountId) {
97
- out[`com.cloudflare.api.account.${accountId}`] = value;
98
- } else {
99
- out[key] = value;
100
- }
94
+ out[key] = value;
101
95
  }
102
96
  return out;
103
97
  };
104
98
 
105
99
  export const resolvePolicies = (
106
100
  policies: ApiTokenPolicy[],
107
- accountId: string | undefined,
108
101
  ): ResolvedPolicy[] =>
109
102
  policies.map((policy) => ({
110
103
  effect: policy.effect,
111
104
  permissionGroups: policy.permissionGroups.map(resolvePermissionGroup),
112
- resources: resolveResources(policy.resources, accountId),
105
+ resources: resolveResources(policy.resources),
113
106
  }));
114
107
 
115
108
  export const policyFingerprint = (policies: ResolvedPolicy[]): string =>
@@ -11,14 +11,12 @@ import {
11
11
  conditionFingerprint,
12
12
  policyFingerprint,
13
13
  resolvePolicies,
14
- type BaseApiTokenProps,
14
+ type ApiTokenProps,
15
15
  } from "./Common.ts";
16
16
 
17
- export type UserApiTokenProps = BaseApiTokenProps;
18
-
19
17
  export type UserApiToken = Resource<
20
18
  "Cloudflare.UserApiToken",
21
- UserApiTokenProps,
19
+ ApiTokenProps,
22
20
  {
23
21
  tokenId: string;
24
22
  name: string;
@@ -102,10 +100,10 @@ export const UserApiTokenProvider = () =>
102
100
  const oldName = output?.name ?? (yield* resolveName(id, olds?.name));
103
101
  const newName = yield* resolveName(id, news.name);
104
102
  const oldPolicyFp = policyFingerprint(
105
- resolvePolicies(olds?.policies ?? [], undefined),
103
+ resolvePolicies(olds?.policies ?? []),
106
104
  );
107
105
  const newPolicyFp = policyFingerprint(
108
- resolvePolicies(news.policies, undefined),
106
+ resolvePolicies(news.policies),
109
107
  );
110
108
  const oldCondFp = conditionFingerprint(olds?.condition);
111
109
  const newCondFp = conditionFingerprint(news.condition);
@@ -121,7 +119,7 @@ export const UserApiTokenProvider = () =>
121
119
  }),
122
120
  create: Effect.fn(function* ({ id, news }) {
123
121
  const name = yield* resolveName(id, news.name);
124
- const policies = resolvePolicies(news.policies, undefined);
122
+ const policies = resolvePolicies(news.policies);
125
123
  const result = yield* createToken({
126
124
  name,
127
125
  policies,
@@ -138,7 +136,7 @@ export const UserApiTokenProvider = () =>
138
136
  }),
139
137
  update: Effect.fn(function* ({ id, news, output }) {
140
138
  const name = yield* resolveName(id, news.name);
141
- const policies = resolvePolicies(news.policies, undefined);
139
+ const policies = resolvePolicies(news.policies);
142
140
  const result = yield* updateToken({
143
141
  tokenId: output.tokenId,
144
142
  name,
@@ -0,0 +1,139 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { Stack } from "../../Stack.ts";
3
+ import { Stage } from "../../Stage.ts";
4
+ import { ArtifactsBinding } from "./ArtifactsBinding.ts";
5
+
6
+ type ArtifactsTypeId = typeof ArtifactsTypeId;
7
+ const ArtifactsTypeId = "Cloudflare.Artifacts" as const;
8
+
9
+ /**
10
+ * Cloudflare validation: 3–63 chars, lowercase alphanumeric and hyphens, must
11
+ * start and end with a lowercase alphanumeric character.
12
+ */
13
+ const ARTIFACTS_NAMESPACE_REGEX = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])?$/;
14
+
15
+ export class InvalidArtifactsNamespaceError extends Error {
16
+ readonly _tag = "InvalidArtifactsNamespaceError" as const;
17
+ constructor(public readonly namespace: string) {
18
+ super(
19
+ `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.`,
20
+ );
21
+ }
22
+ }
23
+
24
+ export type ArtifactsProps = {
25
+ /**
26
+ * Cloudflare namespace name. Namespaces are implicit on Cloudflare — the
27
+ * first repo created against this name conjures the namespace.
28
+ *
29
+ * Must be 3–63 lowercase alphanumeric characters or hyphens, and must start
30
+ * and end with a lowercase alphanumeric character. If omitted, a unique
31
+ * physical name is generated from the resource's logical id, app, and stage.
32
+ */
33
+ namespace?: string;
34
+ };
35
+
36
+ /**
37
+ * Marker for a Cloudflare Artifacts namespace binding.
38
+ *
39
+ * Artifacts namespaces are implicit (created on first repo write) and require
40
+ * no deploy-time provisioning, so this is a pure binding marker rather than
41
+ * a full Resource. The Worker provider sees this object in `bindings: { ... }`
42
+ * and emits the corresponding `{ type: "artifacts", name, namespace }` binding
43
+ * to the script.
44
+ */
45
+ export type Artifacts = {
46
+ kind: ArtifactsTypeId;
47
+ name: string;
48
+ namespace: string;
49
+ };
50
+
51
+ export const isArtifacts = (value: unknown): value is Artifacts =>
52
+ typeof value === "object" &&
53
+ value !== null &&
54
+ "kind" in value &&
55
+ (value as Artifacts).kind === ArtifactsTypeId;
56
+
57
+ /**
58
+ * A Cloudflare Artifacts namespace — the top-level container for Git-compatible
59
+ * versioned repositories. See the
60
+ * {@link https://blog.cloudflare.com/artifacts-git-for-agents-beta/ | Artifacts launch post}
61
+ * and {@link https://developers.cloudflare.com/artifacts/concepts/namespaces/ | Namespaces docs}.
62
+ *
63
+ * Namespaces on Cloudflare are **implicit**: there is no `POST /namespaces`
64
+ * endpoint. The namespace is conjured the first time a repo is created against
65
+ * it (either via the REST API or the Worker binding). Because of that, the
66
+ * Alchemy "resource" is a thin binding marker — there is nothing to provision
67
+ * at deploy time. Repos themselves are typically created at runtime through
68
+ * the bound `Artifacts` API.
69
+ *
70
+ * @section Declaring a Namespace
71
+ * @example Default namespace (a unique physical name is generated)
72
+ * ```typescript
73
+ * const Repos = Cloudflare.Artifacts("Repos");
74
+ * ```
75
+ *
76
+ * @example Override the namespace name (must be lowercase, 3–63 chars)
77
+ * ```typescript
78
+ * const Repos = Cloudflare.Artifacts("Repos", { namespace: "starter-repos" });
79
+ * ```
80
+ *
81
+ * @section Binding to a Worker
82
+ * @example Wiring it into a Worker
83
+ * ```typescript
84
+ * export const Worker = Cloudflare.Worker("Worker", {
85
+ * main: "./src/worker.ts",
86
+ * bindings: { Repos },
87
+ * });
88
+ *
89
+ * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
90
+ * // { Repos: Artifacts }
91
+ * ```
92
+ *
93
+ * @example Async-style worker
94
+ * ```typescript
95
+ * export default {
96
+ * async fetch(request: Request, env: WorkerEnv) {
97
+ * const repo = await env.Repos.create("starter-repo");
98
+ * return Response.json({ remote: repo.remote, token: repo.token });
99
+ * },
100
+ * };
101
+ * ```
102
+ *
103
+ * @example Effect-style worker
104
+ * ```typescript
105
+ * const artifacts = yield* Cloudflare.Artifacts.bind(Repos);
106
+ * const repo = yield* artifacts.create("starter-repo", {
107
+ * setDefaultBranch: "main",
108
+ * });
109
+ * ```
110
+ */
111
+ export const Artifacts: {
112
+ (
113
+ name: string,
114
+ props?: ArtifactsProps,
115
+ ): Effect.Effect<Artifacts, never, Stack | Stage>;
116
+ /**
117
+ * Bind a Cloudflare Artifacts namespace to the surrounding Worker, returning
118
+ * an Effect-native client. See {@link ArtifactsBinding}.
119
+ */
120
+ bind: typeof ArtifactsBinding.bind;
121
+ } = Object.assign(
122
+ Effect.fn(function* (name: string, props?: ArtifactsProps) {
123
+ const namespace = props?.namespace
124
+ ? props.namespace
125
+ : name.toLocaleLowerCase();
126
+ if (!ARTIFACTS_NAMESPACE_REGEX.test(namespace)) {
127
+ return yield* Effect.die(new InvalidArtifactsNamespaceError(namespace));
128
+ }
129
+ return {
130
+ kind: ArtifactsTypeId,
131
+ name,
132
+ namespace,
133
+ } satisfies Artifacts;
134
+ }),
135
+ {
136
+ bind: (...args: Parameters<typeof ArtifactsBinding.bind>) =>
137
+ ArtifactsBinding.bind(...args),
138
+ },
139
+ );
@@ -0,0 +1,188 @@
1
+ import * as Data from "effect/Data";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Binding from "../../Binding.ts";
5
+ import type { ResourceLike } from "../../Resource.ts";
6
+ import { isWorker, WorkerEnvironment } from "../Workers/Worker.ts";
7
+ import { type Artifacts as ArtifactsLike } from "./Artifacts.ts";
8
+
9
+ export class ArtifactsError extends Data.TaggedError("ArtifactsError")<{
10
+ message: string;
11
+ cause: Error;
12
+ }> {}
13
+
14
+ export type Scope = "read" | "write";
15
+
16
+ export type ArtifactsCreateOptions = {
17
+ readOnly?: boolean;
18
+ description?: string;
19
+ setDefaultBranch?: string;
20
+ };
21
+
22
+ export type ArtifactsImportOptions = {
23
+ source: { url: string; branch?: string; depth?: number };
24
+ target: {
25
+ name: string;
26
+ opts?: { description?: string; readOnly?: boolean };
27
+ };
28
+ };
29
+
30
+ export type ArtifactsListOptions = {
31
+ limit?: number;
32
+ cursor?: string;
33
+ };
34
+
35
+ export type ArtifactsForkOptions = {
36
+ description?: string;
37
+ readOnly?: boolean;
38
+ defaultBranchOnly?: boolean;
39
+ };
40
+
41
+ /**
42
+ * Effect-native handle to a single Artifacts repo. Wraps the runtime
43
+ * {@link ArtifactsRepo} so each method returns an Effect.
44
+ */
45
+ export interface ArtifactsRepoClient {
46
+ /** Underlying Cloudflare runtime handle. */
47
+ raw: ArtifactsRepo;
48
+ createToken(
49
+ scope?: Scope,
50
+ ttl?: number,
51
+ ): Effect.Effect<ArtifactsCreateTokenResult, ArtifactsError>;
52
+ listTokens(): Effect.Effect<ArtifactsTokenListResult, ArtifactsError>;
53
+ revokeToken(tokenOrId: string): Effect.Effect<boolean, ArtifactsError>;
54
+ fork(
55
+ name: string,
56
+ opts?: ArtifactsForkOptions,
57
+ ): Effect.Effect<ArtifactsCreateRepoResult, ArtifactsError>;
58
+ }
59
+
60
+ /**
61
+ * Effect-native client for a Cloudflare Artifacts namespace binding.
62
+ *
63
+ * Wraps the runtime {@link Artifacts} binding so each method returns an
64
+ * Effect tagged with {@link ArtifactsError}. Use
65
+ * `Cloudflare.ArtifactsBinding.bind(Repos)` inside a Worker's init phase.
66
+ */
67
+ export interface ArtifactsClient {
68
+ /** Effect resolving to the raw Cloudflare runtime binding. */
69
+ raw: Effect.Effect<Artifacts, never, WorkerEnvironment>;
70
+ create(
71
+ name: string,
72
+ opts?: ArtifactsCreateOptions,
73
+ ): Effect.Effect<
74
+ ArtifactsCreateRepoResult,
75
+ ArtifactsError,
76
+ WorkerEnvironment
77
+ >;
78
+ /** Look up an existing repo by name. Fails with `ArtifactsError` if missing. */
79
+ get(
80
+ name: string,
81
+ ): Effect.Effect<ArtifactsRepoClient, ArtifactsError, WorkerEnvironment>;
82
+ list(
83
+ opts?: ArtifactsListOptions,
84
+ ): Effect.Effect<ArtifactsRepoListResult, ArtifactsError, WorkerEnvironment>;
85
+ delete(
86
+ name: string,
87
+ ): Effect.Effect<boolean, ArtifactsError, WorkerEnvironment>;
88
+ import(
89
+ opts: ArtifactsImportOptions,
90
+ ): Effect.Effect<
91
+ ArtifactsCreateRepoResult,
92
+ ArtifactsError,
93
+ WorkerEnvironment
94
+ >;
95
+ }
96
+
97
+ export class ArtifactsBinding extends Binding.Service<
98
+ ArtifactsBinding,
99
+ (artifacts: ArtifactsLike) => Effect.Effect<ArtifactsClient>
100
+ >()("Cloudflare.Artifacts.Binding") {}
101
+
102
+ export const ArtifactsBindingLive = Layer.effect(
103
+ ArtifactsBinding,
104
+ Effect.gen(function* () {
105
+ const Policy = yield* ArtifactsBindingPolicy;
106
+
107
+ return Effect.fn(function* (artifacts: ArtifactsLike) {
108
+ yield* Policy(artifacts);
109
+ const env = WorkerEnvironment.asEffect();
110
+ const raw = env.pipe(
111
+ Effect.map(
112
+ (env) => (env as Record<string, Artifacts>)[artifacts.name]!,
113
+ ),
114
+ );
115
+
116
+ const use = <T>(
117
+ fn: (raw: Artifacts) => Promise<T>,
118
+ ): Effect.Effect<T, ArtifactsError, WorkerEnvironment> =>
119
+ raw.pipe(Effect.flatMap((raw) => tryPromise(() => fn(raw))));
120
+
121
+ return {
122
+ raw,
123
+ create: (name, opts) => use((raw) => raw.create(name, opts)),
124
+ get: (name) =>
125
+ use((raw) => raw.get(name)).pipe(
126
+ Effect.flatMap((repo) =>
127
+ repo == null
128
+ ? Effect.fail(
129
+ new ArtifactsError({
130
+ message: `Artifacts repo '${name}' not found`,
131
+ cause: new Error("not_found"),
132
+ }),
133
+ )
134
+ : Effect.succeed(wrapRepo(repo as ArtifactsRepo)),
135
+ ),
136
+ ),
137
+ list: (opts) => use((raw) => raw.list(opts)),
138
+ delete: (name) => use((raw) => raw.delete(name)),
139
+ import: (opts) => use((raw) => raw.import(opts)),
140
+ } satisfies ArtifactsClient;
141
+ });
142
+ }),
143
+ );
144
+
145
+ export class ArtifactsBindingPolicy extends Binding.Policy<
146
+ ArtifactsBindingPolicy,
147
+ (artifacts: ArtifactsLike) => Effect.Effect<void>
148
+ >()("Cloudflare.Artifacts.Binding") {}
149
+
150
+ export const ArtifactsBindingPolicyLive = ArtifactsBindingPolicy.layer.succeed(
151
+ Effect.fn(function* (host: ResourceLike, artifacts: ArtifactsLike) {
152
+ if (isWorker(host)) {
153
+ yield* host.bind(artifacts.name, {
154
+ bindings: [
155
+ {
156
+ type: "artifacts",
157
+ name: artifacts.name,
158
+ namespace: artifacts.namespace,
159
+ } as any,
160
+ ],
161
+ });
162
+ } else {
163
+ return yield* Effect.die(
164
+ new Error(`ArtifactsBinding does not support runtime '${host.Type}'`),
165
+ );
166
+ }
167
+ }),
168
+ );
169
+
170
+ const tryPromise = <T>(
171
+ fn: () => Promise<T>,
172
+ ): Effect.Effect<T, ArtifactsError> =>
173
+ Effect.tryPromise({
174
+ try: fn,
175
+ catch: (error: any) =>
176
+ new ArtifactsError({
177
+ message: error?.message ?? "Unknown error",
178
+ cause: error,
179
+ }),
180
+ });
181
+
182
+ const wrapRepo = (raw: ArtifactsRepo): ArtifactsRepoClient => ({
183
+ raw,
184
+ createToken: (scope, ttl) => tryPromise(() => raw.createToken(scope, ttl)),
185
+ listTokens: () => tryPromise(() => raw.listTokens()),
186
+ revokeToken: (tokenOrId) => tryPromise(() => raw.revokeToken(tokenOrId)),
187
+ fork: (name, opts) => tryPromise(() => raw.fork(name, opts)),
188
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./Artifacts.ts";
2
+ export * from "./ArtifactsBinding.ts";
@@ -4,6 +4,7 @@ import * as Build from "../Build/index.ts";
4
4
  import * as Provider from "../Provider.ts";
5
5
  import { Random, RandomProvider } from "../Random.ts";
6
6
  import * as ApiToken from "./ApiToken/index.ts";
7
+ import * as Artifacts from "./Artifacts/index.ts";
7
8
  import { CloudflareAuth } from "./Auth/AuthProvider.ts";
8
9
  import * as CloudflareEnvironment from "./CloudflareEnvironment.ts";
9
10
  import * as Containers from "./Container/index.ts";
@@ -33,6 +34,7 @@ export const providers = () =>
33
34
  Provider.collection([
34
35
  ApiToken.AccountApiToken,
35
36
  ApiToken.UserApiToken,
37
+ Artifacts.ArtifactsBindingPolicy,
36
38
  Command,
37
39
  Containers.Container,
38
40
  D1.D1ConnectionPolicy,
@@ -58,6 +60,7 @@ export const providers = () =>
58
60
  Layer.mergeAll(
59
61
  ApiToken.AccountApiTokenProvider(),
60
62
  ApiToken.UserApiTokenProvider(),
63
+ Artifacts.ArtifactsBindingPolicyLive,
61
64
  Containers.ContainerProvider(),
62
65
  D1.D1ConnectionPolicyLive,
63
66
  D1.DatabaseProvider(),
@@ -1,5 +1,4 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
- // import type * as cf from "@cloudflare/workers-types";
3
2
 
4
3
  import type * as Effect from "effect/Effect";
5
4
  import type { UnwrapEffect } from "../../Util/effect.ts";
@@ -24,6 +23,8 @@ type GetBindingType<T> = T extends Cloudflare.Assets
24
23
  ? KVNamespace
25
24
  : T extends Cloudflare.Queue
26
25
  ? Queue<unknown>
27
- : T extends Cloudflare.DurableObjectNamespaceLike
28
- ? DurableObjectNamespace<Exclude<T["Shape"], undefined>>
29
- : never;
26
+ : T extends Cloudflare.Artifacts
27
+ ? Artifacts
28
+ : T extends Cloudflare.DurableObjectNamespaceLike
29
+ ? DurableObjectNamespace<Exclude<T["Shape"], undefined>>
30
+ : never;
@@ -24,6 +24,10 @@ import Sonda from "sonda/rolldown";
24
24
  import type * as vite from "vite";
25
25
  import * as Artifacts from "../../Artifacts.ts";
26
26
  import * as Binding from "../../Binding.ts";
27
+ import {
28
+ isArtifacts as isArtifactsBinding,
29
+ type Artifacts as ArtifactsBinding,
30
+ } from "../Artifacts/Artifacts.ts";
27
31
  import { hashDirectory, type MemoOptions } from "../../Build/Memo.ts";
28
32
  import * as Bundle from "../../Bundle/Bundle.ts";
29
33
  import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
@@ -183,6 +187,7 @@ export type WorkerBindingResource =
183
187
  | D1Database
184
188
  | KVNamespace
185
189
  | CloudflareQueue
190
+ | ArtifactsBinding
186
191
  | DurableObjectNamespaceLike<any>;
187
192
 
188
193
  export type WorkerBindings = {
@@ -667,7 +672,13 @@ export const Worker: Platform<
667
672
  type: "assets",
668
673
  name: bindingName,
669
674
  }
670
- : isDurableObjectNamespaceLike(binding)
675
+ : isArtifactsBinding(binding)
676
+ ? ({
677
+ type: "artifacts",
678
+ name: bindingName,
679
+ namespace: binding.namespace,
680
+ } as any)
681
+ : isDurableObjectNamespaceLike(binding)
671
682
  ? {
672
683
  type: "durable_object_namespace",
673
684
  name: bindingName,
@@ -1,4 +1,5 @@
1
1
  export * from "./ApiToken/index.ts";
2
+ export * from "./Artifacts/index.ts";
2
3
  export * from "./CloudflareEnvironment.ts";
3
4
  export * from "./Container/index.ts";
4
5
  export * from "./D1/index.ts";