effect-start 0.34.0 → 0.35.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.
Files changed (127) hide show
  1. package/README.md +303 -36
  2. package/dist/Fetch.d.ts +1 -1
  3. package/dist/FileRouter.d.ts +1 -1
  4. package/dist/FileRouterCodegen.d.ts.map +1 -1
  5. package/dist/FileRouterCodegen.js +8 -2
  6. package/dist/FileRouterCodegen.js.map +1 -1
  7. package/dist/Job.d.ts +94 -0
  8. package/dist/Job.d.ts.map +1 -0
  9. package/dist/Job.js +157 -0
  10. package/dist/Job.js.map +1 -0
  11. package/dist/Password.d.ts +1 -1
  12. package/dist/Route.d.ts +20 -15
  13. package/dist/Route.d.ts.map +1 -1
  14. package/dist/Route.js +12 -0
  15. package/dist/Route.js.map +1 -1
  16. package/dist/RouteBody.d.ts +7 -7
  17. package/dist/RouteBody.d.ts.map +1 -1
  18. package/dist/RouteBody.js.map +1 -1
  19. package/dist/RouteHook.d.ts +1 -1
  20. package/dist/RouteHook.d.ts.map +1 -1
  21. package/dist/RouteHook.js.map +1 -1
  22. package/dist/RouteHttp.d.ts.map +1 -1
  23. package/dist/RouteHttp.js +10 -4
  24. package/dist/RouteHttp.js.map +1 -1
  25. package/dist/RouteLink.d.ts +16 -0
  26. package/dist/RouteLink.d.ts.map +1 -0
  27. package/dist/RouteLink.js +23 -0
  28. package/dist/RouteLink.js.map +1 -0
  29. package/dist/RouteMount.d.ts +29 -32
  30. package/dist/RouteMount.d.ts.map +1 -1
  31. package/dist/RouteMount.js.map +1 -1
  32. package/dist/RouteSchema.d.ts +81 -28
  33. package/dist/RouteSchema.d.ts.map +1 -1
  34. package/dist/RouteSchema.js +56 -101
  35. package/dist/RouteSchema.js.map +1 -1
  36. package/dist/RouteSse.d.ts +1 -1
  37. package/dist/RouteSse.d.ts.map +1 -1
  38. package/dist/RouteSse.js.map +1 -1
  39. package/dist/Socket.d.ts +1 -1
  40. package/dist/Start.js +1 -1
  41. package/dist/Start.js.map +1 -1
  42. package/dist/StaticFiles.d.ts +4 -10
  43. package/dist/StaticFiles.d.ts.map +1 -1
  44. package/dist/StaticFiles.js +3 -10
  45. package/dist/StaticFiles.js.map +1 -1
  46. package/dist/System.d.ts +1 -1
  47. package/dist/_Docker.d.ts +1 -1
  48. package/dist/_HtmlScanner.d.ts +42 -0
  49. package/dist/_HtmlScanner.d.ts.map +1 -0
  50. package/dist/_HtmlScanner.js +385 -0
  51. package/dist/_HtmlScanner.js.map +1 -0
  52. package/dist/_RouteLink.d.ts +16 -0
  53. package/dist/_RouteLink.d.ts.map +1 -0
  54. package/dist/_RouteLink.js +22 -0
  55. package/dist/_RouteLink.js.map +1 -0
  56. package/dist/bun/BunRoute.d.ts +4 -6
  57. package/dist/bun/BunRoute.d.ts.map +1 -1
  58. package/dist/bun/BunRoute.js +1 -1
  59. package/dist/bun/BunRoute.js.map +1 -1
  60. package/dist/bundler/Bundle.d.ts +1 -1
  61. package/dist/bundler/BundleRoute.d.ts +5 -6
  62. package/dist/bundler/BundleRoute.d.ts.map +1 -1
  63. package/dist/bundler/BundleRoute.js +5 -11
  64. package/dist/bundler/BundleRoute.js.map +1 -1
  65. package/dist/datastar/watchers/patchElements.js +1 -1
  66. package/dist/datastar/watchers/patchElements.js.map +1 -1
  67. package/dist/experimental/CsrfProtection.d.ts +67 -0
  68. package/dist/experimental/CsrfProtection.d.ts.map +1 -0
  69. package/dist/experimental/CsrfProtection.js +100 -0
  70. package/dist/experimental/CsrfProtection.js.map +1 -0
  71. package/dist/experimental/EncryptedCookies.d.ts +1 -1
  72. package/dist/experimental/KeyValueStore.d.ts +1 -1
  73. package/dist/lint/plugin.js +4 -0
  74. package/dist/lint/plugin.js.map +1 -1
  75. package/dist/sql/SqlClient.d.ts +1 -1
  76. package/dist/studio/Studio.d.ts +1 -1
  77. package/dist/studio/Studio.d.ts.map +1 -1
  78. package/dist/studio/Studio.js +4 -10
  79. package/dist/studio/Studio.js.map +1 -1
  80. package/dist/studio/routes/errors/route.d.ts +3 -3
  81. package/dist/studio/routes/errors/route.d.ts.map +1 -1
  82. package/dist/studio/routes/errors/route.js +3 -2
  83. package/dist/studio/routes/errors/route.js.map +1 -1
  84. package/dist/studio/routes/fiberDetail.d.ts +3 -7
  85. package/dist/studio/routes/fiberDetail.d.ts.map +1 -1
  86. package/dist/studio/routes/fibers/route.d.ts +3 -3
  87. package/dist/studio/routes/layout.d.ts +3 -3
  88. package/dist/studio/routes/logs/route.d.ts +3 -3
  89. package/dist/studio/routes/logs/route.d.ts.map +1 -1
  90. package/dist/studio/routes/logs/route.js +3 -2
  91. package/dist/studio/routes/logs/route.js.map +1 -1
  92. package/dist/studio/routes/metrics/route.d.ts +3 -3
  93. package/dist/studio/routes/route.d.ts +2 -2
  94. package/dist/studio/routes/routes/route.d.ts +2 -2
  95. package/dist/studio/routes/services/route.d.ts +2 -2
  96. package/dist/studio/routes/system/route.d.ts +3 -3
  97. package/dist/studio/routes/traceDetail.d.ts +3 -7
  98. package/dist/studio/routes/traceDetail.d.ts.map +1 -1
  99. package/dist/studio/routes/traces/route.d.ts +3 -3
  100. package/dist/studio/routes/traces/route.d.ts.map +1 -1
  101. package/dist/studio/routes/traces/route.js +3 -2
  102. package/dist/studio/routes/traces/route.js.map +1 -1
  103. package/dist/studio/routes/tree.d.ts +43 -51
  104. package/dist/studio/routes/tree.d.ts.map +1 -1
  105. package/package.json +4 -3
  106. package/src/FileRouterCodegen.ts +8 -2
  107. package/src/Route.ts +55 -34
  108. package/src/RouteBody.ts +15 -15
  109. package/src/RouteHook.ts +3 -3
  110. package/src/RouteHttp.ts +10 -4
  111. package/src/RouteLink.ts +56 -0
  112. package/src/RouteMount.ts +43 -48
  113. package/src/RouteSchema.ts +299 -166
  114. package/src/RouteSse.ts +3 -3
  115. package/src/Start.ts +1 -1
  116. package/src/StaticFiles.ts +10 -24
  117. package/src/_HtmlScanner.ts +415 -0
  118. package/src/bun/BunRoute.ts +11 -11
  119. package/src/bundler/BundleRoute.ts +8 -19
  120. package/src/datastar/watchers/patchElements.ts +1 -1
  121. package/src/dev.d.ts +3 -0
  122. package/src/experimental/CsrfProtection.ts +153 -0
  123. package/src/lint/plugin.js +2 -0
  124. package/src/studio/Studio.ts +4 -14
  125. package/src/studio/routes/errors/route.tsx +3 -2
  126. package/src/studio/routes/logs/route.tsx +3 -2
  127. package/src/studio/routes/traces/route.tsx +3 -2
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Cross-Site Request Forgery (CSRF) protection middleware.
3
+ *
4
+ * CSRF is an attack where a malicious site tricks a user's browser into making
5
+ * a request to your app. For example, a hidden form on `evil.com` could
6
+ * POST to `yourapp.com/transfer?amount=1000` and the browser would send the
7
+ * user's cookies along with it.
8
+ *
9
+ * All modern browsers send the `Sec-Fetch-Site` header on every request.
10
+ *
11
+ * If the header is present, this middleware enforces it.
12
+ * Otherwise, the request isn't coming from a browser, so there's no CSRF risk
13
+ * and it passes through. This means curl and API clients are never blocked.
14
+ *
15
+ * `Sec-Fetch-Site` tells the server where a request came from:
16
+ * - `same-origin` — the request came from the same origin (scheme + host + port)
17
+ * - `same-site` — the request came from a subdomain of the same site
18
+ * - `cross-site` — the request came from a completely different site
19
+ * - `none` — the user navigated directly (e.g. typing in the address bar)
20
+ *
21
+ * Unlike traditional CSRF token approaches (hidden form fields, session-stored
22
+ * tokens), this requires no server-side state and no client-side plumbing.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // in route layer
27
+ * Route.use(
28
+ * CsrfProtection.make(),
29
+ * )
30
+ * ```
31
+ */
32
+
33
+ import * as Data from "effect/Data"
34
+ import * as Effect from "effect/Effect"
35
+ import * as Entity from "../Entity.ts"
36
+ import * as Route from "../Route.ts"
37
+
38
+ export class CsrfError extends Data.TaggedError("CsrfError")<{
39
+ readonly reason: string
40
+ }> {}
41
+
42
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"])
43
+
44
+ export interface Options {
45
+ /**
46
+ * Origins that are allowed to make cross-site requests.
47
+ *
48
+ * Normally a request from `https://accounts.google.com` to your app would be
49
+ * blocked because `Sec-Fetch-Site` is `cross-site`. Adding it here whitelists
50
+ * that origin. Useful for OAuth callbacks, third-party payment providers, or
51
+ * any external service that POSTs back to your app.
52
+ *
53
+ * Must be exact origin strings including scheme (e.g. `"https://example.com"`).
54
+ */
55
+ readonly trustedOrigins?: ReadonlyArray<string>
56
+
57
+ /**
58
+ * Whether to accept `Sec-Fetch-Site: same-site` in addition to `same-origin`.
59
+ * Defaults to `true`.
60
+ *
61
+ * `same-origin` means the request came from exactly the same scheme + host + port.
62
+ * `same-site` means it came from a different subdomain of the same registrable
63
+ * domain (e.g. `dashboard.example.com` → `api.example.com`).
64
+ *
65
+ * Set to `false` if your subdomains are untrusted (e.g. user-generated content
66
+ * on `*.example.com`) and you want to reject requests from sibling subdomains.
67
+ */
68
+ readonly allowSameSite?: boolean
69
+
70
+ }
71
+
72
+ function originTrusted(request: Request, trustedOrigins: ReadonlyArray<string>): boolean {
73
+ const origin = request.headers.get("origin")
74
+ return origin !== null && origin !== "" && trustedOrigins.includes(origin)
75
+ }
76
+
77
+ function originMatchesBase(request: Request): boolean {
78
+ const origin = request.headers.get("origin")
79
+ if (origin === null || origin === "") return true
80
+ const url = new URL(request.url)
81
+ const base = `${url.protocol}//${url.host}`
82
+ return origin === base
83
+ }
84
+
85
+ function reject(reason: string): Entity.Entity<string> {
86
+ return Entity.make(JSON.stringify({ error: reason }, null, 2), {
87
+ status: 403,
88
+ headers: { "content-type": "application/json" },
89
+ })
90
+ }
91
+
92
+ export function make(options?: Options) {
93
+ const trustedOrigins = options?.trustedOrigins ?? []
94
+ const allowSameSite = options?.allowSameSite ?? true
95
+ const safeFetchSites = allowSameSite
96
+ ? new Set(["same-origin", "same-site"])
97
+ : new Set(["same-origin"])
98
+
99
+ return <D extends Route.RouteDescriptor.Any, SB extends {}, P extends Route.Route.Tuple>(
100
+ self: Route.RouteSet<D, SB, P>,
101
+ ): Route.RouteSet<D, SB, [...P, Route.Route<{}, {}, unknown, never, Route.Request>]> => {
102
+ const route = Route.make<{}, {}, unknown, never, Route.Request>((_context, next) =>
103
+ Effect.gen(function* () {
104
+ const request = yield* Route.Request
105
+ const method = request.method.toUpperCase()
106
+
107
+ if (SAFE_METHODS.has(method)) {
108
+ return yield* next()
109
+ }
110
+
111
+ const secFetchSite = request.headers.get("sec-fetch-site")
112
+
113
+ if (secFetchSite === null) {
114
+ return yield* next()
115
+ }
116
+
117
+ if (!originMatchesBase(request) && !originTrusted(request, trustedOrigins)) {
118
+ const origin = request.headers.get("origin")
119
+ const url = new URL(request.url)
120
+ const base = `${url.protocol}//${url.host}`
121
+ return reject(`HTTP Origin header (${origin}) didn't match request.base_url (${base})`)
122
+ }
123
+
124
+ const value = secFetchSite.toLowerCase()
125
+
126
+ if (safeFetchSites.has(value)) {
127
+ const entity = yield* next()
128
+ return Entity.merge(entity, {
129
+ headers: { vary: "Sec-Fetch-Site" },
130
+ })
131
+ }
132
+
133
+ if (value === "cross-site" && originTrusted(request, trustedOrigins)) {
134
+ const entity = yield* next()
135
+ return Entity.merge(entity, {
136
+ headers: { vary: "Sec-Fetch-Site" },
137
+ })
138
+ }
139
+
140
+ return reject(
141
+ value === "cross-site"
142
+ ? "Sec-Fetch-Site header (cross-site) indicates a cross-site request"
143
+ : `Sec-Fetch-Site header is invalid (${JSON.stringify(value)})`,
144
+ )
145
+ }),
146
+ )
147
+
148
+ return Route.set(
149
+ [...Route.items(self), route] as [...P, Route.Route<{}, {}, unknown, never, Route.Request>],
150
+ Route.descriptor(self),
151
+ )
152
+ }
153
+ }
@@ -43,6 +43,8 @@ export default {
43
43
  if (!isLocalImport(source)) return
44
44
  const alias = node.specifiers[0].local.name
45
45
  if (alias === baseName) return
46
+ if (alias === "_" + baseName) return
47
+ if (node.importKind === "type") return
46
48
 
47
49
  context.report({
48
50
  node,
@@ -1,7 +1,5 @@
1
- import * as Effect from "effect/Effect"
2
1
  import * as Layer from "effect/Layer"
3
2
  import * as Route from "../Route.ts"
4
- import * as RouteTree from "../RouteTree.ts"
5
3
  import * as sqlBun from "../sql/bun/index.ts"
6
4
  import type * as SqlClient from "../sql/SqlClient.ts"
7
5
  import * as StudioErrors from "./StudioErrors.ts"
@@ -33,16 +31,8 @@ export function layer(
33
31
 
34
32
  export function layerRoutes(options?: { prefix?: string }) {
35
33
  const prefix = options?.prefix ?? "/studio"
36
-
37
- return Layer.effect(
38
- Route.Routes,
39
- Effect.gen(function* () {
40
- const existing = yield* Route.Routes
41
- StudioStore.store.prefix = prefix
42
- const tree = Route.tree({
43
- [prefix as "/"]: routes,
44
- })
45
- return RouteTree.merge(existing, tree)
46
- }),
47
- )
34
+ StudioStore.store.prefix = prefix
35
+ return Route.layerMerge({
36
+ [prefix as "/"]: routes,
37
+ })
48
38
  }
@@ -8,8 +8,9 @@ import * as Shell from "../../ui/Shell.tsx"
8
8
  const prefix = StudioStore.store.prefix
9
9
 
10
10
  export default Route.get(
11
- Route.html(function* (ctx) {
12
- const url = new URL(ctx.request.url)
11
+ Route.html(function* (_ctx) {
12
+ const request = yield* Route.Request
13
+ const url = new URL(request.url)
13
14
  const search = url.searchParams.get("errorSearch") || ""
14
15
  const tag = url.searchParams.get("errorTag") || ""
15
16
  const allErrors = yield* StudioStore.allErrors()
@@ -8,8 +8,9 @@ import * as Shell from "../../ui/Shell.tsx"
8
8
  const prefix = StudioStore.store.prefix
9
9
 
10
10
  export default Route.get(
11
- Route.html(function* (ctx) {
12
- const url = new URL(ctx.request.url)
11
+ Route.html(function* (_ctx) {
12
+ const request = yield* Route.Request
13
+ const url = new URL(request.url)
13
14
  const level = url.searchParams.get("logLevel") || ""
14
15
  const search = url.searchParams.get("logSearch") || ""
15
16
  let logs = yield* StudioStore.allLogs()
@@ -9,8 +9,9 @@ import * as Traces from "../../ui/Traces.tsx"
9
9
  const prefix = StudioStore.store.prefix
10
10
 
11
11
  export default Route.get(
12
- Route.html(function* (ctx) {
13
- const url = new URL(ctx.request.url)
12
+ Route.html(function* (_ctx) {
13
+ const request = yield* Route.Request
14
+ const url = new URL(request.url)
14
15
  const search = url.searchParams.get("traceSearch") || ""
15
16
  const allSpans = StudioStore.filterOutStudioSpans(yield* StudioStore.allSpans())
16
17
  const names = Array.from(new Set(allSpans.map((s) => s.name))).sort()