@owlmeans/server-api 0.1.18-rc.45 → 0.1.18-rc.47

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 CHANGED
@@ -10,7 +10,7 @@ only when composing a context without that package. WebSocket routes belong to
10
10
  ## Installation
11
11
 
12
12
  ```bash
13
- bun add @owlmeans/server-api@^0.1.18-rc.45
13
+ bun add @owlmeans/server-api@^0.1.18-rc.47
14
14
  ```
15
15
 
16
16
  ## Concepts
@@ -240,7 +240,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
240
240
  your project's skill store (`.agents/skills/`):
241
241
 
242
242
  ```sh
243
- npx @owlmeans/agent-skills@^0.1.18-rc.42
243
+ npx @owlmeans/agent-skills@^0.1.18-rc.45
244
244
  ```
245
245
 
246
246
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-api",
4
- "version": "0.1.18-rc.45",
5
- "generatedAt": "2026-09-29T08:01:58.109Z",
4
+ "version": "0.1.18-rc.47",
5
+ "generatedAt": "2026-10-01T23:27:48.718Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -7,7 +7,7 @@ user-invocable: false
7
7
 
8
8
  # @owlmeans/server-api
9
9
 
10
- **Install:** `bun add @owlmeans/server-api@^0.1.18-rc.45`
10
+ **Install:** `bun add @owlmeans/server-api@^0.1.18-rc.47`
11
11
 
12
12
  Make handlers from the protocol declaration so input and output types stay coupled to the shared
13
13
  contract:
@@ -38,6 +38,12 @@ export const serverBindings = [
38
38
  any protocol and receives all its typed sections plus request metadata. A successful callback
39
39
  return resolves the entrypoint with `EntrypointOutcome.Ok`; a thrown error rejects it.
40
40
 
41
+ A body contract may be a scalar (`schema<string>({ type: 'string' })`, a number, a boolean): the
42
+ server's JSON parser takes any JSON value, so `"abc"`, `42` or `false` reach the handler as that
43
+ value. An unquoted string or an empty body under `application/json` is invalid JSON and answers
44
+ 400 before the handler runs — `@owlmeans/api` never sends either (it quotes a scalar body).
45
+ `tests/json-body.spec.ts` runs it through `appendApiServer`.
46
+
41
47
  ## Wrap exactly once
42
48
 
43
49
  A handler is wrapped by `handlers<Context>()` exactly once — either shape above (create the bound
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/server-api",
3
- "version": "0.1.18-rc.45",
3
+ "version": "0.1.18-rc.47",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -33,15 +33,15 @@
33
33
  "@fastify/helmet": "^13.1.0",
34
34
  "@fastify/middie": "^9.0.2",
35
35
  "@fastify/multipart": "^9.0.1",
36
- "@owlmeans/api": "^0.1.18-rc.41",
37
- "@owlmeans/auth": "^0.1.18-rc.36",
38
- "@owlmeans/auth-common": "^0.1.18-rc.41",
36
+ "@owlmeans/api": "^0.1.18-rc.43",
37
+ "@owlmeans/auth": "^0.1.18-rc.37",
38
+ "@owlmeans/auth-common": "^0.1.18-rc.43",
39
39
  "@owlmeans/context": "^0.1.18-rc.32",
40
40
  "@owlmeans/error": "^0.1.18-rc.36",
41
- "@owlmeans/entrypoint": "^0.1.18-rc.37",
41
+ "@owlmeans/entrypoint": "^0.1.18-rc.38",
42
42
  "@owlmeans/route": "^0.1.18-rc.33",
43
- "@owlmeans/server-context": "^0.1.18-rc.38",
44
- "@owlmeans/server-entrypoint": "^0.1.18-rc.36",
43
+ "@owlmeans/server-context": "^0.1.18-rc.39",
44
+ "@owlmeans/server-entrypoint": "^0.1.18-rc.37",
45
45
  "ajv-errors": "^3.0.0",
46
46
  "fastify": "^5.0.0",
47
47
  "fastify-raw-body": "^5.0.0",
@@ -1,7 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
2
2
  import Fastify from 'fastify'
3
3
  import type { FastifyInstance } from 'fastify'
4
- import { AuthForbidden, AuthorizationError } from '@owlmeans/auth'
4
+ import { AuthForbidden, AuthorizationError, AuthUnknown } from '@owlmeans/auth'
5
5
  import { ResilientError, SEPARATOR } from '@owlmeans/error'
6
6
  import { provideResponse } from '@owlmeans/entrypoint'
7
7
  import { errorExposure, errorStatus, handleError, INCIDENT_ID_HEADER } from '../src/utils/error.js'
@@ -254,6 +254,12 @@ describe('@owlmeans/server-api — handleError status', () => {
254
254
  }
255
255
  })
256
256
 
257
+ test('an unknown authentication method or identity answers 400, as thrown and after a marshal hop', async () => {
258
+ expect((await answer(new AuthUnknown('email-otp'))).status).toBe(400)
259
+ expect((await answer(ResilientError.marshal(new AuthUnknown('entity')))).status).toBe(400)
260
+ expect((await answer(new AuthUnknown(), '/execute')).status).toBe(400)
261
+ })
262
+
257
263
  test('honours an explicitly exposed 5xx without opening arbitrary server statuses', async () => {
258
264
  expect((await answer(new TemporarilyUnavailable())).status).toBe(503)
259
265
  const Unexposed = declaring('ServerApiSpecUnexposed503', 503)
@@ -0,0 +1,72 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
2
+ import { AppType } from '@owlmeans/context'
3
+ import { contract, protocol, schema, typed } from '@owlmeans/entrypoint'
4
+ import { backend, route, RouteMethod } from '@owlmeans/route'
5
+ import { config, makeServerContext } from '@owlmeans/server-context'
6
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
7
+ import { bind } from '@owlmeans/server-entrypoint'
8
+ import type { FastifyInstance } from 'fastify'
9
+ import { handlers } from '../src/protocol.js'
10
+ import { appendApiServer } from '../src/server.js'
11
+
12
+ interface Echo { value: unknown, type: string }
13
+
14
+ const echoOf = <T>(alias: string, body: ReturnType<typeof schema<T>>) => protocol(
15
+ route(`server-api-test:${alias}`, `/echo/${alias}`, backend(null, RouteMethod.POST)),
16
+ contract(body, typed<Echo>()),
17
+ )
18
+
19
+ const text = echoOf('text', schema<string>({ type: 'string' }))
20
+ const count = echoOf('count', schema<number>({ type: 'number' }))
21
+ const flag = echoOf('flag', schema<boolean>({ type: 'boolean' }))
22
+ const record = echoOf('record', schema<{ a: number }>({ type: 'object', properties: { a: { type: 'number' } }, required: ['a'] }))
23
+
24
+ /**
25
+ * A scalar JSON body through the package's own server — `appendApiServer`, its Fastify instance,
26
+ * its body validation and a protocol-bound handler — exactly as `@owlmeans/api` now sends it:
27
+ * JSON text. The client half of the round trip is `@owlmeans/api`'s `body.spec.ts`.
28
+ */
29
+ describe('@owlmeans/server-api — a scalar JSON body', () => {
30
+ let server: FastifyInstance
31
+ let context: ServerContext<ServerConfig>
32
+
33
+ beforeAll(async () => {
34
+ const service = 'server-api-tests'
35
+ context = makeServerContext(config<ServerConfig>(service, {
36
+ services: { [service]: { service, type: AppType.Backend, host: '127.0.0.1', default: true } },
37
+ } as Partial<ServerConfig>)) as ServerContext<ServerConfig>
38
+ const served = appendApiServer(context)
39
+ const api = handlers<ServerContext<ServerConfig>>()
40
+ for (const echo of [text, count, flag, record]) {
41
+ context.registerEntrypoint(bind(echo, api.request(echo, async request => ({ value: request.body, type: typeof request.body }))))
42
+ }
43
+ context.configure()
44
+ await context.init()
45
+ server = served.getApiServer().server
46
+ await server.ready()
47
+ })
48
+
49
+ afterAll(async () => { await server?.close() })
50
+
51
+ const post = async (alias: string, payload: string, type: string = 'application/json') =>
52
+ await server.inject({ method: 'POST', url: `/echo/${alias}`, headers: { 'content-type': type }, payload })
53
+
54
+ const echoed = async (alias: string, payload: string): Promise<Echo> => (await post(alias, payload)).json<Echo>()
55
+
56
+ test('a JSON string, number or boolean reaches the handler as that value', async () => {
57
+ expect(await echoed('text', '"abc"')).toEqual({ value: 'abc', type: 'string' })
58
+ expect(await echoed('text', '""')).toEqual({ value: '', type: 'string' })
59
+ expect(await echoed('text', '"123"')).toEqual({ value: '123', type: 'string' })
60
+ expect(await echoed('count', '42')).toEqual({ value: 42, type: 'number' })
61
+ expect(await echoed('flag', 'false')).toEqual({ value: false, type: 'boolean' })
62
+ })
63
+
64
+ test('an object body is unchanged', async () => {
65
+ expect(await echoed('record', '{"a":1}')).toEqual({ value: { a: 1 }, type: 'object' })
66
+ })
67
+
68
+ test('an unquoted string or an empty JSON body is refused with 400 — what the client no longer sends', async () => {
69
+ expect((await post('text', 'abc')).statusCode).toBe(400)
70
+ expect((await post('text', '')).statusCode).toBe(400)
71
+ })
72
+ })