@shivaedev/server 0.3.1 → 0.4.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/README.md +7 -13
- package/dist/hostChanges.d.ts +2 -1
- package/dist/hostChanges.d.ts.map +1 -1
- package/dist/hostChanges.js +39 -5
- package/dist/hostChanges.js.map +1 -1
- package/dist/hostRpc.d.ts +1 -1
- package/dist/hostRpc.d.ts.map +1 -1
- package/dist/hostRpc.js +4 -2
- package/dist/hostRpc.js.map +1 -1
- package/dist/hostRpcRoutes.d.ts.map +1 -1
- package/dist/hostRpcRoutes.js +6 -2
- package/dist/hostRpcRoutes.js.map +1 -1
- package/dist/socketRpc.d.ts +6 -0
- package/dist/socketRpc.d.ts.map +1 -0
- package/dist/socketRpc.js +27 -0
- package/dist/socketRpc.js.map +1 -0
- package/package.json +5 -5
- package/src/hostChanges.ts +56 -9
- package/src/hostRpc.ts +4 -2
- package/src/hostRpcRoutes.ts +17 -2
- package/src/socketRpc.ts +36 -0
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ The optional modules solve different boundaries. `runtime/` carries Effect servi
|
|
|
35
35
|
|
|
36
36
|
### Host the application
|
|
37
37
|
|
|
38
|
-
`appHost` is the server entry point for an Effect app. For a PostgreSQL app, supply the native RPC group, handler and service Layers, migration loader, session policy and static asset directory. The host completes migrations before acquiring application services, starts Node HTTP with
|
|
38
|
+
`appHost` is the server entry point for an Effect app. For a PostgreSQL app, supply the native RPC group, handler and service Layers, migration loader, session policy and static asset directory. The host completes migrations before acquiring application services, starts Node HTTP with native Effect RPC over a WebSocket upgrade at `/rpc`, and provides SQL, Reactivity and `AppChanges` to those Layers.
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
41
|
import { NodeRuntime } from "@effect/platform-node";
|
|
@@ -89,7 +89,7 @@ RPC request bodies are limited to 1 MiB by default. Set `MAX_BODY_SIZE` to a byt
|
|
|
89
89
|
|
|
90
90
|
For a native Node HTTP route outside RPC, read the request body with `readBoundedBody` from [`@shivaedev/http`](https://github.com/ShivaeDev/platform/tree/main/packages/http#readme), which rejects oversized bodies with a typed 413 failure.
|
|
91
91
|
|
|
92
|
-
RPC uses `localRpcGuard` from `@shivaedev/identity/server/local.ts`: it requires a loopback peer and Host, same-origin or absent Origin, and the NDJSON content type. Session lookup uses transport headers. The host applies `RequestTracing` last to every served RPC, including loopback RPC, so it wraps authentication and handlers without a per-contract tracing declaration. Existing tracing declarations run once at that outermost boundary. With a session policy, contracts declare `Authenticated` and `MaybeAuthenticated` as needed; the host supplies their implementations. For `AuthenticatedUser`, merge `authenticatedUserLayer(sessions)` from `@shivaedev/identity/server/session.ts` beside the handlers. Declared errors retain their Schema types. Applications still own resource authorization and use `rpcBoundary` when they need to map unexpected failures.
|
|
92
|
+
Ordinary POST RPC uses `localRpcGuard` from `@shivaedev/identity/server/local.ts`: it requires a loopback peer and Host, same-origin or absent Origin, and the NDJSON content type. The WebSocket upgrade uses the same loopback, origin and trusted-proxy checks without an HTTP body content type. Session lookup uses the upgrade's transport headers; RPC message headers cannot replace its cookie or Origin. Session middleware revalidates authenticated streams at `streamRevalidationInterval` and ends revoked subscriptions with typed `Unauthorized`. The shared socket remains open; later protected calls use the same session checks. Automatic live subscriptions also recheck the session and query policy before each hint. POST RPC has no `$changes` stream. The host applies `RequestTracing` last to every served RPC, including loopback RPC, so it wraps authentication and handlers without a per-contract tracing declaration. Existing tracing declarations run once at that outermost boundary. With a session policy, contracts declare `Authenticated` and `MaybeAuthenticated` as needed; the host supplies their implementations. For `AuthenticatedUser`, merge `authenticatedUserLayer(sessions)` from `@shivaedev/identity/server/session.ts` beside the handlers. Declared errors retain their Schema types. Applications still own resource authorization and use `rpcBoundary` when they need to map unexpected failures.
|
|
93
93
|
|
|
94
94
|
Every response from the host's native RPC endpoint carries `Cache-Control: private, no-store`, including mixed public and authenticated batches, typed failures, transport rejections, body-read failures and streams. The host sets this at the outer response boundary without inspecting batch contents.
|
|
95
95
|
|
|
@@ -188,22 +188,16 @@ export const Server = appHost({
|
|
|
188
188
|
|
|
189
189
|
`publicConfigLayer` reads `Config.schema` from the host's native ConfigProvider when the handler Layer builds. Missing or invalid required fields fail startup with a typed `ConfigError`. An optional second argument supplies a native Config path: `publicConfigLayer(PublicConfig, "PUBLIC")` reads `PUBLIC_INSTALL_URL` with the default environment provider. Only schema-declared fields are loaded and serialized; other server values never enter this response. The generated query has no session middleware, so it can supply public configuration before sign-in. Merge it alongside authenticated contracts rather than applying authentication to the merged group.
|
|
190
190
|
|
|
191
|
-
On the client, use the same
|
|
191
|
+
On the client, use the same contract `makeClient` and shared WebSocket as other queries:
|
|
192
192
|
|
|
193
193
|
```ts
|
|
194
|
-
import {
|
|
195
|
-
import { FetchHttpClient } from "effect/http";
|
|
196
|
-
import * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
197
|
-
import { RpcClient, RpcSerialization } from "effect/rpc";
|
|
198
|
-
import { bind } from "@shivaedev/contract/bind.ts";
|
|
194
|
+
import { makeClient } from "@shivaedev/contract/bind.ts";
|
|
199
195
|
import { useQuery } from "@shivaedev/react/client/resultState.ts";
|
|
200
196
|
import { PublicConfig } from "./public-config.ts";
|
|
201
197
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}) {}
|
|
206
|
-
const config = bind(PublicConfig, ConfigClient);
|
|
198
|
+
const { api: config } = makeClient(PublicConfig, {
|
|
199
|
+
url: new URL("/rpc", window.location.href).href,
|
|
200
|
+
});
|
|
207
201
|
|
|
208
202
|
export function usePublicConfig() {
|
|
209
203
|
return useQuery(config.get.query());
|
package/dist/hostChanges.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import type { Rpc, RpcGroup } from "effect/rpc";
|
|
3
|
+
import { type SessionPolicy, type SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
3
4
|
import type { AppChanges } from "./appHost.ts";
|
|
4
|
-
export declare function hostChanges<R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, changesService: typeof AppChanges): Layer.Layer<Rpc.ToHandler<Extract<R, {
|
|
5
|
+
export declare function hostChanges<R extends Rpc.Any, TSession extends SessionShape>(group: RpcGroup.RpcGroup<R>, changesService: typeof AppChanges, sessions?: SessionPolicy<TSession>): Layer.Layer<Rpc.ToHandler<Extract<R, {
|
|
5
6
|
readonly _tag: `${string}.$changes`;
|
|
6
7
|
}>>, never, never>;
|
|
7
8
|
//# sourceMappingURL=hostChanges.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hostChanges.d.ts","sourceRoot":"","sources":["../src/hostChanges.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,EAAkB,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"hostChanges.d.ts","sourceRoot":"","sources":["../src/hostChanges.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,EAAkB,MAAM,QAAQ,CAAC;AAEhE,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAIhD,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAE9G,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,wBAAgB,WAAW,CAAC,CAAC,SAAS,GAAG,CAAC,GAAG,EAAE,QAAQ,SAAS,YAAY,EAC3E,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAC3B,cAAc,EAAE,OAAO,UAAU,EACjC,QAAQ,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC;mBAsCoC,GAAG,MAAM,WAAW;mBAG1F"}
|
package/dist/hostChanges.js
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Option, Stream } from "effect";
|
|
2
|
+
import { Headers } from "effect/http";
|
|
3
|
+
import { CurrentViewer } from "@shivaedev/contract/readPolicy.ts";
|
|
4
|
+
import { Unauthorized } from "@shivaedev/errors/taxonomy.ts";
|
|
5
|
+
import { resolveSession } from "@shivaedev/identity/server/session.ts";
|
|
2
6
|
import { resolveChanges } from "@shivaedev/identity/server/sessionKeys.ts";
|
|
3
|
-
export function hostChanges(group, changesService) {
|
|
7
|
+
export function hostChanges(group, changesService, sessions) {
|
|
4
8
|
return Layer.effectContext(Effect.gen(function* () {
|
|
5
9
|
const changes = yield* Effect.serviceOption(changesService);
|
|
6
10
|
const services = yield* Effect.context();
|
|
7
11
|
const handlers = new Map();
|
|
12
|
+
function subscribe(rpc, queries) {
|
|
13
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
14
|
+
const { subscription, context } = yield* resolveChanges(rpc, queries);
|
|
15
|
+
const viewer = yield* CurrentViewer;
|
|
16
|
+
const authorized = authorizeSession(sessions, rpc._tag, viewer, subscription.authorized);
|
|
17
|
+
const stream = Option.isSome(changes)
|
|
18
|
+
? changes.value.subscribe({ ...subscription, authorized }, context)
|
|
19
|
+
: Stream.concat(Stream.succeed({ _tag: "Resync" }), Stream.never);
|
|
20
|
+
if (sessions === undefined || Option.isNone(viewer)) {
|
|
21
|
+
return stream;
|
|
22
|
+
}
|
|
23
|
+
return Stream.interruptWhen(stream, sessionRevoked(sessions, rpc._tag, viewer.value.id));
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
8
26
|
for (const rpc of group.requests.values()) {
|
|
9
27
|
if (!rpc._tag.endsWith(".$changes")) {
|
|
10
28
|
continue;
|
|
@@ -14,12 +32,28 @@ export function hostChanges(group, changesService) {
|
|
|
14
32
|
handlers.set(rpc.key, provided);
|
|
15
33
|
continue;
|
|
16
34
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
handlers.set(rpc.key, {
|
|
36
|
+
context: services,
|
|
37
|
+
handler: ({ queries }) => subscribe(rpc, queries),
|
|
38
|
+
tag: rpc._tag,
|
|
39
|
+
});
|
|
21
40
|
}
|
|
22
41
|
return Context.makeUnsafe(handlers);
|
|
23
42
|
}));
|
|
24
43
|
}
|
|
44
|
+
function authorizeSession(sessions, tag, viewer, policy) {
|
|
45
|
+
if (sessions === undefined) {
|
|
46
|
+
return policy;
|
|
47
|
+
}
|
|
48
|
+
return resolveSession(sessions, Headers.empty, tag).pipe(Effect.map((current) => Option.isNone(viewer) || (Option.isSome(current) && current.value.user.id === viewer.value.id)), Effect.catch(() => Effect.succeed(false)), Effect.flatMap((allowed) => (allowed ? policy : Effect.succeed(false))));
|
|
49
|
+
}
|
|
50
|
+
function sessionRevoked(sessions, tag, id) {
|
|
51
|
+
return Effect.gen(function* () {
|
|
52
|
+
yield* Effect.sleep(sessions.streamRevalidationInterval ?? "30 seconds");
|
|
53
|
+
const current = yield* resolveSession(sessions, Headers.empty, tag).pipe(Effect.catchTag("AuthUnavailable", () => Effect.succeed(undefined)));
|
|
54
|
+
if (current !== undefined && (Option.isNone(current) || current.value.user.id !== id)) {
|
|
55
|
+
return yield* new Unauthorized({ message: "Authentication required" });
|
|
56
|
+
}
|
|
57
|
+
}).pipe(Effect.forever);
|
|
58
|
+
}
|
|
25
59
|
//# sourceMappingURL=hostChanges.js.map
|
package/dist/hostChanges.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hostChanges.js","sourceRoot":"","sources":["../src/hostChanges.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"hostChanges.js","sourceRoot":"","sources":["../src/hostChanges.ts"],"names":[],"mappings":"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"}
|
package/dist/hostRpc.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ declare class HostedReadPolicy extends HostedReadPolicy_base {
|
|
|
7
7
|
}
|
|
8
8
|
export type { HostedReadPolicy };
|
|
9
9
|
export declare const rpcCacheControl: HttpEffect.PreResponseHandler;
|
|
10
|
-
export declare const hostRpc: <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R
|
|
10
|
+
export declare const hostRpc: <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, websocket?: boolean | undefined) => Effect.Effect<Effect.Effect<HttpServerResponse.HttpServerResponse, never, import("effect/http/HttpServerRequest").HttpServerRequest | import("effect/Scope").Scope>, never, import("effect/rpc/RpcSerialization").RpcSerialization | import("effect/Scope").Scope | Exclude<Rpc.Middleware<Rpc.AddMiddleware<Rpc.AddMiddleware<R, typeof HostedReadPolicy>, typeof import("@shivaedev/identity/middleware.ts").RequestTracing>>, HostedReadPolicy> | Exclude<Rpc.ServicesServer<Rpc.AddMiddleware<Rpc.AddMiddleware<R, typeof HostedReadPolicy>, typeof import("@shivaedev/identity/middleware.ts").RequestTracing>>, HostedReadPolicy> | Exclude<Rpc.ToHandler<Rpc.AddMiddleware<Rpc.AddMiddleware<R, typeof HostedReadPolicy>, typeof import("@shivaedev/identity/middleware.ts").RequestTracing>>, HostedReadPolicy>>;
|
|
11
11
|
//# sourceMappingURL=hostRpc.d.ts.map
|
package/dist/hostRpc.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hostRpc.d.ts","sourceRoot":"","sources":["../src/hostRpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,KAAK,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,QAAQ,EAAE,aAAa,EAAa,MAAM,YAAY,CAAC;AAE/E,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;;AAG7F,cAAM,gBAAiB,SAAQ,qBAE7B;CAAG;AAEL,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC,eAAO,MAAM,eAAe,EAAE,UAAU,CAAC,kBACoD,CAAC;AAE9F,eAAO,MAAM,OAAO,GAAgC,CAAC,SAAS,GAAG,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"hostRpc.d.ts","sourceRoot":"","sources":["../src/hostRpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,KAAK,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,QAAQ,EAAE,aAAa,EAAa,MAAM,YAAY,CAAC;AAE/E,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;;AAG7F,cAAM,gBAAiB,SAAQ,qBAE7B;CAAG;AAEL,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC,eAAO,MAAM,eAAe,EAAE,UAAU,CAAC,kBACoD,CAAC;AAE9F,eAAO,MAAM,OAAO,GAAgC,CAAC,SAAS,GAAG,CAAC,GAAG,21BAWnE,CAAC"}
|
package/dist/hostRpc.js
CHANGED
|
@@ -9,12 +9,14 @@ class HostedReadPolicy extends RpcMiddleware.Service()("@shivaedev/server/Hosted
|
|
|
9
9
|
}) {
|
|
10
10
|
}
|
|
11
11
|
export const rpcCacheControl = (_, response) => Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "private, no-store"));
|
|
12
|
-
export const hostRpc = Effect.fnUntraced(function* (group) {
|
|
12
|
+
export const hostRpc = Effect.fnUntraced(function* (group, websocket = false) {
|
|
13
13
|
const guarded = group.middleware(HostedReadPolicy);
|
|
14
14
|
// Keep the policy inside identity middleware so it sees the request's viewer.
|
|
15
15
|
for (const rpc of guarded.requests.values()) {
|
|
16
16
|
Object.assign(rpc, { middlewares: new Set([HostedReadPolicy, ...rpc.middlewares]) });
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
const traced = tracedGroup(guarded);
|
|
19
|
+
const handler = websocket ? RpcServer.toHttpEffectWebsocket(traced) : RpcServer.toHttpEffect(traced);
|
|
20
|
+
return yield* handler.pipe(Effect.provideService(HostedReadPolicy, (effect, request) => Effect.flatMap(CurrentViewer, (viewer) => authorizeReads(effect, request, viewer))));
|
|
19
21
|
});
|
|
20
22
|
//# sourceMappingURL=hostRpc.js.map
|
package/dist/hostRpc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hostRpc.js","sourceRoot":"","sources":["../src/hostRpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAmB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAA2B,aAAa,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC7F,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,gBAAiB,SAAQ,aAAa,CAAC,OAAO,EAAoB,CAAC,oCAAoC,EAAE;IAC9G,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC;CACzD,CAAC;CAAG;AAIL,MAAM,CAAC,MAAM,eAAe,GAAkC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAC7E,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAqB,KAA2B;
|
|
1
|
+
{"version":3,"file":"hostRpc.js","sourceRoot":"","sources":["../src/hostRpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAmB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAA2B,aAAa,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC7F,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,gBAAiB,SAAQ,aAAa,CAAC,OAAO,EAAoB,CAAC,oCAAoC,EAAE;IAC9G,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC;CACzD,CAAC;CAAG;AAIL,MAAM,CAAC,MAAM,eAAe,GAAkC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAC7E,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAqB,KAA2B,EAAE,SAAS,GAAG,KAAK;IACpH,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;IACnD,8EAA8E;IAC9E,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,GAAG,CAAC,CAAC,gBAAgB,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACrG,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CACzB,MAAM,CAAC,cAAc,CAAC,gBAAgB,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAChJ,CAAC;AACH,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hostRpcRoutes.d.ts","sourceRoot":"","sources":["../src/hostRpcRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAmC,UAAU,EAAyC,MAAM,aAAa,CAAC;AAEjH,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,KAAK,EAAqC,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAE3G,OAAO,EAA+C,KAAK,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAEvH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"hostRpcRoutes.d.ts","sourceRoot":"","sources":["../src/hostRpcRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAmC,UAAU,EAAyC,MAAM,aAAa,CAAC;AAEjH,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,KAAK,EAAqC,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAE3G,OAAO,EAA+C,KAAK,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAEvH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAGnE,wBAAgB,aAAa,CAAC,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,SAAS,YAAY,EACnH,OAAO,EAAE;IACR,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;IACjE,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC;CACxC,GAAG,UAAU,CAAC,QAAQ,CAAC,EACxB,cAAc,EAAE,OAAO,UAAU,EACjC,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,EAC3F,SAAS,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC;;;;;;;;0CA6CjD"}
|
package/dist/hostRpcRoutes.js
CHANGED
|
@@ -9,21 +9,25 @@ import { boundedRpcRequest } from "#boundedRpcRequest.ts";
|
|
|
9
9
|
import { hostChanges } from "#hostChanges.ts";
|
|
10
10
|
import { hostRpc, rpcCacheControl } from "#hostRpc.ts";
|
|
11
11
|
import { redactingRpcSerialization } from "#redactingRpcSerialization.ts";
|
|
12
|
+
import { socketRpc } from "#socketRpc.ts";
|
|
12
13
|
export function hostRpcRoutes(options, changesService, { bodySize, trustedProxy, nativeOrigins }, sensitive) {
|
|
13
14
|
const middleware = options.policy === "loopback"
|
|
14
15
|
? requestTracingLayer({ sensitive })
|
|
15
16
|
: Layer.mergeAll(authenticatedLayer(options.sessions), maybeAuthenticatedLayer(options.sessions), requestTracingLayer({ sensitive }));
|
|
16
17
|
return HttpRouter.use((router) => Effect.gen(function* () {
|
|
17
18
|
const wire = redactingRpcSerialization(yield* RpcSerialization.RpcSerialization, sensitive, sensitive.maskFailure);
|
|
18
|
-
const
|
|
19
|
+
const httpGroup = options.group.omit(...[...options.group.requests.keys()].filter((tag) => tag.endsWith(".$changes")));
|
|
20
|
+
const handler = yield* hostRpc(httpGroup).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire));
|
|
19
21
|
const guarded = localRpcGuard(handler, trustedProxy, nativeOrigins);
|
|
22
|
+
const websocket = yield* hostRpc(options.group, true).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire));
|
|
20
23
|
yield* router.add("OPTIONS", options.rpcPath ?? "/rpc", HttpServerResponse.text("Local same-origin requests only", {
|
|
21
24
|
status: HttpStatus.fromLiteral("Forbidden"),
|
|
22
25
|
}));
|
|
26
|
+
yield* router.add("GET", options.rpcPath ?? "/rpc", socketRpc(websocket, options, trustedProxy, nativeOrigins).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire)));
|
|
23
27
|
yield* router.add("POST", options.rpcPath ?? "/rpc", (request) => {
|
|
24
28
|
HttpEffect.appendPreResponseHandlerUnsafe(request, rpcCacheControl);
|
|
25
29
|
return guarded.pipe(Effect.provideService(HttpIncomingMessage.MaxBodySize, ByteSize.bytes(bodySize)), Effect.provideService(HttpServerRequest.HttpServerRequest, boundedRpcRequest(request)), Effect.provideService(RpcSerialization.RpcSerialization, wire));
|
|
26
30
|
});
|
|
27
|
-
})).pipe(Layer.provide(Layer.merge(middleware, hostChanges(options.group, changesService).pipe(Layer.provideMerge(options.handlers)))), Layer.provide(RpcSerialization.layerNdjson));
|
|
31
|
+
})).pipe(Layer.provide(Layer.merge(middleware, hostChanges(options.group, changesService, options.policy === "loopback" ? undefined : options.sessions).pipe(Layer.provideMerge(options.handlers)))), Layer.provide(RpcSerialization.layerNdjson));
|
|
28
32
|
}
|
|
29
33
|
//# sourceMappingURL=hostRpcRoutes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hostRpcRoutes.js","sourceRoot":"","sources":["../src/hostRpcRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjH,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAA2B,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEvE,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAqB,MAAM,uCAAuC,CAAC;AACvH,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"hostRpcRoutes.js","sourceRoot":"","sources":["../src/hostRpcRoutes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect, type Scope } from "effect";
|
|
2
|
+
import { HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
3
|
+
import { type SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
4
|
+
import type { HostPolicy } from "#hostOptions.ts";
|
|
5
|
+
export declare function socketRpc<TSession extends SessionShape>(handler: Effect.Effect<HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest | Scope.Scope>, policy: HostPolicy<TSession>, trustedProxy: string | undefined, nativeOrigins: readonly string[]): Effect.Effect<HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest | Scope.Scope>;
|
|
6
|
+
//# sourceMappingURL=socketRpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socketRpc.d.ts","sourceRoot":"","sources":["../src/socketRpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAW,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAG7E,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,wBAAgB,SAAS,CAAC,QAAQ,SAAS,YAAY,EACtD,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,kBAAkB,EAAE,KAAK,EAAE,iBAAiB,CAAC,iBAAiB,GAAG,KAAK,CAAC,KAAK,CAAC,EACvH,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,aAAa,EAAE,SAAS,MAAM,EAAE,kHAiBhC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Headers, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
3
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
4
|
+
import { localTransport } from "@shivaedev/identity/server/local.ts";
|
|
5
|
+
import { resolveSession } from "@shivaedev/identity/server/session.ts";
|
|
6
|
+
export function socketRpc(handler, policy, trustedProxy, nativeOrigins) {
|
|
7
|
+
return Effect.gen(function* () {
|
|
8
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
9
|
+
if (!localTransport(trustedProxy, nativeOrigins)(request)) {
|
|
10
|
+
return HttpServerResponse.text("Local same-origin requests only", { status: HttpStatus.fromLiteral("Forbidden") });
|
|
11
|
+
}
|
|
12
|
+
if (policy.policy !== "loopback") {
|
|
13
|
+
yield* resolveSession(policy.sessions, Headers.empty, "$connect");
|
|
14
|
+
}
|
|
15
|
+
return yield* handler;
|
|
16
|
+
}).pipe(Effect.catchTags({
|
|
17
|
+
AuthUnavailable: authUnavailable,
|
|
18
|
+
Forbidden: forbidden,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
function authUnavailable() {
|
|
22
|
+
return Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("ServiceUnavailable") }));
|
|
23
|
+
}
|
|
24
|
+
function forbidden() {
|
|
25
|
+
return Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("Forbidden") }));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=socketRpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socketRpc.js","sourceRoot":"","sources":["../src/socketRpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,cAAc,EAAqB,MAAM,uCAAuC,CAAC;AAG1F,MAAM,UAAU,SAAS,CACxB,OAAuH,EACvH,MAA4B,EAC5B,YAAgC,EAChC,aAAgC;IAEhC,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,iBAAiB,CAAC,iBAAiB,CAAC;QAC3D,IAAI,CAAC,cAAc,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3D,OAAO,kBAAkB,CAAC,IAAI,CAAC,iCAAiC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACpH,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAClC,KAAK,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC;IACvB,CAAC,CAAC,CAAC,IAAI,CACN,MAAM,CAAC,SAAS,CAAC;QAChB,eAAe,EAAE,eAAe;QAChC,SAAS,EAAE,SAAS;KACpB,CAAC,CACF,CAAC;AACH,CAAC;AAED,SAAS,eAAe;IACvB,OAAO,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3G,CAAC;AACD,SAAS,SAAS;IACjB,OAAO,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;AAClG,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The Effect application host: HTTP and RPC serving, health, OTLP export, database and migrations, and development mode",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@shivaedev/changes": "0.5.0",
|
|
48
|
-
"@shivaedev/contract": "0.
|
|
48
|
+
"@shivaedev/contract": "0.20.0",
|
|
49
49
|
"@shivaedev/errors": "0.1.0",
|
|
50
50
|
"@shivaedev/http": "0.1.1",
|
|
51
|
-
"@shivaedev/identity": "0.3.
|
|
51
|
+
"@shivaedev/identity": "0.3.2",
|
|
52
52
|
"@shivaedev/local-postgres": "0.7.0",
|
|
53
53
|
"@shivaedev/sql": "0.24.0"
|
|
54
54
|
},
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"@effect/sql-pg": "4.0.5",
|
|
58
58
|
"@effect/vitest": "4.0.5",
|
|
59
59
|
"@shivaedev/jobs": "0.22.0",
|
|
60
|
-
"@shivaedev/quality": "0.44.
|
|
60
|
+
"@shivaedev/quality": "0.44.1",
|
|
61
61
|
"@shivaedev/test": "0.10.0",
|
|
62
62
|
"@shivaedev/test-sql": "0.10.2",
|
|
63
|
-
"@shivaedev/test-story": "0.
|
|
63
|
+
"@shivaedev/test-story": "0.23.0",
|
|
64
64
|
"@types/node": "24.19.2",
|
|
65
65
|
"@types/pg": "8.20.0",
|
|
66
66
|
"better-auth": "1.7.12",
|
package/src/hostChanges.ts
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Option, Stream } from "effect";
|
|
2
|
+
import { Headers } from "effect/http";
|
|
2
3
|
import type { Rpc, RpcGroup } from "effect/rpc";
|
|
4
|
+
import { CurrentViewer, type Viewer } from "@shivaedev/contract/readPolicy.ts";
|
|
3
5
|
import type { QueryReference } from "@shivaedev/contract/serverMetadata.ts";
|
|
6
|
+
import { Unauthorized } from "@shivaedev/errors/taxonomy.ts";
|
|
7
|
+
import { resolveSession, type SessionPolicy, type SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
4
8
|
import { resolveChanges } from "@shivaedev/identity/server/sessionKeys.ts";
|
|
5
9
|
import type { AppChanges } from "./appHost.ts";
|
|
6
10
|
|
|
7
|
-
export function hostChanges<R extends Rpc.Any
|
|
11
|
+
export function hostChanges<R extends Rpc.Any, TSession extends SessionShape>(
|
|
12
|
+
group: RpcGroup.RpcGroup<R>,
|
|
13
|
+
changesService: typeof AppChanges,
|
|
14
|
+
sessions?: SessionPolicy<TSession>,
|
|
15
|
+
) {
|
|
8
16
|
return Layer.effectContext(
|
|
9
17
|
Effect.gen(function* () {
|
|
10
18
|
const changes = yield* Effect.serviceOption(changesService);
|
|
11
19
|
const services = yield* Effect.context<never>();
|
|
12
20
|
const handlers = new Map<string, unknown>();
|
|
21
|
+
function subscribe(rpc: Rpc.Any, queries: readonly QueryReference[]) {
|
|
22
|
+
return Stream.unwrap(
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const { subscription, context } = yield* resolveChanges(rpc, queries);
|
|
25
|
+
const viewer = yield* CurrentViewer;
|
|
26
|
+
const authorized = authorizeSession(sessions, rpc._tag, viewer, subscription.authorized);
|
|
27
|
+
const stream = Option.isSome(changes)
|
|
28
|
+
? changes.value.subscribe({ ...subscription, authorized }, context)
|
|
29
|
+
: Stream.concat(Stream.succeed({ _tag: "Resync" } as const), Stream.never);
|
|
30
|
+
if (sessions === undefined || Option.isNone(viewer)) {
|
|
31
|
+
return stream;
|
|
32
|
+
}
|
|
33
|
+
return Stream.interruptWhen(stream, sessionRevoked(sessions, rpc._tag, viewer.value.id));
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
13
37
|
for (const rpc of group.requests.values()) {
|
|
14
38
|
if (!rpc._tag.endsWith(".$changes")) {
|
|
15
39
|
continue;
|
|
@@ -19,16 +43,39 @@ export function hostChanges<R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, chan
|
|
|
19
43
|
handlers.set(rpc.key, provided);
|
|
20
44
|
continue;
|
|
21
45
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
handlers.set(rpc.key, { context: services, handler, tag: rpc._tag });
|
|
46
|
+
handlers.set(rpc.key, {
|
|
47
|
+
context: services,
|
|
48
|
+
handler: ({ queries }: { readonly queries: readonly QueryReference[] }) => subscribe(rpc, queries),
|
|
49
|
+
tag: rpc._tag,
|
|
50
|
+
});
|
|
30
51
|
}
|
|
31
52
|
return Context.makeUnsafe<Rpc.ToHandler<Extract<R, { readonly _tag: `${string}.$changes` }>>>(handlers);
|
|
32
53
|
}),
|
|
33
54
|
);
|
|
34
55
|
}
|
|
56
|
+
|
|
57
|
+
function authorizeSession<TSession extends SessionShape>(
|
|
58
|
+
sessions: SessionPolicy<TSession> | undefined,
|
|
59
|
+
tag: string,
|
|
60
|
+
viewer: Option.Option<Viewer>,
|
|
61
|
+
policy: Effect.Effect<boolean>,
|
|
62
|
+
) {
|
|
63
|
+
if (sessions === undefined) {
|
|
64
|
+
return policy;
|
|
65
|
+
}
|
|
66
|
+
return resolveSession(sessions, Headers.empty, tag).pipe(
|
|
67
|
+
Effect.map((current) => Option.isNone(viewer) || (Option.isSome(current) && current.value.user.id === viewer.value.id)),
|
|
68
|
+
Effect.catch(() => Effect.succeed(false)),
|
|
69
|
+
Effect.flatMap((allowed) => (allowed ? policy : Effect.succeed(false))),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sessionRevoked<TSession extends SessionShape>(sessions: SessionPolicy<TSession>, tag: string, id: string) {
|
|
74
|
+
return Effect.gen(function* () {
|
|
75
|
+
yield* Effect.sleep(sessions.streamRevalidationInterval ?? "30 seconds");
|
|
76
|
+
const current = yield* resolveSession(sessions, Headers.empty, tag).pipe(Effect.catchTag("AuthUnavailable", () => Effect.succeed(undefined)));
|
|
77
|
+
if (current !== undefined && (Option.isNone(current) || current.value.user.id !== id)) {
|
|
78
|
+
return yield* new Unauthorized({ message: "Authentication required" });
|
|
79
|
+
}
|
|
80
|
+
}).pipe(Effect.forever);
|
|
81
|
+
}
|
package/src/hostRpc.ts
CHANGED
|
@@ -14,13 +14,15 @@ export type { HostedReadPolicy };
|
|
|
14
14
|
export const rpcCacheControl: HttpEffect.PreResponseHandler = (_, response) =>
|
|
15
15
|
Effect.succeed(HttpServerResponse.setHeader(response, "cache-control", "private, no-store"));
|
|
16
16
|
|
|
17
|
-
export const hostRpc = Effect.fnUntraced(function* <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R
|
|
17
|
+
export const hostRpc = Effect.fnUntraced(function* <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>, websocket = false) {
|
|
18
18
|
const guarded = group.middleware(HostedReadPolicy);
|
|
19
19
|
// Keep the policy inside identity middleware so it sees the request's viewer.
|
|
20
20
|
for (const rpc of guarded.requests.values()) {
|
|
21
21
|
Object.assign(rpc, { middlewares: new Set([HostedReadPolicy, ...rpc.middlewares]) });
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
const traced = tracedGroup(guarded);
|
|
24
|
+
const handler = websocket ? RpcServer.toHttpEffectWebsocket(traced) : RpcServer.toHttpEffect(traced);
|
|
25
|
+
return yield* handler.pipe(
|
|
24
26
|
Effect.provideService(HostedReadPolicy, (effect, request) => Effect.flatMap(CurrentViewer, (viewer) => authorizeReads(effect, request, viewer))),
|
|
25
27
|
);
|
|
26
28
|
});
|
package/src/hostRpcRoutes.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { hostRpc, rpcCacheControl } from "#hostRpc.ts";
|
|
|
14
14
|
import type { hostTransport } from "#hostTransport.ts";
|
|
15
15
|
import { redactingRpcSerialization } from "#redactingRpcSerialization.ts";
|
|
16
16
|
import type { schemaSensitiveKeys } from "#schemaSensitiveKeys.ts";
|
|
17
|
+
import { socketRpc } from "#socketRpc.ts";
|
|
17
18
|
|
|
18
19
|
export function hostRpcRoutes<TRpcs extends Rpc.Any, THandlers, TError, TRequirements, TSession extends SessionShape>(
|
|
19
20
|
options: {
|
|
@@ -32,8 +33,10 @@ export function hostRpcRoutes<TRpcs extends Rpc.Any, THandlers, TError, TRequire
|
|
|
32
33
|
return HttpRouter.use((router) =>
|
|
33
34
|
Effect.gen(function* () {
|
|
34
35
|
const wire = redactingRpcSerialization(yield* RpcSerialization.RpcSerialization, sensitive, sensitive.maskFailure);
|
|
35
|
-
const
|
|
36
|
+
const httpGroup = options.group.omit(...[...options.group.requests.keys()].filter((tag) => tag.endsWith(".$changes")));
|
|
37
|
+
const handler = yield* hostRpc(httpGroup).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire));
|
|
36
38
|
const guarded = localRpcGuard(handler, trustedProxy, nativeOrigins);
|
|
39
|
+
const websocket = yield* hostRpc(options.group, true).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire));
|
|
37
40
|
yield* router.add(
|
|
38
41
|
"OPTIONS",
|
|
39
42
|
options.rpcPath ?? "/rpc",
|
|
@@ -41,6 +44,11 @@ export function hostRpcRoutes<TRpcs extends Rpc.Any, THandlers, TError, TRequire
|
|
|
41
44
|
status: HttpStatus.fromLiteral("Forbidden"),
|
|
42
45
|
}),
|
|
43
46
|
);
|
|
47
|
+
yield* router.add(
|
|
48
|
+
"GET",
|
|
49
|
+
options.rpcPath ?? "/rpc",
|
|
50
|
+
socketRpc(websocket, options, trustedProxy, nativeOrigins).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire)),
|
|
51
|
+
);
|
|
44
52
|
yield* router.add("POST", options.rpcPath ?? "/rpc", (request) => {
|
|
45
53
|
HttpEffect.appendPreResponseHandlerUnsafe(request, rpcCacheControl);
|
|
46
54
|
return guarded.pipe(
|
|
@@ -51,7 +59,14 @@ export function hostRpcRoutes<TRpcs extends Rpc.Any, THandlers, TError, TRequire
|
|
|
51
59
|
});
|
|
52
60
|
}),
|
|
53
61
|
).pipe(
|
|
54
|
-
Layer.provide(
|
|
62
|
+
Layer.provide(
|
|
63
|
+
Layer.merge(
|
|
64
|
+
middleware,
|
|
65
|
+
hostChanges(options.group, changesService, options.policy === "loopback" ? undefined : options.sessions).pipe(
|
|
66
|
+
Layer.provideMerge(options.handlers),
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
55
70
|
Layer.provide(RpcSerialization.layerNdjson),
|
|
56
71
|
);
|
|
57
72
|
}
|
package/src/socketRpc.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Effect, type Scope } from "effect";
|
|
2
|
+
import { Headers, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
3
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
4
|
+
import { localTransport } from "@shivaedev/identity/server/local.ts";
|
|
5
|
+
import { resolveSession, type SessionShape } from "@shivaedev/identity/server/session.ts";
|
|
6
|
+
import type { HostPolicy } from "#hostOptions.ts";
|
|
7
|
+
|
|
8
|
+
export function socketRpc<TSession extends SessionShape>(
|
|
9
|
+
handler: Effect.Effect<HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest | Scope.Scope>,
|
|
10
|
+
policy: HostPolicy<TSession>,
|
|
11
|
+
trustedProxy: string | undefined,
|
|
12
|
+
nativeOrigins: readonly string[],
|
|
13
|
+
) {
|
|
14
|
+
return Effect.gen(function* () {
|
|
15
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
16
|
+
if (!localTransport(trustedProxy, nativeOrigins)(request)) {
|
|
17
|
+
return HttpServerResponse.text("Local same-origin requests only", { status: HttpStatus.fromLiteral("Forbidden") });
|
|
18
|
+
}
|
|
19
|
+
if (policy.policy !== "loopback") {
|
|
20
|
+
yield* resolveSession(policy.sessions, Headers.empty, "$connect");
|
|
21
|
+
}
|
|
22
|
+
return yield* handler;
|
|
23
|
+
}).pipe(
|
|
24
|
+
Effect.catchTags({
|
|
25
|
+
AuthUnavailable: authUnavailable,
|
|
26
|
+
Forbidden: forbidden,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function authUnavailable() {
|
|
32
|
+
return Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("ServiceUnavailable") }));
|
|
33
|
+
}
|
|
34
|
+
function forbidden() {
|
|
35
|
+
return Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("Forbidden") }));
|
|
36
|
+
}
|