@thomasfosterau/effect-connect-alchemy-cloudflare 0.0.0 → 0.1.0
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/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/index.d.ts +98 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -4
- package/src/Bindings.ts +52 -0
- package/src/Resend.ts +74 -0
- package/src/index.ts +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Foster
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@thomasfosterau/effect-connect-alchemy-cloudflare`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deploy-time bindings for [`@thomasfosterau/effect-connect`](https://github.com/thomasfosterau/effect-connect/tree/trunk/packages/core#readme) on Cloudflare Workers deployed with [Alchemy](https://alchemy.run)'s Cloudflare provider, [`@alchemy.run/cloudflare`](https://www.npmjs.com/package/@alchemy.run/cloudflare). Each builder returns the record of Worker `env` bindings one provider reads, under that provider's own keys (`RESEND_API_KEY` for Resend, the one provider today). Secrets are `Redacted` values, which Alchemy deploys as secret bindings. Spread the record into your Worker's `env`, and the provider's `resolveTransport(env)` (or `layerConfig`) finds its credentials at runtime.
|
|
4
|
+
|
|
5
|
+
It binds credentials only. It provisions nothing on the vendor's side (domains, API keys). Its types are the Cloudflare provider's: every bindings record is a `WorkerBindings` (each value a `Cloudflare.WorkerBindingResource`), and `Deployed<B>` is `Cloudflare.InferEnv<B>`. At runtime the builders need only `effect`.
|
|
6
|
+
|
|
7
|
+
This package was created using Claude Code, and its API is in flux: expect churn and breaking changes.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add -D @thomasfosterau/effect-connect-alchemy-cloudflare @alchemy.run/cloudflare@2.0.0-beta.85 alchemy@2.0.0-beta.85 effect
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Its peer dependencies are all required: `@alchemy.run/cloudflare` and `alchemy`, exactly `2.0.0-beta.85` (the provider peers on that exact `alchemy`), and `effect` `^4.1.0`, the range both of them require. Node.js 22 or newer is required. The package runs on the deploy machine, inside your `alchemy.run.ts`, never in the Worker.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
On the deploy machine, build the bindings and pass them into the Worker's `env`:
|
|
20
|
+
|
|
21
|
+
```ts skip-type-checking
|
|
22
|
+
import * as Redacted from "effect/Redacted";
|
|
23
|
+
import { resendBindings } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
24
|
+
|
|
25
|
+
const env = {
|
|
26
|
+
...resendBindings({ apiKey: Redacted.make(process.env.RESEND_API_KEY!) }),
|
|
27
|
+
};
|
|
28
|
+
// …then `env` in your Worker resource's props.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or read the same names through `effect/Config` on the deploy machine, with the `…BindingsConfig` forms. Each one is a `Config` of the whole record, and fails when a required value is missing:
|
|
32
|
+
|
|
33
|
+
```ts skip-type-checking
|
|
34
|
+
import * as Effect from "effect/Effect";
|
|
35
|
+
import { resendBindingsConfig } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
36
|
+
|
|
37
|
+
const env = Effect.gen(function* () {
|
|
38
|
+
return { ...(yield* resendBindingsConfig) };
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
In the Worker, the provider reads them back:
|
|
43
|
+
|
|
44
|
+
```ts skip-type-checking
|
|
45
|
+
import * as Resend from "@thomasfosterau/effect-connect-resend";
|
|
46
|
+
|
|
47
|
+
const EmailLive = Resend.resolveTransport(env); // the live sender: RESEND_API_KEY is bound
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
| Export | Binds | Read at runtime by |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `resendBindings`, `resendBindingsConfig`, `ResendBindings`, `ResendBindingsInput` | `RESEND_API_KEY` (secret) | `effect-connect-resend`'s `resolveTransport(env)` (`ResendEnv`) |
|
|
55
|
+
| `WorkerBindings<B>` | — | The constraint every bindings record meets: each value a `Cloudflare.WorkerBindingResource`, so the record spreads into a `Cloudflare.Worker`'s `env`. |
|
|
56
|
+
| `Deployed<B>` | — | `Cloudflare.InferEnv<B>`: the `env` a Worker receives for a bindings record `B`, every value a plain string. Each provider's env type accepts it. |
|
|
57
|
+
|
|
58
|
+
`Deployed<B>` is defined as the provider's own `Cloudflare.InferEnv<B>`, so a Worker typed with it is checked against the env Alchemy actually deploys; the package's tests assert, for every builder against `@alchemy.run/cloudflare` 2.0.0-beta.85, that it is also what `InferEnv` gives a `Cloudflare.Worker` whose `env` is `B`, and that the provider's env type accepts it.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as Config from "effect/Config";
|
|
2
|
+
import * as Cloudflare from "@alchemy.run/cloudflare";
|
|
3
|
+
import * as Redacted from "effect/Redacted";
|
|
4
|
+
/**
|
|
5
|
+
* A record Alchemy's Cloudflare provider accepts as a Worker's `env`: every
|
|
6
|
+
* value is a `Cloudflare.WorkerBindingResource` (a `Redacted` deploys as a
|
|
7
|
+
* secret binding, a string as a plain one). Every builder's record satisfies
|
|
8
|
+
* it, so it spreads into a `Cloudflare.Worker`'s `env` prop as is.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import type { ResendBindings, WorkerBindings } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
13
|
+
*
|
|
14
|
+
* const check = <B extends WorkerBindings<B>>(bindings: B): B => bindings;
|
|
15
|
+
* const accept = (bindings: ResendBindings): ResendBindings => check(bindings);
|
|
16
|
+
* console.log(typeof accept); // "function"
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @since 0.1.0
|
|
20
|
+
*/
|
|
21
|
+
export type WorkerBindings<B> = { readonly [K in keyof B]: Cloudflare.WorkerBindingResource; };
|
|
22
|
+
/**
|
|
23
|
+
* The `env` a Worker receives for a bindings record: the provider's own
|
|
24
|
+
* `Cloudflare.InferEnv` of it. Alchemy deploys a `Redacted` value as a secret
|
|
25
|
+
* binding and a string as a plain one, and the Worker reads both back as
|
|
26
|
+
* plain strings; optional keys stay optional.
|
|
27
|
+
*
|
|
28
|
+
* A provider's env contract (`ResendEnv`, …) accepts this type, which is
|
|
29
|
+
* what the package's tests assert for every builder.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import type { Deployed, ResendBindings } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
34
|
+
*
|
|
35
|
+
* const env: Deployed<ResendBindings> = { RESEND_API_KEY: "re_123" };
|
|
36
|
+
* console.log(env.RESEND_API_KEY); // "re_123"
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @since 0.1.0
|
|
40
|
+
*/
|
|
41
|
+
export type Deployed<B extends WorkerBindings<B>> = Cloudflare.InferEnv<B>;
|
|
42
|
+
/**
|
|
43
|
+
* What {@link resendBindings} takes.
|
|
44
|
+
*
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
export interface ResendBindingsInput {
|
|
48
|
+
/** The Resend API key. */
|
|
49
|
+
readonly apiKey: Redacted.Redacted;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The Resend bindings for a Worker's `env`: `RESEND_API_KEY`, deployed as a
|
|
53
|
+
* secret because it is `Redacted`.
|
|
54
|
+
*
|
|
55
|
+
* @since 0.1.0
|
|
56
|
+
*/
|
|
57
|
+
export interface ResendBindings {
|
|
58
|
+
readonly RESEND_API_KEY: Redacted.Redacted;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The Resend bindings from literal values, to spread into a Worker's `env`.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import * as assert from "node:assert";
|
|
66
|
+
* import * as Redacted from "effect/Redacted";
|
|
67
|
+
* import { resendBindings } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
68
|
+
*
|
|
69
|
+
* const env = resendBindings({ apiKey: Redacted.make("re_123") });
|
|
70
|
+
* assert.strictEqual(Redacted.value(env.RESEND_API_KEY), "re_123");
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @since 0.1.0
|
|
74
|
+
*/
|
|
75
|
+
export declare const resendBindings: (input: ResendBindingsInput) => ResendBindings;
|
|
76
|
+
/**
|
|
77
|
+
* The Resend bindings read through `effect/Config` on the deploy machine:
|
|
78
|
+
* `RESEND_API_KEY` (required, redacted). Fails with a `ConfigError` when it
|
|
79
|
+
* is unset or empty, so a deploy never ships a Worker without its key.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import * as assert from "node:assert";
|
|
84
|
+
* import * as ConfigProvider from "effect/ConfigProvider";
|
|
85
|
+
* import * as Effect from "effect/Effect";
|
|
86
|
+
* import * as Redacted from "effect/Redacted";
|
|
87
|
+
* import { resendBindingsConfig } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
88
|
+
*
|
|
89
|
+
* const env = Effect.runSync(
|
|
90
|
+
* resendBindingsConfig.parse(ConfigProvider.fromEnv({ env: { RESEND_API_KEY: "re_123" } })),
|
|
91
|
+
* );
|
|
92
|
+
* assert.strictEqual(Redacted.value(env.RESEND_API_KEY), "re_123");
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @since 0.1.0
|
|
96
|
+
*/
|
|
97
|
+
export declare const resendBindingsConfig: Config.Config<ResendBindings>;
|
|
98
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as Config from "effect/Config";
|
|
2
|
+
// src/Resend.ts
|
|
3
|
+
/**
|
|
4
|
+
* The Worker bindings `@thomasfosterau/effect-connect-resend` reads
|
|
5
|
+
* (`ResendEnv`): the API key, as a secret.
|
|
6
|
+
*
|
|
7
|
+
* @module Resend
|
|
8
|
+
* @since 0.1.0
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The Resend bindings from literal values, to spread into a Worker's `env`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import * as assert from "node:assert";
|
|
16
|
+
* import * as Redacted from "effect/Redacted";
|
|
17
|
+
* import { resendBindings } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
18
|
+
*
|
|
19
|
+
* const env = resendBindings({ apiKey: Redacted.make("re_123") });
|
|
20
|
+
* assert.strictEqual(Redacted.value(env.RESEND_API_KEY), "re_123");
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
*/
|
|
25
|
+
const resendBindings = (input) => ({ RESEND_API_KEY: input.apiKey });
|
|
26
|
+
/**
|
|
27
|
+
* The Resend bindings read through `effect/Config` on the deploy machine:
|
|
28
|
+
* `RESEND_API_KEY` (required, redacted). Fails with a `ConfigError` when it
|
|
29
|
+
* is unset or empty, so a deploy never ships a Worker without its key.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import * as assert from "node:assert";
|
|
34
|
+
* import * as ConfigProvider from "effect/ConfigProvider";
|
|
35
|
+
* import * as Effect from "effect/Effect";
|
|
36
|
+
* import * as Redacted from "effect/Redacted";
|
|
37
|
+
* import { resendBindingsConfig } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
38
|
+
*
|
|
39
|
+
* const env = Effect.runSync(
|
|
40
|
+
* resendBindingsConfig.parse(ConfigProvider.fromEnv({ env: { RESEND_API_KEY: "re_123" } })),
|
|
41
|
+
* );
|
|
42
|
+
* assert.strictEqual(Redacted.value(env.RESEND_API_KEY), "re_123");
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
const resendBindingsConfig = Config.all({ RESEND_API_KEY: Config.Redacted("RESEND_API_KEY") });
|
|
48
|
+
export { resendBindings, resendBindingsConfig };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/Resend.ts"],"sourcesContent":["/**\n * The Worker bindings `@thomasfosterau/effect-connect-resend` reads\n * (`ResendEnv`): the API key, as a secret.\n *\n * @module Resend\n * @since 0.1.0\n */\n\nimport * as Config from \"effect/Config\";\nimport type * as Redacted from \"effect/Redacted\";\n\n/**\n * What {@link resendBindings} takes.\n *\n * @since 0.1.0\n */\nexport interface ResendBindingsInput {\n /** The Resend API key. */\n readonly apiKey: Redacted.Redacted;\n}\n\n/**\n * The Resend bindings for a Worker's `env`: `RESEND_API_KEY`, deployed as a\n * secret because it is `Redacted`.\n *\n * @since 0.1.0\n */\nexport interface ResendBindings {\n readonly RESEND_API_KEY: Redacted.Redacted;\n}\n\n/**\n * The Resend bindings from literal values, to spread into a Worker's `env`.\n *\n * @example\n * ```ts\n * import * as assert from \"node:assert\";\n * import * as Redacted from \"effect/Redacted\";\n * import { resendBindings } from \"@thomasfosterau/effect-connect-alchemy-cloudflare\";\n *\n * const env = resendBindings({ apiKey: Redacted.make(\"re_123\") });\n * assert.strictEqual(Redacted.value(env.RESEND_API_KEY), \"re_123\");\n * ```\n *\n * @since 0.1.0\n */\nexport const resendBindings = (input: ResendBindingsInput): ResendBindings => ({\n RESEND_API_KEY: input.apiKey,\n});\n\n/**\n * The Resend bindings read through `effect/Config` on the deploy machine:\n * `RESEND_API_KEY` (required, redacted). Fails with a `ConfigError` when it\n * is unset or empty, so a deploy never ships a Worker without its key.\n *\n * @example\n * ```ts\n * import * as assert from \"node:assert\";\n * import * as ConfigProvider from \"effect/ConfigProvider\";\n * import * as Effect from \"effect/Effect\";\n * import * as Redacted from \"effect/Redacted\";\n * import { resendBindingsConfig } from \"@thomasfosterau/effect-connect-alchemy-cloudflare\";\n *\n * const env = Effect.runSync(\n * resendBindingsConfig.parse(ConfigProvider.fromEnv({ env: { RESEND_API_KEY: \"re_123\" } })),\n * );\n * assert.strictEqual(Redacted.value(env.RESEND_API_KEY), \"re_123\");\n * ```\n *\n * @since 0.1.0\n */\nexport const resendBindingsConfig: Config.Config<ResendBindings> = Config.all({\n RESEND_API_KEY: Config.Redacted(\"RESEND_API_KEY\"),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAa,kBAAkB,WAAgD,EAC7E,gBAAgB,MAAM,OACxB;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,uBAAsD,OAAO,IAAI,EAC5E,gBAAgB,OAAO,SAAS,gBAAgB,EAClD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomasfosterau/effect-connect-alchemy-cloudflare",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Alchemy Cloudflare deploy-time bindings for @thomasfosterau/effect-connect: per-provider builders whose keys match each provider's Worker env contract, with secrets as Redacted values, so a Cloudflare Worker deployed with Alchemy's Cloudflare provider gets exactly the bindings resolveTransport(env) reads, typed by the provider's own InferEnv.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"alchemy",
|
|
7
|
+
"cloudflare",
|
|
8
|
+
"effect",
|
|
9
|
+
"email",
|
|
10
|
+
"sms",
|
|
11
|
+
"typescript",
|
|
12
|
+
"workers"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/thomasfosterau/effect-connect#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/thomasfosterau/effect-connect/issues"
|
|
17
|
+
},
|
|
5
18
|
"license": "MIT",
|
|
19
|
+
"author": "Thomas Foster <me@thomasfoster.au>",
|
|
6
20
|
"repository": {
|
|
7
21
|
"type": "git",
|
|
8
|
-
"url": "
|
|
22
|
+
"url": "https://github.com/thomasfosterau/effect-connect.git",
|
|
9
23
|
"directory": "packages/alchemy-cloudflare"
|
|
10
24
|
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"!src/**/*.test.ts",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"default": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
11
40
|
"publishConfig": {
|
|
12
41
|
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@alchemy.run/cloudflare": "2.0.0-beta.85",
|
|
45
|
+
"@effect/language-service": "^0.87.5",
|
|
46
|
+
"@effect/vitest": "4.1.2",
|
|
47
|
+
"@types/node": "^26.6.6",
|
|
48
|
+
"alchemy": "2.0.0-beta.85",
|
|
49
|
+
"effect": "4.1.2",
|
|
50
|
+
"@thomasfosterau/effect-connect-resend": "^0.1.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@alchemy.run/cloudflare": "2.0.0-beta.85",
|
|
54
|
+
"alchemy": "2.0.0-beta.85",
|
|
55
|
+
"effect": "^4.1.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=22"
|
|
13
59
|
}
|
|
14
|
-
}
|
|
60
|
+
}
|
package/src/Bindings.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types that tie a bindings record to Alchemy's Cloudflare provider
|
|
3
|
+
* (`@alchemy.run/cloudflare`): what a record may hold, and what a Worker
|
|
4
|
+
* receives once Alchemy has deployed it.
|
|
5
|
+
*
|
|
6
|
+
* @module Bindings
|
|
7
|
+
* @since 0.1.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type * as Cloudflare from "@alchemy.run/cloudflare";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A record Alchemy's Cloudflare provider accepts as a Worker's `env`: every
|
|
14
|
+
* value is a `Cloudflare.WorkerBindingResource` (a `Redacted` deploys as a
|
|
15
|
+
* secret binding, a string as a plain one). Every builder's record satisfies
|
|
16
|
+
* it, so it spreads into a `Cloudflare.Worker`'s `env` prop as is.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import type { ResendBindings, WorkerBindings } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
21
|
+
*
|
|
22
|
+
* const check = <B extends WorkerBindings<B>>(bindings: B): B => bindings;
|
|
23
|
+
* const accept = (bindings: ResendBindings): ResendBindings => check(bindings);
|
|
24
|
+
* console.log(typeof accept); // "function"
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @since 0.1.0
|
|
28
|
+
*/
|
|
29
|
+
export type WorkerBindings<B> = {
|
|
30
|
+
readonly [K in keyof B]: Cloudflare.WorkerBindingResource;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The `env` a Worker receives for a bindings record: the provider's own
|
|
35
|
+
* `Cloudflare.InferEnv` of it. Alchemy deploys a `Redacted` value as a secret
|
|
36
|
+
* binding and a string as a plain one, and the Worker reads both back as
|
|
37
|
+
* plain strings; optional keys stay optional.
|
|
38
|
+
*
|
|
39
|
+
* A provider's env contract (`ResendEnv`, …) accepts this type, which is
|
|
40
|
+
* what the package's tests assert for every builder.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import type { Deployed, ResendBindings } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
45
|
+
*
|
|
46
|
+
* const env: Deployed<ResendBindings> = { RESEND_API_KEY: "re_123" };
|
|
47
|
+
* console.log(env.RESEND_API_KEY); // "re_123"
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @since 0.1.0
|
|
51
|
+
*/
|
|
52
|
+
export type Deployed<B extends WorkerBindings<B>> = Cloudflare.InferEnv<B>;
|
package/src/Resend.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Worker bindings `@thomasfosterau/effect-connect-resend` reads
|
|
3
|
+
* (`ResendEnv`): the API key, as a secret.
|
|
4
|
+
*
|
|
5
|
+
* @module Resend
|
|
6
|
+
* @since 0.1.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as Config from "effect/Config";
|
|
10
|
+
import type * as Redacted from "effect/Redacted";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What {@link resendBindings} takes.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
export interface ResendBindingsInput {
|
|
18
|
+
/** The Resend API key. */
|
|
19
|
+
readonly apiKey: Redacted.Redacted;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The Resend bindings for a Worker's `env`: `RESEND_API_KEY`, deployed as a
|
|
24
|
+
* secret because it is `Redacted`.
|
|
25
|
+
*
|
|
26
|
+
* @since 0.1.0
|
|
27
|
+
*/
|
|
28
|
+
export interface ResendBindings {
|
|
29
|
+
readonly RESEND_API_KEY: Redacted.Redacted;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The Resend bindings from literal values, to spread into a Worker's `env`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import * as assert from "node:assert";
|
|
38
|
+
* import * as Redacted from "effect/Redacted";
|
|
39
|
+
* import { resendBindings } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
40
|
+
*
|
|
41
|
+
* const env = resendBindings({ apiKey: Redacted.make("re_123") });
|
|
42
|
+
* assert.strictEqual(Redacted.value(env.RESEND_API_KEY), "re_123");
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
export const resendBindings = (input: ResendBindingsInput): ResendBindings => ({
|
|
48
|
+
RESEND_API_KEY: input.apiKey,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The Resend bindings read through `effect/Config` on the deploy machine:
|
|
53
|
+
* `RESEND_API_KEY` (required, redacted). Fails with a `ConfigError` when it
|
|
54
|
+
* is unset or empty, so a deploy never ships a Worker without its key.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import * as assert from "node:assert";
|
|
59
|
+
* import * as ConfigProvider from "effect/ConfigProvider";
|
|
60
|
+
* import * as Effect from "effect/Effect";
|
|
61
|
+
* import * as Redacted from "effect/Redacted";
|
|
62
|
+
* import { resendBindingsConfig } from "@thomasfosterau/effect-connect-alchemy-cloudflare";
|
|
63
|
+
*
|
|
64
|
+
* const env = Effect.runSync(
|
|
65
|
+
* resendBindingsConfig.parse(ConfigProvider.fromEnv({ env: { RESEND_API_KEY: "re_123" } })),
|
|
66
|
+
* );
|
|
67
|
+
* assert.strictEqual(Redacted.value(env.RESEND_API_KEY), "re_123");
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @since 0.1.0
|
|
71
|
+
*/
|
|
72
|
+
export const resendBindingsConfig: Config.Config<ResendBindings> = Config.all({
|
|
73
|
+
RESEND_API_KEY: Config.Redacted("RESEND_API_KEY"),
|
|
74
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/effect-connect-alchemy-cloudflare` — deploy-time bindings
|
|
3
|
+
* for `@thomasfosterau/effect-connect` on Cloudflare Workers deployed with
|
|
4
|
+
* [Alchemy](https://alchemy.run)'s Cloudflare provider
|
|
5
|
+
* (`@alchemy.run/cloudflare`). One builder per provider (Resend today)
|
|
6
|
+
* produces the record of `env` bindings that provider's
|
|
7
|
+
* `resolveTransport(env)` (or `layerConfig`) reads, under the same keys, with
|
|
8
|
+
* secrets as `Redacted` values (Alchemy deploys those as secret bindings) and
|
|
9
|
+
* unset optional values left out. Each also has a `…BindingsConfig` form
|
|
10
|
+
* that reads the values on the deploy machine through `effect/Config`.
|
|
11
|
+
*
|
|
12
|
+
* Its types are the Cloudflare provider's: every record is a
|
|
13
|
+
* `WorkerBindings` (each value a `Cloudflare.WorkerBindingResource`), and
|
|
14
|
+
* `Deployed<B>` is `Cloudflare.InferEnv<B>`, the `env` a Worker bound with
|
|
15
|
+
* it receives. `@alchemy.run/cloudflare` is a required peer for those types
|
|
16
|
+
* alone: nothing imports it at runtime.
|
|
17
|
+
*
|
|
18
|
+
* It binds credentials only: it provisions nothing on the vendor's side
|
|
19
|
+
* (domains, API keys).
|
|
20
|
+
*
|
|
21
|
+
* @module index
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The Cloudflare provider's view of a bindings record (`WorkerBindings`,
|
|
27
|
+
* `Deployed`).
|
|
28
|
+
*
|
|
29
|
+
* @since 0.1.0
|
|
30
|
+
*/
|
|
31
|
+
export * from "./Bindings.ts";
|
|
32
|
+
/**
|
|
33
|
+
* The Resend bindings (`RESEND_API_KEY`).
|
|
34
|
+
*
|
|
35
|
+
* @since 0.1.0
|
|
36
|
+
*/
|
|
37
|
+
export * from "./Resend.ts";
|