@shivaedev/platform 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/better-auth/adapter.d.ts +1 -2
- package/dist/better-auth/adapter.d.ts.map +1 -1
- package/dist/better-auth/adapter.js +2 -2
- package/dist/better-auth/adapter.js.map +1 -1
- package/dist/better-auth/relation.d.ts +15 -15
- package/dist/better-auth/relation.d.ts.map +1 -1
- package/dist/better-auth/relation.js +4 -2
- package/dist/better-auth/relation.js.map +1 -1
- package/dist/better-auth/row-adapter.d.ts.map +1 -1
- package/dist/better-auth/row-adapter.js +16 -15
- package/dist/better-auth/row-adapter.js.map +1 -1
- package/dist/better-auth/where.d.ts +16 -16
- package/dist/better-auth/where.d.ts.map +1 -1
- package/dist/better-auth/where.js +16 -8
- package/dist/better-auth/where.js.map +1 -1
- package/dist/errors/taxonomy.d.ts +2 -2
- package/dist/errors/taxonomy.js +1 -1
- package/dist/errors/taxonomy.js.map +1 -1
- package/dist/node-http/subscription-signal.d.ts +1 -11
- package/dist/node-http/subscription-signal.d.ts.map +1 -1
- package/dist/node-http/subscription-signal.js +11 -10
- package/dist/node-http/subscription-signal.js.map +1 -1
- package/dist/rpc-server/origin.d.ts +2 -2
- package/dist/rpc-server/origin.d.ts.map +1 -1
- package/dist/rpc-server/origin.js +1 -1
- package/dist/rpc-server/origin.js.map +1 -1
- package/dist/rpc-server/redact-cause.js +2 -2
- package/dist/rpc-server/redact-cause.js.map +1 -1
- package/dist/rpc-server/redact.d.ts.map +1 -1
- package/dist/rpc-server/redact.js +16 -8
- package/dist/rpc-server/redact.js.map +1 -1
- package/dist/rpc-server/sensitive.js +11 -11
- package/dist/rpc-server/sensitive.js.map +1 -1
- package/dist/rpc-server/session.d.ts +1 -1
- package/dist/rpc-server/session.d.ts.map +1 -1
- package/dist/rpc-server/session.js +6 -3
- package/dist/rpc-server/session.js.map +1 -1
- package/dist/rpc-server/tracing.d.ts.map +1 -1
- package/dist/rpc-server/tracing.js +5 -3
- package/dist/rpc-server/tracing.js.map +1 -1
- package/dist/runtime/make.d.ts.map +1 -1
- package/dist/runtime/make.js +6 -3
- package/dist/runtime/make.js.map +1 -1
- package/dist/runtime/types.d.ts +0 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/testing/types.d.ts +1 -1
- package/dist/testing/types.d.ts.map +1 -1
- package/dist/testing/vitest.d.ts +1 -1
- package/dist/testing/vitest.d.ts.map +1 -1
- package/dist/testing/vitest.js +2 -2
- package/dist/testing/vitest.js.map +1 -1
- package/package.json +45 -46
- package/src/better-auth/adapter.ts +3 -4
- package/src/better-auth/relation.ts +22 -18
- package/src/better-auth/row-adapter.ts +18 -16
- package/src/better-auth/where.ts +41 -26
- package/src/errors/taxonomy.ts +1 -1
- package/src/node-http/subscription-signal.ts +17 -16
- package/src/rpc-server/origin.ts +3 -3
- package/src/rpc-server/redact-cause.ts +2 -2
- package/src/rpc-server/redact.ts +26 -10
- package/src/rpc-server/sensitive.ts +12 -12
- package/src/rpc-server/session.ts +10 -4
- package/src/rpc-server/tracing.ts +7 -3
- package/src/runtime/make.ts +9 -3
- package/src/runtime/types.ts +0 -1
- package/src/testing/types.ts +1 -1
- package/src/testing/vitest.ts +9 -6
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { all } from "@shivaedev/effect-prisma";
|
|
2
1
|
import type { Effect, Option } from "effect";
|
|
2
|
+
import { all } from "@shivaedev/effect-prisma";
|
|
3
3
|
import { type CleanedWhere, type DynamicField, type Expression, whereExpression } from "./where.ts";
|
|
4
4
|
|
|
5
|
-
export interface DynamicRelation<Requirements> extends Effect.Effect<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
export interface DynamicRelation<Requirements> extends Effect.Effect<readonly Record<string, unknown>[], unknown, Requirements> {
|
|
6
|
+
count: () => Effect.Effect<number, unknown, Requirements>;
|
|
7
|
+
create: (data: Record<string, unknown>) => Effect.Effect<Record<string, unknown>, unknown, Requirements>;
|
|
8
|
+
delete: () => Effect.Effect<Record<string, unknown> | null, unknown, Requirements>;
|
|
9
|
+
deleteAll: () => Effect.Effect<readonly Record<string, unknown>[], unknown, Requirements>;
|
|
10
|
+
first: () => Effect.Effect<Option.Option<Record<string, unknown>>, unknown, Requirements>;
|
|
11
|
+
orderBy: (ordering: (fields: Record<string, DynamicField>) => unknown) => DynamicRelation<Requirements>;
|
|
12
|
+
select: (...fields: readonly string[]) => DynamicRelation<Requirements>;
|
|
13
|
+
skip: (count: number) => DynamicRelation<Requirements>;
|
|
14
|
+
take: (count: number) => DynamicRelation<Requirements>;
|
|
15
|
+
update: (data: Record<string, unknown>) => Effect.Effect<Record<string, unknown> | null, unknown, Requirements>;
|
|
16
|
+
updateAll: (data: Record<string, unknown>) => Effect.Effect<readonly Record<string, unknown>[], unknown, Requirements>;
|
|
17
|
+
where: (predicate: (fields: Record<string, DynamicField>) => Expression) => DynamicRelation<Requirements>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const refineRelation = <Requirements>(
|
|
21
21
|
relation: DynamicRelation<Requirements>,
|
|
22
22
|
options: {
|
|
23
|
-
readonly where?:
|
|
24
|
-
readonly select?:
|
|
23
|
+
readonly where?: readonly CleanedWhere[] | undefined;
|
|
24
|
+
readonly select?: readonly string[] | undefined;
|
|
25
25
|
readonly limit?: number | undefined;
|
|
26
26
|
readonly offset?: number | undefined;
|
|
27
27
|
readonly sortBy?:
|
|
@@ -48,8 +48,12 @@ export const refineRelation = <Requirements>(
|
|
|
48
48
|
return options.sortBy?.direction === "desc" ? field.desc() : field.asc();
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
-
if (options.offset !== undefined)
|
|
52
|
-
|
|
51
|
+
if (options.offset !== undefined) {
|
|
52
|
+
refined = refined.skip(options.offset);
|
|
53
|
+
}
|
|
54
|
+
if (options.limit !== undefined) {
|
|
55
|
+
refined = refined.take(options.limit);
|
|
56
|
+
}
|
|
53
57
|
if (options.select !== undefined && options.select.length > 0) {
|
|
54
58
|
refined = refined.select(...options.select);
|
|
55
59
|
}
|
|
@@ -12,8 +12,8 @@ type Refinement = Parameters<typeof refineRelation>[1];
|
|
|
12
12
|
|
|
13
13
|
interface RowMethods {
|
|
14
14
|
readonly create: (input: { readonly data: Row; readonly model: string; readonly select?: string[] | undefined }) => Promise<Row>;
|
|
15
|
+
readonly findMany: (input: FindManyInput) => Promise<readonly Row[]>;
|
|
15
16
|
readonly findOne: (input: FindOneInput) => Promise<Row | null>;
|
|
16
|
-
readonly findMany: (input: FindManyInput) => Promise<ReadonlyArray<Row>>;
|
|
17
17
|
readonly update: (input: { readonly model: string; readonly update: Row; readonly where: CleanedWhere[] }) => Promise<Row | null>;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -41,38 +41,40 @@ export const makeRowAdapter =
|
|
|
41
41
|
(query: RelationQuery, usePlural: boolean): AdapterFactoryCustomizeAdapterCreator =>
|
|
42
42
|
({ debugLog, getFieldAttributes }) => {
|
|
43
43
|
const isUnique = (model: string, condition: CleanedWhere): boolean =>
|
|
44
|
-
condition.connector !== "OR"
|
|
45
|
-
condition.operator === "eq"
|
|
46
|
-
condition.mode !== "insensitive"
|
|
47
|
-
(condition.field === "id" || getFieldAttributes({ field: condition.field, model }).unique === true);
|
|
44
|
+
condition.connector !== "OR"
|
|
45
|
+
&& condition.operator === "eq"
|
|
46
|
+
&& condition.mode !== "insensitive"
|
|
47
|
+
&& (condition.field === "id" || getFieldAttributes({ field: condition.field, model }).unique === true);
|
|
48
48
|
|
|
49
49
|
return rowTyped({
|
|
50
|
+
count: ({ model, where }) => query(model, { where }, (relation) => relation.count()),
|
|
50
51
|
create: ({ data, model, select }) => {
|
|
51
52
|
debugLog("create", { model });
|
|
52
53
|
return query(model, { select }, (relation) => relation.create(data));
|
|
53
54
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return query(model, {
|
|
55
|
+
delete: ({ model, where }) => {
|
|
56
|
+
const byId = where.some(namesId);
|
|
57
|
+
return query(model, { where }, (relation) => (byId ? Effect.asVoid(relation.delete()) : Effect.asVoid(relation.deleteAll())));
|
|
57
58
|
},
|
|
59
|
+
deleteMany: ({ model, where }) => query(model, { where }, (relation) => Effect.map(relation.deleteAll(), (rows) => rows.length)),
|
|
58
60
|
findMany: ({ join, limit, model, offset, select, sortBy, where }) => {
|
|
59
61
|
rejectJoin(join);
|
|
60
62
|
return query(model, { limit, offset, select, sortBy, where }, (relation) => relation);
|
|
61
63
|
},
|
|
64
|
+
findOne: ({ join, model, select, where }) => {
|
|
65
|
+
rejectJoin(join);
|
|
66
|
+
return query(model, { select, where }, (relation) => Effect.map(relation.first(), Option.getOrNull));
|
|
67
|
+
},
|
|
68
|
+
options: { usePlural },
|
|
62
69
|
update: ({ model, update, where }) => {
|
|
63
|
-
if (where.length === 0)
|
|
70
|
+
if (where.length === 0) {
|
|
71
|
+
return Promise.resolve(null);
|
|
72
|
+
}
|
|
64
73
|
const unique = where.some((condition) => isUnique(model, condition));
|
|
65
74
|
return query(model, { where }, (relation) =>
|
|
66
75
|
unique ? relation.update(update) : Effect.map(relation.updateAll(update), (rows) => rows[0] ?? null),
|
|
67
76
|
);
|
|
68
77
|
},
|
|
69
78
|
updateMany: ({ model, update, where }) => query(model, { where }, (relation) => Effect.map(relation.updateAll(update), (rows) => rows.length)),
|
|
70
|
-
delete: ({ model, where }) => {
|
|
71
|
-
const byId = where.some(namesId);
|
|
72
|
-
return query(model, { where }, (relation) => (byId ? Effect.asVoid(relation.delete()) : Effect.asVoid(relation.deleteAll())));
|
|
73
|
-
},
|
|
74
|
-
deleteMany: ({ model, where }) => query(model, { where }, (relation) => Effect.map(relation.deleteAll(), (rows) => rows.length)),
|
|
75
|
-
count: ({ model, where }) => query(model, { where }, (relation) => relation.count()),
|
|
76
|
-
options: { usePlural },
|
|
77
79
|
});
|
|
78
80
|
};
|
package/src/better-auth/where.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { all, and, not, or } from "@shivaedev/effect-prisma";
|
|
2
1
|
import type { Where } from "better-auth";
|
|
2
|
+
import { all, and, not, or } from "@shivaedev/effect-prisma";
|
|
3
3
|
|
|
4
4
|
export type CleanedWhere = Required<Where>;
|
|
5
5
|
export type Expression = Parameters<typeof and>[number];
|
|
6
6
|
|
|
7
7
|
export interface DynamicField {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
in(value:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
asc: () => unknown;
|
|
9
|
+
desc: () => unknown;
|
|
10
|
+
eq: (value: unknown) => Expression;
|
|
11
|
+
gt: (value: unknown) => Expression;
|
|
12
|
+
gte: (value: unknown) => Expression;
|
|
13
|
+
ilike?: (value: string) => Expression;
|
|
14
|
+
in: (value: readonly unknown[]) => Expression;
|
|
15
|
+
isNotNull: () => Expression;
|
|
16
|
+
isNull: () => Expression;
|
|
17
|
+
like: (value: string) => Expression;
|
|
18
|
+
lt: (value: unknown) => Expression;
|
|
19
|
+
lte: (value: unknown) => Expression;
|
|
20
|
+
neq: (value: unknown) => Expression;
|
|
21
|
+
notIn: (value: readonly unknown[]) => Expression;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const escapeLike = (value: string): string => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
25
25
|
|
|
26
|
-
const nonNullValues = (value: unknown):
|
|
26
|
+
const nonNullValues = (value: unknown): readonly unknown[] => (Array.isArray(value) ? value : [value]).filter((item) => item !== null);
|
|
27
27
|
|
|
28
|
-
const insensitiveStrings = (field: DynamicField, where: CleanedWhere, values:
|
|
28
|
+
const insensitiveStrings = (field: DynamicField, where: CleanedWhere, values: readonly unknown[]) => {
|
|
29
29
|
const ilike = field.ilike;
|
|
30
30
|
if (where.mode !== "insensitive" || ilike === undefined || !values.every((item) => typeof item === "string")) {
|
|
31
31
|
return undefined;
|
|
@@ -52,7 +52,9 @@ const exclusion = (field: DynamicField, where: CleanedWhere): Expression | undef
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
const likePattern = (operator: CleanedWhere["operator"], escaped: string): string => {
|
|
55
|
-
if (operator === "contains")
|
|
55
|
+
if (operator === "contains") {
|
|
56
|
+
return `%${escaped}%`;
|
|
57
|
+
}
|
|
56
58
|
return operator === "starts_with" ? `${escaped}%` : `%${escaped}`;
|
|
57
59
|
};
|
|
58
60
|
|
|
@@ -66,15 +68,22 @@ const pattern = (field: DynamicField, where: CleanedWhere): Expression => {
|
|
|
66
68
|
|
|
67
69
|
const insensitiveEquality = (field: DynamicField, where: CleanedWhere): Expression | undefined => {
|
|
68
70
|
const value = where.value;
|
|
69
|
-
|
|
70
|
-
if (where.
|
|
71
|
-
|
|
71
|
+
const operator = where.operator ?? "eq";
|
|
72
|
+
if (where.mode !== "insensitive" || typeof value !== "string" || field.ilike === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (operator === "eq") {
|
|
76
|
+
return field.ilike(escapeLike(value));
|
|
77
|
+
}
|
|
78
|
+
if (operator === "ne") {
|
|
79
|
+
return not(field.ilike(escapeLike(value)));
|
|
80
|
+
}
|
|
72
81
|
return undefined;
|
|
73
82
|
};
|
|
74
83
|
|
|
75
84
|
const comparison = (field: DynamicField, where: CleanedWhere): Expression | undefined => {
|
|
76
85
|
const value = where.value;
|
|
77
|
-
switch (where.operator) {
|
|
86
|
+
switch (where.operator ?? "eq") {
|
|
78
87
|
case "eq":
|
|
79
88
|
return value === null ? field.isNull() : field.eq(value);
|
|
80
89
|
case "ne":
|
|
@@ -98,7 +107,7 @@ const comparison = (field: DynamicField, where: CleanedWhere): Expression | unde
|
|
|
98
107
|
}
|
|
99
108
|
};
|
|
100
109
|
|
|
101
|
-
export const whereExpression = (fields: Record<string, DynamicField>, where:
|
|
110
|
+
export const whereExpression = (fields: Record<string, DynamicField>, where: readonly CleanedWhere[]): Expression | undefined => {
|
|
102
111
|
const conjunctions: Expression[] = [];
|
|
103
112
|
const disjunctions: Expression[] = [];
|
|
104
113
|
|
|
@@ -108,11 +117,17 @@ export const whereExpression = (fields: Record<string, DynamicField>, where: Rea
|
|
|
108
117
|
throw new TypeError(`Unknown database field: ${condition.field}`);
|
|
109
118
|
}
|
|
110
119
|
const expression = insensitiveEquality(field, condition) ?? comparison(field, condition);
|
|
111
|
-
if (expression === undefined)
|
|
120
|
+
if (expression === undefined) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
112
123
|
(condition.connector === "OR" ? disjunctions : conjunctions).push(expression);
|
|
113
124
|
}
|
|
114
125
|
|
|
115
|
-
if (disjunctions.length > 0)
|
|
116
|
-
|
|
126
|
+
if (disjunctions.length > 0) {
|
|
127
|
+
conjunctions.push(or(...disjunctions));
|
|
128
|
+
}
|
|
129
|
+
if (conjunctions.length === 0) {
|
|
130
|
+
return all();
|
|
131
|
+
}
|
|
117
132
|
return conjunctions.length === 1 ? conjunctions[0] : and(...conjunctions);
|
|
118
133
|
};
|
package/src/errors/taxonomy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
3
|
const described = { message: Schema.String };
|
|
4
|
-
const perField = {
|
|
4
|
+
const perField = { field: Schema.optionalKey(Schema.String), message: Schema.String };
|
|
5
5
|
|
|
6
6
|
export class NotFound extends Schema.TaggedError<NotFound>()("NotFound", described) {}
|
|
7
7
|
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import type { IncomingMessage } from "node:http";
|
|
2
2
|
|
|
3
3
|
export interface NodeSubscriptionSignalOptions {
|
|
4
|
-
/** A Web request, including its signal and an optional srvx node runtime. */
|
|
5
|
-
readonly request?: Request | undefined;
|
|
6
|
-
/** The node request when the transport exposes it directly. */
|
|
7
4
|
readonly nodeRequest?: IncomingMessage | undefined;
|
|
8
|
-
|
|
5
|
+
readonly request?: Request | undefined;
|
|
9
6
|
readonly signals?: Iterable<AbortSignal | undefined>;
|
|
10
7
|
}
|
|
11
8
|
|
|
12
9
|
export interface NodeSubscriptionSignal {
|
|
13
|
-
/** Detach node listeners when the subscription ends. */
|
|
14
10
|
readonly dispose: () => void;
|
|
15
|
-
/** Aborts when any supplied signal or the underlying connection closes. */
|
|
16
11
|
readonly signal: AbortSignal;
|
|
17
12
|
}
|
|
18
13
|
|
|
@@ -26,17 +21,17 @@ const nodeRequestFrom = (request: Request | undefined): IncomingMessage | undefi
|
|
|
26
21
|
return isIncomingMessage(req) ? req : undefined;
|
|
27
22
|
};
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
* Combine Web/procedure abort signals with node request and socket close events.
|
|
31
|
-
* The node events cover long-lived responses under Bun's node:http compatibility
|
|
32
|
-
* layer, where a Web request signal alone may not report an abandoned socket.
|
|
33
|
-
*/
|
|
24
|
+
// Under Bun's node:http compatibility layer a Web request signal may miss an abandoned socket, so node close events cover long-lived responses.
|
|
34
25
|
export const nodeSubscriptionSignal = (options: NodeSubscriptionSignalOptions): NodeSubscriptionSignal => {
|
|
35
26
|
const controller = new AbortController();
|
|
36
27
|
const signals: AbortSignal[] = [controller.signal];
|
|
37
|
-
if (options.request !== undefined)
|
|
28
|
+
if (options.request !== undefined) {
|
|
29
|
+
signals.push(options.request.signal);
|
|
30
|
+
}
|
|
38
31
|
for (const signal of options.signals ?? []) {
|
|
39
|
-
if (signal !== undefined)
|
|
32
|
+
if (signal !== undefined) {
|
|
33
|
+
signals.push(signal);
|
|
34
|
+
}
|
|
40
35
|
}
|
|
41
36
|
|
|
42
37
|
const nodeRequest = options.nodeRequest ?? nodeRequestFrom(options.request);
|
|
@@ -46,17 +41,23 @@ export const nodeSubscriptionSignal = (options: NodeSubscriptionSignalOptions):
|
|
|
46
41
|
|
|
47
42
|
const socket = nodeRequest.socket;
|
|
48
43
|
const onRequestClose = () => {
|
|
49
|
-
if (!nodeRequest.complete)
|
|
44
|
+
if (!nodeRequest.complete) {
|
|
45
|
+
controller.abort();
|
|
46
|
+
}
|
|
50
47
|
};
|
|
51
48
|
const onSocketClose = () => controller.abort();
|
|
52
49
|
nodeRequest.once("close", onRequestClose);
|
|
53
50
|
socket.once("close", onSocketClose);
|
|
54
|
-
if (nodeRequest.destroyed || socket.destroyed)
|
|
51
|
+
if (nodeRequest.destroyed || socket.destroyed) {
|
|
52
|
+
controller.abort();
|
|
53
|
+
}
|
|
55
54
|
|
|
56
55
|
let disposed = false;
|
|
57
56
|
return {
|
|
58
57
|
dispose: () => {
|
|
59
|
-
if (disposed)
|
|
58
|
+
if (disposed) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
60
61
|
disposed = true;
|
|
61
62
|
nodeRequest.removeListener("close", onRequestClose);
|
|
62
63
|
socket.removeListener("close", onSocketClose);
|
package/src/rpc-server/origin.ts
CHANGED
|
@@ -2,15 +2,15 @@ import { Option } from "effect";
|
|
|
2
2
|
import { Headers } from "effect/unstable/http";
|
|
3
3
|
|
|
4
4
|
export interface OriginRequest {
|
|
5
|
+
readonly headers: Headers.Headers;
|
|
5
6
|
readonly origin: Option.Option<string>;
|
|
6
7
|
readonly rpc: string;
|
|
7
|
-
readonly headers: Headers.Headers;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export type OriginPolicy = (request: OriginRequest) => boolean;
|
|
11
11
|
|
|
12
12
|
export interface TrustedOriginsOptions {
|
|
13
|
-
readonly allow:
|
|
13
|
+
readonly allow: readonly string[];
|
|
14
14
|
readonly missing: "allow" | "reject";
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -23,7 +23,7 @@ export const trustedOrigins =
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
export const originRequest = (headers: Headers.Headers, rpc: string): OriginRequest => ({
|
|
26
|
+
headers,
|
|
26
27
|
origin: Headers.get(headers, "origin"),
|
|
27
28
|
rpc,
|
|
28
|
-
headers,
|
|
29
29
|
});
|
|
@@ -11,11 +11,11 @@ const reporterHints = (error: Error, sensitive: SensitiveKey) => ({
|
|
|
11
11
|
const redactError = (error: Error, sensitive: SensitiveKey): Error => {
|
|
12
12
|
const options = error.cause === undefined ? undefined : { cause: redact(error.cause, sensitive) };
|
|
13
13
|
const copy = new Error(redactText(error.message, sensitive), options);
|
|
14
|
-
Object.defineProperty(copy, "name", { value: error.name, writable: true
|
|
14
|
+
Object.defineProperty(copy, "name", { configurable: true, value: error.name, writable: true });
|
|
15
15
|
Object.defineProperty(copy, "stack", {
|
|
16
|
+
configurable: true,
|
|
16
17
|
value: error.stack === undefined ? undefined : redactText(error.stack, sensitive),
|
|
17
18
|
writable: true,
|
|
18
|
-
configurable: true,
|
|
19
19
|
});
|
|
20
20
|
return Object.assign(copy, redact(Object.fromEntries(Object.entries(error)), sensitive), reporterHints(error, sensitive));
|
|
21
21
|
};
|
package/src/rpc-server/redact.ts
CHANGED
|
@@ -5,15 +5,21 @@ const MAX_DEPTH = 8;
|
|
|
5
5
|
const MAX_ENTRIES = 50;
|
|
6
6
|
|
|
7
7
|
const binary = (value: object): string | undefined => {
|
|
8
|
-
if (ArrayBuffer.isView(value))
|
|
9
|
-
|
|
8
|
+
if (ArrayBuffer.isView(value)) {
|
|
9
|
+
return `<${value.constructor.name} ${value.byteLength} bytes>`;
|
|
10
|
+
}
|
|
11
|
+
if (value instanceof ArrayBuffer) {
|
|
12
|
+
return `<ArrayBuffer ${value.byteLength} bytes>`;
|
|
13
|
+
}
|
|
10
14
|
return undefined;
|
|
11
15
|
};
|
|
12
16
|
|
|
13
|
-
const entriesOf = (value: object):
|
|
17
|
+
const entriesOf = (value: object): [string, unknown][] => {
|
|
14
18
|
const entries = Object.entries(value);
|
|
15
|
-
if (!Predicate.isError(value))
|
|
16
|
-
|
|
19
|
+
if (!Predicate.isError(value)) {
|
|
20
|
+
return entries;
|
|
21
|
+
}
|
|
22
|
+
const shape: [string, unknown][] = [
|
|
17
23
|
["name", value.name],
|
|
18
24
|
["message", value.message],
|
|
19
25
|
["stack", value.stack],
|
|
@@ -22,12 +28,22 @@ const entriesOf = (value: object): Array<[string, unknown]> => {
|
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
const walk = (value: unknown, sensitive: SensitiveKey, depth: number): unknown => {
|
|
25
|
-
if (Redacted.isRedacted(value))
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
if (Redacted.isRedacted(value)) {
|
|
32
|
+
return REDACTED;
|
|
33
|
+
}
|
|
34
|
+
if (Predicate.isString(value)) {
|
|
35
|
+
return redactText(value, sensitive);
|
|
36
|
+
}
|
|
37
|
+
if (!Predicate.isObjectOrArray(value)) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
28
40
|
const summary = binary(value);
|
|
29
|
-
if (summary !== undefined)
|
|
30
|
-
|
|
41
|
+
if (summary !== undefined) {
|
|
42
|
+
return summary;
|
|
43
|
+
}
|
|
44
|
+
if (depth >= MAX_DEPTH) {
|
|
45
|
+
return "<truncated>";
|
|
46
|
+
}
|
|
31
47
|
if (Array.isArray(value)) {
|
|
32
48
|
const items = value.slice(0, MAX_ENTRIES).map((item) => walk(item, sensitive, depth + 1));
|
|
33
49
|
return value.length > MAX_ENTRIES ? [...items, `<${value.length - MAX_ENTRIES} more items>`] : items;
|
|
@@ -26,26 +26,26 @@ const SENSITIVE_NAMES = ["session", "pan"];
|
|
|
26
26
|
|
|
27
27
|
export type SensitiveKey = (key: string) => boolean;
|
|
28
28
|
|
|
29
|
-
const wordsOf = (key: string):
|
|
29
|
+
const wordsOf = (key: string): string[] =>
|
|
30
30
|
key
|
|
31
|
-
.replaceAll(/([a-z0-9])([A-Z])/
|
|
32
|
-
.replaceAll(/([A-Z])([A-Z][a-z])/
|
|
31
|
+
.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2")
|
|
32
|
+
.replaceAll(/([A-Z])([A-Z][a-z])/gu, "$1 $2")
|
|
33
33
|
.toLowerCase()
|
|
34
|
-
.split(/[\s_-]+/);
|
|
34
|
+
.split(/[\s_-]+/u);
|
|
35
35
|
|
|
36
36
|
export const isSensitiveKey: SensitiveKey = (key) => {
|
|
37
|
-
const normalized = key.toLowerCase().replaceAll(/[-_]/
|
|
37
|
+
const normalized = key.toLowerCase().replaceAll(/[-_]/gu, "");
|
|
38
38
|
return (
|
|
39
|
-
SENSITIVE_NAMES.includes(normalized)
|
|
40
|
-
SENSITIVE_PARTS.some((part) => normalized.includes(part))
|
|
41
|
-
wordsOf(key).some((word) => SENSITIVE_WORDS.includes(word))
|
|
39
|
+
SENSITIVE_NAMES.includes(normalized)
|
|
40
|
+
|| SENSITIVE_PARTS.some((part) => normalized.includes(part))
|
|
41
|
+
|| wordsOf(key).some((word) => SENSITIVE_WORDS.includes(word))
|
|
42
42
|
);
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
const AUTH_SCHEME = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]+=*/
|
|
46
|
-
const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/
|
|
47
|
-
const URL_PASSWORD = /(\/\/[^:/\s@]+:)[^@/\s]+@/
|
|
48
|
-
const ASSIGNMENT = /([A-Za-z][\w.-]*)(["']?\s*[=:]\s*["']?)([^\s"'&,;]+)/
|
|
45
|
+
const AUTH_SCHEME = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]+=*/giu;
|
|
46
|
+
const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/gu;
|
|
47
|
+
const URL_PASSWORD = /(\/\/[^:/\s@]+:)[^@/\s]+@/gu;
|
|
48
|
+
const ASSIGNMENT = /([A-Za-z][\w.-]*)(["']?\s*[=:]\s*["']?)([^\s"'&,;]+)/gu;
|
|
49
49
|
|
|
50
50
|
const scrub = (text: string, sensitive: SensitiveKey): string =>
|
|
51
51
|
text
|
|
@@ -15,8 +15,8 @@ export interface SessionProvider<Session extends SessionShape> {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface SessionPolicy<Session extends SessionShape> {
|
|
18
|
-
readonly provider: SessionProvider<Session>;
|
|
19
18
|
readonly origin: OriginPolicy;
|
|
19
|
+
readonly provider: SessionProvider<Session>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export const resolveSession: <Session extends SessionShape>(
|
|
@@ -27,7 +27,9 @@ export const resolveSession: <Session extends SessionShape>(
|
|
|
27
27
|
Session extends SessionShape,
|
|
28
28
|
>(policy: SessionPolicy<Session>, headers: Headers.Headers, rpc: string) {
|
|
29
29
|
const transport = yield* transportHeaders(headers);
|
|
30
|
-
if (!policy.origin(originRequest(transport, rpc)))
|
|
30
|
+
if (!policy.origin(originRequest(transport, rpc))) {
|
|
31
|
+
return yield* new Forbidden({ message: "Origin not allowed" });
|
|
32
|
+
}
|
|
31
33
|
return yield* policy.provider.get(transport);
|
|
32
34
|
});
|
|
33
35
|
|
|
@@ -37,7 +39,9 @@ export const authenticatedLayer = <Session extends SessionShape>(policy: Session
|
|
|
37
39
|
Layer.succeed(Authenticated, (effect, { headers, rpc }) =>
|
|
38
40
|
Effect.gen(function* () {
|
|
39
41
|
const session = yield* resolveSession(policy, headers, rpc._tag);
|
|
40
|
-
if (Option.isNone(session))
|
|
42
|
+
if (Option.isNone(session)) {
|
|
43
|
+
return yield* new Unauthorized({ message: "Authentication required" });
|
|
44
|
+
}
|
|
41
45
|
const id = yield* identified(session.value.user.id);
|
|
42
46
|
return yield* effect.pipe(Effect.provideService(Identity, { id }), Effect.annotateLogs({ userId: id }));
|
|
43
47
|
}),
|
|
@@ -47,7 +51,9 @@ export const maybeAuthenticatedLayer = <Session extends SessionShape>(policy: Se
|
|
|
47
51
|
Layer.succeed(MaybeAuthenticated, (effect, { headers, rpc }) =>
|
|
48
52
|
Effect.gen(function* () {
|
|
49
53
|
const session = yield* resolveSession(policy, headers, rpc._tag);
|
|
50
|
-
if (Option.isNone(session))
|
|
54
|
+
if (Option.isNone(session)) {
|
|
55
|
+
return yield* Effect.provideService(effect, OptionalIdentity, Option.none());
|
|
56
|
+
}
|
|
51
57
|
const id = yield* identified(session.value.user.id);
|
|
52
58
|
return yield* effect.pipe(Effect.provideService(OptionalIdentity, Option.some({ id })), Effect.annotateLogs({ userId: id }));
|
|
53
59
|
}),
|
|
@@ -11,7 +11,7 @@ export interface RequestTracingOptions {
|
|
|
11
11
|
readonly sensitive?: SensitiveKey;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const ACCEPTED_REQUEST_ID = /^[A-Za-z0-9._:-]{1,128}
|
|
14
|
+
const ACCEPTED_REQUEST_ID = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
15
15
|
|
|
16
16
|
const hex = (byte: number): string => byte.toString(16).padStart(2, "0");
|
|
17
17
|
|
|
@@ -33,9 +33,13 @@ const failureTag = (cause: Cause.Cause<unknown>): string =>
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
const logFailure = (cause: Cause.Cause<unknown>, payload: unknown, sensitive: SensitiveKey | undefined): Effect.Effect<void> => {
|
|
36
|
-
if (Cause.hasInterruptsOnly(cause))
|
|
36
|
+
if (Cause.hasInterruptsOnly(cause)) {
|
|
37
|
+
return Effect.void;
|
|
38
|
+
}
|
|
37
39
|
const annotations = { "rpc.payload": redact(payload, sensitive) };
|
|
38
|
-
if (!Cause.hasDies(cause))
|
|
40
|
+
if (!Cause.hasDies(cause)) {
|
|
41
|
+
return Effect.annotateLogs(Effect.logInfo("RPC failure"), { ...annotations, "rpc.failure": failureTag(cause) });
|
|
42
|
+
}
|
|
39
43
|
const defects = cause.reasons.filter(Cause.isDieReason).map((reason) => redact(reason.defect, sensitive));
|
|
40
44
|
return Effect.annotateLogs(Effect.logError("RPC defect"), { ...annotations, "rpc.defect": defects });
|
|
41
45
|
};
|
package/src/runtime/make.ts
CHANGED
|
@@ -40,7 +40,9 @@ const make = <Services, BuildError>(layer: Layer.Layer<Services, BuildError>): P
|
|
|
40
40
|
dispose: () => managed.dispose(),
|
|
41
41
|
runPromise: async (effect, options) => {
|
|
42
42
|
const exit = await runPromiseExit(effect, options);
|
|
43
|
-
if (Exit.isSuccess(exit))
|
|
43
|
+
if (Exit.isSuccess(exit)) {
|
|
44
|
+
return exit.value;
|
|
45
|
+
}
|
|
44
46
|
throw Cause.squash(exit.cause);
|
|
45
47
|
},
|
|
46
48
|
runPromiseExit,
|
|
@@ -57,7 +59,9 @@ export const makePlatformRuntime = <Services, BuildError>(
|
|
|
57
59
|
options: PlatformRuntimeOptions = {},
|
|
58
60
|
): PlatformRuntime<Services, BuildError> => {
|
|
59
61
|
const key = Effect.runSync(production) ? undefined : options.developmentCacheKey;
|
|
60
|
-
if (key === undefined)
|
|
62
|
+
if (key === undefined) {
|
|
63
|
+
return make(layer);
|
|
64
|
+
}
|
|
61
65
|
|
|
62
66
|
const cached = runtimeCache.get(key);
|
|
63
67
|
if (cached !== undefined) {
|
|
@@ -68,7 +72,9 @@ export const makePlatformRuntime = <Services, BuildError>(
|
|
|
68
72
|
const cachedRuntime: PlatformRuntime<Services, BuildError> = {
|
|
69
73
|
...runtime,
|
|
70
74
|
dispose: async () => {
|
|
71
|
-
if (runtimeCache.get(key) === cachedRuntime)
|
|
75
|
+
if (runtimeCache.get(key) === cachedRuntime) {
|
|
76
|
+
runtimeCache.delete(key);
|
|
77
|
+
}
|
|
72
78
|
await runtime.dispose();
|
|
73
79
|
},
|
|
74
80
|
};
|
package/src/runtime/types.ts
CHANGED
package/src/testing/types.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { TestContext, Vitest } from "@effect/vitest";
|
|
2
|
+
import type { Effect, Layer } from "effect";
|
|
2
3
|
import type { AnyDatabase, DatabaseService } from "@shivaedev/effect-prisma/testing";
|
|
3
4
|
import type { EffectTRPCAdapter } from "@shivaedev/effect-trpc";
|
|
4
5
|
import type { CallerOptions, CallerResult, EffectCallerFactory, TrpcHarnessTest, TrpcHarnessTester } from "@shivaedev/effect-trpc/testing";
|
|
5
|
-
import type { Effect, Layer } from "effect";
|
|
6
6
|
|
|
7
7
|
export type PlatformHarness<Database extends AnyDatabase, Options, Caller, Extension> = Readonly<{
|
|
8
8
|
db: DatabaseService<Database>;
|
package/src/testing/vitest.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import type { TestContext } from "@effect/vitest";
|
|
2
|
+
import { Effect, type Layer } from "effect";
|
|
2
3
|
import { type AnyDatabase, withTestTransaction } from "@shivaedev/effect-prisma/testing";
|
|
3
4
|
import type { EffectTRPCAdapter } from "@shivaedev/effect-trpc";
|
|
4
5
|
import { type CallerOptions, type CallerResult, type EffectCallerFactory, makeTrpcHarnessIt } from "@shivaedev/effect-trpc/testing";
|
|
5
|
-
import {
|
|
6
|
+
import type { Bivariant } from "@shivaedev/types";
|
|
6
7
|
import type { MakePlatformItOptions, PlatformHarness, PlatformIt } from "./types.ts";
|
|
7
8
|
|
|
8
9
|
interface LooseOptions {
|
|
9
10
|
readonly adapter: Pick<EffectTRPCAdapter<never>, "runWithServices">;
|
|
10
11
|
readonly createCaller: (options?: unknown) => object;
|
|
11
|
-
extend
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
extend?: Bivariant<
|
|
13
|
+
(
|
|
14
|
+
base: { readonly db: unknown; readonly trpc: EffectCallerFactory<unknown, object> },
|
|
15
|
+
context: TestContext,
|
|
16
|
+
) => Effect.Effect<object, unknown, unknown>
|
|
17
|
+
>;
|
|
15
18
|
readonly layer: Layer.Layer<unknown, unknown>;
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -30,9 +33,9 @@ function platformIt<
|
|
|
30
33
|
function platformIt(database: AnyDatabase, options: LooseOptions): unknown {
|
|
31
34
|
const trpcIt = makeTrpcHarnessIt({
|
|
32
35
|
adapter: options.adapter,
|
|
36
|
+
around: (effect) => withTestTransaction(database, effect),
|
|
33
37
|
createCaller: options.createCaller,
|
|
34
38
|
layer: options.layer,
|
|
35
|
-
around: (effect) => withTestTransaction(database, effect),
|
|
36
39
|
makeHarness: (trpc, context) =>
|
|
37
40
|
Effect.gen(function* () {
|
|
38
41
|
const services = yield* Effect.context<unknown>();
|