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.
- package/README.md +303 -36
- package/dist/Fetch.d.ts +1 -1
- package/dist/FileRouter.d.ts +1 -1
- package/dist/FileRouterCodegen.d.ts.map +1 -1
- package/dist/FileRouterCodegen.js +8 -2
- package/dist/FileRouterCodegen.js.map +1 -1
- package/dist/Job.d.ts +94 -0
- package/dist/Job.d.ts.map +1 -0
- package/dist/Job.js +157 -0
- package/dist/Job.js.map +1 -0
- package/dist/Password.d.ts +1 -1
- package/dist/Route.d.ts +20 -15
- package/dist/Route.d.ts.map +1 -1
- package/dist/Route.js +12 -0
- package/dist/Route.js.map +1 -1
- package/dist/RouteBody.d.ts +7 -7
- package/dist/RouteBody.d.ts.map +1 -1
- package/dist/RouteBody.js.map +1 -1
- package/dist/RouteHook.d.ts +1 -1
- package/dist/RouteHook.d.ts.map +1 -1
- package/dist/RouteHook.js.map +1 -1
- package/dist/RouteHttp.d.ts.map +1 -1
- package/dist/RouteHttp.js +10 -4
- package/dist/RouteHttp.js.map +1 -1
- package/dist/RouteLink.d.ts +16 -0
- package/dist/RouteLink.d.ts.map +1 -0
- package/dist/RouteLink.js +23 -0
- package/dist/RouteLink.js.map +1 -0
- package/dist/RouteMount.d.ts +29 -32
- package/dist/RouteMount.d.ts.map +1 -1
- package/dist/RouteMount.js.map +1 -1
- package/dist/RouteSchema.d.ts +81 -28
- package/dist/RouteSchema.d.ts.map +1 -1
- package/dist/RouteSchema.js +56 -101
- package/dist/RouteSchema.js.map +1 -1
- package/dist/RouteSse.d.ts +1 -1
- package/dist/RouteSse.d.ts.map +1 -1
- package/dist/RouteSse.js.map +1 -1
- package/dist/Socket.d.ts +1 -1
- package/dist/Start.js +1 -1
- package/dist/Start.js.map +1 -1
- package/dist/StaticFiles.d.ts +4 -10
- package/dist/StaticFiles.d.ts.map +1 -1
- package/dist/StaticFiles.js +3 -10
- package/dist/StaticFiles.js.map +1 -1
- package/dist/System.d.ts +1 -1
- package/dist/_Docker.d.ts +1 -1
- package/dist/_HtmlScanner.d.ts +42 -0
- package/dist/_HtmlScanner.d.ts.map +1 -0
- package/dist/_HtmlScanner.js +385 -0
- package/dist/_HtmlScanner.js.map +1 -0
- package/dist/_RouteLink.d.ts +16 -0
- package/dist/_RouteLink.d.ts.map +1 -0
- package/dist/_RouteLink.js +22 -0
- package/dist/_RouteLink.js.map +1 -0
- package/dist/bun/BunRoute.d.ts +4 -6
- package/dist/bun/BunRoute.d.ts.map +1 -1
- package/dist/bun/BunRoute.js +1 -1
- package/dist/bun/BunRoute.js.map +1 -1
- package/dist/bundler/Bundle.d.ts +1 -1
- package/dist/bundler/BundleRoute.d.ts +5 -6
- package/dist/bundler/BundleRoute.d.ts.map +1 -1
- package/dist/bundler/BundleRoute.js +5 -11
- package/dist/bundler/BundleRoute.js.map +1 -1
- package/dist/datastar/watchers/patchElements.js +1 -1
- package/dist/datastar/watchers/patchElements.js.map +1 -1
- package/dist/experimental/CsrfProtection.d.ts +67 -0
- package/dist/experimental/CsrfProtection.d.ts.map +1 -0
- package/dist/experimental/CsrfProtection.js +100 -0
- package/dist/experimental/CsrfProtection.js.map +1 -0
- package/dist/experimental/EncryptedCookies.d.ts +1 -1
- package/dist/experimental/KeyValueStore.d.ts +1 -1
- package/dist/lint/plugin.js +4 -0
- package/dist/lint/plugin.js.map +1 -1
- package/dist/sql/SqlClient.d.ts +1 -1
- package/dist/studio/Studio.d.ts +1 -1
- package/dist/studio/Studio.d.ts.map +1 -1
- package/dist/studio/Studio.js +4 -10
- package/dist/studio/Studio.js.map +1 -1
- package/dist/studio/routes/errors/route.d.ts +3 -3
- package/dist/studio/routes/errors/route.d.ts.map +1 -1
- package/dist/studio/routes/errors/route.js +3 -2
- package/dist/studio/routes/errors/route.js.map +1 -1
- package/dist/studio/routes/fiberDetail.d.ts +3 -7
- package/dist/studio/routes/fiberDetail.d.ts.map +1 -1
- package/dist/studio/routes/fibers/route.d.ts +3 -3
- package/dist/studio/routes/layout.d.ts +3 -3
- package/dist/studio/routes/logs/route.d.ts +3 -3
- package/dist/studio/routes/logs/route.d.ts.map +1 -1
- package/dist/studio/routes/logs/route.js +3 -2
- package/dist/studio/routes/logs/route.js.map +1 -1
- package/dist/studio/routes/metrics/route.d.ts +3 -3
- package/dist/studio/routes/route.d.ts +2 -2
- package/dist/studio/routes/routes/route.d.ts +2 -2
- package/dist/studio/routes/services/route.d.ts +2 -2
- package/dist/studio/routes/system/route.d.ts +3 -3
- package/dist/studio/routes/traceDetail.d.ts +3 -7
- package/dist/studio/routes/traceDetail.d.ts.map +1 -1
- package/dist/studio/routes/traces/route.d.ts +3 -3
- package/dist/studio/routes/traces/route.d.ts.map +1 -1
- package/dist/studio/routes/traces/route.js +3 -2
- package/dist/studio/routes/traces/route.js.map +1 -1
- package/dist/studio/routes/tree.d.ts +43 -51
- package/dist/studio/routes/tree.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/FileRouterCodegen.ts +8 -2
- package/src/Route.ts +55 -34
- package/src/RouteBody.ts +15 -15
- package/src/RouteHook.ts +3 -3
- package/src/RouteHttp.ts +10 -4
- package/src/RouteLink.ts +56 -0
- package/src/RouteMount.ts +43 -48
- package/src/RouteSchema.ts +299 -166
- package/src/RouteSse.ts +3 -3
- package/src/Start.ts +1 -1
- package/src/StaticFiles.ts +10 -24
- package/src/_HtmlScanner.ts +415 -0
- package/src/bun/BunRoute.ts +11 -11
- package/src/bundler/BundleRoute.ts +8 -19
- package/src/datastar/watchers/patchElements.ts +1 -1
- package/src/dev.d.ts +3 -0
- package/src/experimental/CsrfProtection.ts +153 -0
- package/src/lint/plugin.js +2 -0
- package/src/studio/Studio.ts +4 -14
- package/src/studio/routes/errors/route.tsx +3 -2
- package/src/studio/routes/logs/route.tsx +3 -2
- 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
|
+
}
|
package/src/lint/plugin.js
CHANGED
package/src/studio/Studio.ts
CHANGED
|
@@ -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
|
|
38
|
-
|
|
39
|
-
|
|
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* (
|
|
12
|
-
const
|
|
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* (
|
|
12
|
-
const
|
|
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* (
|
|
13
|
-
const
|
|
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()
|