@alchemy.run/fly 2.0.0-beta.85 → 2.0.0-beta.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App.d.ts +26 -4
- package/lib/App.d.ts.map +1 -1
- package/lib/App.js +25 -2
- package/lib/App.js.map +1 -1
- package/lib/AuthProvider.d.ts +1 -1
- package/lib/AuthProvider.d.ts.map +1 -1
- package/lib/AuthProvider.js.map +1 -1
- package/lib/BindService.d.ts +7 -7
- package/lib/BindService.d.ts.map +1 -1
- package/lib/BindService.js +20 -27
- package/lib/BindService.js.map +1 -1
- package/lib/Bootstrap.d.ts.map +1 -1
- package/lib/Bootstrap.js +2 -1
- package/lib/Bootstrap.js.map +1 -1
- package/lib/Bucket.d.ts +36 -2
- package/lib/Bucket.d.ts.map +1 -1
- package/lib/Bucket.js +181 -28
- package/lib/Bucket.js.map +1 -1
- package/lib/BucketBinding.d.ts.map +1 -1
- package/lib/BucketBinding.js.map +1 -1
- package/lib/Catalog.d.ts.map +1 -1
- package/lib/Catalog.js.map +1 -1
- package/lib/Certificate.d.ts +25 -1
- package/lib/Certificate.d.ts.map +1 -1
- package/lib/Certificate.js +18 -0
- package/lib/Certificate.js.map +1 -1
- package/lib/Checkpoint.d.ts.map +1 -1
- package/lib/Checkpoint.js.map +1 -1
- package/lib/CheckpointHttp.d.ts.map +1 -1
- package/lib/CheckpointHttp.js.map +1 -1
- package/lib/ConnectPostgres.d.ts.map +1 -1
- package/lib/ConnectPostgres.js.map +1 -1
- package/lib/ConnectPostgresHttp.d.ts.map +1 -1
- package/lib/ConnectPostgresHttp.js.map +1 -1
- package/lib/Contributions.d.ts +1 -1
- package/lib/Contributions.d.ts.map +1 -1
- package/lib/Contributions.js.map +1 -1
- package/lib/Credentials.d.ts +4 -3
- package/lib/Credentials.d.ts.map +1 -1
- package/lib/Credentials.js +9 -5
- package/lib/Credentials.js.map +1 -1
- package/lib/Decrypt.d.ts.map +1 -1
- package/lib/Decrypt.js.map +1 -1
- package/lib/DecryptHttp.d.ts.map +1 -1
- package/lib/DecryptHttp.js.map +1 -1
- package/lib/DeleteObject.d.ts.map +1 -1
- package/lib/DeleteObject.js.map +1 -1
- package/lib/DeleteObjectHttp.d.ts.map +1 -1
- package/lib/DeleteObjectHttp.js.map +1 -1
- package/lib/Deployment.d.ts.map +1 -1
- package/lib/Deployment.js.map +1 -1
- package/lib/DeploymentImages.d.ts.map +1 -1
- package/lib/DeploymentImages.js.map +1 -1
- package/lib/DeploymentState.d.ts.map +1 -1
- package/lib/DeploymentState.js.map +1 -1
- package/lib/Encrypt.d.ts.map +1 -1
- package/lib/Encrypt.js.map +1 -1
- package/lib/EncryptHttp.d.ts.map +1 -1
- package/lib/EncryptHttp.js.map +1 -1
- package/lib/Environment.d.ts.map +1 -1
- package/lib/Environment.js.map +1 -1
- package/lib/Errors.d.ts.map +1 -1
- package/lib/Errors.js.map +1 -1
- package/lib/Exec.d.ts.map +1 -1
- package/lib/Exec.js.map +1 -1
- package/lib/ExecHttp.d.ts.map +1 -1
- package/lib/ExecHttp.js.map +1 -1
- package/lib/GetObject.d.ts.map +1 -1
- package/lib/GetObject.js.map +1 -1
- package/lib/GetObjectHttp.d.ts.map +1 -1
- package/lib/GetObjectHttp.js.map +1 -1
- package/lib/GetSecret.d.ts.map +1 -1
- package/lib/GetSecret.js.map +1 -1
- package/lib/GetSecretHttp.d.ts.map +1 -1
- package/lib/GetSecretHttp.js.map +1 -1
- package/lib/HeadObject.d.ts.map +1 -1
- package/lib/HeadObject.js.map +1 -1
- package/lib/HeadObjectHttp.d.ts.map +1 -1
- package/lib/HeadObjectHttp.js.map +1 -1
- package/lib/IpAssignment.d.ts +24 -1
- package/lib/IpAssignment.d.ts.map +1 -1
- package/lib/IpAssignment.js +17 -0
- package/lib/IpAssignment.js.map +1 -1
- package/lib/ListObjectsV2.d.ts.map +1 -1
- package/lib/ListObjectsV2.js.map +1 -1
- package/lib/ListObjectsV2Http.d.ts.map +1 -1
- package/lib/ListObjectsV2Http.js.map +1 -1
- package/lib/ListSecrets.d.ts.map +1 -1
- package/lib/ListSecrets.js.map +1 -1
- package/lib/ListSecretsHttp.d.ts.map +1 -1
- package/lib/ListSecretsHttp.js.map +1 -1
- package/lib/Machine.d.ts +26 -1
- package/lib/Machine.d.ts.map +1 -1
- package/lib/Machine.js +16 -1
- package/lib/Machine.js.map +1 -1
- package/lib/MachineContainers.d.ts.map +1 -1
- package/lib/MachineContainers.js.map +1 -1
- package/lib/Metadata.d.ts.map +1 -1
- package/lib/Metadata.js.map +1 -1
- package/lib/MountVolume.d.ts +7 -2
- package/lib/MountVolume.d.ts.map +1 -1
- package/lib/MountVolume.js.map +1 -1
- package/lib/Postgres.d.ts +33 -1
- package/lib/Postgres.d.ts.map +1 -1
- package/lib/Postgres.js +20 -0
- package/lib/Postgres.js.map +1 -1
- package/lib/PostgresMigrations.d.ts.map +1 -1
- package/lib/PostgresMigrations.js.map +1 -1
- package/lib/Providers.d.ts +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js.map +1 -1
- package/lib/PutObject.d.ts.map +1 -1
- package/lib/PutObject.js.map +1 -1
- package/lib/PutObjectHttp.d.ts.map +1 -1
- package/lib/PutObjectHttp.js.map +1 -1
- package/lib/ReadRedis.d.ts.map +1 -1
- package/lib/ReadRedis.js.map +1 -1
- package/lib/ReadRedisHttp.d.ts.map +1 -1
- package/lib/ReadRedisHttp.js.map +1 -1
- package/lib/ReadWriteRedis.d.ts.map +1 -1
- package/lib/ReadWriteRedis.js.map +1 -1
- package/lib/ReadWriteRedisHttp.d.ts.map +1 -1
- package/lib/ReadWriteRedisHttp.js.map +1 -1
- package/lib/Redis.d.ts +21 -1
- package/lib/Redis.d.ts.map +1 -1
- package/lib/Redis.js +28 -0
- package/lib/Redis.js.map +1 -1
- package/lib/RedisBinding.d.ts.map +1 -1
- package/lib/RedisBinding.js.map +1 -1
- package/lib/RedisHttp.d.ts.map +1 -1
- package/lib/RedisHttp.js.map +1 -1
- package/lib/Region.d.ts.map +1 -1
- package/lib/Region.js.map +1 -1
- package/lib/Secret.d.ts +22 -1
- package/lib/Secret.d.ts.map +1 -1
- package/lib/Secret.js +15 -0
- package/lib/Secret.js.map +1 -1
- package/lib/SecretHttp.d.ts.map +1 -1
- package/lib/SecretHttp.js.map +1 -1
- package/lib/SecretKey.d.ts +22 -1
- package/lib/SecretKey.d.ts.map +1 -1
- package/lib/SecretKey.js +15 -0
- package/lib/SecretKey.js.map +1 -1
- package/lib/SecretKeyHttp.d.ts.map +1 -1
- package/lib/SecretKeyHttp.js.map +1 -1
- package/lib/Service.d.ts +96 -21
- package/lib/Service.d.ts.map +1 -1
- package/lib/Service.js +251 -37
- package/lib/Service.js.map +1 -1
- package/lib/Sign.d.ts.map +1 -1
- package/lib/Sign.js.map +1 -1
- package/lib/SignHttp.d.ts.map +1 -1
- package/lib/SignHttp.js.map +1 -1
- package/lib/Sprite.d.ts +22 -3
- package/lib/Sprite.d.ts.map +1 -1
- package/lib/Sprite.js +15 -0
- package/lib/Sprite.js.map +1 -1
- package/lib/SpriteHttp.d.ts.map +1 -1
- package/lib/SpriteHttp.js.map +1 -1
- package/lib/Verify.d.ts.map +1 -1
- package/lib/Verify.js.map +1 -1
- package/lib/VerifyHttp.d.ts.map +1 -1
- package/lib/VerifyHttp.js.map +1 -1
- package/lib/Volume.d.ts +3 -3
- package/lib/Volume.d.ts.map +1 -1
- package/lib/Volume.js +17 -1
- package/lib/Volume.js.map +1 -1
- package/lib/VolumeSnapshot.d.ts +32 -1
- package/lib/VolumeSnapshot.d.ts.map +1 -1
- package/lib/VolumeSnapshot.js +23 -0
- package/lib/VolumeSnapshot.js.map +1 -1
- package/lib/Website/AssetDeployment.d.ts.map +1 -1
- package/lib/Website/AssetDeployment.js.map +1 -1
- package/lib/Website/Astro.d.ts.map +1 -1
- package/lib/Website/Astro.js.map +1 -1
- package/lib/Website/Foldkit.d.ts.map +1 -1
- package/lib/Website/Foldkit.js.map +1 -1
- package/lib/Website/FrameworkSite.d.ts.map +1 -1
- package/lib/Website/FrameworkSite.js.map +1 -1
- package/lib/Website/Nextjs.d.ts.map +1 -1
- package/lib/Website/Nextjs.js.map +1 -1
- package/lib/Website/Nuxt.d.ts.map +1 -1
- package/lib/Website/Nuxt.js.map +1 -1
- package/lib/Website/Octane.d.ts.map +1 -1
- package/lib/Website/Octane.js.map +1 -1
- package/lib/Website/ReactRouter.d.ts.map +1 -1
- package/lib/Website/ReactRouter.js.map +1 -1
- package/lib/Website/SolidStart.d.ts.map +1 -1
- package/lib/Website/SolidStart.js.map +1 -1
- package/lib/Website/StaticSite.d.ts.map +1 -1
- package/lib/Website/StaticSite.js.map +1 -1
- package/lib/Website/SvelteKit.d.ts.map +1 -1
- package/lib/Website/SvelteKit.js.map +1 -1
- package/lib/Website/TanStackStart.d.ts.map +1 -1
- package/lib/Website/TanStackStart.js.map +1 -1
- package/lib/Website/Vinext.d.ts.map +1 -1
- package/lib/Website/Vinext.js.map +1 -1
- package/lib/Website/Vite.d.ts.map +1 -1
- package/lib/Website/Vite.js.map +1 -1
- package/lib/Website/Vocs.d.ts.map +1 -1
- package/lib/Website/Vocs.js.map +1 -1
- package/lib/Website/Waku.d.ts.map +1 -1
- package/lib/Website/Waku.js.map +1 -1
- package/lib/Website/index.d.ts.map +1 -1
- package/lib/Website/index.js.map +1 -1
- package/lib/WriteCertificates.d.ts.map +1 -1
- package/lib/WriteCertificates.js.map +1 -1
- package/lib/WriteCertificatesHttp.d.ts.map +1 -1
- package/lib/WriteCertificatesHttp.js.map +1 -1
- package/lib/WriteRedis.d.ts.map +1 -1
- package/lib/WriteRedis.js.map +1 -1
- package/lib/WriteRedisHttp.d.ts.map +1 -1
- package/lib/WriteRedisHttp.js.map +1 -1
- package/lib/WriteSecret.d.ts.map +1 -1
- package/lib/WriteSecret.js.map +1 -1
- package/lib/WriteSecretHttp.d.ts.map +1 -1
- package/lib/WriteSecretHttp.js.map +1 -1
- package/lib/bluegreen.d.ts +5 -4
- package/lib/bluegreen.d.ts.map +1 -1
- package/lib/bluegreen.js +1 -1
- package/lib/bluegreen.js.map +1 -1
- package/lib/hosted.d.ts +16 -0
- package/lib/hosted.d.ts.map +1 -1
- package/lib/hosted.js +101 -11
- package/lib/hosted.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/internal/Oidc.d.ts +15 -0
- package/lib/internal/Oidc.d.ts.map +1 -0
- package/lib/internal/Oidc.js +73 -0
- package/lib/internal/Oidc.js.map +1 -0
- package/lib/internal/RemoteBuilder.d.ts.map +1 -1
- package/lib/internal/RemoteBuilder.js.map +1 -1
- package/lib/leases.d.ts +6 -2
- package/lib/leases.d.ts.map +1 -1
- package/lib/leases.js +25 -5
- package/lib/leases.js.map +1 -1
- package/lib/ports.d.ts.map +1 -1
- package/lib/ports.js.map +1 -1
- package/lib/replicas.d.ts +38 -26
- package/lib/replicas.d.ts.map +1 -1
- package/lib/replicas.js +57 -2
- package/lib/replicas.js.map +1 -1
- package/lib/rpc.d.ts +14 -12
- package/lib/rpc.d.ts.map +1 -1
- package/lib/rpc.js +38 -40
- package/lib/rpc.js.map +1 -1
- package/package.json +15 -15
- package/src/App.ts +36 -3
- package/src/BindService.ts +27 -34
- package/src/Bootstrap.ts +2 -0
- package/src/Bucket.ts +264 -33
- package/src/Certificate.ts +32 -1
- package/src/Credentials.ts +9 -5
- package/src/IpAssignment.ts +31 -1
- package/src/Machine.ts +34 -1
- package/src/MountVolume.ts +5 -2
- package/src/Postgres.ts +43 -1
- package/src/Redis.ts +43 -1
- package/src/Secret.ts +29 -1
- package/src/SecretKey.ts +29 -1
- package/src/Service.ts +389 -70
- package/src/Sprite.ts +34 -8
- package/src/Volume.ts +25 -1
- package/src/VolumeSnapshot.ts +39 -1
- package/src/bluegreen.ts +4 -1
- package/src/hosted.ts +118 -13
- package/src/internal/Oidc.ts +99 -0
- package/src/leases.ts +35 -7
- package/src/replicas.ts +63 -2
- package/src/rpc.ts +37 -40
package/src/replicas.ts
CHANGED
|
@@ -653,6 +653,8 @@ export const leaseSnapshot = Effect.fn(function* (
|
|
|
653
653
|
(machine) =>
|
|
654
654
|
Effect.gen(function* () {
|
|
655
655
|
if (!machine.id || machine.host_status === "unreachable") return;
|
|
656
|
+
// Already deleted by this operation (a scale-down retired it).
|
|
657
|
+
if (leases.wasRemoved(machine.id)) return;
|
|
656
658
|
const id = machine.id;
|
|
657
659
|
yield* leases.acquire([id]).pipe(
|
|
658
660
|
Effect.tap(() => Effect.sync(() => leased.add(id))),
|
|
@@ -670,7 +672,7 @@ export const leaseSnapshot = Effect.fn(function* (
|
|
|
670
672
|
).pipe(Effect.timeout("30 seconds"));
|
|
671
673
|
const observed: FlyMachine[] = [];
|
|
672
674
|
for (const machine of ordered) {
|
|
673
|
-
if (!machine.id) continue;
|
|
675
|
+
if (!machine.id || leases.wasRemoved(machine.id)) continue;
|
|
674
676
|
if (machine.host_status === "unreachable") {
|
|
675
677
|
observed.push(machine);
|
|
676
678
|
continue;
|
|
@@ -1187,7 +1189,17 @@ const reconcileLeasedReplicas = Effect.fn(function* (
|
|
|
1187
1189
|
skip_launch: input.skipLaunch === true ? true : undefined,
|
|
1188
1190
|
min_secrets_version: input.minSecretsVersion,
|
|
1189
1191
|
})
|
|
1190
|
-
.pipe(
|
|
1192
|
+
.pipe(
|
|
1193
|
+
// A storage host out of capacity may still have made the Machine;
|
|
1194
|
+
// names are unique per App, so a retry that made it before hits
|
|
1195
|
+
// Conflict and the listing below finds it.
|
|
1196
|
+
Effect.retry({
|
|
1197
|
+
while: (error) => error._tag === "StorageHostCapacity",
|
|
1198
|
+
schedule: Schedule.spaced("5 seconds"),
|
|
1199
|
+
times: 6,
|
|
1200
|
+
}),
|
|
1201
|
+
Effect.catchTag("Conflict", () => Effect.undefined),
|
|
1202
|
+
);
|
|
1191
1203
|
current =
|
|
1192
1204
|
created ??
|
|
1193
1205
|
(yield* listMachinesByApp(input.appName).pipe(
|
|
@@ -1478,3 +1490,52 @@ export const observeReplicaSet = Effect.fn(function* (input: {
|
|
|
1478
1490
|
};
|
|
1479
1491
|
return undefined;
|
|
1480
1492
|
});
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* The Machines a lookup names: those whose name (or Alchemy base name) is
|
|
1496
|
+
* `name`, or `name-N` for a replica, or every Machine in the App when
|
|
1497
|
+
* `name` is omitted. Of an Alchemy deployment, only the newest generation
|
|
1498
|
+
* that finished rolling out counts; a Machine without that metadata
|
|
1499
|
+
* counts as it is. `undefined` when nothing in the App matches.
|
|
1500
|
+
*/
|
|
1501
|
+
export const lookupReplicaSet = Effect.fn(function* (input: { appName: string; name?: string }) {
|
|
1502
|
+
const named = (yield* listMachinesByApp(input.appName)).filter((machine) => {
|
|
1503
|
+
if (input.name === undefined) return true;
|
|
1504
|
+
const base = machine.config?.metadata?.[alchemyMetadataKeys.baseName];
|
|
1505
|
+
return (
|
|
1506
|
+
base === input.name ||
|
|
1507
|
+
machine.name === input.name ||
|
|
1508
|
+
(base === undefined && machine.name?.startsWith(`${input.name}-`) === true)
|
|
1509
|
+
);
|
|
1510
|
+
});
|
|
1511
|
+
const active = named.filter((machine) => {
|
|
1512
|
+
const phase = machine.config?.metadata?.[alchemyMetadataKeys.phase];
|
|
1513
|
+
return (phase === undefined || phase === "active") && machine.cordoned !== true;
|
|
1514
|
+
});
|
|
1515
|
+
const sequenceOf = (machine: FlyMachine) =>
|
|
1516
|
+
Number(machine.config?.metadata?.[alchemyMetadataKeys.sequence] ?? 0);
|
|
1517
|
+
const newest = Math.max(0, ...active.map(sequenceOf));
|
|
1518
|
+
const listed = active
|
|
1519
|
+
.filter((machine) => sequenceOf(machine) === newest)
|
|
1520
|
+
.sort((a, b) => replicaIndexOf(a) - replicaIndexOf(b));
|
|
1521
|
+
if (listed.length === 0) return undefined;
|
|
1522
|
+
const volumesById = new Map<string, FlyVolume>();
|
|
1523
|
+
for (const machine of listed) {
|
|
1524
|
+
for (const mount of machine.config?.mounts ?? []) {
|
|
1525
|
+
const volumeId = mount.volume;
|
|
1526
|
+
if (volumeId === undefined || volumesById.has(volumeId)) continue;
|
|
1527
|
+
const volume = yield* getVolumeById(input.appName, volumeId);
|
|
1528
|
+
if (volume !== undefined) volumesById.set(volumeId, volume);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
const replicas = listed.map((machine) => toReplica(machine, volumesById));
|
|
1532
|
+
return toReplicaSet(
|
|
1533
|
+
replicas,
|
|
1534
|
+
input.appName,
|
|
1535
|
+
listed[0]?.config?.metadata?.[alchemyMetadataKeys.baseName] ??
|
|
1536
|
+
input.name ??
|
|
1537
|
+
replicas[0]?.name ??
|
|
1538
|
+
"",
|
|
1539
|
+
listed[0]?.config?.services,
|
|
1540
|
+
);
|
|
1541
|
+
});
|
package/src/rpc.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
2
|
import type { HttpEffect } from "alchemy/Http";
|
|
3
3
|
import { RPC_PATH_PREFIX, serveRpc } from "alchemy/Rpc";
|
|
4
|
+
import * as Clock from "effect/Clock";
|
|
5
|
+
import * as Config from "effect/Config";
|
|
4
6
|
import * as Effect from "effect/Effect";
|
|
5
7
|
import * as FileSystem from "effect/FileSystem";
|
|
6
8
|
import { HttpServerRequest } from "effect/http/HttpServerRequest";
|
|
7
9
|
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
8
10
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Machine env var holding the Apps allowed to call the Service's methods,
|
|
13
|
+
* comma-separated: the Apps of the Services that bind it.
|
|
14
|
+
*/
|
|
15
|
+
export const RPC_CALLERS_ENV = "ALCHEMY_FLY_RPC_CALLERS";
|
|
11
16
|
/** Machine env var holding the organization callers must belong to. */
|
|
12
17
|
export const RPC_ORG_ENV = "ALCHEMY_FLY_ORG";
|
|
13
18
|
/**
|
|
@@ -15,8 +20,6 @@ export const RPC_ORG_ENV = "ALCHEMY_FLY_ORG";
|
|
|
15
20
|
* Requests on it must come from Fly's private network.
|
|
16
21
|
*/
|
|
17
22
|
export const BINDING_PORT_ENV = "ALCHEMY_FLY_BINDING_PORT";
|
|
18
|
-
/** Request header carrying the caller token. */
|
|
19
|
-
export const RPC_TOKEN_HEADER = "x-alchemy-rpc-token";
|
|
20
23
|
/** Published port Alchemy adds when a Service has no plain-HTTP port. */
|
|
21
24
|
export const DEFAULT_BINDING_PORT = 7780;
|
|
22
25
|
|
|
@@ -26,21 +29,16 @@ const FLY_SRC_SIGNATURE_HEADER = "fly-src-signature";
|
|
|
26
29
|
const FLY_SRC_PUBLIC_KEY = "/.fly/fly-src.pub";
|
|
27
30
|
/** DER prefix that wraps a raw 32-byte ed25519 key as SubjectPublicKeyInfo. */
|
|
28
31
|
const ED25519_SPKI_PREFIX = "302a300506032b6570032100";
|
|
29
|
-
/**
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Oldest accepted `Fly-Src` timestamp, in seconds. The signature covers
|
|
34
|
+
* only the header, so a short window limits replay; Fly suggests 10s, and
|
|
35
|
+
* the rest allows for clock skew between its proxy and the Machine.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_FLY_SRC_AGE_SECONDS = 30;
|
|
31
38
|
|
|
32
39
|
const header = (headers: Record<string, string | undefined>, name: string): string =>
|
|
33
40
|
headers[name] ?? "";
|
|
34
41
|
|
|
35
|
-
const tokensEqual = (left: string, right: string): boolean => {
|
|
36
|
-
if (left.length !== right.length) return false;
|
|
37
|
-
let mismatch = 0;
|
|
38
|
-
for (let i = 0; i < left.length; i++) {
|
|
39
|
-
mismatch |= left.charCodeAt(i) ^ right.charCodeAt(i);
|
|
40
|
-
}
|
|
41
|
-
return mismatch === 0;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
42
|
let publicKey: crypto.KeyObject | undefined;
|
|
45
43
|
|
|
46
44
|
/** Load Fly's `Fly-Src` verification key once per instance. */
|
|
@@ -64,40 +62,42 @@ const loadPublicKey = Effect.gen(function* () {
|
|
|
64
62
|
});
|
|
65
63
|
|
|
66
64
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* over its private network, so a public
|
|
65
|
+
* The calling App named by a valid `Fly-Src` signature from Fly's proxy for
|
|
66
|
+
* a caller in `org`, or `undefined`. Fly adds the header only to requests
|
|
67
|
+
* that arrive over its private network and signs who sent them, so a public
|
|
68
|
+
* request can never pass and no caller can claim another App.
|
|
70
69
|
*/
|
|
71
70
|
export const verifyFlySrc = (headers: Record<string, string | undefined>, org: string) =>
|
|
72
71
|
Effect.gen(function* () {
|
|
73
72
|
const source = header(headers, FLY_SRC_HEADER);
|
|
74
73
|
const signature = header(headers, FLY_SRC_SIGNATURE_HEADER);
|
|
75
|
-
if (source.length === 0 || signature.length === 0 || org.length === 0) return
|
|
74
|
+
if (source.length === 0 || signature.length === 0 || org.length === 0) return undefined;
|
|
76
75
|
const key = yield* loadPublicKey;
|
|
77
|
-
if (key === undefined) return
|
|
76
|
+
if (key === undefined) return undefined;
|
|
78
77
|
const valid = yield* Effect.try(() =>
|
|
79
78
|
crypto.verify(null, Buffer.from(source), key, Buffer.from(signature, "base64")),
|
|
80
79
|
).pipe(Effect.orElseSucceed(() => false));
|
|
81
|
-
if (!valid) return
|
|
80
|
+
if (!valid) return undefined;
|
|
82
81
|
const fields = Object.fromEntries(
|
|
83
82
|
source.split(";").map((pair) => {
|
|
84
83
|
const at = pair.indexOf("=");
|
|
85
84
|
return [pair.slice(0, at), pair.slice(at + 1)];
|
|
86
85
|
}),
|
|
87
86
|
);
|
|
88
|
-
const now = yield*
|
|
87
|
+
const now = (yield* Clock.currentTimeMillis) / 1000;
|
|
89
88
|
const age = now - Number(fields.ts);
|
|
90
|
-
|
|
89
|
+
const fresh = age >= -MAX_FLY_SRC_AGE_SECONDS && age <= MAX_FLY_SRC_AGE_SECONDS;
|
|
90
|
+
return fields.org === org && fresh && fields.app ? fields.app : undefined;
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
const unauthorized = HttpServerResponse.text("Unauthorized", { status: 401 });
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Serve a Service's RPC methods on `/__rpc__/*` and everything else with
|
|
97
|
-
* `fallback`. RPC calls must carry
|
|
98
|
-
*
|
|
99
|
-
* port Alchemy added for bindings must carry the `Fly-Src`
|
|
100
|
-
* that port never answers the public internet.
|
|
97
|
+
* `fallback`. RPC calls must carry a valid `Fly-Src` signature from the
|
|
98
|
+
* Service's organization naming an App that binds the Service. Every
|
|
99
|
+
* request on the port Alchemy added for bindings must carry the `Fly-Src`
|
|
100
|
+
* signature, so that port never answers the public internet.
|
|
101
101
|
*/
|
|
102
102
|
export const serveFlyRpc = <Req = never>(
|
|
103
103
|
shape: Record<string, unknown>,
|
|
@@ -107,23 +107,20 @@ export const serveFlyRpc = <Req = never>(
|
|
|
107
107
|
const request = yield* HttpServerRequest;
|
|
108
108
|
const headers = request.headers as Record<string, string | undefined>;
|
|
109
109
|
const isRpc = request.url.includes(RPC_PATH_PREFIX);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
const readEnv = (name: string) => Config.String(name).pipe(Effect.orElseSucceed(() => ""));
|
|
111
|
+
const env = {
|
|
112
|
+
bindingPort: yield* readEnv(BINDING_PORT_ENV),
|
|
113
|
+
org: yield* readEnv(RPC_ORG_ENV),
|
|
114
|
+
callers: yield* readEnv(RPC_CALLERS_ENV),
|
|
115
|
+
};
|
|
115
116
|
const bindingPort = env.bindingPort;
|
|
116
117
|
const onBindingPort =
|
|
117
118
|
bindingPort.length > 0 && header(headers, "fly-forwarded-port") === bindingPort;
|
|
118
119
|
if (!isRpc && !onBindingPort) return yield* fallback;
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (expected.length === 0 || provided.length === 0 || !tokensEqual(provided, expected)) {
|
|
124
|
-
return unauthorized;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
120
|
+
const caller = yield* verifyFlySrc(headers, env.org);
|
|
121
|
+
if (caller === undefined) return unauthorized;
|
|
122
|
+
// Methods answer only the Apps of the Services that bind this one.
|
|
123
|
+
if (isRpc && !env.callers.split(",").includes(caller)) return unauthorized;
|
|
127
124
|
return yield* serveRpc(shape, fallback);
|
|
128
125
|
});
|
|
129
126
|
|