@shivaedev/platform 0.13.0 → 0.14.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.
- package/CHANGELOG.md +24 -0
- package/README.md +25 -4
- package/dist/server/appHost.d.ts +9 -16
- package/dist/server/appHost.d.ts.map +1 -1
- package/dist/server/appHost.js +5 -2
- package/dist/server/appHost.js.map +1 -1
- package/dist/server/appHostHttp.d.ts +3 -2
- package/dist/server/appHostHttp.d.ts.map +1 -1
- package/dist/server/appHostHttp.js +5 -1
- package/dist/server/appHostHttp.js.map +1 -1
- package/dist/server/databaseLayer.d.ts +9 -2
- package/dist/server/databaseLayer.d.ts.map +1 -1
- package/dist/server/databaseLayer.js +5 -2
- package/dist/server/databaseLayer.js.map +1 -1
- package/dist/server/healthRoute.d.ts +3 -0
- package/dist/server/healthRoute.d.ts.map +1 -0
- package/dist/server/healthRoute.js +18 -0
- package/dist/server/healthRoute.js.map +1 -0
- package/dist/server/otlpHttpClient.d.ts +4 -0
- package/dist/server/otlpHttpClient.d.ts.map +1 -0
- package/dist/server/otlpHttpClient.js +24 -0
- package/dist/server/otlpHttpClient.js.map +1 -0
- package/dist/server/otlpTracing.d.ts.map +1 -1
- package/dist/server/otlpTracing.js +6 -3
- package/dist/server/otlpTracing.js.map +1 -1
- package/dist/server/redactOtlpUrl.d.ts +2 -0
- package/dist/server/redactOtlpUrl.d.ts.map +1 -0
- package/dist/server/redactOtlpUrl.js +41 -0
- package/dist/server/redactOtlpUrl.js.map +1 -0
- package/dist/server/safeOtlpSerialization.d.ts +2 -1
- package/dist/server/safeOtlpSerialization.d.ts.map +1 -1
- package/dist/server/safeOtlpSerialization.js +25 -16
- package/dist/server/safeOtlpSerialization.js.map +1 -1
- package/package.json +17 -7
- package/src/server/appHost.ts +41 -29
- package/src/server/appHostHttp.ts +26 -2
- package/src/server/databaseLayer.ts +17 -7
- package/src/server/healthRoute.ts +27 -0
- package/src/server/otlpHttpClient.ts +35 -0
- package/src/server/otlpTracing.ts +15 -5
- package/src/server/redactOtlpUrl.ts +48 -0
- package/src/server/safeOtlpSerialization.ts +42 -29
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { REDACTED } from "#rpc-server/sensitive.ts";
|
|
2
|
+
function query(value) {
|
|
3
|
+
return value
|
|
4
|
+
.split("&")
|
|
5
|
+
.map((part) => (part === "" ? part : `${part.split("=", 1)[0]}=${REDACTED}`))
|
|
6
|
+
.join("&");
|
|
7
|
+
}
|
|
8
|
+
function url(value) {
|
|
9
|
+
const fragment = value.indexOf("#");
|
|
10
|
+
const target = fragment === -1 ? value : value.slice(0, fragment);
|
|
11
|
+
const separator = target.indexOf("?");
|
|
12
|
+
return ((separator === -1 ? target : `${target.slice(0, separator + 1)}${query(target.slice(separator + 1))}`).replaceAll(/(?<authority>(?:\b[a-z][a-z0-9+.-]*:)?\/\/)[^/?#]*@/giu, `$<authority>${REDACTED}@`) + (fragment === -1 ? "" : `#${REDACTED}`));
|
|
13
|
+
}
|
|
14
|
+
function structured(value) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(JSON.parse(value, (key, item) => (typeof item === "string" ? redactOtlpUrl(item, key) : item)));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function redactOtlpUrl(value, key = "") {
|
|
23
|
+
if (/(?:^|[._-])(?:fragment|userinfo|username|password)$/iu.test(key)) {
|
|
24
|
+
return REDACTED;
|
|
25
|
+
}
|
|
26
|
+
if (/(?:^|[._-])query$/iu.test(key)) {
|
|
27
|
+
return url(`?${value}`).slice(1);
|
|
28
|
+
}
|
|
29
|
+
if (/(?:^|[._-])(?:url|uri|target)(?:$|[._-])/iu.test(key) || /(?:^|[._-])(?:referer|referrer|location|origin)$/iu.test(key)) {
|
|
30
|
+
return url(value);
|
|
31
|
+
}
|
|
32
|
+
if (/^\s*[[{]/u.test(value)) {
|
|
33
|
+
const redacted = structured(value);
|
|
34
|
+
if (redacted !== undefined) {
|
|
35
|
+
return redacted;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Quotes and whitespace can belong to secret values, so free text has no safe closing delimiter.
|
|
39
|
+
return value.replaceAll(/(?:[a-z][a-z0-9+.-]*:\/\/|[/?#]).*/gisu, url);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=redactOtlpUrl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redactOtlpUrl.js","sourceRoot":"","sources":["../../src/server/redactOtlpUrl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,SAAS,KAAK,CAAC,KAAa;IAC3B,OAAO,KAAK;SACV,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC,CAAC;SAC5E,IAAI,CAAC,GAAG,CAAC,CAAC;AACb,CAAC;AAED,SAAS,GAAG,CAAC,KAAa;IACzB,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO,CACN,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,CAChH,wDAAwD,EACxD,eAAe,QAAQ,GAAG,CAC1B,GAAG,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC,CAC3C,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAChC,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAW,EAAE,IAAa,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxI,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,GAAG,GAAG,EAAE;IACpD,IAAI,uDAAuD,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,QAAQ,CAAC;IACjB,CAAC;IACD,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,OAAO,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,4CAA4C,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,oDAAoD,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9H,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,QAAQ,CAAC;QACjB,CAAC;IACF,CAAC;IACD,iGAAiG;IACjG,OAAO,KAAK,CAAC,UAAU,CAAC,wCAAwC,EAAE,GAAG,CAAC,CAAC;AACxE,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import { OtlpSerialization } from "effect/observability";
|
|
3
|
-
export declare
|
|
3
|
+
export declare function redactOtlpText(value: string, key?: string): string;
|
|
4
|
+
export declare const safeOtlpSerialization: Layer.Layer<OtlpSerialization.OtlpSerialization, never, OtlpSerialization.OtlpSerialization>;
|
|
4
5
|
//# sourceMappingURL=safeOtlpSerialization.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safeOtlpSerialization.d.ts","sourceRoot":"","sources":["../../src/server/safeOtlpSerialization.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"safeOtlpSerialization.d.ts","sourceRoot":"","sources":["../../src/server/safeOtlpSerialization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAOzD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAK,UAErD;AA2BD,eAAO,MAAM,qBAAqB,8FAwBjC,CAAC"}
|
|
@@ -1,31 +1,40 @@
|
|
|
1
|
-
import { Layer } from "effect";
|
|
2
|
-
import { HttpBody } from "effect/http";
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
3
2
|
import { OtlpSerialization } from "effect/observability";
|
|
3
|
+
import { redactOtlpUrl } from "#redactOtlpUrl.ts";
|
|
4
4
|
import { isSensitiveKey, REDACTED, redactText } from "#rpc-server/sensitive.ts";
|
|
5
5
|
function sensitive(key) {
|
|
6
6
|
return isSensitiveKey(key) || ["db.user", "db.user.name", "db.connection_string"].includes(key.toLowerCase());
|
|
7
7
|
}
|
|
8
|
-
function
|
|
9
|
-
return redactText(value,
|
|
8
|
+
export function redactOtlpText(value, key = "") {
|
|
9
|
+
return redactText(redactOtlpUrl(value, key), sensitive);
|
|
10
10
|
}
|
|
11
|
-
function attributeValue(value) {
|
|
11
|
+
function attributeValue(value, key) {
|
|
12
12
|
return {
|
|
13
13
|
...value,
|
|
14
|
-
...(typeof value.stringValue === "string" ? { stringValue:
|
|
15
|
-
...(value.arrayValue ? { arrayValue: { values: value.arrayValue.values.map(attributeValue) } } : {}),
|
|
14
|
+
...(typeof value.stringValue === "string" ? { stringValue: redactOtlpText(value.stringValue, key) } : {}),
|
|
15
|
+
...(value.arrayValue ? { arrayValue: { values: value.arrayValue.values.map((item) => attributeValue(item, key)) } } : {}),
|
|
16
16
|
...(value.kvlistValue ? { kvlistValue: { values: attributes(value.kvlistValue.values) } } : {}),
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
function attributes(values) {
|
|
20
20
|
return values.map(({ key, value }) => ({
|
|
21
21
|
key,
|
|
22
|
-
value: sensitive(key) ? { stringValue: REDACTED } : attributeValue(value),
|
|
22
|
+
value: sensitive(key) ? { stringValue: REDACTED } : attributeValue(value, key),
|
|
23
23
|
}));
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
function spanName(span) {
|
|
26
|
+
if (span.kind === 2) {
|
|
27
|
+
const method = span.attributes.find(({ key }) => key === "http.request.method")?.value.stringValue;
|
|
28
|
+
const route = span.attributes.find(({ key }) => key === "http.route")?.value.stringValue;
|
|
29
|
+
if (typeof method === "string") {
|
|
30
|
+
return typeof route === "string" ? `${method} ${route}` : method;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return span.name;
|
|
34
|
+
}
|
|
35
|
+
export const safeOtlpSerialization = Layer.effect(OtlpSerialization.OtlpSerialization, Effect.map(OtlpSerialization.OtlpSerialization, (serialization) => ({
|
|
36
|
+
...serialization,
|
|
37
|
+
traces: (data) => serialization.traces({
|
|
29
38
|
...data,
|
|
30
39
|
resourceSpans: data.resourceSpans.map((resource) => ({
|
|
31
40
|
...resource,
|
|
@@ -35,13 +44,13 @@ export const safeOtlpSerialization = Layer.succeed(OtlpSerialization.OtlpSeriali
|
|
|
35
44
|
spans: scope.spans.map((span) => ({
|
|
36
45
|
...span,
|
|
37
46
|
attributes: attributes(span.attributes),
|
|
38
|
-
events: span.events.map((event) => ({ ...event, attributes: attributes(event.attributes), name:
|
|
47
|
+
events: span.events.map((event) => ({ ...event, attributes: attributes(event.attributes), name: redactOtlpText(event.name) })),
|
|
39
48
|
links: span.links.map((link) => ({ ...link, attributes: attributes(link.attributes) })),
|
|
40
|
-
name:
|
|
41
|
-
status: { ...span.status, ...(span.status.message === undefined ? {} : { message:
|
|
49
|
+
name: redactOtlpText(spanName(span)),
|
|
50
|
+
status: { ...span.status, ...(span.status.message === undefined ? {} : { message: redactOtlpText(span.status.message) }) },
|
|
42
51
|
})),
|
|
43
52
|
})),
|
|
44
53
|
})),
|
|
45
54
|
}),
|
|
46
|
-
});
|
|
55
|
+
})));
|
|
47
56
|
//# sourceMappingURL=safeOtlpSerialization.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safeOtlpSerialization.js","sourceRoot":"","sources":["../../src/server/safeOtlpSerialization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"safeOtlpSerialization.js","sourceRoot":"","sources":["../../src/server/safeOtlpSerialization.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Opinionated Effect application infrastructure for ShivaeDev services",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/platform#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -186,10 +186,20 @@
|
|
|
186
186
|
"types": "./dist/server/hostOptions.d.ts",
|
|
187
187
|
"default": "./dist/server/hostOptions.js"
|
|
188
188
|
},
|
|
189
|
+
"#otlpHttpClient.ts": {
|
|
190
|
+
"source": "./src/server/otlpHttpClient.ts",
|
|
191
|
+
"types": "./dist/server/otlpHttpClient.d.ts",
|
|
192
|
+
"default": "./dist/server/otlpHttpClient.js"
|
|
193
|
+
},
|
|
189
194
|
"#safeOtlpSerialization.ts": {
|
|
190
195
|
"source": "./src/server/safeOtlpSerialization.ts",
|
|
191
196
|
"types": "./dist/server/safeOtlpSerialization.d.ts",
|
|
192
197
|
"default": "./dist/server/safeOtlpSerialization.js"
|
|
198
|
+
},
|
|
199
|
+
"#redactOtlpUrl.ts": {
|
|
200
|
+
"source": "./src/server/redactOtlpUrl.ts",
|
|
201
|
+
"types": "./dist/server/redactOtlpUrl.d.ts",
|
|
202
|
+
"default": "./dist/server/redactOtlpUrl.js"
|
|
193
203
|
}
|
|
194
204
|
},
|
|
195
205
|
"exports": {
|
|
@@ -362,8 +372,8 @@
|
|
|
362
372
|
"README.md"
|
|
363
373
|
],
|
|
364
374
|
"dependencies": {
|
|
365
|
-
"@shivaedev/effect-contract": "0.5.
|
|
366
|
-
"@shivaedev/effect-sql": "0.
|
|
375
|
+
"@shivaedev/effect-contract": "0.5.1",
|
|
376
|
+
"@shivaedev/effect-sql": "0.5.0",
|
|
367
377
|
"@shivaedev/types": "0.2.1"
|
|
368
378
|
},
|
|
369
379
|
"devDependencies": {
|
|
@@ -376,8 +386,8 @@
|
|
|
376
386
|
"@prisma-next/sql-contract": "0.16.0",
|
|
377
387
|
"@prisma-next/sql-orm-client": "0.16.0",
|
|
378
388
|
"@prisma-next/target-postgres": "0.16.0",
|
|
379
|
-
"@shivaedev/effect-prisma": "0.9.
|
|
380
|
-
"@shivaedev/effect-trpc": "0.7.
|
|
389
|
+
"@shivaedev/effect-prisma": "0.9.1",
|
|
390
|
+
"@shivaedev/effect-trpc": "0.7.1",
|
|
381
391
|
"@shivaedev/quality": "0.22.0",
|
|
382
392
|
"@trpc/client": "11.19.0",
|
|
383
393
|
"@trpc/server": "11.19.0",
|
|
@@ -394,8 +404,8 @@
|
|
|
394
404
|
"@effect/sql-pg": "4.0.4",
|
|
395
405
|
"@effect/vitest": "4.0.4",
|
|
396
406
|
"@prisma-next/sql-orm-client": "0.16.0",
|
|
397
|
-
"@shivaedev/effect-prisma": "0.9.
|
|
398
|
-
"@shivaedev/effect-trpc": "0.7.
|
|
407
|
+
"@shivaedev/effect-prisma": "0.9.1",
|
|
408
|
+
"@shivaedev/effect-trpc": "0.7.1",
|
|
399
409
|
"@trpc/server": "11.19.0",
|
|
400
410
|
"better-auth": "1.7.7",
|
|
401
411
|
"effect": "4.0.4"
|
package/src/server/appHost.ts
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
|
-
import { Context, type Duration, type Effect
|
|
1
|
+
import { Context, type Duration, type Effect } from "effect";
|
|
2
2
|
import type { Rpc } from "effect/rpc";
|
|
3
3
|
import type { Migrator } from "effect/sql";
|
|
4
4
|
import type { makeLiveServer } from "@shivaedev/effect-contract/makeLiveServer.ts";
|
|
5
|
-
import type { PostgresChangesOptions } from "@shivaedev/effect-sql/postgresChanges.ts";
|
|
6
5
|
import type { SessionPolicy, SessionShape } from "#rpc-server/session.ts";
|
|
7
|
-
import { appHostHttp, migratedHost } from "
|
|
8
|
-
import { databaseLayer } from "
|
|
9
|
-
import type { HostOptions, HostPolicy } from "
|
|
6
|
+
import { appHostHttp, deferredHost, migratedHost } from "#server/appHostHttp.ts";
|
|
7
|
+
import { type DatabaseConfig, databaseLayer } from "#server/databaseLayer.ts";
|
|
8
|
+
import type { HostOptions, HostPolicy } from "#server/hostOptions.ts";
|
|
10
9
|
|
|
11
10
|
export class AppChanges extends Context.Service<AppChanges, Effect.Success<typeof makeLiveServer>>()("@shivaedev/platform/AppChanges") {}
|
|
12
11
|
export interface AppDatabaseOptions<R = never> {
|
|
13
|
-
readonly config?:
|
|
14
|
-
readonly databaseUrl?: Redacted.Redacted<string>;
|
|
15
|
-
readonly applicationName?: string;
|
|
16
|
-
readonly maxConnections?: number;
|
|
17
|
-
readonly changes?: Omit<PostgresChangesOptions, "client" | "resync">;
|
|
18
|
-
};
|
|
12
|
+
readonly config?: DatabaseConfig;
|
|
19
13
|
readonly migrations: {
|
|
20
14
|
readonly loader: Migrator.Loader<R>;
|
|
15
|
+
readonly mode?: "migrate" | "check";
|
|
21
16
|
readonly table?: string;
|
|
22
17
|
readonly lockTimeout?: Duration.Input;
|
|
23
18
|
};
|
|
@@ -80,30 +75,35 @@ export function appHost<
|
|
|
80
75
|
TServiceRequirements = never,
|
|
81
76
|
TMigrations = never,
|
|
82
77
|
TSession extends SessionShape = SessionShape,
|
|
78
|
+
TApplicationError = never,
|
|
79
|
+
TApplicationRequirements = never,
|
|
83
80
|
>(
|
|
84
81
|
options:
|
|
82
|
+
| (AppDatabaseOptions<TMigrations> & {
|
|
83
|
+
readonly application: Effect.Effect<
|
|
84
|
+
AppHttpHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TSession>,
|
|
85
|
+
TApplicationError,
|
|
86
|
+
TApplicationRequirements
|
|
87
|
+
>;
|
|
88
|
+
})
|
|
85
89
|
| AppHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TMigrations, TSession>
|
|
86
90
|
| AppLocalHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TMigrations>,
|
|
87
91
|
): ReturnType<
|
|
88
|
-
typeof
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
readonly sessions?: never;
|
|
102
|
-
readonly migrations?: undefined;
|
|
103
|
-
},
|
|
104
|
-
): ReturnType<
|
|
105
|
-
typeof appHostHttp<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, SessionShape>
|
|
92
|
+
typeof deferredHost<
|
|
93
|
+
TRpcs,
|
|
94
|
+
THandlers,
|
|
95
|
+
THandlerError,
|
|
96
|
+
THandlerRequirements,
|
|
97
|
+
TServices,
|
|
98
|
+
TServiceError,
|
|
99
|
+
TServiceRequirements,
|
|
100
|
+
TMigrations,
|
|
101
|
+
TSession,
|
|
102
|
+
TApplicationError,
|
|
103
|
+
TApplicationRequirements
|
|
104
|
+
>
|
|
106
105
|
>;
|
|
106
|
+
|
|
107
107
|
export function appHost<
|
|
108
108
|
TRpcs extends Rpc.Any,
|
|
109
109
|
THandlers,
|
|
@@ -126,12 +126,24 @@ export function appHost<
|
|
|
126
126
|
TServiceRequirements = never,
|
|
127
127
|
TMigrations = never,
|
|
128
128
|
TSession extends SessionShape = SessionShape,
|
|
129
|
+
TApplicationError = never,
|
|
130
|
+
TApplicationRequirements = never,
|
|
129
131
|
>(
|
|
130
132
|
options:
|
|
133
|
+
| (AppDatabaseOptions<TMigrations> & {
|
|
134
|
+
readonly application: Effect.Effect<
|
|
135
|
+
AppHttpHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TSession>,
|
|
136
|
+
TApplicationError,
|
|
137
|
+
TApplicationRequirements
|
|
138
|
+
>;
|
|
139
|
+
})
|
|
131
140
|
| AppHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TMigrations, TSession>
|
|
132
141
|
| AppLocalHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TMigrations>
|
|
133
142
|
| AppHttpHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TSession>,
|
|
134
143
|
) {
|
|
144
|
+
if ("application" in options) {
|
|
145
|
+
return deferredHost(options.application, appDatabase(options));
|
|
146
|
+
}
|
|
135
147
|
return options.migrations === undefined
|
|
136
148
|
? appHostHttp(options)
|
|
137
149
|
: migratedHost({ ...options, migrations: options.migrations }, appDatabase({ ...options, migrations: options.migrations }));
|
|
@@ -3,7 +3,7 @@ import { NodeHttpServer, NodeServices } from "@effect/platform-node";
|
|
|
3
3
|
import { ByteSize, Config, Effect, Layer, Option, Schema } from "effect";
|
|
4
4
|
import { HttpIncomingMessage, HttpRouter, HttpServerRequest, HttpStaticServer } from "effect/http";
|
|
5
5
|
import { type Rpc, RpcSerialization, RpcServer } from "effect/rpc";
|
|
6
|
-
import type { AppHostOptions, AppLocalHostOptions } from "#appHost.ts";
|
|
6
|
+
import type { AppHostOptions, AppHttpHostOptions, AppLocalHostOptions } from "#appHost.ts";
|
|
7
7
|
import { boundedRpcRequest } from "#boundedRpcRequest.ts";
|
|
8
8
|
import type { databaseLayer } from "#databaseLayer.ts";
|
|
9
9
|
import type { HostOptions, HostPolicy } from "#hostOptions.ts";
|
|
@@ -13,6 +13,7 @@ import { localRpcGuard } from "#rpc-server/local.ts";
|
|
|
13
13
|
import { authenticatedLayer, maybeAuthenticatedLayer, type SessionShape } from "#rpc-server/session.ts";
|
|
14
14
|
import { requestTracingLayer } from "#rpc-server/tracing.ts";
|
|
15
15
|
import { TrustedProxy } from "#rpc-server/trustedProxy.ts";
|
|
16
|
+
import { healthRoute } from "#server/healthRoute.ts";
|
|
16
17
|
export function appHostHttp<
|
|
17
18
|
TRpcs extends Rpc.Any,
|
|
18
19
|
THandlers,
|
|
@@ -74,7 +75,7 @@ export function appHostHttp<
|
|
|
74
75
|
});
|
|
75
76
|
}),
|
|
76
77
|
);
|
|
77
|
-
return HttpRouter.serve(Layer.mergeAll(routes, assets, options.routes ?? Layer.empty), { disableLogger: true }).pipe(
|
|
78
|
+
return HttpRouter.serve(Layer.mergeAll(routes, healthRoute, assets, options.routes ?? Layer.empty), { disableLogger: true }).pipe(
|
|
78
79
|
Layer.provideMerge(application),
|
|
79
80
|
Layer.provideMerge(server),
|
|
80
81
|
Layer.provide(NodeServices.layer),
|
|
@@ -102,3 +103,26 @@ export function migratedHost<
|
|
|
102
103
|
const services = Layer.mergeAll(options.services ?? Layer.empty);
|
|
103
104
|
return appHostHttp({ ...options, services: services.pipe(Layer.provideMerge(database)) });
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
export function deferredHost<
|
|
108
|
+
TRpcs extends Rpc.Any,
|
|
109
|
+
THandlers,
|
|
110
|
+
THandlerError,
|
|
111
|
+
THandlerRequirements,
|
|
112
|
+
TServices,
|
|
113
|
+
TServiceError,
|
|
114
|
+
TServiceRequirements,
|
|
115
|
+
TMigrations,
|
|
116
|
+
TSession extends SessionShape,
|
|
117
|
+
TApplicationError,
|
|
118
|
+
TApplicationRequirements,
|
|
119
|
+
>(
|
|
120
|
+
application: Effect.Effect<
|
|
121
|
+
AppHttpHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TSession>,
|
|
122
|
+
TApplicationError,
|
|
123
|
+
TApplicationRequirements
|
|
124
|
+
>,
|
|
125
|
+
database: ReturnType<typeof databaseLayer<TMigrations>>,
|
|
126
|
+
) {
|
|
127
|
+
return Layer.unwrap(Effect.map(application, (options) => appHostHttp(options))).pipe(Layer.provideMerge(database));
|
|
128
|
+
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import * as PgClient from "@effect/sql-pg/PgClient";
|
|
2
|
-
import { Config, Effect, Layer } from "effect";
|
|
2
|
+
import { Config, Effect, Layer, type Redacted } from "effect";
|
|
3
3
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
4
|
import { makeLiveServer } from "@shivaedev/effect-contract/makeLiveServer.ts";
|
|
5
|
+
import { assertPostgresMigrations } from "@shivaedev/effect-sql/assertPostgresMigrations.ts";
|
|
5
6
|
import { migratePostgres } from "@shivaedev/effect-sql/migrations.ts";
|
|
6
|
-
import { postgresChanges } from "@shivaedev/effect-sql/postgresChanges.ts";
|
|
7
|
-
import type { AppChanges, AppDatabaseOptions } from "
|
|
7
|
+
import { type PostgresChangesOptions, postgresChanges } from "@shivaedev/effect-sql/postgresChanges.ts";
|
|
8
|
+
import type { AppChanges, AppDatabaseOptions } from "#server/appHost.ts";
|
|
9
|
+
|
|
10
|
+
export interface DatabaseConfig {
|
|
11
|
+
readonly applicationName?: string;
|
|
12
|
+
readonly changes?: Omit<PostgresChangesOptions, "client" | "resync">;
|
|
13
|
+
readonly databaseUrl?: Redacted.Redacted<string>;
|
|
14
|
+
readonly maxConnections?: number;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService: typeof AppChanges) {
|
|
10
18
|
const pool = Layer.unwrap(
|
|
@@ -17,11 +25,13 @@ export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService:
|
|
|
17
25
|
});
|
|
18
26
|
}),
|
|
19
27
|
);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
const prepared = Layer.effectDiscard(
|
|
29
|
+
options.migrations.mode === "check"
|
|
30
|
+
? assertPostgresMigrations(options.migrations)
|
|
31
|
+
: migratePostgres({ ...options.migrations, lockTimeout: options.migrations.lockTimeout ?? "5 seconds" }),
|
|
32
|
+
).pipe(Layer.provideMerge(pool));
|
|
23
33
|
const live = Layer.effect(changesService, makeLiveServer).pipe(Layer.provideMerge(Reactivity.layer));
|
|
24
|
-
const base = Layer.merge(
|
|
34
|
+
const base = Layer.merge(prepared, live);
|
|
25
35
|
const changes = options.config?.changes;
|
|
26
36
|
if (changes === undefined) {
|
|
27
37
|
return base;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect, Fiber, Option } from "effect";
|
|
2
|
+
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
3
|
+
import { SqlClient } from "effect/sql";
|
|
4
|
+
|
|
5
|
+
export const healthRoute = HttpRouter.use((router) =>
|
|
6
|
+
Effect.gen(function* () {
|
|
7
|
+
const database = yield* Effect.serviceOption(SqlClient.SqlClient);
|
|
8
|
+
const scope = yield* Effect.scope;
|
|
9
|
+
const probe = Effect.gen(function* () {
|
|
10
|
+
if (Option.isNone(database)) {
|
|
11
|
+
return 503;
|
|
12
|
+
}
|
|
13
|
+
const query = yield* database.value`select 1`.pipe(Effect.forkIn(scope));
|
|
14
|
+
return yield* Fiber.join(query).pipe(
|
|
15
|
+
Effect.timeout("2 seconds"),
|
|
16
|
+
// Driver cancellation may need network I/O; keep cleanup in the host scope without delaying the response.
|
|
17
|
+
Effect.ensuring(Fiber.interrupt(query).pipe(Effect.forkIn(scope), Effect.asVoid)),
|
|
18
|
+
Effect.match({ onFailure: () => 503, onSuccess: () => 200 }),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
yield* router.add(
|
|
22
|
+
"GET",
|
|
23
|
+
"/health",
|
|
24
|
+
Effect.map(probe, (status) => HttpServerResponse.empty({ headers: { "cache-control": "no-store" }, status })),
|
|
25
|
+
);
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { FetchHttpClient, HttpClient } from "effect/http";
|
|
3
|
+
import { redactOtlpText } from "#safeOtlpSerialization.ts";
|
|
4
|
+
|
|
5
|
+
export const otlpHttpClient = Layer.effect(
|
|
6
|
+
HttpClient.HttpClient,
|
|
7
|
+
Effect.gen(function* () {
|
|
8
|
+
const client = yield* HttpClient.HttpClient;
|
|
9
|
+
let warned = false;
|
|
10
|
+
function warnOnce(diagnostic: Effect.Effect<Record<string, unknown>>) {
|
|
11
|
+
return Effect.suspend(() => {
|
|
12
|
+
if (warned) {
|
|
13
|
+
return Effect.void;
|
|
14
|
+
}
|
|
15
|
+
warned = true;
|
|
16
|
+
return Effect.flatMap(diagnostic, (annotations) => Effect.logWarning("OTLP trace export failed").pipe(Effect.annotateLogs(annotations)));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return client.pipe(
|
|
20
|
+
HttpClient.tap((response) => {
|
|
21
|
+
if (response.status >= 200 && response.status < 300) {
|
|
22
|
+
warned = false;
|
|
23
|
+
return Effect.void;
|
|
24
|
+
}
|
|
25
|
+
return warnOnce(
|
|
26
|
+
response.text.pipe(
|
|
27
|
+
Effect.orElseSucceed(() => "Unable to read collector response"),
|
|
28
|
+
Effect.map((body) => ({ "otlp.response": redactOtlpText(body), "otlp.status": response.status })),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
}),
|
|
32
|
+
HttpClient.tapError((error) => warnOnce(Effect.succeed({ "otlp.failure": error.reason._tag }))),
|
|
33
|
+
);
|
|
34
|
+
}),
|
|
35
|
+
).pipe(Layer.provide(FetchHttpClient.layer));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Config, Effect, Layer, Schema } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { OtlpSerialization, OtlpTracer } from "effect/observability";
|
|
3
|
+
import { otlpHttpClient } from "#otlpHttpClient.ts";
|
|
4
4
|
import { safeOtlpSerialization } from "#safeOtlpSerialization.ts";
|
|
5
5
|
|
|
6
6
|
export const otlpTracing: Layer.Layer<never, Config.ConfigError> = Layer.unwrap(
|
|
@@ -19,16 +19,26 @@ export const otlpTracing: Layer.Layer<never, Config.ConfigError> = Layer.unwrap(
|
|
|
19
19
|
if (endpoint === undefined) {
|
|
20
20
|
return Layer.empty;
|
|
21
21
|
}
|
|
22
|
+
const configuredProtocol = yield* Config.String("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL").pipe(
|
|
23
|
+
Config.orElse(() => Config.String("OTEL_EXPORTER_OTLP_PROTOCOL")),
|
|
24
|
+
Config.withDefault("http/protobuf"),
|
|
25
|
+
);
|
|
26
|
+
const protocol = yield* Schema.decodeUnknownEffect(Schema.Literals(["http/protobuf", "http/json"]), { reportInput: true })(
|
|
27
|
+
configuredProtocol,
|
|
28
|
+
).pipe(Effect.mapError((cause) => new Config.ConfigError(cause)));
|
|
22
29
|
const attributes: Record<string, string> = yield* Config.Record(
|
|
23
30
|
Schema.StringFromUriComponent,
|
|
24
31
|
Schema.StringFromUriComponent,
|
|
25
32
|
"OTEL_RESOURCE_ATTRIBUTES",
|
|
26
33
|
).pipe(Config.withDefault({}));
|
|
27
34
|
const serviceName = yield* Config.String("OTEL_SERVICE_NAME").pipe(Config.withDefault(attributes["service.name"] ?? "unknown_service"));
|
|
35
|
+
const serviceVersion = yield* Config.String("OTEL_SERVICE_VERSION").pipe(Config.withDefault(undefined));
|
|
28
36
|
const headers = yield* Config.Record(Schema.String, Schema.StringFromUriComponent, "OTEL_EXPORTER_OTLP_HEADERS").pipe(Config.withDefault({}));
|
|
29
|
-
return OtlpTracer.layer({ headers, resource: { attributes, serviceName }, url: endpoint.toString() }).pipe(
|
|
30
|
-
Layer.provide(
|
|
31
|
-
|
|
37
|
+
return OtlpTracer.layer({ headers, resource: { attributes, serviceName, serviceVersion }, url: endpoint.toString() }).pipe(
|
|
38
|
+
Layer.provide(
|
|
39
|
+
safeOtlpSerialization.pipe(Layer.provide(protocol === "http/protobuf" ? OtlpSerialization.layerProtobuf : OtlpSerialization.layerJson)),
|
|
40
|
+
),
|
|
41
|
+
Layer.provide(otlpHttpClient),
|
|
32
42
|
);
|
|
33
43
|
}),
|
|
34
44
|
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { REDACTED } from "#rpc-server/sensitive.ts";
|
|
2
|
+
|
|
3
|
+
function query(value: string): string {
|
|
4
|
+
return value
|
|
5
|
+
.split("&")
|
|
6
|
+
.map((part) => (part === "" ? part : `${part.split("=", 1)[0]}=${REDACTED}`))
|
|
7
|
+
.join("&");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function url(value: string): string {
|
|
11
|
+
const fragment = value.indexOf("#");
|
|
12
|
+
const target = fragment === -1 ? value : value.slice(0, fragment);
|
|
13
|
+
const separator = target.indexOf("?");
|
|
14
|
+
return (
|
|
15
|
+
(separator === -1 ? target : `${target.slice(0, separator + 1)}${query(target.slice(separator + 1))}`).replaceAll(
|
|
16
|
+
/(?<authority>(?:\b[a-z][a-z0-9+.-]*:)?\/\/)[^/?#]*@/giu,
|
|
17
|
+
`$<authority>${REDACTED}@`,
|
|
18
|
+
) + (fragment === -1 ? "" : `#${REDACTED}`)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function structured(value: string): string | undefined {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(JSON.parse(value, (key: string, item: unknown) => (typeof item === "string" ? redactOtlpUrl(item, key) : item)));
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function redactOtlpUrl(value: string, key = ""): string {
|
|
31
|
+
if (/(?:^|[._-])(?:fragment|userinfo|username|password)$/iu.test(key)) {
|
|
32
|
+
return REDACTED;
|
|
33
|
+
}
|
|
34
|
+
if (/(?:^|[._-])query$/iu.test(key)) {
|
|
35
|
+
return url(`?${value}`).slice(1);
|
|
36
|
+
}
|
|
37
|
+
if (/(?:^|[._-])(?:url|uri|target)(?:$|[._-])/iu.test(key) || /(?:^|[._-])(?:referer|referrer|location|origin)$/iu.test(key)) {
|
|
38
|
+
return url(value);
|
|
39
|
+
}
|
|
40
|
+
if (/^\s*[[{]/u.test(value)) {
|
|
41
|
+
const redacted = structured(value);
|
|
42
|
+
if (redacted !== undefined) {
|
|
43
|
+
return redacted;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Quotes and whitespace can belong to secret values, so free text has no safe closing delimiter.
|
|
47
|
+
return value.replaceAll(/(?:[a-z][a-z0-9+.-]*:\/\/|[/?#]).*/gisu, url);
|
|
48
|
+
}
|