alchemy 2.0.0-beta.50 → 2.0.0-beta.51
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/bin/exec.js +117 -113
- package/bin/exec.js.map +1 -1
- package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/BucketEventSource.js +13 -2
- package/lib/AWS/Lambda/BucketEventSource.js.map +1 -1
- package/lib/AWS/S3/Bucket.d.ts.map +1 -1
- package/lib/AWS/S3/Bucket.js +49 -0
- package/lib/AWS/S3/Bucket.js.map +1 -1
- package/lib/AWS/S3/BucketNotifications.d.ts +10 -0
- package/lib/AWS/S3/BucketNotifications.d.ts.map +1 -1
- package/lib/AWS/S3/BucketNotifications.js.map +1 -1
- package/lib/Cli/commands/cloudflare.d.ts +1 -1
- package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts +1 -1
- package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts.map +1 -1
- package/lib/Cloudflare/AiGateway/AiGatewayBinding.js +6 -2
- package/lib/Cloudflare/AiGateway/AiGatewayBinding.js.map +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +3 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/Ruleset/Ruleset.d.ts +82 -0
- package/lib/Cloudflare/Ruleset/Ruleset.d.ts.map +1 -0
- package/lib/Cloudflare/Ruleset/Ruleset.js +126 -0
- package/lib/Cloudflare/Ruleset/Ruleset.js.map +1 -0
- package/lib/Cloudflare/Ruleset/index.d.ts +2 -0
- package/lib/Cloudflare/Ruleset/index.d.ts.map +1 -0
- package/lib/Cloudflare/Ruleset/index.js +2 -0
- package/lib/Cloudflare/Ruleset/index.js.map +1 -0
- package/lib/Cloudflare/SecretsStore/Secret.d.ts +1 -0
- package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/Secret.js +2 -0
- package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/DurableObjectChatPersistence.d.ts +69 -0
- package/lib/Cloudflare/Workers/DurableObjectChatPersistence.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/DurableObjectChatPersistence.js +104 -0
- package/lib/Cloudflare/Workers/DurableObjectChatPersistence.js.map +1 -0
- package/lib/Cloudflare/Workers/Worker.d.ts +3 -2
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +9 -0
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -1
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBinding.js.map +1 -1
- package/lib/Cloudflare/Workers/index.d.ts +1 -0
- package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/index.js +1 -0
- package/lib/Cloudflare/Workers/index.js.map +1 -1
- package/lib/Cloudflare/Zaraz/ZarazConfig.d.ts.map +1 -1
- 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/Deploy.d.ts +3 -2
- package/lib/Deploy.d.ts.map +1 -1
- package/lib/Deploy.js.map +1 -1
- package/lib/Destroy.d.ts +3 -2
- package/lib/Destroy.d.ts.map +1 -1
- package/lib/Destroy.js.map +1 -1
- package/lib/Drizzle/Schema.d.ts.map +1 -1
- package/lib/Drizzle/Schema.js +6 -3
- package/lib/Drizzle/Schema.js.map +1 -1
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +11 -10
- package/lib/Output.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +5 -1
- package/lib/Plan.js.map +1 -1
- package/lib/Planetscale/MySQL/MySQLDatabase.d.ts +0 -2
- package/lib/Planetscale/MySQL/MySQLDatabase.d.ts.map +1 -1
- package/lib/Planetscale/MySQL/MySQLDatabase.js +1 -3
- package/lib/Planetscale/MySQL/MySQLDatabase.js.map +1 -1
- package/lib/Planetscale/Postgres/PostgresDatabase.d.ts +0 -2
- package/lib/Planetscale/Postgres/PostgresDatabase.d.ts.map +1 -1
- package/lib/Planetscale/Postgres/PostgresDatabase.js +1 -3
- package/lib/Planetscale/Postgres/PostgresDatabase.js.map +1 -1
- package/lib/Platform.d.ts +4 -4
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Platform.js.map +1 -1
- package/lib/Resource.d.ts +6 -0
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Resource.js +2 -0
- package/lib/Resource.js.map +1 -1
- package/lib/Stack.d.ts +5 -4
- package/lib/Stack.d.ts.map +1 -1
- package/lib/Stack.js.map +1 -1
- package/lib/Test/Core.d.ts +2 -2
- package/lib/Test/Core.d.ts.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AWS/Lambda/BucketEventSource.ts +16 -1
- package/src/AWS/S3/Bucket.ts +78 -0
- package/src/AWS/S3/BucketNotifications.ts +10 -0
- package/src/Cloudflare/AiGateway/AiGatewayBinding.ts +8 -3
- package/src/Cloudflare/Providers.ts +3 -0
- package/src/Cloudflare/Ruleset/Ruleset.ts +223 -0
- package/src/Cloudflare/Ruleset/index.ts +1 -0
- package/src/Cloudflare/SecretsStore/Secret.ts +4 -0
- package/src/Cloudflare/Workers/DurableObjectChatPersistence.ts +131 -0
- package/src/Cloudflare/Workers/Worker.ts +6 -1
- package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +8 -0
- package/src/Cloudflare/Workers/WorkerBinding.ts +2 -0
- package/src/Cloudflare/Workers/index.ts +1 -0
- package/src/Cloudflare/index.ts +1 -0
- package/src/Deploy.ts +2 -1
- package/src/Destroy.ts +2 -1
- package/src/Drizzle/Schema.ts +11 -3
- package/src/Output.ts +12 -10
- package/src/Plan.ts +5 -1
- package/src/Planetscale/MySQL/MySQLDatabase.ts +1 -6
- package/src/Planetscale/Postgres/PostgresDatabase.ts +1 -5
- package/src/Platform.ts +12 -4
- package/src/Resource.ts +10 -0
- package/src/Stack.ts +11 -10
package/package.json
CHANGED
|
@@ -113,13 +113,25 @@ export const BucketEventSourcePolicyLive = BucketEventSourcePolicy.layer.effect(
|
|
|
113
113
|
bucket: Bucket,
|
|
114
114
|
{
|
|
115
115
|
events: Events = ["s3:ObjectCreated:*"],
|
|
116
|
+
prefix,
|
|
117
|
+
suffix,
|
|
116
118
|
}: {
|
|
117
119
|
events?: S3EventType[];
|
|
120
|
+
prefix?: string;
|
|
121
|
+
suffix?: string;
|
|
118
122
|
} = {},
|
|
119
123
|
) {
|
|
120
124
|
if (Lambda.isFunction(host)) {
|
|
125
|
+
const filterRules = [
|
|
126
|
+
...(prefix !== undefined
|
|
127
|
+
? [{ Name: "prefix" as const, Value: prefix }]
|
|
128
|
+
: []),
|
|
129
|
+
...(suffix !== undefined
|
|
130
|
+
? [{ Name: "suffix" as const, Value: suffix }]
|
|
131
|
+
: []),
|
|
132
|
+
];
|
|
121
133
|
yield* Permission(`AWS.Lambda.InvokeFunction(${bucket.LogicalId})`, {
|
|
122
|
-
action: "lambda
|
|
134
|
+
action: "lambda:InvokeFunction",
|
|
123
135
|
functionName: host.functionName,
|
|
124
136
|
principal: "s3.amazonaws.com",
|
|
125
137
|
sourceArn: bucket.bucketArn,
|
|
@@ -130,6 +142,9 @@ export const BucketEventSourcePolicyLive = BucketEventSourcePolicy.layer.effect(
|
|
|
130
142
|
{
|
|
131
143
|
LambdaFunctionArn: host.functionArn,
|
|
132
144
|
Events,
|
|
145
|
+
...(filterRules.length > 0
|
|
146
|
+
? { Filter: { Key: { FilterRules: filterRules } } }
|
|
147
|
+
: {}),
|
|
133
148
|
},
|
|
134
149
|
],
|
|
135
150
|
},
|
package/src/AWS/S3/Bucket.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Region } from "@distilled.cloud/aws/Region";
|
|
2
2
|
import type { BucketLocationConstraint } from "@distilled.cloud/aws/s3";
|
|
3
3
|
import * as s3 from "@distilled.cloud/aws/s3";
|
|
4
|
+
import * as Arr from "effect/Array";
|
|
4
5
|
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Order from "effect/Order";
|
|
5
7
|
import * as Schedule from "effect/Schedule";
|
|
6
8
|
import type { ScopedPlanStatusSession } from "../../Cli/Cli.ts";
|
|
7
9
|
import { isResolved } from "../../Diff.ts";
|
|
@@ -475,6 +477,75 @@ export const BucketProvider = () =>
|
|
|
475
477
|
yield* session.note(`Removed bucket policy: ${bucketName}`);
|
|
476
478
|
});
|
|
477
479
|
|
|
480
|
+
// Apply S3 event notification configuration declared via bindings
|
|
481
|
+
// (e.g. `S3.notifications(bucket).subscribe(...)`). Without this the
|
|
482
|
+
// binding is recorded in state but never reaches the bucket, so no
|
|
483
|
+
// events are ever delivered.
|
|
484
|
+
// Canonical form of the Lambda targets, ignoring S3-assigned `Id`s and
|
|
485
|
+
// event ordering, so drift detection doesn't churn across deploys.
|
|
486
|
+
const canonicalLambda = (
|
|
487
|
+
configs: readonly s3.LambdaFunctionConfiguration[],
|
|
488
|
+
) =>
|
|
489
|
+
JSON.stringify(
|
|
490
|
+
Arr.map(configs, (c) => ({
|
|
491
|
+
arn: c.LambdaFunctionArn,
|
|
492
|
+
events: Arr.sort(c.Events ?? [], Order.String),
|
|
493
|
+
filter: c.Filter ?? null,
|
|
494
|
+
})),
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const syncBucketNotifications = Effect.fnUntraced(function* ({
|
|
498
|
+
bucketName,
|
|
499
|
+
bindings,
|
|
500
|
+
session,
|
|
501
|
+
operation,
|
|
502
|
+
}: {
|
|
503
|
+
bucketName: string;
|
|
504
|
+
session: ScopedPlanStatusSession;
|
|
505
|
+
bindings: ResourceBinding<Bucket["Binding"]>[];
|
|
506
|
+
operation: "create" | "update";
|
|
507
|
+
}) {
|
|
508
|
+
const desired = Arr.flatMap(
|
|
509
|
+
bindings,
|
|
510
|
+
(binding) =>
|
|
511
|
+
binding.data.notificationConfiguration
|
|
512
|
+
?.LambdaFunctionConfigurations ?? [],
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
// Nothing declared — leave any externally-managed config untouched.
|
|
516
|
+
if (Arr.isReadonlyArrayEmpty(desired)) return;
|
|
517
|
+
|
|
518
|
+
const existing = yield* s3.getBucketNotificationConfiguration({
|
|
519
|
+
Bucket: bucketName,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
if (
|
|
523
|
+
canonicalLambda(existing.LambdaFunctionConfigurations ?? []) ===
|
|
524
|
+
canonicalLambda(desired)
|
|
525
|
+
) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
yield* Effect.logInfo(
|
|
530
|
+
`S3 Bucket ${operation}: applying notification configuration to ${bucketName}`,
|
|
531
|
+
);
|
|
532
|
+
yield* s3.putBucketNotificationConfiguration({
|
|
533
|
+
Bucket: bucketName,
|
|
534
|
+
// Preserve any Topic/Queue/EventBridge config already on the bucket;
|
|
535
|
+
// only manage the Lambda targets declared through bindings.
|
|
536
|
+
NotificationConfiguration: {
|
|
537
|
+
...existing,
|
|
538
|
+
LambdaFunctionConfigurations: desired,
|
|
539
|
+
},
|
|
540
|
+
// The Lambda invoke permission is created as a separate resource
|
|
541
|
+
// that may be applied after this bucket reconcile. Skip S3's
|
|
542
|
+
// synchronous test-invoke so the PUT doesn't fail on ordering;
|
|
543
|
+
// the permission exists by the time events are delivered.
|
|
544
|
+
SkipDestinationValidation: true,
|
|
545
|
+
});
|
|
546
|
+
yield* session.note(`Updated bucket notifications: ${bucketName}`);
|
|
547
|
+
});
|
|
548
|
+
|
|
478
549
|
return {
|
|
479
550
|
stables: ["bucketName", "bucketArn", "region", "accountId"],
|
|
480
551
|
// S3 bucket names are globally unique. `headBucket` succeeds only when
|
|
@@ -554,6 +625,13 @@ export const BucketProvider = () =>
|
|
|
554
625
|
operation,
|
|
555
626
|
});
|
|
556
627
|
|
|
628
|
+
yield* syncBucketNotifications({
|
|
629
|
+
bucketName: resolved.bucketName,
|
|
630
|
+
bindings,
|
|
631
|
+
session,
|
|
632
|
+
operation,
|
|
633
|
+
});
|
|
634
|
+
|
|
557
635
|
if (operation === "create") {
|
|
558
636
|
yield* session.note(`Ensured bucket: ${resolved.bucketName}`);
|
|
559
637
|
}
|
|
@@ -24,6 +24,16 @@ export type BucketNotification = {
|
|
|
24
24
|
export interface NotificationsProps<Events extends S3EventType[]> {
|
|
25
25
|
/** S3 event types to subscribe to. Defaults to all event types. */
|
|
26
26
|
events?: Events;
|
|
27
|
+
/**
|
|
28
|
+
* Only deliver events for object keys beginning with this prefix.
|
|
29
|
+
* Maps to an S3 `prefix` filter rule on the notification configuration.
|
|
30
|
+
*/
|
|
31
|
+
prefix?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Only deliver events for object keys ending with this suffix.
|
|
34
|
+
* Maps to an S3 `suffix` filter rule on the notification configuration.
|
|
35
|
+
*/
|
|
36
|
+
suffix?: string;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -76,7 +76,7 @@ export interface AiGatewayClient {
|
|
|
76
76
|
): Effect.Effect<Response, AiGatewayError, RuntimeContext>;
|
|
77
77
|
|
|
78
78
|
model(
|
|
79
|
-
options: LanguageModelOptions,
|
|
79
|
+
options: Omit<LanguageModelOptions, "client">,
|
|
80
80
|
): Layer.Layer<LanguageModel, never, RuntimeContext>;
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -166,7 +166,7 @@ export const AiGatewayBindingLive = Layer.effect(
|
|
|
166
166
|
Effect.flatMap((gateway) => tryPromise(() => fn(gateway))),
|
|
167
167
|
);
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
const self: AiGatewayClient = {
|
|
170
170
|
raw: ai,
|
|
171
171
|
gateway: runtimeGateway,
|
|
172
172
|
id: gatewayIdAccessor,
|
|
@@ -175,8 +175,13 @@ export const AiGatewayBindingLive = Layer.effect(
|
|
|
175
175
|
getLog: (logId) => use((gateway) => gateway.getLog(logId)),
|
|
176
176
|
getUrl: (provider) => use((gateway) => gateway.getUrl(provider)),
|
|
177
177
|
run: (data, options) => use((gateway) => gateway.run(data, options)),
|
|
178
|
-
model: (options) =>
|
|
178
|
+
model: (options) =>
|
|
179
|
+
makeLanguageModelLayer({
|
|
180
|
+
...options,
|
|
181
|
+
client: self,
|
|
182
|
+
}),
|
|
179
183
|
} satisfies AiGatewayClient;
|
|
184
|
+
return self;
|
|
180
185
|
});
|
|
181
186
|
}),
|
|
182
187
|
);
|
|
@@ -31,6 +31,7 @@ import * as KV from "./KV/index.ts";
|
|
|
31
31
|
import * as Queue from "./Queue/index.ts";
|
|
32
32
|
import * as R2 from "./R2/index.ts";
|
|
33
33
|
import * as RateLimit from "./RateLimit/index.ts";
|
|
34
|
+
import * as Ruleset from "./Ruleset/index.ts";
|
|
34
35
|
import * as SecretsStore from "./SecretsStore/index.ts";
|
|
35
36
|
import * as Tunnel from "./Tunnel/index.ts";
|
|
36
37
|
import * as Vectorize from "./Vectorize/index.ts";
|
|
@@ -85,6 +86,7 @@ export const providers = () =>
|
|
|
85
86
|
R2.R2Bucket,
|
|
86
87
|
R2.R2BucketBindingPolicy,
|
|
87
88
|
RateLimit.RateLimitBindingPolicy,
|
|
89
|
+
Ruleset.Ruleset,
|
|
88
90
|
SecretsStore.SecretBindingPolicy,
|
|
89
91
|
SecretsStore.SecretsStore,
|
|
90
92
|
SecretsStore.Secret,
|
|
@@ -138,6 +140,7 @@ export const providers = () =>
|
|
|
138
140
|
R2.R2BucketBindingPolicyLive,
|
|
139
141
|
R2.R2BucketProvider(),
|
|
140
142
|
RateLimit.RateLimitBindingPolicyLive,
|
|
143
|
+
Ruleset.RulesetProvider(),
|
|
141
144
|
SecretsStore.SecretBindingPolicyLive,
|
|
142
145
|
SecretsStore.SecretsStoreProvider(),
|
|
143
146
|
SecretsStore.StoreSecretProvider(),
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as rulesets from "@distilled.cloud/cloudflare/rulesets";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { deepEqual, isResolved } from "../../Diff.ts";
|
|
4
|
+
import { createPhysicalName } from "../../PhysicalName.ts";
|
|
5
|
+
import * as Provider from "../../Provider.ts";
|
|
6
|
+
import { Resource } from "../../Resource.ts";
|
|
7
|
+
import type { Providers } from "../Providers.ts";
|
|
8
|
+
import type { Zone, ZoneAttributes } from "../Zone/index.ts";
|
|
9
|
+
|
|
10
|
+
export type RulesetPhase = rulesets.CreateRulesetForZoneRequest["phase"];
|
|
11
|
+
export type RulesetRule = NonNullable<
|
|
12
|
+
rulesets.PutPhasForZoneRequest["rules"]
|
|
13
|
+
>[number];
|
|
14
|
+
export type RulesetOutputRule = Omit<
|
|
15
|
+
NonNullable<rulesets.GetPhasResponse["rules"]>[number],
|
|
16
|
+
"lastUpdated" | "version"
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
export type RulesetProps = {
|
|
20
|
+
/**
|
|
21
|
+
* Zone to apply the ruleset to. Pass a `Cloudflare.Zone`.
|
|
22
|
+
*/
|
|
23
|
+
zone: Zone;
|
|
24
|
+
/**
|
|
25
|
+
* Ruleset phase entrypoint to own.
|
|
26
|
+
*/
|
|
27
|
+
phase: RulesetPhase;
|
|
28
|
+
/**
|
|
29
|
+
* Rules to apply to the phase entrypoint.
|
|
30
|
+
*/
|
|
31
|
+
rules: RulesetRule[];
|
|
32
|
+
/**
|
|
33
|
+
* Human-readable name for the ruleset.
|
|
34
|
+
* @default ${app}-${stage}-${id}
|
|
35
|
+
*/
|
|
36
|
+
name?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Description for the ruleset.
|
|
39
|
+
*/
|
|
40
|
+
description?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type RulesetKind =
|
|
44
|
+
| "managed"
|
|
45
|
+
| "custom"
|
|
46
|
+
| "root"
|
|
47
|
+
| "zone"
|
|
48
|
+
| (string & {});
|
|
49
|
+
|
|
50
|
+
export type Ruleset = Resource<
|
|
51
|
+
"Cloudflare.Ruleset",
|
|
52
|
+
RulesetProps,
|
|
53
|
+
{
|
|
54
|
+
/** The unique ID of the ruleset (Cloudflare `id`). */
|
|
55
|
+
rulesetId: string;
|
|
56
|
+
/**
|
|
57
|
+
* Zone the ruleset phase entrypoint belongs to. Alchemy-flattened
|
|
58
|
+
* identifier — not part of Cloudflare's phase-entrypoint response.
|
|
59
|
+
*/
|
|
60
|
+
zoneId: string;
|
|
61
|
+
/** The kind of the ruleset. */
|
|
62
|
+
kind: RulesetKind;
|
|
63
|
+
/** The human-readable name of the ruleset. */
|
|
64
|
+
name: string;
|
|
65
|
+
/** The phase of the ruleset. */
|
|
66
|
+
phase: RulesetPhase;
|
|
67
|
+
/** An informative description of the ruleset. */
|
|
68
|
+
description: string | undefined;
|
|
69
|
+
/** The list of rules in the ruleset. */
|
|
70
|
+
rules: RulesetOutputRule[];
|
|
71
|
+
/** The timestamp of when the ruleset was last modified. */
|
|
72
|
+
lastUpdated: string;
|
|
73
|
+
/** The version of the ruleset. */
|
|
74
|
+
version: string;
|
|
75
|
+
},
|
|
76
|
+
never,
|
|
77
|
+
Providers
|
|
78
|
+
>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A Cloudflare Ruleset phase entrypoint for a zone.
|
|
82
|
+
*
|
|
83
|
+
* This resource owns the entire ruleset for a phase entrypoint. Rules managed
|
|
84
|
+
* elsewhere in the same phase can be overwritten on deploy.
|
|
85
|
+
*
|
|
86
|
+
* @section WAF Rules
|
|
87
|
+
* @example Block probes in the custom firewall phase
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const zone = yield* Cloudflare.Zone("MyZone", { name: "example.com" });
|
|
90
|
+
* const waf = yield* Cloudflare.Ruleset("WafRules", {
|
|
91
|
+
* zone,
|
|
92
|
+
* phase: "http_request_firewall_custom",
|
|
93
|
+
* rules: [
|
|
94
|
+
* {
|
|
95
|
+
* description: "Block exploit probes",
|
|
96
|
+
* expression: `lower(http.request.uri.path) contains "/.env"`,
|
|
97
|
+
* action: "block",
|
|
98
|
+
* },
|
|
99
|
+
* ],
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export const Ruleset = Resource<Ruleset>("Cloudflare.Ruleset")({});
|
|
104
|
+
|
|
105
|
+
const isNotFoundError = (error: unknown): boolean =>
|
|
106
|
+
typeof error === "object" &&
|
|
107
|
+
error !== null &&
|
|
108
|
+
(("status" in error && (error as { status: unknown }).status === 404) ||
|
|
109
|
+
("_tag" in error && (error as { _tag: unknown })._tag === "NotFound"));
|
|
110
|
+
|
|
111
|
+
// A `Zone` prop is resolved to its attributes before a lifecycle op runs, so
|
|
112
|
+
// `zone.zoneId` is normally a plain string even though the `Zone` resource
|
|
113
|
+
// type statically exposes it as an `Output`. During `diff` (plan time) the
|
|
114
|
+
// zone can still be unresolved — e.g. when it's being created in the same
|
|
115
|
+
// deploy — in which case `zoneId` is not yet a string. Callers must treat a
|
|
116
|
+
// non-string result as "not resolved yet".
|
|
117
|
+
const zoneIdOf = (zone: Zone): string | undefined => {
|
|
118
|
+
const zoneId = (zone as unknown as Partial<ZoneAttributes>).zoneId;
|
|
119
|
+
return typeof zoneId === "string" ? zoneId : undefined;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const RulesetProvider = () =>
|
|
123
|
+
Provider.effect(
|
|
124
|
+
Ruleset,
|
|
125
|
+
Effect.gen(function* () {
|
|
126
|
+
const getPhas = yield* rulesets.getPhasForZone;
|
|
127
|
+
const putPhas = yield* rulesets.putPhasForZone;
|
|
128
|
+
|
|
129
|
+
const createRulesetName = (id: string, name: string | undefined) =>
|
|
130
|
+
Effect.gen(function* () {
|
|
131
|
+
return name ?? (yield* createPhysicalName({ id }));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
stables: ["zoneId", "phase"],
|
|
136
|
+
diff: Effect.fn(function* ({ id, olds, news, output }) {
|
|
137
|
+
if (!isResolved(news)) return undefined;
|
|
138
|
+
const desiredZoneId = zoneIdOf(news.zone);
|
|
139
|
+
|
|
140
|
+
// The desired zone id may still be an unresolved Output (e.g. the
|
|
141
|
+
// zone is being created in the same deploy). Only make a zone-change
|
|
142
|
+
// replacement decision once we have a concrete id.
|
|
143
|
+
if (desiredZoneId !== undefined) {
|
|
144
|
+
if (output?.zoneId && desiredZoneId !== output.zoneId) {
|
|
145
|
+
return { action: "replace" } as const;
|
|
146
|
+
}
|
|
147
|
+
const oldZoneId = olds.zone ? zoneIdOf(olds.zone) : undefined;
|
|
148
|
+
if (oldZoneId !== undefined && oldZoneId !== desiredZoneId) {
|
|
149
|
+
return { action: "replace" } as const;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (olds.phase !== news.phase) {
|
|
153
|
+
return { action: "replace" } as const;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const oldName =
|
|
157
|
+
output?.name ?? (yield* createRulesetName(id, olds.name));
|
|
158
|
+
const name = yield* createRulesetName(id, news.name);
|
|
159
|
+
if (
|
|
160
|
+
oldName !== name ||
|
|
161
|
+
olds.description !== news.description ||
|
|
162
|
+
!deepEqual(olds.rules, news.rules)
|
|
163
|
+
) {
|
|
164
|
+
return { action: "update" } as const;
|
|
165
|
+
}
|
|
166
|
+
}),
|
|
167
|
+
reconcile: Effect.fn(function* ({ id, news, output }) {
|
|
168
|
+
const zoneId = output?.zoneId ?? zoneIdOf(news.zone);
|
|
169
|
+
if (zoneId === undefined) {
|
|
170
|
+
return yield* Effect.fail(
|
|
171
|
+
new Error("Cloudflare Ruleset: zone id is not resolved"),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const name = yield* createRulesetName(id, news.name ?? output?.name);
|
|
175
|
+
const ruleset = yield* putPhas({
|
|
176
|
+
zoneId,
|
|
177
|
+
rulesetPhase: news.phase,
|
|
178
|
+
name,
|
|
179
|
+
description: news.description,
|
|
180
|
+
rules: news.rules,
|
|
181
|
+
});
|
|
182
|
+
return toRulesetAttributes(zoneId, ruleset);
|
|
183
|
+
}),
|
|
184
|
+
delete: Effect.fn(function* ({ olds, output }) {
|
|
185
|
+
yield* putPhas({
|
|
186
|
+
zoneId: output.zoneId,
|
|
187
|
+
rulesetPhase: olds.phase,
|
|
188
|
+
name: output.name,
|
|
189
|
+
description: output.description,
|
|
190
|
+
rules: [],
|
|
191
|
+
}).pipe(Effect.catchIf(isNotFoundError, () => Effect.void));
|
|
192
|
+
}),
|
|
193
|
+
read: Effect.fn(function* ({ olds, output }) {
|
|
194
|
+
const zoneId = output?.zoneId ?? zoneIdOf(olds.zone);
|
|
195
|
+
if (zoneId === undefined) return undefined;
|
|
196
|
+
return yield* getPhas({
|
|
197
|
+
zoneId,
|
|
198
|
+
rulesetPhase: output?.phase ?? olds.phase,
|
|
199
|
+
}).pipe(
|
|
200
|
+
Effect.map((ruleset) => toRulesetAttributes(zoneId, ruleset)),
|
|
201
|
+
Effect.catchIf(isNotFoundError, () => Effect.succeed(undefined)),
|
|
202
|
+
);
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
export const toRulesetAttributes = (
|
|
209
|
+
zoneId: string,
|
|
210
|
+
ruleset: rulesets.GetPhasResponse | rulesets.PutPhasResponse,
|
|
211
|
+
): Ruleset["Attributes"] => ({
|
|
212
|
+
rulesetId: ruleset.id,
|
|
213
|
+
zoneId,
|
|
214
|
+
kind: ruleset.kind,
|
|
215
|
+
name: ruleset.name,
|
|
216
|
+
phase: ruleset.phase,
|
|
217
|
+
description: ruleset.description ?? undefined,
|
|
218
|
+
rules: (ruleset.rules ?? []).map(
|
|
219
|
+
({ lastUpdated: _lastUpdated, version: _version, ...rule }) => rule,
|
|
220
|
+
),
|
|
221
|
+
lastUpdated: ruleset.lastUpdated,
|
|
222
|
+
version: ruleset.version,
|
|
223
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Ruleset.ts";
|
|
@@ -54,6 +54,10 @@ export type Secret = Resource<
|
|
|
54
54
|
Providers
|
|
55
55
|
>;
|
|
56
56
|
|
|
57
|
+
export const isSecret = (value: unknown): value is Secret =>
|
|
58
|
+
typeof value === "object" &&
|
|
59
|
+
(value as any)?.Type === "Cloudflare.SecretsStore.Secret";
|
|
60
|
+
|
|
57
61
|
export type SecretStatus = "pending" | "active" | "deleted";
|
|
58
62
|
|
|
59
63
|
// Distilled widened generated string enums to open unions (`string & {}`); the
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import {
|
|
5
|
+
BackingPersistence,
|
|
6
|
+
PersistenceError,
|
|
7
|
+
type BackingPersistenceStore,
|
|
8
|
+
} from "effect/unstable/persistence/Persistence";
|
|
9
|
+
import { DurableObjectState } from "./DurableObjectState.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A `BackingPersistence` layer (Effect AI persistence module) backed
|
|
13
|
+
* by the surrounding Durable Object's `state.storage`. Drop-in storage
|
|
14
|
+
* for `Persistence.layerResultPersisted({ storeId })` so chat history,
|
|
15
|
+
* cached AI responses, or any other persisted state lives in the DO
|
|
16
|
+
* SQLite store with `${storeId}:` key namespacing.
|
|
17
|
+
*
|
|
18
|
+
* Multiple `storeId`s can coexist within a single Durable Object —
|
|
19
|
+
* keys are namespaced with a `${storeId}:` prefix so they don't
|
|
20
|
+
* collide.
|
|
21
|
+
*
|
|
22
|
+
* :::caution
|
|
23
|
+
* TTL is currently ignored; Durable Object storage has no native TTL.
|
|
24
|
+
* If you need expiry, layer a TTL-aware backing store on top, or run
|
|
25
|
+
* a periodic `clear`.
|
|
26
|
+
* :::
|
|
27
|
+
*
|
|
28
|
+
* @binding
|
|
29
|
+
*
|
|
30
|
+
* @section Wiring it into a chat-backing DO
|
|
31
|
+
* @example Persisted chat history per DO instance
|
|
32
|
+
* `Persistence.layerResultPersisted({ storeId })` is the seam Effect
|
|
33
|
+
* AI exposes for cached/replayable AI calls. Layer
|
|
34
|
+
* `DurableObjectChatPersistence` underneath and every entry is stored
|
|
35
|
+
* in the DO's `state.storage` under the `alchemy.chat:` prefix.
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import * as Cloudflare from "alchemy/Cloudflare";
|
|
38
|
+
* import * as Effect from "effect/Effect";
|
|
39
|
+
* import * as Layer from "effect/Layer";
|
|
40
|
+
* import { Chat, LanguageModel } from "effect/unstable/ai";
|
|
41
|
+
* import { Persistence } from "effect/unstable/persistence";
|
|
42
|
+
*
|
|
43
|
+
* export default class ChatBackend extends Cloudflare.DurableObjectNamespace<ChatBackend>()(
|
|
44
|
+
* "ChatBackend",
|
|
45
|
+
* Effect.gen(function* () {
|
|
46
|
+
* return Effect.gen(function* () {
|
|
47
|
+
* const persistence = yield* Persistence.layerResultPersisted({
|
|
48
|
+
* storeId: "alchemy.chat",
|
|
49
|
+
* }).pipe(Layer.provide(Cloudflare.DurableObjectChatPersistence));
|
|
50
|
+
*
|
|
51
|
+
* return {
|
|
52
|
+
* send: (threadId: string, prompt: string) =>
|
|
53
|
+
* LanguageModel.generateText({ prompt }).pipe(Effect.provide(persistence)),
|
|
54
|
+
* };
|
|
55
|
+
* });
|
|
56
|
+
* }),
|
|
57
|
+
* ) {}
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @section Multiple stores in the same DO
|
|
61
|
+
* @example Separate `storeId`s coexist
|
|
62
|
+
* Different `storeId`s namespace their keys with `${storeId}:`, so
|
|
63
|
+
* one DO can keep, say, chat history *and* an audit log in separate
|
|
64
|
+
* stores without colliding.
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const aiPersistence = yield* Persistence.layerResultPersisted({
|
|
67
|
+
* storeId: "alchemy.chat",
|
|
68
|
+
* }).pipe(Layer.provide(Cloudflare.DurableObjectChatPersistence));
|
|
69
|
+
*
|
|
70
|
+
* const auditPersistence = yield* Persistence.layerResultPersisted({
|
|
71
|
+
* storeId: "alchemy.audit",
|
|
72
|
+
* }).pipe(Layer.provide(Cloudflare.DurableObjectChatPersistence));
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export const DurableObjectChatPersistence = Layer.effect(BackingPersistence)(
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
const state = yield* DurableObjectState;
|
|
78
|
+
const storage = state.storage;
|
|
79
|
+
|
|
80
|
+
const wrapErr = (op: string, key?: string) => (cause: unknown) =>
|
|
81
|
+
new PersistenceError({
|
|
82
|
+
message: `Failed to ${op}${key !== undefined ? ` key ${key}` : ""} in DurableObject storage`,
|
|
83
|
+
cause,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return BackingPersistence.of({
|
|
87
|
+
make: (storeId) =>
|
|
88
|
+
Effect.sync(() => {
|
|
89
|
+
const prefixed = (k: string) => `${storeId}:${k}`;
|
|
90
|
+
return {
|
|
91
|
+
get: (key) =>
|
|
92
|
+
storage
|
|
93
|
+
.get<object>(prefixed(key))
|
|
94
|
+
.pipe(Effect.mapError(wrapErr("get", key))),
|
|
95
|
+
getMany: (keys) =>
|
|
96
|
+
storage.get<object>(keys.map(prefixed)).pipe(
|
|
97
|
+
Effect.mapError(wrapErr("getMany")),
|
|
98
|
+
Effect.map(
|
|
99
|
+
(map) =>
|
|
100
|
+
keys.map((k) => map.get(prefixed(k))) as Arr.NonEmptyArray<
|
|
101
|
+
object | undefined
|
|
102
|
+
>,
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
set: (key, value, _ttl) =>
|
|
106
|
+
storage
|
|
107
|
+
.put(prefixed(key), value)
|
|
108
|
+
.pipe(Effect.mapError(wrapErr("set", key))),
|
|
109
|
+
setMany: (entries) =>
|
|
110
|
+
storage
|
|
111
|
+
.put(
|
|
112
|
+
Object.fromEntries(entries.map(([k, v]) => [prefixed(k), v])),
|
|
113
|
+
)
|
|
114
|
+
.pipe(Effect.mapError(wrapErr("setMany"))),
|
|
115
|
+
remove: (key) =>
|
|
116
|
+
storage
|
|
117
|
+
.delete(prefixed(key))
|
|
118
|
+
.pipe(Effect.asVoid, Effect.mapError(wrapErr("remove", key))),
|
|
119
|
+
clear: storage.list({ prefix: `${storeId}:` }).pipe(
|
|
120
|
+
Effect.flatMap((map) => {
|
|
121
|
+
const ks = [...map.keys()];
|
|
122
|
+
if (ks.length === 0) return Effect.void;
|
|
123
|
+
return Effect.asVoid(storage.delete(ks));
|
|
124
|
+
}),
|
|
125
|
+
Effect.mapError(wrapErr("clear")),
|
|
126
|
+
),
|
|
127
|
+
} satisfies BackingPersistenceStore;
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type * as cf from "@cloudflare/workers-types";
|
|
2
2
|
import * as workers from "@distilled.cloud/cloudflare/workers";
|
|
3
3
|
import * as zones from "@distilled.cloud/cloudflare/zones";
|
|
4
|
+
import type { ConfigError } from "effect/Config";
|
|
4
5
|
import * as Config from "effect/Config";
|
|
5
6
|
import * as Context from "effect/Context";
|
|
6
7
|
import * as Data from "effect/Data";
|
|
@@ -708,7 +709,11 @@ export const Worker: Platform<
|
|
|
708
709
|
id: string,
|
|
709
710
|
props:
|
|
710
711
|
| InputProps<WorkerProps<Bindings, Assets>>
|
|
711
|
-
| Effect.Effect<
|
|
712
|
+
| Effect.Effect<
|
|
713
|
+
InputProps<WorkerProps<Bindings, Assets>>,
|
|
714
|
+
ConfigError,
|
|
715
|
+
Req
|
|
716
|
+
>,
|
|
712
717
|
): Effect.Effect<
|
|
713
718
|
Worker<{
|
|
714
719
|
[binding in keyof NormalizedBindings<
|
|
@@ -20,6 +20,7 @@ import { isKVNamespace } from "../KV/KVNamespace.ts";
|
|
|
20
20
|
import { isQueue } from "../Queue/Queue.ts";
|
|
21
21
|
import { isR2Bucket } from "../R2/R2Bucket.ts";
|
|
22
22
|
import { isRateLimit } from "../RateLimit/RateLimit.ts";
|
|
23
|
+
import { isSecret } from "../SecretsStore/Secret.ts";
|
|
23
24
|
import { isVectorizeIndex } from "../Vectorize/VectorizeIndex.ts";
|
|
24
25
|
import { isAssets } from "./Assets.ts";
|
|
25
26
|
import { isDurableObjectNamespaceLike } from "./DurableObjectNamespace.ts";
|
|
@@ -210,6 +211,13 @@ const toBinding = (
|
|
|
210
211
|
name: bindingName,
|
|
211
212
|
indexName: binding.indexName,
|
|
212
213
|
};
|
|
214
|
+
} else if (isSecret(binding)) {
|
|
215
|
+
return {
|
|
216
|
+
type: "secrets_store_secret",
|
|
217
|
+
name: bindingName,
|
|
218
|
+
secretName: binding.secretName,
|
|
219
|
+
storeId: binding.storeId,
|
|
220
|
+
};
|
|
213
221
|
} else if (isDynamicWorkerLoader(binding)) {
|
|
214
222
|
return {
|
|
215
223
|
type: "worker_loader",
|
|
@@ -18,6 +18,7 @@ import type { KVNamespace } from "../KV/KVNamespace.ts";
|
|
|
18
18
|
import type { Queue } from "../Queue/Queue.ts";
|
|
19
19
|
import type { R2Bucket } from "../R2/R2Bucket.ts";
|
|
20
20
|
import type { RateLimit } from "../RateLimit/RateLimit.ts";
|
|
21
|
+
import type { Secret } from "../SecretsStore/Secret.ts";
|
|
21
22
|
import type { VectorizeIndex } from "../Vectorize/VectorizeIndex.ts";
|
|
22
23
|
import type { Assets } from "./Assets.ts";
|
|
23
24
|
import type { DurableObjectNamespaceLike } from "./DurableObjectNamespace.ts";
|
|
@@ -55,6 +56,7 @@ export type WorkerBindingResource =
|
|
|
55
56
|
| Images
|
|
56
57
|
| Hyperdrive
|
|
57
58
|
| VectorizeIndex
|
|
59
|
+
| Secret
|
|
58
60
|
| Worker
|
|
59
61
|
| DynamicWorkerLoader
|
|
60
62
|
| DurableObjectNamespaceLike<any>;
|
|
@@ -2,6 +2,7 @@ export * from "./Assets.ts";
|
|
|
2
2
|
export * from "./ConfigProvider.ts";
|
|
3
3
|
export * from "./CronEventSource.ts";
|
|
4
4
|
export * from "./DurableObjectBridge.ts";
|
|
5
|
+
export * from "./DurableObjectChatPersistence.ts";
|
|
5
6
|
export * from "./DurableObjectNamespace.ts";
|
|
6
7
|
export * from "./DurableObjectState.ts";
|
|
7
8
|
export * from "./DurableObjectStorage.ts";
|
package/src/Cloudflare/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./Providers.ts";
|
|
|
17
17
|
export * from "./Queue/index.ts";
|
|
18
18
|
export * from "./R2/index.ts";
|
|
19
19
|
export * from "./RateLimit/index.ts";
|
|
20
|
+
export * from "./Ruleset/index.ts";
|
|
20
21
|
export * from "./SecretsStore/index.ts";
|
|
21
22
|
export * from "./StateStore/index.ts";
|
|
22
23
|
export * from "./Tunnel/index.ts";
|
package/src/Deploy.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConfigError } from "effect/Config";
|
|
1
2
|
import * as Effect from "effect/Effect";
|
|
2
3
|
import type * as Scope from "effect/Scope";
|
|
3
4
|
import { AlchemyContext } from "./AlchemyContext.ts";
|
|
@@ -14,7 +15,7 @@ export const deploy = <A>({
|
|
|
14
15
|
scope,
|
|
15
16
|
force,
|
|
16
17
|
}: {
|
|
17
|
-
stack: StackEffect<CompiledStack<A>, Stage | AlchemyContext>;
|
|
18
|
+
stack: StackEffect<CompiledStack<A>, ConfigError, Stage | AlchemyContext>;
|
|
18
19
|
stage: string;
|
|
19
20
|
dev?: boolean;
|
|
20
21
|
/** See {@link evalStack} — when set, scoped resources outlive `deploy`. */
|