@shivaedev/server 0.2.0 → 0.3.1
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/README.md +105 -87
- package/dist/AppHostFactory.d.ts +9 -8
- package/dist/AppHostFactory.d.ts.map +1 -1
- package/dist/AppHostInput.d.ts +4 -3
- package/dist/AppHostInput.d.ts.map +1 -1
- package/dist/appHost.d.ts +30 -5
- package/dist/appHost.d.ts.map +1 -1
- package/dist/appHost.js +4 -3
- package/dist/appHost.js.map +1 -1
- package/dist/appHostHttp.d.ts +45 -47
- package/dist/appHostHttp.d.ts.map +1 -1
- package/dist/appHostHttp.js +13 -42
- package/dist/appHostHttp.js.map +1 -1
- package/dist/development.d.ts.map +1 -1
- package/dist/development.js +2 -1
- package/dist/development.js.map +1 -1
- package/dist/healthRoute.d.ts.map +1 -1
- package/dist/healthRoute.js +3 -2
- package/dist/healthRoute.js.map +1 -1
- package/dist/hostApplicationRoutes.d.ts +3 -0
- package/dist/hostApplicationRoutes.d.ts.map +1 -0
- package/dist/hostApplicationRoutes.js +6 -0
- package/dist/hostApplicationRoutes.js.map +1 -0
- package/dist/hostChanges.js +1 -1
- package/dist/hostChanges.js.map +1 -1
- package/dist/hostOptions.d.ts +6 -2
- package/dist/hostOptions.d.ts.map +1 -1
- package/dist/hostRpc.d.ts +11 -0
- package/dist/hostRpc.d.ts.map +1 -0
- package/dist/hostRpc.js +20 -0
- package/dist/hostRpc.js.map +1 -0
- package/dist/hostRpcRoutes.d.ts +23 -0
- package/dist/hostRpcRoutes.d.ts.map +1 -0
- package/dist/hostRpcRoutes.js +29 -0
- package/dist/hostRpcRoutes.js.map +1 -0
- package/dist/nativeClientOrigins.d.ts +2 -1
- package/dist/nativeClientOrigins.d.ts.map +1 -1
- package/dist/nativeClientOrigins.js +11 -5
- package/dist/nativeClientOrigins.js.map +1 -1
- package/dist/nodeHttpServer.d.ts +7 -0
- package/dist/nodeHttpServer.d.ts.map +1 -0
- package/dist/nodeHttpServer.js +15 -0
- package/dist/nodeHttpServer.js.map +1 -0
- package/dist/otlpHttpClient.d.ts.map +1 -1
- package/dist/otlpHttpClient.js +2 -1
- package/dist/otlpHttpClient.js.map +1 -1
- package/package.json +12 -11
- package/src/AppHostFactory.ts +47 -44
- package/src/AppHostInput.ts +27 -4
- package/src/appHost.ts +25 -7
- package/src/appHostHttp.ts +68 -72
- package/src/development.ts +2 -1
- package/src/healthRoute.ts +3 -2
- package/src/hostApplicationRoutes.ts +8 -0
- package/src/hostChanges.ts +2 -2
- package/src/hostOptions.ts +20 -2
- package/src/hostRpc.ts +26 -0
- package/src/hostRpcRoutes.ts +57 -0
- package/src/nativeClientOrigins.ts +16 -5
- package/src/nodeHttpServer.ts +24 -0
- package/src/otlpHttpClient.ts +2 -1
package/src/AppHostInput.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Effect, Layer } from "effect";
|
|
|
2
2
|
import type { Rpc } from "effect/rpc";
|
|
3
3
|
import type { SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
4
4
|
import type { AppDatabaseOptions, AppHostOptions, AppHttpHostOptions, AppLocalHostOptions } from "#appHost.ts";
|
|
5
|
+
import type { HostRouteRequirements } from "#hostOptions.ts";
|
|
5
6
|
|
|
6
7
|
export type HostPreflight<TServices = never, TError = never, TRequirements = never> =
|
|
7
8
|
| { readonly preflight: Layer.Layer<TServices, TError, TRequirements> }
|
|
@@ -22,11 +23,27 @@ export type AppHostInput<
|
|
|
22
23
|
TPreflight = never,
|
|
23
24
|
TPreflightError = never,
|
|
24
25
|
TPreflightRequirements = never,
|
|
26
|
+
TRouteError = never,
|
|
27
|
+
TRouteRequirements = HostRouteRequirements,
|
|
25
28
|
> = HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
|
|
26
29
|
(
|
|
27
30
|
| (AppDatabaseOptions<TMigrations> & {
|
|
28
31
|
readonly application: Effect.Effect<
|
|
29
|
-
AppHttpHostOptions<
|
|
32
|
+
AppHttpHostOptions<
|
|
33
|
+
TRpcs,
|
|
34
|
+
THandlers,
|
|
35
|
+
THandlerError,
|
|
36
|
+
THandlerRequirements,
|
|
37
|
+
TServices,
|
|
38
|
+
TServiceError,
|
|
39
|
+
TServiceRequirements,
|
|
40
|
+
TSession,
|
|
41
|
+
never,
|
|
42
|
+
never,
|
|
43
|
+
never,
|
|
44
|
+
TRouteError,
|
|
45
|
+
TRouteRequirements
|
|
46
|
+
>,
|
|
30
47
|
TApplicationError,
|
|
31
48
|
TApplicationRequirements
|
|
32
49
|
>;
|
|
@@ -43,7 +60,9 @@ export type AppHostInput<
|
|
|
43
60
|
TSession,
|
|
44
61
|
TPreflight,
|
|
45
62
|
TPreflightError,
|
|
46
|
-
TPreflightRequirements
|
|
63
|
+
TPreflightRequirements,
|
|
64
|
+
TRouteError,
|
|
65
|
+
TRouteRequirements
|
|
47
66
|
>
|
|
48
67
|
| AppLocalHostOptions<
|
|
49
68
|
TRpcs,
|
|
@@ -56,7 +75,9 @@ export type AppHostInput<
|
|
|
56
75
|
TMigrations,
|
|
57
76
|
TPreflight,
|
|
58
77
|
TPreflightError,
|
|
59
|
-
TPreflightRequirements
|
|
78
|
+
TPreflightRequirements,
|
|
79
|
+
TRouteError,
|
|
80
|
+
TRouteRequirements
|
|
60
81
|
>
|
|
61
82
|
| AppHttpHostOptions<
|
|
62
83
|
TRpcs,
|
|
@@ -69,6 +90,8 @@ export type AppHostInput<
|
|
|
69
90
|
TSession,
|
|
70
91
|
TPreflight,
|
|
71
92
|
TPreflightError,
|
|
72
|
-
TPreflightRequirements
|
|
93
|
+
TPreflightRequirements,
|
|
94
|
+
TRouteError,
|
|
95
|
+
TRouteRequirements
|
|
73
96
|
>
|
|
74
97
|
);
|
package/src/appHost.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
|
|
1
2
|
import { Context, type Effect, Layer } from "effect";
|
|
2
3
|
import type { Rpc } from "effect/rpc";
|
|
3
4
|
import type { makeLiveServer } from "@shivaedev/contract/makeLiveServer.ts";
|
|
@@ -6,7 +7,7 @@ import type { AppHostFactory } from "#AppHostFactory.ts";
|
|
|
6
7
|
import type { AppHostInput, HostPreflight } from "#AppHostInput.ts";
|
|
7
8
|
import { appHostHttp, deferredHost, migratedHost } from "#appHostHttp.ts";
|
|
8
9
|
import { type DatabaseConfig, databaseLayer } from "#databaseLayer.ts";
|
|
9
|
-
import type {
|
|
10
|
+
import type { HostCoreOptions, HostPolicy, HostRouteRequirements, HostRoutes } from "#hostOptions.ts";
|
|
10
11
|
import type { MigrationOptions } from "#MigrationOptions.ts";
|
|
11
12
|
|
|
12
13
|
export class AppChanges extends Context.Service<AppChanges, Effect.Success<typeof makeLiveServer>>()("@shivaedev/server/AppChanges") {}
|
|
@@ -32,8 +33,11 @@ export type AppHostOptions<
|
|
|
32
33
|
TPreflight = never,
|
|
33
34
|
TPreflightError = never,
|
|
34
35
|
TPreflightRequirements = never,
|
|
36
|
+
TRouteError = never,
|
|
37
|
+
TRouteRequirements = HostRouteRequirements,
|
|
35
38
|
> = HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
|
|
36
|
-
|
|
39
|
+
HostCoreOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> &
|
|
40
|
+
HostRoutes<TRouteError, TRouteRequirements> & {
|
|
37
41
|
readonly migrations: AppDatabaseOptions<TMigrations>["migrations"];
|
|
38
42
|
readonly policy?: undefined;
|
|
39
43
|
readonly sessions: SessionPolicy<TSession>;
|
|
@@ -51,8 +55,11 @@ export type AppLocalHostOptions<
|
|
|
51
55
|
TPreflight = never,
|
|
52
56
|
TPreflightError = never,
|
|
53
57
|
TPreflightRequirements = never,
|
|
58
|
+
TRouteError = never,
|
|
59
|
+
TRouteRequirements = HostRouteRequirements,
|
|
54
60
|
> = HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
|
|
55
|
-
|
|
61
|
+
HostCoreOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> &
|
|
62
|
+
HostRoutes<TRouteError, TRouteRequirements> & {
|
|
56
63
|
readonly migrations: AppDatabaseOptions<TMigrations>["migrations"];
|
|
57
64
|
readonly policy: "loopback";
|
|
58
65
|
readonly sessions?: never;
|
|
@@ -70,8 +77,11 @@ export type AppHttpHostOptions<
|
|
|
70
77
|
TPreflight = never,
|
|
71
78
|
TPreflightError = never,
|
|
72
79
|
TPreflightRequirements = never,
|
|
80
|
+
TRouteError = never,
|
|
81
|
+
TRouteRequirements = HostRouteRequirements,
|
|
73
82
|
> = HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
|
|
74
|
-
|
|
83
|
+
HostCoreOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> &
|
|
84
|
+
HostRoutes<TRouteError, TRouteRequirements> &
|
|
75
85
|
HostPolicy<TSession> & { readonly migrations?: undefined };
|
|
76
86
|
export const appHost: AppHostFactory = <
|
|
77
87
|
TRpcs extends Rpc.Any,
|
|
@@ -88,6 +98,8 @@ export const appHost: AppHostFactory = <
|
|
|
88
98
|
TPreflight = never,
|
|
89
99
|
TPreflightError = never,
|
|
90
100
|
TPreflightRequirements = never,
|
|
101
|
+
TRouteError = never,
|
|
102
|
+
TRouteRequirements = HostRouteRequirements,
|
|
91
103
|
>(
|
|
92
104
|
options: AppHostInput<
|
|
93
105
|
TRpcs,
|
|
@@ -103,15 +115,21 @@ export const appHost: AppHostFactory = <
|
|
|
103
115
|
TApplicationRequirements,
|
|
104
116
|
TPreflight,
|
|
105
117
|
TPreflightError,
|
|
106
|
-
TPreflightRequirements
|
|
118
|
+
TPreflightRequirements,
|
|
119
|
+
TRouteError,
|
|
120
|
+
TRouteRequirements
|
|
107
121
|
>,
|
|
108
122
|
) => {
|
|
109
123
|
if ("application" in options) {
|
|
110
|
-
return deferredHost(options.application, appDatabase(options), AppChanges).pipe(
|
|
124
|
+
return deferredHost(options.application, appDatabase(options), AppChanges).pipe(
|
|
125
|
+
Layer.provideMerge(options.preflight ?? Layer.empty),
|
|
126
|
+
Layer.provide(NodeCrypto.layer),
|
|
127
|
+
);
|
|
111
128
|
}
|
|
112
129
|
return options.migrations === undefined
|
|
113
|
-
? appHostHttp(options, AppChanges).pipe(Layer.provideMerge(options.preflight ?? Layer.empty))
|
|
130
|
+
? appHostHttp(options, AppChanges).pipe(Layer.provideMerge(options.preflight ?? Layer.empty), Layer.provide(NodeCrypto.layer))
|
|
114
131
|
: migratedHost({ ...options, migrations: options.migrations }, appDatabase({ ...options, migrations: options.migrations }), AppChanges).pipe(
|
|
115
132
|
Layer.provideMerge(options.preflight ?? Layer.empty),
|
|
133
|
+
Layer.provide(NodeCrypto.layer),
|
|
116
134
|
);
|
|
117
135
|
};
|
package/src/appHostHttp.ts
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import type { Authenticated, MaybeAuthenticated, RequestTracing } from "@shivaedev/identity/middleware.ts";
|
|
7
|
-
import { localRpcGuard } from "@shivaedev/identity/server/local.ts";
|
|
8
|
-
import { authenticatedLayer, maybeAuthenticatedLayer, type SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
9
|
-
import { requestTracingLayer } from "@shivaedev/identity/server/tracing.ts";
|
|
1
|
+
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
|
2
|
+
import { Effect, Layer } from "effect";
|
|
3
|
+
import { HttpRouter, HttpServer, HttpStaticServer } from "effect/http";
|
|
4
|
+
import type { Rpc } from "effect/rpc";
|
|
5
|
+
import type { SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
10
6
|
import type { AppChanges, AppHttpHostOptions } from "#appHost.ts";
|
|
11
|
-
import { boundedRpcRequest } from "#boundedRpcRequest.ts";
|
|
12
7
|
import type { databaseLayer } from "#databaseLayer.ts";
|
|
13
8
|
import { healthRoute } from "#healthRoute.ts";
|
|
14
|
-
import {
|
|
15
|
-
import type { HostOptions, HostPolicy } from "#hostOptions.ts";
|
|
9
|
+
import { hostApplicationRoutes } from "#hostApplicationRoutes.ts";
|
|
10
|
+
import type { HostOptions, HostPolicy, HostRouteRequirements } from "#hostOptions.ts";
|
|
11
|
+
import { hostRpcRoutes } from "#hostRpcRoutes.ts";
|
|
16
12
|
import { hostTransport } from "#hostTransport.ts";
|
|
17
13
|
import type { MigrationOptions } from "#MigrationOptions.ts";
|
|
18
14
|
import { nativeClientOrigins } from "#nativeClientOrigins.ts";
|
|
15
|
+
import { nodeHttpServer } from "#nodeHttpServer.ts";
|
|
19
16
|
import { otlpTracing } from "#otlpTracing.ts";
|
|
20
17
|
import { RedactionPolicy } from "#RedactionPolicy.ts";
|
|
21
18
|
import { redactingDiagnostics } from "#redactingDiagnostics.ts";
|
|
22
|
-
import { redactingRpcSerialization } from "#redactingRpcSerialization.ts";
|
|
23
19
|
import { schemaSensitiveKeys } from "#schemaSensitiveKeys.ts";
|
|
24
|
-
import { tracedGroup } from "#tracedGroup.ts";
|
|
25
|
-
|
|
26
|
-
const rpcCacheControl: HttpEffect.PreResponseHandler = (_, response) =>
|
|
27
|
-
Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "private, no-store"));
|
|
28
20
|
|
|
29
21
|
export function appHostHttp<
|
|
30
22
|
TRpcs extends Rpc.Any,
|
|
@@ -35,71 +27,45 @@ export function appHostHttp<
|
|
|
35
27
|
TServiceError,
|
|
36
28
|
TServiceRequirements,
|
|
37
29
|
TSession extends SessionShape,
|
|
30
|
+
TRouteError = never,
|
|
31
|
+
TRouteRequirements = HostRouteRequirements,
|
|
38
32
|
>(
|
|
39
|
-
options: HostOptions<
|
|
33
|
+
options: HostOptions<
|
|
34
|
+
TRpcs,
|
|
35
|
+
THandlers,
|
|
36
|
+
THandlerError,
|
|
37
|
+
THandlerRequirements,
|
|
38
|
+
TServices,
|
|
39
|
+
TServiceError,
|
|
40
|
+
TServiceRequirements,
|
|
41
|
+
TRouteError,
|
|
42
|
+
TRouteRequirements
|
|
43
|
+
> &
|
|
44
|
+
HostPolicy<TSession>,
|
|
40
45
|
changesService: typeof AppChanges,
|
|
41
46
|
) {
|
|
42
47
|
const sensitive = schemaSensitiveKeys(options.group, options.config?.sensitiveKeys);
|
|
43
48
|
return Layer.unwrap(
|
|
44
49
|
Effect.gen(function* () {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
? requestTracingLayer({ sensitive })
|
|
49
|
-
: Layer.mergeAll(authenticatedLayer(options.sessions), maybeAuthenticatedLayer(options.sessions), requestTracingLayer({ sensitive }));
|
|
50
|
-
const routes = HttpRouter.use((router) =>
|
|
51
|
-
Effect.gen(function* () {
|
|
52
|
-
const wire = redactingRpcSerialization(yield* RpcSerialization.RpcSerialization, sensitive, sensitive.maskFailure);
|
|
53
|
-
const handler = yield* RpcServer.toHttpEffect(tracedGroup(options.group)).pipe(
|
|
54
|
-
Effect.provideService(RpcSerialization.RpcSerialization, wire),
|
|
55
|
-
);
|
|
56
|
-
const guarded = localRpcGuard(handler, trustedProxy, nativeOrigins);
|
|
57
|
-
if (nativeOrigins.length > 0) {
|
|
58
|
-
yield* router.add("OPTIONS", options.rpcPath ?? "/rpc", HttpServerResponse.text("Local same-origin requests only", { status: 403 }));
|
|
59
|
-
}
|
|
60
|
-
yield* router.add("POST", options.rpcPath ?? "/rpc", (request) => {
|
|
61
|
-
HttpEffect.appendPreResponseHandlerUnsafe(request, rpcCacheControl);
|
|
62
|
-
return guarded.pipe(
|
|
63
|
-
Effect.provideService(HttpIncomingMessage.MaxBodySize, ByteSize.bytes(bodySize)),
|
|
64
|
-
Effect.provideService(HttpServerRequest.HttpServerRequest, boundedRpcRequest(request)),
|
|
65
|
-
Effect.provideService(RpcSerialization.RpcSerialization, wire),
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
}),
|
|
69
|
-
).pipe(
|
|
70
|
-
Layer.provide(Layer.merge(middleware, hostChanges(options.group, changesService).pipe(Layer.provideMerge(options.handlers)))),
|
|
71
|
-
Layer.provide(RpcSerialization.layerNdjson),
|
|
72
|
-
Layer.provide(nativeClientOrigins(nativeOrigins, trustedProxy)),
|
|
73
|
-
);
|
|
50
|
+
const transport = yield* hostTransport(options);
|
|
51
|
+
const { bodySize, host, trustedProxy, nativeOrigins } = transport;
|
|
52
|
+
const routes = hostRpcRoutes(options, changesService, transport, sensitive);
|
|
74
53
|
const assets =
|
|
75
54
|
options.assets === undefined
|
|
76
55
|
? Layer.empty
|
|
77
56
|
: HttpStaticServer.layer(typeof options.assets === "string" ? { root: options.assets } : options.assets);
|
|
78
57
|
const application = Layer.mergeAll(options.services ?? Layer.empty);
|
|
79
|
-
const server =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
host,
|
|
85
|
-
port,
|
|
86
|
-
...(options.config?.disablePreemptiveShutdown === undefined
|
|
87
|
-
? {}
|
|
88
|
-
: { disablePreemptiveShutdown: options.config.disablePreemptiveShutdown }),
|
|
89
|
-
});
|
|
90
|
-
}),
|
|
58
|
+
const server = nodeHttpServer(host, options.config);
|
|
59
|
+
const applicationRoutes = hostApplicationRoutes(options.routes, bodySize);
|
|
60
|
+
const httpRoutes = Layer.mergeAll(routes, healthRoute, assets, applicationRoutes).pipe(
|
|
61
|
+
Layer.provide(nativeClientOrigins(nativeOrigins, trustedProxy, false, options.rpcPath)),
|
|
62
|
+
Layer.provide(nativeClientOrigins(nativeOrigins, trustedProxy, options.nativeOrigins === undefined ? "preflight" : true)),
|
|
91
63
|
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
64
|
+
return Layer.unwrap(
|
|
65
|
+
HttpRouter.toHttpEffect(httpRoutes).pipe(
|
|
66
|
+
Effect.map((handler) => HttpServer.serve(handler).pipe(HttpServer.withLogAddress, Layer.provideMerge(server))),
|
|
95
67
|
),
|
|
96
|
-
|
|
97
|
-
);
|
|
98
|
-
return HttpRouter.serve(Layer.mergeAll(routes, healthRoute, assets, applicationRoutes), { disableLogger: true }).pipe(
|
|
99
|
-
Layer.provideMerge(application),
|
|
100
|
-
Layer.provideMerge(server),
|
|
101
|
-
Layer.provide(NodeServices.layer),
|
|
102
|
-
);
|
|
68
|
+
).pipe(Layer.provideMerge(application), Layer.provideMerge(NodeHttpServer.layerHttpServices));
|
|
103
69
|
}),
|
|
104
70
|
).pipe(Layer.provide(redactingDiagnostics(sensitive)), Layer.provide(otlpTracing), Layer.provide(Layer.succeed(RedactionPolicy, sensitive)));
|
|
105
71
|
}
|
|
@@ -114,9 +80,23 @@ export function migratedHost<
|
|
|
114
80
|
TServiceRequirements,
|
|
115
81
|
TMigrations,
|
|
116
82
|
TSession extends SessionShape,
|
|
83
|
+
TRouteError = never,
|
|
84
|
+
TRouteRequirements = HostRouteRequirements,
|
|
117
85
|
>(
|
|
118
|
-
options: HostOptions<
|
|
119
|
-
|
|
86
|
+
options: HostOptions<
|
|
87
|
+
TRpcs,
|
|
88
|
+
THandlers,
|
|
89
|
+
THandlerError,
|
|
90
|
+
THandlerRequirements,
|
|
91
|
+
TServices,
|
|
92
|
+
TServiceError,
|
|
93
|
+
TServiceRequirements,
|
|
94
|
+
TRouteError,
|
|
95
|
+
TRouteRequirements
|
|
96
|
+
> &
|
|
97
|
+
HostPolicy<TSession> & {
|
|
98
|
+
readonly migrations: MigrationOptions<TMigrations>;
|
|
99
|
+
},
|
|
120
100
|
database: ReturnType<typeof databaseLayer<TMigrations>>,
|
|
121
101
|
changesService: typeof AppChanges,
|
|
122
102
|
) {
|
|
@@ -136,9 +116,25 @@ export function deferredHost<
|
|
|
136
116
|
TSession extends SessionShape,
|
|
137
117
|
TApplicationError,
|
|
138
118
|
TApplicationRequirements,
|
|
119
|
+
TRouteError = never,
|
|
120
|
+
TRouteRequirements = HostRouteRequirements,
|
|
139
121
|
>(
|
|
140
122
|
application: Effect.Effect<
|
|
141
|
-
AppHttpHostOptions<
|
|
123
|
+
AppHttpHostOptions<
|
|
124
|
+
TRpcs,
|
|
125
|
+
THandlers,
|
|
126
|
+
THandlerError,
|
|
127
|
+
THandlerRequirements,
|
|
128
|
+
TServices,
|
|
129
|
+
TServiceError,
|
|
130
|
+
TServiceRequirements,
|
|
131
|
+
TSession,
|
|
132
|
+
never,
|
|
133
|
+
never,
|
|
134
|
+
never,
|
|
135
|
+
TRouteError,
|
|
136
|
+
TRouteRequirements
|
|
137
|
+
>,
|
|
142
138
|
TApplicationError,
|
|
143
139
|
TApplicationRequirements
|
|
144
140
|
>,
|
package/src/development.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import
|
|
2
|
+
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
3
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
3
4
|
import { Effect } from "effect";
|
|
4
5
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
5
6
|
|
package/src/healthRoute.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect, Fiber, Option } from "effect";
|
|
2
2
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
3
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
3
4
|
import { SqlClient } from "effect/sql";
|
|
4
5
|
|
|
5
6
|
export const healthRoute = HttpRouter.use((router) =>
|
|
@@ -8,14 +9,14 @@ export const healthRoute = HttpRouter.use((router) =>
|
|
|
8
9
|
const scope = yield* Effect.scope;
|
|
9
10
|
const probe = Effect.gen(function* () {
|
|
10
11
|
if (Option.isNone(database)) {
|
|
11
|
-
return
|
|
12
|
+
return HttpStatus.fromLiteral("ServiceUnavailable");
|
|
12
13
|
}
|
|
13
14
|
const query = yield* database.value`select 1`.pipe(Effect.forkIn(scope));
|
|
14
15
|
return yield* Fiber.join(query).pipe(
|
|
15
16
|
Effect.timeout("2 seconds"),
|
|
16
17
|
// Driver cancellation may need network I/O; keep cleanup in the host scope without delaying the response.
|
|
17
18
|
Effect.ensuring(Fiber.interrupt(query).pipe(Effect.forkIn(scope), Effect.asVoid)),
|
|
18
|
-
Effect.match({ onFailure: () =>
|
|
19
|
+
Effect.match({ onFailure: () => HttpStatus.fromLiteral("ServiceUnavailable"), onSuccess: () => HttpStatus.fromLiteral("OK") }),
|
|
19
20
|
);
|
|
20
21
|
});
|
|
21
22
|
yield* router.add(
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ByteSize, Effect, Layer } from "effect";
|
|
2
|
+
import { HttpIncomingMessage, HttpRouter } from "effect/http";
|
|
3
|
+
|
|
4
|
+
export function hostApplicationRoutes<TError, TRequirements>(routes: Layer.Layer<never, TError, TRequirements> | undefined, bodySize: number) {
|
|
5
|
+
return (routes ?? Layer.empty).pipe(
|
|
6
|
+
Layer.provide(HttpRouter.middleware((effect) => Effect.provideService(effect, HttpIncomingMessage.MaxBodySize, ByteSize.bytes(bodySize))).layer),
|
|
7
|
+
);
|
|
8
|
+
}
|
package/src/hostChanges.ts
CHANGED
|
@@ -21,8 +21,8 @@ export function hostChanges<R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, chan
|
|
|
21
21
|
}
|
|
22
22
|
function handler({ queries }: { readonly queries: readonly QueryReference[] }) {
|
|
23
23
|
return Stream.unwrap(
|
|
24
|
-
Effect.map(resolveChanges(rpc, queries), ({
|
|
25
|
-
Option.isSome(changes) ? changes.value.
|
|
24
|
+
Effect.map(resolveChanges(rpc, queries), ({ subscription, context }) =>
|
|
25
|
+
Option.isSome(changes) ? changes.value.subscribe(subscription, context) : Stream.never,
|
|
26
26
|
),
|
|
27
27
|
);
|
|
28
28
|
}
|
package/src/hostOptions.ts
CHANGED
|
@@ -4,7 +4,9 @@ import type { Rpc, RpcGroup } from "effect/rpc";
|
|
|
4
4
|
import type { SessionPolicy, SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
5
5
|
import type { AppDatabaseOptions } from "./appHost.ts";
|
|
6
6
|
|
|
7
|
-
export type
|
|
7
|
+
export type HostRouteRequirements = HttpRouter.HttpRouter | HttpRouter.Request<"Error", unknown>;
|
|
8
|
+
|
|
9
|
+
export type HostCoreOptions<TRpcs extends Rpc.Any, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> = {
|
|
8
10
|
readonly assets?: string | Parameters<typeof HttpStaticServer.layer>[0];
|
|
9
11
|
readonly config?: AppDatabaseOptions["config"] & {
|
|
10
12
|
readonly port?: number;
|
|
@@ -18,13 +20,29 @@ export type HostOptions<TRpcs extends Rpc.Any, THandlers, THandlerError, THandle
|
|
|
18
20
|
readonly group: RpcGroup.RpcGroup<TRpcs>;
|
|
19
21
|
readonly handlers: Layer.Layer<THandlers, THandlerError, THandlerRequirements>;
|
|
20
22
|
readonly nativeOrigins?: readonly string[];
|
|
21
|
-
readonly routes?: Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", unknown>>;
|
|
22
23
|
readonly rpcPath?: HttpRouter.PathInput;
|
|
23
24
|
} & (
|
|
24
25
|
| { readonly services: Layer.Layer<TServices, TServiceError, TServiceRequirements> }
|
|
25
26
|
| ([TServices | TServiceError | TServiceRequirements] extends [never] ? { readonly services?: never } : never)
|
|
26
27
|
);
|
|
27
28
|
|
|
29
|
+
export interface HostRoutes<TRouteError = never, TRouteRequirements = HostRouteRequirements> {
|
|
30
|
+
readonly routes?: Layer.Layer<never, TRouteError, TRouteRequirements>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type HostOptions<
|
|
34
|
+
TRpcs extends Rpc.Any,
|
|
35
|
+
THandlers,
|
|
36
|
+
THandlerError,
|
|
37
|
+
THandlerRequirements,
|
|
38
|
+
TServices,
|
|
39
|
+
TServiceError,
|
|
40
|
+
TServiceRequirements,
|
|
41
|
+
TRouteError = never,
|
|
42
|
+
TRouteRequirements = HostRouteRequirements,
|
|
43
|
+
> = HostCoreOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> &
|
|
44
|
+
HostRoutes<TRouteError, TRouteRequirements>;
|
|
45
|
+
|
|
28
46
|
export type HostPolicy<TSession extends SessionShape> =
|
|
29
47
|
| { readonly policy?: undefined; readonly sessions: SessionPolicy<TSession> }
|
|
30
48
|
| { readonly policy: "loopback"; readonly sessions?: never };
|
package/src/hostRpc.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { type HttpEffect, HttpServerResponse } from "effect/http";
|
|
3
|
+
import { type Rpc, type RpcGroup, RpcMiddleware, RpcServer } from "effect/rpc";
|
|
4
|
+
import { authorizeReads, CurrentViewer } from "@shivaedev/contract/readPolicy.ts";
|
|
5
|
+
import { MissingReadPolicy, QueryReadDenied } from "@shivaedev/contract/readPolicyErrors.ts";
|
|
6
|
+
import { tracedGroup } from "#tracedGroup.ts";
|
|
7
|
+
|
|
8
|
+
class HostedReadPolicy extends RpcMiddleware.Service<HostedReadPolicy>()("@shivaedev/server/HostedReadPolicy", {
|
|
9
|
+
error: Schema.Union([MissingReadPolicy, QueryReadDenied]),
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
export type { HostedReadPolicy };
|
|
13
|
+
|
|
14
|
+
export const rpcCacheControl: HttpEffect.PreResponseHandler = (_, response) =>
|
|
15
|
+
Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "private, no-store"));
|
|
16
|
+
|
|
17
|
+
export const hostRpc = Effect.fnUntraced(function* <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>) {
|
|
18
|
+
const guarded = group.middleware(HostedReadPolicy);
|
|
19
|
+
// Keep the policy inside identity middleware so it sees the request's viewer.
|
|
20
|
+
for (const rpc of guarded.requests.values()) {
|
|
21
|
+
Object.assign(rpc, { middlewares: new Set([HostedReadPolicy, ...rpc.middlewares]) });
|
|
22
|
+
}
|
|
23
|
+
return yield* RpcServer.toHttpEffect(tracedGroup(guarded)).pipe(
|
|
24
|
+
Effect.provideService(HostedReadPolicy, (effect, request) => Effect.flatMap(CurrentViewer, (viewer) => authorizeReads(effect, request, viewer))),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ByteSize, Effect, Layer } from "effect";
|
|
2
|
+
import { HttpEffect, HttpIncomingMessage, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
3
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
4
|
+
import { type Rpc, type RpcGroup, RpcSerialization } from "effect/rpc";
|
|
5
|
+
import type { Authenticated, MaybeAuthenticated, RequestTracing } from "@shivaedev/identity/middleware.ts";
|
|
6
|
+
import { localRpcGuard } from "@shivaedev/identity/server/local.ts";
|
|
7
|
+
import { authenticatedLayer, maybeAuthenticatedLayer, type SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
8
|
+
import { requestTracingLayer } from "@shivaedev/identity/server/tracing.ts";
|
|
9
|
+
import type { AppChanges } from "#appHost.ts";
|
|
10
|
+
import { boundedRpcRequest } from "#boundedRpcRequest.ts";
|
|
11
|
+
import { hostChanges } from "#hostChanges.ts";
|
|
12
|
+
import type { HostPolicy } from "#hostOptions.ts";
|
|
13
|
+
import { hostRpc, rpcCacheControl } from "#hostRpc.ts";
|
|
14
|
+
import type { hostTransport } from "#hostTransport.ts";
|
|
15
|
+
import { redactingRpcSerialization } from "#redactingRpcSerialization.ts";
|
|
16
|
+
import type { schemaSensitiveKeys } from "#schemaSensitiveKeys.ts";
|
|
17
|
+
|
|
18
|
+
export function hostRpcRoutes<TRpcs extends Rpc.Any, THandlers, TError, TRequirements, TSession extends SessionShape>(
|
|
19
|
+
options: {
|
|
20
|
+
readonly group: RpcGroup.RpcGroup<TRpcs>;
|
|
21
|
+
readonly handlers: Layer.Layer<THandlers, TError, TRequirements>;
|
|
22
|
+
readonly rpcPath?: HttpRouter.PathInput;
|
|
23
|
+
} & HostPolicy<TSession>,
|
|
24
|
+
changesService: typeof AppChanges,
|
|
25
|
+
{ bodySize, trustedProxy, nativeOrigins }: Effect.Success<ReturnType<typeof hostTransport>>,
|
|
26
|
+
sensitive: ReturnType<typeof schemaSensitiveKeys>,
|
|
27
|
+
) {
|
|
28
|
+
const middleware: Layer.Layer<RequestTracing> | Layer.Layer<Authenticated | MaybeAuthenticated | RequestTracing> =
|
|
29
|
+
options.policy === "loopback"
|
|
30
|
+
? requestTracingLayer({ sensitive })
|
|
31
|
+
: Layer.mergeAll(authenticatedLayer(options.sessions), maybeAuthenticatedLayer(options.sessions), requestTracingLayer({ sensitive }));
|
|
32
|
+
return HttpRouter.use((router) =>
|
|
33
|
+
Effect.gen(function* () {
|
|
34
|
+
const wire = redactingRpcSerialization(yield* RpcSerialization.RpcSerialization, sensitive, sensitive.maskFailure);
|
|
35
|
+
const handler = yield* hostRpc(options.group).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire));
|
|
36
|
+
const guarded = localRpcGuard(handler, trustedProxy, nativeOrigins);
|
|
37
|
+
yield* router.add(
|
|
38
|
+
"OPTIONS",
|
|
39
|
+
options.rpcPath ?? "/rpc",
|
|
40
|
+
HttpServerResponse.text("Local same-origin requests only", {
|
|
41
|
+
status: HttpStatus.fromLiteral("Forbidden"),
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
yield* router.add("POST", options.rpcPath ?? "/rpc", (request) => {
|
|
45
|
+
HttpEffect.appendPreResponseHandlerUnsafe(request, rpcCacheControl);
|
|
46
|
+
return guarded.pipe(
|
|
47
|
+
Effect.provideService(HttpIncomingMessage.MaxBodySize, ByteSize.bytes(bodySize)),
|
|
48
|
+
Effect.provideService(HttpServerRequest.HttpServerRequest, boundedRpcRequest(request)),
|
|
49
|
+
Effect.provideService(RpcSerialization.RpcSerialization, wire),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
}),
|
|
53
|
+
).pipe(
|
|
54
|
+
Layer.provide(Layer.merge(middleware, hostChanges(options.group, changesService).pipe(Layer.provideMerge(options.handlers)))),
|
|
55
|
+
Layer.provide(RpcSerialization.layerNdjson),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { Headers, HttpMiddleware, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
3
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
3
4
|
import { localTransport } from "@shivaedev/identity/server/local.ts";
|
|
4
5
|
|
|
5
6
|
export const NativeOrigins = Schema.Array(
|
|
@@ -16,7 +17,13 @@ export const NativeOrigins = Schema.Array(
|
|
|
16
17
|
),
|
|
17
18
|
);
|
|
18
19
|
|
|
19
|
-
export function nativeClientOrigins(
|
|
20
|
+
export function nativeClientOrigins(
|
|
21
|
+
origins: readonly string[],
|
|
22
|
+
trustedProxy: string | undefined,
|
|
23
|
+
auth: boolean | "preflight" = false,
|
|
24
|
+
rpcPath: HttpRouter.PathInput = "/rpc",
|
|
25
|
+
) {
|
|
26
|
+
const matchesPath = auth ? (path: string) => path === "/api/auth" || path.startsWith("/api/auth/") : (path: string) => path === rpcPath;
|
|
20
27
|
const allowed = localTransport(trustedProxy, origins);
|
|
21
28
|
const cors = HttpMiddleware.cors({
|
|
22
29
|
allowedHeaders: ["Authorization", "Content-Type"],
|
|
@@ -28,16 +35,20 @@ export function nativeClientOrigins(origins: readonly string[], trustedProxy: st
|
|
|
28
35
|
return HttpRouter.middleware((effect) =>
|
|
29
36
|
Effect.gen(function* () {
|
|
30
37
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
31
|
-
if (
|
|
38
|
+
if (!matchesPath(request.url.split("?")[0] ?? "")) {
|
|
32
39
|
return yield* effect;
|
|
33
40
|
}
|
|
34
|
-
if ((auth || request.method === "OPTIONS") && !allowed(request)) {
|
|
35
|
-
return HttpServerResponse.text("Local same-origin requests only", {
|
|
41
|
+
if ((auth === true || request.method === "OPTIONS") && !allowed(request)) {
|
|
42
|
+
return HttpServerResponse.text("Local same-origin requests only", {
|
|
43
|
+
status: HttpStatus.fromLiteral("Forbidden"),
|
|
44
|
+
});
|
|
36
45
|
}
|
|
37
46
|
if (!origins.includes(request.headers.origin ?? "")) {
|
|
38
47
|
return yield* effect;
|
|
39
48
|
}
|
|
40
|
-
const native = request.modify({
|
|
49
|
+
const native = request.modify({
|
|
50
|
+
headers: Headers.remove(request.headers, "cookie"),
|
|
51
|
+
});
|
|
41
52
|
return yield* cors(effect).pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, native));
|
|
42
53
|
}),
|
|
43
54
|
).layer;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
|
3
|
+
import { Config, type Duration, Effect, Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
export function nodeHttpServer(
|
|
6
|
+
host: string,
|
|
7
|
+
config?: {
|
|
8
|
+
readonly port?: number;
|
|
9
|
+
readonly gracefulShutdownTimeout?: Duration.Input;
|
|
10
|
+
readonly disablePreemptiveShutdown?: boolean;
|
|
11
|
+
},
|
|
12
|
+
) {
|
|
13
|
+
return Layer.unwrap(
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
const port = config?.port ?? (yield* Config.Int("PORT").pipe(Config.withDefault(3000)));
|
|
16
|
+
return NodeHttpServer.layer(createServer, {
|
|
17
|
+
gracefulShutdownTimeout: config?.gracefulShutdownTimeout,
|
|
18
|
+
host,
|
|
19
|
+
port,
|
|
20
|
+
...(config?.disablePreemptiveShutdown === undefined ? {} : { disablePreemptiveShutdown: config.disablePreemptiveShutdown }),
|
|
21
|
+
});
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
}
|
package/src/otlpHttpClient.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect, Layer } from "effect";
|
|
2
2
|
import { FetchHttpClient, HttpClient, HttpClientError, type HttpClientRequest } from "effect/http";
|
|
3
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
3
4
|
import { RedactionPolicy } from "#RedactionPolicy.ts";
|
|
4
5
|
import { redactOtlpText } from "#safeOtlpSerialization.ts";
|
|
5
6
|
|
|
@@ -36,7 +37,7 @@ export function otlpHttpClient(timeout = 10_000) {
|
|
|
36
37
|
),
|
|
37
38
|
),
|
|
38
39
|
HttpClient.tap((response) => {
|
|
39
|
-
if (response.status >=
|
|
40
|
+
if (response.status >= HttpStatus.fromLiteral("OK") && response.status < HttpStatus.fromLiteral("MultipleChoices")) {
|
|
40
41
|
warned = false;
|
|
41
42
|
return Effect.void;
|
|
42
43
|
}
|