@shivaedev/platform 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +371 -112
- package/dist/errors/rejectedField.d.ts +7 -0
- package/dist/errors/rejectedField.d.ts.map +1 -0
- package/dist/errors/{rejected-field.js → rejectedField.js} +4 -2
- package/dist/errors/rejectedField.js.map +1 -0
- package/dist/errors/taxonomy.d.ts +10 -0
- package/dist/errors/taxonomy.d.ts.map +1 -1
- package/dist/errors/taxonomy.js +4 -0
- package/dist/errors/taxonomy.js.map +1 -1
- package/dist/rpc/identity.d.ts.map +1 -1
- package/dist/rpc/identity.js.map +1 -1
- package/dist/rpc/middleware.d.ts +1 -1
- package/dist/rpc/middleware.d.ts.map +1 -1
- package/dist/rpc/middleware.js +1 -1
- package/dist/rpc/middleware.js.map +1 -1
- package/dist/server/appHost.d.ts +61 -0
- package/dist/server/appHost.d.ts.map +1 -0
- package/dist/server/appHost.js +14 -0
- package/dist/server/appHost.js.map +1 -0
- package/dist/server/appHostHttp.d.ts +11 -0
- package/dist/server/appHostHttp.d.ts.map +1 -0
- package/dist/server/appHostHttp.js +51 -0
- package/dist/server/appHostHttp.js.map +1 -0
- package/dist/{better-auth → server/better-auth}/adapter.d.ts +1 -1
- package/dist/server/better-auth/adapter.d.ts.map +1 -0
- package/dist/{better-auth → server/better-auth}/adapter.js +26 -14
- package/dist/server/better-auth/adapter.js.map +1 -0
- package/dist/{better-auth → server/better-auth}/relation.d.ts +15 -15
- package/dist/server/better-auth/relation.d.ts.map +1 -0
- package/dist/{better-auth → server/better-auth}/relation.js +2 -2
- package/dist/server/better-auth/relation.js.map +1 -0
- package/dist/server/better-auth/rowAdapter.d.ts +8 -0
- package/dist/server/better-auth/rowAdapter.d.ts.map +1 -0
- package/dist/server/better-auth/rowAdapter.js +52 -0
- package/dist/server/better-auth/rowAdapter.js.map +1 -0
- package/dist/{better-auth → server/better-auth}/where.d.ts +1 -1
- package/dist/server/better-auth/where.d.ts.map +1 -0
- package/dist/{better-auth → server/better-auth}/where.js +22 -18
- package/dist/server/better-auth/where.js.map +1 -0
- package/dist/server/boundedRpcRequest.d.ts +3 -0
- package/dist/server/boundedRpcRequest.d.ts.map +1 -0
- package/dist/server/boundedRpcRequest.js +41 -0
- package/dist/server/boundedRpcRequest.js.map +1 -0
- package/dist/server/databaseLayer.d.ts +6 -0
- package/dist/server/databaseLayer.d.ts.map +1 -0
- package/dist/server/databaseLayer.js +29 -0
- package/dist/server/databaseLayer.js.map +1 -0
- package/dist/server/hostOptions.d.ts +28 -0
- package/dist/server/hostOptions.d.ts.map +1 -0
- package/dist/server/hostOptions.js +2 -0
- package/dist/server/hostOptions.js.map +1 -0
- package/dist/{node-http/subscription-signal.d.ts → server/node-http/subscriptionSignal.d.ts} +2 -2
- package/dist/server/node-http/subscriptionSignal.d.ts.map +1 -0
- package/dist/{node-http/subscription-signal.js → server/node-http/subscriptionSignal.js} +20 -13
- package/dist/server/node-http/subscriptionSignal.js.map +1 -0
- package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts +4 -0
- package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts.map +1 -0
- package/dist/server/rpc-server/adapters/betterAuthSessions.js +14 -0
- package/dist/server/rpc-server/adapters/betterAuthSessions.js.map +1 -0
- package/dist/server/rpc-server/boundary.d.ts +9 -0
- package/dist/server/rpc-server/boundary.d.ts.map +1 -0
- package/dist/server/rpc-server/boundary.js +18 -0
- package/dist/server/rpc-server/boundary.js.map +1 -0
- package/dist/server/rpc-server/local.d.ts +7 -0
- package/dist/server/rpc-server/local.d.ts.map +1 -0
- package/dist/server/rpc-server/local.js +70 -0
- package/dist/server/rpc-server/local.js.map +1 -0
- package/dist/{rpc-server → server/rpc-server}/origin.d.ts +3 -3
- package/dist/server/rpc-server/origin.d.ts.map +1 -0
- package/dist/server/rpc-server/origin.js +16 -0
- package/dist/server/rpc-server/origin.js.map +1 -0
- package/dist/server/rpc-server/redact.d.ts +3 -0
- package/dist/server/rpc-server/redact.d.ts.map +1 -0
- package/dist/{rpc-server → server/rpc-server}/redact.js +9 -7
- package/dist/server/rpc-server/redact.js.map +1 -0
- package/dist/server/rpc-server/redactCause.d.ts +6 -0
- package/dist/server/rpc-server/redactCause.d.ts.map +1 -0
- package/dist/server/rpc-server/redactCause.js +36 -0
- package/dist/server/rpc-server/redactCause.js.map +1 -0
- package/dist/server/rpc-server/sensitive.d.ts +5 -0
- package/dist/server/rpc-server/sensitive.d.ts.map +1 -0
- package/dist/{rpc-server → server/rpc-server}/sensitive.js +21 -17
- package/dist/server/rpc-server/sensitive.js.map +1 -0
- package/dist/server/rpc-server/session.d.ts +21 -0
- package/dist/server/rpc-server/session.d.ts.map +1 -0
- package/dist/server/rpc-server/session.js +37 -0
- package/dist/server/rpc-server/session.js.map +1 -0
- package/dist/{rpc-server → server/rpc-server}/tracing.d.ts +1 -1
- package/dist/server/rpc-server/tracing.d.ts.map +1 -0
- package/dist/server/rpc-server/tracing.js +42 -0
- package/dist/server/rpc-server/tracing.js.map +1 -0
- package/dist/server/rpc-server/transportHeaders.d.ts +4 -0
- package/dist/server/rpc-server/transportHeaders.d.ts.map +1 -0
- package/dist/server/rpc-server/transportHeaders.js +6 -0
- package/dist/server/rpc-server/transportHeaders.js.map +1 -0
- package/dist/server/rpc-server/trustedProxy.d.ts +6 -0
- package/dist/server/rpc-server/trustedProxy.d.ts.map +1 -0
- package/dist/server/rpc-server/trustedProxy.js +35 -0
- package/dist/server/rpc-server/trustedProxy.js.map +1 -0
- package/dist/server/runtime/makePlatform.d.ts +4 -0
- package/dist/server/runtime/makePlatform.d.ts.map +1 -0
- package/dist/{runtime/make.js → server/runtime/makePlatform.js} +11 -9
- package/dist/server/runtime/makePlatform.js.map +1 -0
- package/dist/server/runtime/types.d.ts +19 -0
- package/dist/server/runtime/types.d.ts.map +1 -0
- package/dist/{runtime → server/runtime}/types.js.map +1 -1
- package/dist/server/testing/makePlatformIt.d.ts +8 -0
- package/dist/server/testing/makePlatformIt.d.ts.map +1 -0
- package/dist/{testing/vitest.js → server/testing/makePlatformIt.js} +7 -3
- package/dist/server/testing/makePlatformIt.js.map +1 -0
- package/dist/server/testing/types.d.ts +28 -0
- package/dist/server/testing/types.d.ts.map +1 -0
- package/dist/{testing → server/testing}/types.js.map +1 -1
- package/docs/north-star.md +36 -0
- package/docs/roadmap.md +30 -0
- package/package.json +333 -32
- package/src/errors/{rejected-field.ts → rejectedField.ts} +3 -6
- package/src/errors/taxonomy.ts +2 -8
- package/src/rpc/identity.ts +0 -4
- package/src/rpc/middleware.ts +19 -10
- package/src/server/appHost.ts +130 -0
- package/src/server/appHostHttp.ts +101 -0
- package/src/{better-auth → server/better-auth}/adapter.ts +26 -30
- package/src/{better-auth → server/better-auth}/relation.ts +17 -19
- package/src/{better-auth/row-adapter.ts → server/better-auth/rowAdapter.ts} +19 -21
- package/src/{better-auth → server/better-auth}/where.ts +22 -32
- package/src/server/boundedRpcRequest.ts +47 -0
- package/src/server/databaseLayer.ts +36 -0
- package/src/server/hostOptions.ts +25 -0
- package/src/{node-http/subscription-signal.ts → server/node-http/subscriptionSignal.ts} +19 -22
- package/src/{rpc-server/adapters/better-auth-sessions.ts → server/rpc-server/adapters/betterAuthSessions.ts} +10 -11
- package/src/server/rpc-server/boundary.ts +26 -0
- package/src/server/rpc-server/local.ts +72 -0
- package/src/{rpc-server → server/rpc-server}/origin.ts +11 -14
- package/src/{rpc-server → server/rpc-server}/redact.ts +9 -11
- package/src/server/rpc-server/redactCause.ts +40 -0
- package/src/{rpc-server → server/rpc-server}/sensitive.ts +17 -22
- package/src/{rpc-server → server/rpc-server}/session.ts +23 -24
- package/src/{rpc-server → server/rpc-server}/tracing.ts +16 -19
- package/src/{rpc-server/transport.ts → server/rpc-server/transportHeaders.ts} +4 -4
- package/src/server/rpc-server/trustedProxy.ts +36 -0
- package/src/{runtime/make.ts → server/runtime/makePlatform.ts} +19 -27
- package/src/server/runtime/types.ts +24 -0
- package/src/{testing/vitest.ts → server/testing/makePlatformIt.ts} +27 -23
- package/src/server/testing/types.ts +38 -0
- package/dist/better-auth/adapter.d.ts.map +0 -1
- package/dist/better-auth/adapter.js.map +0 -1
- package/dist/better-auth/relation.d.ts.map +0 -1
- package/dist/better-auth/relation.js.map +0 -1
- package/dist/better-auth/row-adapter.d.ts +0 -8
- package/dist/better-auth/row-adapter.d.ts.map +0 -1
- package/dist/better-auth/row-adapter.js +0 -46
- package/dist/better-auth/row-adapter.js.map +0 -1
- package/dist/better-auth/where.d.ts.map +0 -1
- package/dist/better-auth/where.js.map +0 -1
- package/dist/errors/rejected-field.d.ts +0 -7
- package/dist/errors/rejected-field.d.ts.map +0 -1
- package/dist/errors/rejected-field.js.map +0 -1
- package/dist/node-http/subscription-signal.d.ts.map +0 -1
- package/dist/node-http/subscription-signal.js.map +0 -1
- package/dist/rpc-server/adapters/better-auth-sessions.d.ts +0 -4
- package/dist/rpc-server/adapters/better-auth-sessions.d.ts.map +0 -1
- package/dist/rpc-server/adapters/better-auth-sessions.js +0 -11
- package/dist/rpc-server/adapters/better-auth-sessions.js.map +0 -1
- package/dist/rpc-server/origin.d.ts.map +0 -1
- package/dist/rpc-server/origin.js +0 -12
- package/dist/rpc-server/origin.js.map +0 -1
- package/dist/rpc-server/redact-cause.d.ts +0 -6
- package/dist/rpc-server/redact-cause.d.ts.map +0 -1
- package/dist/rpc-server/redact-cause.js +0 -28
- package/dist/rpc-server/redact-cause.js.map +0 -1
- package/dist/rpc-server/redact.d.ts +0 -3
- package/dist/rpc-server/redact.d.ts.map +0 -1
- package/dist/rpc-server/redact.js.map +0 -1
- package/dist/rpc-server/sensitive.d.ts +0 -5
- package/dist/rpc-server/sensitive.d.ts.map +0 -1
- package/dist/rpc-server/sensitive.js.map +0 -1
- package/dist/rpc-server/session.d.ts +0 -21
- package/dist/rpc-server/session.d.ts.map +0 -1
- package/dist/rpc-server/session.js +0 -31
- package/dist/rpc-server/session.js.map +0 -1
- package/dist/rpc-server/tracing.d.ts.map +0 -1
- package/dist/rpc-server/tracing.js +0 -34
- package/dist/rpc-server/tracing.js.map +0 -1
- package/dist/rpc-server/transport.d.ts +0 -4
- package/dist/rpc-server/transport.d.ts.map +0 -1
- package/dist/rpc-server/transport.js +0 -4
- package/dist/rpc-server/transport.js.map +0 -1
- package/dist/runtime/make.d.ts +0 -4
- package/dist/runtime/make.d.ts.map +0 -1
- package/dist/runtime/make.js.map +0 -1
- package/dist/runtime/types.d.ts +0 -19
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/testing/types.d.ts +0 -28
- package/dist/testing/types.d.ts.map +0 -1
- package/dist/testing/vitest.d.ts +0 -8
- package/dist/testing/vitest.d.ts.map +0 -1
- package/dist/testing/vitest.js.map +0 -1
- package/src/rpc-server/redact-cause.ts +0 -41
- package/src/runtime/types.ts +0 -24
- package/src/testing/types.ts +0 -43
- /package/dist/{runtime → server/runtime}/types.js +0 -0
- /package/dist/{testing → server/testing}/types.js +0 -0
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { Effect, Option } from "effect";
|
|
2
|
-
import type { Headers } from "effect/
|
|
2
|
+
import type { Headers } from "effect/http";
|
|
3
3
|
import { AuthUnavailable } from "#errors/taxonomy.ts";
|
|
4
4
|
import { redact } from "#rpc-server/redact.ts";
|
|
5
5
|
import type { SessionProvider, SessionShape } from "#rpc-server/session.ts";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const unavailable = (cause: unknown) =>
|
|
10
|
-
Effect.andThen(
|
|
6
|
+
export type GetSession<TSession extends SessionShape> = (headers: globalThis.Headers) => Promise<TSession | null>;
|
|
7
|
+
function unavailable(cause: unknown) {
|
|
8
|
+
return Effect.andThen(
|
|
11
9
|
Effect.logError("Authentication provider failed", redact(cause)),
|
|
12
10
|
Effect.fail(new AuthUnavailable({ message: "Authentication is temporarily unavailable" })),
|
|
13
11
|
);
|
|
14
|
-
|
|
15
|
-
export
|
|
16
|
-
|
|
12
|
+
}
|
|
13
|
+
export function betterAuthSessions<TSession extends SessionShape>(getSession: GetSession<TSession>): SessionProvider<TSession> {
|
|
14
|
+
const betterAuthSession = Effect.fn("PlatformRpc.betterAuthSession")(function* (headers: Headers.Headers) {
|
|
17
15
|
const session = yield* Effect.tryPromise(() => getSession(new globalThis.Headers(headers))).pipe(
|
|
18
16
|
Effect.catch((error) => unavailable(error.cause)),
|
|
19
17
|
);
|
|
20
18
|
return Option.fromNullishOr(session);
|
|
21
|
-
})
|
|
22
|
-
}
|
|
19
|
+
});
|
|
20
|
+
return { get: betterAuthSession };
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Cause, Effect, Schema } from "effect";
|
|
2
|
+
import { isSensitiveKey, redactText } from "./sensitive.ts";
|
|
3
|
+
export interface RpcBoundaryOptions<E> {
|
|
4
|
+
readonly message?: string;
|
|
5
|
+
readonly unexpected: (message: string) => E;
|
|
6
|
+
}
|
|
7
|
+
export function rpcBoundary<
|
|
8
|
+
TSchema extends Schema.ConstraintDecoder<{
|
|
9
|
+
readonly message: string;
|
|
10
|
+
}>,
|
|
11
|
+
>(schema: TSchema, options: RpcBoundaryOptions<TSchema["Type"]>) {
|
|
12
|
+
const declared = Schema.is(schema);
|
|
13
|
+
return <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, TSchema["Type"], R> =>
|
|
14
|
+
Effect.catchCause(self, (cause) => {
|
|
15
|
+
if (Cause.hasInterruptsOnly(cause)) {
|
|
16
|
+
return Effect.failCause(Cause.fromReasons(cause.reasons.filter(Cause.isInterruptReason)));
|
|
17
|
+
}
|
|
18
|
+
const reason = cause.reasons[0];
|
|
19
|
+
if (cause.reasons.length === 1 && reason && Cause.isFailReason(reason) && declared(reason.error)) {
|
|
20
|
+
return Effect.fail(reason.error);
|
|
21
|
+
}
|
|
22
|
+
const rejection = options.unexpected(redactText(options.message ?? "Internal server error", isSensitiveKey));
|
|
23
|
+
Object.defineProperty(rejection, "cause", { configurable: true, enumerable: false, value: cause });
|
|
24
|
+
return Effect.failCause(Cause.fromReasons([...Cause.fail(rejection).reasons, ...cause.reasons.filter(Cause.isInterruptReason)]));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { BlockList } from "node:net";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
4
|
+
import { RpcSerialization } from "effect/rpc";
|
|
5
|
+
import { proxyAddresses, proxyMatches } from "./trustedProxy.ts";
|
|
6
|
+
|
|
7
|
+
function loopbackAddress(address: string): boolean {
|
|
8
|
+
const unmapped = unmapAddress(address);
|
|
9
|
+
return unmapped === "::1" || (/^127(?:\.\d{1,3}){3}$/u.test(unmapped) && unmapped.split(".").every((part) => Number(part) <= 255));
|
|
10
|
+
}
|
|
11
|
+
function unmapAddress(address: string): string {
|
|
12
|
+
return address.replace(/^::ffff:/iu, "");
|
|
13
|
+
}
|
|
14
|
+
function loopbackHost(host: string | undefined): boolean {
|
|
15
|
+
if (host === undefined) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const authority = /^(?<hostname>\[[^\]]+\]|[^:]+)(?::(?<port>\d{1,5}))?$/u.exec(host)?.groups;
|
|
19
|
+
if (authority === undefined || (authority.port !== undefined && Number(authority.port) > 65_535)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const hostname = authority.hostname?.toLowerCase();
|
|
23
|
+
return hostname !== undefined && (hostname === "localhost" || hostname === "[::1]" || loopbackAddress(hostname));
|
|
24
|
+
}
|
|
25
|
+
export function sameOrigin(request: HttpServerRequest.HttpServerRequest): boolean {
|
|
26
|
+
const origin = request.headers.origin;
|
|
27
|
+
if (origin === undefined) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(origin);
|
|
32
|
+
return url.protocol === "http:" && url.origin === origin && url.host === request.headers.host;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function loopbackOnly<E, R>(
|
|
38
|
+
handler: (request: HttpServerRequest.HttpServerRequest) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>,
|
|
39
|
+
) {
|
|
40
|
+
return (request: HttpServerRequest.HttpServerRequest) =>
|
|
41
|
+
loopbackHost(request.headers.host) && Option.match(request.remoteAddress, { onNone: () => true, onSome: loopbackAddress })
|
|
42
|
+
? handler(request)
|
|
43
|
+
: Effect.succeed(HttpServerResponse.text("Only loopback hosts are served", { status: 403 }));
|
|
44
|
+
}
|
|
45
|
+
function proxyOrigin(request: HttpServerRequest.HttpServerRequest, trustedProxy: BlockList | undefined): boolean {
|
|
46
|
+
if (trustedProxy === undefined || !Option.exists(request.remoteAddress, (address) => proxyMatches(trustedProxy, address))) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const host = request.headers["x-forwarded-host"];
|
|
50
|
+
const protocol = request.headers["x-forwarded-proto"];
|
|
51
|
+
if (host === undefined || !/^(?:\[[\da-f:]+\]|[\da-z.-]+)(?::\d{1,5})?$/iu.test(host) || (protocol !== "http" && protocol !== "https")) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(`${protocol}://${host}`);
|
|
56
|
+
return request.headers.origin === undefined || request.headers.origin === url.origin;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function localRpcGuard<E, R>(effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>, trustedProxy?: string) {
|
|
62
|
+
const addresses = trustedProxy === undefined ? undefined : proxyAddresses(trustedProxy);
|
|
63
|
+
return Effect.gen(function* () {
|
|
64
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
65
|
+
const serialization = yield* RpcSerialization.RpcSerialization;
|
|
66
|
+
const local = loopbackHost(request.headers.host) && Option.exists(request.remoteAddress, loopbackAddress);
|
|
67
|
+
const contentType = request.headers["content-type"]?.split(";")[0]?.trim().toLowerCase();
|
|
68
|
+
return ((local && sameOrigin(request)) || proxyOrigin(request, addresses)) && contentType === serialization.contentType
|
|
69
|
+
? yield* effect
|
|
70
|
+
: HttpServerResponse.text("Local same-origin requests only", { status: 403 });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import { Option } from "effect";
|
|
2
|
-
import { Headers } from "effect/
|
|
3
|
-
|
|
2
|
+
import { Headers } from "effect/http";
|
|
4
3
|
export interface OriginRequest {
|
|
5
4
|
readonly headers: Headers.Headers;
|
|
6
5
|
readonly origin: Option.Option<string>;
|
|
7
6
|
readonly rpc: string;
|
|
8
7
|
}
|
|
9
|
-
|
|
10
8
|
export type OriginPolicy = (request: OriginRequest) => boolean;
|
|
11
|
-
|
|
12
9
|
export interface TrustedOriginsOptions {
|
|
13
10
|
readonly allow: readonly string[];
|
|
14
11
|
readonly missing: "allow" | "reject";
|
|
15
12
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
(options: TrustedOriginsOptions): OriginPolicy =>
|
|
19
|
-
({ origin }) =>
|
|
13
|
+
export function trustedOrigins(options: TrustedOriginsOptions): OriginPolicy {
|
|
14
|
+
return ({ origin }) =>
|
|
20
15
|
Option.match(origin, {
|
|
21
16
|
onNone: () => options.missing === "allow",
|
|
22
17
|
onSome: (value) => options.allow.includes(value),
|
|
23
18
|
});
|
|
24
|
-
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
}
|
|
20
|
+
export function originRequest(headers: Headers.Headers, rpc: string): OriginRequest {
|
|
21
|
+
return {
|
|
22
|
+
headers,
|
|
23
|
+
origin: Headers.get(headers, "origin"),
|
|
24
|
+
rpc,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -3,8 +3,7 @@ import { isSensitiveKey, REDACTED, redactText, type SensitiveKey } from "./sensi
|
|
|
3
3
|
|
|
4
4
|
const MAX_DEPTH = 8;
|
|
5
5
|
const MAX_ENTRIES = 50;
|
|
6
|
-
|
|
7
|
-
const binary = (value: object): string | undefined => {
|
|
6
|
+
function binary(value: object): string | undefined {
|
|
8
7
|
if (ArrayBuffer.isView(value)) {
|
|
9
8
|
return `<${value.constructor.name} ${value.byteLength} bytes>`;
|
|
10
9
|
}
|
|
@@ -12,9 +11,8 @@ const binary = (value: object): string | undefined => {
|
|
|
12
11
|
return `<ArrayBuffer ${value.byteLength} bytes>`;
|
|
13
12
|
}
|
|
14
13
|
return undefined;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const entriesOf = (value: object): [string, unknown][] => {
|
|
14
|
+
}
|
|
15
|
+
function entriesOf(value: object): [string, unknown][] {
|
|
18
16
|
const entries = Object.entries(value);
|
|
19
17
|
if (!Predicate.isError(value)) {
|
|
20
18
|
return entries;
|
|
@@ -25,9 +23,8 @@ const entriesOf = (value: object): [string, unknown][] => {
|
|
|
25
23
|
["stack", value.stack],
|
|
26
24
|
];
|
|
27
25
|
return value.cause === undefined ? [...shape, ...entries] : [...shape, ...entries, ["cause", value.cause]];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const walk = (value: unknown, sensitive: SensitiveKey, depth: number): unknown => {
|
|
26
|
+
}
|
|
27
|
+
function walk(value: unknown, sensitive: SensitiveKey, depth: number): unknown {
|
|
31
28
|
if (Redacted.isRedacted(value)) {
|
|
32
29
|
return REDACTED;
|
|
33
30
|
}
|
|
@@ -53,6 +50,7 @@ const walk = (value: unknown, sensitive: SensitiveKey, depth: number): unknown =
|
|
|
53
50
|
.slice(0, MAX_ENTRIES)
|
|
54
51
|
.map(([key, nested]): [string, unknown] => [key, sensitive(key) ? REDACTED : walk(nested, sensitive, depth + 1)]);
|
|
55
52
|
return Object.fromEntries(entries.length > MAX_ENTRIES ? [...kept, ["<truncated>", `${entries.length - MAX_ENTRIES} more keys`]] : kept);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
}
|
|
54
|
+
export function redact(value: unknown, sensitive: SensitiveKey = isSensitiveKey): unknown {
|
|
55
|
+
return walk(value, sensitive, 0);
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Cause, Context, ErrorReporter, Predicate } from "effect";
|
|
2
|
+
import { redact } from "./redact.ts";
|
|
3
|
+
import { isSensitiveKey, redactText, type SensitiveKey } from "./sensitive.ts";
|
|
4
|
+
|
|
5
|
+
function reporterHints(error: Error, sensitive: SensitiveKey) {
|
|
6
|
+
return {
|
|
7
|
+
...(ErrorReporter.isIgnored(error) ? { [ErrorReporter.ignore]: true } : {}),
|
|
8
|
+
...(ErrorReporter.severity in error ? { [ErrorReporter.severity]: ErrorReporter.getSeverity(error) } : {}),
|
|
9
|
+
...(ErrorReporter.attributes in error ? { [ErrorReporter.attributes]: redact(ErrorReporter.getAttributes(error), sensitive) } : {}),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function redactError(error: Error, sensitive: SensitiveKey): Error {
|
|
13
|
+
const options = error.cause === undefined ? undefined : { cause: redact(error.cause, sensitive) };
|
|
14
|
+
const copy = new Error(redactText(error.message, sensitive), options);
|
|
15
|
+
Object.defineProperty(copy, "name", { configurable: true, value: error.name, writable: true });
|
|
16
|
+
Object.defineProperty(copy, "stack", {
|
|
17
|
+
configurable: true,
|
|
18
|
+
value: error.stack === undefined ? undefined : redactText(error.stack, sensitive),
|
|
19
|
+
writable: true,
|
|
20
|
+
});
|
|
21
|
+
return Object.assign(copy, redact(Object.fromEntries(Object.entries(error)), sensitive), reporterHints(error, sensitive));
|
|
22
|
+
}
|
|
23
|
+
export function redactDefect(defect: unknown, sensitive: SensitiveKey = isSensitiveKey): unknown {
|
|
24
|
+
return Predicate.isError(defect) ? redactError(defect, sensitive) : redact(defect, sensitive);
|
|
25
|
+
}
|
|
26
|
+
export function redactCause<E>(cause: Cause.Cause<E>, sensitive: SensitiveKey = isSensitiveKey): Cause.Cause<E> {
|
|
27
|
+
return Cause.fromReasons(
|
|
28
|
+
cause.reasons.flatMap<Cause.Reason<E>>((reason) =>
|
|
29
|
+
Cause.isDieReason(reason)
|
|
30
|
+
? Cause.annotate(Cause.die(redactDefect(reason.defect, sensitive)), Context.makeUnsafe(reason.annotations)).reasons
|
|
31
|
+
: [reason],
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
export function redactingErrorReporter(reporter: ErrorReporter.ErrorReporter, sensitive: SensitiveKey = isSensitiveKey): ErrorReporter.ErrorReporter {
|
|
36
|
+
return {
|
|
37
|
+
[ErrorReporter.TypeId]: ErrorReporter.TypeId,
|
|
38
|
+
report: (options) => reporter.report({ ...options, cause: redactCause(options.cause, sensitive) }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export const REDACTED = "<redacted>";
|
|
2
|
-
|
|
3
2
|
const MAX_STRING = 2048;
|
|
4
3
|
const SENSITIVE_PARTS = [
|
|
5
4
|
"password",
|
|
@@ -23,38 +22,34 @@ const SENSITIVE_PARTS = [
|
|
|
23
22
|
];
|
|
24
23
|
const SENSITIVE_WORDS = ["otp", "pin", "cvv", "cvc", "ssn", "iban"];
|
|
25
24
|
const SENSITIVE_NAMES = ["session", "pan"];
|
|
26
|
-
|
|
27
25
|
export type SensitiveKey = (key: string) => boolean;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.replaceAll(/([
|
|
32
|
-
.replaceAll(/([A-Z])([A-Z][a-z])/gu, "$1 $2")
|
|
26
|
+
function wordsOf(key: string): string[] {
|
|
27
|
+
return key
|
|
28
|
+
.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower> $<upper>")
|
|
29
|
+
.replaceAll(/(?<initial>[A-Z])(?<word>[A-Z][a-z])/gu, "$<initial> $<word>")
|
|
33
30
|
.toLowerCase()
|
|
34
31
|
.split(/[\s_-]+/u);
|
|
35
|
-
|
|
36
|
-
export
|
|
32
|
+
}
|
|
33
|
+
export function isSensitiveKey(key: string): boolean {
|
|
37
34
|
const normalized = key.toLowerCase().replaceAll(/[-_]/gu, "");
|
|
38
35
|
return (
|
|
39
36
|
SENSITIVE_NAMES.includes(normalized)
|
|
40
37
|
|| SENSITIVE_PARTS.some((part) => normalized.includes(part))
|
|
41
38
|
|| wordsOf(key).some((word) => SENSITIVE_WORDS.includes(word))
|
|
42
39
|
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const AUTH_SCHEME = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]+=*/giu;
|
|
40
|
+
}
|
|
41
|
+
const AUTH_SCHEME = /\b(?<scheme>Bearer|Basic)\s+[A-Za-z0-9._~+/-]+=*/giu;
|
|
46
42
|
const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/gu;
|
|
47
|
-
const URL_PASSWORD = /(
|
|
48
|
-
const ASSIGNMENT = /([A-Za-z][\w.-]*)(["']?\s*[=:]\s*["']?)([^\s"'&,;]+)/gu;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.replaceAll(AUTH_SCHEME, `$1 ${REDACTED}`)
|
|
43
|
+
const URL_PASSWORD = /(?<authority>\/\/[^:/\s@]+:)[^@/\s]+@/gu;
|
|
44
|
+
const ASSIGNMENT = /(?<key>[A-Za-z][\w.-]*)(?<separator>["']?\s*[=:]\s*["']?)(?<value>[^\s"'&,;]+)/gu;
|
|
45
|
+
function scrub(text: string, sensitive: SensitiveKey): string {
|
|
46
|
+
return text
|
|
47
|
+
.replaceAll(AUTH_SCHEME, `$<scheme> ${REDACTED}`)
|
|
53
48
|
.replaceAll(JWT, REDACTED)
|
|
54
|
-
.replaceAll(URL_PASSWORD,
|
|
49
|
+
.replaceAll(URL_PASSWORD, `$<authority>${REDACTED}@`)
|
|
55
50
|
.replaceAll(ASSIGNMENT, (match: string, key: string, separator: string) => (sensitive(key) ? `${key}${separator}${REDACTED}` : match));
|
|
56
|
-
|
|
57
|
-
export
|
|
51
|
+
}
|
|
52
|
+
export function redactText(text: string, sensitive: SensitiveKey): string {
|
|
58
53
|
const scrubbed = scrub(text, sensitive);
|
|
59
54
|
return scrubbed.length > MAX_STRING ? `${scrubbed.slice(0, MAX_STRING)}…<${scrubbed.length - MAX_STRING} more chars>` : scrubbed;
|
|
60
|
-
}
|
|
55
|
+
}
|
|
@@ -1,42 +1,40 @@
|
|
|
1
1
|
import { Effect, Layer, Option } from "effect";
|
|
2
|
-
import type { Headers } from "effect/
|
|
2
|
+
import type { Headers } from "effect/http";
|
|
3
3
|
import { type AuthUnavailable, Forbidden, Unauthorized } from "#errors/taxonomy.ts";
|
|
4
4
|
import { Identity, OptionalIdentity } from "#rpc/identity.ts";
|
|
5
5
|
import { Authenticated, MaybeAuthenticated } from "#rpc/middleware.ts";
|
|
6
6
|
import { type OriginPolicy, originRequest } from "./origin.ts";
|
|
7
|
-
import { transportHeaders } from "./
|
|
8
|
-
|
|
7
|
+
import { transportHeaders } from "./transportHeaders.ts";
|
|
9
8
|
export interface SessionShape {
|
|
10
|
-
readonly user: {
|
|
9
|
+
readonly user: {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
};
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
readonly get: (headers: Headers.Headers) => Effect.Effect<Option.Option<Session>, AuthUnavailable>;
|
|
13
|
+
export interface SessionProvider<TSession extends SessionShape> {
|
|
14
|
+
readonly get: (headers: Headers.Headers) => Effect.Effect<Option.Option<TSession>, AuthUnavailable>;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
export interface SessionPolicy<Session extends SessionShape> {
|
|
16
|
+
export interface SessionPolicy<TSession extends SessionShape> {
|
|
18
17
|
readonly origin: OriginPolicy;
|
|
19
|
-
readonly provider: SessionProvider<
|
|
18
|
+
readonly provider: SessionProvider<TSession>;
|
|
20
19
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
policy: SessionPolicy<Session>,
|
|
20
|
+
export const resolveSession: <TSession extends SessionShape>(
|
|
21
|
+
policy: SessionPolicy<TSession>,
|
|
24
22
|
headers: Headers.Headers,
|
|
25
23
|
rpc: string,
|
|
26
|
-
) => Effect.Effect<Option.Option<
|
|
27
|
-
|
|
28
|
-
>(policy: SessionPolicy<
|
|
24
|
+
) => Effect.Effect<Option.Option<TSession>, AuthUnavailable | Forbidden> = Effect.fn("PlatformRpc.resolveSession")(function* <
|
|
25
|
+
TSession extends SessionShape,
|
|
26
|
+
>(policy: SessionPolicy<TSession>, headers: Headers.Headers, rpc: string) {
|
|
29
27
|
const transport = yield* transportHeaders(headers);
|
|
30
28
|
if (!policy.origin(originRequest(transport, rpc))) {
|
|
31
29
|
return yield* new Forbidden({ message: "Origin not allowed" });
|
|
32
30
|
}
|
|
33
31
|
return yield* policy.provider.get(transport);
|
|
34
32
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export
|
|
39
|
-
Layer.succeed(Authenticated, (effect, { headers, rpc }) =>
|
|
33
|
+
function identified(id: string) {
|
|
34
|
+
return Effect.andThen(Effect.annotateCurrentSpan("user.id", id), Effect.succeed(id));
|
|
35
|
+
}
|
|
36
|
+
export function authenticatedLayer<TSession extends SessionShape>(policy: SessionPolicy<TSession>): Layer.Layer<Authenticated> {
|
|
37
|
+
return Layer.succeed(Authenticated, (effect, { headers, rpc }) =>
|
|
40
38
|
Effect.gen(function* () {
|
|
41
39
|
const session = yield* resolveSession(policy, headers, rpc._tag);
|
|
42
40
|
if (Option.isNone(session)) {
|
|
@@ -46,9 +44,9 @@ export const authenticatedLayer = <Session extends SessionShape>(policy: Session
|
|
|
46
44
|
return yield* effect.pipe(Effect.provideService(Identity, { id }), Effect.annotateLogs({ userId: id }));
|
|
47
45
|
}),
|
|
48
46
|
);
|
|
49
|
-
|
|
50
|
-
export
|
|
51
|
-
Layer.succeed(MaybeAuthenticated, (effect, { headers, rpc }) =>
|
|
47
|
+
}
|
|
48
|
+
export function maybeAuthenticatedLayer<TSession extends SessionShape>(policy: SessionPolicy<TSession>): Layer.Layer<MaybeAuthenticated> {
|
|
49
|
+
return Layer.succeed(MaybeAuthenticated, (effect, { headers, rpc }) =>
|
|
52
50
|
Effect.gen(function* () {
|
|
53
51
|
const session = yield* resolveSession(policy, headers, rpc._tag);
|
|
54
52
|
if (Option.isNone(session)) {
|
|
@@ -58,3 +56,4 @@ export const maybeAuthenticatedLayer = <Session extends SessionShape>(policy: Se
|
|
|
58
56
|
return yield* effect.pipe(Effect.provideService(OptionalIdentity, Option.some({ id })), Effect.annotateLogs({ userId: id }));
|
|
59
57
|
}),
|
|
60
58
|
);
|
|
59
|
+
}
|
|
@@ -1,38 +1,35 @@
|
|
|
1
1
|
import { Cause, Effect, Layer, Option, Predicate, Random } from "effect";
|
|
2
|
-
import { Headers } from "effect/
|
|
2
|
+
import { Headers } from "effect/http";
|
|
3
3
|
import { RequestId } from "#rpc/identity.ts";
|
|
4
4
|
import { RequestTracing } from "#rpc/middleware.ts";
|
|
5
5
|
import { redact } from "./redact.ts";
|
|
6
|
-
import { redactCause } from "./
|
|
6
|
+
import { redactCause } from "./redactCause.ts";
|
|
7
7
|
import type { SensitiveKey } from "./sensitive.ts";
|
|
8
|
-
|
|
9
8
|
export interface RequestTracingOptions {
|
|
10
9
|
readonly header?: string;
|
|
11
10
|
readonly sensitive?: SensitiveKey;
|
|
12
11
|
}
|
|
13
|
-
|
|
14
12
|
const ACCEPTED_REQUEST_ID = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
function hex(byte: number): string {
|
|
14
|
+
return byte.toString(16).padStart(2, "0");
|
|
15
|
+
}
|
|
18
16
|
const generateRequestId = Effect.map(Effect.all(Array.from({ length: 16 }, () => Random.nextIntBetween(0, 255))), (bytes) => bytes.map(hex).join(""));
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Option.match(
|
|
17
|
+
function requestIdOf(headers: Headers.Headers, header: string): Effect.Effect<string> {
|
|
18
|
+
return Option.match(
|
|
22
19
|
Option.filter(Headers.get(headers, header), (value) => ACCEPTED_REQUEST_ID.test(value)),
|
|
23
20
|
{
|
|
24
21
|
onNone: () => generateRequestId,
|
|
25
22
|
onSome: Effect.succeed,
|
|
26
23
|
},
|
|
27
24
|
);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Option.match(Cause.findErrorOption(cause), {
|
|
25
|
+
}
|
|
26
|
+
function failureTag(cause: Cause.Cause<unknown>): string {
|
|
27
|
+
return Option.match(Cause.findErrorOption(cause), {
|
|
31
28
|
onNone: () => "unknown",
|
|
32
29
|
onSome: (error) => (Predicate.hasProperty(error, "_tag") && Predicate.isString(error._tag) ? error._tag : "unknown"),
|
|
33
30
|
});
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
}
|
|
32
|
+
function logFailure(cause: Cause.Cause<unknown>, payload: unknown, sensitive: SensitiveKey | undefined): Effect.Effect<void> {
|
|
36
33
|
if (Cause.hasInterruptsOnly(cause)) {
|
|
37
34
|
return Effect.void;
|
|
38
35
|
}
|
|
@@ -42,10 +39,9 @@ const logFailure = (cause: Cause.Cause<unknown>, payload: unknown, sensitive: Se
|
|
|
42
39
|
}
|
|
43
40
|
const defects = cause.reasons.filter(Cause.isDieReason).map((reason) => redact(reason.defect, sensitive));
|
|
44
41
|
return Effect.annotateLogs(Effect.logError("RPC defect"), { ...annotations, "rpc.defect": defects });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Layer.succeed(RequestTracing, (effect, { headers, payload, rpc }) =>
|
|
42
|
+
}
|
|
43
|
+
export function requestTracingLayer(options: RequestTracingOptions = {}): Layer.Layer<RequestTracing> {
|
|
44
|
+
return Layer.succeed(RequestTracing, (effect, { headers, payload, rpc }) =>
|
|
49
45
|
Effect.gen(function* () {
|
|
50
46
|
const requestId = yield* requestIdOf(headers, options.header ?? "x-request-id");
|
|
51
47
|
yield* Effect.annotateCurrentSpan({ "request.id": requestId, "rpc.method": rpc._tag });
|
|
@@ -58,3 +54,4 @@ export const requestTracingLayer = (options: RequestTracingOptions = {}): Layer.
|
|
|
58
54
|
);
|
|
59
55
|
}),
|
|
60
56
|
);
|
|
57
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Effect, Option } from "effect";
|
|
2
|
-
import { type Headers, HttpServerRequest } from "effect/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Effect.map(
|
|
2
|
+
import { type Headers, HttpServerRequest } from "effect/http";
|
|
3
|
+
export function transportHeaders(rpcHeaders: Headers.Headers): Effect.Effect<Headers.Headers> {
|
|
4
|
+
return Effect.map(
|
|
6
5
|
Effect.serviceOption(HttpServerRequest.HttpServerRequest),
|
|
7
6
|
Option.match({ onNone: () => rpcHeaders, onSome: (request) => request.headers }),
|
|
8
7
|
);
|
|
8
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BlockList, isIP } from "node:net";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
function addProxyAddress(addresses: BlockList, entry: string): boolean {
|
|
5
|
+
const parts = entry.trim().split("/");
|
|
6
|
+
const address = parts[0] ?? "";
|
|
7
|
+
const family = isIP(address);
|
|
8
|
+
if (family === 0 || address.includes("%") || parts.length > 2) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const type = family === 4 ? "ipv4" : "ipv6";
|
|
12
|
+
const prefix = parts[1];
|
|
13
|
+
if (prefix === undefined) {
|
|
14
|
+
addresses.addAddress(address, type);
|
|
15
|
+
} else {
|
|
16
|
+
if (!/^\d+$/u.test(prefix) || Number(prefix) > (family === 4 ? 32 : 128)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
addresses.addSubnet(address, Number(prefix), type);
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
export function proxyAddresses(value: string): BlockList | undefined {
|
|
24
|
+
const addresses = new BlockList();
|
|
25
|
+
return value.split(",").every((entry) => addProxyAddress(addresses, entry)) ? addresses : undefined;
|
|
26
|
+
}
|
|
27
|
+
export const TrustedProxy = Schema.String.check(
|
|
28
|
+
Schema.makeFilter((value) => proxyAddresses(value) !== undefined, {
|
|
29
|
+
message: "Expected a comma-separated list of IPv4 or IPv6 addresses or CIDR ranges",
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
export function proxyMatches(addresses: BlockList, address: string): boolean {
|
|
33
|
+
const unmapped = address.replace(/^::ffff:(?=\d+\.)/iu, "");
|
|
34
|
+
const family = isIP(unmapped);
|
|
35
|
+
return family !== 0 && !unmapped.includes("%") && addresses.check(unmapped, family === 4 ? "ipv4" : "ipv6");
|
|
36
|
+
}
|
|
@@ -7,31 +7,28 @@ const RuntimeCacheKey = Symbol.for("@shivaedev/platform/runtime-cache");
|
|
|
7
7
|
const sharedCache: unknown = Reflect.get(globalThis, RuntimeCacheKey);
|
|
8
8
|
const runtimeCache: Map<string | symbol, unknown> = sharedCache instanceof Map ? sharedCache : new Map<string | symbol, unknown>();
|
|
9
9
|
Reflect.set(globalThis, RuntimeCacheKey, runtimeCache);
|
|
10
|
-
|
|
11
10
|
// One development cache key belongs to one makePlatformRuntime call site, so its entry has that site's services and build error.
|
|
12
|
-
function cachedEntry<
|
|
11
|
+
function cachedEntry<TServices, TBuildError>(runtime: unknown): PlatformRuntime<TServices, TBuildError>;
|
|
13
12
|
function cachedEntry(runtime: unknown): unknown {
|
|
14
13
|
return runtime;
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
const make = <Services, BuildError>(layer: Layer.Layer<Services, BuildError>): PlatformRuntime<Services, BuildError> => {
|
|
15
|
+
function make<TServices, TBuildError>(layer: Layer.Layer<TServices, TBuildError>): PlatformRuntime<TServices, TBuildError> {
|
|
18
16
|
const managed = ManagedRuntime.make(layer);
|
|
19
17
|
const services = new AsyncLocalStorage<Context.Context<never>>();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
function currentServices() {
|
|
19
|
+
return services.getStore();
|
|
20
|
+
}
|
|
21
|
+
function withAmbient<A, E, TRequirements extends TServices>(
|
|
22
|
+
effect: Effect.Effect<A, E, TRequirements>,
|
|
23
|
+
): Effect.Effect<A, E | TBuildError, TRequirements> {
|
|
25
24
|
const ambient = currentServices();
|
|
26
25
|
return ambient === undefined
|
|
27
26
|
? effect
|
|
28
27
|
: Effect.flatMap(managed.contextEffect, (base) => Effect.provideContext(effect, Context.merge(base, ambient)));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const runPromiseExit: PlatformRuntime<Services, BuildError>["runPromiseExit"] = (effect, options) =>
|
|
28
|
+
}
|
|
29
|
+
const runPromiseExit: PlatformRuntime<TServices, TBuildError>["runPromiseExit"] = (effect, options) =>
|
|
32
30
|
managed.runPromiseExit(withAmbient(effect), options);
|
|
33
|
-
|
|
34
|
-
const runtime: PlatformRuntime<Services, BuildError> = {
|
|
31
|
+
const runtime: PlatformRuntime<TServices, TBuildError> = {
|
|
35
32
|
contextEffect: Effect.map(managed.contextEffect, (base) => {
|
|
36
33
|
const ambient = currentServices();
|
|
37
34
|
return ambient === undefined ? base : Context.merge(base, ambient);
|
|
@@ -48,28 +45,23 @@ const make = <Services, BuildError>(layer: Layer.Layer<Services, BuildError>): P
|
|
|
48
45
|
runPromiseExit,
|
|
49
46
|
runWithServices: (context, evaluate) => services.run(context, evaluate),
|
|
50
47
|
};
|
|
51
|
-
|
|
52
48
|
return runtime;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
export const makePlatformRuntime = <Services, BuildError>(
|
|
58
|
-
layer: Layer.Layer<Services, BuildError>,
|
|
49
|
+
}
|
|
50
|
+
const production = Effect.map(Effect.orDie(Config.String("NODE_ENV").pipe(Config.withDefault(""))), (environment) => environment === "production");
|
|
51
|
+
export function makePlatformRuntime<TServices, TBuildError>(
|
|
52
|
+
layer: Layer.Layer<TServices, TBuildError>,
|
|
59
53
|
options: PlatformRuntimeOptions = {},
|
|
60
|
-
): PlatformRuntime<
|
|
54
|
+
): PlatformRuntime<TServices, TBuildError> {
|
|
61
55
|
const key = Effect.runSync(production) ? undefined : options.developmentCacheKey;
|
|
62
56
|
if (key === undefined) {
|
|
63
57
|
return make(layer);
|
|
64
58
|
}
|
|
65
|
-
|
|
66
59
|
const cached = runtimeCache.get(key);
|
|
67
60
|
if (cached !== undefined) {
|
|
68
|
-
return cachedEntry<
|
|
61
|
+
return cachedEntry<TServices, TBuildError>(cached);
|
|
69
62
|
}
|
|
70
|
-
|
|
71
63
|
const runtime = make(layer);
|
|
72
|
-
const cachedRuntime: PlatformRuntime<
|
|
64
|
+
const cachedRuntime: PlatformRuntime<TServices, TBuildError> = {
|
|
73
65
|
...runtime,
|
|
74
66
|
dispose: async () => {
|
|
75
67
|
if (runtimeCache.get(key) === cachedRuntime) {
|
|
@@ -80,4 +72,4 @@ export const makePlatformRuntime = <Services, BuildError>(
|
|
|
80
72
|
};
|
|
81
73
|
runtimeCache.set(key, cachedRuntime);
|
|
82
74
|
return cachedRuntime;
|
|
83
|
-
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Context, Effect, Exit } from "effect";
|
|
2
|
+
export interface PlatformRuntimeOptions {
|
|
3
|
+
readonly developmentCacheKey?: string | symbol;
|
|
4
|
+
}
|
|
5
|
+
export interface PlatformRuntime<TServices, TBuildError> {
|
|
6
|
+
readonly contextEffect: Effect.Effect<Context.Context<TServices>, TBuildError>;
|
|
7
|
+
readonly currentServices: () => Context.Context<never> | undefined;
|
|
8
|
+
readonly dispose: () => Promise<void>;
|
|
9
|
+
readonly runPromise: <A, E, TRequirements extends TServices>(
|
|
10
|
+
effect: Effect.Effect<A, E, TRequirements>,
|
|
11
|
+
options?: {
|
|
12
|
+
readonly signal?: AbortSignal;
|
|
13
|
+
},
|
|
14
|
+
) => Promise<A>;
|
|
15
|
+
readonly runPromiseExit: <A, E, TRequirements extends TServices>(
|
|
16
|
+
effect: Effect.Effect<A, E, TRequirements>,
|
|
17
|
+
options?: {
|
|
18
|
+
readonly signal?: AbortSignal;
|
|
19
|
+
},
|
|
20
|
+
) => Promise<Exit.Exit<A, E | TBuildError>>;
|
|
21
|
+
readonly runWithServices: <TProvided, TValue>(services: Context.Context<TProvided>, evaluate: () => TValue) => TValue;
|
|
22
|
+
}
|
|
23
|
+
export type PlatformRuntimeServices<TRuntime> = TRuntime extends PlatformRuntime<infer TServices, infer _TBuildError> ? TServices : never;
|
|
24
|
+
export type PlatformRuntimeBuildError<TRuntime> = TRuntime extends PlatformRuntime<infer _TServices, infer TBuildError> ? TBuildError : never;
|