@shivaedev/platform 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +130 -12
  3. package/dist/rpc/identity.d.ts +8 -2
  4. package/dist/rpc/identity.d.ts.map +1 -1
  5. package/dist/rpc/identity.js +2 -0
  6. package/dist/rpc/identity.js.map +1 -1
  7. package/dist/rpc/middleware.d.ts +4 -1
  8. package/dist/rpc/middleware.d.ts.map +1 -1
  9. package/dist/rpc/middleware.js +4 -0
  10. package/dist/rpc/middleware.js.map +1 -1
  11. package/dist/rpc/publicConfig.d.ts +3 -0
  12. package/dist/rpc/publicConfig.d.ts.map +1 -0
  13. package/dist/rpc/publicConfig.js +8 -0
  14. package/dist/rpc/publicConfig.js.map +1 -0
  15. package/dist/server/better-auth/mount.d.ts +61 -0
  16. package/dist/server/better-auth/mount.d.ts.map +1 -0
  17. package/dist/server/better-auth/mount.js +59 -0
  18. package/dist/server/better-auth/mount.js.map +1 -0
  19. package/dist/server/better-auth/testing.d.ts +50 -0
  20. package/dist/server/better-auth/testing.d.ts.map +1 -0
  21. package/dist/server/better-auth/testing.js +18 -0
  22. package/dist/server/better-auth/testing.js.map +1 -0
  23. package/dist/server/databaseLayer.d.ts +2 -1
  24. package/dist/server/databaseLayer.d.ts.map +1 -1
  25. package/dist/server/databaseLayer.js +2 -0
  26. package/dist/server/databaseLayer.js.map +1 -1
  27. package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts +3 -1
  28. package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts.map +1 -1
  29. package/dist/server/rpc-server/adapters/betterAuthSessions.js +13 -22
  30. package/dist/server/rpc-server/adapters/betterAuthSessions.js.map +1 -1
  31. package/dist/server/rpc-server/publicConfigLayer.d.ts +4 -0
  32. package/dist/server/rpc-server/publicConfigLayer.d.ts.map +1 -0
  33. package/dist/server/rpc-server/publicConfigLayer.js +8 -0
  34. package/dist/server/rpc-server/publicConfigLayer.js.map +1 -0
  35. package/dist/server/rpc-server/session.d.ts +6 -2
  36. package/dist/server/rpc-server/session.d.ts.map +1 -1
  37. package/dist/server/rpc-server/session.js +16 -6
  38. package/dist/server/rpc-server/session.js.map +1 -1
  39. package/package.json +14 -10
  40. package/src/rpc/identity.ts +8 -2
  41. package/src/rpc/middleware.ts +7 -1
  42. package/src/rpc/publicConfig.ts +9 -0
  43. package/src/server/better-auth/mount.ts +68 -0
  44. package/src/server/better-auth/testing.ts +21 -0
  45. package/src/server/databaseLayer.ts +4 -1
  46. package/src/server/rpc-server/adapters/betterAuthSessions.ts +19 -23
  47. package/src/server/rpc-server/publicConfigLayer.ts +11 -0
  48. package/src/server/rpc-server/session.ts +38 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.1 - 2026-10-18
4
+
5
+ ### Changed
6
+
7
+ - Release with updated workspace dependencies.
8
+
9
+ ## 0.18.0 - 2026-10-18
10
+
11
+ ### Added
12
+
13
+ - effect-sql exports `clientOptions(options, { snakeCase? })` and `SqlNameTransforms` from `clientOptions.ts`. New clients configured with this helper use Effect SQL's native camelCase-to-snake_case query names and snake_case-to-camelCase results by default. Pass `{ snakeCase: false }` to preserve native names, or supply native transform functions in the options.
14
+ - Platform's app database and app host configuration accept native `transformQueryNames` and `transformResultNames`, so applications can use effect-sql's client options without replacing the host's database layer.
15
+ - Declare public client configuration once with `publicConfig(schema)` from `rpc/publicConfig.ts`. Load it at host startup with `publicConfigLayer` from `server/rpc-server/publicConfigLayer.ts` and read the generated contract through native atoms and effect-react's `useQuery`. Only declared fields reach the client; missing or invalid required values fail startup with a typed `ConfigError`.
16
+ - `@shivaedev/platform/better-auth.ts` exposes `betterAuthMount(options)`, returning a typed Better Auth instance and native `/api/auth/*` route Layer with a 503 fallback, trusted-peer forwarded-IP handling and Config-controlled rate limiting. Applications supply their Better Auth options and RPC session policy.
17
+ - `authDatabase(url)` scopes the Better Auth PostgreSQL pool, logs idle-client errors at Debug and waits for clients to disconnect on release. The entry uses the existing Better Auth integration and the optional `pg` peer.
18
+ - Platform exports `authTestUtils(options?)` and `signedInUser(auth, fields)` from `server/better-auth/testing.ts` for typed Better Auth test utilities and signed-in story preconditions with user fields and roles, without OAuth or password hashing.
19
+ - Test-story exports `seedCookies(cookies)` and `DomCookie` from `seedCookies.ts` to seed happy-dom's cookie container, including HttpOnly session cookies accepted by a real HTTP host.
20
+
21
+ ### Changed
22
+
23
+ - Newly generated applications configure snake_case SQL columns through effect-sql. The starter migrates existing note ownership and word-count columns while retaining camelCase model fields.
24
+ - Import `betterAuthMount` and `authDatabase` from `@shivaedev/platform/server/better-auth/mount.ts`, the module's own path under the package's wildcard export. The separate `@shivaedev/platform/better-auth.ts` entry is removed.
25
+
26
+ ### Breaking
27
+
28
+ - Provider accounts load when a handler asks for them. Replace `SessionUser.providerAccounts` array reads with `yield* (yield* SessionUserWithProviderAccounts).providerAccounts`, declare `AuthenticatedUserWithProviderAccounts` on the RPC, and install `authenticatedUserWithProviderAccountsLayer(sessions)`. Add `AuthUnavailable` and `Unauthorized` to the RPC's error schema because account lookup failures now fail the reading handler. Base `SessionUserValue` no longer has `providerAccounts`.
29
+ - A custom session provider supplies `user.providerAccounts` as `Effect<readonly ProviderAccountValue[], AuthUnavailable | Unauthorized>`, described by the new `ProviderAccountsSessionShape`, instead of an array. The request boundary memoizes this Effect and projects its results to provider and account IDs. `betterAuthSessions(getSession, getProviderAccounts)` supplies this shape; without a lookup its session user has no accounts field. `SessionUserWithProviderAccountsValue` describes the accounts-capable handler profile. Identity-only requests do not call the lookup, and repeated reads share one lookup per request.
30
+
3
31
  ## 0.17.0 - 2026-10-18
4
32
 
5
33
  ### Added
package/README.md CHANGED
@@ -144,6 +144,65 @@ For a deployment behind a proxy, set `HOST=0.0.0.0` and `TRUSTED_PROXY` to a com
144
144
 
145
145
  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.
146
146
 
147
+ ### Public client configuration
148
+
149
+ Use one `publicConfig` declaration per application as the one path for server configuration that a client needs. Declare only public fields in a shared Schema; the declaration creates a standard effect-contract query, so applications write no config RPC. Keep private configuration in server modules.
150
+
151
+ ```ts
152
+ import { Schema } from "effect";
153
+ import { publicConfig } from "@shivaedev/platform/rpc/publicConfig.ts";
154
+
155
+ export const PublicConfig = publicConfig(
156
+ Schema.Struct({ installUrl: Schema.URL }).pipe(
157
+ Schema.encodeKeys({ installUrl: "INSTALL_URL" }),
158
+ ),
159
+ );
160
+ ```
161
+
162
+ On the server, merge the declaration into the application's group and its generated handler Layer into the handlers:
163
+
164
+ ```ts
165
+ import { Layer } from "effect";
166
+ import { appHost } from "@shivaedev/platform/server/appHost.ts";
167
+ import { publicConfigLayer } from "@shivaedev/platform/server/rpc-server/publicConfigLayer.ts";
168
+ import { Api, Handlers, sessions } from "./app.ts";
169
+ import { PublicConfig } from "./public-config.ts";
170
+
171
+ export const Server = appHost({
172
+ group: Api.merge(PublicConfig),
173
+ handlers: Layer.merge(Handlers, publicConfigLayer(PublicConfig)),
174
+ sessions,
175
+ });
176
+ ```
177
+
178
+ `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.
179
+
180
+ On the client, use the same native `AtomRpc.Service`, NDJSON HTTP protocol and effect-contract `bind` as other queries:
181
+
182
+ ```ts
183
+ import { Layer } from "effect";
184
+ import { FetchHttpClient } from "effect/http";
185
+ import * as AtomRpc from "effect/reactivity/AtomRpc";
186
+ import { RpcClient, RpcSerialization } from "effect/rpc";
187
+ import { bind } from "@shivaedev/effect-contract/bind.ts";
188
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
189
+ import { PublicConfig } from "./public-config.ts";
190
+
191
+ class ConfigClient extends AtomRpc.Service<ConfigClient>()("app/ConfigClient", {
192
+ group: PublicConfig,
193
+ protocol: RpcClient.layerProtocolHttp({ url: "/rpc" }).pipe(
194
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson]),
195
+ ),
196
+ }) {}
197
+ const config = bind(PublicConfig, ConfigClient);
198
+
199
+ export function usePublicConfig() {
200
+ return useQuery(config.get.query());
201
+ }
202
+ ```
203
+
204
+ Render inside the application's native registry provider. The query's `data` is an `Option<{ readonly installUrl: URL }>`: the same codec validates server configuration and decodes the RPC response on the client. Public configuration uses the existing atom registry and query lifecycle.
205
+
147
206
  ### 1. Once per application: contracts and session policy
148
207
 
149
208
  Declare the RPC group in a shared module:
@@ -153,7 +212,7 @@ import { Effect, Schema } from "effect";
153
212
  import { Rpc, RpcGroup } from "effect/rpc";
154
213
  import { Forbidden } from "@shivaedev/platform/errors/taxonomy.ts";
155
214
  import { Identity } from "@shivaedev/platform/rpc/identity.ts";
156
- import { Authenticated, RequestTracing } from "@shivaedev/platform/rpc/middleware.ts";
215
+ import { Authenticated } from "@shivaedev/platform/rpc/middleware.ts";
157
216
 
158
217
  export const Account = RpcGroup.make(
159
218
  Rpc.make("ReadOwn", {
@@ -161,7 +220,7 @@ export const Account = RpcGroup.make(
161
220
  payload: { userId: Schema.String },
162
221
  success: Schema.String,
163
222
  }),
164
- ).middleware(Authenticated).middleware(RequestTracing);
223
+ ).middleware(Authenticated);
165
224
 
166
225
  export const Handlers = Account.toLayer({
167
226
  ReadOwn: ({ userId }) =>
@@ -222,9 +281,14 @@ Here `sessions` is the application's policy from the previous example, with a Be
222
281
 
223
282
  #### Reading provider account IDs
224
283
 
225
- Opt in to provider account IDs by passing Better Auth's account lookup as the second argument to `betterAuthSessions`. It runs after a session is found, using the same transport headers:
284
+ Pass Better Auth's account lookup as the second argument to `betterAuthSessions`. Declare `AuthenticatedUserWithProviderAccounts` on account-reading RPCs and merge `authenticatedUserWithProviderAccountsLayer(sessions)` alongside their handlers. This boundary requires a provider whose user has a `providerAccounts` Effect; a provider without the second lookup does not satisfy that type.
226
285
 
227
286
  ```ts
287
+ import { SessionUserWithProviderAccounts } from "@shivaedev/platform/rpc/identity.ts";
288
+ import { AuthenticatedUserWithProviderAccounts } from "@shivaedev/platform/rpc/middleware.ts";
289
+ import { AuthUnavailable, Unauthorized } from "@shivaedev/platform/errors/taxonomy.ts";
290
+ import { authenticatedUserWithProviderAccountsLayer } from "@shivaedev/platform/server/rpc-server/session.ts";
291
+
228
292
  const sessions = {
229
293
  origin: trustedOrigins({ allow: ["https://app.example"], missing: "reject" }),
230
294
  provider: betterAuthSessions(
@@ -233,14 +297,18 @@ const sessions = {
233
297
  ),
234
298
  };
235
299
 
236
- const githubAccount = Effect.map(SessionUser, (user) =>
237
- user.providerAccounts?.find((account) => account.providerId === "github"),
238
- );
300
+ const githubAccount = Effect.gen(function* () {
301
+ const user = yield* SessionUserWithProviderAccounts;
302
+ const accounts = yield* user.providerAccounts;
303
+ return accounts.find((account) => account.providerId === "github");
304
+ });
239
305
  ```
240
306
 
241
- Use `authenticatedUserLayer(sessions)` as above. `SessionUserValue.providerAccounts` is an optional readonly array of `ProviderAccountValue`, each containing only `{ providerId: string, accountId: string }`. GitHub's numeric user ID is represented as a string, for example `{ providerId: "github", accountId: "123" }`; it is not Better Auth's account-row id. A user without a GitHub account has no matching entry. Without the second lookup, the profile omits `providerAccounts`. No application SQL against Better Auth's `account` table is needed.
307
+ Declare `Schema.Union([AuthUnavailable, Unauthorized])` in the account-reading RPC's `error` schema: lookup failures now come from the handler's Effect. An outage fails with `AuthUnavailable`; a session that expires before the lookup fails with `Unauthorized`. Neither failure exposes provider exception details.
242
308
 
243
- An account lookup failure follows the same `AuthUnavailable` path as a session lookup failure.
309
+ The lookup uses the same transport headers and runs only when the handler evaluates `user.providerAccounts`. The boundary memoizes its result in that invocation's Effect context, including concurrent reads, without sharing it across requests or users. Identity-only handlers never run the accounts lookup. `SessionUserWithProviderAccountsValue.providerAccounts` is a required `Effect<readonly ProviderAccountValue[], AuthUnavailable | Unauthorized>`. Each result contains only `{ providerId: string, accountId: string }`; for example, GitHub's numeric user ID is the string `"123"`, not Better Auth's account-row id. `SessionUser` and `SessionUserValue` contain the base profile without accounts.
310
+
311
+ A custom session provider supplies `user.providerAccounts` as a lazy Effect with that same result and failure type, described by `ProviderAccountsSessionShape`. The boundary owns request-local memoization and the reduction to public IDs; custom providers need not cache or project the returned account objects.
244
312
 
245
313
  ### Requiring an admin
246
314
 
@@ -441,6 +509,53 @@ export async function consumeSubscription(
441
509
 
442
510
  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.
443
511
 
512
+ ### Mount Better Auth
513
+
514
+ `betterAuthMount(options)` returns the Better Auth instance and a native route Layer for `/api/auth/*`. It accepts Better Auth options, including application plugins and session policy. Merge `routes` into the HTTP host and use `auth.api.getSession` with `betterAuthSessions` for the application's RPC session provider.
515
+
516
+ ```ts
517
+ import { Config, Effect } from "effect";
518
+ import { authDatabase, betterAuthMount } from "@shivaedev/platform/server/better-auth/mount.ts";
519
+
520
+ const authentication = Effect.gen(function* () {
521
+ const database = yield* authDatabase(yield* Config.Redacted("DATABASE_URL"));
522
+ return yield* betterAuthMount({
523
+ database,
524
+ emailAndPassword: { enabled: true },
525
+ session: { cookieCache: { enabled: false } },
526
+ });
527
+ });
528
+ ```
529
+
530
+ Run this Effect in the host's scope. The pool logs idle-client errors at Debug and replaces failed connections on the next query; closing the scope disconnects its clients. A rejected auth handler returns HTTP 503 with `Authentication is temporarily unavailable`.
531
+
532
+ The mount reads `BETTER_AUTH_SECRET` unless options supply `secret`. `PUBLIC_URL` supplies the default base URL, trusted origin and HTTPS secure-cookie setting; without it, the defaults accept loopback HTTP. Application options can override those defaults. RPC Origin policy remains an application choice.
533
+
534
+ `AUTH_RATE_LIMIT_ENABLED` is a Config boolean that defaults to true and takes precedence over `options.rateLimit.enabled`; other Better Auth rate-limit options pass through. `TRUSTED_PROXY` supplies the same address and CIDR policy used by the host. An untrusted transport peer loses `x-forwarded-for` and the configured client-IP header before Better Auth sees the request. `AUTH_CLIENT_IP_HEADER` defaults to `x-forwarded-for`. With no trusted proxy, Better Auth ignores forwarded IPs and shares a rate-limit bucket per path.
535
+
536
+ ### Signed-in story preconditions
537
+
538
+ Use `authTestUtils()` from `@shivaedev/platform/server/better-auth/testing.ts` in a test-only Better Auth instance with the host's database, secret and cookie policy. The wrapper preserves the plugin's typed `$context.test` under `exactOptionalPropertyTypes`. Keep the plugin out of production auth options.
539
+
540
+ ```ts
541
+ import { betterAuth } from "better-auth";
542
+ import { authTestUtils, signedInUser } from "@shivaedev/platform/server/better-auth/testing.ts";
543
+
544
+ const testAuth = betterAuth({
545
+ ...options,
546
+ plugins: [...(options.plugins ?? []), authTestUtils()],
547
+ });
548
+ const { user, headers, cookies } = await signedInUser(testAuth, {
549
+ email: "member@example.test",
550
+ name: "Member",
551
+ role: "user,admin",
552
+ });
553
+ ```
554
+
555
+ `options` is the application's auth configuration. `signedInUser` saves the supplied user fields and mints one session through Better Auth's `testUtils`, without OAuth or password hashing. It returns Better Auth's login result: the user, session, signed request headers, browser cookies and token. Roles require the application's role plugin, such as Better Auth's `admin()`. Send `headers` to the real host, or pass `cookies` to test-story's `seedCookies` in a DOM story after setting its host location with `domFetch`. Keep a real sign-in story for each sign-in flow; feature stories start from the minted precondition.
556
+
557
+ With database-backed sessions and cookie caching disabled, revocation rejects that same minted session on the next protected HTTP or RPC request. Cookie caching and secondary storage can change revocation timing.
558
+
444
559
  ### Optional Better Auth storage on Effect Prisma
445
560
 
446
561
  When Better Auth's rows belong to an Effect Prisma database, give its adapter the same database service and runtime:
@@ -514,13 +629,13 @@ Import the module that defines the name; there is no package-root entry. Paths b
514
629
  | --- | --- |
515
630
  | `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`. |
516
631
  | `errors/rejectedField.ts` | `rejectedField(unknown)`: the field and message of a field rejection. |
517
- | `rpc/identity.ts` | `RequestId`, `Identity`, `OptionalIdentity`, `SessionUser`: request-local services. |
518
- | `rpc/middleware.ts` | `RequestTracing`, `Authenticated`, `AuthenticatedUser`, `MaybeAuthenticated`: native RPC middleware declarations. |
632
+ | `rpc/identity.ts` | `RequestId`, `Identity`, `OptionalIdentity`, `SessionUser`, `SessionUserWithProviderAccounts`: request-local services. |
633
+ | `rpc/middleware.ts` | `RequestTracing`, `Authenticated`, `AuthenticatedUser`, `AuthenticatedUserWithProviderAccounts`, `MaybeAuthenticated`: native RPC middleware declarations. |
519
634
  | `rpc/requireAdmin.ts` | `requireAdmin`: the signed-in user with an admin role. |
520
635
  | `server/appHost.ts` | `appHost`, `appDatabase`, `AppChanges`: scoped HTTP and PostgreSQL composition with loopback defaults and optional proxy trust. |
521
636
  | `server/boundedBody.ts` | `readBoundedBody`, `RequestBodyTooLarge`: bounded body bytes and a typed HTTP 413 failure. |
522
637
  | `server/otlpTracing.ts` | `otlpTracing`: opt-in Effect OTLP trace export for worker Layers. |
523
- | `server/rpc-server/session.ts` | `authenticatedLayer`, `maybeAuthenticatedLayer`, `authenticatedUserLayer`, `withSessionUser`, `resolveSession`: session middleware implementations and the identity boundary. |
638
+ | `server/rpc-server/session.ts` | `authenticatedLayer`, `maybeAuthenticatedLayer`, `authenticatedUserLayer`, `authenticatedUserWithProviderAccountsLayer`, `withSessionUser`, `resolveSession`: session middleware implementations and the identity boundary. |
524
639
  | `server/rpc-server/adapters/betterAuthSessions.ts` | `betterAuthSessions(getSession, getProviderAccounts?)`: a session provider from Better Auth lookups. |
525
640
  | `server/rpc-server/origin.ts` | `trustedOrigins({ allow, missing })`: an exact Origin allowlist. |
526
641
  | `server/rpc-server/local.ts` | `localRpcGuard`, `loopbackOnly`, `sameOrigin`, `trustedProxyPeer`, `trustedProxyRanges`: loopback and proxy guards. |
@@ -530,6 +645,8 @@ Import the module that defines the name; there is no package-root entry. Paths b
530
645
  | `server/rpc-server/redact.ts`, `server/rpc-server/redactCause.ts`, `server/rpc-server/sensitive.ts` | `redact`, `redactCause`, `redactDefect`, `redactingErrorReporter`, `isSensitiveKey`, `redactText`: diagnostic redaction. |
531
646
  | `server/runtime/makePlatform.ts` | `makePlatformRuntime(layer, { developmentCacheKey? })`. |
532
647
  | `server/node-http/subscriptionSignal.ts` | `nodeSubscriptionSignal({ nodeRequest?, request?, signals? })`, returning `{ signal, dispose }`. |
648
+ | `server/better-auth/mount.ts` | `betterAuthMount(options)`, `authDatabase(url)`: native auth routes and a scoped PostgreSQL pool. |
649
+ | `server/better-auth/testing.ts` | `authTestUtils(options?)`, `signedInUser(auth, fields)`: test-only typed Better Auth helpers and minted user sessions. |
533
650
  | `server/better-auth/adapter.ts` | `effectPrismaAdapter(Database, runtime, options?)`: a Better Auth adapter over Effect Prisma; `modelName` maps model names. |
534
651
  | `server/testing/makePlatformIt.ts` | `makePlatformIt(Database)({ adapter, createCaller, layer, extend? })`. |
535
652
 
@@ -547,13 +664,14 @@ Install the optional peers for the selected integration:
547
664
  | --- | --- |
548
665
  | `server/boundedBody.ts` | `@effect/platform-node` |
549
666
  | `server/appHost.ts` | `@effect/platform-node`, `@effect/sql-pg` |
667
+ | `server/better-auth/mount.ts` | `better-auth`, `pg` |
550
668
  | `server/better-auth/adapter.ts` | `better-auth`, `@shivaedev/effect-prisma`, `@prisma-next/sql-orm-client` |
551
669
  | `server/testing/makePlatformIt.ts` | `@effect/vitest`, `@shivaedev/effect-prisma`, `@shivaedev/effect-trpc`, `@trpc/server` |
552
670
 
553
671
  - The package needs Node 24 or newer.
554
672
  - Keep the auth provider's configuration, cookie settings and resource authorization in the application. The session adapter takes a session lookup and an optional provider-account lookup and returns a session result; it does not write provider refresh cookies into RPC responses.
555
673
  - Without an `HttpServerRequest`, `transportHeaders` falls back to the supplied RPC headers. An application using another transport must decide what headers it can trust.
556
- - `appHost` adds tracing last on its server-side copy of the group. When composing a native RPC server directly, add `RequestTracing` last on the group, as in the example, so it wraps authentication too. Middleware outside that tracing boundary is responsible for its own diagnostics.
674
+ - `appHost` adds tracing last on its server-side copy of the group. When composing a native RPC server directly, add `RequestTracing` last on the group so it wraps authentication too; see the [native-server example](https://github.com/ShivaeDev/platform/blob/main/docs/framework/request-context.md#request-id-identity-and-logging). Middleware outside that tracing boundary is responsible for its own diagnostics.
557
675
  - Redaction recognizes common shapes. Keep secrets out of messages and extend the key predicate for application-specific sensitive fields.
558
676
  - A development cache key is ignored when `NODE_ENV` is `production`. The application owns runtime disposal.
559
677
  - The Better Auth storage adapter's default model mapping capitalizes the model name. Supply `modelName` for a generated schema with another naming convention. Native Better Auth joins (`advanced.database.joins`) are rejected; leave that option off so Better Auth reads related rows with separate queries.
@@ -1,4 +1,5 @@
1
- import { Context, type Option } from "effect";
1
+ import { Context, type Effect, type Option } from "effect";
2
+ import type { AuthUnavailable, Unauthorized } from "#errors/taxonomy.ts";
2
3
  declare const RequestId_base: Context.ServiceClass<RequestId, "@shivaedev/platform/rpc/RequestId", string>;
3
4
  export declare class RequestId extends RequestId_base {
4
5
  }
@@ -18,11 +19,16 @@ export interface ProviderAccountValue {
18
19
  export interface SessionUserValue extends IdentityValue {
19
20
  readonly email: string;
20
21
  readonly name: string;
21
- readonly providerAccounts?: readonly ProviderAccountValue[];
22
22
  readonly role?: string | null | undefined;
23
23
  }
24
24
  declare const SessionUser_base: Context.ServiceClass<SessionUser, "@shivaedev/platform/rpc/SessionUser", SessionUserValue>;
25
25
  export declare class SessionUser extends SessionUser_base {
26
26
  }
27
+ export interface SessionUserWithProviderAccountsValue extends SessionUserValue {
28
+ readonly providerAccounts: Effect.Effect<readonly ProviderAccountValue[], AuthUnavailable | Unauthorized>;
29
+ }
30
+ declare const SessionUserWithProviderAccounts_base: Context.ServiceClass<SessionUserWithProviderAccounts, "@shivaedev/platform/rpc/SessionUserWithProviderAccounts", SessionUserWithProviderAccountsValue>;
31
+ export declare class SessionUserWithProviderAccounts extends SessionUserWithProviderAccounts_base {
32
+ }
27
33
  export {};
28
34
  //# sourceMappingURL=identity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/rpc/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;;AAC9C,qBAAa,SAAU,SAAQ,cAAyE;CAAG;AAC3G,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACpB;;AACD,qBAAa,QAAS,SAAQ,aAA8E;CAAG;;AAC/G,qBAAa,gBAAiB,SAAQ,qBAA6G;CAAG;AACtJ,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AACD,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC1C;;AACD,qBAAa,WAAY,SAAQ,gBAAuF;CAAG"}
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/rpc/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;;AACzE,qBAAa,SAAU,SAAQ,cAAyE;CAAG;AAC3G,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACpB;;AACD,qBAAa,QAAS,SAAQ,aAA8E;CAAG;;AAC/G,qBAAa,gBAAiB,SAAQ,qBAA6G;CAAG;AACtJ,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AACD,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC1C;;AACD,qBAAa,WAAY,SAAQ,gBAAuF;CAAG;AAC3H,MAAM,WAAW,oCAAqC,SAAQ,gBAAgB;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,oBAAoB,EAAE,EAAE,eAAe,GAAG,YAAY,CAAC,CAAC;CAC1G;;AACD,qBAAa,+BAAgC,SAAQ,oCAEpD;CAAG"}
@@ -7,4 +7,6 @@ export class OptionalIdentity extends Context.Service()("@shivaedev/platform/rpc
7
7
  }
8
8
  export class SessionUser extends Context.Service()("@shivaedev/platform/rpc/SessionUser") {
9
9
  }
10
+ export class SessionUserWithProviderAccounts extends Context.Service()("@shivaedev/platform/rpc/SessionUserWithProviderAccounts") {
11
+ }
10
12
  //# sourceMappingURL=identity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/rpc/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAe,MAAM,QAAQ,CAAC;AAC9C,MAAM,OAAO,SAAU,SAAQ,OAAO,CAAC,OAAO,EAAqB,CAAC,mCAAmC,CAAC;CAAG;AAI3G,MAAM,OAAO,QAAS,SAAQ,OAAO,CAAC,OAAO,EAA2B,CAAC,kCAAkC,CAAC;CAAG;AAC/G,MAAM,OAAO,gBAAiB,SAAQ,OAAO,CAAC,OAAO,EAAkD,CAAC,0CAA0C,CAAC;CAAG;AAWtJ,MAAM,OAAO,WAAY,SAAQ,OAAO,CAAC,OAAO,EAAiC,CAAC,qCAAqC,CAAC;CAAG"}
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/rpc/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA4B,MAAM,QAAQ,CAAC;AAE3D,MAAM,OAAO,SAAU,SAAQ,OAAO,CAAC,OAAO,EAAqB,CAAC,mCAAmC,CAAC;CAAG;AAI3G,MAAM,OAAO,QAAS,SAAQ,OAAO,CAAC,OAAO,EAA2B,CAAC,kCAAkC,CAAC;CAAG;AAC/G,MAAM,OAAO,gBAAiB,SAAQ,OAAO,CAAC,OAAO,EAAkD,CAAC,0CAA0C,CAAC;CAAG;AAUtJ,MAAM,OAAO,WAAY,SAAQ,OAAO,CAAC,OAAO,EAAiC,CAAC,qCAAqC,CAAC;CAAG;AAI3H,MAAM,OAAO,+BAAgC,SAAQ,OAAO,CAAC,OAAO,EAAyE,CAC5I,yDAAyD,CACzD;CAAG"}
@@ -1,7 +1,7 @@
1
1
  import { Schema } from "effect";
2
2
  import { RpcMiddleware } from "effect/rpc";
3
3
  import { AuthUnavailable, Forbidden, Unauthorized } from "#errors/taxonomy.ts";
4
- import type { Identity, OptionalIdentity, RequestId, SessionUser } from "./identity.ts";
4
+ import type { Identity, OptionalIdentity, RequestId, SessionUser, SessionUserWithProviderAccounts } from "./identity.ts";
5
5
  declare const RequestTracing_base: RpcMiddleware.ServiceClass<RequestTracing, "@shivaedev/platform/rpc/RequestTracing", RequestId, Schema.Never, never, never, false>;
6
6
  export declare class RequestTracing extends RequestTracing_base {
7
7
  }
@@ -14,5 +14,8 @@ export declare class MaybeAuthenticated extends MaybeAuthenticated_base {
14
14
  declare const AuthenticatedUser_base: RpcMiddleware.ServiceClass<AuthenticatedUser, "@shivaedev/platform/rpc/AuthenticatedUser", Identity | SessionUser, Schema.Union<readonly [typeof Unauthorized, typeof AuthUnavailable, typeof Forbidden]>, never, never, false>;
15
15
  export declare class AuthenticatedUser extends AuthenticatedUser_base {
16
16
  }
17
+ declare const AuthenticatedUserWithProviderAccounts_base: RpcMiddleware.ServiceClass<AuthenticatedUserWithProviderAccounts, "@shivaedev/platform/rpc/AuthenticatedUserWithProviderAccounts", Identity | SessionUser | SessionUserWithProviderAccounts, Schema.Union<readonly [typeof Unauthorized, typeof AuthUnavailable, typeof Forbidden]>, never, never, false>;
18
+ export declare class AuthenticatedUserWithProviderAccounts extends AuthenticatedUserWithProviderAccounts_base {
19
+ }
17
20
  export {};
18
21
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/rpc/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;;AACxF,qBAAa,cAAe,SAAQ,mBAKS;CAAG;;AAChD,qBAAa,aAAc,SAAQ,kBAOjC;CAAG;;AACL,qBAAa,kBAAmB,SAAQ,uBAKgE;CAAG;;AAC3G,qBAAa,iBAAkB,SAAQ,sBAOrC;CAAG"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/rpc/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,+BAA+B,EAAE,MAAM,eAAe,CAAC;;AACzH,qBAAa,cAAe,SAAQ,mBAKS;CAAG;;AAChD,qBAAa,aAAc,SAAQ,kBAOjC;CAAG;;AACL,qBAAa,kBAAmB,SAAQ,uBAKgE;CAAG;;AAC3G,qBAAa,iBAAkB,SAAQ,sBAOrC;CAAG;;AACL,qBAAa,qCAAsC,SAAQ,0CAKzD;CAAG"}
@@ -13,4 +13,8 @@ export class AuthenticatedUser extends RpcMiddleware.Service()("@shivaedev/platf
13
13
  error: Schema.Union([Unauthorized, AuthUnavailable, Forbidden]),
14
14
  }) {
15
15
  }
16
+ export class AuthenticatedUserWithProviderAccounts extends RpcMiddleware.Service()("@shivaedev/platform/rpc/AuthenticatedUserWithProviderAccounts", {
17
+ error: Schema.Union([Unauthorized, AuthUnavailable, Forbidden]),
18
+ }) {
19
+ }
16
20
  //# sourceMappingURL=middleware.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/rpc/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE/E,MAAM,OAAO,cAAe,SAAQ,aAAa,CAAC,OAAO,EAKtD,CAAC,wCAAwC,CAAC;CAAG;AAChD,MAAM,OAAO,aAAc,SAAQ,aAAa,CAAC,OAAO,EAKrD,CAAC,uCAAuC,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;CAC/D,CAAC;CAAG;AACL,MAAM,OAAO,kBAAmB,SAAQ,aAAa,CAAC,OAAO,EAK1D,CAAC,4CAA4C,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;CAAG;AAC3G,MAAM,OAAO,iBAAkB,SAAQ,aAAa,CAAC,OAAO,EAKzD,CAAC,2CAA2C,EAAE;IAChD,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;CAC/D,CAAC;CAAG"}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/rpc/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE/E,MAAM,OAAO,cAAe,SAAQ,aAAa,CAAC,OAAO,EAKtD,CAAC,wCAAwC,CAAC;CAAG;AAChD,MAAM,OAAO,aAAc,SAAQ,aAAa,CAAC,OAAO,EAKrD,CAAC,uCAAuC,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;CAC/D,CAAC;CAAG;AACL,MAAM,OAAO,kBAAmB,SAAQ,aAAa,CAAC,OAAO,EAK1D,CAAC,4CAA4C,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;CAAG;AAC3G,MAAM,OAAO,iBAAkB,SAAQ,aAAa,CAAC,OAAO,EAKzD,CAAC,2CAA2C,EAAE;IAChD,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;CAC/D,CAAC;CAAG;AACL,MAAM,OAAO,qCAAsC,SAAQ,aAAa,CAAC,OAAO,EAG7E,CAAC,+DAA+D,EAAE;IACpE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;CAC/D,CAAC;CAAG"}
@@ -0,0 +1,3 @@
1
+ import type { Schema } from "effect";
2
+ export declare function publicConfig<T, E>(schema: Schema.Codec<T, E>): import("@shivaedev/effect-contract/contract.ts").Contract<"platform.publicConfig", readonly [NoInfer<import("@shivaedev/effect-contract/operation.ts").Query<"get", Schema.Void, Schema.Codec<T, E, never, never>, {}>>], readonly [], import("effect/rpc/Rpc").Rpc<"platform.publicConfig.get", Schema.Void, Schema.Codec<T, E, never, never>, Schema.Never, never, never>, readonly []>;
3
+ //# sourceMappingURL=publicConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicConfig.d.ts","sourceRoot":"","sources":["../../src/rpc/publicConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIrC,wBAAgB,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,6XAI5D"}
@@ -0,0 +1,8 @@
1
+ import { contract } from "@shivaedev/effect-contract/contract.ts";
2
+ import { query } from "@shivaedev/effect-contract/operation.ts";
3
+ export function publicConfig(schema) {
4
+ return contract("platform.publicConfig", {
5
+ queries: [query("get", { reads: () => [], success: schema })],
6
+ });
7
+ }
8
+ //# sourceMappingURL=publicConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicConfig.js","sourceRoot":"","sources":["../../src/rpc/publicConfig.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,MAAM,yCAAyC,CAAC;AAEhE,MAAM,UAAU,YAAY,CAAO,MAA0B;IAC5D,OAAO,QAAQ,CAAC,uBAAuB,EAAE;QACxC,OAAO,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;KAC7D,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { type BetterAuthOptions } from "better-auth";
2
+ import { Config, Effect, Redacted } from "effect";
3
+ import { HttpRouter } from "effect/http";
4
+ import { Pool } from "pg";
5
+ export declare const betterAuthMount: <const TOptions extends BetterAuthOptions>(options: TOptions) => Effect.Effect<{
6
+ auth: import("better-auth").Auth<TOptions & {
7
+ advanced: {
8
+ disableCSRFCheck?: boolean | undefined;
9
+ disableOriginCheck?: boolean | undefined;
10
+ crossSubDomainCookies?: {
11
+ enabled: boolean;
12
+ additionalCookies?: string[];
13
+ domain?: string;
14
+ } | undefined;
15
+ cookies?: {
16
+ [key: string]: {
17
+ name?: string;
18
+ attributes?: import("better-auth").CookieOptions;
19
+ };
20
+ } | undefined;
21
+ defaultCookieAttributes?: import("better-auth").CookieOptions | undefined;
22
+ cookiePrefix?: string | undefined;
23
+ database?: {
24
+ defaultFindManyLimit?: number;
25
+ generateId?: import("better-auth").GenerateIdFn | false | "serial" | "uuid";
26
+ joins?: boolean;
27
+ validateSchema?: boolean;
28
+ } | undefined;
29
+ trustedProxyHeaders?: boolean | undefined;
30
+ backgroundTasks?: {
31
+ handler: (promise: Promise<unknown>) => void;
32
+ };
33
+ skipTrailingSlashes?: boolean;
34
+ useSecureCookies: boolean | undefined;
35
+ ipAddress: {
36
+ disableIpTracking?: boolean;
37
+ ipv6Subnet?: number;
38
+ ipAddressHeaders: string[];
39
+ trustedProxies: string[];
40
+ };
41
+ };
42
+ baseURL: import("better-auth").BaseURLConfig;
43
+ rateLimit: {
44
+ window?: number | undefined;
45
+ max?: number | undefined;
46
+ modelName?: "rateLimit" | import("better-auth").LiteralString;
47
+ fields?: Partial<Record<"count" | "key" | "lastRequest", string>>;
48
+ customRules?: {
49
+ [key: string]: import("better-auth").BetterAuthRateLimitRule | false | ((request: Request, currentRule: import("better-auth").BetterAuthRateLimitRule) => import("better-auth").Awaitable<false | import("better-auth").BetterAuthRateLimitRule>);
50
+ } | undefined;
51
+ storage?: ("memory" | "database" | "secondary-storage") | undefined;
52
+ customStorage?: import("better-auth").BetterAuthRateLimitStorage;
53
+ enabled: boolean;
54
+ };
55
+ secret: string;
56
+ trustedOrigins: string[] | ((request?: Request | undefined) => import("better-auth").Awaitable<(string | undefined | null)[]>);
57
+ }>;
58
+ routes: import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter>;
59
+ }, Config.ConfigError, never>;
60
+ export declare const authDatabase: (url: Redacted.Redacted<string>) => Effect.Effect<Pool, never, import("effect/Scope").Scope>;
61
+ //# sourceMappingURL=mount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../../../src/server/better-auth/mount.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAU,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,IAAI,EAAmB,MAAM,IAAI,CAAC;AAG3C,eAAO,MAAM,eAAe,SAA0D,QAAQ,SAAS,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAsCtH,CAAC;AAEH,eAAO,MAAM,YAAY,8FAqBvB,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { Config, Effect, Option, Redacted } from "effect";
3
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
4
+ import { Pool } from "pg";
5
+ import { trustedProxyPeer, trustedProxyRanges } from "#server/rpc-server/local.ts";
6
+ export const betterAuthMount = Effect.fn("Platform.betterAuthMount")(function* (options) {
7
+ const secret = options.secret ?? Redacted.value(yield* Config.Redacted("BETTER_AUTH_SECRET"));
8
+ const publicUrl = yield* Config.URL("PUBLIC_URL").pipe(Config.option);
9
+ const origin = Option.map(publicUrl, (url) => url.origin);
10
+ const trustedProxy = Option.getOrUndefined(yield* Config.String("TRUSTED_PROXY").pipe(Config.option));
11
+ const clientIpHeader = yield* Config.String("AUTH_CLIENT_IP_HEADER").pipe(Config.withDefault("x-forwarded-for"));
12
+ const rateLimitEnabled = yield* Config.Boolean("AUTH_RATE_LIMIT_ENABLED").pipe(Config.withDefault(true));
13
+ const isTrustedProxy = trustedProxyPeer(trustedProxy);
14
+ const trustedProxies = trustedProxyRanges(trustedProxy);
15
+ const auth = betterAuth({
16
+ ...options,
17
+ advanced: {
18
+ useSecureCookies: Option.isSome(publicUrl) && publicUrl.value.protocol === "https:",
19
+ ...options.advanced,
20
+ ipAddress: {
21
+ ...options.advanced?.ipAddress,
22
+ ipAddressHeaders: trustedProxies.length === 0 ? [] : [clientIpHeader],
23
+ trustedProxies,
24
+ },
25
+ },
26
+ "baseURL": options.baseURL ?? Option.getOrElse(origin, () => ({ allowedHosts: ["127.0.0.1:*"], protocol: "http" })),
27
+ rateLimit: { ...options.rateLimit, enabled: rateLimitEnabled },
28
+ secret,
29
+ trustedOrigins: options.trustedOrigins ?? [Option.getOrElse(origin, () => "http://127.0.0.1:*")],
30
+ });
31
+ const routes = HttpRouter.use((router) => router.add("*", "/api/auth/*", (request) => Effect.gen(function* () {
32
+ const web = yield* HttpServerRequest.toWeb(request);
33
+ if (!Option.exists(request.remoteAddress, isTrustedProxy)) {
34
+ web.headers.delete("x-forwarded-for");
35
+ web.headers.delete(clientIpHeader);
36
+ }
37
+ return HttpServerResponse.fromWeb(yield* Effect.tryPromise(() => auth.handler(web)));
38
+ }).pipe(Effect.catch(() => Effect.succeed(HttpServerResponse.text("Authentication is temporarily unavailable", { status: 503 }))))));
39
+ return { auth, routes };
40
+ });
41
+ export const authDatabase = Effect.fn("Platform.authDatabase")(function* (url) {
42
+ const clients = new Set();
43
+ const run = Effect.runForkWith(yield* Effect.context());
44
+ return yield* Effect.acquireRelease(Effect.sync(() => {
45
+ const pool = new Pool({ connectionString: Redacted.value(url) });
46
+ pool.on("connect", (client) => clients.add(client));
47
+ pool.on("remove", (client) => clients.delete(client));
48
+ pool.on("error", (error) => {
49
+ run(Effect.logDebug({ event: "auth_database_idle_client_failed", message: error.message }));
50
+ });
51
+ return pool;
52
+ }), (pool) => Effect.promise(async () => {
53
+ // pg-pool resolves end before its clients finish disconnecting.
54
+ const disconnected = [...clients].map((client) => new Promise((resolve) => client.once("end", resolve)));
55
+ await pool.end();
56
+ await Promise.all(disconnected);
57
+ }));
58
+ });
59
+ //# sourceMappingURL=mount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.js","sourceRoot":"","sources":["../../../src/server/better-auth/mount.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { testUtils } from "better-auth/plugins";
2
+ export declare function authTestUtils(options?: Parameters<typeof testUtils>[0]): {
3
+ id: "test-utils";
4
+ version: string;
5
+ options: import("better-auth/plugins").TestUtilsOptions;
6
+ init(context: Parameters<(ctx: import("better-auth").AuthContext) => {
7
+ context: {
8
+ test: import("better-auth/plugins").TestHelpers;
9
+ };
10
+ options: {
11
+ databaseHooks: {
12
+ verification: {
13
+ create: {
14
+ after(verification: {
15
+ identifier: string;
16
+ value: string;
17
+ } | null): Promise<void>;
18
+ };
19
+ };
20
+ };
21
+ } | undefined;
22
+ }>[0]): {
23
+ context: {
24
+ test: import("better-auth/plugins").TestHelpers;
25
+ };
26
+ options?: never;
27
+ } | {
28
+ context: {
29
+ test: import("better-auth/plugins").TestHelpers;
30
+ };
31
+ options: {
32
+ databaseHooks: {
33
+ verification: {
34
+ create: {
35
+ after(verification: {
36
+ identifier: string;
37
+ value: string;
38
+ } | null): Promise<void>;
39
+ };
40
+ };
41
+ };
42
+ };
43
+ };
44
+ };
45
+ type TestContext = ReturnType<ReturnType<typeof authTestUtils>["init"]>["context"];
46
+ export declare function signedInUser(auth: {
47
+ readonly $context: Promise<TestContext>;
48
+ }, fields: Parameters<TestContext["test"]["createUser"]>[0]): Promise<import("better-auth/plugins").LoginResult>;
49
+ export {};
50
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../src/server/better-auth/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC;;;;kBAIvD,UAAU;;;;;;;;;;;;;;;;MAAoB,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;EAMhD;AAED,KAAK,WAAW,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAEnF,wBAAsB,YAAY,CAAC,IAAI,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;CAAE,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,sDAI7I"}
@@ -0,0 +1,18 @@
1
+ import { testUtils } from "better-auth/plugins";
2
+ export function authTestUtils(options) {
3
+ const plugin = testUtils(options);
4
+ return {
5
+ ...plugin,
6
+ init(context) {
7
+ const result = plugin.init(context);
8
+ // Remove this wrapper once Better Auth's testUtils omits undefined options.
9
+ return result.options === undefined ? { context: result.context } : { context: result.context, options: result.options };
10
+ },
11
+ };
12
+ }
13
+ export async function signedInUser(auth, fields) {
14
+ const { test } = await auth.$context;
15
+ const user = await test.saveUser(test.createUser(fields));
16
+ return test.login({ userId: user.id });
17
+ }
18
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../../src/server/better-auth/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD,MAAM,UAAU,aAAa,CAAC,OAAyC;IACtE,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO;QACN,GAAG,MAAM;QACT,IAAI,CAAC,OAA0C;YAC9C,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACpC,4EAA4E;YAC5E,OAAO,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;QAC1H,CAAC;KACD,CAAC;AACH,CAAC;AAID,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAiD,EAAE,MAAwD;IAC7I,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC;IACrC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1D,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC"}
@@ -1,9 +1,10 @@
1
1
  import * as PgClient from "@effect/sql-pg/PgClient";
2
2
  import { Config, Layer, type Redacted } from "effect";
3
3
  import * as Reactivity from "effect/reactivity/Reactivity";
4
+ import type { SqlNameTransforms } from "@shivaedev/effect-sql/clientOptions.ts";
4
5
  import { type PostgresChangesOptions } from "@shivaedev/effect-sql/postgresChanges.ts";
5
6
  import type { AppChanges, AppDatabaseOptions } from "#server/appHost.ts";
6
- export interface DatabaseConfig {
7
+ export interface DatabaseConfig extends SqlNameTransforms {
7
8
  readonly applicationName?: string;
8
9
  readonly changes?: Omit<PostgresChangesOptions, "client" | "resync">;
9
10
  readonly databaseUrl?: Redacted.Redacted<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"databaseLayer.d.ts","sourceRoot":"","sources":["../../src/server/databaseLayer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAU,KAAK,EAAE,KAAK,QAAQ,EAAY,MAAM,QAAQ,CAAC;AACxE,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAI3D,OAAO,EAAE,KAAK,sBAAsB,EAAmB,MAAM,0CAA0C,CAAC;AACxG,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGzE,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,UAAU,+UA0BjG"}
1
+ {"version":3,"file":"databaseLayer.d.ts","sourceRoot":"","sources":["../../src/server/databaseLayer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAU,KAAK,EAAE,KAAK,QAAQ,EAAY,MAAM,QAAQ,CAAC;AACxE,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAG3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAEhF,OAAO,EAAE,KAAK,sBAAsB,EAAmB,MAAM,0CAA0C,CAAC;AACxG,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGzE,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACxD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,UAAU,+UA4BjG"}