alchemy 2.0.0-beta.13 → 2.0.0-beta.15
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/alchemy.js +9477 -9290
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +6 -2
- package/bin/cli.js +61 -0
- package/bin/commands/deploy.ts +6 -4
- package/bin/commands/login.ts +1 -1
- package/bin/commands/logs.ts +1 -1
- package/bin/commands/state.ts +233 -0
- package/bin/commands/tail.ts +1 -1
- package/lib/AWS/AutoScaling/LaunchTemplate.d.ts +1 -1
- package/lib/AWS/EC2/Instance.d.ts +1 -1
- package/lib/AWS/EC2/hosted.d.ts +2 -2
- package/lib/AWS/ECS/Task.d.ts +4 -4
- package/lib/AWS/ECS/Task.d.ts.map +1 -1
- package/lib/AWS/ECS/Task.js +2 -2
- package/lib/AWS/ECS/Task.js.map +1 -1
- package/lib/AWS/Environment.d.ts +2 -2
- package/lib/AWS/Lambda/BucketEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.d.ts +2 -2
- package/lib/AWS/Lambda/Function.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.js +1 -2
- package/lib/AWS/Lambda/Function.js.map +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts.map +1 -1
- package/lib/AWS/SQS/QueueSink.d.ts.map +1 -1
- package/lib/AWS/SQS/QueueSink.js +1 -2
- package/lib/AWS/SQS/QueueSink.js.map +1 -1
- package/lib/Agent/coding/bash.d.ts +2 -6
- package/lib/Agent/coding/bash.d.ts.map +1 -1
- package/lib/Agent/coding/bash.js +2 -3
- package/lib/Agent/coding/bash.js.map +1 -1
- package/lib/Agent/coding/edit.d.ts +1 -1
- package/lib/Agent/coding/glob.d.ts +2 -4
- package/lib/Agent/coding/glob.d.ts.map +1 -1
- package/lib/Agent/coding/glob.js +1 -2
- package/lib/Agent/coding/glob.js.map +1 -1
- package/lib/Agent/coding/grep.d.ts +2 -4
- package/lib/Agent/coding/grep.d.ts.map +1 -1
- package/lib/Agent/coding/grep.js +1 -2
- package/lib/Agent/coding/grep.js.map +1 -1
- package/lib/Agent/coding/read.d.ts +1 -1
- package/lib/Agent/coding/write.d.ts +1 -1
- package/lib/Agent/lsp/client.d.ts +2 -2
- package/lib/Agent/util/ripgrep.d.ts +2 -2
- package/lib/AlchemyContext.d.ts +13 -0
- package/lib/AlchemyContext.d.ts.map +1 -0
- package/lib/AlchemyContext.js +18 -0
- package/lib/AlchemyContext.js.map +1 -0
- package/lib/Auth/AuthProvider.js +1 -1
- package/lib/Auth/AuthProvider.js.map +1 -1
- package/lib/Auth/Profile.d.ts +1 -1
- package/lib/Auth/index.d.ts +3 -1
- package/lib/Auth/index.d.ts.map +1 -1
- package/lib/Auth/index.js +3 -0
- package/lib/Auth/index.js.map +1 -1
- package/lib/Build/Command.d.ts +1 -1
- package/lib/Build/Memo.d.ts +1 -1
- package/lib/Bundle/Docker.d.ts +3 -3
- package/lib/Bundle/TempRoot.d.ts +3 -3
- package/lib/Cli/LoggingCli.d.ts +4 -0
- package/lib/Cli/LoggingCli.d.ts.map +1 -0
- package/lib/{Test/TestCli.js → Cli/LoggingCli.js} +3 -3
- package/lib/Cli/LoggingCli.js.map +1 -0
- package/lib/Cli/index.d.ts +0 -1
- package/lib/Cli/index.d.ts.map +1 -1
- package/lib/Cli/index.js +4 -1
- package/lib/Cli/index.js.map +1 -1
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts +58 -0
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/AccountApiToken.js +137 -0
- package/lib/Cloudflare/ApiToken/AccountApiToken.js.map +1 -0
- package/lib/Cloudflare/ApiToken/Common.d.ts +91 -0
- package/lib/Cloudflare/ApiToken/Common.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/Common.js +61 -0
- package/lib/Cloudflare/ApiToken/Common.js.map +1 -0
- package/lib/Cloudflare/ApiToken/PermissionGroups.d.ts +1549 -0
- package/lib/Cloudflare/ApiToken/PermissionGroups.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/PermissionGroups.js +1856 -0
- package/lib/Cloudflare/ApiToken/PermissionGroups.js.map +1 -0
- package/lib/Cloudflare/ApiToken/UserApiToken.d.ts +49 -0
- package/lib/Cloudflare/ApiToken/UserApiToken.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/UserApiToken.js +113 -0
- package/lib/Cloudflare/ApiToken/UserApiToken.js.map +1 -0
- package/lib/Cloudflare/ApiToken/index.d.ts +5 -0
- package/lib/Cloudflare/ApiToken/index.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/index.js +5 -0
- package/lib/Cloudflare/ApiToken/index.js.map +1 -0
- package/lib/Cloudflare/Auth/AuthProvider.js +2 -2
- package/lib/Cloudflare/Auth/AuthProvider.js.map +1 -1
- package/lib/Cloudflare/CloudflareEnvironment.d.ts +1 -1
- package/lib/Cloudflare/Container/ContainerApplication.d.ts +2 -2
- package/lib/Cloudflare/Container/ContainerApplication.d.ts.map +1 -1
- package/lib/Cloudflare/Container/ContainerApplication.js +7 -2
- package/lib/Cloudflare/Container/ContainerApplication.js.map +1 -1
- package/lib/Cloudflare/EdgeSession.d.ts +53 -0
- package/lib/Cloudflare/EdgeSession.d.ts.map +1 -0
- package/lib/Cloudflare/EdgeSession.js +78 -0
- package/lib/Cloudflare/EdgeSession.js.map +1 -0
- package/lib/Cloudflare/Providers.d.ts +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +4 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +61 -0
- package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Api.js +103 -0
- package/lib/Cloudflare/StateStore/Api.js.map +1 -0
- package/lib/Cloudflare/StateStore/State.d.ts +39 -0
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/State.js +217 -0
- package/lib/Cloudflare/StateStore/State.js.map +1 -0
- package/lib/Cloudflare/StateStore/Store.d.ts +82 -0
- package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Store.js +191 -0
- package/lib/Cloudflare/StateStore/Store.js.map +1 -0
- package/lib/Cloudflare/StateStore/Token.d.ts +43 -0
- package/lib/Cloudflare/StateStore/Token.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Token.js +61 -0
- package/lib/Cloudflare/StateStore/Token.js.map +1 -0
- package/lib/Cloudflare/StateStore/index.d.ts +2 -0
- package/lib/Cloudflare/StateStore/index.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/index.js +2 -0
- package/lib/Cloudflare/StateStore/index.js.map +1 -0
- package/lib/Cloudflare/Workers/Assets.d.ts +2 -2
- package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts +2 -2
- package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/DurableObjectNamespace.js +0 -1
- package/lib/Cloudflare/Workers/DurableObjectNamespace.js.map +1 -1
- package/lib/Cloudflare/Workers/HttpServer.d.ts +1 -1
- package/lib/Cloudflare/Workers/HttpServer.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/HttpServer.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +5 -5
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +0 -1
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/index.d.ts +4 -1
- package/lib/Cloudflare/index.d.ts.map +1 -1
- package/lib/Cloudflare/index.js +4 -1
- package/lib/Cloudflare/index.js.map +1 -1
- package/lib/Deploy.d.ts +10 -0
- package/lib/Deploy.d.ts.map +1 -0
- package/lib/Deploy.js +12 -0
- package/lib/Deploy.js.map +1 -0
- package/lib/Destroy.d.ts +8 -3
- package/lib/Destroy.d.ts.map +1 -1
- package/lib/Destroy.js +8 -8
- package/lib/Destroy.js.map +1 -1
- package/lib/GitHub/Providers.d.ts +3 -0
- package/lib/GitHub/Providers.d.ts.map +1 -0
- package/lib/GitHub/Providers.js +5 -0
- package/lib/GitHub/Providers.js.map +1 -0
- package/lib/GitHub/index.d.ts +1 -0
- package/lib/GitHub/index.d.ts.map +1 -1
- package/lib/GitHub/index.js +1 -0
- package/lib/GitHub/index.js.map +1 -1
- package/lib/Http.d.ts +1 -1
- package/lib/Http.d.ts.map +1 -1
- package/lib/Http.js +7 -3
- package/lib/Http.js.map +1 -1
- package/lib/Kubernetes/client.d.ts +10 -10
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +4 -0
- package/lib/Output.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +4 -0
- package/lib/Plan.js.map +1 -1
- package/lib/Platform.d.ts +2 -2
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Platform.js.map +1 -1
- package/lib/Provider.d.ts.map +1 -1
- package/lib/Provider.js +0 -1
- package/lib/Provider.js.map +1 -1
- package/lib/Stack.d.ts +24 -12
- package/lib/Stack.d.ts.map +1 -1
- package/lib/Stack.js +47 -10
- package/lib/Stack.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +96 -0
- package/lib/State/HttpStateApi.d.ts.map +1 -0
- package/lib/State/HttpStateApi.js +96 -0
- package/lib/State/HttpStateApi.js.map +1 -0
- package/lib/State/HttpStateStore.d.ts +10 -0
- package/lib/State/HttpStateStore.d.ts.map +1 -0
- package/lib/State/HttpStateStore.js +102 -0
- package/lib/State/HttpStateStore.js.map +1 -0
- package/lib/State/InMemoryState.d.ts +2 -30
- package/lib/State/InMemoryState.d.ts.map +1 -1
- package/lib/State/InMemoryState.js +23 -18
- package/lib/State/InMemoryState.js.map +1 -1
- package/lib/State/LocalState.d.ts +4 -2
- package/lib/State/LocalState.d.ts.map +1 -1
- package/lib/State/LocalState.js +9 -41
- package/lib/State/LocalState.js.map +1 -1
- package/lib/State/State.d.ts +11 -4
- package/lib/State/State.d.ts.map +1 -1
- package/lib/State/State.js +1 -1
- package/lib/State/State.js.map +1 -1
- package/lib/State/StateEncoding.d.ts +31 -0
- package/lib/State/StateEncoding.d.ts.map +1 -0
- package/lib/State/StateEncoding.js +82 -0
- package/lib/State/StateEncoding.js.map +1 -0
- package/lib/State/Sync.d.ts +23 -0
- package/lib/State/Sync.d.ts.map +1 -0
- package/lib/State/Sync.js +49 -0
- package/lib/State/Sync.js.map +1 -0
- package/lib/State/index.d.ts +4 -0
- package/lib/State/index.d.ts.map +1 -1
- package/lib/State/index.js +4 -0
- package/lib/State/index.js.map +1 -1
- package/lib/Test/Bun.d.ts +13 -17
- package/lib/Test/Bun.d.ts.map +1 -1
- package/lib/Test/Bun.js +17 -36
- package/lib/Test/Bun.js.map +1 -1
- package/lib/Test/TestState.d.ts +1 -2
- package/lib/Test/TestState.d.ts.map +1 -1
- package/lib/Test/TestState.js +5 -9
- package/lib/Test/TestState.js.map +1 -1
- package/lib/Test/Vitest.d.ts +3 -2
- package/lib/Test/Vitest.d.ts.map +1 -1
- package/lib/Test/Vitest.js +20 -11
- package/lib/Test/Vitest.js.map +1 -1
- package/lib/Test/index.d.ts +0 -1
- package/lib/Test/index.d.ts.map +1 -1
- package/lib/Test/index.js +0 -1
- package/lib/Test/index.js.map +1 -1
- package/lib/Util/FileLogger.d.ts +2 -2
- package/lib/Util/FileLogger.d.ts.map +1 -1
- package/lib/Util/FileLogger.js +2 -2
- package/lib/Util/FileLogger.js.map +1 -1
- package/lib/cli/index.d.ts +10 -5
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +2 -43045
- package/lib/cli/index.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/package.json +30 -15
- package/src/AWS/ECS/Task.ts +4 -4
- package/src/AWS/Lambda/Function.ts +2 -3
- package/src/AWS/SQS/QueueSink.ts +1 -2
- package/src/Agent/coding/bash.ts +2 -3
- package/src/Agent/coding/glob.ts +1 -2
- package/src/Agent/coding/grep.ts +1 -2
- package/src/AlchemyContext.ts +27 -0
- package/src/Auth/AuthProvider.ts +1 -1
- package/src/Auth/index.ts +3 -1
- package/src/{Test/TestCli.ts → Cli/LoggingCli.ts} +2 -2
- package/src/Cli/index.ts +4 -1
- package/src/Cloudflare/ApiToken/AccountApiToken.ts +197 -0
- package/src/Cloudflare/ApiToken/Common.ts +148 -0
- package/src/Cloudflare/ApiToken/PermissionGroups.ts +1910 -0
- package/src/Cloudflare/ApiToken/UserApiToken.ts +168 -0
- package/src/Cloudflare/ApiToken/index.ts +4 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +2 -2
- package/src/Cloudflare/Container/ContainerApplication.ts +8 -2
- package/src/Cloudflare/EdgeSession.ts +144 -0
- package/src/Cloudflare/Providers.ts +5 -0
- package/src/Cloudflare/StateStore/Api.ts +159 -0
- package/src/Cloudflare/StateStore/State.ts +318 -0
- package/src/Cloudflare/StateStore/Store.ts +290 -0
- package/src/Cloudflare/StateStore/Token.ts +68 -0
- package/src/Cloudflare/StateStore/index.ts +1 -0
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +1 -6
- package/src/Cloudflare/Workers/HttpServer.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +7 -7
- package/src/Cloudflare/index.ts +4 -1
- package/src/Deploy.ts +25 -0
- package/src/Destroy.ts +25 -13
- package/src/GitHub/Providers.ts +6 -0
- package/src/GitHub/index.ts +1 -0
- package/src/Http.ts +31 -16
- package/src/Output.ts +3 -2
- package/src/Plan.ts +3 -0
- package/src/Platform.ts +8 -2
- package/src/Provider.ts +0 -2
- package/src/Stack.ts +146 -45
- package/src/State/HttpStateApi.ts +155 -0
- package/src/State/HttpStateStore.ts +156 -0
- package/src/State/InMemoryState.ts +15 -17
- package/src/State/LocalState.ts +13 -45
- package/src/State/State.ts +13 -4
- package/src/State/StateEncoding.ts +83 -0
- package/src/State/Sync.ts +89 -0
- package/src/State/index.ts +4 -0
- package/src/Test/Bun.ts +31 -89
- package/src/Test/TestState.ts +6 -8
- package/src/Test/Vitest.ts +32 -19
- package/src/Test/index.ts +0 -1
- package/src/Util/FileLogger.ts +2 -2
- package/src/index.ts +2 -1
- package/lib/Config.d.ts +0 -20
- package/lib/Config.d.ts.map +0 -1
- package/lib/Config.js +0 -22
- package/lib/Config.js.map +0 -1
- package/lib/Observability.d.ts +0 -2
- package/lib/Observability.d.ts.map +0 -1
- package/lib/Observability.js +0 -1
- package/lib/Observability.js.map +0 -1
- package/lib/Test/TestCli.d.ts +0 -4
- package/lib/Test/TestCli.d.ts.map +0 -1
- package/lib/Test/TestCli.js.map +0 -1
- package/src/Config.ts +0 -31
- package/src/Observability.ts +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as user from "@distilled.cloud/cloudflare/user";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Redacted from "effect/Redacted";
|
|
4
|
+
import { isResolved } from "../../Diff.ts";
|
|
5
|
+
import { createPhysicalName } from "../../PhysicalName.ts";
|
|
6
|
+
import * as Provider from "../../Provider.ts";
|
|
7
|
+
import { Resource } from "../../Resource.ts";
|
|
8
|
+
import type { Providers } from "../Providers.ts";
|
|
9
|
+
import {
|
|
10
|
+
buildConditionPayload,
|
|
11
|
+
conditionFingerprint,
|
|
12
|
+
policyFingerprint,
|
|
13
|
+
resolvePolicies,
|
|
14
|
+
type BaseApiTokenProps,
|
|
15
|
+
} from "./Common.ts";
|
|
16
|
+
|
|
17
|
+
export type UserApiTokenProps = BaseApiTokenProps;
|
|
18
|
+
|
|
19
|
+
export type UserApiToken = Resource<
|
|
20
|
+
"Cloudflare.UserApiToken",
|
|
21
|
+
UserApiTokenProps,
|
|
22
|
+
{
|
|
23
|
+
tokenId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
status: "active" | "disabled" | "expired";
|
|
26
|
+
/**
|
|
27
|
+
* The plaintext token value. Cloudflare returns this only once, on
|
|
28
|
+
* creation, so we persist it here for downstream consumers.
|
|
29
|
+
*/
|
|
30
|
+
value: Redacted.Redacted<string>;
|
|
31
|
+
},
|
|
32
|
+
never,
|
|
33
|
+
Providers
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A Cloudflare user-owned API token (`POST /user/tokens`).
|
|
38
|
+
*
|
|
39
|
+
* User-owned tokens are tied to the authenticated user's identity. They can
|
|
40
|
+
* be created by any authenticated user (including OAuth-derived sessions
|
|
41
|
+
* from `alchemy login`) without needing the account-level
|
|
42
|
+
* `API Tokens > Write` permission, but they are also revoked if the user
|
|
43
|
+
* leaves the account.
|
|
44
|
+
*
|
|
45
|
+
* For CI tokens, prefer {@link AccountApiToken} so the token survives
|
|
46
|
+
* personnel changes.
|
|
47
|
+
*
|
|
48
|
+
* Policy `resources` are passed through verbatim — no `accountId` rewriting
|
|
49
|
+
* is performed because user tokens aren't bound to a single account.
|
|
50
|
+
*
|
|
51
|
+
* @section Creating a Token
|
|
52
|
+
* @example A token bound to the authenticated user
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const token = yield* Cloudflare.UserApiToken("personal-token", {
|
|
55
|
+
* name: "my-personal-token",
|
|
56
|
+
* policies: [
|
|
57
|
+
* {
|
|
58
|
+
* effect: "allow",
|
|
59
|
+
* permissionGroups: ["Workers Scripts Read"],
|
|
60
|
+
* resources: { [`com.cloudflare.api.account.${accountId}`]: "*" },
|
|
61
|
+
* },
|
|
62
|
+
* ],
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export const UserApiToken = Resource<UserApiToken>("Cloudflare.UserApiToken");
|
|
67
|
+
|
|
68
|
+
type UserApiTokenAttributes = UserApiToken["Attributes"];
|
|
69
|
+
|
|
70
|
+
const resolveName = (id: string, name: string | undefined) =>
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
return name ?? (yield* createPhysicalName({ id }));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const UserApiTokenProvider = () =>
|
|
76
|
+
Provider.effect(
|
|
77
|
+
UserApiToken,
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const createToken = yield* user.createToken;
|
|
80
|
+
const updateToken = yield* user.updateToken;
|
|
81
|
+
const deleteToken = yield* user.deleteToken;
|
|
82
|
+
const getToken = yield* user.getToken;
|
|
83
|
+
|
|
84
|
+
const buildAttributes = (
|
|
85
|
+
tokenData: {
|
|
86
|
+
id?: string | null;
|
|
87
|
+
name?: string | null;
|
|
88
|
+
status?: "active" | "disabled" | "expired" | null;
|
|
89
|
+
},
|
|
90
|
+
value: Redacted.Redacted<string>,
|
|
91
|
+
): UserApiTokenAttributes => ({
|
|
92
|
+
tokenId: tokenData.id ?? "",
|
|
93
|
+
name: tokenData.name ?? "",
|
|
94
|
+
status: tokenData.status ?? "active",
|
|
95
|
+
value,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
stables: ["tokenId"],
|
|
100
|
+
diff: Effect.fn(function* ({ id, olds, news, output }) {
|
|
101
|
+
if (!isResolved(news)) return undefined;
|
|
102
|
+
const oldName = output?.name ?? (yield* resolveName(id, olds?.name));
|
|
103
|
+
const newName = yield* resolveName(id, news.name);
|
|
104
|
+
const oldPolicyFp = policyFingerprint(
|
|
105
|
+
resolvePolicies(olds?.policies ?? [], undefined),
|
|
106
|
+
);
|
|
107
|
+
const newPolicyFp = policyFingerprint(
|
|
108
|
+
resolvePolicies(news.policies, undefined),
|
|
109
|
+
);
|
|
110
|
+
const oldCondFp = conditionFingerprint(olds?.condition);
|
|
111
|
+
const newCondFp = conditionFingerprint(news.condition);
|
|
112
|
+
if (
|
|
113
|
+
oldName !== newName ||
|
|
114
|
+
oldPolicyFp !== newPolicyFp ||
|
|
115
|
+
oldCondFp !== newCondFp ||
|
|
116
|
+
(olds?.expiresOn ?? undefined) !== (news.expiresOn ?? undefined) ||
|
|
117
|
+
(olds?.notBefore ?? undefined) !== (news.notBefore ?? undefined)
|
|
118
|
+
) {
|
|
119
|
+
return { action: "update" } as const;
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
create: Effect.fn(function* ({ id, news }) {
|
|
123
|
+
const name = yield* resolveName(id, news.name);
|
|
124
|
+
const policies = resolvePolicies(news.policies, undefined);
|
|
125
|
+
const result = yield* createToken({
|
|
126
|
+
name,
|
|
127
|
+
policies,
|
|
128
|
+
condition: buildConditionPayload(news.condition),
|
|
129
|
+
expiresOn: news.expiresOn,
|
|
130
|
+
notBefore: news.notBefore,
|
|
131
|
+
});
|
|
132
|
+
if (!result.value) {
|
|
133
|
+
return yield* Effect.die(
|
|
134
|
+
`Cloudflare did not return a value for token "${name}".`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return buildAttributes(result, Redacted.make(result.value));
|
|
138
|
+
}),
|
|
139
|
+
update: Effect.fn(function* ({ id, news, output }) {
|
|
140
|
+
const name = yield* resolveName(id, news.name);
|
|
141
|
+
const policies = resolvePolicies(news.policies, undefined);
|
|
142
|
+
const result = yield* updateToken({
|
|
143
|
+
tokenId: output.tokenId,
|
|
144
|
+
name,
|
|
145
|
+
policies,
|
|
146
|
+
condition: buildConditionPayload(news.condition),
|
|
147
|
+
expiresOn: news.expiresOn,
|
|
148
|
+
notBefore: news.notBefore,
|
|
149
|
+
});
|
|
150
|
+
// Cloudflare doesn't return the value on update; preserve the
|
|
151
|
+
// value captured at creation.
|
|
152
|
+
return buildAttributes(result, output.value);
|
|
153
|
+
}),
|
|
154
|
+
delete: Effect.fn(function* ({ output }) {
|
|
155
|
+
yield* deleteToken({ tokenId: output.tokenId }).pipe(
|
|
156
|
+
Effect.catchTag("TokenNotFound", () => Effect.void),
|
|
157
|
+
);
|
|
158
|
+
}),
|
|
159
|
+
read: Effect.fn(function* ({ output }) {
|
|
160
|
+
if (!output?.tokenId) return undefined;
|
|
161
|
+
return yield* getToken({ tokenId: output.tokenId }).pipe(
|
|
162
|
+
Effect.map((token) => buildAttributes(token, output.value)),
|
|
163
|
+
Effect.catchTag("TokenNotFound", () => Effect.succeed(undefined)),
|
|
164
|
+
);
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
@@ -5,6 +5,7 @@ import * as Option from "effect/Option";
|
|
|
5
5
|
import * as Schedule from "effect/Schedule";
|
|
6
6
|
import type * as rolldown from "rolldown";
|
|
7
7
|
import { AdoptPolicy } from "../../AdoptPolicy.ts";
|
|
8
|
+
import { AlchemyContext } from "../../AlchemyContext.ts";
|
|
8
9
|
import * as Bundle from "../../Bundle/Bundle.ts";
|
|
9
10
|
import {
|
|
10
11
|
dockerBuild,
|
|
@@ -16,7 +17,6 @@ import {
|
|
|
16
17
|
findCwdForBundle,
|
|
17
18
|
getStableContextDir,
|
|
18
19
|
} from "../../Bundle/TempRoot.ts";
|
|
19
|
-
import { DotAlchemy } from "../../Config.ts";
|
|
20
20
|
import { deepEqual, isResolved } from "../../Diff.ts";
|
|
21
21
|
import { createPhysicalName } from "../../PhysicalName.ts";
|
|
22
22
|
import {
|
|
@@ -359,7 +359,7 @@ export const ContainerProvider = () =>
|
|
|
359
359
|
const adoptPolicy = yield* Effect.serviceOption(AdoptPolicy).pipe(
|
|
360
360
|
Effect.map(Option.getOrElse(() => false)),
|
|
361
361
|
);
|
|
362
|
-
const dotAlchemy = yield*
|
|
362
|
+
const { dotAlchemy } = yield* AlchemyContext;
|
|
363
363
|
const fs = yield* FileSystem.FileSystem;
|
|
364
364
|
const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
|
|
365
365
|
const createContainerApplication =
|
|
@@ -498,6 +498,12 @@ export const ContainerProvider = () =>
|
|
|
498
498
|
...external,
|
|
499
499
|
],
|
|
500
500
|
platform: "node",
|
|
501
|
+
resolve: {
|
|
502
|
+
conditionNames:
|
|
503
|
+
runtime === "bun"
|
|
504
|
+
? ["bun", "import", "module", "default"]
|
|
505
|
+
: ["node", "import", "module", "default"],
|
|
506
|
+
},
|
|
501
507
|
plugins,
|
|
502
508
|
treeshake: true,
|
|
503
509
|
},
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Credentials } from "@distilled.cloud/cloudflare/Credentials";
|
|
2
|
+
import * as workers from "@distilled.cloud/cloudflare/workers";
|
|
3
|
+
import * as Data from "effect/Data";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
6
|
+
import { CloudflareEnvironment } from "./CloudflareEnvironment.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A "remote binding" as understood by the Cloudflare Workers
|
|
10
|
+
* edge-preview API. We reuse the distilled request schema so the
|
|
11
|
+
* shape tracks Cloudflare's surface.
|
|
12
|
+
*/
|
|
13
|
+
type EdgeMetadata = NonNullable<
|
|
14
|
+
NonNullable<workers.CreateScriptEdgePreviewRequest["metadata"]>
|
|
15
|
+
>;
|
|
16
|
+
export type EdgeBinding = NonNullable<
|
|
17
|
+
NonNullable<EdgeMetadata["bindings"]>
|
|
18
|
+
>[number];
|
|
19
|
+
|
|
20
|
+
export class EdgeSessionError extends Data.TaggedError("EdgeSessionError")<{
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly cause?: unknown;
|
|
23
|
+
}> {}
|
|
24
|
+
|
|
25
|
+
export interface EdgeSessionOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Name used as the preview script identifier on Cloudflare's side.
|
|
28
|
+
* Shows up as the hostname: `{scriptName}.{subdomain}.workers.dev`.
|
|
29
|
+
*/
|
|
30
|
+
readonly scriptName: string;
|
|
31
|
+
/** ES-module worker script(s). The first file is the entrypoint. */
|
|
32
|
+
readonly files: ReadonlyArray<File>;
|
|
33
|
+
/** Remote bindings attached to the preview worker. */
|
|
34
|
+
readonly bindings: ReadonlyArray<EdgeBinding>;
|
|
35
|
+
/** @default "2025-04-28" */
|
|
36
|
+
readonly compatibilityDate?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Information needed to call a running edge-preview worker. Attach
|
|
41
|
+
* `headers` (including the `cf-workers-preview-token`) and fetch
|
|
42
|
+
* `url` or a path under it.
|
|
43
|
+
*/
|
|
44
|
+
export interface EdgeSessionHandle {
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly headers: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DEFAULT_COMPATIBILITY_DATE = "2025-04-28";
|
|
50
|
+
|
|
51
|
+
const wrap = <A, E, R>(effect: Effect.Effect<A, E, R>, message: string) =>
|
|
52
|
+
effect.pipe(
|
|
53
|
+
Effect.mapError((cause) => new EdgeSessionError({ message, cause })),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ask Cloudflare for a preview upload token. Some accounts return an
|
|
58
|
+
* `exchangeUrl` that must be GET'd to swap the session token for a
|
|
59
|
+
* short-lived upload token; when present we do the swap, falling back
|
|
60
|
+
* to the original if the exchange fails.
|
|
61
|
+
*/
|
|
62
|
+
const createUploadToken = Effect.gen(function* () {
|
|
63
|
+
const env = yield* CloudflareEnvironment;
|
|
64
|
+
const http = yield* HttpClient.HttpClient;
|
|
65
|
+
const createSubdomainEdgePreviewSession =
|
|
66
|
+
yield* workers.createSubdomainEdgePreviewSession;
|
|
67
|
+
const { token, exchangeUrl } = yield* createSubdomainEdgePreviewSession({
|
|
68
|
+
accountId: env.accountId,
|
|
69
|
+
});
|
|
70
|
+
if (!exchangeUrl) return token;
|
|
71
|
+
const json = yield* http.get(exchangeUrl).pipe(
|
|
72
|
+
Effect.flatMap((r) => r.json),
|
|
73
|
+
Effect.timeout(30_000),
|
|
74
|
+
Effect.catch(() => Effect.succeed(null as unknown)),
|
|
75
|
+
);
|
|
76
|
+
if (
|
|
77
|
+
typeof json === "object" &&
|
|
78
|
+
json !== null &&
|
|
79
|
+
"token" in json &&
|
|
80
|
+
typeof (json as Record<string, unknown>).token === "string"
|
|
81
|
+
) {
|
|
82
|
+
return (json as { token: string }).token;
|
|
83
|
+
}
|
|
84
|
+
return token;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const uploadScript = (options: EdgeSessionOptions, uploadToken: string) =>
|
|
88
|
+
Effect.gen(function* () {
|
|
89
|
+
const env = yield* CloudflareEnvironment;
|
|
90
|
+
const createScriptEdgePreview = yield* workers.createScriptEdgePreview;
|
|
91
|
+
return yield* createScriptEdgePreview({
|
|
92
|
+
accountId: env.accountId,
|
|
93
|
+
scriptName: options.scriptName,
|
|
94
|
+
cfPreviewUploadConfigToken: uploadToken,
|
|
95
|
+
wranglerSessionConfig: { workersDev: true, minimalMode: true },
|
|
96
|
+
metadata: {
|
|
97
|
+
compatibilityDate:
|
|
98
|
+
options.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE,
|
|
99
|
+
bindings: options.bindings as EdgeMetadata["bindings"],
|
|
100
|
+
mainModule: options.files[0]!.name,
|
|
101
|
+
},
|
|
102
|
+
// Distilled's generated schema expects a mutable array; our
|
|
103
|
+
// public surface keeps `readonly` so callers can pass a frozen
|
|
104
|
+
// tuple without a cast on their side.
|
|
105
|
+
files: options.files.slice(),
|
|
106
|
+
}).pipe(Effect.timeout(30_000));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const workerHost = (scriptName: string) =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const env = yield* CloudflareEnvironment;
|
|
112
|
+
const getSubdomain = yield* workers.getSubdomain;
|
|
113
|
+
const { subdomain } = yield* getSubdomain({ accountId: env.accountId });
|
|
114
|
+
return `${scriptName}.${subdomain}.workers.dev`;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Upload a short-lived preview worker to Cloudflare's edge and
|
|
119
|
+
* return its URL plus the headers needed to reach it.
|
|
120
|
+
*
|
|
121
|
+
* Preview workers are ephemeral: they live only while a client keeps
|
|
122
|
+
* sending requests with the returned preview token and expire soon
|
|
123
|
+
* after. Nothing is deployed or billed.
|
|
124
|
+
*/
|
|
125
|
+
export const createEdgeSession = (
|
|
126
|
+
options: EdgeSessionOptions,
|
|
127
|
+
): Effect.Effect<
|
|
128
|
+
EdgeSessionHandle,
|
|
129
|
+
EdgeSessionError,
|
|
130
|
+
CloudflareEnvironment | HttpClient.HttpClient | Credentials
|
|
131
|
+
> =>
|
|
132
|
+
Effect.gen(function* () {
|
|
133
|
+
const [{ previewToken }, host] = yield* Effect.all(
|
|
134
|
+
[
|
|
135
|
+
createUploadToken.pipe(Effect.flatMap((t) => uploadScript(options, t))),
|
|
136
|
+
workerHost(options.scriptName),
|
|
137
|
+
],
|
|
138
|
+
{ concurrency: "unbounded" },
|
|
139
|
+
);
|
|
140
|
+
return {
|
|
141
|
+
url: `https://${host}`,
|
|
142
|
+
headers: { "cf-workers-preview-token": previewToken },
|
|
143
|
+
} satisfies EdgeSessionHandle;
|
|
144
|
+
}).pipe((e) => wrap(e, "Failed to create edge preview session"));
|
|
@@ -3,6 +3,7 @@ import { Command } from "../Build/Command.ts";
|
|
|
3
3
|
import * as Build from "../Build/index.ts";
|
|
4
4
|
import * as Provider from "../Provider.ts";
|
|
5
5
|
import { Random, RandomProvider } from "../Random.ts";
|
|
6
|
+
import * as ApiToken from "./ApiToken/index.ts";
|
|
6
7
|
import { CloudflareAuth } from "./Auth/AuthProvider.ts";
|
|
7
8
|
import * as CloudflareEnvironment from "./CloudflareEnvironment.ts";
|
|
8
9
|
import * as Containers from "./Container/index.ts";
|
|
@@ -30,6 +31,8 @@ export const providers = () =>
|
|
|
30
31
|
Layer.effect(
|
|
31
32
|
Providers,
|
|
32
33
|
Provider.collection([
|
|
34
|
+
ApiToken.AccountApiToken,
|
|
35
|
+
ApiToken.UserApiToken,
|
|
33
36
|
Command,
|
|
34
37
|
Containers.Container,
|
|
35
38
|
D1.D1ConnectionPolicy,
|
|
@@ -53,6 +56,8 @@ export const providers = () =>
|
|
|
53
56
|
).pipe(
|
|
54
57
|
Layer.provide(
|
|
55
58
|
Layer.mergeAll(
|
|
59
|
+
ApiToken.AccountApiTokenProvider(),
|
|
60
|
+
ApiToken.UserApiTokenProvider(),
|
|
56
61
|
Containers.ContainerProvider(),
|
|
57
62
|
D1.D1ConnectionPolicyLive,
|
|
58
63
|
D1.DatabaseProvider(),
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BearerTokenValidator,
|
|
3
|
+
StateApi,
|
|
4
|
+
StateAuthLive,
|
|
5
|
+
} from "alchemy/State/HttpStateApi";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Layer from "effect/Layer";
|
|
8
|
+
import * as Path from "effect/Path";
|
|
9
|
+
import * as Etag from "effect/unstable/http/Etag";
|
|
10
|
+
import * as HttpPlatform from "effect/unstable/http/HttpPlatform";
|
|
11
|
+
import * as HttpRouter from "effect/unstable/http/HttpRouter";
|
|
12
|
+
import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
|
|
13
|
+
import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
|
|
14
|
+
import crypto from "node:crypto";
|
|
15
|
+
import { SecretBindingLive } from "../SecretsStore/SecretBinding.ts";
|
|
16
|
+
import * as Secret from "../SecretsStore/Secret.ts";
|
|
17
|
+
import { Worker } from "../Workers/Worker.ts";
|
|
18
|
+
import Store from "./Store.ts";
|
|
19
|
+
import { AuthToken } from "./Token.ts";
|
|
20
|
+
|
|
21
|
+
export const STATE_STORE_SCRIPT_NAME = "alchemy-state-store" as const;
|
|
22
|
+
|
|
23
|
+
export default Worker(
|
|
24
|
+
"Api",
|
|
25
|
+
{
|
|
26
|
+
name: STATE_STORE_SCRIPT_NAME,
|
|
27
|
+
main: import.meta.path,
|
|
28
|
+
url: true,
|
|
29
|
+
compatibility: {
|
|
30
|
+
flags: ["nodejs_compat"],
|
|
31
|
+
date: "2026-03-17",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const secret = yield* Secret.Secret.bind(AuthToken);
|
|
36
|
+
const store = yield* Store;
|
|
37
|
+
|
|
38
|
+
const bearerTokenValidator = Layer.effect(
|
|
39
|
+
BearerTokenValidator,
|
|
40
|
+
secret.get().pipe(
|
|
41
|
+
Effect.map((expected) =>
|
|
42
|
+
BearerTokenValidator.of({
|
|
43
|
+
validate: (token) =>
|
|
44
|
+
!!expected && timingSafeEqual(token, expected)
|
|
45
|
+
? Effect.void
|
|
46
|
+
: Effect.fail(new HttpApiError.Unauthorized()),
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
Effect.orDie,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const stateApi = HttpApiBuilder.group(StateApi, "state", (handlers) =>
|
|
54
|
+
handlers
|
|
55
|
+
.handle("listStacks", () =>
|
|
56
|
+
store.getByName(Store.ROOT_DO_NAME).listStacks(),
|
|
57
|
+
)
|
|
58
|
+
.handle("listStages", ({ params }) =>
|
|
59
|
+
store.getByName(params.stack).listStages(),
|
|
60
|
+
)
|
|
61
|
+
.handle("listResources", ({ params }) =>
|
|
62
|
+
store.getByName(params.stack).listResources({ stage: params.stage }),
|
|
63
|
+
)
|
|
64
|
+
.handle("getState", ({ params }) =>
|
|
65
|
+
store
|
|
66
|
+
.getByName(params.stack)
|
|
67
|
+
.get({ stage: params.stage, fqn: decodeURIComponent(params.fqn) }),
|
|
68
|
+
)
|
|
69
|
+
.handle("setState", ({ params, payload }) =>
|
|
70
|
+
store
|
|
71
|
+
.getByName(params.stack)
|
|
72
|
+
.set({
|
|
73
|
+
stage: params.stage,
|
|
74
|
+
fqn: decodeURIComponent(params.fqn),
|
|
75
|
+
value: payload as any,
|
|
76
|
+
})
|
|
77
|
+
.pipe(
|
|
78
|
+
Effect.tap(() =>
|
|
79
|
+
store
|
|
80
|
+
.getByName(Store.ROOT_DO_NAME)
|
|
81
|
+
.registerStack({ stack: params.stack }),
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
.handle("deleteState", ({ params }) =>
|
|
86
|
+
// The DO method is `remove`, not `delete` — `delete` is
|
|
87
|
+
// reserved by Cloudflare's RPC stub proxy.
|
|
88
|
+
store
|
|
89
|
+
.getByName(params.stack)
|
|
90
|
+
.remove({
|
|
91
|
+
stage: params.stage,
|
|
92
|
+
fqn: decodeURIComponent(params.fqn),
|
|
93
|
+
})
|
|
94
|
+
.pipe(Effect.asVoid),
|
|
95
|
+
)
|
|
96
|
+
.handle("getReplacedResources", ({ params }) =>
|
|
97
|
+
store
|
|
98
|
+
.getByName(params.stack)
|
|
99
|
+
.getReplacedResources({ stage: params.stage }),
|
|
100
|
+
)
|
|
101
|
+
.handle("deleteStack", ({ params, query }) =>
|
|
102
|
+
store
|
|
103
|
+
.getByName(params.stack)
|
|
104
|
+
.deleteStack(
|
|
105
|
+
query.stage === undefined ? {} : { stage: query.stage },
|
|
106
|
+
)
|
|
107
|
+
.pipe(
|
|
108
|
+
Effect.flatMap(() =>
|
|
109
|
+
query.stage === undefined
|
|
110
|
+
? store
|
|
111
|
+
.getByName(Store.ROOT_DO_NAME)
|
|
112
|
+
.unregisterStack({ stack: params.stack })
|
|
113
|
+
: Effect.void,
|
|
114
|
+
),
|
|
115
|
+
Effect.asVoid,
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
fetch: HttpApiBuilder.layer(StateApi).pipe(
|
|
122
|
+
Layer.provide(stateApi),
|
|
123
|
+
Layer.provide(StateAuthLive),
|
|
124
|
+
Layer.provide(bearerTokenValidator),
|
|
125
|
+
// The state-store worker never serves files, so HttpPlatform's
|
|
126
|
+
// file-response surface is stubbed.
|
|
127
|
+
Layer.provide([Etag.layer, HttpPlatformStub, Path.layer]),
|
|
128
|
+
HttpRouter.toHttpEffect,
|
|
129
|
+
),
|
|
130
|
+
};
|
|
131
|
+
}).pipe(Effect.provide(Layer.mergeAll(SecretBindingLive))),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Stub `HttpPlatform` for the worker. The state-store API never
|
|
136
|
+
* issues file responses, so both surface methods die if invoked. Lets
|
|
137
|
+
* us avoid pulling in a `FileSystem` dependency that workers don't
|
|
138
|
+
* have.
|
|
139
|
+
*/
|
|
140
|
+
const HttpPlatformStub = Layer.succeed(HttpPlatform.HttpPlatform, {
|
|
141
|
+
fileResponse: () => Effect.die("HttpPlatform.fileResponse not supported"),
|
|
142
|
+
fileWebResponse: () =>
|
|
143
|
+
Effect.die("HttpPlatform.fileWebResponse not supported"),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Timing-safe string comparison using the Workers runtime's built-in
|
|
148
|
+
* `crypto.subtle.timingSafeEqual`.
|
|
149
|
+
*
|
|
150
|
+
* @see https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/
|
|
151
|
+
*/
|
|
152
|
+
const timingSafeEqual = (a: string, b: string): boolean => {
|
|
153
|
+
const encoder = new TextEncoder();
|
|
154
|
+
const aBytes = encoder.encode(a);
|
|
155
|
+
const bBytes = encoder.encode(b);
|
|
156
|
+
if (aBytes.byteLength !== bBytes.byteLength) return false;
|
|
157
|
+
// @ts-expect-error - TODO(sam)
|
|
158
|
+
return crypto.subtle.timingSafeEqual(aBytes, bBytes);
|
|
159
|
+
};
|