@effect-app/infra 4.0.0-beta.22 → 4.0.0-beta.220

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 (310) hide show
  1. package/CHANGELOG.md +1640 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +7 -7
  4. package/dist/CUPS.d.ts.map +1 -1
  5. package/dist/CUPS.js +10 -12
  6. package/dist/Emailer/Sendgrid.d.ts +14 -14
  7. package/dist/Emailer/Sendgrid.d.ts.map +1 -1
  8. package/dist/Emailer/Sendgrid.js +16 -15
  9. package/dist/Emailer/fake.d.ts +1 -1
  10. package/dist/Emailer/service.d.ts +10 -4
  11. package/dist/Emailer/service.d.ts.map +1 -1
  12. package/dist/Emailer/service.js +3 -3
  13. package/dist/Emailer.d.ts +1 -1
  14. package/dist/MainFiberSet.d.ts +9 -9
  15. package/dist/MainFiberSet.d.ts.map +1 -1
  16. package/dist/MainFiberSet.js +3 -3
  17. package/dist/Model/Repository/Registry.d.ts +20 -0
  18. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  19. package/dist/Model/Repository/Registry.js +17 -0
  20. package/dist/Model/Repository/ext.d.ts +33 -15
  21. package/dist/Model/Repository/ext.d.ts.map +1 -1
  22. package/dist/Model/Repository/ext.js +54 -2
  23. package/dist/Model/Repository/internal/internal.d.ts +6 -6
  24. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  25. package/dist/Model/Repository/internal/internal.js +103 -51
  26. package/dist/Model/Repository/legacy.d.ts +1 -1
  27. package/dist/Model/Repository/makeRepo.d.ts +7 -6
  28. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  29. package/dist/Model/Repository/makeRepo.js +5 -1
  30. package/dist/Model/Repository/service.d.ts +28 -23
  31. package/dist/Model/Repository/service.d.ts.map +1 -1
  32. package/dist/Model/Repository/validation.d.ts +46 -17
  33. package/dist/Model/Repository/validation.d.ts.map +1 -1
  34. package/dist/Model/Repository/validation.js +5 -5
  35. package/dist/Model/Repository.d.ts +2 -1
  36. package/dist/Model/Repository.d.ts.map +1 -1
  37. package/dist/Model/Repository.js +2 -1
  38. package/dist/Model/dsl.d.ts +4 -4
  39. package/dist/Model/dsl.d.ts.map +1 -1
  40. package/dist/Model/filter/filterApi.d.ts +5 -5
  41. package/dist/Model/filter/filterApi.d.ts.map +1 -1
  42. package/dist/Model/filter/types/errors.d.ts +1 -1
  43. package/dist/Model/filter/types/fields.d.ts +1 -1
  44. package/dist/Model/filter/types/path/common.d.ts +1 -1
  45. package/dist/Model/filter/types/path/eager.d.ts +1 -1
  46. package/dist/Model/filter/types/path/eager.d.ts.map +1 -1
  47. package/dist/Model/filter/types/path/eager.js +1 -1
  48. package/dist/Model/filter/types/path/index.d.ts +1 -1
  49. package/dist/Model/filter/types/utils.d.ts +1 -1
  50. package/dist/Model/filter/types/validator.d.ts +1 -1
  51. package/dist/Model/filter/types.d.ts +1 -1
  52. package/dist/Model/query/dsl.d.ts +139 -16
  53. package/dist/Model/query/dsl.d.ts.map +1 -1
  54. package/dist/Model/query/dsl.js +187 -1
  55. package/dist/Model/query/new-kid-interpreter.d.ts +76 -7
  56. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  57. package/dist/Model/query/new-kid-interpreter.js +122 -6
  58. package/dist/Model/query.d.ts +1 -1
  59. package/dist/Model.d.ts +2 -1
  60. package/dist/Model.d.ts.map +1 -1
  61. package/dist/Model.js +2 -1
  62. package/dist/QueueMaker/SQLQueue.d.ts +5 -7
  63. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  64. package/dist/QueueMaker/SQLQueue.js +130 -116
  65. package/dist/QueueMaker/errors.d.ts +2 -2
  66. package/dist/QueueMaker/errors.d.ts.map +1 -1
  67. package/dist/QueueMaker/memQueue.d.ts +7 -4
  68. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  69. package/dist/QueueMaker/memQueue.js +75 -63
  70. package/dist/QueueMaker/sbqueue.d.ts +6 -3
  71. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  72. package/dist/QueueMaker/sbqueue.js +52 -53
  73. package/dist/QueueMaker/service.d.ts +1 -1
  74. package/dist/RequestContext.d.ts +74 -35
  75. package/dist/RequestContext.d.ts.map +1 -1
  76. package/dist/RequestContext.js +13 -14
  77. package/dist/RequestFiberSet.d.ts +7 -7
  78. package/dist/RequestFiberSet.d.ts.map +1 -1
  79. package/dist/RequestFiberSet.js +3 -3
  80. package/dist/Store/ContextMapContainer.d.ts +19 -3
  81. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  82. package/dist/Store/ContextMapContainer.js +13 -3
  83. package/dist/Store/Cosmos/query.d.ts +5 -1
  84. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  85. package/dist/Store/Cosmos/query.js +113 -34
  86. package/dist/Store/Cosmos.d.ts +1 -1
  87. package/dist/Store/Cosmos.d.ts.map +1 -1
  88. package/dist/Store/Cosmos.js +335 -243
  89. package/dist/Store/Disk.d.ts +2 -2
  90. package/dist/Store/Disk.d.ts.map +1 -1
  91. package/dist/Store/Disk.js +72 -35
  92. package/dist/Store/Memory.d.ts +6 -4
  93. package/dist/Store/Memory.d.ts.map +1 -1
  94. package/dist/Store/Memory.js +242 -58
  95. package/dist/Store/SQL/Pg.d.ts +4 -0
  96. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  97. package/dist/Store/SQL/Pg.js +231 -0
  98. package/dist/Store/SQL/query.d.ts +42 -0
  99. package/dist/Store/SQL/query.d.ts.map +1 -0
  100. package/dist/Store/SQL/query.js +479 -0
  101. package/dist/Store/SQL.d.ts +20 -0
  102. package/dist/Store/SQL.d.ts.map +1 -0
  103. package/dist/Store/SQL.js +446 -0
  104. package/dist/Store/codeFilter.d.ts +1 -1
  105. package/dist/Store/codeFilter.d.ts.map +1 -1
  106. package/dist/Store/codeFilter.js +4 -2
  107. package/dist/Store/index.d.ts +5 -2
  108. package/dist/Store/index.d.ts.map +1 -1
  109. package/dist/Store/index.js +15 -3
  110. package/dist/Store/service.d.ts +22 -8
  111. package/dist/Store/service.d.ts.map +1 -1
  112. package/dist/Store/service.js +24 -6
  113. package/dist/Store/utils.d.ts +1 -1
  114. package/dist/Store/utils.d.ts.map +1 -1
  115. package/dist/Store/utils.js +3 -4
  116. package/dist/Store.d.ts +1 -1
  117. package/dist/adapters/SQL/Model.d.ts +31 -42
  118. package/dist/adapters/SQL/Model.d.ts.map +1 -1
  119. package/dist/adapters/SQL/Model.js +29 -38
  120. package/dist/adapters/SQL.d.ts +1 -1
  121. package/dist/adapters/ServiceBus.d.ts +11 -11
  122. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  123. package/dist/adapters/ServiceBus.js +25 -21
  124. package/dist/adapters/cosmos-client.d.ts +3 -3
  125. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  126. package/dist/adapters/cosmos-client.js +3 -3
  127. package/dist/adapters/index.d.ts +8 -2
  128. package/dist/adapters/index.d.ts.map +1 -1
  129. package/dist/adapters/index.js +8 -2
  130. package/dist/adapters/logger.d.ts +1 -1
  131. package/dist/adapters/logger.d.ts.map +1 -1
  132. package/dist/adapters/memQueue.d.ts +3 -3
  133. package/dist/adapters/memQueue.d.ts.map +1 -1
  134. package/dist/adapters/memQueue.js +3 -3
  135. package/dist/adapters/mongo-client.d.ts +3 -3
  136. package/dist/adapters/mongo-client.d.ts.map +1 -1
  137. package/dist/adapters/mongo-client.js +3 -3
  138. package/dist/adapters/redis-client.d.ts +3 -3
  139. package/dist/adapters/redis-client.d.ts.map +1 -1
  140. package/dist/adapters/redis-client.js +3 -3
  141. package/dist/api/ContextProvider.d.ts +8 -8
  142. package/dist/api/ContextProvider.d.ts.map +1 -1
  143. package/dist/api/ContextProvider.js +6 -6
  144. package/dist/api/codec.d.ts +1 -1
  145. package/dist/api/internal/RequestContextMiddleware.d.ts +2 -2
  146. package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
  147. package/dist/api/internal/RequestContextMiddleware.js +9 -6
  148. package/dist/api/internal/auth.d.ts +44 -6
  149. package/dist/api/internal/auth.d.ts.map +1 -1
  150. package/dist/api/internal/auth.js +160 -29
  151. package/dist/api/internal/events.d.ts +3 -3
  152. package/dist/api/internal/events.d.ts.map +1 -1
  153. package/dist/api/internal/events.js +10 -8
  154. package/dist/api/internal/health.d.ts +1 -1
  155. package/dist/api/layerUtils.d.ts +6 -6
  156. package/dist/api/layerUtils.d.ts.map +1 -1
  157. package/dist/api/layerUtils.js +5 -5
  158. package/dist/api/middlewares.d.ts +1 -1
  159. package/dist/api/reportError.d.ts +1 -1
  160. package/dist/api/routing/middleware/RouterMiddleware.d.ts +4 -4
  161. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  162. package/dist/api/routing/middleware/middleware.d.ts +39 -3
  163. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  164. package/dist/api/routing/middleware/middleware.js +48 -16
  165. package/dist/api/routing/middleware.d.ts +1 -2
  166. package/dist/api/routing/middleware.d.ts.map +1 -1
  167. package/dist/api/routing/middleware.js +1 -2
  168. package/dist/api/routing/schema/jwt.d.ts +1 -1
  169. package/dist/api/routing/schema/jwt.d.ts.map +1 -1
  170. package/dist/api/routing/tsort.d.ts +1 -1
  171. package/dist/api/routing/tsort.d.ts.map +1 -1
  172. package/dist/api/routing/utils.d.ts +3 -3
  173. package/dist/api/routing/utils.d.ts.map +1 -1
  174. package/dist/api/routing.d.ts +80 -37
  175. package/dist/api/routing.d.ts.map +1 -1
  176. package/dist/api/routing.js +109 -41
  177. package/dist/api/setupRequest.d.ts +8 -5
  178. package/dist/api/setupRequest.d.ts.map +1 -1
  179. package/dist/api/setupRequest.js +12 -7
  180. package/dist/api/util.d.ts +1 -1
  181. package/dist/arbs.d.ts +1 -1
  182. package/dist/arbs.d.ts.map +1 -1
  183. package/dist/arbs.js +5 -3
  184. package/dist/errorReporter.d.ts +4 -4
  185. package/dist/errorReporter.d.ts.map +1 -1
  186. package/dist/errorReporter.js +20 -25
  187. package/dist/errors.d.ts +1 -1
  188. package/dist/fileUtil.d.ts +1 -1
  189. package/dist/fileUtil.d.ts.map +1 -1
  190. package/dist/index.d.ts +1 -1
  191. package/dist/logger/jsonLogger.d.ts +1 -1
  192. package/dist/logger/logFmtLogger.d.ts +1 -1
  193. package/dist/logger/shared.d.ts +1 -1
  194. package/dist/logger/shared.js +2 -2
  195. package/dist/logger.d.ts +1 -1
  196. package/dist/logger.d.ts.map +1 -1
  197. package/dist/otel.d.ts +75 -0
  198. package/dist/otel.d.ts.map +1 -0
  199. package/dist/otel.js +65 -0
  200. package/dist/rateLimit.d.ts +9 -3
  201. package/dist/rateLimit.d.ts.map +1 -1
  202. package/dist/rateLimit.js +5 -11
  203. package/dist/test.d.ts +2 -2
  204. package/dist/test.d.ts.map +1 -1
  205. package/dist/test.js +1 -1
  206. package/dist/vitest.d.ts +1 -1
  207. package/examples/query.ts +42 -38
  208. package/package.json +46 -37
  209. package/src/CUPS.ts +9 -11
  210. package/src/Emailer/Sendgrid.ts +17 -14
  211. package/src/Emailer/service.ts +9 -3
  212. package/src/MainFiberSet.ts +5 -6
  213. package/src/Model/Repository/Registry.ts +33 -0
  214. package/src/Model/Repository/ext.ts +96 -10
  215. package/src/Model/Repository/internal/internal.ts +218 -149
  216. package/src/Model/Repository/makeRepo.ts +12 -10
  217. package/src/Model/Repository/service.ts +31 -22
  218. package/src/Model/Repository/validation.ts +4 -4
  219. package/src/Model/Repository.ts +1 -0
  220. package/src/Model/dsl.ts +3 -3
  221. package/src/Model/filter/types/path/eager.ts +1 -2
  222. package/src/Model/query/dsl.ts +348 -18
  223. package/src/Model/query/new-kid-interpreter.ts +206 -6
  224. package/src/Model.ts +1 -0
  225. package/src/QueueMaker/SQLQueue.ts +144 -152
  226. package/src/QueueMaker/memQueue.ts +104 -103
  227. package/src/QueueMaker/sbqueue.ts +70 -86
  228. package/src/RequestContext.ts +14 -16
  229. package/src/RequestFiberSet.ts +2 -2
  230. package/src/Store/ContextMapContainer.ts +41 -2
  231. package/src/Store/Cosmos/query.ts +140 -43
  232. package/src/Store/Cosmos.ts +482 -349
  233. package/src/Store/Disk.ts +102 -65
  234. package/src/Store/Memory.ts +275 -87
  235. package/src/Store/SQL/Pg.ts +361 -0
  236. package/src/Store/SQL/query.ts +539 -0
  237. package/src/Store/SQL.ts +731 -0
  238. package/src/Store/codeFilter.ts +3 -1
  239. package/src/Store/index.ts +17 -2
  240. package/src/Store/service.ts +41 -10
  241. package/src/Store/utils.ts +23 -22
  242. package/src/adapters/SQL/Model.ts +41 -40
  243. package/src/adapters/ServiceBus.ts +125 -121
  244. package/src/adapters/cosmos-client.ts +2 -2
  245. package/src/adapters/index.ts +7 -0
  246. package/src/adapters/memQueue.ts +2 -2
  247. package/src/adapters/mongo-client.ts +2 -2
  248. package/src/adapters/redis-client.ts +2 -2
  249. package/src/api/ContextProvider.ts +12 -13
  250. package/src/api/internal/RequestContextMiddleware.ts +15 -5
  251. package/src/api/internal/auth.ts +246 -44
  252. package/src/api/internal/events.ts +13 -9
  253. package/src/api/layerUtils.ts +8 -8
  254. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  255. package/src/api/routing/middleware/middleware.ts +55 -14
  256. package/src/api/routing/middleware.ts +0 -2
  257. package/src/api/routing.ts +296 -131
  258. package/src/api/setupRequest.ts +28 -8
  259. package/src/arbs.ts +4 -2
  260. package/src/errorReporter.ts +62 -74
  261. package/src/logger/shared.ts +1 -1
  262. package/src/otel.ts +152 -0
  263. package/src/rateLimit.ts +30 -22
  264. package/src/test.ts +1 -1
  265. package/test/auth.test.ts +101 -0
  266. package/test/contextProvider.test.ts +11 -11
  267. package/test/controller.test.ts +21 -30
  268. package/test/dist/auth.test.d.ts.map +1 -0
  269. package/test/dist/contextProvider.test.d.ts.map +1 -1
  270. package/test/dist/controller.test.d.ts.map +1 -1
  271. package/test/dist/date-query.test.d.ts.map +1 -0
  272. package/test/dist/fixtures.d.ts +26 -12
  273. package/test/dist/fixtures.d.ts.map +1 -1
  274. package/test/dist/fixtures.js +12 -10
  275. package/test/dist/query.test.d.ts.map +1 -1
  276. package/test/dist/rawQuery.test.d.ts.map +1 -1
  277. package/test/dist/repository-ext.test.d.ts.map +1 -0
  278. package/test/dist/requires.test.d.ts.map +1 -1
  279. package/test/dist/router-generator.test.d.ts.map +1 -0
  280. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  281. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  282. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  283. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  284. package/test/dist/sql-store.test.d.ts.map +1 -0
  285. package/test/fixtures.ts +11 -9
  286. package/test/query.test.ts +813 -38
  287. package/test/rawQuery.test.ts +301 -20
  288. package/test/repository-ext.test.ts +60 -0
  289. package/test/requires.test.ts +6 -6
  290. package/test/router-generator.test.ts +183 -0
  291. package/test/routing-interruptibility.test.ts +63 -0
  292. package/test/rpc-e2e-invalidation.test.ts +251 -0
  293. package/test/rpc-multi-middleware.test.ts +78 -9
  294. package/test/rpc-stream-fullstack.test.ts +300 -0
  295. package/test/sql-store.test.ts +1592 -0
  296. package/test/validateSample.test.ts +15 -12
  297. package/tsconfig.examples.json +1 -1
  298. package/tsconfig.json +0 -1
  299. package/tsconfig.json.bak +2 -2
  300. package/tsconfig.src.json +35 -35
  301. package/tsconfig.test.json +2 -2
  302. package/dist/Operations.d.ts +0 -55
  303. package/dist/Operations.d.ts.map +0 -1
  304. package/dist/Operations.js +0 -102
  305. package/dist/OperationsRepo.d.ts +0 -41
  306. package/dist/OperationsRepo.d.ts.map +0 -1
  307. package/dist/OperationsRepo.js +0 -14
  308. package/eslint.config.mjs +0 -24
  309. package/src/Operations.ts +0 -235
  310. package/src/OperationsRepo.ts +0 -16
@@ -1,42 +1,244 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- /* eslint-disable unused-imports/no-unused-vars */
3
- import { Data, Effect } from "effect-app"
1
+ import { Data, Effect, Option } from "effect-app"
4
2
  import { HttpHeaders, HttpMiddleware, HttpServerRequest, HttpServerResponse } from "effect-app/http"
5
- import { auth, InsufficientScopeError, InvalidRequestError, InvalidTokenError, UnauthorizedError } from "express-oauth2-jwt-bearer"
3
+ import { createRemoteJWKSet, jwtVerify } from "jose"
6
4
 
7
- // // Authorization middleware. When used, the Access Token must
8
- // // exist and be verified against the Auth0 JSON Web Key Set.
5
+ const getHeaders = (error: string, description: string, scopes?: ReadonlyArray<string>) => ({
6
+ "WWW-Authenticate": `Bearer realm="api", error="${error}", error_description="${description.replace(/"/g, "'")}"${
7
+ scopes ? `, scope="${scopes.join(" ")}"` : ""
8
+ }`
9
+ })
9
10
 
10
- // type Errors = InsufficientScopeError | InvalidRequestError | InvalidTokenError | UnauthorizedError
11
- type Config = Parameters<typeof auth>[0]
12
- export const checkJWTI = (config: Config) => {
13
- const mw = auth(config)
14
- return Effect.fnUntraced(function*(headers: HttpHeaders.Headers) {
15
- return yield* Effect.callback<
16
- void,
17
- InsufficientScopeError | InvalidRequestError | InvalidTokenError | UnauthorizedError
18
- >(
19
- (resume) => {
20
- const next = (err?: unknown) => {
21
- if (!err) return resume(Effect.void)
22
- if (
23
- err instanceof InsufficientScopeError
24
- || err instanceof InvalidRequestError
25
- || err instanceof InvalidTokenError
26
- || err instanceof UnauthorizedError
27
- ) {
28
- return resume(Effect.fail(err))
11
+ export class UnauthorizedError extends Error {
12
+ readonly status: number = 401
13
+ readonly statusCode: number = 401
14
+ headers = { "WWW-Authenticate": "Bearer realm=\"api\"" }
15
+
16
+ constructor(message = "Unauthorized") {
17
+ super(message)
18
+ this.name = this.constructor.name
19
+ }
20
+ }
21
+
22
+ export class InvalidRequestError extends UnauthorizedError {
23
+ readonly code: string
24
+ override readonly status = 400
25
+ override readonly statusCode = 400
26
+
27
+ constructor(message = "Invalid Request", useErrorCode = true) {
28
+ super(message)
29
+ this.code = useErrorCode ? "invalid_request" : ""
30
+ if (useErrorCode) {
31
+ this.headers = getHeaders(this.code, this.message)
32
+ }
33
+ }
34
+ }
35
+
36
+ export class InvalidTokenError extends UnauthorizedError {
37
+ readonly code = "invalid_token"
38
+
39
+ constructor(message = "Invalid Token") {
40
+ super(message)
41
+ this.headers = getHeaders(this.code, this.message)
42
+ }
43
+ }
44
+
45
+ export class InsufficientScopeError extends UnauthorizedError {
46
+ readonly code = "insufficient_scope"
47
+ override readonly status = 403
48
+ override readonly statusCode = 403
49
+
50
+ constructor(scopes?: ReadonlyArray<string>, message = "Insufficient Scope") {
51
+ super(message)
52
+ this.headers = getHeaders(this.code, this.message, scopes)
53
+ }
54
+ }
55
+
56
+ export interface JwtVerifierOptions {
57
+ readonly audience?: string | Array<string> | ReadonlyArray<string>
58
+ readonly clockTolerance?: number
59
+ readonly issuer?: string
60
+ readonly issuerBaseURL?: string
61
+ readonly jwksUri?: string
62
+ readonly maxTokenAge?: number
63
+ readonly secret?: string
64
+ readonly strict?: boolean
65
+ readonly tokenSigningAlg?: string
66
+ }
67
+
68
+ export interface AuthOptions extends JwtVerifierOptions {
69
+ readonly authRequired?: boolean
70
+ }
71
+
72
+ type Config = AuthOptions
73
+
74
+ type JwtError = InsufficientScopeError | InvalidRequestError | InvalidTokenError | UnauthorizedError
75
+
76
+ type ResolvedConfigBase = {
77
+ readonly audience: string | Array<string> | undefined
78
+ readonly clockTolerance: number
79
+ readonly issuer: string | undefined
80
+ readonly maxTokenAge: number | undefined
81
+ readonly strict: boolean
82
+ readonly tokenSigningAlg: string | undefined
83
+ }
84
+
85
+ type ResolvedConfig =
86
+ & ResolvedConfigBase
87
+ & (
88
+ | {
89
+ readonly key: ReturnType<typeof createRemoteJWKSet>
90
+ readonly keyType: "jwks"
91
+ }
92
+ | {
93
+ readonly key: Uint8Array
94
+ readonly keyType: "secret"
95
+ }
96
+ )
97
+
98
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null
99
+
100
+ const getErrorMessage = (error: unknown) => error instanceof Error ? error.message : String(error)
101
+
102
+ const normalizeAudience = (audience: Config["audience"]): string | Array<string> | undefined =>
103
+ Array.isArray(audience) ? Array.from(audience) : audience as string | undefined
104
+
105
+ const buildDiscoveryUrl = (issuerBaseURL: string) => {
106
+ const url = new URL(issuerBaseURL)
107
+ if (!url.pathname.includes("/.well-known/")) {
108
+ url.pathname = url.pathname.endsWith("/")
109
+ ? `${url.pathname}.well-known/openid-configuration`
110
+ : `${url.pathname}/.well-known/openid-configuration`
111
+ }
112
+ url.search = ""
113
+ url.hash = ""
114
+ return url
115
+ }
116
+
117
+ const fetchDiscoveryDocumentPromise = async (issuerBaseURL: string) => {
118
+ const response = await fetch(buildDiscoveryUrl(issuerBaseURL))
119
+ if (!response.ok) {
120
+ throw new Error(`Failed to fetch authorization server metadata: ${response.status}`)
121
+ }
122
+ const json = await response.json()
123
+ if (!isRecord(json) || typeof json["issuer"] !== "string" || typeof json["jwks_uri"] !== "string") {
124
+ throw new Error("Invalid authorization server metadata")
125
+ }
126
+ return { issuer: json["issuer"], jwksUri: json["jwks_uri"] }
127
+ }
128
+
129
+ const getAuthorizationToken = (headers: HttpHeaders.Headers, authRequired: boolean) => {
130
+ const authorization = HttpHeaders.get(headers, "authorization")
131
+ if (Option.isNone(authorization)) {
132
+ return authRequired ? Effect.fail(new UnauthorizedError()) : Effect.succeed(Option.none<string>())
133
+ }
134
+
135
+ const [scheme, token] = authorization.value.split(" ")
136
+ if (!scheme || !token || scheme.toLowerCase() !== "bearer") {
137
+ return Effect.fail(new InvalidRequestError("", false))
138
+ }
139
+
140
+ return Effect.succeed(Option.some(token))
141
+ }
142
+
143
+ const makeResolveConfig = (config: Config) => {
144
+ let cached: Promise<ResolvedConfig> | undefined
145
+
146
+ return Effect.tryPromise({
147
+ try: () => {
148
+ if (!cached) {
149
+ cached = (async (): Promise<ResolvedConfig> => {
150
+ const discovery = config.issuerBaseURL
151
+ ? await fetchDiscoveryDocumentPromise(config.issuerBaseURL)
152
+ : undefined
153
+
154
+ const issuer = config.issuer ?? discovery?.issuer
155
+ const jwksUri = config.jwksUri ?? discovery?.jwksUri
156
+ const secret = config.secret
157
+ const base = {
158
+ audience: normalizeAudience(config.audience),
159
+ clockTolerance: config.clockTolerance ?? 5,
160
+ issuer,
161
+ maxTokenAge: config.maxTokenAge,
162
+ strict: config.strict ?? false,
163
+ tokenSigningAlg: config.tokenSigningAlg
164
+ } satisfies ResolvedConfigBase
165
+
166
+ if (!issuer && !secret) {
167
+ throw new InvalidRequestError("JWT config requires 'issuer', 'issuerBaseURL', or 'secret'")
29
168
  }
30
- return resume(Effect.die(err))
31
- }
32
- const r = { headers, query: {}, body: {}, is: () => false, method: "POST" } // is("urlencoded")
33
- try {
34
- mw(r as any, {} as any, next)
35
- } catch (e) {
36
- return resume(Effect.die(e))
37
- }
169
+
170
+ if (!secret) {
171
+ if (!jwksUri) {
172
+ throw new InvalidRequestError("JWT config requires 'jwksUri', 'issuerBaseURL', or 'secret'")
173
+ }
174
+
175
+ return {
176
+ ...base,
177
+ key: createRemoteJWKSet(new URL(jwksUri)),
178
+ keyType: "jwks"
179
+ }
180
+ }
181
+
182
+ return {
183
+ ...base,
184
+ key: new TextEncoder().encode(secret),
185
+ keyType: "secret"
186
+ }
187
+ })()
38
188
  }
189
+
190
+ return cached
191
+ },
192
+ catch: (error) =>
193
+ error instanceof InvalidRequestError || error instanceof InvalidTokenError
194
+ ? error
195
+ : new InvalidTokenError(getErrorMessage(error))
196
+ })
197
+ }
198
+
199
+ const verifyToken =
200
+ (resolveConfig: Effect.Effect<ResolvedConfig, InvalidRequestError | InvalidTokenError>) => (token: string) =>
201
+ resolveConfig.pipe(
202
+ Effect.flatMap((config) => {
203
+ const options = {
204
+ clockTolerance: config.clockTolerance,
205
+ ...(config.tokenSigningAlg ? { algorithms: [config.tokenSigningAlg] } : {}),
206
+ ...(config.audience !== undefined ? { audience: config.audience } : {}),
207
+ ...(config.issuer !== undefined ? { issuer: config.issuer } : {}),
208
+ ...(config.maxTokenAge !== undefined ? { maxTokenAge: config.maxTokenAge } : {})
209
+ }
210
+ const verified = config.keyType === "jwks"
211
+ ? Effect.tryPromise({
212
+ try: () => jwtVerify(token, config.key, options).then(({ protectedHeader }) => ({ protectedHeader })),
213
+ catch: (error) => new InvalidTokenError(getErrorMessage(error))
214
+ })
215
+ : Effect.tryPromise({
216
+ try: () => jwtVerify(token, config.key, options).then(({ protectedHeader }) => ({ protectedHeader })),
217
+ catch: (error) => new InvalidTokenError(getErrorMessage(error))
218
+ })
219
+
220
+ return verified.pipe(
221
+ Effect.flatMap(({ protectedHeader }) => {
222
+ const typ = protectedHeader.typ?.toLowerCase().replace(/^application\//, "")
223
+ return config.strict && typ !== "at+jwt"
224
+ ? Effect.fail(new InvalidTokenError("Unexpected 'typ' value"))
225
+ : Effect.void
226
+ })
227
+ )
228
+ })
39
229
  )
230
+
231
+ export const checkJWTI = (config: Config) => {
232
+ const resolveConfig = makeResolveConfig(config)
233
+ const verify = verifyToken(resolveConfig)
234
+
235
+ return Effect.fnUntraced(function*(headers: HttpHeaders.Headers) {
236
+ const token = yield* getAuthorizationToken(headers, config.authRequired !== false)
237
+ if (Option.isNone(token)) {
238
+ return
239
+ }
240
+
241
+ yield* verify(token.value)
40
242
  })
41
243
  }
42
244
 
@@ -45,24 +247,24 @@ export const checkJwt = (config: Config) => {
45
247
  return HttpMiddleware.make((app) =>
46
248
  Effect.gen(function*() {
47
249
  const req = yield* HttpServerRequest.HttpServerRequest
48
- const response = yield* check(req.headers).pipe(Effect.catch((e) =>
49
- HttpServerResponse.json({ message: e.message }, {
50
- status: e.status,
51
- headers: HttpHeaders.fromInput(e.headers)
52
- })
53
- ))
250
+ const response = yield* check(req.headers).pipe(
251
+ Effect.catch((error: JwtError) =>
252
+ HttpServerResponse.json({ message: error.message }, {
253
+ status: error.status,
254
+ headers: HttpHeaders.fromInput(error.headers)
255
+ })
256
+ )
257
+ )
258
+
54
259
  if (response) {
55
260
  return response
56
261
  }
262
+
57
263
  return yield* app
58
264
  })
59
265
  )
60
266
  }
61
267
 
62
268
  export class JWTError extends Data.TaggedClass("JWTError")<{
63
- error:
64
- | InsufficientScopeError
65
- | InvalidRequestError
66
- | InvalidTokenError
67
- | UnauthorizedError
269
+ error: JwtError
68
270
  }> {}
@@ -1,6 +1,7 @@
1
1
  import { Duration, Effect, pipe, S, Schedule, Stream } from "effect-app"
2
2
  import { HttpHeaders, HttpServerResponse } from "effect-app/http"
3
3
  import { reportError } from "../../errorReporter.js"
4
+ import { storeId } from "../../Store/Memory.js"
4
5
  import { setupRequestContextFromCurrent } from "../setupRequest.js"
5
6
 
6
7
  // Tell the client to retry every 10 seconds if connectivity is lost
@@ -9,29 +10,32 @@ const keepAlive = Stream.fromEffectSchedule(Effect.succeed(":keep-alive"), Sched
9
10
 
10
11
  let connId = BigInt(0)
11
12
 
12
- export const makeSSE = <A extends { id: any }, SI, SR>(
13
- schema: S.Codec<A, SI, SR>
13
+ export const makeSSE = <A extends { id: any }, SI, SRD, SRE>(
14
+ schema: S.Codec<A, SI, SRD, SRE>
14
15
  ) =>
15
16
  <E, R>(events: Stream.Stream<{ evt: A; namespace: string }, E, R>) =>
16
17
  Effect
17
18
  .gen(function*() {
18
19
  const id = connId++
19
- const ctx = yield* Effect.services<R | SR>()
20
+ const ctx = yield* Effect.context<R | SRD | SRE>()
20
21
  const res = HttpServerResponse.stream(
21
22
  // workaround for different scoped behaviour for streams in Bun
22
23
  // https://discord.com/channels/795981131316985866/1098177242598756412/1389646879675125861
23
24
  Effect
24
25
  .gen(function*() {
25
- yield* Effect.annotateCurrentSpan({ connectionId: id.toString() })
26
- yield* Effect.logInfo("$ start listening to events, id: " + id.toString())
27
- yield* Effect.addFinalizer(() => Effect.logInfo("$ end listening to events, id: " + id.toString()))
26
+ const ns = yield* storeId
27
+ yield* Effect.annotateCurrentSpan({ "network.connection.id": id.toString() })
28
+ yield* Effect.logInfo("$ start listening to events, id: " + id.toString() + ", ns: " + ns)
29
+ yield* Effect.addFinalizer(() =>
30
+ Effect.logInfo("$ end listening to events, id: " + id.toString() + ", ns: " + ns)
31
+ )
28
32
 
29
33
  const enc = new TextEncoder()
30
34
 
31
- const encode = S.encodeEffect(S.fromJsonString(schema))
35
+ const encode = S.encodeEffect(S.fromJsonString(S.toCodecJson(schema)))
32
36
 
33
37
  const eventStream = Stream.mapEffect(
34
- events,
38
+ Stream.filter(events, (_) => _.namespace === ns),
35
39
  (_) =>
36
40
  encode(_.evt)
37
41
  .pipe(Effect.map((data) => `id: ${_.evt.id}\ndata: ${data}`))
@@ -42,7 +46,7 @@ export const makeSSE = <A extends { id: any }, SI, SR>(
42
46
  Stream.merge(keepAlive),
43
47
  // Keep this unary so pipe receives a function, not a Stream value.
44
48
  (self) => Stream.merge(self, eventStream, { haltStrategy: "either" }),
45
- Stream.tapCause((cause) => Effect.logError("SSE error", cause)),
49
+ Stream.tapCause((cause) => Effect.logError("SSE error, id: " + id.toString() + ", ns: " + ns, cause)),
46
50
  Stream.map((_) => enc.encode(_ + "\n\n"))
47
51
  )
48
52
 
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { Effect, type Layer, type NonEmptyReadonlyArray, Option, ServiceMap } from "effect-app"
2
+ import { Context, Effect, type Layer, type NonEmptyReadonlyArray, Option } from "effect-app"
3
3
  import { InfraLogger } from "../logger.js"
4
4
 
5
5
  // TODO: These LayerUtils are flaky, like in dependencies as a readonly array, it breaks when there are two entries
@@ -27,7 +27,7 @@ export namespace LayerUtils {
27
27
  }
28
28
 
29
29
  export type ContextTagWithDefault<Id, A, LayerE, LayerR> =
30
- & ServiceMap.Service<Id, A>
30
+ & Context.Service<Id, A>
31
31
  & {
32
32
  Default: Layer.Layer<Id, LayerE, LayerR>
33
33
  }
@@ -36,29 +36,29 @@ export namespace ContextTagWithDefault {
36
36
  export type Base<A> = ContextTagWithDefault<any, A, any, any>
37
37
  }
38
38
 
39
- export type GetContext<T> = T extends ServiceMap.ServiceMap<infer Y> ? Y : never
39
+ export type GetContext<T> = T extends Context.Context<infer Y> ? Y : never
40
40
 
41
41
  export const mergeContexts = Effect.fnUntraced(
42
42
  function*<
43
43
  T extends readonly {
44
44
  maker: any
45
- handle: Effect.Effect<ServiceMap.ServiceMap<any> | Option.Option<ServiceMap.ServiceMap<any>>>
45
+ handle: Effect.Effect<Context.Context<any> | Option.Option<Context.Context<any>>>
46
46
  }[]
47
47
  >(
48
48
  makers: T
49
49
  ) {
50
- let context = ServiceMap.empty()
50
+ let context = Context.empty()
51
51
  for (const mw of makers) {
52
52
  const ctx = yield* mw.handle.pipe(Effect.provide(context))
53
- const moreContext = ServiceMap.isServiceMap(ctx) ? Option.some(ctx) : ctx
53
+ const moreContext = Context.isContext(ctx) ? Option.some(ctx) : ctx
54
54
  yield* InfraLogger.logDebug(
55
55
  "Built dynamic context for middleware" + (mw.maker.key ?? mw.maker),
56
56
  Option.map(moreContext, (c) => (c as any).toJSON().services)
57
57
  )
58
58
  if (moreContext.value) {
59
- context = ServiceMap.merge(context, moreContext.value)
59
+ context = Context.merge(context, moreContext.value)
60
60
  }
61
61
  }
62
- return context as ServiceMap.ServiceMap<Effect.Success<T[number]["handle"]>>
62
+ return context as Context.Context<Effect.Success<T[number]["handle"]>>
63
63
  }
64
64
  )
@@ -1,13 +1,13 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-return */
3
3
  /* eslint-disable @typescript-eslint/no-explicit-any */
4
- import { type Layer, type ServiceMap } from "effect-app"
4
+ import { type Context, type Layer } from "effect-app"
5
5
  import { type GetContextConfig, type RpcContextMap } from "effect-app/rpc/RpcContextMap"
6
6
  import { type RpcMiddlewareV4 } from "effect-app/rpc/RpcMiddleware"
7
7
  // module:
8
8
  //
9
9
 
10
- // v4: middleware tags are ServiceMap.Service (not Effect) — they carry the RpcMiddlewareV4 as their service Shape
10
+ // v4: middleware tags are Context.Service (not Effect) — they carry the RpcMiddlewareV4 as their service Shape
11
11
  export type RouterMiddleware<
12
12
  Self,
13
13
  RequestContextMap extends Record<string, RpcContextMap.Any>, // what services will the middlware provide dynamically to the next, or raise errors.
@@ -18,9 +18,9 @@ export type RouterMiddleware<
18
18
  _ContextProviderR, // what the context provider requires
19
19
  RequestContextId
20
20
  > =
21
- & ServiceMap.Service<Self, RpcMiddlewareV4<ContextProviderA, ContextProviderE, never>>
21
+ & Context.Service<Self, RpcMiddlewareV4<ContextProviderA, ContextProviderE, never>>
22
22
  & {
23
23
  readonly Default: Layer.Layer<Self, MakeMiddlewareE, MakeMiddlewareR>
24
- readonly requestContext: ServiceMap.Service<RequestContextId, GetContextConfig<RequestContextMap>>
24
+ readonly requestContext: Context.Service<RequestContextId, GetContextConfig<RequestContextMap>>
25
25
  readonly requestContextMap: RequestContextMap
26
26
  }
@@ -1,10 +1,14 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { Cause, Config, Effect, Layer, Schema } from "effect"
3
3
  import { ConfigureInterruptibilityMiddleware, DevMode, DevModeMiddleware, LoggerMiddleware, RequestCacheMiddleware } from "effect-app/middleware"
4
+ import { RpcContextMap, type RpcMiddleware } from "effect-app/rpc"
4
5
  import { pretty } from "effect-app/utils"
6
+ import * as Array from "effect/Array"
7
+ import * as Context from "effect/Context"
8
+ import { type Rpc } from "effect/unstable/rpc"
5
9
  import { logError, reportError } from "../../../errorReporter.js"
6
10
  import { InfraLogger } from "../../../logger.js"
7
- import { determineMethod, isCommand } from "../utils.js"
11
+ import { WithNsTransaction } from "../../../Store/SQL.js"
8
12
 
9
13
  const logRequestError = logError("Request")
10
14
  const reportRequestError = reportError("Request")
@@ -26,22 +30,20 @@ export const RequestCacheMiddlewareLive = Layer.succeed(
26
30
  const isOptimisticConcurrencyException = (input: unknown) =>
27
31
  typeof input === "object" && input !== null && "_tag" in input && input._tag === "OptimisticConcurrencyException"
28
32
 
33
+ export const RequestType = Context.Reference<"command" | "query">(
34
+ "@effect-app/infra/api/routing/RequestType",
35
+ { defaultValue: () => "query" }
36
+ )
37
+
29
38
  export const ConfigureInterruptibilityMiddlewareLive = Layer.effect(
30
39
  ConfigureInterruptibilityMiddleware,
31
40
  Effect.gen(function*() {
32
- const cache = new Map()
33
- const getCached = (key: string, schema: Schema.Top) => {
34
- const existing = cache.get(key)
35
- if (existing) return existing
36
- const n = determineMethod(key, schema)
37
- cache.set(key, n)
38
- return n
39
- }
40
41
  return (effect, { rpc }) => {
41
- const method = getCached(rpc._tag, rpc.payloadSchema)
42
+ const requestType = Context.get(rpc.annotations, RequestType)
43
+ const isCommand = requestType === "command"
42
44
 
43
- effect = isCommand(method)
44
- ? Effect.retry(Effect.uninterruptible(effect), { times: 1, while: isOptimisticConcurrencyException })
45
+ effect = isCommand
46
+ ? Effect.retry(effect, { times: 1, while: isOptimisticConcurrencyException })
45
47
  : Effect.interruptible(effect)
46
48
 
47
49
  return effect
@@ -57,8 +59,8 @@ export const LoggerMiddlewareLive = Layer
57
59
  return (effect, { headers, payload, rpc }) =>
58
60
  Effect
59
61
  .annotateCurrentSpan({
60
- "request.name": rpc._tag,
61
- "requestInput": typeof payload === "object" && payload !== null
62
+ "rpc.method": rpc._tag,
63
+ "rpc.request.payload": typeof payload === "object" && payload !== null
62
64
  ? Object.entries(payload).reduce((prev, [key, value]: [string, unknown]) => {
63
65
  prev[key] = key === "password"
64
66
  ? "<redacted>"
@@ -126,3 +128,42 @@ export const DefaultGenericMiddlewaresLive = Layer.mergeAll(
126
128
  LoggerMiddlewareLive,
127
129
  DevModeMiddlewareLive
128
130
  )
131
+
132
+ /**
133
+ * Config entry for `RequestContextMap` that controls per-RPC transaction wrapping.
134
+ * Defaults to `false` (no transaction). Set `requiresTransaction: true` on a route to enable.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * class RequestContextMap extends RpcContextMap.makeMap({
139
+ * requiresTransaction: requiresTransactionConfig,
140
+ * // ...
141
+ * }) {}
142
+ * ```
143
+ */
144
+ export const requiresTransactionConfig = RpcContextMap.makeCustom()(Schema.Never, false)
145
+
146
+ /**
147
+ * Creates the middleware Effect for SQL transaction wrapping.
148
+ * Requires `WithNsTransaction` service.
149
+ * Reads `requiresTransaction` from the RPC config; defaults to `false`.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * const SqlTransactionMiddlewareLive = Layer.effect(
154
+ * SqlTransactionMiddleware,
155
+ * makeSqlTransactionMiddleware(RequestContextMap)
156
+ * )
157
+ * ```
158
+ */
159
+ export const makeSqlTransactionMiddleware = Effect.fnUntraced(function*(
160
+ rcm: { getConfig: (rpc: Rpc.AnyWithProps) => { readonly requiresTransaction?: boolean } }
161
+ ) {
162
+ const withTx = yield* WithNsTransaction
163
+ const mw: RpcMiddleware.RpcMiddlewareV4<never, never, never> = (effect, { rpc }) => {
164
+ const { requiresTransaction } = rcm.getConfig(rpc)
165
+ if (requiresTransaction !== true) return effect
166
+ return withTx(effect)
167
+ }
168
+ return mw
169
+ })
@@ -2,5 +2,3 @@
2
2
  export * from "./middleware/middleware.js"
3
3
  export * from "./middleware/RouterMiddleware.js"
4
4
  // codegen:end
5
-
6
- export * as Middleware from "./middleware.js"