@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 - 2026-10-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `appHost` accepts `rpcPath` to choose the NDJSON RPC route; it still defaults to `/rpc`.
|
|
8
|
+
- `appHost` accepts `policy: "loopback"` for local RPC without a session provider; `AppLocalHostOptions` describes this form. The transport still requires a loopback peer and Host, matching or absent Origin, and NDJSON content type; proxy trust does not apply to this policy.
|
|
9
|
+
- `appHost` can omit `migrations` to start without a PostgreSQL pool or database configuration. `AppHttpHostOptions` describes this form while `AppHostOptions` retains its required migrations. Applications can provide an `appDatabase` layer through `services`; existing callers that supply migrations keep the migrated database composition.
|
|
10
|
+
- `appDatabase` and the migrated `appHost` accept `config.maxConnections` to bound the PostgreSQL pool.
|
|
11
|
+
- `appHost` forwards `config.disablePreemptiveShutdown` to the native Node host so a scoped daemon can interrupt its RPC streams during shutdown. The native default remains unchanged when omitted.
|
|
12
|
+
- `appHost` accepts an optional native HTTP router layer through `routes`, so application endpoints such as Better Auth share the RPC and asset host.
|
|
13
|
+
- `appHost.assets` also accepts native static-server options, including `spa: true` for browser route deep links. Existing directory strings keep their behavior.
|
|
14
|
+
- `AppHostOptions.config.trustedProxy` / `TRUSTED_PROXY` and `localRpcGuard(effect, trustedProxy?)` accept comma-separated IPv4 and IPv6 addresses and CIDR ranges, including IPv4-mapped IPv6 peers. Invalid proxy addresses or ranges fail `appHost` startup with a typed `ConfigError` before migrations, services or HTTP start.
|
|
15
|
+
- `appHost` accepts `config.maxBodySize` and the `MAX_BODY_SIZE` environment variable to limit RPC request bodies in bytes; explicit config takes precedence.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `appHost` accepts native router layers with typed request error annotations in `routes`, so applications can keep their route failures typed through the HTTP boundary.
|
|
20
|
+
- RPC request bodies default to a 1 MiB limit. Oversized NDJSON requests return a 4xx before any RPC handler runs.
|
|
21
|
+
|
|
22
|
+
### Breaking
|
|
23
|
+
|
|
24
|
+
- Requires stable Effect 4.0.4 in place of 4.0.0-rc.112. Effect 4.0.4 moves the former `effect/unstable/*` modules to `effect/*` and renames constructors such as `Config.string` to `Config.String`. Install `@effect/platform-node`, `@effect/sql-pg` and `@effect/vitest` 4.0.4 alongside it; `@effect/vitest` 4.0.4 requires Vitest 5.
|
|
25
|
+
|
|
26
|
+
## 0.6.1 - 2026-10-12
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `appHost`, `appDatabase`, `AppChanges`, `AppHostOptions` and `AppDatabaseOptions` in `appHost.ts`: one scoped local HTTP/NDJSON RPC host with transport guard, session middleware, static assets, migrated PostgreSQL and commit-bound live hints; the database Layer also serves workers. The host requires the optional `@effect/platform-node` and `@effect/sql-pg` peers.
|
|
31
|
+
- `AppHostOptions.config.host` / `HOST` and `config.trustedProxy` / `TRUSTED_PROXY` allow a non-loopback listener and same-origin HTTP/NDJSON RPC through an exact trusted proxy peer. `localRpcGuard(effect, trustedProxy?)` accepts the same proxy address; loopback defaults and typed RPC refusals are preserved.
|
|
32
|
+
- `rpc-server/local.ts` exports `localRpcGuard(effect)` to reject local HTTP RPC requests before the handler runs unless their remote address and Host are loopback, their Origin is absent or matches the HTTP Host, and their content type matches the native RPC serialization. It needs no session provider. The module also exports `loopbackOnly(handler)` and `sameOrigin(request)` for local HTTP routes.
|
|
33
|
+
- Export `InternalServerError` and `ServiceUnavailable` Schema errors from `errors/taxonomy.ts` for RPC contracts without authentication middleware.
|
|
34
|
+
- Export `rpcBoundary(schema, { unexpected, message? })` and `RpcBoundaryOptions` from `rpc-server/boundary.ts`. Declared rejections keep their identity; unexpected failures and defects become one declared rejection with a redacted public message and the original server-only cause. Success and interruption remain intact.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Pin the optional `better-auth` peer dependency to 1.6.33, the current 1.6
|
|
39
|
+
release, up from 1.6.30.
|
|
40
|
+
- Keep the existing core defining-module import paths through explicit exports while grouping their peer-free implementation under `src/core/` and enforcing its optional-peer boundary from the package manifest.
|
|
41
|
+
- Prefix testing harness type parameters with `T`; their constraints, defaults and inferred types stay the same.
|
|
42
|
+
- Document the package purpose, request boundaries, integrations and API with agent guidance, a north star and a roadmap; publish `docs/` alongside the README.
|
|
43
|
+
- Build package JavaScript, declarations and source maps with TypeScript 7.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Declare `@shivaedev/types` as a dependency of the shipped testing entry point instead of a development-only dependency.
|
|
48
|
+
|
|
3
49
|
## 0.6.0 - 2026-10-04
|
|
4
50
|
|
|
5
51
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,161 +1,420 @@
|
|
|
1
1
|
# @shivaedev/platform
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
one consistent application shape over supporting every possible combination of
|
|
5
|
-
database, transport, and test framework.
|
|
3
|
+
Shared errors, request identity and server boundaries for Effect applications. A feature can say who called it and why it refused, while the application supplies its session policy once. The package also connects Effect services to Promise callers, Node subscriptions and optional Better Auth, Prisma and tRPC integrations.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
`@shivaedev/effect-prisma` and `@shivaedev/effect-trpc`.
|
|
5
|
+
## Why you want this
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
`errors/`, `rpc/` and `rpc-server/` need nothing else. Install the optional peers
|
|
12
|
-
for the modules that use them:
|
|
7
|
+
Every feature needs the same boundary questions answered: which user made this request, is the session missing or its provider unavailable, and which failures can the caller act on? Writing that glue repeatedly makes it harder to see the feature's own decision:
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
```ts
|
|
10
|
+
const Handlers = Account.toLayer({
|
|
11
|
+
ReadOwn: ({ userId }) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const identity = yield* Identity;
|
|
14
|
+
if (identity.id !== userId) {
|
|
15
|
+
return yield* new Forbidden({ message: "Not your account" });
|
|
16
|
+
}
|
|
17
|
+
return identity.id;
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`Account` is a native RPC group with authenticated middleware. Its handler reads the identity for this invocation and decides whether that user may read the account. Platform supplies the common error classes and request services; feature code keeps the resource-ownership check. Concurrent requests receive their own identities, and a provider outage reaches the caller as `AuthUnavailable` instead of pretending the caller signed out.
|
|
23
|
+
|
|
24
|
+
## Using it
|
|
25
|
+
|
|
26
|
+
### How to think about it
|
|
27
|
+
|
|
28
|
+
There are three parts to a native RPC boundary:
|
|
29
|
+
|
|
30
|
+
1. **The shared contract** names Schema errors and middleware tags. The browser imports these declarations too, so `errors/` and `rpc/` contain browser-safe data and services.
|
|
31
|
+
2. **The server composition** implements the tags with Layers from `rpc-server/`. A session provider resolves a session; an Origin policy decides whether its request may reach that provider; tracing supplies a request id and diagnostic context.
|
|
32
|
+
3. **The feature handler** reads `Identity`, `OptionalIdentity` or `RequestId` and runs application code. Authentication supplies a user id. Authorization still decides whether that user may perform this operation.
|
|
33
|
+
|
|
34
|
+
An **Origin policy** is a function of the transport request's origin, headers and RPC name. A **session provider** returns `Option<Session>` or fails with `AuthUnavailable`; a session carries `user.id`. These are application inputs, not a second authentication framework.
|
|
35
|
+
|
|
36
|
+
The optional modules solve different boundaries. `runtime/` carries Effect services into Promise callers, `node-http/` combines subscription cancellation sources, `better-auth/adapter.ts` connects Better Auth storage to Effect Prisma, and `testing/vitest.ts` composes the Prisma and tRPC test harnesses. Choose the modules that match the application rather than adopting every integration.
|
|
37
|
+
|
|
38
|
+
### Host the application
|
|
39
|
+
|
|
40
|
+
`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.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { NodeRuntime } from "@effect/platform-node";
|
|
44
|
+
import { Layer } from "effect";
|
|
45
|
+
import { appHost } from "@shivaedev/platform/appHost.ts";
|
|
46
|
+
import { Api, Handlers, Services, migrations, sessions } from "./app.ts";
|
|
47
|
+
|
|
48
|
+
const Server = appHost({
|
|
49
|
+
group: Api,
|
|
50
|
+
handlers: Handlers,
|
|
51
|
+
services: Services,
|
|
52
|
+
migrations: { loader: migrations },
|
|
53
|
+
sessions,
|
|
54
|
+
assets: "./public",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
NodeRuntime.runMain(Layer.launch(Server));
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`app.ts` exports the application's contracts, native Layers, numbered `Migrator.Loader` and `SessionPolicy`. The host needs the optional peers `@effect/platform-node` and `@effect/sql-pg` at the workspace's Effect version; install both for this entry point. The `errors/`, `node-http/`, `rpc/`, `rpc-server/` and `runtime/` entries still work without them.
|
|
61
|
+
|
|
62
|
+
`assets` accepts a directory string or native `HttpStaticServer.layer` options. For a browser app whose routes must also work on direct navigation or reload, use `{ root: "dist", spa: true }` or merge `HttpStaticServer.layer({ root: "dist", spa: true })` from `effect/http` alongside the handler Layer. Native route Layers can register additional endpoints in that same composition; `routes` also accepts a native `Layer<never, never, HttpRouter>` for endpoints on the same listener. The [starter](https://github.com/ShivaeDev/platform/tree/main/examples/starter) uses it for static assets and Better Auth at `/api/auth/*`, with provider-owned cookies and the host's RPC session middleware.
|
|
63
|
+
|
|
64
|
+
The listener uses `HOST` or `127.0.0.1`, uses `PORT` or 3000, and reads `DATABASE_URL`. Set overrides in the one `config` object: `host`, `port` (0 selects an ephemeral port), `trustedProxy`, `maxBodySize`, `databaseUrl` (an Effect `Redacted<string>`), `applicationName`, and optional `changes`. Migration options accept `table` and `lockTimeout` (default `effect_sql_migrations` and five seconds). Native Layer failures remain in the build error channel; missing application dependencies remain in the requirement type.
|
|
65
|
+
|
|
66
|
+
RPC request bodies are limited to 1 MiB by default. Set `MAX_BODY_SIZE` to a byte count or override it with `config.maxBodySize`, a nonnegative safe integer. An oversized NDJSON body returns a 4xx before any RPC handler runs; real HTTP tests cover fixed-length and chunked requests, byte counting, and configuration precedence. This limit applies to the host's RPC route.
|
|
67
|
+
|
|
68
|
+
RPC uses the existing `localRpcGuard`: it requires a loopback peer and Host, same-origin or absent Origin, and the NDJSON content type. Session lookup uses transport headers. With a session policy, contracts declare `Authenticated`, `MaybeAuthenticated` and `RequestTracing` as needed; the host supplies their implementations. Declared errors retain their Schema types. Applications still own resource authorization and use `rpcBoundary` when they need to map unexpected failures.
|
|
69
|
+
|
|
70
|
+
For local tools, use `policy: "loopback"` with contracts that do not use session middleware to serve RPC without a session provider. The guard requires a loopback peer and Host, absent or same-origin Origin, and NDJSON content type. This policy does not use `TRUSTED_PROXY` or `config.trustedProxy`. Set `rpcPath` to choose another native RPC route; `/rpc` remains the default.
|
|
18
71
|
|
|
19
|
-
|
|
72
|
+
Omit `migrations` to host an app without a database. This form creates no SQL pool and reads no `DATABASE_URL`; `AppHttpHostOptions` describes it. `AppHostOptions` retains its required migrations for existing typed callers. A service that needs SQL still requires a SQL Layer. Provide `appDatabase` to the application’s service Layer when it owns database composition, as Fleet's daemon does with its migration loader and namespace ledger. Real HTTP tests cover the custom route, session-free loopback guard and database-free startup; Fleet's PostgreSQL daemon tests cover the adopted composition.
|
|
20
73
|
|
|
21
|
-
|
|
74
|
+
Set `config.maxConnections` on `appDatabase` or a migrated `appHost` to bound the pool. A daemon with long-lived RPC streams can use `config.disablePreemptiveShutdown: true` to let scope closure interrupt those streams before closing the listener; omitting it keeps Node's native graceful-shutdown default. Fleet's daemon tests cover one bounded pool and scoped provider and listener cleanup, including follower reconnection after shutdown.
|
|
75
|
+
|
|
76
|
+
For a deployment behind a proxy, set `HOST=0.0.0.0` and `TRUSTED_PROXY` to a comma-separated list of proxy peer IPs or CIDR ranges (IPv4 or IPv6), or use `config.host` and `config.trustedProxy`. Only matching peers may supply `X-Forwarded-Host` and `X-Forwarded-Proto` (`http` or `https`) for the guard's same-origin check; the proxy must overwrite both headers and preserve Origin. A single authority is required, and comma-separated forwarding chains are refused. IPv4-mapped IPv6 peers match their IPv4 address. Malformed addresses or ranges fail `appHost` startup with a typed `ConfigError`. NDJSON and the application's session Origin policy still apply. Real forwarding-proxy tests cover trusted and untrusted peers, configuration overrides and typed refusals; they do not establish any hosting provider's proxy configuration. See the [deployment quickstart](https://github.com/ShivaeDev/platform/blob/main/docs/framework/quickstart.md#deploy).
|
|
77
|
+
|
|
78
|
+
A subscription handler reads `AppChanges` and returns `changes.stream(keys)` after its authorization check. Writes use `transact` and `invalidateOnCommit` from effect-sql: local streams receive committed invalidations and rollback emits none. For delivery between processes, set `config.changes: { channel: "app_changes" }`; the host binds PostgreSQL LISTEN/NOTIFY to the same SQL pool and resyncs streams when its listener recovers. The native heartbeat and retry options may be supplied in `changes`. A worker can provide `appDatabase({ migrations, config })` to its service Layer to share this database and delivery composition without starting HTTP.
|
|
79
|
+
|
|
80
|
+
The [composed PostgreSQL fixture](https://github.com/ShivaeDev/platform/blob/main/docs/framework/composed-example.md) uses this host for its HTTP servers and `appDatabase` for its worker. It proves commit visibility, rollback silence, typed refusals and delivery between server processes. Platform's host tests also cover static files, environment configuration, guard rejection before session lookup, startup ordering, migration failure and scoped shutdown.
|
|
81
|
+
|
|
82
|
+
### 1. Once per application: contracts and session policy
|
|
83
|
+
|
|
84
|
+
Declare the RPC group in a shared module:
|
|
22
85
|
|
|
23
86
|
```ts
|
|
24
|
-
import {
|
|
87
|
+
import { Effect, Schema } from "effect";
|
|
88
|
+
import { Rpc, RpcGroup } from "effect/rpc";
|
|
89
|
+
import { Forbidden } from "@shivaedev/platform/errors/taxonomy.ts";
|
|
90
|
+
import { Identity } from "@shivaedev/platform/rpc/identity.ts";
|
|
91
|
+
import { Authenticated, RequestTracing } from "@shivaedev/platform/rpc/middleware.ts";
|
|
92
|
+
|
|
93
|
+
export const Account = RpcGroup.make(
|
|
94
|
+
Rpc.make("ReadOwn", {
|
|
95
|
+
error: Forbidden,
|
|
96
|
+
payload: { userId: Schema.String },
|
|
97
|
+
success: Schema.String,
|
|
98
|
+
}),
|
|
99
|
+
).middleware(Authenticated).middleware(RequestTracing);
|
|
100
|
+
|
|
101
|
+
export const Handlers = Account.toLayer({
|
|
102
|
+
ReadOwn: ({ userId }) =>
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const identity = yield* Identity;
|
|
105
|
+
if (identity.id !== userId) {
|
|
106
|
+
return yield* new Forbidden({ message: "Not your account" });
|
|
107
|
+
}
|
|
108
|
+
return identity.id;
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The middleware declaration makes `Identity` available to guarded handlers and adds `Unauthorized`, `AuthUnavailable` and `Forbidden` to the client's failure type. An unguarded handler that requires `Identity` cannot become a service-free handler Layer. Keep the group declaration in the shared module and the handler implementation on the server when organizing application files.
|
|
25
114
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
115
|
+
Supply a provider and an explicit Origin policy as the host's `sessions` input. For a Better Auth provider, adapt its existing session lookup:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { betterAuthSessions } from "@shivaedev/platform/rpc-server/adapters/better-auth-sessions.ts";
|
|
119
|
+
import { trustedOrigins } from "@shivaedev/platform/rpc-server/origin.ts";
|
|
120
|
+
import { auth } from "./auth.ts";
|
|
121
|
+
|
|
122
|
+
export const sessions = {
|
|
123
|
+
origin: trustedOrigins({ allow: ["http://localhost:3000"], missing: "allow" }),
|
|
124
|
+
provider: betterAuthSessions((headers) => auth.api.getSession({ headers })),
|
|
125
|
+
};
|
|
29
126
|
```
|
|
30
127
|
|
|
31
|
-
The
|
|
32
|
-
The runtime also carries transaction- and request-scoped service overrides
|
|
33
|
-
across promise boundaries.
|
|
128
|
+
`trustedOrigins({ allow, missing })` accepts an exact allowlist and supports `missing: "reject"` or `"allow"`; a custom `OriginPolicy` can decide from the RPC name or transport headers. A denied policy fails with `Forbidden` before provider lookup. The host's HTTP guard also checks Origin against the local request's Host.
|
|
34
129
|
|
|
35
|
-
|
|
130
|
+
The application's `auth` owns its storage and session configuration. A null lookup becomes a missing session. A thrown or rejected lookup is logged in redacted form and fails with the generic `AuthUnavailable` message. It remains a failure even on an RPC with optional authentication.
|
|
36
131
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
132
|
+
### Transport headers and request ids
|
|
133
|
+
|
|
134
|
+
Session resolution reads `HttpServerRequest`: the HTTP POST or WebSocket upgrade request. Credentials and Origin supplied inside an RPC message cannot replace those transport headers. An application-owned middleware can use `transportHeaders(rpcHeaders)` for the same source of headers.
|
|
135
|
+
|
|
136
|
+
Tracing uses the RPC headers for correlation. `requestTracingLayer()` accepts `x-request-id`; `{ header: "x-correlation-id" }` changes the name. Missing, malformed or oversized values receive generated 32-character lowercase hexadecimal ids. A request id reaches the `RequestId` service, log annotations as `requestId`, and the server span as `request.id`; the RPC method reaches both as `rpc.method`.
|
|
137
|
+
|
|
138
|
+
Caller-chosen request ids are correlation data. Keep authorization tied to the authenticated identity.
|
|
139
|
+
|
|
140
|
+
### 2. Once per feature: identity and typed refusals
|
|
141
|
+
|
|
142
|
+
Use `Authenticated` when the operation needs `Identity`, and `MaybeAuthenticated` with `maybeAuthenticatedLayer(policy)` when it reads `OptionalIdentity`:
|
|
41
143
|
|
|
42
144
|
```ts
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
145
|
+
import { Effect, Option, Schema } from "effect";
|
|
146
|
+
import { Rpc, RpcGroup } from "effect/rpc";
|
|
147
|
+
import { OptionalIdentity } from "@shivaedev/platform/rpc/identity.ts";
|
|
148
|
+
import { MaybeAuthenticated } from "@shivaedev/platform/rpc/middleware.ts";
|
|
149
|
+
|
|
150
|
+
export const Greetings = RpcGroup.make(
|
|
151
|
+
Rpc.make("Greeting", { success: Schema.String }),
|
|
152
|
+
).middleware(MaybeAuthenticated);
|
|
153
|
+
|
|
154
|
+
export const GreetingHandlers = Greetings.toLayer({
|
|
155
|
+
Greeting: () =>
|
|
156
|
+
Effect.map(OptionalIdentity, (identity) =>
|
|
157
|
+
Option.match(identity, {
|
|
158
|
+
onNone: () => "hello anonymous",
|
|
159
|
+
onSome: ({ id }) => `hello ${id}`,
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
});
|
|
56
163
|
```
|
|
57
164
|
|
|
58
|
-
The
|
|
59
|
-
|
|
165
|
+
A missing session produces `Option.none()` here. The same policy still rejects forbidden origins and distinguishes provider failure from an anonymous caller. Signed-in authenticated requests annotate the user's id on logs as `userId` and spans as `user.id`.
|
|
166
|
+
|
|
167
|
+
For an HTTP RPC server that serves only the local machine, wrap its HTTP effect with `localRpcGuard` from `@shivaedev/platform/rpc-server/local.ts`. Provide the native `HttpServerRequest` and `RpcSerialization` services around the returned effect, using the same serialization as the RPC server. No session provider is required.
|
|
168
|
+
|
|
169
|
+
By default, the guard returns HTTP 403 before running the effect unless the transport reports a loopback peer address, the Host is a loopback host, any Origin matches the HTTP Host, and the content type matches the serialization. An absent peer address is rejected. Real Node HTTP tests cover accepted local requests and rejected Origin, Host and content-type requests, including a DNS-rebinding Host on a loopback connection. The optional second argument, `localRpcGuard(effect, trustedProxy)`, enables the same address and range forwarding policy as `appHost`.
|
|
60
170
|
|
|
61
|
-
|
|
171
|
+
The same module exports `loopbackOnly(handler)` for local page routes and `sameOrigin(request)` for an HTTP Origin check. `loopbackOnly` checks Host and any known peer address; it permits an absent peer address so embedded Fetch page handlers remain usable. It does not check Origin or content type.
|
|
62
172
|
|
|
63
|
-
|
|
64
|
-
Prisma database and runtime, including Better Auth transactions:
|
|
173
|
+
Declare expected refusals in the RPC's error schema:
|
|
65
174
|
|
|
66
175
|
```ts
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
176
|
+
import { Schema } from "effect";
|
|
177
|
+
import { Rpc } from "effect/rpc";
|
|
178
|
+
import { BadRequest, Conflict } from "@shivaedev/platform/errors/taxonomy.ts";
|
|
179
|
+
|
|
180
|
+
export const Rename = Rpc.make("Rename", {
|
|
181
|
+
error: Schema.Union([BadRequest, Conflict]),
|
|
182
|
+
payload: { name: Schema.String },
|
|
183
|
+
success: Schema.String,
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A handler fails with `new Conflict({ field: "name", message: "Name is taken" })`. Native RPC JSON decodes the rejection back to the error instance with its field. Taxonomy errors can be declared on an RPC with no authentication middleware. `rejectedField` reads either that instance or tagged JSON:
|
|
69
188
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
189
|
+
```ts
|
|
190
|
+
import { rejectedField } from "@shivaedev/platform/errors/rejected-field.ts";
|
|
191
|
+
import { Conflict } from "@shivaedev/platform/errors/taxonomy.ts";
|
|
192
|
+
|
|
193
|
+
const field = rejectedField(
|
|
194
|
+
new Conflict({ field: "name", message: "Name is taken" }),
|
|
195
|
+
);
|
|
73
196
|
```
|
|
74
197
|
|
|
75
|
-
|
|
76
|
-
session policy, and authorization remain application configuration. Native
|
|
77
|
-
experimental Better Auth joins are not supported; the standard adapter join
|
|
78
|
-
fallback remains available.
|
|
198
|
+
`field` is `Option.some({ field: "name", message: "Name is taken" })`. Any value with string `_tag`, `field` and `message` qualifies, including an application's own tagged error. An untagged object, a missing field or a non-string field yields `Option.none()`.
|
|
79
199
|
|
|
80
|
-
|
|
200
|
+
For tRPC, use these classes with the rejection schema and formatter from [effect-trpc](https://github.com/ShivaeDev/platform/tree/main/packages/effect-trpc). The composed HTTP boundary carries rejection data and maps statuses, including `TooManyRequests` as 429 and `AuthUnavailable` as 503.
|
|
81
201
|
|
|
82
|
-
|
|
83
|
-
`NotFound`, `Unauthorized`, `Forbidden`, `BadRequest` and `Conflict` (both with an
|
|
84
|
-
optional `field`), `PreconditionFailed`, `TooManyRequests` and `AuthUnavailable`.
|
|
85
|
-
Use them as native RPC error schemas or effect-contract rejections.
|
|
86
|
-
`rejectedField(error)` from `@shivaedev/platform/errors/rejected-field.ts` extracts `{ field, message }` from any field rejection so a
|
|
87
|
-
form can show it.
|
|
202
|
+
### Handling unexpected failures
|
|
88
203
|
|
|
89
|
-
|
|
90
|
-
`MaybeAuthenticated` middleware, and `@shivaedev/platform/rpc/identity.ts` declares the
|
|
91
|
-
`RequestId`, `Identity` and `OptionalIdentity` services they provide.
|
|
92
|
-
These modules are browser-safe, so shared `RpcGroup` declarations can use them. Implement
|
|
93
|
-
the middleware on the server with the modules under `@shivaedev/platform/rpc-server/`:
|
|
204
|
+
At the handler edge, `rpcBoundary(schema, options)` keeps declared rejections and maps other failures and defects into one declared fallback. It leaves successful values and interruption intact:
|
|
94
205
|
|
|
95
206
|
```ts
|
|
96
|
-
import {
|
|
97
|
-
import {
|
|
98
|
-
import {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
207
|
+
import { Schema } from "effect";
|
|
208
|
+
import { Conflict, InternalServerError } from "@shivaedev/platform/errors/taxonomy.ts";
|
|
209
|
+
import { rpcBoundary } from "@shivaedev/platform/rpc-server/boundary.ts";
|
|
210
|
+
|
|
211
|
+
const Rejection = Schema.Union([Conflict, InternalServerError]);
|
|
212
|
+
const expose = rpcBoundary(Rejection, {
|
|
213
|
+
unexpected: (message) => new InternalServerError({ message }),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export const save = storageSave.pipe(expose);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`storageSave` is the application's Effect. `unexpected` constructs a member of the declared schema from the public message. The default message is `Internal server error`; `message` supplies application text, with credential-shaped strings redacted. The original Effect cause is kept on the server rejection's non-enumerable `cause`, so an RPC server span can trace it. Keep `cause` out of the wire schema, as the taxonomy classes do. `ServiceUnavailable` describes a declared availability failure without implying a session-provider outage.
|
|
220
|
+
|
|
221
|
+
The native HTTP RPC fixture in `src/server/rpc-server/unauthenticatedRejections.spec.ts` proves decoded taxonomy types, redacted fallback messages and the original server trace cause without session middleware. Work Board's `src/server/rpc/rejectionTypes.spec.ts` proves its adoption through real loopback HTTP.
|
|
222
|
+
|
|
223
|
+
### Request diagnostics
|
|
224
|
+
|
|
225
|
+
Tracing logs declared failures with `RPC failure` and their tag, and defects with `RPC defect`. The diagnostic payload masks nested credential-shaped keys and Effect `Redacted` values. Credential-shaped strings, such as bearer tokens, URL passwords and sensitive `key=value` text, are masked too. Strings retain at most 2,048 characters before a truncation marker; collections retain at most 50 entries with a marker for the remainder, and binary values become size summaries.
|
|
226
|
+
|
|
227
|
+
Defects are also redacted in the cause that reaches the client, server span and error reporters. An `Error` keeps its error shape with redacted fields and cause. Extend the sensitive-key rule explicitly:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { isSensitiveKey } from "@shivaedev/platform/rpc-server/sensitive.ts";
|
|
231
|
+
import { requestTracingLayer } from "@shivaedev/platform/rpc-server/tracing.ts";
|
|
232
|
+
|
|
233
|
+
export const Tracing = requestTracingLayer({
|
|
234
|
+
sensitive: (key) => isSensitiveKey(key) || key === "email",
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The example adds `email` to the default policy. For reporting outside RPC, `redactingErrorReporter(reporter)` wraps an Effect error reporter and passes it a redacted cause while preserving reporting hints such as severity and redacted attributes.
|
|
239
|
+
|
|
240
|
+
### Promise callers: one application runtime
|
|
241
|
+
|
|
242
|
+
Build the application Layer once. `runWithServices` supplies an invocation's overrides to subsequent calls on the same runtime, across Promise awaits:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { Context, Layer } from "effect";
|
|
246
|
+
import { makePlatformRuntime } from "@shivaedev/platform/runtime/make.ts";
|
|
247
|
+
|
|
248
|
+
class Greeting extends Context.Service<Greeting, string>()("app/Greeting") {}
|
|
249
|
+
|
|
250
|
+
const runtime = makePlatformRuntime(Layer.succeed(Greeting, "application"));
|
|
251
|
+
const base = await runtime.runPromise(Greeting);
|
|
252
|
+
const request = await runtime.runWithServices(
|
|
253
|
+
Context.make(Greeting, "request"),
|
|
254
|
+
async () => {
|
|
255
|
+
await Promise.resolve();
|
|
256
|
+
return runtime.runPromise(Greeting);
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
await runtime.dispose();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`base` is `"application"` and `request` is `"request"`. The application's resource Layer is acquired once across calls. An explicit `developmentCacheKey` shares a runtime between calls with that key during development. Each key belongs to one application runtime definition.
|
|
263
|
+
|
|
264
|
+
A nested `runWithServices` call replaces the outer invocation's overrides for
|
|
265
|
+
its callback. Merge the contexts explicitly when the nested call must retain
|
|
266
|
+
those overrides.
|
|
267
|
+
|
|
268
|
+
### Node subscriptions: combine cancellation and clean up
|
|
269
|
+
|
|
270
|
+
A long-lived response can receive cancellation from a Web request, a procedure and its underlying Node connection:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import type { IncomingMessage } from "node:http";
|
|
274
|
+
import { nodeSubscriptionSignal } from "@shivaedev/platform/node-http/subscription-signal.ts";
|
|
275
|
+
|
|
276
|
+
export async function consumeSubscription(
|
|
277
|
+
request: Request,
|
|
278
|
+
nodeRequest: IncomingMessage,
|
|
279
|
+
procedureSignal: AbortSignal,
|
|
280
|
+
consume: (signal: AbortSignal) => Promise<void>,
|
|
281
|
+
) {
|
|
282
|
+
const subscription = nodeSubscriptionSignal({
|
|
283
|
+
nodeRequest,
|
|
284
|
+
request,
|
|
285
|
+
signals: [procedureSignal],
|
|
286
|
+
});
|
|
287
|
+
try {
|
|
288
|
+
await consume(subscription.signal);
|
|
289
|
+
} finally {
|
|
290
|
+
subscription.dispose();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The signal aborts on an additional signal, socket close, incomplete request close or an already destroyed connection. Normal completion of a request does not abort a continuing response. Without an explicit `nodeRequest`, the helper can read one from an srvx Web request's `runtime.node.req`. `dispose()` removes Node listeners and can be called repeatedly.
|
|
296
|
+
|
|
297
|
+
### Optional Better Auth storage on Effect Prisma
|
|
298
|
+
|
|
299
|
+
When Better Auth's rows belong to an Effect Prisma database, give its adapter the same database service and runtime:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { betterAuth } from "better-auth";
|
|
303
|
+
import { effectPrismaAdapter } from "@shivaedev/platform/better-auth/adapter.ts";
|
|
304
|
+
import { makePlatformRuntime } from "@shivaedev/platform/runtime/make.ts";
|
|
305
|
+
import { Database, DatabaseLive } from "./database.ts";
|
|
306
|
+
|
|
307
|
+
export const runtime = makePlatformRuntime(DatabaseLive);
|
|
308
|
+
export const auth = betterAuth({
|
|
309
|
+
database: effectPrismaAdapter(Database, runtime, {
|
|
310
|
+
modelName: (model) =>
|
|
311
|
+
`Auth${model.length === 0 ? model : `${model[0]?.toUpperCase()}${model.slice(1)}`}`,
|
|
109
312
|
}),
|
|
110
|
-
)
|
|
313
|
+
});
|
|
111
314
|
```
|
|
112
315
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
session provider fails with `AuthUnavailable`, never as a signed-out user.
|
|
117
|
-
Failures log once with a redacted payload. The Origin policy is a function the
|
|
118
|
-
application supplies; see the
|
|
119
|
-
[request context guide](../../docs/framework/request-context.md) for the
|
|
120
|
-
trade-offs between browsers and native clients.
|
|
316
|
+
The application's database module owns its generated contract and Layer. This mapping selects `AuthUser` for Better Auth's `user` model. The adapter translates create, read, update, count and delete operations through Effect Prisma. It carries a Better Auth transaction's services through its Promise callback; a failed callback rolls the transaction back.
|
|
317
|
+
|
|
318
|
+
An empty `update` filter returns null. Empty filters on `updateMany` and `deleteMany` affect all rows, so callers must choose those operations deliberately.
|
|
121
319
|
|
|
122
|
-
|
|
320
|
+
### In tests: one Prisma/tRPC composition
|
|
123
321
|
|
|
124
|
-
|
|
322
|
+
Set up the application harness once in `test-support/`:
|
|
125
323
|
|
|
126
324
|
```ts
|
|
127
|
-
import {
|
|
325
|
+
import { Effect } from "effect";
|
|
326
|
+
import { makePlatformIt } from "@shivaedev/platform/testing/vitest.ts";
|
|
327
|
+
import { adapter, Database, router, TestLive } from "./application.ts";
|
|
128
328
|
|
|
129
329
|
export const it = makePlatformIt(Database)({
|
|
130
|
-
adapter
|
|
131
|
-
createCaller: (options =
|
|
132
|
-
|
|
133
|
-
extend: ({ db, trpc }) =>
|
|
330
|
+
adapter,
|
|
331
|
+
createCaller: (options = { actor: "default" }) => router.createCaller(options),
|
|
332
|
+
extend: ({ db }) =>
|
|
134
333
|
Effect.succeed({
|
|
135
|
-
|
|
136
|
-
factories: makeFactories(db),
|
|
137
|
-
fixtures: makeFixtures({ db, trpc }),
|
|
334
|
+
userExists: (id: string) => db.AuthUser.where({ id }).exists(),
|
|
138
335
|
}),
|
|
139
|
-
|
|
336
|
+
layer: TestLive,
|
|
337
|
+
});
|
|
140
338
|
```
|
|
141
339
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
340
|
+
`application.ts` supplies the application's Effect tRPC adapter, generated database service, router and test Layer. The example router has `createUser` and `findUser` procedures, and its caller context has an `actor` string. `extend` adds application helpers alongside `db`, `trpc` and `promise`; types are inferred from the database, caller and extension.
|
|
341
|
+
|
|
342
|
+
Write each feature test against the caller and database:
|
|
145
343
|
|
|
146
344
|
```ts
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
345
|
+
import { expect } from "@effect/vitest";
|
|
346
|
+
import { Option } from "effect";
|
|
347
|
+
import { it } from "./test-support/it.ts";
|
|
348
|
+
|
|
349
|
+
it.effectApp("stores the user's email", function* ({ db, trpc, userExists }) {
|
|
350
|
+
const id = crypto.randomUUID();
|
|
351
|
+
const input = { email: `${id}@example.test`, id, name: "Ada" };
|
|
352
|
+
const created = yield* trpc.createUser(input);
|
|
353
|
+
const stored = Option.getOrThrow(yield* db.AuthUser.where({ id }).first());
|
|
354
|
+
|
|
355
|
+
expect(stored.email).toBe(input.email);
|
|
356
|
+
expect(yield* userExists(created.id)).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
```
|
|
151
359
|
|
|
152
|
-
|
|
153
|
-
|
|
360
|
+
The direct database, tRPC caller and extension share the test's PostgreSQL transaction. Successful and expected-failing tests roll their writes back. `trpc({ actor: "admin" })` selects another caller context within the test. `promise(() => auth.api.getSession({ headers }))` runs a Promise boundary with the test's services; the Better Auth adapter tests use that helper to join the rollback transaction.
|
|
361
|
+
|
|
362
|
+
### API
|
|
363
|
+
|
|
364
|
+
Import the module that defines the name; there is no package-root entry. Paths below follow `@shivaedev/platform/`.
|
|
365
|
+
|
|
366
|
+
| Module | Names and purpose |
|
|
367
|
+
| --- | --- |
|
|
368
|
+
| `appHost.ts` | `appHost({ group, handlers, services, migrations?, sessions?, policy?, rpcPath?, assets?, routes?, config? })`, `appDatabase({ migrations, config? })`, `AppChanges`, `AppHostOptions`, `AppHttpHostOptions`, `AppLocalHostOptions`, `AppDatabaseOptions`: scoped HTTP and PostgreSQL composition with loopback defaults and optional proxy trust. |
|
|
369
|
+
| `errors/taxonomy.ts` | `NotFound`, `Unauthorized`, `Forbidden`, `BadRequest`, `Conflict`, `PreconditionFailed`, `TooManyRequests`, `AuthUnavailable`, `InternalServerError`, `ServiceUnavailable`: Schema tagged error classes with a `message`; `BadRequest` and `Conflict` also accept an optional `field`. |
|
|
370
|
+
| `errors/rejected-field.ts` | `rejectedField(unknown): Option<RejectedField>`; `RejectedField` is `{ field: string, message: string }`. |
|
|
371
|
+
| `rpc/identity.ts` | `RequestId`: string service. `Identity`: `IdentityValue`, `{ id: string }`. `OptionalIdentity`: `Option<IdentityValue>`. |
|
|
372
|
+
| `rpc/middleware.ts` | `RequestTracing`, `Authenticated`, `MaybeAuthenticated`: native RPC middleware declarations. |
|
|
373
|
+
| `rpc-server/boundary.ts` | `rpcBoundary(schema, { unexpected, message? })`; `RpcBoundaryOptions<Error>`: declared failures pass through, unexpected failures become one declared rejection. |
|
|
374
|
+
| `rpc-server/session.ts` | `authenticatedLayer(policy)`, `maybeAuthenticatedLayer(policy)`, `resolveSession(policy, headers, rpc)`; `SessionShape`, `SessionProvider<Session>`, `SessionPolicy<Session>`. |
|
|
375
|
+
| `rpc-server/adapters/better-auth-sessions.ts` | `betterAuthSessions(getSession)`; `GetSession<Session>` is a Promise lookup returning `Session \| null`. |
|
|
376
|
+
| `rpc-server/origin.ts` | `trustedOrigins({ allow, missing })`, `originRequest(headers, rpc)`; `OriginPolicy`, `OriginRequest`, `TrustedOriginsOptions`. |
|
|
377
|
+
| `rpc-server/local.ts` | `localRpcGuard(effect)`: HTTP RPC guard using native request and serialization services; `loopbackOnly(handler)`, `sameOrigin(request)`. |
|
|
378
|
+
| `rpc-server/transport.ts` | `transportHeaders(rpcHeaders)`: Effect returning the headers used for a transport-level decision. |
|
|
379
|
+
| `rpc-server/tracing.ts` | `requestTracingLayer({ header?, sensitive? })`; `RequestTracingOptions`. |
|
|
380
|
+
| `rpc-server/redact.ts` | `redact(value, sensitive?)`: unknown diagnostic value. |
|
|
381
|
+
| `rpc-server/redact-cause.ts` | `redactCause(cause, sensitive?)`, `redactDefect(defect, sensitive?)`, `redactingErrorReporter(reporter, sensitive?)`. |
|
|
382
|
+
| `rpc-server/sensitive.ts` | `isSensitiveKey`, `redactText`, `REDACTED`; `SensitiveKey` is `(key: string) => boolean`. |
|
|
383
|
+
| `runtime/make.ts` | `makePlatformRuntime(layer, { developmentCacheKey? })`. |
|
|
384
|
+
| `runtime/types.ts` | `PlatformRuntime<Services, BuildError>`, `PlatformRuntimeOptions`, `PlatformRuntimeServices<Runtime>`, `PlatformRuntimeBuildError<Runtime>`. |
|
|
385
|
+
| `node-http/subscription-signal.ts` | `nodeSubscriptionSignal({ nodeRequest?, request?, signals? })` returns `{ signal, dispose }`; `NodeSubscriptionSignalOptions`, `NodeSubscriptionSignal`. |
|
|
386
|
+
| `better-auth/adapter.ts` | `effectPrismaAdapter(Database, runtime, { debugLogs?, modelName?, usePlural? })`; `EffectPrismaAdapterOptions`. |
|
|
387
|
+
| `testing/vitest.ts` | `makePlatformIt(Database)({ adapter, createCaller, layer, extend? })`. |
|
|
388
|
+
| `testing/types.ts` | `MakePlatformItOptions`, `PlatformHarness`, `PlatformIt`, `PlatformTest`, `PlatformTester`. |
|
|
389
|
+
|
|
390
|
+
`PlatformRuntime` exposes `runPromise(effect, { signal? })`, `runPromiseExit(effect, { signal? })`, `runWithServices(context, evaluate)`, `contextEffect`, `currentServices()` and `dispose()`. The harness exposes native Vitest methods plus `effectApp(name, body, options?)`; its tester also has `skip`, `skipIf`, `runIf`, `only`, `each` and `fails`.
|
|
391
|
+
|
|
392
|
+
The exports also include the adapter's translation modules: `makeRowAdapter` and `RelationQuery` in `better-auth/row-adapter.ts`, `refineRelation` and `DynamicRelation` in `better-auth/relation.ts`, and `whereExpression`, `CleanedWhere`, `DynamicField` and `Expression` in `better-auth/where.ts`. Application storage setup uses `effectPrismaAdapter`.
|
|
393
|
+
|
|
394
|
+
### Install, setup and limits
|
|
395
|
+
|
|
396
|
+
```sh
|
|
397
|
+
pnpm add @shivaedev/platform effect@4.0.4
|
|
154
398
|
```
|
|
155
399
|
|
|
156
|
-
`
|
|
157
|
-
|
|
400
|
+
Effect is the only required peer. The peer-free implementation lives in the shared `src/errors/` and `src/rpc/` folders and the `src/server/node-http/`, `src/server/rpc-server/` and `src/server/runtime/` folders; explicit exports retain the defining-module import paths listed above. The package-local `quality.fences.config.ts` prevents core from reaching optional peers listed in the manifest. The core errors, RPC declarations, server middleware, runtime and Node HTTP modules can be imported without the optional peers, including the Promise-based `betterAuthSessions` function. The runtime uses Node's `AsyncLocalStorage`; browser consumers import the shared `errors/` and `rpc/` modules.
|
|
401
|
+
|
|
402
|
+
Install the optional peers for the selected integration:
|
|
403
|
+
|
|
404
|
+
| Integration | Additional peers |
|
|
405
|
+
| --- | --- |
|
|
406
|
+
| `appHost.ts` | `@effect/platform-node`, `@effect/sql-pg` |
|
|
407
|
+
| `better-auth/adapter.ts` | `better-auth`, `@shivaedev/effect-prisma`, `@prisma-next/sql-orm-client` |
|
|
408
|
+
| `testing/vitest.ts` | `@effect/vitest`, `@shivaedev/effect-prisma`, `@shivaedev/effect-trpc`, `@trpc/server` |
|
|
409
|
+
|
|
410
|
+
- The package declares Node 24 or newer and uses the Effect version in its peer manifest.
|
|
411
|
+
- Keep the auth provider's configuration, cookie settings and resource authorization in the application. The session adapter takes only a lookup function and returns a session result; it does not write provider refresh cookies into RPC responses.
|
|
412
|
+
- Without an `HttpServerRequest`, `transportHeaders` falls back to the supplied RPC headers. An application using another transport must decide what headers it can trust.
|
|
413
|
+
- Add tracing last on the native group, as in the example, so it wraps the authentication middleware too. Middleware outside that tracing boundary is responsible for its own diagnostics.
|
|
414
|
+
- Redaction recognizes common shapes. Keep secrets out of messages and extend the key predicate for application-specific sensitive fields.
|
|
415
|
+
- A development cache key is ignored when `NODE_ENV` is `production`. The application owns runtime disposal.
|
|
416
|
+
- The Better Auth storage adapter's default model mapping capitalizes the model name. Supply `modelName` for a generated schema with another naming convention. Experimental native Better Auth joins are rejected.
|
|
417
|
+
- Package session tests use real Better Auth with ephemeral SQLite and injected HTTP; the WebSocket test uses an in-memory socket. Node signal tests simulate close events. These checks do not establish deployed browser, mobile or Bun behavior.
|
|
418
|
+
- The storage and harness tests require real PostgreSQL and skip without `PLATFORM_EFFECT_PRISMA_TEST_DATABASE_URL`. A skipped test supplies no transaction evidence.
|
|
158
419
|
|
|
159
|
-
|
|
160
|
-
rebuilding the worker-scoped test Layer. `effectApp` also supports `skip`,
|
|
161
|
-
`skipIf`, `runIf`, `only`, `each`, and `fails`.
|
|
420
|
+
For the purpose and trade-offs, read [the north star](https://github.com/ShivaeDev/platform/blob/main/packages/platform/docs/north-star.md). Implementation status and maintainer decisions live in [the package roadmap](https://github.com/ShivaeDev/platform/blob/main/packages/platform/docs/roadmap.md). The [framework request-context guide](https://github.com/ShivaeDev/platform/blob/main/docs/framework/request-context.md) explains the browser and native-client Origin choices.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rejectedField.d.ts","sourceRoot":"","sources":["../../src/errors/rejectedField.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAE1E"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Option, Schema } from "effect";
|
|
2
2
|
const Rejected = Schema.Struct({ _tag: Schema.String, field: Schema.String, message: Schema.String });
|
|
3
3
|
const decode = Schema.decodeUnknownOption(Rejected);
|
|
4
|
-
export
|
|
5
|
-
|
|
4
|
+
export function rejectedField(error) {
|
|
5
|
+
return Option.map(decode(error), ({ field, message }) => ({ field, message }));
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=rejectedField.js.map
|