alchemy 2.0.0-beta.28 → 2.0.0-beta.29
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 +98 -6
- package/bin/exec.js.map +1 -1
- package/lib/AWS/IAM/SAMLProvider.d.ts +2 -1
- package/lib/AWS/IAM/SAMLProvider.d.ts.map +1 -1
- package/lib/AWS/IAM/SAMLProvider.js +30 -9
- package/lib/AWS/IAM/SAMLProvider.js.map +1 -1
- package/lib/Apply.d.ts.map +1 -1
- package/lib/Apply.js +55 -11
- package/lib/Apply.js.map +1 -1
- package/lib/Cli/commands/bootstrap.d.ts +1 -1
- package/lib/Cli/commands/bootstrap.d.ts.map +1 -1
- package/lib/Cli/commands/bootstrap.js +2 -1
- package/lib/Cli/commands/bootstrap.js.map +1 -1
- package/lib/Cloudflare/Access.d.ts +19 -0
- package/lib/Cloudflare/Access.d.ts.map +1 -0
- package/lib/Cloudflare/Access.js +64 -0
- package/lib/Cloudflare/Access.js.map +1 -0
- package/lib/Cloudflare/AiGateway/AiGateway.d.ts +244 -0
- package/lib/Cloudflare/AiGateway/AiGateway.d.ts.map +1 -0
- package/lib/Cloudflare/AiGateway/AiGateway.js +269 -0
- package/lib/Cloudflare/AiGateway/AiGateway.js.map +1 -0
- package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts +103 -0
- package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts.map +1 -0
- package/lib/Cloudflare/AiGateway/AiGatewayBinding.js +99 -0
- package/lib/Cloudflare/AiGateway/AiGatewayBinding.js.map +1 -0
- package/lib/Cloudflare/AiGateway/index.d.ts +3 -0
- package/lib/Cloudflare/AiGateway/index.d.ts.map +1 -0
- package/lib/Cloudflare/AiGateway/index.js +3 -0
- package/lib/Cloudflare/AiGateway/index.js.map +1 -0
- package/lib/Cloudflare/EdgeSession.d.ts +2 -1
- package/lib/Cloudflare/EdgeSession.d.ts.map +1 -1
- package/lib/Cloudflare/EdgeSession.js +8 -4
- package/lib/Cloudflare/EdgeSession.js.map +1 -1
- package/lib/Cloudflare/Providers.d.ts +2 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +5 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +10 -0
- package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Api.js +12 -1
- package/lib/Cloudflare/StateStore/Api.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +4 -3
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/State.js +103 -13
- package/lib/Cloudflare/StateStore/State.js.map +1 -1
- package/lib/Cloudflare/Workers/InferEnv.d.ts +1 -1
- package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +2 -1
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +40 -31
- package/lib/Cloudflare/Workers/Worker.js.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/Plan.d.ts +8 -0
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +12 -5
- package/lib/Plan.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +19 -1
- package/lib/State/HttpStateApi.d.ts.map +1 -1
- package/lib/State/HttpStateApi.js +18 -1
- package/lib/State/HttpStateApi.js.map +1 -1
- package/lib/Util/cached-function.d.ts +38 -0
- package/lib/Util/cached-function.d.ts.map +1 -0
- package/lib/Util/cached-function.js +53 -0
- package/lib/Util/cached-function.js.map +1 -0
- package/lib/Util/scc.d.ts +12 -0
- package/lib/Util/scc.d.ts.map +1 -0
- package/lib/Util/scc.js +81 -0
- package/lib/Util/scc.js.map +1 -0
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/AWS/IAM/SAMLProvider.ts +47 -18
- package/src/Apply.ts +72 -8
- package/src/Cli/commands/bootstrap.ts +2 -0
- package/src/Cloudflare/Access.ts +110 -0
- package/src/Cloudflare/AiGateway/AiGateway.ts +526 -0
- package/src/Cloudflare/AiGateway/AiGatewayBinding.ts +190 -0
- package/src/Cloudflare/AiGateway/index.ts +2 -0
- package/src/Cloudflare/EdgeSession.ts +15 -5
- package/src/Cloudflare/Providers.ts +7 -0
- package/src/Cloudflare/StateStore/Api.ts +18 -0
- package/src/Cloudflare/StateStore/State.ts +130 -12
- package/src/Cloudflare/Workers/InferEnv.ts +7 -5
- package/src/Cloudflare/Workers/Worker.ts +44 -33
- package/src/Cloudflare/index.ts +1 -0
- package/src/Plan.ts +21 -5
- package/src/State/HttpStateApi.ts +22 -1
- package/src/Util/cached-function.ts +75 -0
- package/src/Util/scc.ts +89 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemy",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.29",
|
|
4
4
|
"homepage": "https://alchemy.run",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
"@distilled.cloud/axiom": "^0.15.2",
|
|
262
262
|
"@distilled.cloud/cloudflare": "^0.15.2",
|
|
263
263
|
"@distilled.cloud/cloudflare-rolldown-plugin": "0.3.0",
|
|
264
|
-
"@distilled.cloud/cloudflare-runtime": "0.1.
|
|
264
|
+
"@distilled.cloud/cloudflare-runtime": "0.1.2",
|
|
265
265
|
"@distilled.cloud/cloudflare-vite-plugin": "0.2.0",
|
|
266
266
|
"@distilled.cloud/core": "^0.15.2",
|
|
267
267
|
"@effect/vitest": ">=4.0.0-beta.58 || >=4.0.0",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as iam from "@distilled.cloud/aws/iam";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import * as Redacted from "effect/Redacted";
|
|
4
|
+
import { AWSEnvironment } from "../Environment.ts";
|
|
4
5
|
import { isResolved } from "../../Diff.ts";
|
|
5
6
|
import * as Provider from "../../Provider.ts";
|
|
6
7
|
import { Resource } from "../../Resource.ts";
|
|
7
8
|
import type { Providers } from "../Providers.ts";
|
|
8
|
-
import { diffTags } from "../../Tags.ts";
|
|
9
|
+
import { createInternalTags, diffTags, hasTags } from "../../Tags.ts";
|
|
9
10
|
import { toTagRecord, unwrapRedactedString } from "./common.ts";
|
|
10
11
|
|
|
11
12
|
export interface SAMLProviderProps {
|
|
@@ -100,19 +101,44 @@ export const SAMLProviderProvider = () =>
|
|
|
100
101
|
tags: toTagRecord(tags.Tags),
|
|
101
102
|
};
|
|
102
103
|
}),
|
|
103
|
-
create: Effect.fn(function* ({ news, session }) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
104
|
+
create: Effect.fn(function* ({ id, news, session }) {
|
|
105
|
+
const internalTags = yield* createInternalTags(id);
|
|
106
|
+
const tags = {
|
|
107
|
+
...internalTags,
|
|
108
|
+
...news.tags,
|
|
109
|
+
};
|
|
110
|
+
const response = yield* iam
|
|
111
|
+
.createSAMLProvider({
|
|
112
|
+
Name: news.name,
|
|
113
|
+
SAMLMetadataDocument: news.samlMetadataDocument,
|
|
114
|
+
AssertionEncryptionMode: news.assertionEncryptionMode,
|
|
115
|
+
AddPrivateKey: news.addPrivateKey
|
|
116
|
+
? unwrapRedactedString(news.addPrivateKey)
|
|
117
|
+
: undefined,
|
|
118
|
+
Tags: Object.entries(tags).map(([Key, Value]) => ({
|
|
119
|
+
Key,
|
|
120
|
+
Value,
|
|
121
|
+
})),
|
|
122
|
+
})
|
|
123
|
+
.pipe(
|
|
124
|
+
Effect.catchTag("EntityAlreadyExistsException", () =>
|
|
125
|
+
Effect.gen(function* () {
|
|
126
|
+
const accountId = (yield* AWSEnvironment).accountId;
|
|
127
|
+
const existingArn = `arn:aws:iam::${accountId}:saml-provider/${news.name}`;
|
|
128
|
+
const existingTags = yield* iam.listSAMLProviderTags({
|
|
129
|
+
SAMLProviderArn: existingArn,
|
|
130
|
+
});
|
|
131
|
+
if (!hasTags(internalTags, existingTags.Tags)) {
|
|
132
|
+
return yield* Effect.fail(
|
|
133
|
+
new Error(
|
|
134
|
+
`SAML provider '${news.name}' already exists and is not managed by alchemy`,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return { SAMLProviderArn: existingArn };
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
116
142
|
const samlProviderArn = response.SAMLProviderArn ?? news.name;
|
|
117
143
|
yield* session.note(samlProviderArn);
|
|
118
144
|
return {
|
|
@@ -121,10 +147,10 @@ export const SAMLProviderProvider = () =>
|
|
|
121
147
|
samlProviderUUID: undefined,
|
|
122
148
|
samlMetadataDocument: news.samlMetadataDocument,
|
|
123
149
|
assertionEncryptionMode: news.assertionEncryptionMode,
|
|
124
|
-
tags
|
|
150
|
+
tags,
|
|
125
151
|
};
|
|
126
152
|
}),
|
|
127
|
-
update: Effect.fn(function* ({ news, olds, output, session }) {
|
|
153
|
+
update: Effect.fn(function* ({ id, news, olds, output, session }) {
|
|
128
154
|
yield* iam.updateSAMLProvider({
|
|
129
155
|
SAMLProviderArn: output.samlProviderArn,
|
|
130
156
|
SAMLMetadataDocument:
|
|
@@ -136,7 +162,10 @@ export const SAMLProviderProvider = () =>
|
|
|
136
162
|
? unwrapRedactedString(news.addPrivateKey)
|
|
137
163
|
: undefined,
|
|
138
164
|
});
|
|
139
|
-
const
|
|
165
|
+
const internalTags = yield* createInternalTags(id);
|
|
166
|
+
const oldTags = { ...internalTags, ...(olds.tags ?? {}) };
|
|
167
|
+
const newTags = { ...internalTags, ...(news.tags ?? {}) };
|
|
168
|
+
const { removed, upsert } = diffTags(oldTags, newTags);
|
|
140
169
|
if (upsert.length > 0) {
|
|
141
170
|
yield* iam.tagSAMLProvider({
|
|
142
171
|
SAMLProviderArn: output.samlProviderArn,
|
|
@@ -156,7 +185,7 @@ export const SAMLProviderProvider = () =>
|
|
|
156
185
|
samlProviderUUID: output.samlProviderUUID,
|
|
157
186
|
samlMetadataDocument: news.samlMetadataDocument,
|
|
158
187
|
assertionEncryptionMode: news.assertionEncryptionMode,
|
|
159
|
-
tags:
|
|
188
|
+
tags: newTags,
|
|
160
189
|
};
|
|
161
190
|
}),
|
|
162
191
|
delete: Effect.fn(function* ({ output }) {
|
package/src/Apply.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as Cause from "effect/Cause";
|
|
1
2
|
import * as Deferred from "effect/Deferred";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
3
4
|
import * as Option from "effect/Option";
|
|
@@ -246,6 +247,8 @@ const executePlan = Effect.fnUntraced(function* (
|
|
|
246
247
|
{ concurrency: "unbounded" },
|
|
247
248
|
);
|
|
248
249
|
|
|
250
|
+
const failures: LifecycleFailure[] = [];
|
|
251
|
+
|
|
249
252
|
yield* Effect.all(
|
|
250
253
|
Object.entries(plan.resources).map(([fqn, node]) =>
|
|
251
254
|
executeNode(
|
|
@@ -260,12 +263,30 @@ const executePlan = Effect.fnUntraced(function* (
|
|
|
260
263
|
stage,
|
|
261
264
|
getOutputs,
|
|
262
265
|
waitForDeps,
|
|
266
|
+
failures,
|
|
267
|
+
plan.cycleMembers.has(fqn),
|
|
263
268
|
),
|
|
264
269
|
),
|
|
265
270
|
{ concurrency: "unbounded" },
|
|
266
271
|
);
|
|
272
|
+
|
|
273
|
+
if (failures.length > 0) {
|
|
274
|
+
// Aggregate every collected lifecycle failure into a single parallel Cause
|
|
275
|
+
// so the apply ends with one combined error containing every distinct
|
|
276
|
+
// failure / defect that occurred across the concurrent fibers.
|
|
277
|
+
yield* Effect.failCause(
|
|
278
|
+
failures.map((f) => f.cause).reduce(Cause.combine),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
267
281
|
});
|
|
268
282
|
|
|
283
|
+
interface LifecycleFailure {
|
|
284
|
+
fqn: string;
|
|
285
|
+
logicalId: string;
|
|
286
|
+
type: string;
|
|
287
|
+
cause: Cause.Cause<unknown>;
|
|
288
|
+
}
|
|
289
|
+
|
|
269
290
|
const executeNode = (
|
|
270
291
|
fqn: string,
|
|
271
292
|
node: Apply,
|
|
@@ -292,6 +313,8 @@ const executeNode = (
|
|
|
292
313
|
stage: string,
|
|
293
314
|
getOutputs: () => Record<string, any>,
|
|
294
315
|
waitForDeps: (fqns: string[]) => Effect.Effect<void[], never, never>,
|
|
316
|
+
failures: LifecycleFailure[],
|
|
317
|
+
inCycle: boolean,
|
|
295
318
|
): Effect.Effect<void, never, never> =>
|
|
296
319
|
Effect.gen(function* () {
|
|
297
320
|
const logicalId = node.resource.LogicalId;
|
|
@@ -548,14 +571,26 @@ const executeNode = (
|
|
|
548
571
|
|
|
549
572
|
// ── update ──
|
|
550
573
|
if (node.action === "update") {
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
574
|
+
// Cycle members publish their previous live attr *before* waiting on
|
|
575
|
+
// upstreams so the SCC can converge — peers in the cycle would
|
|
576
|
+
// otherwise deadlock waiting on each other. Phase 3 (`converge`)
|
|
577
|
+
// re-runs each peer's update against fresh outputs once everyone
|
|
578
|
+
// has settled.
|
|
579
|
+
//
|
|
580
|
+
// Linear (DAG) update nodes skip this entirely and simply wait for
|
|
581
|
+
// fresh upstream outputs, mirroring the create flow. This is the
|
|
582
|
+
// important property: a downstream of a non-cycle update never
|
|
583
|
+
// observes the upstream's stale attr, which prevents wasted/
|
|
584
|
+
// destructive intermediate updates (e.g. a Worker deploying with
|
|
585
|
+
// stale Build assets).
|
|
586
|
+
if (inCycle) {
|
|
587
|
+
yield* storeAndSignal({
|
|
588
|
+
output: node.state.attr,
|
|
589
|
+
props: node.state.props,
|
|
590
|
+
bindings: node.state.bindings ?? [],
|
|
591
|
+
instanceId,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
559
594
|
|
|
560
595
|
yield* waitForDeps(allUpstreamFqns());
|
|
561
596
|
const outputs = getOutputs();
|
|
@@ -657,6 +692,10 @@ const executeNode = (
|
|
|
657
692
|
bindings: bindingOutputs,
|
|
658
693
|
instanceId,
|
|
659
694
|
};
|
|
695
|
+
// Signal here for the linear (non-cycle) path. For in-cycle updates
|
|
696
|
+
// the deferred has already been resolved by the early `storeAndSignal`
|
|
697
|
+
// above and `signalReady` is a no-op the second time.
|
|
698
|
+
yield* signalReady;
|
|
660
699
|
|
|
661
700
|
yield* markTerminal("updated");
|
|
662
701
|
return;
|
|
@@ -830,6 +869,31 @@ const executeNode = (
|
|
|
830
869
|
return yield* Effect.die(`Unknown action: ${node.action}`);
|
|
831
870
|
});
|
|
832
871
|
}).pipe(
|
|
872
|
+
Effect.catchCause((cause) =>
|
|
873
|
+
// Record the failure, propagate it to any downstream resources waiting on
|
|
874
|
+
// our Deferred (so their waitForDeps short-circuits instead of deadlocking),
|
|
875
|
+
// emit a "fail" status to the session, and resolve to void so Effect.all
|
|
876
|
+
// does not interrupt sibling fibers. The aggregated cause is raised at
|
|
877
|
+
// the end of executePlan.
|
|
878
|
+
Effect.gen(function* () {
|
|
879
|
+
failures.push({
|
|
880
|
+
fqn,
|
|
881
|
+
logicalId: node.resource.LogicalId,
|
|
882
|
+
type: node.resource.Type,
|
|
883
|
+
cause,
|
|
884
|
+
});
|
|
885
|
+
yield* Deferred.failCause(
|
|
886
|
+
ready[fqn],
|
|
887
|
+
cause as Cause.Cause<never>,
|
|
888
|
+
);
|
|
889
|
+
yield* session.emit({
|
|
890
|
+
kind: "status-change",
|
|
891
|
+
id: node.resource.LogicalId,
|
|
892
|
+
type: node.resource.Type,
|
|
893
|
+
status: "fail",
|
|
894
|
+
});
|
|
895
|
+
}),
|
|
896
|
+
),
|
|
833
897
|
Effect.withSpan("apply.resource", {
|
|
834
898
|
attributes: {
|
|
835
899
|
"alchemy.resource.fqn": fqn,
|
|
@@ -20,6 +20,7 @@ import * as AWSEnvironment from "../../AWS/Environment";
|
|
|
20
20
|
import * as AWSRegion from "../../AWS/Region";
|
|
21
21
|
import { AuthProviders } from "../../Auth/AuthProvider";
|
|
22
22
|
import { withProfileOverride } from "../../Auth/Profile";
|
|
23
|
+
import * as CloudflareAccess from "../../Cloudflare/Access.ts";
|
|
23
24
|
import { CloudflareAuth } from "../../Cloudflare/Auth/AuthProvider";
|
|
24
25
|
import * as CloudflareEnvironment from "../../Cloudflare/CloudflareEnvironment";
|
|
25
26
|
import * as CloudflareCredentials from "../../Cloudflare/Credentials";
|
|
@@ -190,6 +191,7 @@ const bootstrapCloudflareCommand = Command.make(
|
|
|
190
191
|
Layer.mergeAll(
|
|
191
192
|
CloudflareCredentials.fromAuthProvider(),
|
|
192
193
|
CloudflareEnvironment.fromProfile(),
|
|
194
|
+
CloudflareAccess.AccessLive,
|
|
193
195
|
),
|
|
194
196
|
authLayer,
|
|
195
197
|
);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as Config from "effect/Config";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as ChildProcess from "effect/unstable/process/ChildProcess";
|
|
8
|
+
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
|
|
9
|
+
import { cachedFunction } from "../Util/cached-function.ts";
|
|
10
|
+
|
|
11
|
+
export class AccessError extends Schema.TaggedErrorClass<AccessError>()(
|
|
12
|
+
"AccessError",
|
|
13
|
+
{
|
|
14
|
+
message: Schema.String,
|
|
15
|
+
cause: Schema.optional(Schema.Defect),
|
|
16
|
+
},
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
export class Access extends Context.Service<
|
|
20
|
+
Access,
|
|
21
|
+
{
|
|
22
|
+
readonly getAccessHeaders: (
|
|
23
|
+
domain: string,
|
|
24
|
+
) => Effect.Effect<Record<string, string>, AccessError>;
|
|
25
|
+
}
|
|
26
|
+
>()("alchemy/Cloudflare/Access") {}
|
|
27
|
+
|
|
28
|
+
export const AccessLive = Layer.effect(
|
|
29
|
+
Access,
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
32
|
+
const domainUsesAccess = yield* cachedFunction((domain: string) =>
|
|
33
|
+
Effect.promise((signal) =>
|
|
34
|
+
fetch(`https://${domain}`, { redirect: "manual", signal }),
|
|
35
|
+
).pipe(
|
|
36
|
+
Effect.map(
|
|
37
|
+
(response) =>
|
|
38
|
+
response.status === 302 &&
|
|
39
|
+
(response.headers
|
|
40
|
+
.get("location")
|
|
41
|
+
?.includes("cloudflareaccess.com") ??
|
|
42
|
+
false),
|
|
43
|
+
),
|
|
44
|
+
Effect.timeout(1000),
|
|
45
|
+
Effect.catch(() => Effect.succeed(false)),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
const login = (domain: string) =>
|
|
49
|
+
ChildProcess.make("cloudflared", ["access", "login", domain]).pipe(
|
|
50
|
+
spawner.spawn,
|
|
51
|
+
Effect.flatMap((process) => Stream.runCollect(process.stdout)),
|
|
52
|
+
Effect.mapError(
|
|
53
|
+
(error) =>
|
|
54
|
+
new AccessError({
|
|
55
|
+
message:
|
|
56
|
+
`The domain "${domain}" uses Cloudflare Access, but \`cloudflared\` is not installed. ` +
|
|
57
|
+
`Please install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation.`,
|
|
58
|
+
cause: error,
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
Effect.flatMap((stdout) => {
|
|
62
|
+
const matches = stdout
|
|
63
|
+
.toString()
|
|
64
|
+
.match(/fetched your token:\n\n(.*)/m);
|
|
65
|
+
return matches && matches.length >= 2
|
|
66
|
+
? Effect.succeed({ Cookie: `CF_Authorization=${matches[1]}` })
|
|
67
|
+
: Effect.fail(
|
|
68
|
+
new AccessError({
|
|
69
|
+
message: "Failed to authenticate with Cloudflare Access",
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}),
|
|
73
|
+
Effect.scoped,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const getEnv = (name: string) =>
|
|
77
|
+
Config.string(name)
|
|
78
|
+
.asEffect()
|
|
79
|
+
.pipe(Effect.catchTag("ConfigError", () => Effect.succeed(undefined)));
|
|
80
|
+
|
|
81
|
+
return Access.of({
|
|
82
|
+
getAccessHeaders: Effect.fn(function* (domain) {
|
|
83
|
+
if (!(yield* domainUsesAccess(domain))) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
const clientId = yield* getEnv("CLOUDFLARE_ACCESS_CLIENT_ID");
|
|
87
|
+
const clientSecret = yield* getEnv("CLOUDFLARE_ACCESS_CLIENT_SECRET");
|
|
88
|
+
if (clientId && clientSecret) {
|
|
89
|
+
return {
|
|
90
|
+
"CF-Access-Client-Id": clientId,
|
|
91
|
+
"CF-Access-Client-Secret": clientSecret,
|
|
92
|
+
} as Record<string, string>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (clientId !== undefined || clientSecret !== undefined) {
|
|
96
|
+
yield* Effect.logWarning(
|
|
97
|
+
"Both CLOUDFLARE_ACCESS_CLIENT_ID and CLOUDFLARE_ACCESS_CLIENT_SECRET must be set to use Access Service Token authentication. " +
|
|
98
|
+
`Only ${
|
|
99
|
+
clientId !== undefined
|
|
100
|
+
? "CLOUDFLARE_ACCESS_CLIENT_ID"
|
|
101
|
+
: "CLOUDFLARE_ACCESS_CLIENT_SECRET"
|
|
102
|
+
} was found.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return yield* login(domain);
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
}),
|
|
110
|
+
);
|