@shivaedev/identity 0.3.0 → 0.3.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 (2) hide show
  1. package/README.md +131 -133
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -15,11 +15,11 @@ import { Authenticated } from "@shivaedev/identity/middleware.ts";
15
15
  export const Account = RpcGroup.make(Rpc.make("WhoAmI", { success: Schema.String })).middleware(Authenticated);
16
16
 
17
17
  export const Handlers = Account.toLayer({
18
- WhoAmI: () =>
19
- Effect.gen(function* () {
20
- const identity = yield* Identity;
21
- return identity.id;
22
- }),
18
+ WhoAmI: () =>
19
+ Effect.gen(function* () {
20
+ const identity = yield* Identity;
21
+ return identity.id;
22
+ }),
23
23
  });
24
24
  ```
25
25
 
@@ -30,7 +30,6 @@ The browser imports `Account` as it is; the server supplies the session middlewa
30
30
  ### How to think about it
31
31
 
32
32
  There are three parts to an authenticated RPC boundary:
33
-
34
33
  1. **The shared contract** names the middleware tags (`Authenticated`, `MaybeAuthenticated`, `AuthenticatedUser`, permission middleware) and the request-local services they provide. These modules are browser-safe, because the client imports the contract.
35
34
  2. **The server composition** implements the tags with Layers from `server/`. A session provider resolves a session; an Origin policy decides whether its request may reach that provider; request tracing supplies a request id and diagnostic context.
36
35
  3. **The feature handler** reads `Identity`, `OptionalIdentity`, `SessionUser` or `RequestId` and runs application code. Authentication supplies a user id. Authorization still decides whether that user may perform this operation.
@@ -49,22 +48,22 @@ import { Identity } from "@shivaedev/identity/identity.ts";
49
48
  import { Authenticated } from "@shivaedev/identity/middleware.ts";
50
49
 
51
50
  export const Account = RpcGroup.make(
52
- Rpc.make("ReadOwn", {
53
- error: Forbidden,
54
- payload: { userId: Schema.String },
55
- success: Schema.String,
56
- }),
51
+ Rpc.make("ReadOwn", {
52
+ error: Forbidden,
53
+ payload: { userId: Schema.String },
54
+ success: Schema.String,
55
+ }),
57
56
  ).middleware(Authenticated);
58
57
 
59
58
  export const Handlers = Account.toLayer({
60
- ReadOwn: ({ userId }) =>
61
- Effect.gen(function* () {
62
- const identity = yield* Identity;
63
- if (identity.id !== userId) {
64
- return yield* new Forbidden({ message: "Not your account" });
65
- }
66
- return identity.id;
67
- }),
59
+ ReadOwn: ({ userId }) =>
60
+ Effect.gen(function* () {
61
+ const identity = yield* Identity;
62
+ if (identity.id !== userId) {
63
+ return yield* new Forbidden({ message: "Not your account" });
64
+ }
65
+ return identity.id;
66
+ }),
68
67
  });
69
68
  ```
70
69
 
@@ -78,8 +77,8 @@ import { trustedOrigins } from "@shivaedev/identity/server/origin.ts";
78
77
  import { auth } from "./auth.ts";
79
78
 
80
79
  export const sessions = {
81
- origin: trustedOrigins({ allow: ["http://localhost:3000"], missing: "allow" }),
82
- provider: betterAuthSessions((headers) => auth.api.getSession({ headers })),
80
+ origin: trustedOrigins({ allow: ["http://localhost:3000"], missing: "allow" }),
81
+ provider: betterAuthSessions((headers) => auth.api.getSession({ headers })),
83
82
  };
84
83
  ```
85
84
 
@@ -96,12 +95,12 @@ import { Config, Effect } from "effect";
96
95
  import { authDatabase, betterAuthMount } from "@shivaedev/identity/server/better-auth/mount.ts";
97
96
 
98
97
  const authentication = Effect.gen(function* () {
99
- const database = yield* authDatabase(yield* Config.Redacted("DATABASE_URL"));
100
- return yield* betterAuthMount({
101
- database,
102
- emailAndPassword: { enabled: true },
103
- session: { cookieCache: { enabled: false } },
104
- });
98
+ const database = yield* authDatabase(yield* Config.Redacted("DATABASE_URL"));
99
+ return yield* betterAuthMount({
100
+ database,
101
+ emailAndPassword: { enabled: true },
102
+ session: { cookieCache: { enabled: false } },
103
+ });
105
104
  });
106
105
  ```
107
106
 
@@ -145,7 +144,7 @@ A native client signs in through `/api/auth/sign-in/email` with `credentials: "o
145
144
 
146
145
  Native sign-out revokes the stored session. Signing in again while sending the current bearer replaces it and revokes the old session after successful sign-in. Streams keep their original request headers, so the old session ends with `Unauthorized` on the next revalidation; it never adopts the replacement. Keep cookie caching disabled for database-backed revocation and choose `streamRevalidationInterval` for the required delivery window.
147
146
 
148
- `appHost`'s `nativeOrigins` option admits those exact origins through the RPC transport guard and the auth routes. Wildcards, patterns and URLs with paths are rejected at startup. Keep the list aligned with `bearerSessions.origins`, Better Auth's `trustedOrigins` and the RPC session origin policy: the later origin policies alone cannot open the transport guard. Omit the option to keep the existing same-origin transport policy.
147
+ `appHost`'s `nativeOrigins` option admits those exact origins through the RPC transport guard and the auth routes. Wildcards, patterns and URLs with paths are rejected at startup. Keep the list aligned with `bearerSessions.origins`, Better Auth's `trustedOrigins` and the RPC session origin policy: the later origin policies alone cannot open the transport guard. Without the option, the transport guard keeps its same-origin policy.
149
148
 
150
149
  The host answers native preflights with POST for RPC, GET and POST for auth, and only `Authorization` and `Content-Type` headers. Native responses echo the allowed origin exactly with `Vary: Origin`; auth responses expose `set-auth-token`. The host never enables CORS credentials. It strips cookies from admitted native requests before auth or RPC session resolution, so only the signed bearer session can authenticate them. This prevents native access from opening a cookie CSRF path, even if a caller sends a cookie. Resource authorization remains app-owned.
151
150
 
@@ -163,17 +162,17 @@ import { betterAuthSessions } from "@shivaedev/identity/server/adapters/betterAu
163
162
  import { trustedOrigins } from "@shivaedev/identity/server/origin.ts";
164
163
 
165
164
  const authentication = Effect.gen(function* () {
166
- const database = yield* authDatabase(yield* Config.Redacted("DATABASE_URL"));
167
- const mounted = yield* betterAuthMount(
168
- { database, session: { cookieCache: { enabled: false } }, socialProviders: { github } },
169
- { signIn: { accounts: allowedAccounts, recheckInterval: "5 minutes", rule: inOrganization } },
170
- );
171
- yield* mounted.revokeUnadmitted;
172
- const sessions = {
173
- origin: trustedOrigins({ allow: ["https://app.example"], missing: "reject" }),
174
- provider: betterAuthSessions(mounted.admittedSessions((headers) => mounted.auth.api.getSession({ headers }))),
175
- };
176
- return { ...mounted, sessions };
165
+ const database = yield* authDatabase(yield* Config.Redacted("DATABASE_URL"));
166
+ const mounted = yield* betterAuthMount(
167
+ { database, session: { cookieCache: { enabled: false } }, socialProviders: { github } },
168
+ { signIn: { accounts: allowedAccounts, recheckInterval: "5 minutes", rule: inOrganization } },
169
+ );
170
+ yield* mounted.revokeUnadmitted;
171
+ const sessions = {
172
+ origin: trustedOrigins({ allow: ["https://app.example"], missing: "reject" }),
173
+ provider: betterAuthSessions(mounted.admittedSessions((headers) => mounted.auth.api.getSession({ headers }))),
174
+ };
175
+ return { ...mounted, sessions };
177
176
  });
178
177
  ```
179
178
 
@@ -199,18 +198,16 @@ import { Rpc, RpcGroup } from "effect/rpc";
199
198
  import { OptionalIdentity } from "@shivaedev/identity/identity.ts";
200
199
  import { MaybeAuthenticated } from "@shivaedev/identity/middleware.ts";
201
200
 
202
- export const Greetings = RpcGroup.make(
203
- Rpc.make("Greeting", { success: Schema.String }),
204
- ).middleware(MaybeAuthenticated);
201
+ export const Greetings = RpcGroup.make(Rpc.make("Greeting", { success: Schema.String })).middleware(MaybeAuthenticated);
205
202
 
206
203
  export const GreetingHandlers = Greetings.toLayer({
207
- Greeting: () =>
208
- Effect.map(OptionalIdentity, (identity) =>
209
- Option.match(identity, {
210
- onNone: () => "hello anonymous",
211
- onSome: ({ id }) => `hello ${id}`,
212
- }),
213
- ),
204
+ Greeting: () =>
205
+ Effect.map(OptionalIdentity, (identity) =>
206
+ Option.match(identity, {
207
+ onNone: () => "hello anonymous",
208
+ onSome: ({ id }) => `hello ${id}`,
209
+ }),
210
+ ),
214
211
  });
215
212
  ```
216
213
 
@@ -230,7 +227,7 @@ Declare expected refusals with the shared tagged error schemas from [`@shivaedev
230
227
 
231
228
  ### Reading the signed-in user
232
229
 
233
- When a handler needs the current user's name and email, declare `AuthenticatedUser` and read `SessionUser`. `Identity` keeps its `{ id }` shape; the new middleware supplies both services for that invocation. Its provider must return `user: { id, name, email }`, described by `UserSessionShape`.
230
+ When a handler needs the current user's name and email, declare `AuthenticatedUser` and read `SessionUser`. `Identity` keeps its `{ id }` shape; `AuthenticatedUser` supplies both services for that invocation. Its provider must return `user: { id, name, email }`, described by `UserSessionShape`.
234
231
 
235
232
  ```ts
236
233
  import { Effect, Layer, Schema } from "effect";
@@ -240,13 +237,13 @@ import { AuthenticatedUser } from "@shivaedev/identity/middleware.ts";
240
237
  import { authenticatedUserLayer } from "@shivaedev/identity/server/session.ts";
241
238
 
242
239
  export const Account = RpcGroup.make(
243
- Rpc.make("Whoami", {
244
- success: Schema.Struct({ id: Schema.String, name: Schema.String, email: Schema.String }),
245
- }),
240
+ Rpc.make("Whoami", {
241
+ success: Schema.Struct({ id: Schema.String, name: Schema.String, email: Schema.String }),
242
+ }),
246
243
  ).middleware(AuthenticatedUser);
247
244
 
248
245
  export const AccountHandlers = Account.toLayer({
249
- Whoami: () => Effect.map(SessionUser, (user) => user),
246
+ Whoami: () => Effect.map(SessionUser, (user) => user),
250
247
  });
251
248
 
252
249
  export const AccountServer = Layer.merge(AccountHandlers, authenticatedUserLayer(sessions));
@@ -265,21 +262,21 @@ import { AuthUnavailable, Unauthorized } from "@shivaedev/errors/taxonomy.ts";
265
262
  import { authenticatedUserWithProviderAccountsLayer } from "@shivaedev/identity/server/session.ts";
266
263
 
267
264
  const sessions = {
268
- origin: trustedOrigins({ allow: ["https://app.example"], missing: "reject" }),
269
- provider: betterAuthSessions(
270
- (headers) => auth.api.getSession({ headers }),
271
- (headers) => auth.api.listUserAccounts({ headers }),
272
- ),
265
+ origin: trustedOrigins({ allow: ["https://app.example"], missing: "reject" }),
266
+ provider: betterAuthSessions(
267
+ (headers) => auth.api.getSession({ headers }),
268
+ (headers) => auth.api.listUserAccounts({ headers }),
269
+ ),
273
270
  };
274
271
 
275
272
  const githubAccount = Effect.gen(function* () {
276
- const user = yield* SessionUserWithProviderAccounts;
277
- const accounts = yield* user.providerAccounts;
278
- return accounts.find((account) => account.providerId === "github");
273
+ const user = yield* SessionUserWithProviderAccounts;
274
+ const accounts = yield* user.providerAccounts;
275
+ return accounts.find((account) => account.providerId === "github");
279
276
  });
280
277
  ```
281
278
 
282
- 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.
279
+ Declare `Schema.Union([AuthUnavailable, Unauthorized])` in the account-reading RPC's `error` schema: lookup failures 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.
283
280
 
284
281
  The lookup uses the same transport headers and runs only when the handler evaluates `user.providerAccounts`. The boundary memoizes a successful result in that invocation's Effect context, including concurrent reads, without sharing it across requests or users. A failed lookup is not retained: the next read tries again. 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.
285
282
 
@@ -327,7 +324,7 @@ import { permissionLayer } from "@shivaedev/identity/server/permissions.ts";
327
324
  class ToolsPermission extends permissionMiddleware<ToolsPermission>()("app/ToolsPermission", UseTools) {}
328
325
  const Tools = RpcGroup.make(Rpc.make("tools", { success: Schema.String }).middleware(ToolsPermission));
329
326
  const handlers = Tools.toLayer({ tools: () => Effect.map(SessionUser, (user) => user.email) }).pipe(
330
- Layer.provideMerge(permissionLayer(ToolsPermission, sessions)),
327
+ Layer.provideMerge(permissionLayer(ToolsPermission, sessions)),
331
328
  );
332
329
  ```
333
330
 
@@ -340,13 +337,15 @@ import { HttpRouter, HttpServerResponse } from "effect/http";
340
337
  import { permissionRoute } from "@shivaedev/identity/server/permissions.ts";
341
338
 
342
339
  const routes = HttpRouter.use((router) =>
343
- router.add("GET", "/tools", permissionRoute(sessions, UseTools)(() =>
344
- Effect.map(SessionUser, (user) => HttpServerResponse.text(user.email)),
345
- )),
340
+ router.add(
341
+ "GET",
342
+ "/tools",
343
+ permissionRoute(sessions, UseTools)(() => Effect.map(SessionUser, (user) => HttpServerResponse.text(user.email))),
344
+ ),
346
345
  );
347
346
  ```
348
347
 
349
- An operation on one record reads that record's owner and uses the application's access policy. With a request-local user, `const { role } = yield* Access.resolve(user.role)` recovers the application's closed role type; `yield* Access.requireAccess({ id: user.id, role }, UseTools, record.ownerId)` then permits an explicit `any` grant or an `own` grant for that owner. Keep these resource decisions in the handler or domain service. Browser interfaces can use `hasPermission(user, permission)` from `rpc/permission.ts` over the client session's grants; the server always resolves its own grants.
348
+ An operation on one record reads that record's owner and uses the application's access policy. With a request-local user, `const { role } = yield* Access.resolve(user.role)` recovers the application's closed role type; `yield* Access.requireAccess({ id: user.id, role }, UseTools, record.ownerId)` then permits an explicit `any` grant or an `own` grant for that owner. Keep these resource decisions in the handler or domain service. Browser interfaces can use `hasPermission(user, permission)` from `@shivaedev/identity/permission.ts` over the client session's grants; the server always resolves its own grants.
350
349
 
351
350
  ### Session-scoped contract keys
352
351
 
@@ -356,7 +355,7 @@ The host's generated `$changes` handler accepts `{ queries: [{ query: "get", pay
356
355
 
357
356
  `authenticatedLayer`, `authenticatedUserLayer`, `authenticatedUserWithProviderAccountsLayer` and `permissionLayer` run scoped command invalidation inside the authenticated invocation. Successful commands publish their scoped keys after commit and omit keys the handler's transactions already published. A rejected transaction publishes no keys.
358
357
 
359
- Hosted applications use `Api.application.toLayer(...)` and let `appHost` own `$changes`. When migrating a custom change handler, accept query references and resolve them inside `Stream.unwrap`, after authentication has provided `Identity`:
358
+ Hosted applications use `Api.application.toLayer(...)` and let `appHost` own `$changes`. A custom change handler accepts query references and resolves them inside `Stream.unwrap`, after authentication has provided `Identity`:
360
359
 
361
360
  ```ts
362
361
  import { Effect, Stream } from "effect";
@@ -364,13 +363,10 @@ import { resolveChanges } from "@shivaedev/identity/server/sessionKeys.ts";
364
363
 
365
364
  const changesRpc = Api.requests.get("preferences.$changes")!;
366
365
  const handlers = Api.toLayer({
367
- "preferences.$changes": ({ queries }) => Stream.unwrap(
368
- Effect.map(resolveChanges(changesRpc, queries), ({ subscription, context }) =>
369
- changes.subscribe(subscription, context)
370
- )
371
- ),
372
- "preferences.get": getPreference,
373
- "preferences.save": savePreference,
366
+ "preferences.$changes": ({ queries }) =>
367
+ Stream.unwrap(Effect.map(resolveChanges(changesRpc, queries), ({ subscription, context }) => changes.subscribe(subscription, context))),
368
+ "preferences.get": getPreference,
369
+ "preferences.save": savePreference,
374
370
  });
375
371
  ```
376
372
 
@@ -385,12 +381,12 @@ import { Identity } from "@shivaedev/identity/identity.ts";
385
381
  import { commandChanges } from "@shivaedev/identity/server/commandChanges.ts";
386
382
 
387
383
  const authentication = Layer.succeed(MyAuthentication, (effect, { headers, rpc, payload }) =>
388
- Effect.gen(function* () {
389
- const session = yield* requireTransportSession(headers, rpc);
390
- return yield* authorizeReads(commandChanges(effect, rpc, payload), { rpc, payload }, Option.some({ id: session.user.id })).pipe(
391
- Effect.provideService(Identity, { id: session.user.id })
392
- );
393
- })
384
+ Effect.gen(function* () {
385
+ const session = yield* requireTransportSession(headers, rpc);
386
+ return yield* authorizeReads(commandChanges(effect, rpc, payload), { rpc, payload }, Option.some({ id: session.user.id })).pipe(
387
+ Effect.provideService(Identity, { id: session.user.id }),
388
+ );
389
+ }),
394
390
  );
395
391
  ```
396
392
 
@@ -404,9 +400,9 @@ Set `SessionPolicy.streamRevalidationInterval` to an Effect `Duration.Input` to
404
400
 
405
401
  ```ts
406
402
  const sessions = {
407
- origin: trustedOrigins({ allow: ["https://app.example.com"], missing: "reject" }),
408
- provider: betterAuthSessions((headers) => auth.api.getSession({ headers })),
409
- streamRevalidationInterval: "5 seconds",
403
+ origin: trustedOrigins({ allow: ["https://app.example.com"], missing: "reject" }),
404
+ provider: betterAuthSessions((headers) => auth.api.getSession({ headers })),
405
+ streamRevalidationInterval: "5 seconds",
410
406
  };
411
407
  ```
412
408
 
@@ -416,14 +412,12 @@ An `AuthUnavailable` failure during revalidation leaves the stream running and t
416
412
 
417
413
  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.
418
414
 
419
- react’s `actionTrace` groups an action’s RPC Effects under one browser trace; editor saves and create submissions group their save Effects automatically. Native `RpcClient.layerProtocolHttp` with `FetchHttpClient.layer` sends W3C `traceparent` on each request. appHost continues that HTTP trace for RPC handling, including without an installed tracer or exporter. Each RPC span records `request.id` and `rpc.method`; native SQL spans executed by the handler inherit that trace.
420
-
421
- When a handler enqueues through jobs, the job stores the current span context. Wrap enqueueing in an application-named producer span with `Effect.withSpan("Receipt.enqueue", { kind: "producer" })` to show the enqueue operation in the action trace. Worker attempts run in their own trace, linked back to the captured sender span. A trace viewer can follow that link between the action and its job.
422
-
423
415
  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`.
424
416
 
425
417
  Caller-chosen request ids are correlation data. Keep authorization tied to the authenticated identity.
426
418
 
419
+ The app host of `@shivaedev/server` adds this tracing to its RPC group and continues the caller's W3C trace; react's [action tracing](https://github.com/ShivaeDev/platform/blob/main/packages/react/README.md#trace-an-action-across-rpcs) follows one browser action through RPC, SQL and a queued job.
420
+
427
421
  ### Signed-in story preconditions
428
422
 
429
423
  Use `testUtils()` from `better-auth/plugins` in a test-only Better Auth instance with the host's database, secret and cookie policy. Keep the plugin out of production auth options.
@@ -436,14 +430,14 @@ import { testUtils } from "better-auth/plugins";
436
430
  import { signedInUser } from "@shivaedev/identity/server/better-auth/testing.ts";
437
431
 
438
432
  const testAuth = betterAuth({
439
- ...options,
440
- plugins: [...(options.plugins ?? []), testUtils()],
433
+ ...options,
434
+ plugins: [...(options.plugins ?? []), testUtils()],
441
435
  });
442
436
  const { user, headers, cookies } = await signedInUser(testAuth, {
443
- email: "member@example.test",
444
- name: "Member",
445
- providerAccounts: [{ providerId: "github", accountId: "123" }],
446
- role: "user,admin",
437
+ email: "member@example.test",
438
+ name: "Member",
439
+ providerAccounts: [{ providerId: "github", accountId: "123" }],
440
+ role: "user,admin",
447
441
  });
448
442
  ```
449
443
 
@@ -458,25 +452,29 @@ import { seedCookies } from "@shivaedev/test-story/server/seedCookies.ts";
458
452
  import { signedInUser, storedProviderAccounts } from "@shivaedev/identity/server/better-auth/testing.ts";
459
453
 
460
454
  const member = app.trait("account", "a member starts signed in with GitHub", function* ({ auth }) {
461
- const { cookies } = yield* Effect.promise(() => signedInUser(auth, {
462
- email: "member@example.test",
463
- name: "Member",
464
- providerAccounts: [{ providerId: "github", accountId: "123" }],
465
- }));
466
- yield* seedCookies(cookies);
455
+ const { cookies } = yield* Effect.promise(() =>
456
+ signedInUser(auth, {
457
+ email: "member@example.test",
458
+ name: "Member",
459
+ providerAccounts: [{ providerId: "github", accountId: "123" }],
460
+ }),
461
+ );
462
+ yield* seedCookies(cookies);
467
463
  });
468
464
 
469
465
  app.it("the member's provider account stores no credentials", [member], function* ({ session, story }) {
470
- const { user } = yield* session();
471
- const accounts = yield* Effect.promise(() => storedProviderAccounts(story.engine.auth, user.id));
472
- expect(accounts).toEqual([{
473
- providerId: "github",
474
- accountId: "123",
475
- hasAccessToken: false,
476
- hasRefreshToken: false,
477
- hasIdToken: false,
478
- hasPassword: false,
479
- }]);
466
+ const { user } = yield* session();
467
+ const accounts = yield* Effect.promise(() => storedProviderAccounts(story.engine.auth, user.id));
468
+ expect(accounts).toEqual([
469
+ {
470
+ providerId: "github",
471
+ accountId: "123",
472
+ hasAccessToken: false,
473
+ hasRefreshToken: false,
474
+ hasIdToken: false,
475
+ hasPassword: false,
476
+ },
477
+ ]);
480
478
  });
481
479
  ```
482
480
 
@@ -492,32 +490,32 @@ Register the Effect with a worker schedule, as the starter does at midnight UTC.
492
490
 
493
491
  Import the module that defines the name; there is no package-root entry. Paths below follow `@shivaedev/identity/`.
494
492
 
495
- | Module | Names and purpose |
496
- | --- | --- |
497
- | `identity.ts` | `RequestId`, `Identity`, `OptionalIdentity`, `SessionUser`, `SessionUserWithProviderAccounts`: request-local services. |
498
- | `middleware.ts` | `RequestTracing`, `Authenticated`, `AuthenticatedUser`, `AuthenticatedUserWithProviderAccounts`, `MaybeAuthenticated`: native RPC middleware declarations. |
499
- | `permission.ts` | Native permission middleware declarations and predicates over resolved grants. |
500
- | `server/permissions.ts` | Permission middleware Layers and HTTP route guards. |
501
- | `server/session.ts` | `authenticatedLayer`, `maybeAuthenticatedLayer`, `authenticatedUserLayer`, `authenticatedUserWithProviderAccountsLayer`, `withSessionUser`, `resolveSession`: session middleware implementations and the identity boundary. |
502
- | `server/adapters/betterAuthSessions.ts` | `betterAuthSessions(getSession, getProviderAccounts?)`: a session provider from Better Auth lookups. |
503
- | `server/origin.ts` | `trustedOrigins({ allow, missing })`: an exact Origin allowlist. |
504
- | `server/local.ts` | `localRpcGuard`, `loopbackOnly`, `sameOrigin`, `trustedProxyPeer`, `trustedProxyRanges`: loopback and proxy guards. |
505
- | `server/transportHeaders.ts` | `transportHeaders(rpcHeaders)`: the headers used for a transport-level decision. |
506
- | `server/tracing.ts` | `requestTracingLayer({ header?, sensitive? })`: the `RequestTracing` implementation. |
507
- | `server/sessionKeys.ts` | `resolveChanges`: session-scoped change subscriptions for custom change handlers. |
508
- | `server/better-auth/mount.ts` | `betterAuthMount(options, { signIn?, bearerSessions? })`, returning `{ auth, routes, admittedSessions, revokeUnadmitted }`, and `authDatabase(url, { max?, application_name? }?)`: native auth routes, the sign-in policy and a scoped PostgreSQL pool. |
509
- | `server/better-auth/allowedAccounts.ts` | `AllowedAccounts`, `allowedAccounts`: the provider-account list Schema and its `AUTH_ALLOWED_ACCOUNTS` Config. |
510
- | `server/better-auth/signInGate.ts` | `SignInPolicy`: the `accounts`, `rule` and `recheckInterval` of a sign-in policy. |
511
- | `server/better-auth/revokeUnadmitted.ts` | `RevokedSessions`: the `{ users, sessions }` counts of a revocation. |
512
- | `server/better-auth/userProfiles.ts` | Public profile lookups for other users. |
513
- | `server/better-auth/sweepExpiredSessions.ts` | `sweepExpiredSessions`: delete expired sessions through the application's SQL client and return the number removed. |
514
- | `server/better-auth/testDatabase.ts` | `authTestDatabase(pool)`: test-only identity/session readouts and faults for the default PostgreSQL auth schema. |
515
- | `server/better-auth/testing.ts` | `signedInUser(auth, fields)`, `storedProviderAccounts(auth, userId)`: test-only minted user sessions and credential storage observations. |
493
+ | Module | Names and purpose |
494
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
495
+ | `identity.ts` | `RequestId`, `Identity`, `OptionalIdentity`, `SessionUser`, `SessionUserWithProviderAccounts`: request-local services. |
496
+ | `middleware.ts` | `RequestTracing`, `Authenticated`, `AuthenticatedUser`, `AuthenticatedUserWithProviderAccounts`, `MaybeAuthenticated`: native RPC middleware declarations. |
497
+ | `permission.ts` | `permissionMiddleware`, `hasPermission`, `requirePermission`: native permission middleware declarations and predicates over resolved grants. |
498
+ | `server/permissions.ts` | `permissionLayer`, `permissionRoute`: permission middleware Layers and HTTP route guards. |
499
+ | `server/session.ts` | `authenticatedLayer`, `maybeAuthenticatedLayer`, `authenticatedUserLayer`, `authenticatedUserWithProviderAccountsLayer`, `withSessionUser`, `resolveSession`: session middleware implementations and the identity boundary. |
500
+ | `server/adapters/betterAuthSessions.ts` | `betterAuthSessions(getSession, getProviderAccounts?)`: a session provider from Better Auth lookups. |
501
+ | `server/origin.ts` | `trustedOrigins({ allow, missing })`: an exact Origin allowlist. |
502
+ | `server/local.ts` | `localRpcGuard`, `loopbackOnly`, `sameOrigin`, `trustedProxyPeer`, `trustedProxyRanges`: loopback and proxy guards. |
503
+ | `server/transportHeaders.ts` | `transportHeaders(rpcHeaders)`: the headers used for a transport-level decision. |
504
+ | `server/tracing.ts` | `requestTracingLayer({ header?, sensitive? })`: the `RequestTracing` implementation. |
505
+ | `server/sessionKeys.ts` | `resolveChanges`: session-scoped change subscriptions for custom change handlers. |
506
+ | `server/commandChanges.ts` | `commandChanges(effect, rpc, payload)`: scoped command invalidation for custom authentication middleware. |
507
+ | `server/better-auth/mount.ts` | `betterAuthMount(options, { signIn?, bearerSessions?, roles? })`, returning `{ auth, routes, sessions, admittedSessions, revokeUnadmitted }`, and `authDatabase(url, { max?, application_name? }?)`: native auth routes, the sign-in policy and a scoped PostgreSQL pool. |
508
+ | `server/better-auth/allowedAccounts.ts` | `AllowedAccounts`, `allowedAccounts`: the provider-account list Schema and its `AUTH_ALLOWED_ACCOUNTS` Config. |
509
+ | `server/better-auth/signInGate.ts` | `SignInPolicy`: the `accounts`, `rule` and `recheckInterval` of a sign-in policy. |
510
+ | `server/better-auth/revokeUnadmitted.ts` | `RevokedSessions`: the `{ users, sessions }` counts of a revocation. |
511
+ | `server/better-auth/userProfiles.ts` | Public profile lookups for other users. |
512
+ | `server/better-auth/sweepExpiredSessions.ts` | `sweepExpiredSessions`: delete expired sessions through the application's SQL client and return the number removed. |
513
+ | `server/better-auth/testDatabase.ts` | `authTestDatabase(pool)`: test-only identity/session readouts and faults for the default PostgreSQL auth schema. |
514
+ | `server/better-auth/testing.ts` | `signedInUser(auth, fields)`, `storedProviderAccounts(auth, userId)`: test-only minted user sessions and credential storage observations. |
516
515
 
517
516
  ### Install, setup and limits
518
517
 
519
518
  Install `@shivaedev/identity` with `effect` as its peer. Everything outside `server/better-auth/` works with only `effect` installed, including the Promise-based `betterAuthSessions` adapter. Better Auth mounts and test fixtures need the optional peers `better-auth` and `pg`; `sweepExpiredSessions` needs only the application's Effect SQL client. Browser code imports only `identity.ts`, `middleware.ts` and `permission.ts`.
520
-
521
519
  - The package needs Node 24 or newer.
522
520
  - 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.
523
521
  - Without an `HttpServerRequest`, `transportHeaders` falls back to the supplied RPC headers. An application using another transport must decide what headers it can trust.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/identity",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Request identity, sessions, transport trust and permission enforcement for Effect RPC servers",
5
5
  "keywords": [
6
6
  "effect"
@@ -46,14 +46,14 @@
46
46
  "dependencies": {
47
47
  "@shivaedev/access": "0.1.1",
48
48
  "@shivaedev/changes": "0.5.0",
49
- "@shivaedev/contract": "0.19.0",
49
+ "@shivaedev/contract": "0.19.1",
50
50
  "@shivaedev/errors": "0.1.0",
51
51
  "@shivaedev/sql": "0.24.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@effect/platform-node": "4.0.5",
55
55
  "@effect/sql-pg": "4.0.5",
56
- "@shivaedev/quality": "0.43.0",
56
+ "@shivaedev/quality": "0.44.0",
57
57
  "@shivaedev/test-sql": "0.10.2",
58
58
  "@shivaedev/test-story": "0.22.0",
59
59
  "@types/node": "24.19.2",