@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 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 NDJSON RPC at `/rpc`, and provides SQL, Reactivity and `AppChanges` to those Layers.
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 native `AtomRpc.Service`, NDJSON HTTP protocol and contract `bind` as other queries:
191
+ On the client, use the same contract `makeClient` and shared WebSocket as other queries:
192
192
 
193
193
  ```ts
194
- import { Layer } from "effect";
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
- class ConfigClient extends AtomRpc.Service<ConfigClient>()("app/ConfigClient", {
203
- group: PublicConfig,
204
- protocol: RpcClient.layerProtocolHttp({ url: "/rpc" }).pipe(Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson])),
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());
@@ -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;AAChE,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,wBAAgB,WAAW,CAAC,CAAC,SAAS,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,UAAU;mBAwBtC,GAAG,MAAM,WAAW;mBAG1F"}
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"}
@@ -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
- function handler({ queries }) {
18
- return Stream.unwrap(Effect.map(resolveChanges(rpc, queries), ({ subscription, context }) => Option.isSome(changes) ? changes.value.subscribe(subscription, context) : Stream.never));
19
- }
20
- handlers.set(rpc.key, { context: services, handler, tag: rpc._tag });
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
@@ -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;AAGhE,OAAO,EAAE,cAAc,EAAE,MAAM,2CAA2C,CAAC;AAG3E,MAAM,UAAU,WAAW,CAAoB,KAA2B,EAAE,cAAiC;IAC5G,OAAO,KAAK,CAAC,aAAa,CACzB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,EAAS,CAAC;QAChD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;QAC5C,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;gBACrC,SAAS;YACV,CAAC;YACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjD,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACtD,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;gBAChC,SAAS;YACV,CAAC;YACD,SAAS,OAAO,CAAC,EAAE,OAAO,EAAmD;gBAC5E,OAAO,MAAM,CAAC,MAAM,CACnB,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,EAAE,CACtE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACtF,CACD,CAAC;YACH,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACtE,CAAC;QACD,OAAO,OAAO,CAAC,UAAU,CAAqE,QAAQ,CAAC,CAAC;IACzG,CAAC,CAAC,CACF,CAAC;AACH,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>) => 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>>;
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
@@ -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,0zBASnE,CAAC"}
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
- return yield* RpcServer.toHttpEffect(tracedGroup(guarded)).pipe(Effect.provideService(HostedReadPolicy, (effect, request) => Effect.flatMap(CurrentViewer, (viewer) => authorizeReads(effect, request, viewer))));
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
@@ -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;IACjG,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,OAAO,KAAK,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAC9D,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
+ {"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;AAEnE,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;;;;;;;;0CA+BjD"}
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"}
@@ -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 handler = yield* hostRpc(options.group).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire));
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;AAG1E,MAAM,UAAU,aAAa,CAC5B,OAIwB,EACxB,cAAiC,EACjC,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAoD,EAC3F,SAAiD;IAEjD,MAAM,UAAU,GACf,OAAO,CAAC,MAAM,KAAK,UAAU;QAC5B,CAAC,CAAC,mBAAmB,CAAC,EAAE,SAAS,EAAE,CAAC;QACpC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,uBAAuB,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IACxI,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAChC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,IAAI,GAAG,yBAAyB,CAAC,KAAK,CAAC,CAAC,gBAAgB,CAAC,gBAAgB,EAAE,SAAS,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;QACnH,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,gBAAgB,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC;QACnH,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;QACpE,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAChB,SAAS,EACT,OAAO,CAAC,OAAO,IAAI,MAAM,EACzB,kBAAkB,CAAC,IAAI,CAAC,iCAAiC,EAAE;YAC1D,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,WAAW,CAAC;SAC3C,CAAC,CACF,CAAC;QACF,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,IAAI,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE;YAChE,UAAU,CAAC,8BAA8B,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;YACpE,OAAO,OAAO,CAAC,IAAI,CAClB,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,WAAW,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAChF,MAAM,CAAC,cAAc,CAAC,iBAAiB,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC,EACtF,MAAM,CAAC,cAAc,CAAC,gBAAgB,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAC9D,CAAC;QACH,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CACF,CAAC,IAAI,CACL,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC7H,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAC3C,CAAC;AACH,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.1",
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.19.1",
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.1",
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.0",
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.22.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",
@@ -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>(group: RpcGroup.RpcGroup<R>, changesService: typeof AppChanges) {
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
- function handler({ queries }: { readonly queries: readonly QueryReference[] }) {
23
- return Stream.unwrap(
24
- Effect.map(resolveChanges(rpc, queries), ({ subscription, context }) =>
25
- Option.isSome(changes) ? changes.value.subscribe(subscription, context) : Stream.never,
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
- return yield* RpcServer.toHttpEffect(tracedGroup(guarded)).pipe(
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
  });
@@ -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 handler = yield* hostRpc(options.group).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire));
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(Layer.merge(middleware, hostChanges(options.group, changesService).pipe(Layer.provideMerge(options.handlers)))),
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
  }
@@ -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
+ }