@shivaedev/server 0.0.0-stage → 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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -2
  3. package/dist/AppHostFactory.d.ts +15 -0
  4. package/dist/AppHostFactory.d.ts.map +1 -0
  5. package/dist/AppHostFactory.js +2 -0
  6. package/dist/AppHostFactory.js.map +1 -0
  7. package/dist/AppHostInput.d.ts +13 -0
  8. package/dist/AppHostInput.d.ts.map +1 -0
  9. package/dist/AppHostInput.js +2 -0
  10. package/dist/AppHostInput.js.map +1 -0
  11. package/dist/MigrationOptions.d.ts +15 -0
  12. package/dist/MigrationOptions.d.ts.map +1 -0
  13. package/dist/MigrationOptions.js +2 -0
  14. package/dist/MigrationOptions.js.map +1 -0
  15. package/dist/RedactionPolicy.d.ts +4 -0
  16. package/dist/RedactionPolicy.d.ts.map +1 -0
  17. package/dist/RedactionPolicy.js +4 -0
  18. package/dist/RedactionPolicy.js.map +1 -0
  19. package/dist/appHost.d.ts +47 -0
  20. package/dist/appHost.d.ts.map +1 -0
  21. package/dist/appHost.js +17 -0
  22. package/dist/appHost.js.map +1 -0
  23. package/dist/appHostHttp.d.ts +88 -0
  24. package/dist/appHostHttp.d.ts.map +1 -0
  25. package/dist/appHostHttp.js +67 -0
  26. package/dist/appHostHttp.js.map +1 -0
  27. package/dist/boundedRpcRequest.d.ts +3 -0
  28. package/dist/boundedRpcRequest.d.ts.map +1 -0
  29. package/dist/boundedRpcRequest.js +22 -0
  30. package/dist/boundedRpcRequest.js.map +1 -0
  31. package/dist/databaseAddress.d.ts +4 -0
  32. package/dist/databaseAddress.d.ts.map +1 -0
  33. package/dist/databaseAddress.js +25 -0
  34. package/dist/databaseAddress.js.map +1 -0
  35. package/dist/databaseLayer.d.ts +14 -0
  36. package/dist/databaseLayer.d.ts.map +1 -0
  37. package/dist/databaseLayer.js +100 -0
  38. package/dist/databaseLayer.js.map +1 -0
  39. package/dist/development.d.ts +10 -0
  40. package/dist/development.d.ts.map +1 -0
  41. package/dist/development.js +20 -0
  42. package/dist/development.js.map +1 -0
  43. package/dist/healthRoute.d.ts +3 -0
  44. package/dist/healthRoute.d.ts.map +1 -0
  45. package/dist/healthRoute.js +18 -0
  46. package/dist/healthRoute.js.map +1 -0
  47. package/dist/hostChanges.d.ts +7 -0
  48. package/dist/hostChanges.d.ts.map +1 -0
  49. package/dist/hostChanges.js +25 -0
  50. package/dist/hostChanges.js.map +1 -0
  51. package/dist/hostOptions.d.ts +34 -0
  52. package/dist/hostOptions.d.ts.map +1 -0
  53. package/dist/hostOptions.js +2 -0
  54. package/dist/hostOptions.js.map +1 -0
  55. package/dist/hostTransport.d.ts +16 -0
  56. package/dist/hostTransport.d.ts.map +1 -0
  57. package/dist/hostTransport.js +16 -0
  58. package/dist/hostTransport.js.map +1 -0
  59. package/dist/nativeClientOrigins.d.ts +4 -0
  60. package/dist/nativeClientOrigins.d.ts.map +1 -0
  61. package/dist/nativeClientOrigins.js +38 -0
  62. package/dist/nativeClientOrigins.js.map +1 -0
  63. package/dist/node-http/subscriptionSignal.d.ts +12 -0
  64. package/dist/node-http/subscriptionSignal.d.ts.map +1 -0
  65. package/dist/node-http/subscriptionSignal.js +55 -0
  66. package/dist/node-http/subscriptionSignal.js.map +1 -0
  67. package/dist/otlpHttpClient.d.ts +4 -0
  68. package/dist/otlpHttpClient.d.ts.map +1 -0
  69. package/dist/otlpHttpClient.js +36 -0
  70. package/dist/otlpHttpClient.js.map +1 -0
  71. package/dist/otlpTracing.d.ts +3 -0
  72. package/dist/otlpTracing.d.ts.map +1 -0
  73. package/dist/otlpTracing.js +35 -0
  74. package/dist/otlpTracing.js.map +1 -0
  75. package/dist/redactOtlpUrl.d.ts +2 -0
  76. package/dist/redactOtlpUrl.d.ts.map +1 -0
  77. package/dist/redactOtlpUrl.js +41 -0
  78. package/dist/redactOtlpUrl.js.map +1 -0
  79. package/dist/redactingDiagnostics.d.ts +5 -0
  80. package/dist/redactingDiagnostics.d.ts.map +1 -0
  81. package/dist/redactingDiagnostics.js +25 -0
  82. package/dist/redactingDiagnostics.js.map +1 -0
  83. package/dist/redactingRpcSerialization.d.ts +4 -0
  84. package/dist/redactingRpcSerialization.d.ts.map +1 -0
  85. package/dist/redactingRpcSerialization.js +74 -0
  86. package/dist/redactingRpcSerialization.js.map +1 -0
  87. package/dist/redactingTracer.d.ts +4 -0
  88. package/dist/redactingTracer.d.ts.map +1 -0
  89. package/dist/redactingTracer.js +32 -0
  90. package/dist/redactingTracer.js.map +1 -0
  91. package/dist/rpc-server/publicConfigLayer.d.ts +4 -0
  92. package/dist/rpc-server/publicConfigLayer.d.ts.map +1 -0
  93. package/dist/rpc-server/publicConfigLayer.js +9 -0
  94. package/dist/rpc-server/publicConfigLayer.js.map +1 -0
  95. package/dist/runtime/makePlatform.d.ts +4 -0
  96. package/dist/runtime/makePlatform.d.ts.map +1 -0
  97. package/dist/runtime/makePlatform.js +65 -0
  98. package/dist/runtime/makePlatform.js.map +1 -0
  99. package/dist/runtime/types.d.ts +19 -0
  100. package/dist/runtime/types.d.ts.map +1 -0
  101. package/dist/runtime/types.js +2 -0
  102. package/dist/runtime/types.js.map +1 -0
  103. package/dist/safeOtlpSerialization.d.ts +6 -0
  104. package/dist/safeOtlpSerialization.d.ts.map +1 -0
  105. package/dist/safeOtlpSerialization.js +67 -0
  106. package/dist/safeOtlpSerialization.js.map +1 -0
  107. package/dist/schemaSensitiveKeys.d.ts +5 -0
  108. package/dist/schemaSensitiveKeys.d.ts.map +1 -0
  109. package/dist/schemaSensitiveKeys.js +143 -0
  110. package/dist/schemaSensitiveKeys.js.map +1 -0
  111. package/dist/setupDatabases.d.ts +3 -0
  112. package/dist/setupDatabases.d.ts.map +1 -0
  113. package/dist/setupDatabases.js +12 -0
  114. package/dist/setupDatabases.js.map +1 -0
  115. package/dist/tracedGroup.d.ts +4 -0
  116. package/dist/tracedGroup.d.ts.map +1 -0
  117. package/dist/tracedGroup.js +13 -0
  118. package/dist/tracedGroup.js.map +1 -0
  119. package/package.json +110 -3
  120. package/src/AppHostFactory.ts +121 -0
  121. package/src/AppHostInput.ts +74 -0
  122. package/src/MigrationOptions.ts +15 -0
  123. package/src/RedactionPolicy.ts +4 -0
  124. package/src/appHost.ts +117 -0
  125. package/src/appHostHttp.ts +147 -0
  126. package/src/boundedRpcRequest.ts +28 -0
  127. package/src/databaseAddress.ts +25 -0
  128. package/src/databaseLayer.ts +127 -0
  129. package/src/development.ts +34 -0
  130. package/src/healthRoute.ts +27 -0
  131. package/src/hostChanges.ts +34 -0
  132. package/src/hostOptions.ts +30 -0
  133. package/src/hostTransport.ts +26 -0
  134. package/src/nativeClientOrigins.ts +44 -0
  135. package/src/node-http/subscriptionSignal.ts +64 -0
  136. package/src/otlpHttpClient.ts +54 -0
  137. package/src/otlpTracing.ts +61 -0
  138. package/src/redactOtlpUrl.ts +48 -0
  139. package/src/redactingDiagnostics.ts +33 -0
  140. package/src/redactingRpcSerialization.ts +85 -0
  141. package/src/redactingTracer.ts +37 -0
  142. package/src/rpc-server/publicConfigLayer.ts +12 -0
  143. package/src/runtime/makePlatform.ts +75 -0
  144. package/src/runtime/types.ts +24 -0
  145. package/src/safeOtlpSerialization.ts +73 -0
  146. package/src/schemaSensitiveKeys.ts +148 -0
  147. package/src/setupDatabases.ts +12 -0
  148. package/src/tracedGroup.ts +14 -0
@@ -0,0 +1,121 @@
1
+ import type { Effect, Layer } from "effect";
2
+ import type { Rpc } from "effect/rpc";
3
+ import type { SessionShape } from "@shivaedev/identity/server/session.ts";
4
+ import type { HostPreflight } from "#AppHostInput.ts";
5
+ import type { AppDatabaseOptions, AppHostOptions, AppHttpHostOptions, AppLocalHostOptions } from "#appHost.ts";
6
+ import type { appHostHttp, deferredHost } from "#appHostHttp.ts";
7
+
8
+ type PreflightHost<THost extends Layer.Any, TServices, TError, TRequirements> = Layer.Layer<
9
+ Layer.Success<THost> | TServices,
10
+ Layer.Error<THost> | TError,
11
+ Exclude<Layer.Services<THost>, TServices> | TRequirements
12
+ >;
13
+
14
+ export interface AppHostFactory {
15
+ <
16
+ TRpcs extends Rpc.Any,
17
+ THandlers,
18
+ THandlerError,
19
+ THandlerRequirements,
20
+ TServices = never,
21
+ TServiceError = never,
22
+ TServiceRequirements = never,
23
+ TMigrations = never,
24
+ TSession extends SessionShape = SessionShape,
25
+ TApplicationError = never,
26
+ TApplicationRequirements = never,
27
+ TPreflight = never,
28
+ TPreflightError = never,
29
+ TPreflightRequirements = never,
30
+ >(
31
+ options: HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
32
+ (
33
+ | (AppDatabaseOptions<TMigrations> & {
34
+ readonly application: Effect.Effect<
35
+ AppHttpHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TSession>,
36
+ TApplicationError,
37
+ TApplicationRequirements
38
+ >;
39
+ })
40
+ | AppHostOptions<
41
+ TRpcs,
42
+ THandlers,
43
+ THandlerError,
44
+ THandlerRequirements,
45
+ TServices,
46
+ TServiceError,
47
+ TServiceRequirements,
48
+ TMigrations,
49
+ TSession,
50
+ TPreflight,
51
+ TPreflightError,
52
+ TPreflightRequirements
53
+ >
54
+ | AppLocalHostOptions<
55
+ TRpcs,
56
+ THandlers,
57
+ THandlerError,
58
+ THandlerRequirements,
59
+ TServices,
60
+ TServiceError,
61
+ TServiceRequirements,
62
+ TMigrations,
63
+ TPreflight,
64
+ TPreflightError,
65
+ TPreflightRequirements
66
+ >
67
+ ),
68
+ ): PreflightHost<
69
+ ReturnType<
70
+ typeof deferredHost<
71
+ TRpcs,
72
+ THandlers,
73
+ THandlerError,
74
+ THandlerRequirements,
75
+ TServices,
76
+ TServiceError,
77
+ TServiceRequirements,
78
+ TMigrations,
79
+ TSession,
80
+ TApplicationError,
81
+ TApplicationRequirements
82
+ >
83
+ >,
84
+ NoInfer<TPreflight>,
85
+ NoInfer<TPreflightError>,
86
+ NoInfer<TPreflightRequirements>
87
+ >;
88
+
89
+ <
90
+ TRpcs extends Rpc.Any,
91
+ THandlers,
92
+ THandlerError,
93
+ THandlerRequirements,
94
+ TServices = never,
95
+ TServiceError = never,
96
+ TServiceRequirements = never,
97
+ TSession extends SessionShape = SessionShape,
98
+ TPreflight = never,
99
+ TPreflightError = never,
100
+ TPreflightRequirements = never,
101
+ >(
102
+ options: AppHttpHostOptions<
103
+ TRpcs,
104
+ THandlers,
105
+ THandlerError,
106
+ THandlerRequirements,
107
+ TServices,
108
+ TServiceError,
109
+ TServiceRequirements,
110
+ TSession,
111
+ TPreflight,
112
+ TPreflightError,
113
+ TPreflightRequirements
114
+ >,
115
+ ): PreflightHost<
116
+ ReturnType<typeof appHostHttp<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TSession>>,
117
+ NoInfer<TPreflight>,
118
+ NoInfer<TPreflightError>,
119
+ NoInfer<TPreflightRequirements>
120
+ >;
121
+ }
@@ -0,0 +1,74 @@
1
+ import type { Effect, Layer } from "effect";
2
+ import type { Rpc } from "effect/rpc";
3
+ import type { SessionShape } from "@shivaedev/identity/server/session.ts";
4
+ import type { AppDatabaseOptions, AppHostOptions, AppHttpHostOptions, AppLocalHostOptions } from "#appHost.ts";
5
+
6
+ export type HostPreflight<TServices = never, TError = never, TRequirements = never> =
7
+ | { readonly preflight: Layer.Layer<TServices, TError, TRequirements> }
8
+ | ([TServices | TError | TRequirements] extends [never] ? { readonly preflight?: never } : never);
9
+
10
+ export type AppHostInput<
11
+ TRpcs extends Rpc.Any,
12
+ THandlers,
13
+ THandlerError,
14
+ THandlerRequirements,
15
+ TServices = never,
16
+ TServiceError = never,
17
+ TServiceRequirements = never,
18
+ TMigrations = never,
19
+ TSession extends SessionShape = SessionShape,
20
+ TApplicationError = never,
21
+ TApplicationRequirements = never,
22
+ TPreflight = never,
23
+ TPreflightError = never,
24
+ TPreflightRequirements = never,
25
+ > = HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
26
+ (
27
+ | (AppDatabaseOptions<TMigrations> & {
28
+ readonly application: Effect.Effect<
29
+ AppHttpHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TSession>,
30
+ TApplicationError,
31
+ TApplicationRequirements
32
+ >;
33
+ })
34
+ | AppHostOptions<
35
+ TRpcs,
36
+ THandlers,
37
+ THandlerError,
38
+ THandlerRequirements,
39
+ TServices,
40
+ TServiceError,
41
+ TServiceRequirements,
42
+ TMigrations,
43
+ TSession,
44
+ TPreflight,
45
+ TPreflightError,
46
+ TPreflightRequirements
47
+ >
48
+ | AppLocalHostOptions<
49
+ TRpcs,
50
+ THandlers,
51
+ THandlerError,
52
+ THandlerRequirements,
53
+ TServices,
54
+ TServiceError,
55
+ TServiceRequirements,
56
+ TMigrations,
57
+ TPreflight,
58
+ TPreflightError,
59
+ TPreflightRequirements
60
+ >
61
+ | AppHttpHostOptions<
62
+ TRpcs,
63
+ THandlers,
64
+ THandlerError,
65
+ THandlerRequirements,
66
+ TServices,
67
+ TServiceError,
68
+ TServiceRequirements,
69
+ TSession,
70
+ TPreflight,
71
+ TPreflightError,
72
+ TPreflightRequirements
73
+ >
74
+ );
@@ -0,0 +1,15 @@
1
+ import type { Duration } from "effect";
2
+ import type { Migrator } from "effect/sql";
3
+ import type { SchemaComponent } from "@shivaedev/sql/SchemaComponent.ts";
4
+
5
+ export interface MigrationOptions<R = never> {
6
+ readonly components?: readonly SchemaComponent<R>[];
7
+ readonly loader: Migrator.Loader<R>;
8
+ readonly lockTimeout?: Duration.Input;
9
+ readonly mode?: "migrate" | "check" | "wait";
10
+ readonly pollInterval?: Duration.Input;
11
+ readonly progressInterval?: Duration.Input;
12
+ readonly release?: string;
13
+ readonly table?: string;
14
+ readonly waitTimeout?: Duration.Input;
15
+ }
@@ -0,0 +1,4 @@
1
+ import { Context } from "effect";
2
+ import { isSensitiveKey, type SensitiveKey } from "@shivaedev/errors/sensitive.ts";
3
+
4
+ export const RedactionPolicy = Context.Reference<SensitiveKey>("@shivaedev/server/RedactionPolicy", { defaultValue: () => isSensitiveKey });
package/src/appHost.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { Context, type Effect, Layer } from "effect";
2
+ import type { Rpc } from "effect/rpc";
3
+ import type { makeLiveServer } from "@shivaedev/contract/makeLiveServer.ts";
4
+ import type { SessionPolicy, SessionShape } from "@shivaedev/identity/server/session.ts";
5
+ import type { AppHostFactory } from "#AppHostFactory.ts";
6
+ import type { AppHostInput, HostPreflight } from "#AppHostInput.ts";
7
+ import { appHostHttp, deferredHost, migratedHost } from "#appHostHttp.ts";
8
+ import { type DatabaseConfig, databaseLayer } from "#databaseLayer.ts";
9
+ import type { HostOptions, HostPolicy } from "#hostOptions.ts";
10
+ import type { MigrationOptions } from "#MigrationOptions.ts";
11
+
12
+ export class AppChanges extends Context.Service<AppChanges, Effect.Success<typeof makeLiveServer>>()("@shivaedev/server/AppChanges") {}
13
+ export interface AppDatabaseOptions<R = never> {
14
+ readonly config?: DatabaseConfig;
15
+ readonly migrations: MigrationOptions<R>;
16
+ }
17
+
18
+ export function appDatabase<R>(options: AppDatabaseOptions<R>) {
19
+ return databaseLayer(options, AppChanges);
20
+ }
21
+
22
+ export type AppHostOptions<
23
+ TRpcs extends Rpc.Any,
24
+ THandlers,
25
+ THandlerError,
26
+ THandlerRequirements,
27
+ TServices = never,
28
+ TServiceError = never,
29
+ TServiceRequirements = never,
30
+ TMigrations = never,
31
+ TSession extends SessionShape = SessionShape,
32
+ TPreflight = never,
33
+ TPreflightError = never,
34
+ TPreflightRequirements = never,
35
+ > = HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
36
+ HostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> & {
37
+ readonly migrations: AppDatabaseOptions<TMigrations>["migrations"];
38
+ readonly policy?: undefined;
39
+ readonly sessions: SessionPolicy<TSession>;
40
+ };
41
+
42
+ export type AppLocalHostOptions<
43
+ TRpcs extends Rpc.Any,
44
+ THandlers,
45
+ THandlerError,
46
+ THandlerRequirements,
47
+ TServices = never,
48
+ TServiceError = never,
49
+ TServiceRequirements = never,
50
+ TMigrations = never,
51
+ TPreflight = never,
52
+ TPreflightError = never,
53
+ TPreflightRequirements = never,
54
+ > = HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
55
+ HostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> & {
56
+ readonly migrations: AppDatabaseOptions<TMigrations>["migrations"];
57
+ readonly policy: "loopback";
58
+ readonly sessions?: never;
59
+ };
60
+
61
+ export type AppHttpHostOptions<
62
+ TRpcs extends Rpc.Any,
63
+ THandlers,
64
+ THandlerError,
65
+ THandlerRequirements,
66
+ TServices = never,
67
+ TServiceError = never,
68
+ TServiceRequirements = never,
69
+ TSession extends SessionShape = SessionShape,
70
+ TPreflight = never,
71
+ TPreflightError = never,
72
+ TPreflightRequirements = never,
73
+ > = HostPreflight<TPreflight, TPreflightError, TPreflightRequirements> &
74
+ HostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> &
75
+ HostPolicy<TSession> & { readonly migrations?: undefined };
76
+ export const appHost: AppHostFactory = <
77
+ TRpcs extends Rpc.Any,
78
+ THandlers,
79
+ THandlerError,
80
+ THandlerRequirements,
81
+ TServices = never,
82
+ TServiceError = never,
83
+ TServiceRequirements = never,
84
+ TMigrations = never,
85
+ TSession extends SessionShape = SessionShape,
86
+ TApplicationError = never,
87
+ TApplicationRequirements = never,
88
+ TPreflight = never,
89
+ TPreflightError = never,
90
+ TPreflightRequirements = never,
91
+ >(
92
+ options: AppHostInput<
93
+ TRpcs,
94
+ THandlers,
95
+ THandlerError,
96
+ THandlerRequirements,
97
+ TServices,
98
+ TServiceError,
99
+ TServiceRequirements,
100
+ TMigrations,
101
+ TSession,
102
+ TApplicationError,
103
+ TApplicationRequirements,
104
+ TPreflight,
105
+ TPreflightError,
106
+ TPreflightRequirements
107
+ >,
108
+ ) => {
109
+ if ("application" in options) {
110
+ return deferredHost(options.application, appDatabase(options), AppChanges).pipe(Layer.provideMerge(options.preflight ?? Layer.empty));
111
+ }
112
+ return options.migrations === undefined
113
+ ? appHostHttp(options, AppChanges).pipe(Layer.provideMerge(options.preflight ?? Layer.empty))
114
+ : migratedHost({ ...options, migrations: options.migrations }, appDatabase({ ...options, migrations: options.migrations }), AppChanges).pipe(
115
+ Layer.provideMerge(options.preflight ?? Layer.empty),
116
+ );
117
+ };
@@ -0,0 +1,147 @@
1
+ import { createServer } from "node:http";
2
+ import { NodeHttpServer, NodeServices } from "@effect/platform-node";
3
+ import { ByteSize, Config, Effect, Layer } from "effect";
4
+ import { HttpEffect, HttpIncomingMessage, HttpRouter, HttpServerRequest, HttpServerResponse, HttpStaticServer } from "effect/http";
5
+ import { type Rpc, RpcSerialization, RpcServer } from "effect/rpc";
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";
10
+ import type { AppChanges, AppHttpHostOptions } from "#appHost.ts";
11
+ import { boundedRpcRequest } from "#boundedRpcRequest.ts";
12
+ import type { databaseLayer } from "#databaseLayer.ts";
13
+ import { healthRoute } from "#healthRoute.ts";
14
+ import { hostChanges } from "#hostChanges.ts";
15
+ import type { HostOptions, HostPolicy } from "#hostOptions.ts";
16
+ import { hostTransport } from "#hostTransport.ts";
17
+ import type { MigrationOptions } from "#MigrationOptions.ts";
18
+ import { nativeClientOrigins } from "#nativeClientOrigins.ts";
19
+ import { otlpTracing } from "#otlpTracing.ts";
20
+ import { RedactionPolicy } from "#RedactionPolicy.ts";
21
+ import { redactingDiagnostics } from "#redactingDiagnostics.ts";
22
+ import { redactingRpcSerialization } from "#redactingRpcSerialization.ts";
23
+ 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
+
29
+ export function appHostHttp<
30
+ TRpcs extends Rpc.Any,
31
+ THandlers,
32
+ THandlerError,
33
+ THandlerRequirements,
34
+ TServices,
35
+ TServiceError,
36
+ TServiceRequirements,
37
+ TSession extends SessionShape,
38
+ >(
39
+ options: HostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> & HostPolicy<TSession>,
40
+ changesService: typeof AppChanges,
41
+ ) {
42
+ const sensitive = schemaSensitiveKeys(options.group, options.config?.sensitiveKeys);
43
+ return Layer.unwrap(
44
+ Effect.gen(function* () {
45
+ const { bodySize, trustedProxy, nativeOrigins } = yield* hostTransport(options);
46
+ const middleware: Layer.Layer<RequestTracing> | Layer.Layer<Authenticated | MaybeAuthenticated | RequestTracing> =
47
+ options.policy === "loopback"
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
+ );
74
+ const assets =
75
+ options.assets === undefined
76
+ ? Layer.empty
77
+ : HttpStaticServer.layer(typeof options.assets === "string" ? { root: options.assets } : options.assets);
78
+ const application = Layer.mergeAll(options.services ?? Layer.empty);
79
+ const server = Layer.unwrap(
80
+ Effect.gen(function* () {
81
+ const port = options.config?.port ?? (yield* Config.Int("PORT").pipe(Config.withDefault(3000)));
82
+ const host = options.config?.host ?? (yield* Config.String("HOST").pipe(Config.withDefault("127.0.0.1")));
83
+ return NodeHttpServer.layer(createServer, {
84
+ gracefulShutdownTimeout: options.config?.gracefulShutdownTimeout,
85
+ host,
86
+ port,
87
+ ...(options.config?.disablePreemptiveShutdown === undefined
88
+ ? {}
89
+ : { disablePreemptiveShutdown: options.config.disablePreemptiveShutdown }),
90
+ });
91
+ }),
92
+ );
93
+ const applicationRoutes = (options.routes ?? Layer.empty).pipe(
94
+ Layer.provide(options.nativeOrigins === undefined ? Layer.empty : nativeClientOrigins(nativeOrigins, trustedProxy, true)),
95
+ );
96
+ return HttpRouter.serve(Layer.mergeAll(routes, healthRoute, assets, applicationRoutes), { disableLogger: true }).pipe(
97
+ Layer.provideMerge(application),
98
+ Layer.provideMerge(server),
99
+ Layer.provide(NodeServices.layer),
100
+ );
101
+ }),
102
+ ).pipe(Layer.provide(redactingDiagnostics(sensitive)), Layer.provide(otlpTracing), Layer.provide(Layer.succeed(RedactionPolicy, sensitive)));
103
+ }
104
+
105
+ export function migratedHost<
106
+ TRpcs extends Rpc.Any,
107
+ THandlers,
108
+ THandlerError,
109
+ THandlerRequirements,
110
+ TServices,
111
+ TServiceError,
112
+ TServiceRequirements,
113
+ TMigrations,
114
+ TSession extends SessionShape,
115
+ >(
116
+ options: HostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> &
117
+ HostPolicy<TSession> & { readonly migrations: MigrationOptions<TMigrations> },
118
+ database: ReturnType<typeof databaseLayer<TMigrations>>,
119
+ changesService: typeof AppChanges,
120
+ ) {
121
+ const services = Layer.mergeAll(options.services ?? Layer.empty);
122
+ return appHostHttp({ ...options, services: services.pipe(Layer.provideMerge(database)) }, changesService);
123
+ }
124
+
125
+ export function deferredHost<
126
+ TRpcs extends Rpc.Any,
127
+ THandlers,
128
+ THandlerError,
129
+ THandlerRequirements,
130
+ TServices,
131
+ TServiceError,
132
+ TServiceRequirements,
133
+ TMigrations,
134
+ TSession extends SessionShape,
135
+ TApplicationError,
136
+ TApplicationRequirements,
137
+ >(
138
+ application: Effect.Effect<
139
+ AppHttpHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TSession>,
140
+ TApplicationError,
141
+ TApplicationRequirements
142
+ >,
143
+ database: ReturnType<typeof databaseLayer<TMigrations>>,
144
+ changesService: typeof AppChanges,
145
+ ) {
146
+ return Layer.unwrap(Effect.map(application, (options) => appHostHttp(options, changesService))).pipe(Layer.provideMerge(database));
147
+ }
@@ -0,0 +1,28 @@
1
+ import { Effect } from "effect";
2
+ import { HttpServerError, type HttpServerRequest } from "effect/http";
3
+ import { readBoundedBody } from "@shivaedev/http/server/boundedBody.ts";
4
+
5
+ export function boundedRpcRequest(request: HttpServerRequest.HttpServerRequest): HttpServerRequest.HttpServerRequest {
6
+ const arrayBuffer = readBoundedBody(request).pipe(
7
+ Effect.mapError((cause) =>
8
+ cause._tag === "RequestBodyTooLarge"
9
+ ? new HttpServerError.HttpServerError({ reason: new HttpServerError.RequestParseError({ cause, request }) })
10
+ : cause,
11
+ ),
12
+ Effect.cached,
13
+ Effect.runSync,
14
+ );
15
+ const text = Effect.map(arrayBuffer, (body) => new TextDecoder().decode(body));
16
+ // The native text reader destroys the HTTP socket on overflow, preventing a 4xx response.
17
+ return new Proxy(request, {
18
+ get: (target, key, receiver) => {
19
+ if (key === "text") {
20
+ return text;
21
+ }
22
+ if (key === "arrayBuffer") {
23
+ return arrayBuffer;
24
+ }
25
+ return Reflect.get(target, key, receiver);
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,25 @@
1
+ import type { PgPoolConfig } from "@effect/sql-pg/PgClient";
2
+ import { Redacted } from "effect";
3
+
4
+ export function databaseAddress(value: Redacted.Redacted<string>): Pick<PgPoolConfig, "host" | "port"> {
5
+ try {
6
+ const url = new URL(Redacted.value(value));
7
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
8
+ return {};
9
+ }
10
+ const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : decodeURIComponent(url.hostname);
11
+ // PgConnection applies query parameters in order, after the authority.
12
+ const host = url.searchParams.getAll("host").at(-1) ?? hostname;
13
+ const rawPort = url.searchParams.getAll("port").at(-1) ?? url.port;
14
+ const port = rawPort === "" ? undefined : Number(rawPort);
15
+ if (host.includes(",") || (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535))) {
16
+ return {};
17
+ }
18
+ return {
19
+ ...(host === "" && !url.searchParams.has("host") ? {} : { host }),
20
+ ...(port === undefined ? {} : { port }),
21
+ };
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
@@ -0,0 +1,127 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createConnection } from "node:net";
3
+ import * as PgClient from "@effect/sql-pg/PgClient";
4
+ import { Config, Duration, Effect, Fiber, Layer, Redacted, Schedule } from "effect";
5
+ import * as Reactivity from "effect/reactivity/Reactivity";
6
+ import { makeLiveServer } from "@shivaedev/contract/makeLiveServer.ts";
7
+ import { assertPostgresMigrations, postgresMigrationError } from "@shivaedev/sql/assertPostgresMigrations.ts";
8
+ import type { SqlNameTransforms } from "@shivaedev/sql/clientOptions.ts";
9
+ import { coordinateWorkerRelease } from "@shivaedev/sql/coordinateWorkerRelease.ts";
10
+ import { migratePostgres } from "@shivaedev/sql/migrations.ts";
11
+ import { type PendingPostgresMigrations, pendingPostgresMigrations } from "@shivaedev/sql/pendingPostgresMigrations.ts";
12
+ import { type PostgresChangesOptions, postgresChanges } from "@shivaedev/sql/postgresChanges.ts";
13
+ import { WorkerRelease } from "@shivaedev/sql/WorkerRelease.ts";
14
+ import type { AppChanges, AppDatabaseOptions } from "#appHost.ts";
15
+ import { databaseAddress } from "./databaseAddress.ts";
16
+
17
+ export interface DatabaseConfig extends SqlNameTransforms {
18
+ readonly applicationName?: string;
19
+ readonly changes?: Partial<Omit<PostgresChangesOptions, "client" | "resync">>;
20
+ readonly databaseUrl?: Redacted.Redacted<string>;
21
+ readonly maxConnections?: number;
22
+ }
23
+
24
+ export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService: typeof AppChanges) {
25
+ const pool = Layer.unwrap(
26
+ Effect.gen(function* () {
27
+ const url = options.config?.databaseUrl ?? (yield* Config.Redacted("DATABASE_URL"));
28
+ const address = databaseAddress(url);
29
+ const disconnected = new Set<Promise<void>>();
30
+ // Native pool shutdown sends Terminate without waiting for its sockets to close.
31
+ yield* Effect.addFinalizer(() => Effect.promise(() => Promise.all([...disconnected])));
32
+ return PgClient.layer({
33
+ stream: () => {
34
+ const target = new URL(Redacted.value(url));
35
+ const host = address.host ?? target.searchParams.getAll("host").at(-1) ?? (decodeURIComponent(target.hostname) || "localhost");
36
+ const port = address.port ?? 5432;
37
+ const socket = createConnection(host.startsWith("/") ? { path: `${host}/.s.PGSQL.${port}` } : { host, noDelay: true, port });
38
+ const closed = new Promise<void>((resolve) =>
39
+ socket.once("close", () => {
40
+ disconnected.delete(closed);
41
+ resolve();
42
+ }),
43
+ );
44
+ disconnected.add(closed);
45
+ return socket;
46
+ },
47
+ transformQueryNames: options.config?.transformQueryNames,
48
+ transformResultNames: options.config?.transformResultNames,
49
+ url,
50
+ ...address,
51
+ ...(options.config?.maxConnections === undefined ? {} : { maxConnections: options.config.maxConnections }),
52
+ ...(options.config?.applicationName === undefined ? {} : { applicationName: options.config.applicationName }),
53
+ });
54
+ }),
55
+ );
56
+ const prepared = Layer.effectDiscard(prepareMigrations(options.migrations)).pipe(Layer.provideMerge(pool));
57
+ const live = Layer.effect(changesService, makeLiveServer).pipe(Layer.provideMerge(Reactivity.layer));
58
+ const migrations = options.migrations;
59
+ const worker =
60
+ migrations.mode === "wait" && migrations.release !== undefined
61
+ ? Layer.effect(WorkerRelease, coordinateWorkerRelease(migrations.release, migrations.pollInterval, migrations.table))
62
+ : Layer.empty;
63
+ const base = Layer.merge(worker.pipe(Layer.provideMerge(prepared)), live);
64
+ const changes = options.config?.changes;
65
+ return Layer.unwrap(
66
+ Effect.gen(function* () {
67
+ const client = yield* PgClient.PgClient;
68
+ const hints = yield* changesService;
69
+ const url = options.config?.databaseUrl ?? (yield* Config.Redacted("DATABASE_URL"));
70
+ const name = yield* Config.String("APP_NAME").pipe(Config.withDefault(new URL(Redacted.value(url)).pathname));
71
+ const channel = changes?.channel ?? `changes_${createHash("sha256").update(name).digest("hex").slice(0, 32)}`;
72
+ return postgresChanges({ ...changes, channel, client, resync: hints.resync });
73
+ }),
74
+ ).pipe(Layer.provideMerge(base));
75
+ }
76
+
77
+ function prepareMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
78
+ switch (migrations.mode) {
79
+ case "check": {
80
+ const schema = { ...migrations };
81
+ delete schema.release;
82
+ return assertPostgresMigrations(schema);
83
+ }
84
+ case "wait":
85
+ return waitForMigrations(migrations);
86
+ default:
87
+ return migratePostgres({ ...migrations, lockTimeout: migrations.lockTimeout ?? "5 seconds" }).pipe(Effect.asVoid);
88
+ }
89
+ }
90
+
91
+ function waitForMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
92
+ return Effect.gen(function* () {
93
+ const progressInterval = Duration.fromInputUnsafe(migrations.progressInterval ?? "30 seconds");
94
+ let pending: PendingPostgresMigrations = { components: [], migrations: [] };
95
+ let progress: Fiber.Fiber<never> | undefined;
96
+ function log(message: string) {
97
+ return Effect.suspend(() =>
98
+ Effect.logInfo(message).pipe(Effect.annotateLogs({ pendingComponents: pending.components, pendingMigrations: pending.migrations })),
99
+ );
100
+ }
101
+ yield* Effect.gen(function* () {
102
+ pending = yield* pendingPostgresMigrations(migrations);
103
+ if (pending.migrations.length === 0 && pending.components.length === 0) {
104
+ return;
105
+ }
106
+ if (progress === undefined) {
107
+ yield* log("Waiting for database migrations");
108
+ progress = yield* Effect.forever(Effect.sleep(progressInterval).pipe(Effect.andThen(log("Still waiting for database migrations")))).pipe(
109
+ Effect.forkScoped,
110
+ );
111
+ }
112
+ return yield* Effect.fail(postgresMigrationError(pending));
113
+ }).pipe(
114
+ Effect.retry({
115
+ schedule: Schedule.max([Schedule.spaced(migrations.pollInterval ?? "1 second"), Schedule.during(migrations.waitTimeout ?? "5 minutes")]),
116
+ while: (error) => error._tag === "MigrationError" && error.kind === "BadState",
117
+ }),
118
+ Effect.tapError((error) =>
119
+ error._tag === "MigrationError" && error.kind === "BadState" ? log("Timed out waiting for database migrations") : Effect.void,
120
+ ),
121
+ );
122
+ if (progress !== undefined) {
123
+ yield* Fiber.interrupt(progress);
124
+ yield* Effect.logInfo("Database migrations current");
125
+ }
126
+ }).pipe(Effect.scoped);
127
+ }