alchemy 0.24.0 → 0.24.1
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.
- package/lib/aws/function.d.ts.map +1 -1
- package/lib/aws/function.js +1 -4
- package/lib/aws/function.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +3 -3
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bindings.js.map +1 -1
- package/lib/cloudflare/bound.d.ts +3 -3
- package/lib/cloudflare/bound.d.ts.map +1 -1
- package/lib/cloudflare/do-state-store/index.d.ts +2 -0
- package/lib/cloudflare/do-state-store/index.d.ts.map +1 -0
- package/lib/cloudflare/do-state-store/index.js +2 -0
- package/lib/cloudflare/do-state-store/index.js.map +1 -0
- package/lib/cloudflare/do-state-store/internal.d.ts +19 -0
- package/lib/cloudflare/do-state-store/internal.d.ts.map +1 -0
- package/lib/cloudflare/do-state-store/internal.js +163 -0
- package/lib/cloudflare/do-state-store/internal.js.map +1 -0
- package/lib/cloudflare/do-state-store/store.d.ts +59 -0
- package/lib/cloudflare/do-state-store/store.d.ts.map +1 -0
- package/lib/cloudflare/do-state-store/store.js +120 -0
- package/lib/cloudflare/do-state-store/store.js.map +1 -0
- package/lib/cloudflare/do-state-store/types.d.ts +82 -0
- package/lib/cloudflare/do-state-store/types.d.ts.map +1 -0
- package/lib/cloudflare/do-state-store/types.js +1 -0
- package/lib/cloudflare/do-state-store/types.js.map +1 -0
- package/lib/cloudflare/do-state-store/worker.d.ts +27 -0
- package/lib/cloudflare/do-state-store/worker.d.ts.map +1 -0
- package/lib/cloudflare/do-state-store/worker.js +170 -0
- package/lib/cloudflare/do-state-store/worker.js.map +1 -0
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/pipeline.js +1 -1
- package/lib/cloudflare/pipeline.js.map +1 -1
- package/lib/cloudflare/queue-consumer.d.ts +14 -1
- package/lib/cloudflare/queue-consumer.d.ts.map +1 -1
- package/lib/cloudflare/queue-consumer.js +17 -0
- package/lib/cloudflare/queue-consumer.js.map +1 -1
- package/lib/cloudflare/website.d.ts +30 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +8 -5
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-metadata.js +1 -1
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +40 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +40 -38
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.js +1 -1
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/fs/file-system-state-store.d.ts +3 -1
- package/lib/fs/file-system-state-store.d.ts.map +1 -1
- package/lib/fs/file-system-state-store.js +2 -2
- package/lib/fs/file-system-state-store.js.map +1 -1
- package/lib/stripe/card.d.ts +171 -0
- package/lib/stripe/card.d.ts.map +1 -0
- package/lib/stripe/card.js +148 -0
- package/lib/stripe/card.js.map +1 -0
- package/lib/stripe/client.d.ts +9 -0
- package/lib/stripe/client.d.ts.map +1 -0
- package/lib/stripe/client.js +94 -0
- package/lib/stripe/client.js.map +1 -0
- package/lib/stripe/coupon.d.ts +135 -0
- package/lib/stripe/coupon.d.ts.map +1 -0
- package/lib/stripe/coupon.js +161 -0
- package/lib/stripe/coupon.js.map +1 -0
- package/lib/stripe/customer.d.ts +269 -0
- package/lib/stripe/customer.d.ts.map +1 -0
- package/lib/stripe/customer.js +289 -0
- package/lib/stripe/customer.js.map +1 -0
- package/lib/stripe/entitlements-feature.d.ts +84 -0
- package/lib/stripe/entitlements-feature.d.ts.map +1 -0
- package/lib/stripe/entitlements-feature.js +102 -0
- package/lib/stripe/entitlements-feature.js.map +1 -0
- package/lib/stripe/file.d.ts +143 -0
- package/lib/stripe/file.d.ts.map +1 -0
- package/lib/stripe/file.js +97 -0
- package/lib/stripe/file.js.map +1 -0
- package/lib/stripe/index.d.ts +11 -0
- package/lib/stripe/index.d.ts.map +1 -1
- package/lib/stripe/index.js +11 -0
- package/lib/stripe/index.js.map +1 -1
- package/lib/stripe/meter.d.ts +10 -1
- package/lib/stripe/meter.d.ts.map +1 -1
- package/lib/stripe/meter.js +13 -7
- package/lib/stripe/meter.js.map +1 -1
- package/lib/stripe/portal-configuration.d.ts +286 -0
- package/lib/stripe/portal-configuration.d.ts.map +1 -0
- package/lib/stripe/portal-configuration.js +320 -0
- package/lib/stripe/portal-configuration.js.map +1 -0
- package/lib/stripe/price.d.ts +10 -1
- package/lib/stripe/price.d.ts.map +1 -1
- package/lib/stripe/price.js +34 -13
- package/lib/stripe/price.js.map +1 -1
- package/lib/stripe/product-feature.d.ts +76 -0
- package/lib/stripe/product-feature.d.ts.map +1 -0
- package/lib/stripe/product-feature.js +74 -0
- package/lib/stripe/product-feature.js.map +1 -0
- package/lib/stripe/product.d.ts +10 -1
- package/lib/stripe/product.d.ts.map +1 -1
- package/lib/stripe/product.js +33 -14
- package/lib/stripe/product.js.map +1 -1
- package/lib/stripe/promotion-code.d.ts +138 -0
- package/lib/stripe/promotion-code.d.ts.map +1 -0
- package/lib/stripe/promotion-code.js +150 -0
- package/lib/stripe/promotion-code.js.map +1 -0
- package/lib/stripe/shipping-rate.d.ts +188 -0
- package/lib/stripe/shipping-rate.d.ts.map +1 -0
- package/lib/stripe/shipping-rate.js +198 -0
- package/lib/stripe/shipping-rate.js.map +1 -0
- package/lib/stripe/tax-rate.d.ts +130 -0
- package/lib/stripe/tax-rate.d.ts.map +1 -0
- package/lib/stripe/tax-rate.js +126 -0
- package/lib/stripe/tax-rate.js.map +1 -0
- package/lib/stripe/webhook.d.ts +10 -1
- package/lib/stripe/webhook.d.ts.map +1 -1
- package/lib/stripe/webhook.js +59 -14
- package/lib/stripe/webhook.js.map +1 -1
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/bun.js +2 -6
- package/lib/test/bun.js.map +1 -1
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +3 -7
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/retry.d.ts +1 -1
- package/lib/util/retry.d.ts.map +1 -1
- package/lib/util/retry.js.map +1 -1
- package/package.json +2 -1
- package/src/aws/function.ts +3 -6
- package/src/cloudflare/bindings.ts +3 -2
- package/src/cloudflare/bound.ts +3 -3
- package/src/cloudflare/do-state-store/index.ts +1 -0
- package/src/cloudflare/do-state-store/internal.ts +215 -0
- package/src/cloudflare/do-state-store/store.ts +188 -0
- package/src/cloudflare/do-state-store/types.ts +74 -0
- package/src/cloudflare/do-state-store/worker.ts +199 -0
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/pipeline.ts +1 -1
- package/src/cloudflare/queue-consumer.ts +59 -1
- package/src/cloudflare/website.ts +38 -6
- package/src/cloudflare/worker-metadata.ts +1 -1
- package/src/cloudflare/worker.ts +66 -46
- package/src/cloudflare/wrangler.json.ts +1 -1
- package/src/fs/file-system-state-store.ts +7 -2
- package/src/stripe/card.ts +320 -0
- package/src/stripe/client.ts +145 -0
- package/src/stripe/coupon.ts +280 -0
- package/src/stripe/customer.ts +528 -0
- package/src/stripe/entitlements-feature.ts +169 -0
- package/src/stripe/file.ts +219 -0
- package/src/stripe/index.ts +11 -0
- package/src/stripe/meter.ts +23 -7
- package/src/stripe/portal-configuration.ts +609 -0
- package/src/stripe/price.ts +47 -14
- package/src/stripe/product-feature.ts +150 -0
- package/src/stripe/product.ts +48 -15
- package/src/stripe/promotion-code.ts +268 -0
- package/src/stripe/shipping-rate.ts +362 -0
- package/src/stripe/tax-rate.ts +218 -0
- package/src/stripe/webhook.ts +82 -15
- package/src/test/bun.ts +2 -7
- package/src/test/vitest.ts +3 -8
- package/src/util/retry.ts +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { bundle } from "../../esbuild/index.ts";
|
|
3
|
+
import { withExponentialBackoff } from "../../util/retry.ts";
|
|
4
|
+
import { handleApiError } from "../api-error.ts";
|
|
5
|
+
import type { CloudflareApi } from "../api.ts";
|
|
6
|
+
import { putWorker } from "../worker.ts";
|
|
7
|
+
import type { DOStateStoreAPI } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
interface DOStateStoreClientOptions {
|
|
10
|
+
app: string;
|
|
11
|
+
stage: string;
|
|
12
|
+
url: string;
|
|
13
|
+
token: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class StateStoreError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
message: string,
|
|
19
|
+
readonly retryable: boolean,
|
|
20
|
+
) {
|
|
21
|
+
super(message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DOStateStoreClient {
|
|
26
|
+
constructor(private readonly options: DOStateStoreClientOptions) {}
|
|
27
|
+
|
|
28
|
+
async rpc<T extends keyof DOStateStoreAPI.API>(
|
|
29
|
+
method: T,
|
|
30
|
+
params: DOStateStoreAPI.API[T]["params"],
|
|
31
|
+
): Promise<DOStateStoreAPI.API[T]["result"]> {
|
|
32
|
+
return await withExponentialBackoff(
|
|
33
|
+
async () => {
|
|
34
|
+
const res = await this.fetch("/rpc", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
method,
|
|
42
|
+
params,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
if (!res.headers.get("Content-Type")?.includes("application/json")) {
|
|
46
|
+
throw new StateStoreError(
|
|
47
|
+
`Unexpected response of type "${res.headers.get("Content-Type")}" from state store: ${res.status} ${res.statusText} ${await res.text()}`,
|
|
48
|
+
true,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const body = await res.json<DOStateStoreAPI.Response>();
|
|
52
|
+
if (!body.success) {
|
|
53
|
+
throw new StateStoreError(
|
|
54
|
+
`State store "${method}" request failed with status ${res.status}: ${body.error}`,
|
|
55
|
+
res.status >= 500,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return body.result;
|
|
59
|
+
},
|
|
60
|
+
(error) => {
|
|
61
|
+
if (error instanceof StateStoreError) {
|
|
62
|
+
return error.retryable;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
5,
|
|
67
|
+
500,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async validate(): Promise<Response> {
|
|
72
|
+
return await this.fetch("/rpc", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
Accept: "application/json",
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
method: "validate",
|
|
80
|
+
params: null,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async fetch(path: string, init: RequestInit = {}): Promise<Response> {
|
|
86
|
+
const url = new URL(path, this.options.url);
|
|
87
|
+
url.searchParams.set("app", this.options.app);
|
|
88
|
+
url.searchParams.set("stage", this.options.stage);
|
|
89
|
+
return await fetch(url, {
|
|
90
|
+
...init,
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${this.options.token}`,
|
|
93
|
+
...init.headers,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const TAG = "alchemy-state-store:2025-06-03";
|
|
100
|
+
|
|
101
|
+
const cache = new Map<string, string>();
|
|
102
|
+
|
|
103
|
+
export async function upsertStateStoreWorker(
|
|
104
|
+
api: CloudflareApi,
|
|
105
|
+
workerName: string,
|
|
106
|
+
token: string,
|
|
107
|
+
) {
|
|
108
|
+
const key = `worker:${workerName}`;
|
|
109
|
+
const cached = cache.get(key);
|
|
110
|
+
if (cached === TAG) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { found, tag } = await getWorkerStatus(api, workerName);
|
|
114
|
+
if (found && tag === TAG) {
|
|
115
|
+
cache.set(key, TAG);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const script = await bundleWorkerScript();
|
|
119
|
+
await putWorker(api, workerName, script, {
|
|
120
|
+
main_module: "worker.js",
|
|
121
|
+
compatibility_date: "2025-06-01",
|
|
122
|
+
compatibility_flags: ["nodejs_compat"],
|
|
123
|
+
bindings: [
|
|
124
|
+
{
|
|
125
|
+
name: "DOFS_STATE_STORE",
|
|
126
|
+
type: "durable_object_namespace",
|
|
127
|
+
class_name: "DOFSStateStore",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "DOFS_TOKEN",
|
|
131
|
+
type: "secret_text",
|
|
132
|
+
text: token,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
migrations: !found
|
|
136
|
+
? {
|
|
137
|
+
new_sqlite_classes: ["DOFSStateStore"],
|
|
138
|
+
}
|
|
139
|
+
: undefined,
|
|
140
|
+
tags: [TAG],
|
|
141
|
+
observability: {
|
|
142
|
+
enabled: true,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const subdomainRes = await api.post(
|
|
146
|
+
`/accounts/${api.accountId}/workers/scripts/${workerName}/subdomain`,
|
|
147
|
+
{ enabled: true, preview_enabled: false },
|
|
148
|
+
{
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
if (!subdomainRes.ok) {
|
|
153
|
+
await handleApiError(
|
|
154
|
+
subdomainRes,
|
|
155
|
+
"creating worker subdomain",
|
|
156
|
+
"worker",
|
|
157
|
+
workerName,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
cache.set(key, TAG);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function getWorkerStatus(api: CloudflareApi, workerName: string) {
|
|
164
|
+
const res = await api.get(
|
|
165
|
+
`/accounts/${api.accountId}/workers/scripts/${workerName}/settings`,
|
|
166
|
+
);
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
return {
|
|
169
|
+
found: false,
|
|
170
|
+
tag: undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const json: {
|
|
174
|
+
result: {
|
|
175
|
+
tags: string[];
|
|
176
|
+
};
|
|
177
|
+
} = await res.json();
|
|
178
|
+
return {
|
|
179
|
+
found: true,
|
|
180
|
+
tag: json.result.tags.find((tag) => tag.startsWith("alchemy-state-store:")),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function getAccountSubdomain(api: CloudflareApi) {
|
|
185
|
+
const key = `subdomain:${api.accountId}`;
|
|
186
|
+
const cached = cache.get(key);
|
|
187
|
+
if (cached) {
|
|
188
|
+
return cached;
|
|
189
|
+
}
|
|
190
|
+
const res = await api.get(`/accounts/${api.accountId}/workers/subdomain`);
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Failed to get account subdomain: ${res.status} ${res.statusText}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const json: { result: { subdomain: string } } = await res.json();
|
|
197
|
+
const subdomain = json.result.subdomain;
|
|
198
|
+
cache.set(key, subdomain);
|
|
199
|
+
return subdomain;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function bundleWorkerScript() {
|
|
203
|
+
const result = await bundle({
|
|
204
|
+
entryPoint: path.join(__dirname, "worker.ts"),
|
|
205
|
+
bundle: true,
|
|
206
|
+
format: "esm",
|
|
207
|
+
target: "es2022",
|
|
208
|
+
external: ["cloudflare:*", "node:crypto"],
|
|
209
|
+
write: false,
|
|
210
|
+
});
|
|
211
|
+
if (!result.outputFiles?.[0]) {
|
|
212
|
+
throw new Error("Failed to bundle worker.ts");
|
|
213
|
+
}
|
|
214
|
+
return result.outputFiles[0].text;
|
|
215
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { alchemy } from "../../alchemy.ts";
|
|
2
|
+
import { ResourceScope } from "../../resource.ts";
|
|
3
|
+
import type { Scope } from "../../scope.ts";
|
|
4
|
+
import type { State, StateStore } from "../../state.ts";
|
|
5
|
+
import { deserializeState } from "../../state.ts";
|
|
6
|
+
import { createCloudflareApi, type CloudflareApiOptions } from "../api.ts";
|
|
7
|
+
import {
|
|
8
|
+
DOStateStoreClient,
|
|
9
|
+
getAccountSubdomain,
|
|
10
|
+
upsertStateStoreWorker,
|
|
11
|
+
} from "./internal.ts";
|
|
12
|
+
|
|
13
|
+
export interface DOStateStoreOptions extends CloudflareApiOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The prefix to use for state keys.
|
|
16
|
+
* Each app and stage has its own state store, so this is primarily for testing.
|
|
17
|
+
* @default "alchemy"
|
|
18
|
+
*/
|
|
19
|
+
prefix?: string;
|
|
20
|
+
worker?:
|
|
21
|
+
| {
|
|
22
|
+
/**
|
|
23
|
+
* The name of the worker to use
|
|
24
|
+
* @default "alchemy-state"
|
|
25
|
+
*/
|
|
26
|
+
name?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The token to use for the worker
|
|
29
|
+
* @default the value of the ALCHEMY_STATE_TOKEN environment variable
|
|
30
|
+
*/
|
|
31
|
+
token?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Whether to force the creation of a new worker
|
|
34
|
+
*/
|
|
35
|
+
force?: boolean;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
/**
|
|
39
|
+
* The URL of an existing state store worker to use
|
|
40
|
+
*/
|
|
41
|
+
url: string;
|
|
42
|
+
/**
|
|
43
|
+
* The token to use for the worker
|
|
44
|
+
* @default the value of the ALCHEMY_STATE_TOKEN environment variable
|
|
45
|
+
*/
|
|
46
|
+
token?: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class DOStateStore implements StateStore {
|
|
51
|
+
private prefix: string;
|
|
52
|
+
private client?: Promise<DOStateStoreClient>;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
private readonly scope: Scope,
|
|
56
|
+
private readonly options: DOStateStoreOptions = {},
|
|
57
|
+
) {
|
|
58
|
+
this.prefix = [options.prefix ?? "alchemy", ...scope.chain, ""].join("/");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async createClient() {
|
|
62
|
+
const token =
|
|
63
|
+
this.options.worker?.token ??
|
|
64
|
+
(await alchemy.secret.env.ALCHEMY_STATE_TOKEN).unencrypted;
|
|
65
|
+
if (this.options.worker && "url" in this.options.worker) {
|
|
66
|
+
const client = new DOStateStoreClient({
|
|
67
|
+
app: this.scope.appName ?? "alchemy",
|
|
68
|
+
stage: this.scope.stage,
|
|
69
|
+
url: this.options.worker.url,
|
|
70
|
+
token,
|
|
71
|
+
});
|
|
72
|
+
// The worker is already created and should be ready to use.
|
|
73
|
+
// If it's not, we'll fail fast with a helpful error message.
|
|
74
|
+
const res = await client.validate();
|
|
75
|
+
if (res.status === 200) {
|
|
76
|
+
return client;
|
|
77
|
+
}
|
|
78
|
+
if (res.status === 401) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"A worker URL was provided to DOStateStore, but the token is incorrect. Correct the token or remove the worker URL to create a new worker.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
throw new Error(
|
|
84
|
+
`A worker URL was provided to DOStateStore, but the worker status is ${res.status} ${res.statusText}.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const workerName = this.options.worker?.name ?? "alchemy-state";
|
|
88
|
+
const api = await createCloudflareApi(this.options);
|
|
89
|
+
const [subdomain, _] = await Promise.all([
|
|
90
|
+
getAccountSubdomain(api),
|
|
91
|
+
upsertStateStoreWorker(api, workerName, token),
|
|
92
|
+
]);
|
|
93
|
+
const client = new DOStateStoreClient({
|
|
94
|
+
app: this.scope.appName ?? "alchemy",
|
|
95
|
+
stage: this.scope.stage,
|
|
96
|
+
url: `https://${workerName}.${subdomain}.workers.dev`,
|
|
97
|
+
token,
|
|
98
|
+
});
|
|
99
|
+
// This ensures the token is correct and the worker is ready to use.
|
|
100
|
+
// RPC methods are retried, which is what we want here in case the worker is not ready yet.
|
|
101
|
+
await client.rpc("validate", null);
|
|
102
|
+
return client;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async getClient() {
|
|
106
|
+
this.client ??= this.createClient();
|
|
107
|
+
return await this.client;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async init() {
|
|
111
|
+
await this.getClient();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async list(): Promise<string[]> {
|
|
115
|
+
const client = await this.getClient();
|
|
116
|
+
const res = await client.rpc("list", { prefix: this.prefix });
|
|
117
|
+
return res.map((key) => this.deserializeKey(key));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async count(): Promise<number> {
|
|
121
|
+
const client = await this.getClient();
|
|
122
|
+
const res = await client.rpc("count", { prefix: this.prefix });
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async get(key: string): Promise<State | undefined> {
|
|
127
|
+
const client = await this.getClient();
|
|
128
|
+
const res = await client.rpc("get", { key: this.serializeKey(key) });
|
|
129
|
+
if (res) {
|
|
130
|
+
return await this.deserializeState(res);
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async getBatch(ids: string[]): Promise<Record<string, State>> {
|
|
136
|
+
const client = await this.getClient();
|
|
137
|
+
const res = await client.rpc("getBatch", {
|
|
138
|
+
keys: ids.map((id) => this.serializeKey(id)),
|
|
139
|
+
});
|
|
140
|
+
return await this.deserializeStates(res);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async all(): Promise<Record<string, State>> {
|
|
144
|
+
const client = await this.getClient();
|
|
145
|
+
const res = await client.rpc("all", { prefix: this.prefix });
|
|
146
|
+
return await this.deserializeStates(res);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async set(key: string, value: State): Promise<void> {
|
|
150
|
+
const client = await this.getClient();
|
|
151
|
+
await client.rpc("set", { key: this.serializeKey(key), value });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async delete(key: string): Promise<void> {
|
|
155
|
+
const client = await this.getClient();
|
|
156
|
+
await client.rpc("delete", { key: this.serializeKey(key) });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async deserializeStates(
|
|
160
|
+
input: Record<string, string>,
|
|
161
|
+
): Promise<Record<string, State>> {
|
|
162
|
+
return Object.fromEntries(
|
|
163
|
+
await Promise.all(
|
|
164
|
+
Object.entries(input).map(async ([key, value]) => [
|
|
165
|
+
this.deserializeKey(key),
|
|
166
|
+
await this.deserializeState(value),
|
|
167
|
+
]),
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async deserializeState(input: string): Promise<State> {
|
|
173
|
+
const state = await deserializeState(this.scope, input);
|
|
174
|
+
if (state.output === undefined) {
|
|
175
|
+
state.output = {} as any;
|
|
176
|
+
}
|
|
177
|
+
state.output[ResourceScope] = this.scope;
|
|
178
|
+
return state;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private deserializeKey(key: string): string {
|
|
182
|
+
return key.replace(this.prefix, "").replaceAll(":", "/");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private serializeKey(key: string): string {
|
|
186
|
+
return `${this.prefix}${key.replaceAll("/", ":")}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { State } from "../../state.ts";
|
|
2
|
+
|
|
3
|
+
export namespace DOStateStoreAPI {
|
|
4
|
+
export type API = {
|
|
5
|
+
validate: {
|
|
6
|
+
method: "validate";
|
|
7
|
+
params: null;
|
|
8
|
+
result: null;
|
|
9
|
+
};
|
|
10
|
+
get: {
|
|
11
|
+
method: "get";
|
|
12
|
+
params: { key: string };
|
|
13
|
+
result: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
getBatch: {
|
|
16
|
+
method: "getBatch";
|
|
17
|
+
params: { keys: string[] };
|
|
18
|
+
result: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
all: {
|
|
21
|
+
method: "all";
|
|
22
|
+
params: { prefix: string };
|
|
23
|
+
result: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
set: {
|
|
26
|
+
method: "set";
|
|
27
|
+
params: { key: string; value: State };
|
|
28
|
+
result: null;
|
|
29
|
+
};
|
|
30
|
+
delete: {
|
|
31
|
+
method: "delete";
|
|
32
|
+
params: { key: string };
|
|
33
|
+
result: null;
|
|
34
|
+
};
|
|
35
|
+
list: {
|
|
36
|
+
method: "list";
|
|
37
|
+
params: { prefix: string };
|
|
38
|
+
result: string[];
|
|
39
|
+
};
|
|
40
|
+
count: {
|
|
41
|
+
method: "count";
|
|
42
|
+
params: { prefix: string };
|
|
43
|
+
result: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface Context {
|
|
48
|
+
app: string;
|
|
49
|
+
stage: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Request = {
|
|
53
|
+
[K in keyof API]: {
|
|
54
|
+
method: K;
|
|
55
|
+
params: API[K]["params"];
|
|
56
|
+
};
|
|
57
|
+
}[keyof API];
|
|
58
|
+
|
|
59
|
+
export interface SuccessResponse<T extends keyof API = keyof API> {
|
|
60
|
+
success: true;
|
|
61
|
+
status: number;
|
|
62
|
+
result: API[T]["result"];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ErrorResponse {
|
|
66
|
+
success: false;
|
|
67
|
+
status: number;
|
|
68
|
+
error: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type Response<T extends keyof API = keyof API> =
|
|
72
|
+
| SuccessResponse<T>
|
|
73
|
+
| ErrorResponse;
|
|
74
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|
2
|
+
import { Fs } from "dofs";
|
|
3
|
+
import { timingSafeEqual } from "node:crypto";
|
|
4
|
+
import type { State } from "../../state.ts";
|
|
5
|
+
import type { DOStateStoreAPI } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
DOFS_STATE_STORE: DurableObjectNamespace<DOFSStateStore>;
|
|
9
|
+
DOFS_TOKEN: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default class extends WorkerEntrypoint<Env> {
|
|
13
|
+
override async fetch(request: Request) {
|
|
14
|
+
try {
|
|
15
|
+
const result = await this.handle(request);
|
|
16
|
+
const body: DOStateStoreAPI.Response = {
|
|
17
|
+
success: true,
|
|
18
|
+
status: 200,
|
|
19
|
+
result: result ?? undefined,
|
|
20
|
+
};
|
|
21
|
+
return Response.json(body, { status: body.status });
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return APIError.fromUnknown(error).toResponse();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async handle(request: Request) {
|
|
28
|
+
if (!this.authorize(request)) {
|
|
29
|
+
throw new APIError("Unauthorized", 401);
|
|
30
|
+
}
|
|
31
|
+
const stub = this.getStub(request);
|
|
32
|
+
const body = await this.parseBody(request);
|
|
33
|
+
switch (body.method) {
|
|
34
|
+
case "validate":
|
|
35
|
+
return null;
|
|
36
|
+
case "all":
|
|
37
|
+
return await stub.all(body.params.prefix);
|
|
38
|
+
case "count":
|
|
39
|
+
return await stub.count(body.params.prefix);
|
|
40
|
+
case "delete":
|
|
41
|
+
return await stub.delete(body.params.key);
|
|
42
|
+
case "get":
|
|
43
|
+
return await stub.get(body.params.key);
|
|
44
|
+
case "getBatch":
|
|
45
|
+
return await stub.getBatch(body.params.keys);
|
|
46
|
+
case "list":
|
|
47
|
+
return await stub.list(body.params.prefix);
|
|
48
|
+
case "set":
|
|
49
|
+
return await stub.set(body.params.key, body.params.value);
|
|
50
|
+
default: {
|
|
51
|
+
const _: never = body;
|
|
52
|
+
throw new APIError("Method not found", 404);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private authorize(request: Request): boolean {
|
|
58
|
+
const token = request.headers.get("Authorization")?.split(" ")[1];
|
|
59
|
+
if (!token) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return timingSafeEqual(
|
|
63
|
+
new TextEncoder().encode(token),
|
|
64
|
+
new TextEncoder().encode(this.env.DOFS_TOKEN),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private getStub(request: Request): DurableObjectStub<DOFSStateStore> {
|
|
69
|
+
const url = new URL(request.url);
|
|
70
|
+
const app = url.searchParams.get("app");
|
|
71
|
+
const stage = url.searchParams.get("stage");
|
|
72
|
+
if (!app || !stage) {
|
|
73
|
+
throw new APIError("Missing app or stage", 400);
|
|
74
|
+
}
|
|
75
|
+
return this.env.DOFS_STATE_STORE.get(
|
|
76
|
+
this.env.DOFS_STATE_STORE.idFromName(`${app}/${stage}`),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async parseBody(request: Request): Promise<DOStateStoreAPI.Request> {
|
|
81
|
+
try {
|
|
82
|
+
return await request.json<DOStateStoreAPI.Request>();
|
|
83
|
+
} catch {
|
|
84
|
+
throw new APIError("Invalid JSON", 400);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class APIError extends Error {
|
|
90
|
+
constructor(
|
|
91
|
+
message: string,
|
|
92
|
+
public readonly status: number,
|
|
93
|
+
) {
|
|
94
|
+
super(message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
toResponse(): Response {
|
|
98
|
+
return Response.json(
|
|
99
|
+
{
|
|
100
|
+
success: false,
|
|
101
|
+
status: this.status,
|
|
102
|
+
error: this.message,
|
|
103
|
+
},
|
|
104
|
+
{ status: this.status },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static fromUnknown(error: unknown): APIError {
|
|
109
|
+
if (error instanceof APIError) {
|
|
110
|
+
return error;
|
|
111
|
+
}
|
|
112
|
+
if (error instanceof Error) {
|
|
113
|
+
return new APIError(error.message, 500);
|
|
114
|
+
}
|
|
115
|
+
return new APIError("An unknown error occurred.", 500);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class DOFSStateStore extends DurableObject<Env> {
|
|
120
|
+
fs = new Fs(this.ctx, this.env, {
|
|
121
|
+
chunkSize: 256 * 1024,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
async all(prefix: string): Promise<Record<string, string>> {
|
|
125
|
+
const keys = this.list(prefix);
|
|
126
|
+
return await this.getBatch(keys);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
count(prefix: string): number {
|
|
130
|
+
return this.list(prefix).length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
delete(key: string): void {
|
|
134
|
+
try {
|
|
135
|
+
this.fs.unlink(key);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (isErrorCode(error, "ENOENT")) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async get(key: string): Promise<string | undefined> {
|
|
145
|
+
try {
|
|
146
|
+
const file = this.fs.readFile(key);
|
|
147
|
+
return new Response(file).text();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (isErrorCode(error, "ENOENT")) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async getBatch(keys: string[]): Promise<Record<string, string>> {
|
|
157
|
+
const result: Record<string, string> = {};
|
|
158
|
+
await Promise.all(
|
|
159
|
+
keys.map(async (key) => {
|
|
160
|
+
const value = await this.get(key);
|
|
161
|
+
if (value) {
|
|
162
|
+
result[key] = value;
|
|
163
|
+
}
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
list(prefix: string): string[] {
|
|
170
|
+
try {
|
|
171
|
+
return this.fs
|
|
172
|
+
.listDir(prefix, { recursive: true })
|
|
173
|
+
.filter((item) => item !== "." && item !== "..");
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (isErrorCode(error, "ENOENT")) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async set(key: string, value: State): Promise<void> {
|
|
183
|
+
this.ensureDir(key);
|
|
184
|
+
await this.fs.writeFile(key, JSON.stringify(value));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private ensureDir(path: string): void {
|
|
188
|
+
const dir = path.split("/").slice(0, -1).join("/");
|
|
189
|
+
try {
|
|
190
|
+
this.fs.mkdir(dir, { recursive: true });
|
|
191
|
+
} catch {
|
|
192
|
+
// directory already exists, ignore
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const isErrorCode = (error: unknown, code: string) => {
|
|
198
|
+
return error instanceof Error && "message" in error && error.message === code;
|
|
199
|
+
};
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from "./d1-database.ts";
|
|
|
18
18
|
export * from "./d1-export.ts";
|
|
19
19
|
export * from "./d1-import.ts";
|
|
20
20
|
export * from "./dns-records.ts";
|
|
21
|
+
export * from "./do-state-store/index.ts";
|
|
21
22
|
export * from "./durable-object-namespace.ts";
|
|
22
23
|
export * from "./hyperdrive.ts";
|
|
23
24
|
export * from "./kv-namespace.ts";
|
|
@@ -364,7 +364,7 @@ const PipelineResource = Resource("cloudflare::Pipeline", async function <
|
|
|
364
364
|
// Update operation
|
|
365
365
|
if (this.output?.id) {
|
|
366
366
|
// Check if name is being changed, which is not allowed
|
|
367
|
-
if (
|
|
367
|
+
if (pipelineName !== this.output.name) {
|
|
368
368
|
throw new Error(
|
|
369
369
|
"Cannot update Pipeline name after creation. Pipeline name is immutable.",
|
|
370
370
|
);
|