@owlmeans/server-api 0.1.18-rc.8 → 0.1.18

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 (140) hide show
  1. package/README.md +203 -46
  2. package/agent-meta/manifest.json +3 -3
  3. package/agent-meta/skills/server-api/SKILL.md +142 -22
  4. package/build/consts.local.d.ts +2 -0
  5. package/build/consts.local.d.ts.map +1 -0
  6. package/build/consts.local.js +2 -0
  7. package/build/consts.local.js.map +1 -0
  8. package/build/errors.d.ts +12 -0
  9. package/build/errors.d.ts.map +1 -1
  10. package/build/errors.js +16 -0
  11. package/build/errors.js.map +1 -1
  12. package/build/helper.d.ts +3 -6
  13. package/build/helper.d.ts.map +1 -1
  14. package/build/helper.js +10 -1
  15. package/build/helper.js.map +1 -1
  16. package/build/hold.d.ts +35 -0
  17. package/build/hold.d.ts.map +1 -0
  18. package/build/hold.js +57 -0
  19. package/build/hold.js.map +1 -0
  20. package/build/index.d.ts +2 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +2 -0
  23. package/build/index.js.map +1 -1
  24. package/build/protocol.d.ts +18 -0
  25. package/build/protocol.d.ts.map +1 -0
  26. package/build/protocol.js +87 -0
  27. package/build/protocol.js.map +1 -0
  28. package/build/server.d.ts +2 -3
  29. package/build/server.d.ts.map +1 -1
  30. package/build/server.js +42 -34
  31. package/build/server.js.map +1 -1
  32. package/build/types.d.ts +31 -2
  33. package/build/types.d.ts.map +1 -1
  34. package/build/types.local.d.ts +38 -0
  35. package/build/types.local.d.ts.map +1 -0
  36. package/build/types.local.js +2 -0
  37. package/build/types.local.js.map +1 -0
  38. package/build/utils/consts.d.ts +2 -0
  39. package/build/utils/consts.d.ts.map +1 -0
  40. package/build/utils/consts.js +2 -0
  41. package/build/utils/consts.js.map +1 -0
  42. package/build/utils/consts.local.d.ts +5 -0
  43. package/build/utils/consts.local.d.ts.map +1 -0
  44. package/build/utils/consts.local.js +5 -0
  45. package/build/utils/consts.local.js.map +1 -0
  46. package/build/utils/context/types.d.ts +13 -0
  47. package/build/utils/context/types.d.ts.map +1 -0
  48. package/build/utils/context/types.js +2 -0
  49. package/build/utils/context/types.js.map +1 -0
  50. package/build/utils/context.d.ts +3 -3
  51. package/build/utils/context.d.ts.map +1 -1
  52. package/build/utils/context.js +5 -2
  53. package/build/utils/context.js.map +1 -1
  54. package/build/utils/error/types.d.ts +52 -0
  55. package/build/utils/error/types.d.ts.map +1 -0
  56. package/build/utils/error/types.js +2 -0
  57. package/build/utils/error/types.js.map +1 -0
  58. package/build/utils/error.d.ts +5 -15
  59. package/build/utils/error.d.ts.map +1 -1
  60. package/build/utils/error.js +134 -29
  61. package/build/utils/error.js.map +1 -1
  62. package/build/utils/guards.d.ts +1 -5
  63. package/build/utils/guards.d.ts.map +1 -1
  64. package/build/utils/guards.js +15 -19
  65. package/build/utils/guards.js.map +1 -1
  66. package/build/utils/index.d.ts +6 -1
  67. package/build/utils/index.d.ts.map +1 -1
  68. package/build/utils/index.js +2 -1
  69. package/build/utils/index.js.map +1 -1
  70. package/build/utils/log/consts.d.ts +14 -0
  71. package/build/utils/log/consts.d.ts.map +1 -0
  72. package/build/utils/log/consts.js +17 -0
  73. package/build/utils/log/consts.js.map +1 -0
  74. package/build/utils/log/types.d.ts +42 -0
  75. package/build/utils/log/types.d.ts.map +1 -0
  76. package/build/utils/log/types.js +2 -0
  77. package/build/utils/log/types.js.map +1 -0
  78. package/build/utils/log.d.ts +4 -0
  79. package/build/utils/log.d.ts.map +1 -0
  80. package/build/utils/log.js +170 -0
  81. package/build/utils/log.js.map +1 -0
  82. package/build/utils/payload/types.d.ts +18 -0
  83. package/build/utils/payload/types.d.ts.map +1 -0
  84. package/build/utils/payload/types.js +2 -0
  85. package/build/utils/payload/types.js.map +1 -0
  86. package/build/utils/payload.d.ts +3 -15
  87. package/build/utils/payload.d.ts.map +1 -1
  88. package/build/utils/payload.js +41 -57
  89. package/build/utils/payload.js.map +1 -1
  90. package/build/utils/schema.d.ts +7 -0
  91. package/build/utils/schema.d.ts.map +1 -0
  92. package/build/utils/schema.js +20 -0
  93. package/build/utils/schema.js.map +1 -0
  94. package/build/utils/server.d.ts +3 -6
  95. package/build/utils/server.d.ts.map +1 -1
  96. package/build/utils/server.js +12 -13
  97. package/build/utils/server.js.map +1 -1
  98. package/build/utils/types.d.ts +18 -0
  99. package/build/utils/types.d.ts.map +1 -0
  100. package/build/utils/types.js +2 -0
  101. package/build/utils/types.js.map +1 -0
  102. package/build/utils/types.local.d.ts +9 -0
  103. package/build/utils/types.local.d.ts.map +1 -0
  104. package/build/utils/types.local.js +2 -0
  105. package/build/utils/types.local.js.map +1 -0
  106. package/package.json +23 -19
  107. package/src/consts.local.ts +1 -0
  108. package/src/errors.ts +18 -0
  109. package/src/helper.ts +13 -8
  110. package/src/hold.ts +66 -0
  111. package/src/index.ts +2 -0
  112. package/src/protocol.ts +149 -0
  113. package/src/server.ts +72 -69
  114. package/src/types.local.ts +44 -0
  115. package/src/types.ts +38 -4
  116. package/src/utils/consts.local.ts +7 -0
  117. package/src/utils/consts.ts +1 -0
  118. package/src/utils/context/types.ts +13 -0
  119. package/src/utils/context.ts +9 -4
  120. package/src/utils/error/types.ts +52 -0
  121. package/src/utils/error.ts +156 -29
  122. package/src/utils/guards.ts +16 -26
  123. package/src/utils/index.ts +6 -1
  124. package/src/utils/log/consts.ts +18 -0
  125. package/src/utils/log/types.ts +43 -0
  126. package/src/utils/log.ts +193 -0
  127. package/src/utils/payload/types.ts +18 -0
  128. package/src/utils/payload.ts +42 -60
  129. package/src/utils/schema.ts +23 -0
  130. package/src/utils/server.ts +16 -21
  131. package/src/utils/types.local.ts +12 -0
  132. package/src/utils/types.ts +18 -0
  133. package/tests/error-status.spec.ts +349 -0
  134. package/tests/handlers.spec.ts +83 -0
  135. package/tests/json-body.spec.ts +72 -0
  136. package/tests/log-adapter.spec.ts +62 -0
  137. package/tests/log-redaction.spec.ts +181 -0
  138. package/tests/tsconfig.json +12 -0
  139. package/tsconfig.json +1 -0
  140. package/build/.gitkeep +0 -0
@@ -0,0 +1,193 @@
1
+ import { LogController } from 'fastify'
2
+ import type { FastifyBaseLogger, FastifyReply, FastifyRequest } from 'fastify'
3
+ import { FORBIDDEN_ERROR, SERVER_ERROR, UNAUTHORIZED_ERROR } from '@owlmeans/api'
4
+ import { logger, type LogLevel, type Logger } from '@owlmeans/log'
5
+ import type { Severity } from './types.local.js'
6
+ import { QUERY_IN_TEXT, SAFE_DATA_DEPTH, UNSAFE_LOG_KEYS } from './log/consts.js'
7
+ import type { FastifyLogUtils, RequestSummary } from './log/types.js'
8
+
9
+ const pathOf = (url: string): string => url.split('?')[0]
10
+
11
+ const withoutQueries = (text: string): string => text.replace(QUERY_IN_TEXT, '$1')
12
+
13
+ /** The error itself, or — when its message names a URL with a query — a copy without the query. */
14
+ const safeError = (error: Error): Error => {
15
+ const message = withoutQueries(error.message)
16
+ if (message === error.message) {
17
+ return error
18
+ }
19
+ const copy: Error = Object.assign(Object.create(Object.getPrototypeOf(error) as object) as Error, error)
20
+ copy.message = message
21
+ copy.stack = error.stack != null ? withoutQueries(error.stack) : undefined
22
+ return copy
23
+ }
24
+
25
+ const isPlain = (value: object): boolean => {
26
+ const proto = Object.getPrototypeOf(value)
27
+ return proto === Object.prototype || proto === null
28
+ }
29
+
30
+ const levelName = (log: Logger): string => {
31
+ const order: LogLevel[] = ['debug', 'info', 'warn', 'error']
32
+ return order.find(level => log.enabled(level)) ?? 'silent'
33
+ }
34
+
35
+ export const createFastifyLogUtils = (): FastifyLogUtils => {
36
+ const http = logger('http')
37
+
38
+ const requestOf = (request: unknown): RequestSummary => {
39
+ const { method, url } = (request ?? {}) as { method?: unknown, url?: unknown }
40
+ return {
41
+ ...(typeof method === 'string' ? { method } : {}),
42
+ ...(typeof url === 'string' ? { path: pathOf(url) } : {}),
43
+ }
44
+ }
45
+
46
+ /** A class instance as a record may carry it: a request or reply by summary, anything else by name. */
47
+ const instanceOf = (value: object): unknown => {
48
+ const shape = value as { method?: unknown, url?: unknown, statusCode?: unknown }
49
+ if (typeof shape.method === 'string' && typeof shape.url === 'string') {
50
+ return requestOf(value)
51
+ }
52
+ if (typeof shape.statusCode === 'number') {
53
+ return { statusCode: shape.statusCode }
54
+ }
55
+ const name = value.constructor?.name ?? 'object'
56
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
57
+ return `[${name} ${value.byteLength} bytes]`
58
+ }
59
+ return `[${name}]`
60
+ }
61
+
62
+ const safe = (value: unknown, depth: number, seen: WeakSet<object>): unknown => {
63
+ if (typeof value === 'string') {
64
+ return withoutQueries(value)
65
+ }
66
+ if (value instanceof Error) {
67
+ return safeError(value)
68
+ }
69
+ if (value == null || typeof value !== 'object' || value instanceof Date) {
70
+ return value
71
+ }
72
+ if (seen.has(value)) {
73
+ return '[circular]'
74
+ }
75
+ if (!Array.isArray(value) && !isPlain(value)) {
76
+ return instanceOf(value)
77
+ }
78
+ if (depth >= SAFE_DATA_DEPTH) {
79
+ return '[…]'
80
+ }
81
+ seen.add(value)
82
+ if (Array.isArray(value)) {
83
+ return value.map(item => safe(item, depth + 1, seen))
84
+ }
85
+ const out: Record<string, unknown> = {}
86
+ for (const [key, item] of Object.entries(value)) {
87
+ // A count or a flag under such a key (`limits.files: 5`) says nothing a caller sent.
88
+ if (UNSAFE_LOG_KEYS.has(key) && typeof item !== 'number' && typeof item !== 'boolean') {
89
+ continue
90
+ }
91
+ out[key] = key === 'url' && typeof item === 'string' ? pathOf(item) : safe(item, depth + 1, seen)
92
+ }
93
+ return out
94
+ }
95
+
96
+ const safeData = (data: unknown): unknown => safe(data, 0, new WeakSet())
97
+
98
+ const failure: FastifyLogUtils['failure'] = (error, request, status, incidentId) => {
99
+ const where = { ...requestOf(request), status, ...(incidentId != null ? { incidentId } : {}) }
100
+ const safe = safeError(error)
101
+ if (status >= SERVER_ERROR) {
102
+ http.error('Request failed', { err: safe, ...where })
103
+ } else if (status === FORBIDDEN_ERROR) {
104
+ http.warn('Access forbidden', { type: (error as { type?: unknown }).type, message: safe.message, ...where },
105
+ { event: 'access.forbidden' })
106
+ } else if (status === UNAUTHORIZED_ERROR) {
107
+ http.debug('Authentication refused', { message: safe.message, ...where }, { event: 'auth.refused' })
108
+ } else {
109
+ const code = (error as { code?: unknown }).code
110
+ http.debug('Request refused', { ...(typeof code === 'string' ? { code } : {}), message: safe.message, ...where })
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Fastify announces its listening address at info; the server writes `server.listening` itself.
116
+ * Its request lines no longer reach the adapter at all — `controller()` owns them.
117
+ */
118
+ const levelOf = (level: Severity, message: unknown): Severity =>
119
+ level === 'info' && typeof message === 'string' && message.startsWith('Server listening at') ? 'debug' : level
120
+
121
+ const fastifyLogger = (log: Logger = http, bindings?: Record<string, unknown>): FastifyBaseLogger => {
122
+ const call = (requested: Severity) => (first: unknown, second?: unknown): void => {
123
+ const [object, message] = typeof first === 'string' || first instanceof Error
124
+ ? [undefined, first] : [first, second]
125
+ const level = levelOf(requested, message)
126
+ if (!log.enabled(level)) {
127
+ return
128
+ }
129
+ const data = bindings != null && Object.keys(bindings).length > 0
130
+ ? { ...bindings, ...(object != null && typeof object === 'object' ? object : {}) } : object
131
+ const text = message == null ? '' : message instanceof Error ? safeError(message) : withoutQueries(String(message))
132
+ log[level](text, safeData(data))
133
+ }
134
+
135
+ const adapter = {
136
+ get level(): string { return levelName(log) },
137
+ set level(_value: string) { /* the level belongs to the process, not to this server */ },
138
+ fatal: call('error'), error: call('error'), warn: call('warn'),
139
+ info: call('info'), debug: call('debug'), trace: call('debug'),
140
+ silent: () => undefined,
141
+ // Fastify's second argument (pino options: its own `req`/`res` serializers) is ignored —
142
+ // `safeData` is this adapter's serializer.
143
+ child: (extra: Record<string, unknown>) => fastifyLogger(log, { ...bindings, ...extra }),
144
+ }
145
+ return adapter as unknown as FastifyBaseLogger
146
+ }
147
+
148
+ /**
149
+ * Fastify accepts a `logController` only as an instance of its own class, so this one member is a
150
+ * subclass; its methods use no `this`.
151
+ */
152
+ const controller = (): LogController => {
153
+ class SafeLogController extends LogController {
154
+ override incomingRequest (): void { /* `onResponse` writes the one record of a request */ }
155
+
156
+ override requestCompleted (error: Error | null | undefined, request: FastifyRequest, reply: FastifyReply): void {
157
+ if (error != null) {
158
+ http.error('Response failed', { err: error, ...requestOf(request), status: reply.statusCode })
159
+ }
160
+ }
161
+
162
+ override defaultErrorLog (error: Error, request: FastifyRequest, reply: FastifyReply): void {
163
+ failure(error, request, reply.statusCode)
164
+ }
165
+
166
+ override streamError (error: Error, request: FastifyRequest, reply: FastifyReply): void {
167
+ const where = { ...requestOf(request), status: reply.statusCode }
168
+ if ((error as { code?: unknown }).code === 'ERR_STREAM_PREMATURE_CLOSE') {
169
+ http.debug('Response stream closed prematurely', where)
170
+ } else {
171
+ http.warn('Response stream failed after its headers were sent', { err: error, ...where })
172
+ }
173
+ }
174
+
175
+ override routeNotFound (request: FastifyRequest): void {
176
+ http.debug('Route not found', requestOf(request))
177
+ }
178
+
179
+ override writeHeadError (error: Error, request: FastifyRequest, reply: FastifyReply): void {
180
+ http.warn('Response headers not written', { err: error, ...requestOf(request), status: reply.statusCode })
181
+ }
182
+
183
+ override serializerError (error: Error, request: FastifyRequest, _reply: FastifyReply, metadata: { statusCode: number }): void {
184
+ http.error('Response serializer failed', { err: error, ...requestOf(request), status: metadata.statusCode })
185
+ }
186
+ }
187
+ return new SafeLogController()
188
+ }
189
+
190
+ return { logger: fastifyLogger, controller, safeData, requestOf, failure }
191
+ }
192
+
193
+ export const fastifyLogUtils = createFastifyLogUtils()
@@ -0,0 +1,18 @@
1
+ import type { AbstractRequest, AbstractResponse } from '@owlmeans/entrypoint'
2
+ import type { Request, Response } from '../../types.js'
3
+
4
+ /** Moves an exchange between Fastify and the entrypoint shapes: the request in, the response out. */
5
+ export interface PayloadHelper {
6
+ /** The entrypoint request of a Fastify request; `provision` keeps the original on it. */
7
+ provideRequest: (alias: string, req: Request, provision?: boolean) => AbstractRequest
8
+ /**
9
+ * Emits the response onto the fastify reply if the handler produced an
10
+ * error or an outcome. Returns `true` when something was actually sent so
11
+ * callers don't rely on `reply.sent`, which in fastify v5 only flips once
12
+ * the raw socket finishes writing (asynchronously) and therefore reads
13
+ * `false` synchronously right after `reply.send()`.
14
+ *
15
+ * @throws {Error}
16
+ */
17
+ executeResponse: <T>(response: AbstractResponse<T>, reply: Response, throwOnError?: boolean) => boolean
18
+ }
@@ -1,72 +1,54 @@
1
1
  import { EntrypointOutcome } from '@owlmeans/entrypoint'
2
2
  import type { AbstractResponse, AbstractRequest } from '@owlmeans/entrypoint'
3
3
  import type { Request, Response } from '../types.js'
4
- import { ACCEPTED, CREATED, OK, SERVER_ERROR } from '@owlmeans/api'
5
- import type { AnySchemaObject } from 'ajv'
4
+ import { ACCEPTED, CREATED, OK } from '@owlmeans/api'
5
+ import { httpErrorHelper } from './error.js'
6
+ import type { PayloadHelper } from './payload/types.js'
6
7
 
7
- export const provideRequest = (alias: string, req: Request, provision?: boolean): AbstractRequest => {
8
- provision = provision ?? false
9
- return {
10
- alias,
11
- auth: (req as any)._auth ?? undefined,
12
- params: req.params as Record<string, string | number | undefined | null>,
13
- body: req.body as Record<string, any>,
14
- headers: req.headers,
15
- query: req.query as Record<string, string | number | undefined | null>,
16
- path: req.url,
17
- original: provision ? req : undefined
18
- }
19
- }
20
-
21
- /**
22
- * Emits the response onto the fastify reply if the handler produced an
23
- * error or an outcome. Returns `true` when something was actually sent so
24
- * callers don't rely on `reply.sent`, which in fastify v5 only flips once
25
- * the raw socket finishes writing (asynchronously) and therefore reads
26
- * `false` synchronously right after `reply.send()`.
27
- *
28
- * @throws {Error}
29
- */
30
- export const executeResponse = <T>(response: AbstractResponse<T>, reply: Response, throwOnError?: boolean): boolean => {
31
- if (response.error != null) {
32
- if (throwOnError ?? false) {
33
- throw response.error
8
+ export const createPayloadHelper = (): PayloadHelper => {
9
+ const provideRequest = (alias: string, req: Request, provision?: boolean): AbstractRequest => {
10
+ provision = provision ?? false
11
+ return {
12
+ alias,
13
+ auth: (req as any)._auth ?? undefined,
14
+ // Resolved once by `authorize` and carried by reference, exactly like `_auth`, so a handler
15
+ // reached through any path sees the same entity the guard admitted.
16
+ entity: (req as any)._entity ?? undefined,
17
+ params: req.params as Record<string, string | number | undefined | null>,
18
+ body: req.body as Record<string, any>,
19
+ headers: req.headers,
20
+ query: req.query as Record<string, string | number | undefined | null>,
21
+ path: req.url,
22
+ original: provision ? req : undefined
34
23
  }
35
- reply.code(SERVER_ERROR).send(response.error.message)
36
- return true
37
- } else if (response.outcome != null) {
38
- switch (response.outcome) {
39
- case EntrypointOutcome.Accepted:
40
- reply.code(ACCEPTED).send(response.value)
41
- break
42
- case EntrypointOutcome.Created:
43
- reply.code(CREATED).send(response.value)
44
- break
45
- case EntrypointOutcome.Ok:
46
- default:
47
- reply.code(OK).send(response.value)
48
- }
49
- return true
50
24
  }
51
25
 
52
- return false
53
- }
54
-
55
-
56
- export const fixFormatDates = (schema: AnySchemaObject) => {
57
- if (schema.type === 'object') {
58
- if (schema.format === 'date-time') {
59
- schema.type = 'string'
60
- schema.format = 'date-time'
61
- if (schema.required != null) {
62
- delete schema.required
26
+ const executeResponse = <T>(response: AbstractResponse<T>, reply: Response, throwOnError?: boolean): boolean => {
27
+ if (response.error != null) {
28
+ if (throwOnError ?? false) {
29
+ throw response.error
30
+ }
31
+ httpErrorHelper.handleError(response.error, reply)
32
+ return true
33
+ } else if (response.outcome != null) {
34
+ switch (response.outcome) {
35
+ case EntrypointOutcome.Accepted:
36
+ reply.code(ACCEPTED).send(response.value)
37
+ break
38
+ case EntrypointOutcome.Created:
39
+ reply.code(CREATED).send(response.value)
40
+ break
41
+ case EntrypointOutcome.Ok:
42
+ default:
43
+ reply.code(OK).send(response.value)
63
44
  }
64
- } else if (schema.properties != null) {
65
- schema.properties = Object.fromEntries(
66
- Object.entries(schema.properties).map(([key, value]) => [key, fixFormatDates(value as AnySchemaObject)])
67
- )
45
+ return true
68
46
  }
47
+
48
+ return false
69
49
  }
70
50
 
71
- return schema
51
+ return { provideRequest, executeResponse }
72
52
  }
53
+
54
+ export const payloadHelper = createPayloadHelper()
@@ -0,0 +1,23 @@
1
+ import type { AnySchemaObject } from 'ajv'
2
+
3
+ /**
4
+ * A request schema Fastify can serve: a `date-time` object becomes a `date-time` string (and loses
5
+ * its `required` list), recursively through object properties. Mutates and returns `schema`.
6
+ */
7
+ export const fixFormatDates = (schema: AnySchemaObject): AnySchemaObject => {
8
+ if (schema.type === 'object') {
9
+ if (schema.format === 'date-time') {
10
+ schema.type = 'string'
11
+ schema.format = 'date-time'
12
+ if (schema.required != null) {
13
+ delete schema.required
14
+ }
15
+ } else if (schema.properties != null) {
16
+ schema.properties = Object.fromEntries(
17
+ Object.entries(schema.properties).map(([key, value]) => [key, fixFormatDates(value as AnySchemaObject)])
18
+ )
19
+ }
20
+ }
21
+
22
+ return schema
23
+ }
@@ -1,29 +1,27 @@
1
- import { AppType } from '@owlmeans/context'
1
+ import { AppType, assertContext } from '@owlmeans/context'
2
2
  import type { FastifyReply, FastifyRequest } from 'fastify'
3
- import { assertContext } from '@owlmeans/context'
4
3
  import type { FixerService, ServerEntrypoint } from '@owlmeans/server-entrypoint'
5
- import type { CommonEntrypoint } from '@owlmeans/entrypoint'
6
- import type { GateService } from '@owlmeans/entrypoint'
7
- import { provideResponse } from '@owlmeans/entrypoint'
8
- import type { ServerContext, ServerConfig } from '@owlmeans/server-context'
4
+ import { type CommonEntrypoint, type GateService, provideResponse } from '@owlmeans/entrypoint'
9
5
  import { ResilientError } from '@owlmeans/error'
10
6
  import { OK } from '@owlmeans/api'
11
- import { handleError } from './error.js'
12
- import { executeResponse, provideRequest } from './payload.js'
7
+ import { logger } from '@owlmeans/log'
8
+ import { httpErrorHelper } from './error.js'
9
+ import { payloadHelper } from './payload.js'
13
10
  import { authorize } from './guards.js'
14
11
  import { RouteProtocols } from '@owlmeans/route'
12
+ import type { ServerConfig2, ServerContext2 } from './types.local.js'
15
13
 
16
- type Config = ServerConfig
17
- type Context = ServerContext<Config>
18
14
 
19
- export const canServeModule = (context: Context, module: CommonEntrypoint): module is ServerEntrypoint<unknown> => {
15
+ export const canServeModule = (context: ServerContext2, module: CommonEntrypoint): module is ServerEntrypoint<unknown> => {
20
16
  if (module.route.route.type !== AppType.Backend) {
21
17
  return false
22
18
  }
23
19
  if (module.route.route.service != null && module.route.route.service !== context.cfg.service) {
24
20
  return false
25
21
  }
26
- if (module.route.route.protocol === RouteProtocols.SOCKET) {
22
+ // HTTP serves only its own protocol. Custom transports are handled by their owning packages.
23
+ if (module.route.route.protocol != null
24
+ && module.route.route.protocol !== RouteProtocols.WEB) {
27
25
  return false
28
26
  }
29
27
 
@@ -33,20 +31,20 @@ export const canServeModule = (context: Context, module: CommonEntrypoint): modu
33
31
  export const createServerHandler = (module: ServerEntrypoint<FastifyRequest>, location: string) =>
34
32
  async (req: FastifyRequest, reply: FastifyReply) => {
35
33
  // We passed context using fastify request object
36
- let context = assertContext<Config, Context>((req as any)._ctx, location)
34
+ let context = assertContext<ServerConfig2, ServerContext2>((req as any)._ctx, location)
37
35
  try {
38
36
  const authorized = await authorize(context, module, req, reply)
39
37
  context = authorized[0]
40
38
  module = authorized[1]
41
39
 
42
40
  const response = provideResponse(reply)
43
- const request = provideRequest(module.alias, req, true)
41
+ const request = payloadHelper.provideRequest(module.alias, req, true)
44
42
 
45
43
  const gates = module.getGates()
46
44
  for (const [srv, params] of gates) {
47
45
  const gate: GateService = context.service(srv)
48
46
  await gate.assert(request, response, params)
49
- executeResponse(response, reply, true)
47
+ payloadHelper.executeResponse(response, reply, true)
50
48
  }
51
49
 
52
50
  await module.handle(request, response)
@@ -57,20 +55,17 @@ export const createServerHandler = (module: ServerEntrypoint<FastifyRequest>, lo
57
55
  // synchronously right after `reply.send()`. Track the emitted state
58
56
  // explicitly instead, and only fall back to a default response when
59
57
  // neither `executeResponse` nor the handler itself (hijack) replied.
60
- const responded = executeResponse(response, reply, true)
58
+ const responded = payloadHelper.executeResponse(response, reply, true)
61
59
  if (!responded && !reply.sent) {
62
- console.warn(`SENDS DEFAULT RESPONSE: ${module.alias}`)
60
+ logger('http').warn(`SENDS DEFAULT RESPONSE: ${module.alias}`)
63
61
  reply.code(OK).send(response.value)
64
62
  }
65
63
  } catch (error) {
66
- console.error(`Error in ${module.alias} (${location})`)
67
- console.error(JSON.stringify(error, null, 2))
68
- console.error(error)
69
64
  if (module.fixer != null) {
70
65
  const fixer: FixerService = context.service(module.fixer)
71
66
  fixer.handle(reply, ResilientError.ensure(error as Error))
72
67
  return
73
68
  }
74
- handleError(error as Error, reply)
69
+ httpErrorHelper.handleError(error as Error, reply, httpErrorHelper.errorExposure(context.cfg))
75
70
  }
76
71
  }
@@ -0,0 +1,12 @@
1
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
2
+ import type { Config as ApiConfig } from '../types.js'
3
+
4
+ export type Config = ServerConfig
5
+
6
+ export interface Context<C extends Config = Config> extends ServerContext<C> { }
7
+
8
+ export type Severity = 'debug' | 'info' | 'warn' | 'error'
9
+
10
+ export type ServerConfig2 = ApiConfig
11
+
12
+ export type ServerContext2 = ServerContext<ServerConfig2>
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What an error class may declare about the HTTP status it is answered with.
3
+ *
4
+ * Structural on purpose: the classes that declare it live in packages that must not depend on an
5
+ * HTTP server (`@owlmeans/payment`, a product's contract package), so they write a plain
6
+ * `public static httpStatus = 409` and nothing imports this type.
7
+ */
8
+ export interface HttpStatusDeclaration {
9
+ httpStatus?: unknown
10
+ /** Explicit opt-in for a declared 5xx response. Client-error declarations need no opt-in. */
11
+ allowServerErrorStatus?: unknown
12
+ }
13
+
14
+ export interface SerializedHttpError {
15
+ status: number
16
+ body: string
17
+ incidentId: string
18
+ }