@shivaedev/identity 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -12
- package/dist/middleware.d.ts +5 -4
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +7 -4
- package/dist/middleware.js.map +1 -1
- package/dist/permission.d.ts +2 -1
- package/dist/permission.d.ts.map +1 -1
- package/dist/permission.js +2 -1
- package/dist/permission.js.map +1 -1
- package/dist/server/adapters/betterAuthSessions.d.ts.map +1 -1
- package/dist/server/adapters/betterAuthSessions.js +2 -1
- package/dist/server/adapters/betterAuthSessions.js.map +1 -1
- package/dist/server/better-auth/clientIp.d.ts +8 -0
- package/dist/server/better-auth/clientIp.d.ts.map +1 -0
- package/dist/server/better-auth/clientIp.js +28 -0
- package/dist/server/better-auth/clientIp.js.map +1 -0
- package/dist/server/better-auth/mount.d.ts +1 -1
- package/dist/server/better-auth/mount.d.ts.map +1 -1
- package/dist/server/better-auth/mount.js +6 -6
- package/dist/server/better-auth/mount.js.map +1 -1
- package/dist/server/better-auth/mountRoutes.d.ts +1 -0
- package/dist/server/better-auth/mountRoutes.d.ts.map +1 -1
- package/dist/server/better-auth/mountRoutes.js +6 -8
- package/dist/server/better-auth/mountRoutes.js.map +1 -1
- package/dist/server/better-auth/sweepExpiredSessions.d.ts +4 -0
- package/dist/server/better-auth/sweepExpiredSessions.d.ts.map +1 -0
- package/dist/server/better-auth/sweepExpiredSessions.js +12 -0
- package/dist/server/better-auth/sweepExpiredSessions.js.map +1 -0
- package/dist/server/better-auth/testDatabase.d.ts +1 -1
- package/dist/server/better-auth/testDatabase.d.ts.map +1 -1
- package/dist/server/better-auth/testDatabase.js +2 -2
- package/dist/server/better-auth/testDatabase.js.map +1 -1
- package/dist/server/commandChanges.d.ts +0 -1
- package/dist/server/commandChanges.d.ts.map +1 -1
- package/dist/server/commandChanges.js +6 -7
- package/dist/server/commandChanges.js.map +1 -1
- package/dist/server/local.d.ts.map +1 -1
- package/dist/server/local.js +3 -2
- package/dist/server/local.js.map +1 -1
- package/dist/server/permissions.d.ts.map +1 -1
- package/dist/server/permissions.js +9 -3
- package/dist/server/permissions.js.map +1 -1
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +8 -7
- package/dist/server/session.js.map +1 -1
- package/dist/server/sessionKeys.d.ts +8 -4
- package/dist/server/sessionKeys.d.ts.map +1 -1
- package/dist/server/sessionKeys.js +11 -7
- package/dist/server/sessionKeys.js.map +1 -1
- package/package.json +9 -8
- package/src/middleware.ts +7 -4
- package/src/permission.ts +2 -1
- package/src/server/adapters/betterAuthSessions.ts +2 -1
- package/src/server/better-auth/clientIp.ts +34 -0
- package/src/server/better-auth/mount.ts +7 -6
- package/src/server/better-auth/mountRoutes.ts +13 -8
- package/src/server/better-auth/sweepExpiredSessions.ts +13 -0
- package/src/server/better-auth/testDatabase.ts +7 -3
- package/src/server/commandChanges.ts +6 -8
- package/src/server/local.ts +3 -2
- package/src/server/permissions.ts +15 -7
- package/src/server/session.ts +8 -7
- package/src/server/sessionKeys.ts +12 -8
|
@@ -3,9 +3,13 @@ import type { Pool } from "pg";
|
|
|
3
3
|
|
|
4
4
|
export function authTestDatabase(pool: Pool) {
|
|
5
5
|
return {
|
|
6
|
-
expireSessions:
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
expireSessions: (userId?: string) =>
|
|
7
|
+
Effect.tryPromise(async () => {
|
|
8
|
+
await pool.query(
|
|
9
|
+
`update "session" set "expiresAt" = now() - interval '1 second'${userId === undefined ? "" : ' where "userId" = $1'}`,
|
|
10
|
+
userId === undefined ? [] : [userId],
|
|
11
|
+
);
|
|
12
|
+
}),
|
|
9
13
|
omitImageColumn: Effect.tryPromise(async () => {
|
|
10
14
|
await pool.query('alter table "user" drop column image');
|
|
11
15
|
}),
|
|
@@ -2,21 +2,18 @@ import { Context, Effect, Option } from "effect";
|
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
3
|
import type { Rpc } from "effect/rpc";
|
|
4
4
|
import { SqlClient } from "effect/sql";
|
|
5
|
-
import { invalidationKeys } from "@shivaedev/
|
|
6
|
-
import {
|
|
5
|
+
import { invalidationKeys } from "@shivaedev/changes/keys.ts";
|
|
6
|
+
import { CurrentViewer } from "@shivaedev/contract/readPolicy.ts";
|
|
7
|
+
import { CommandInvalidation } from "@shivaedev/contract/serverMetadata.ts";
|
|
7
8
|
import { ChangeObserver, invalidateOnCommit, transact } from "@shivaedev/sql/transact.ts";
|
|
8
9
|
import { sessionKeyContext } from "#server/sessionKeys.ts";
|
|
9
10
|
|
|
10
|
-
export function sessionCommandChanges<A, E, R>(effect: Effect.Effect<A, E, R>, rpc: Rpc.AnyWithProps, payload: unknown) {
|
|
11
|
-
return Option.isSome(Context.getOption(rpc.annotations, KeyScope)) ? commandChanges(effect, rpc, payload) : effect;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
export function commandChanges<A, E, R>(effect: Effect.Effect<A, E, R>, rpc: Rpc.AnyWithProps, payload: unknown) {
|
|
15
12
|
const invalidates = Context.getOption(rpc.annotations, CommandInvalidation);
|
|
16
13
|
if (Option.isNone(invalidates)) {
|
|
17
14
|
return effect;
|
|
18
15
|
}
|
|
19
|
-
const declared = invalidates.value;
|
|
16
|
+
const declared = invalidates.value.invalidates;
|
|
20
17
|
return Effect.gen(function* () {
|
|
21
18
|
const context = yield* sessionKeyContext(rpc);
|
|
22
19
|
const sql = yield* Effect.serviceOption(SqlClient.SqlClient);
|
|
@@ -36,7 +33,8 @@ export function commandChanges<A, E, R>(effect: Effect.Effect<A, E, R>, rpc: Rpc
|
|
|
36
33
|
return observer(event);
|
|
37
34
|
}),
|
|
38
35
|
);
|
|
39
|
-
const
|
|
36
|
+
const viewer = yield* CurrentViewer;
|
|
37
|
+
const keys = invalidationKeys(declared(payload, result, viewer), context).filter((key) => !published.has(key));
|
|
40
38
|
if (keys.length > 0) {
|
|
41
39
|
const announce = Option.isNone(sql)
|
|
42
40
|
? reactivity.value.invalidate(keys)
|
package/src/server/local.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect, Option } from "effect";
|
|
2
2
|
import { HttpServerRequest, HttpServerResponse, HttpTraceContext } from "effect/http";
|
|
3
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
3
4
|
import { RpcSerialization } from "effect/rpc";
|
|
4
5
|
import { proxyAddresses, proxyMatches } from "./trustedProxy.ts";
|
|
5
6
|
import { trustedProxyRanges as normalizedRanges } from "./trustedProxyRanges.ts";
|
|
@@ -40,7 +41,7 @@ export function loopbackOnly<E, R>(
|
|
|
40
41
|
return (request: HttpServerRequest.HttpServerRequest) =>
|
|
41
42
|
loopbackHost(request.headers.host) && Option.match(request.remoteAddress, { onNone: () => true, onSome: loopbackAddress })
|
|
42
43
|
? handler(request)
|
|
43
|
-
: Effect.succeed(HttpServerResponse.text("Only loopback hosts are served", { status:
|
|
44
|
+
: Effect.succeed(HttpServerResponse.text("Only loopback hosts are served", { status: HttpStatus.fromLiteral("Forbidden") }));
|
|
44
45
|
}
|
|
45
46
|
export function trustedProxyPeer(trustedProxy?: string): (address: string) => boolean {
|
|
46
47
|
const addresses = trustedProxy === undefined ? undefined : proxyAddresses(trustedProxy);
|
|
@@ -86,7 +87,7 @@ export function localRpcGuard<E, R>(
|
|
|
86
87
|
const contentType = request.headers["content-type"]?.split(";")[0]?.trim().toLowerCase();
|
|
87
88
|
return allowed(request) && contentType === serialization.contentType
|
|
88
89
|
? yield* continueHttpTrace(effect, request)
|
|
89
|
-
: HttpServerResponse.text("Local same-origin requests only", { status:
|
|
90
|
+
: HttpServerResponse.text("Local same-origin requests only", { status: HttpStatus.fromLiteral("Forbidden") });
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Effect, Layer, Option } from "effect";
|
|
2
2
|
import { type HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
3
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
3
4
|
import type { RpcMiddleware } from "effect/rpc";
|
|
4
5
|
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
6
|
+
import { authorizeReads } from "@shivaedev/contract/readPolicy.ts";
|
|
7
|
+
import type { MissingReadPolicy, QueryReadDenied } from "@shivaedev/contract/readPolicyErrors.ts";
|
|
5
8
|
import { type AuthUnavailable, Forbidden, type Unauthorized } from "@shivaedev/errors/taxonomy.ts";
|
|
6
9
|
import { Identity, type SessionUser } from "#identity.ts";
|
|
7
10
|
import { hasPermission, type permissionMiddleware, requirePermission } from "#permission.ts";
|
|
8
|
-
import {
|
|
11
|
+
import { commandChanges } from "#server/commandChanges.ts";
|
|
9
12
|
import { revalidateStream } from "./revalidateStream.ts";
|
|
10
13
|
import { resolveSession, type SessionPolicy, type UserSessionShape, withSessionUser } from "./session.ts";
|
|
11
14
|
|
|
@@ -14,16 +17,17 @@ export function permissionLayer<TMiddleware, TSession extends UserSessionShape>(
|
|
|
14
17
|
policy: SessionPolicy<TSession>,
|
|
15
18
|
) {
|
|
16
19
|
const permission = middleware.permission;
|
|
17
|
-
const guard: RpcMiddleware.RpcMiddleware<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const guard: RpcMiddleware.RpcMiddleware<
|
|
21
|
+
Identity | SessionUser,
|
|
22
|
+
Unauthorized | AuthUnavailable | Forbidden | MissingReadPolicy | QueryReadDenied,
|
|
23
|
+
never
|
|
24
|
+
> = (effect, { headers, rpc, payload }) => {
|
|
21
25
|
const lookup = Effect.tap(resolveSession(policy, headers, rpc._tag), (session) =>
|
|
22
26
|
Option.isSome(session) && !hasPermission(session.value.user, permission)
|
|
23
27
|
? Effect.fail(new Forbidden({ message: `Permission required: ${permission.name} (${permission.strength})` }))
|
|
24
28
|
: Effect.void,
|
|
25
29
|
);
|
|
26
|
-
return
|
|
30
|
+
return Effect.flatMap(Identity, (viewer) => authorizeReads(commandChanges(effect, rpc, payload), { payload, rpc }, Option.some(viewer))).pipe(
|
|
27
31
|
(invocation) => Effect.flatMap(Identity, ({ id }) => invocation.pipe(revalidateStream(policy, rpc, id, lookup))),
|
|
28
32
|
(invocation) => Effect.andThen(requirePermission(permission), invocation),
|
|
29
33
|
withSessionUser(policy, headers, rpc._tag),
|
|
@@ -34,7 +38,11 @@ export function permissionLayer<TMiddleware, TSession extends UserSessionShape>(
|
|
|
34
38
|
function rejection(error: Unauthorized | Forbidden | AuthUnavailable) {
|
|
35
39
|
return HttpServerResponse.empty({
|
|
36
40
|
headers: { "cache-control": "private, no-store" },
|
|
37
|
-
status: {
|
|
41
|
+
status: {
|
|
42
|
+
AuthUnavailable: HttpStatus.fromLiteral("ServiceUnavailable"),
|
|
43
|
+
Forbidden: HttpStatus.fromLiteral("Forbidden"),
|
|
44
|
+
Unauthorized: HttpStatus.fromLiteral("Unauthorized"),
|
|
45
|
+
}[error._tag],
|
|
38
46
|
});
|
|
39
47
|
}
|
|
40
48
|
|
package/src/server/session.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Duration, Effect, Exit, Layer, Option } from "effect";
|
|
|
2
2
|
import type { Headers } from "effect/http";
|
|
3
3
|
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
4
4
|
import type { RoleSet } from "@shivaedev/access/roleSet.ts";
|
|
5
|
+
import { authorizeReads } from "@shivaedev/contract/readPolicy.ts";
|
|
5
6
|
import { type AuthUnavailable, Forbidden, Unauthorized } from "@shivaedev/errors/taxonomy.ts";
|
|
6
7
|
import {
|
|
7
8
|
Identity,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
type SessionUserWithProviderAccountsValue,
|
|
13
14
|
} from "#identity.ts";
|
|
14
15
|
import { Authenticated, AuthenticatedUser, AuthenticatedUserWithProviderAccounts, MaybeAuthenticated } from "#middleware.ts";
|
|
15
|
-
import {
|
|
16
|
+
import { commandChanges } from "#server/commandChanges.ts";
|
|
16
17
|
import { type OriginPolicy, originRequest } from "./origin.ts";
|
|
17
18
|
import { resolveRole } from "./resolveRole.ts";
|
|
18
19
|
import { revalidateStream } from "./revalidateStream.ts";
|
|
@@ -64,7 +65,7 @@ export function authenticatedLayer<TSession extends SessionShape>(policy: Sessio
|
|
|
64
65
|
return yield* new Unauthorized({ message: "Authentication required" });
|
|
65
66
|
}
|
|
66
67
|
const id = yield* identified(session.value.user.id);
|
|
67
|
-
return yield*
|
|
68
|
+
return yield* authorizeReads(commandChanges(effect, rpc, payload), { payload, rpc }, Option.some({ id })).pipe(
|
|
68
69
|
revalidateStream(policy, rpc, id, resolveSession(policy, headers, rpc._tag)),
|
|
69
70
|
Effect.provideService(Identity, { id }),
|
|
70
71
|
Effect.annotateLogs({ userId: id }),
|
|
@@ -73,14 +74,14 @@ export function authenticatedLayer<TSession extends SessionShape>(policy: Sessio
|
|
|
73
74
|
);
|
|
74
75
|
}
|
|
75
76
|
export function maybeAuthenticatedLayer<TSession extends SessionShape>(policy: SessionPolicy<TSession>): Layer.Layer<MaybeAuthenticated> {
|
|
76
|
-
return Layer.succeed(MaybeAuthenticated, (effect, { headers, rpc }) =>
|
|
77
|
+
return Layer.succeed(MaybeAuthenticated, (effect, { headers, rpc, payload }) =>
|
|
77
78
|
Effect.gen(function* () {
|
|
78
79
|
const session = yield* resolveSession(policy, headers, rpc._tag);
|
|
79
80
|
if (Option.isNone(session)) {
|
|
80
|
-
return yield* Effect.provideService(effect, OptionalIdentity, Option.none());
|
|
81
|
+
return yield* Effect.provideService(authorizeReads(effect, { payload, rpc }, Option.none()), OptionalIdentity, Option.none());
|
|
81
82
|
}
|
|
82
83
|
const id = yield* identified(session.value.user.id);
|
|
83
|
-
return yield* effect.pipe(
|
|
84
|
+
return yield* authorizeReads(effect, { payload, rpc }, Option.some({ id })).pipe(
|
|
84
85
|
revalidateStream(policy, rpc, id, resolveSession(policy, headers, rpc._tag)),
|
|
85
86
|
Effect.provideService(OptionalIdentity, Option.some({ id })),
|
|
86
87
|
Effect.annotateLogs({ userId: id }),
|
|
@@ -109,7 +110,7 @@ export function withSessionUser<TSession extends UserSessionShape>(policy: Sessi
|
|
|
109
110
|
}
|
|
110
111
|
export function authenticatedUserLayer<TSession extends UserSessionShape>(policy: SessionPolicy<TSession>): Layer.Layer<AuthenticatedUser> {
|
|
111
112
|
return Layer.succeed(AuthenticatedUser, (effect, { headers, rpc, payload }) =>
|
|
112
|
-
|
|
113
|
+
Effect.flatMap(Identity, (viewer) => authorizeReads(commandChanges(effect, rpc, payload), { payload, rpc }, Option.some(viewer))).pipe(
|
|
113
114
|
(invocation) =>
|
|
114
115
|
Effect.flatMap(Identity, ({ id }) => invocation.pipe(revalidateStream(policy, rpc, id, resolveSession(policy, headers, rpc._tag)))),
|
|
115
116
|
withSessionUser(policy, headers, rpc._tag),
|
|
@@ -136,7 +137,7 @@ export function authenticatedUserWithProviderAccountsLayer<TSession extends Prov
|
|
|
136
137
|
);
|
|
137
138
|
const user = sessionProfile(session.value.user);
|
|
138
139
|
yield* identified(id);
|
|
139
|
-
return yield*
|
|
140
|
+
return yield* authorizeReads(commandChanges(effect, rpc, payload), { payload, rpc }, Option.some({ id })).pipe(
|
|
140
141
|
revalidateStream(policy, rpc, id, resolveSession(policy, headers, rpc._tag)),
|
|
141
142
|
Effect.provideService(Identity, { id }),
|
|
142
143
|
Effect.provideService(SessionUser, user),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Context, Effect, Option } from "effect";
|
|
2
2
|
import type { Rpc } from "effect/rpc";
|
|
3
|
-
import type { KeyContext } from "@shivaedev/
|
|
4
|
-
import {
|
|
3
|
+
import type { KeyContext } from "@shivaedev/changes/keys.ts";
|
|
4
|
+
import { querySubscription } from "@shivaedev/contract/querySubscription.ts";
|
|
5
|
+
import { CurrentViewer } from "@shivaedev/contract/readPolicy.ts";
|
|
6
|
+
import { ChangesQueries, KeyScope, type QueryReference } from "@shivaedev/contract/serverMetadata.ts";
|
|
5
7
|
import { Identity } from "#identity.ts";
|
|
6
8
|
|
|
7
9
|
export function sessionKeyContext(rpc: Pick<Rpc.AnyWithProps, "annotations">): Effect.Effect<KeyContext | undefined> {
|
|
@@ -17,9 +19,11 @@ export function sessionKeyContext(rpc: Pick<Rpc.AnyWithProps, "annotations">): E
|
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
export const resolveChanges = Effect.fnUntraced(function* (rpc: Pick<Rpc.AnyWithProps, "annotations">, references: readonly QueryReference[]) {
|
|
23
|
+
const identity = yield* Effect.serviceOption(Identity);
|
|
24
|
+
const current = yield* CurrentViewer;
|
|
25
|
+
const viewer = Option.orElse(identity, () => current);
|
|
26
|
+
const queries = yield* Context.getUnsafe(rpc.annotations, ChangesQueries).resolve(references);
|
|
27
|
+
const subscription = yield* querySubscription(queries, viewer);
|
|
28
|
+
return { context: yield* sessionKeyContext(rpc), subscription };
|
|
29
|
+
});
|