alchemy 0.93.4 → 0.93.7

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 (73) hide show
  1. package/bin/alchemy.js +1966 -1825
  2. package/bin/alchemy.ts +2 -0
  3. package/bin/commands/state.ts +58 -0
  4. package/bin/services/execute-alchemy.ts +9 -1
  5. package/lib/alchemy.d.ts.map +1 -1
  6. package/lib/alchemy.js +7 -0
  7. package/lib/alchemy.js.map +1 -1
  8. package/lib/cloudflare/access-application.d.ts +204 -0
  9. package/lib/cloudflare/access-application.d.ts.map +1 -0
  10. package/lib/cloudflare/access-application.js +205 -0
  11. package/lib/cloudflare/access-application.js.map +1 -0
  12. package/lib/cloudflare/access-group.d.ts +115 -0
  13. package/lib/cloudflare/access-group.d.ts.map +1 -0
  14. package/lib/cloudflare/access-group.js +147 -0
  15. package/lib/cloudflare/access-group.js.map +1 -0
  16. package/lib/cloudflare/access-identity-provider.d.ts +261 -0
  17. package/lib/cloudflare/access-identity-provider.d.ts.map +1 -0
  18. package/lib/cloudflare/access-identity-provider.js +229 -0
  19. package/lib/cloudflare/access-identity-provider.js.map +1 -0
  20. package/lib/cloudflare/access-policy.d.ts +168 -0
  21. package/lib/cloudflare/access-policy.d.ts.map +1 -0
  22. package/lib/cloudflare/access-policy.js +176 -0
  23. package/lib/cloudflare/access-policy.js.map +1 -0
  24. package/lib/cloudflare/access-rule.d.ts +248 -0
  25. package/lib/cloudflare/access-rule.d.ts.map +1 -0
  26. package/lib/cloudflare/access-rule.js +37 -0
  27. package/lib/cloudflare/access-rule.js.map +1 -0
  28. package/lib/cloudflare/access-service-token.d.ts +86 -0
  29. package/lib/cloudflare/access-service-token.d.ts.map +1 -0
  30. package/lib/cloudflare/access-service-token.js +127 -0
  31. package/lib/cloudflare/access-service-token.js.map +1 -0
  32. package/lib/cloudflare/index.d.ts +6 -0
  33. package/lib/cloudflare/index.d.ts.map +1 -1
  34. package/lib/cloudflare/index.js +6 -0
  35. package/lib/cloudflare/index.js.map +1 -1
  36. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  37. package/lib/cloudflare/miniflare/build-worker-options.js +17 -3
  38. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  39. package/lib/state/file-system-state-store.d.ts +1 -0
  40. package/lib/state/file-system-state-store.d.ts.map +1 -1
  41. package/lib/state/file-system-state-store.js +16 -0
  42. package/lib/state/file-system-state-store.js.map +1 -1
  43. package/lib/state/operations.d.ts +1 -0
  44. package/lib/state/operations.d.ts.map +1 -1
  45. package/lib/state/operations.js +24 -0
  46. package/lib/state/operations.js.map +1 -1
  47. package/lib/state/proxy.d.ts +1 -0
  48. package/lib/state/proxy.d.ts.map +1 -1
  49. package/lib/state/proxy.js +3 -0
  50. package/lib/state/proxy.js.map +1 -1
  51. package/lib/state-cli.d.ts +9 -0
  52. package/lib/state-cli.d.ts.map +1 -0
  53. package/lib/state-cli.js +110 -0
  54. package/lib/state-cli.js.map +1 -0
  55. package/lib/state.d.ts +2 -0
  56. package/lib/state.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/alchemy.ts +9 -0
  59. package/src/cloudflare/access-application.ts +489 -0
  60. package/src/cloudflare/access-group.ts +271 -0
  61. package/src/cloudflare/access-identity-provider.ts +584 -0
  62. package/src/cloudflare/access-policy.ts +392 -0
  63. package/src/cloudflare/access-rule.ts +307 -0
  64. package/src/cloudflare/access-service-token.ts +255 -0
  65. package/src/cloudflare/index.ts +6 -0
  66. package/src/cloudflare/miniflare/build-worker-options.ts +20 -3
  67. package/src/state/file-system-state-store.ts +16 -0
  68. package/src/state/operations.ts +22 -0
  69. package/src/state/proxy.ts +4 -0
  70. package/src/state-cli.ts +134 -0
  71. package/src/state.ts +2 -0
  72. package/workers/cloudflare-state-store.js +17 -0
  73. package/workers/tunnel-proxy.js +1 -1
@@ -0,0 +1,255 @@
1
+ import { alchemy } from "../alchemy.ts";
2
+ import type { Context } from "../context.ts";
3
+ import { Resource, ResourceKind } from "../resource.ts";
4
+ import type { Secret } from "../secret.ts";
5
+ import { logger } from "../util/logger.ts";
6
+ import { isCloudflareApiError } from "./api-error.ts";
7
+ import {
8
+ extractCloudflareResult,
9
+ type CloudflareApiListResponse,
10
+ } from "./api-response.ts";
11
+ import {
12
+ createCloudflareApi,
13
+ type CloudflareApi,
14
+ type CloudflareApiOptions,
15
+ } from "./api.ts";
16
+
17
+ /**
18
+ * Properties for creating or updating an {@link AccessServiceToken}.
19
+ */
20
+ export interface AccessServiceTokenProps extends CloudflareApiOptions {
21
+ /**
22
+ * Display name of the service token.
23
+ *
24
+ * @default ${app}-${stage}-${id}
25
+ */
26
+ name?: string;
27
+
28
+ /**
29
+ * How long the token is valid for. Format: a Cloudflare duration string
30
+ * such as `"24h"`, `"30d"`, or `"8760h"` (one year).
31
+ *
32
+ * @default "8760h"
33
+ */
34
+ duration?: string;
35
+
36
+ /**
37
+ * Adopt an existing service token with the same name instead of failing
38
+ * with a duplicate-name error.
39
+ *
40
+ * Note: when adopting, the `clientSecret` cannot be recovered — the output
41
+ * `clientSecret` will be `undefined`. Recreate the resource (or rotate via
42
+ * the Cloudflare dashboard) if you need a new secret.
43
+ *
44
+ * @default false
45
+ */
46
+ adopt?: boolean;
47
+
48
+ /**
49
+ * Whether to delete the token when removed from Alchemy.
50
+ *
51
+ * @default true
52
+ */
53
+ delete?: boolean;
54
+ }
55
+
56
+ /**
57
+ * Output for an {@link AccessServiceToken}.
58
+ */
59
+ export type AccessServiceToken = Omit<
60
+ AccessServiceTokenProps,
61
+ "delete" | "adopt"
62
+ > & {
63
+ /** Cloudflare-assigned token UUID. */
64
+ id: string;
65
+ /** Display name of the token. */
66
+ name: string;
67
+ /**
68
+ * Value sent in the `CF-Access-Client-Id` header when authenticating.
69
+ */
70
+ clientId: string;
71
+ /**
72
+ * Value sent in the `CF-Access-Client-Secret` header when authenticating.
73
+ * **Returned only on creation** — `undefined` for adopted tokens.
74
+ */
75
+ clientSecret?: Secret;
76
+ /** ISO 8601 creation timestamp. */
77
+ createdAt: string;
78
+ /** ISO 8601 last-update timestamp. */
79
+ updatedAt: string;
80
+ };
81
+
82
+ /**
83
+ * Type guard for {@link AccessServiceToken}.
84
+ */
85
+ export function isAccessServiceToken(
86
+ resource: any,
87
+ ): resource is AccessServiceToken {
88
+ return resource?.[ResourceKind] === "cloudflare::AccessServiceToken";
89
+ }
90
+
91
+ interface CloudflareAccessServiceToken {
92
+ id: string;
93
+ name: string;
94
+ client_id: string;
95
+ client_secret?: string;
96
+ duration?: string;
97
+ created_at: string;
98
+ updated_at: string;
99
+ }
100
+
101
+ /**
102
+ * Creates a Cloudflare Zero Trust [Access service token](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/)
103
+ * for machine-to-machine authentication against Access-protected applications.
104
+ *
105
+ * The returned `clientSecret` is only available on creation — store it
106
+ * immediately. Adopted tokens have `clientSecret: undefined`.
107
+ *
108
+ * @example
109
+ * // Basic service token (defaults to 1 year duration)
110
+ * const token = await AccessServiceToken("ci-token", {
111
+ * name: "ci-runner",
112
+ * });
113
+ *
114
+ * @example
115
+ * // Custom duration
116
+ * const shortLived = await AccessServiceToken("preview", {
117
+ * name: "preview-deploy-token",
118
+ * duration: "720h", // 30 days
119
+ * });
120
+ */
121
+ export const AccessServiceToken = Resource(
122
+ "cloudflare::AccessServiceToken",
123
+ async function (
124
+ this: Context<AccessServiceToken>,
125
+ id: string,
126
+ props: AccessServiceTokenProps,
127
+ ): Promise<AccessServiceToken> {
128
+ const api = await createCloudflareApi(props);
129
+ const name = props.name ?? this.scope.createPhysicalName(id);
130
+ const basePath = `/accounts/${api.accountId}/access/service_tokens`;
131
+
132
+ if (this.phase === "delete") {
133
+ if (this.output?.id && props.delete !== false) {
134
+ await deleteAccessServiceToken(api, this.output.id);
135
+ }
136
+ return this.destroy();
137
+ }
138
+
139
+ const body: Record<string, unknown> = { name };
140
+ if (props.duration) body.duration = props.duration;
141
+
142
+ let token: CloudflareAccessServiceToken;
143
+ if (this.phase === "update" && this.output?.id) {
144
+ token = await extractCloudflareResult<CloudflareAccessServiceToken>(
145
+ `update access service token "${name}"`,
146
+ api.put(`${basePath}/${this.output.id}`, body),
147
+ );
148
+ } else {
149
+ const adopt = props.adopt ?? this.scope.adopt;
150
+ try {
151
+ token = await extractCloudflareResult<CloudflareAccessServiceToken>(
152
+ `create access service token "${name}"`,
153
+ api.post(basePath, body),
154
+ );
155
+ } catch (err) {
156
+ if (adopt && isAccessDuplicateNameError(err)) {
157
+ const existing = await findAccessServiceTokenByName(api, name);
158
+ if (!existing) {
159
+ throw new Error(
160
+ `Service token "${name}" already exists but could not be found for adoption.`,
161
+ { cause: err },
162
+ );
163
+ }
164
+ logger.log(
165
+ `Adopting existing access service token "${name}" (${existing.id})`,
166
+ );
167
+ token = await extractCloudflareResult<CloudflareAccessServiceToken>(
168
+ `adopt access service token "${name}"`,
169
+ api.put(`${basePath}/${existing.id}`, body),
170
+ );
171
+ } else {
172
+ throw err;
173
+ }
174
+ }
175
+ }
176
+
177
+ // clientSecret is only returned on creation; retain on subsequent updates.
178
+ const clientSecret = token.client_secret
179
+ ? alchemy.secret(token.client_secret)
180
+ : this.output?.clientSecret;
181
+
182
+ return {
183
+ id: token.id,
184
+ name: token.name,
185
+ clientId: token.client_id,
186
+ clientSecret,
187
+ duration: token.duration,
188
+ createdAt: token.created_at,
189
+ updatedAt: token.updated_at,
190
+ };
191
+ },
192
+ );
193
+
194
+ /**
195
+ * Cloudflare returns error code 12132 ("Access service token already exists")
196
+ * for duplicate names.
197
+ */
198
+ function isAccessDuplicateNameError(err: unknown): boolean {
199
+ if (
200
+ isCloudflareApiError(err, { status: 409 }) ||
201
+ isCloudflareApiError(err, { status: 400 })
202
+ ) {
203
+ const data = err.errorData;
204
+ if (
205
+ Array.isArray(data) &&
206
+ data.some(
207
+ (e) => "message" in e && /already exists/i.test(String(e.message)),
208
+ )
209
+ ) {
210
+ return true;
211
+ }
212
+ }
213
+ return false;
214
+ }
215
+
216
+ /**
217
+ * Look up an existing service token by name across paginated results.
218
+ */
219
+ async function findAccessServiceTokenByName(
220
+ api: CloudflareApi,
221
+ name: string,
222
+ ): Promise<CloudflareAccessServiceToken | null> {
223
+ let page = 1;
224
+ const perPage = 50;
225
+ while (true) {
226
+ const response = await api.get(
227
+ `/accounts/${api.accountId}/access/service_tokens?page=${page}&per_page=${perPage}`,
228
+ );
229
+ if (!response.ok) return null;
230
+ const data =
231
+ (await response.json()) as CloudflareApiListResponse<CloudflareAccessServiceToken>;
232
+ const match = data.result.find((t) => t.name === name);
233
+ if (match) return match;
234
+ const info = data.result_info;
235
+ if (!info || info.page * info.per_page >= info.total_count) return null;
236
+ page++;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Delete a service token. No-op on 404.
242
+ */
243
+ async function deleteAccessServiceToken(
244
+ api: CloudflareApi,
245
+ tokenId: string,
246
+ ): Promise<void> {
247
+ const response = await api.delete(
248
+ `/accounts/${api.accountId}/access/service_tokens/${tokenId}`,
249
+ );
250
+ if (!response.ok && response.status !== 404) {
251
+ logger.error(
252
+ `Error deleting access service token ${tokenId}: ${response.status} ${response.statusText}`,
253
+ );
254
+ }
255
+ }
@@ -1,5 +1,11 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
2
 
3
+ export * from "./access-application.ts";
4
+ export * from "./access-group.ts";
5
+ export * from "./access-identity-provider.ts";
6
+ export * from "./access-policy.ts";
7
+ export * from "./access-rule.ts";
8
+ export * from "./access-service-token.ts";
3
9
  export * from "./account-api-token.ts";
4
10
  export * from "./account-id.ts";
5
11
  export * from "./ai-crawler.ts";
@@ -1,9 +1,11 @@
1
1
  import * as miniflare from "miniflare";
2
2
  import path from "pathe";
3
+ import { DockerApi } from "../../docker/api.ts";
3
4
  import { assertNever } from "../../util/assert-never.ts";
4
5
  import { reservePort } from "../../util/find-open-port.ts";
5
6
  import type { HTTPServer } from "../../util/http.ts";
6
7
  import { logger } from "../../util/logger.ts";
8
+ import { memoize } from "../../util/memoize.ts";
7
9
  import { isAiSearchNamespace } from "../ai-search-namespace.ts";
8
10
  import { isAiSearch } from "../ai-search.ts";
9
11
  import type { CloudflareApi } from "../api.ts";
@@ -18,6 +20,11 @@ import type { WorkerBundle, WorkerBundleSource } from "../worker-bundle.ts";
18
20
  import type { AssetsConfig } from "../worker.ts";
19
21
  import { createRemoteProxyWorker } from "./remote-binding-proxy.ts";
20
22
 
23
+ // Copied from @cloudflare/containers-shared:
24
+ // https://github.com/cloudflare/workers-sdk/blob/fc2d8838b3c5395d5d248e8073a393f7dbb9536d/packages/containers-shared/src/images.ts#L20
25
+ const CONTAINER_EGRESS_INTERCEPTOR_IMAGE =
26
+ "cloudflare/proxy-everything:3cb1195@sha256:0ef6716c52430096900b150d84a3302057d6cd2319dae7987128c85d0733e3c8";
27
+
21
28
  export interface MiniflareWorkerInput {
22
29
  api: CloudflareApi;
23
30
  id: string;
@@ -189,9 +196,7 @@ export const buildWorkerOptions = async (
189
196
  },
190
197
  };
191
198
  options.containerEngine = {
192
- localDocker: {
193
- socketPath: await getDockerSocketPath(),
194
- },
199
+ localDocker: await getLocalDocker(),
195
200
  };
196
201
  break;
197
202
  }
@@ -632,6 +637,18 @@ const isRemoteBinding = (binding: Binding) => {
632
637
  );
633
638
  };
634
639
 
640
+ const getLocalDocker = memoize(async () => {
641
+ const docker = new DockerApi();
642
+ const [socketPath] = await Promise.all([
643
+ getDockerSocketPath(),
644
+ docker.pullImage(CONTAINER_EGRESS_INTERCEPTOR_IMAGE),
645
+ ]);
646
+ return {
647
+ socketPath,
648
+ containerEgressInterceptorImage: CONTAINER_EGRESS_INTERCEPTOR_IMAGE,
649
+ };
650
+ });
651
+
635
652
  /**
636
653
  * DOCKER_HOST env is standardized
637
654
  * docker has an option to expose on tcp://localhost:2375; so we check 2375 if the user has it enabled
@@ -106,6 +106,22 @@ export class FileSystemStateStore implements StateStore {
106
106
  }
107
107
  }
108
108
 
109
+ async listScopes(): Promise<string[]> {
110
+ try {
111
+ const entries = await fs.promises.readdir(this.dir, {
112
+ withFileTypes: true,
113
+ });
114
+ return entries
115
+ .filter((dirent) => dirent.isDirectory())
116
+ .map((dirent) => dirent.name);
117
+ } catch (error: any) {
118
+ if (error.code === "ENOENT") {
119
+ return [];
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+
109
125
  async all(): Promise<Record<string, State>> {
110
126
  return this.getBatch(await this.list());
111
127
  }
@@ -30,6 +30,8 @@ export class SQLiteStateStoreOperations implements StateStore {
30
30
  return;
31
31
  case "list":
32
32
  return this.list();
33
+ case "listScopes":
34
+ return this.listScopes();
33
35
  case "count":
34
36
  return this.count();
35
37
  case "get": {
@@ -62,6 +64,26 @@ export class SQLiteStateStoreOperations implements StateStore {
62
64
  .where(eq(schema.resources.scope, this.context.chain));
63
65
  return result.map((r) => r.id);
64
66
  }
67
+ async listScopes() {
68
+ const rows = await this.db
69
+ .selectDistinct({ scope: schema.resources.scope })
70
+ .from(schema.resources);
71
+ const prefix = this.context.chain;
72
+ const names = new Set<string>();
73
+ for (const row of rows) {
74
+ const scope = row.scope as string[];
75
+ if (scope.length !== prefix.length + 1) continue;
76
+ let matches = true;
77
+ for (let i = 0; i < prefix.length; i++) {
78
+ if (scope[i] !== prefix[i]) {
79
+ matches = false;
80
+ break;
81
+ }
82
+ }
83
+ if (matches) names.add(scope[prefix.length]!);
84
+ }
85
+ return [...names];
86
+ }
65
87
  async count() {
66
88
  const result = await this.db.$count(
67
89
  this.db
@@ -39,6 +39,10 @@ export abstract class StateStoreProxy implements StateStore {
39
39
  return this.run("list", []);
40
40
  }
41
41
 
42
+ listScopes(): Promise<string[]> {
43
+ return this.run("listScopes", []);
44
+ }
45
+
42
46
  count(): Promise<number> {
43
47
  return this.run("count", []);
44
48
  }
@@ -0,0 +1,134 @@
1
+ import { Scope } from "./scope.ts";
2
+ import type { State } from "./state.ts";
3
+
4
+ export type StateCommand = "tree" | "list" | "get";
5
+
6
+ interface StateNode {
7
+ resources: string[];
8
+ children: Record<string, StateNode>;
9
+ }
10
+
11
+ async function walk(scope: Scope): Promise<StateNode> {
12
+ const resources = await scope.state.list();
13
+ const children: Record<string, StateNode> = {};
14
+ if (typeof scope.state.listScopes === "function") {
15
+ const childNames = await scope.state.listScopes();
16
+ for (const name of childNames) {
17
+ const childScope = new Scope({ parent: scope, scopeName: name });
18
+ children[name] = await walk(childScope);
19
+ }
20
+ }
21
+ return { resources, children };
22
+ }
23
+
24
+ function renderTree(rootName: string, node: StateNode): string {
25
+ const lines: string[] = [rootName];
26
+ appendNode(lines, node, "");
27
+ return lines.join("\n");
28
+ }
29
+
30
+ function appendNode(lines: string[], node: StateNode, indent: string) {
31
+ const childEntries = Object.entries(node.children).sort(([a], [b]) =>
32
+ a.localeCompare(b),
33
+ );
34
+ const resources = [...node.resources].sort();
35
+ const total = resources.length + childEntries.length;
36
+ let i = 0;
37
+ for (const r of resources) {
38
+ const last = i === total - 1;
39
+ lines.push(`${indent}${last ? "└─" : "├─"} ${r}`);
40
+ i++;
41
+ }
42
+ for (const [name, child] of childEntries) {
43
+ const last = i === total - 1;
44
+ lines.push(`${indent}${last ? "└─" : "├─"} ${name}`);
45
+ appendNode(lines, child, `${indent}${last ? " " : "│ "}`);
46
+ i++;
47
+ }
48
+ }
49
+
50
+ function flatten(prefix: string[], node: StateNode, out: string[]) {
51
+ for (const r of [...node.resources].sort()) {
52
+ out.push([...prefix, r].join("/"));
53
+ }
54
+ for (const [name, child] of Object.entries(node.children).sort(([a], [b]) =>
55
+ a.localeCompare(b),
56
+ )) {
57
+ flatten([...prefix, name], child, out);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Run an `alchemy state` subcommand against the configured state store
63
+ * and exit. Invoked by `alchemy.ts` when the user's program is launched
64
+ * via `alchemy state ...` — the user's resources are never created.
65
+ */
66
+ export async function runStateCommand(
67
+ scope: Scope,
68
+ command: StateCommand,
69
+ arg?: string,
70
+ ): Promise<never> {
71
+ if (command === "get") {
72
+ const fqn = arg;
73
+ if (!fqn) {
74
+ console.error("alchemy state get: missing resource fqn");
75
+ return process.exit(1);
76
+ }
77
+ const state = await findByFqn(scope, fqn);
78
+ if (state === undefined) {
79
+ console.log(`(not found: ${fqn})`);
80
+ return process.exit(1);
81
+ }
82
+ console.log(JSON.stringify(state, replacer, 2));
83
+ return process.exit(0);
84
+ }
85
+
86
+ const node = await walk(scope);
87
+
88
+ if (command === "list") {
89
+ const out: string[] = [];
90
+ flatten(scope.chain, node, out);
91
+ if (out.length === 0) {
92
+ console.log("(no resources)");
93
+ } else {
94
+ for (const fqn of out) console.log(fqn);
95
+ }
96
+ return process.exit(0);
97
+ }
98
+
99
+ // tree
100
+ const rootName = scope.chain.join("/");
101
+ if (node.resources.length === 0 && Object.keys(node.children).length === 0) {
102
+ console.log(`${rootName}\n(empty)`);
103
+ return process.exit(0);
104
+ }
105
+ console.log(renderTree(rootName, node));
106
+ return process.exit(0);
107
+ }
108
+
109
+ async function findByFqn(
110
+ scope: Scope,
111
+ fqn: string,
112
+ ): Promise<State | undefined> {
113
+ const parts = fqn.split("/").filter(Boolean);
114
+ // Strip leading chain (app/stage/...) if user passed full fqn
115
+ const chain = scope.chain;
116
+ let path = parts;
117
+ if (parts.length > chain.length && chain.every((c, i) => c === parts[i])) {
118
+ path = parts.slice(chain.length);
119
+ }
120
+ if (path.length === 0) return undefined;
121
+ let current: Scope = scope;
122
+ for (let i = 0; i < path.length - 1; i++) {
123
+ current = new Scope({ parent: current, scopeName: path[i]! });
124
+ }
125
+ return current.state.get(path[path.length - 1]!);
126
+ }
127
+
128
+ function replacer(_key: string, value: unknown) {
129
+ if (typeof value === "bigint") return value.toString();
130
+ if (value && typeof value === "object" && (value as any).type === "Buffer") {
131
+ return "[Buffer]";
132
+ }
133
+ return value;
134
+ }
package/src/state.ts CHANGED
@@ -40,4 +40,6 @@ export interface StateStore {
40
40
  all(): Promise<Record<string, State>>;
41
41
  set(key: string, value: State): Promise<void>;
42
42
  delete(key: string): Promise<void>;
43
+ /** List names of nested scopes within this scope, if supported. */
44
+ listScopes?(): Promise<string[]>;
43
45
  }
@@ -4693,6 +4693,7 @@ var SQLiteStateStoreOperations = class {
4693
4693
  case "init": return;
4694
4694
  case "deinit": return;
4695
4695
  case "list": return this.list();
4696
+ case "listScopes": return this.listScopes();
4696
4697
  case "count": return this.count();
4697
4698
  case "get": {
4698
4699
  const [key] = params;
@@ -4717,6 +4718,22 @@ var SQLiteStateStoreOperations = class {
4717
4718
  async list() {
4718
4719
  return (await this.db.select({ id: resources.id }).from(resources).where(eq(resources.scope, this.context.chain))).map((r) => r.id);
4719
4720
  }
4721
+ async listScopes() {
4722
+ const rows = await this.db.selectDistinct({ scope: resources.scope }).from(resources);
4723
+ const prefix = this.context.chain;
4724
+ const names = /* @__PURE__ */ new Set();
4725
+ for (const row of rows) {
4726
+ const scope = row.scope;
4727
+ if (scope.length !== prefix.length + 1) continue;
4728
+ let matches = true;
4729
+ for (let i = 0; i < prefix.length; i++) if (scope[i] !== prefix[i]) {
4730
+ matches = false;
4731
+ break;
4732
+ }
4733
+ if (matches) names.add(scope[prefix.length]);
4734
+ }
4735
+ return [...names];
4736
+ }
4720
4737
  async count() {
4721
4738
  return await this.db.$count(this.db.select().from(resources).where(eq(resources.scope, this.context.chain)));
4722
4739
  }
@@ -83,7 +83,7 @@ const renderErrorHtml = (props) => `
83
83
  Alchemy</a>.</p>
84
84
  </div>
85
85
  <div class="bg-slate-200 px-5 py-3 flex flex-col w-full max-w-lg">
86
- <p class="text-sm text-slate-500">Alchemy 0.93.4</p>
86
+ <p class="text-sm text-slate-500">Alchemy 0.93.7</p>
87
87
  </div>
88
88
  </div>
89
89
  </body>