@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +25 -4
  3. package/dist/server/appHost.d.ts +9 -16
  4. package/dist/server/appHost.d.ts.map +1 -1
  5. package/dist/server/appHost.js +5 -2
  6. package/dist/server/appHost.js.map +1 -1
  7. package/dist/server/appHostHttp.d.ts +3 -2
  8. package/dist/server/appHostHttp.d.ts.map +1 -1
  9. package/dist/server/appHostHttp.js +5 -1
  10. package/dist/server/appHostHttp.js.map +1 -1
  11. package/dist/server/databaseLayer.d.ts +9 -2
  12. package/dist/server/databaseLayer.d.ts.map +1 -1
  13. package/dist/server/databaseLayer.js +5 -2
  14. package/dist/server/databaseLayer.js.map +1 -1
  15. package/dist/server/healthRoute.d.ts +3 -0
  16. package/dist/server/healthRoute.d.ts.map +1 -0
  17. package/dist/server/healthRoute.js +18 -0
  18. package/dist/server/healthRoute.js.map +1 -0
  19. package/dist/server/otlpHttpClient.d.ts +4 -0
  20. package/dist/server/otlpHttpClient.d.ts.map +1 -0
  21. package/dist/server/otlpHttpClient.js +24 -0
  22. package/dist/server/otlpHttpClient.js.map +1 -0
  23. package/dist/server/otlpTracing.d.ts.map +1 -1
  24. package/dist/server/otlpTracing.js +6 -3
  25. package/dist/server/otlpTracing.js.map +1 -1
  26. package/dist/server/redactOtlpUrl.d.ts +2 -0
  27. package/dist/server/redactOtlpUrl.d.ts.map +1 -0
  28. package/dist/server/redactOtlpUrl.js +41 -0
  29. package/dist/server/redactOtlpUrl.js.map +1 -0
  30. package/dist/server/safeOtlpSerialization.d.ts +2 -1
  31. package/dist/server/safeOtlpSerialization.d.ts.map +1 -1
  32. package/dist/server/safeOtlpSerialization.js +25 -16
  33. package/dist/server/safeOtlpSerialization.js.map +1 -1
  34. package/package.json +17 -7
  35. package/src/server/appHost.ts +41 -29
  36. package/src/server/appHostHttp.ts +26 -2
  37. package/src/server/databaseLayer.ts +17 -7
  38. package/src/server/healthRoute.ts +27 -0
  39. package/src/server/otlpHttpClient.ts +35 -0
  40. package/src/server/otlpTracing.ts +15 -5
  41. package/src/server/redactOtlpUrl.ts +48 -0
  42. 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 const safeOtlpSerialization: Layer.Layer<OtlpSerialization.OtlpSerialization, never, never>;
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,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAG/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAwBzD,eAAO,MAAM,qBAAqB,gEAsBhC,CAAC"}
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 text(value) {
9
- return redactText(value, sensitive).replaceAll(/(?<scheme>\b[a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/giu, `$<scheme>${REDACTED}@`);
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: text(value.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
- export const safeOtlpSerialization = Layer.succeed(OtlpSerialization.OtlpSerialization, {
26
- logs: HttpBody.jsonUnsafe,
27
- metrics: HttpBody.jsonUnsafe,
28
- traces: (data) => HttpBody.jsonUnsafe({
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: text(event.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: text(span.name),
41
- status: { ...span.status, ...(span.status.message === undefined ? {} : { message: text(span.status.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":"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"}
1
+ {"version":3,"file":"safeOtlpSerialization.js","sourceRoot":"","sources":["../../src/server/safeOtlpSerialization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEhF,SAAS,SAAS,CAAC,GAAW;IAC7B,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,EAAE,sBAAsB,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;AAC/G,CAAC;AACD,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,GAAG,GAAG,EAAE;IACrD,OAAO,UAAU,CAAC,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,SAAS,CAAC,CAAC;AACzD,CAAC;AACD,SAAS,cAAc,CAAC,KAA4B,EAAE,GAAW;IAChE,OAAO;QACN,GAAG,KAAK;QACR,GAAG,CAAC,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,cAAc,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzH,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/F,CAAC;AACH,CAAC;AACD,SAAS,UAAU,CAAC,MAA+B;IAClD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACtC,GAAG;QACH,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC;KAC9E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,QAAQ,CAAC,IAAoG;IACrH,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,qBAAqB,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC;QACnG,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,YAAY,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC;QACzF,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;QAClE,CAAC;IACF,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC,MAAM,CAChD,iBAAiB,CAAC,iBAAiB,EACnC,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,iBAAiB,EAAE,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IACnE,GAAG,aAAa;IAChB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAChB,aAAa,CAAC,MAAM,CAAC;QACpB,GAAG,IAAI;QACP,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;YACpD,GAAG,QAAQ;YACX,QAAQ,EAAE,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE;YACxF,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC/C,GAAG,KAAK;gBACR,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;oBACjC,GAAG,IAAI;oBACP,UAAU,EAAE,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC;oBACvC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBAC9H,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;oBACvF,IAAI,EAAE,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;oBACpC,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE;iBAC1H,CAAC,CAAC;aACH,CAAC,CAAC;SACH,CAAC,CAAC;KACH,CAAC;CACH,CAAC,CAAC,CACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/platform",
3
- "version": "0.13.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.0",
366
- "@shivaedev/effect-sql": "0.4.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.0",
380
- "@shivaedev/effect-trpc": "0.7.0",
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.0",
398
- "@shivaedev/effect-trpc": "0.7.0",
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"
@@ -1,23 +1,18 @@
1
- import { Context, type Duration, type Effect, type Redacted } from "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 "./appHostHttp.ts";
8
- import { databaseLayer } from "./databaseLayer.ts";
9
- import type { HostOptions, HostPolicy } from "./hostOptions.ts";
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 migratedHost<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TMigrations, TSession>
89
- >;
90
- export function appHost<
91
- TRpcs extends Rpc.Any,
92
- THandlers,
93
- THandlerError,
94
- THandlerRequirements,
95
- TServices = never,
96
- TServiceError = never,
97
- TServiceRequirements = never,
98
- >(
99
- options: HostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> & {
100
- readonly policy: "loopback";
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 "./appHost.ts";
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 migrated = Layer.effectDiscard(migratePostgres({ ...options.migrations, lockTimeout: options.migrations.lockTimeout ?? "5 seconds" })).pipe(
21
- Layer.provideMerge(pool),
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(migrated, live);
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 { FetchHttpClient } from "effect/http";
3
- import { OtlpTracer } from "effect/observability";
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(safeOtlpSerialization),
31
- Layer.provide(FetchHttpClient.layer),
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
+ }