@owlmeans/api 0.1.18-rc.3 → 0.1.18-rc.30

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
@@ -1,6 +1,6 @@
1
1
  # @owlmeans/api
2
2
 
3
- HTTP API client service for OwlMeans client contexts — handles module-based requests with auth token injection.
3
+ HTTP API client service for OwlMeans client contexts — carries entrypoint calls with auth token injection.
4
4
 
5
5
  ## Overview
6
6
 
@@ -12,7 +12,7 @@ HTTP API client service for OwlMeans client contexts — handles module-based re
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/api
15
+ bun add @owlmeans/api@^0.1.18-rc.30
16
16
  ```
17
17
 
18
18
  ## Usage
@@ -54,7 +54,7 @@ Registers the API client in the context.
54
54
 
55
55
  ## Related Packages
56
56
 
57
- - [`@owlmeans/client-module`](../client-module) — `ClientModule<T>` uses this service to make requests
57
+ - [`@owlmeans/client-entrypoint`](../client-entrypoint) — bound protocols use this service to make requests
58
58
  - [`@owlmeans/web-client`](../web-client) — registers this service via `makeContext`
59
59
 
60
60
  <!-- owlmeans:agent-guidance:start -->
@@ -65,7 +65,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
65
65
  your project's skill store (`.agents/skills/`):
66
66
 
67
67
  ```sh
68
- npx @owlmeans/agent-skills
68
+ npx @owlmeans/agent-skills@^0.1.18-rc.30
69
69
  ```
70
70
 
71
71
  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/api",
4
- "version": "0.1.18-rc.0",
5
- "generatedAt": "2026-08-16T22:20:50.518Z",
4
+ "version": "0.1.18-rc.30",
5
+ "generatedAt": "2026-09-19T13:42:56.286Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: api
3
- description: How to use @owlmeans/api — HTTP client service (axios-based) used to call entrypoints across services. Auto-invoked when importing the API service or when ctx.entrypoint(...).call() under the hood is involved.
3
+ description: How to use @owlmeans/api — the axios-based HTTP client service that carries entrypoint calls between services, its status-to-outcome mapping, typed transport errors and per-request timeout/abort. Auto-invoked when importing the API client, or when ctx.entrypoint(...).call() under the hood is involved.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,28 +8,89 @@ user-invocable: false
8
8
  # @owlmeans/api
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/api": "^0.1.18-rc.0"` in `dependencies`
11
+ **Install:** `"@owlmeans/api": "^0.1.18-rc.30"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
15
15
  | Export | Description |
16
16
  |--------|-------------|
17
- | `makeApiService()` | Factory for the HTTP client service (axios) |
18
- | `Api` types | Service interface |
19
- | Errors | Typed transport errors (Network, Timeout, HttpStatus) |
20
- | Constants | Default timeout, retry counts |
17
+ | `createApiService(alias?)` | Factory for the HTTP client service (axios) |
18
+ | `appendApiClient(ctx, alias?)` | Register it and make it the context's default `webService` when none is set |
19
+ | `ApiClient` | Service interface — a single `handler(req, reply)` |
20
+ | `ApiError`, `ApiClientError`, `ServerCrashedError`, `ServerAuthError` | Typed transport errors |
21
+ | Constants | Status codes (`OK`, `CREATED`, `ACCEPTED`, `FINISHED`, `UNAUTHORIZED_ERROR`, `FORBIDDEN_ERROR`, `SERVER_ERROR`), `DEFAULT_ALIAS` (`web-client`) |
22
+
23
+ ## How a call is carried
24
+
25
+ `ep.call(...)` / `ep.invoke(...)` hand the request to whatever is bound to the route's protocol. When
26
+ a transport service is registered under `transportAlias(protocol)` it takes the call; otherwise this
27
+ package's client does, over HTTP. Either way the consumer writes the same line and never learns which
28
+ one ran.
29
+
30
+ When it is this client, the entrypoint answers where it lives — `path()` for the path, `address()` for
31
+ host, port, base, protocol and whether the hop is TLS — and `makeSecurityHelper` assembles the URL from
32
+ that. A per-request `host`, `base` or `unsecure` still overrides it. `:params` come from
33
+ `request.params` and a missing one is a `SyntaxError`, not a literal `:id` in the URL.
34
+
35
+ A request whose `canceled` flag is already set is dropped without a round trip.
36
+
37
+ ## Per-request controls
38
+
39
+ These live on the request the caller passes and are forwarded to the transport:
40
+
41
+ - `timeout` — milliseconds for the single round trip; omitted or `0` means no timeout, so a stuck
42
+ peer hangs forever unless a caller bounds it.
43
+ - `signal` — an `AbortSignal`, which aborts the request in flight.
44
+ - `headers` — a `content-type` of `application/x-www-form-urlencoded` makes the body serialize
45
+ through `qs` instead of JSON. A string body on a `POST` with no `content-type` gets
46
+ `application/json` and is sent verbatim rather than re-quoted.
47
+
48
+ ## Reading the answer
49
+
50
+ `validateStatus` is `() => true`, so **no status throws**: every answer the peer sends, 4xx and 5xx
51
+ included, comes back and is mapped here.
52
+
53
+ | Status | Result |
54
+ |--------|--------|
55
+ | 200 / 202 | Resolves with the body and an `Ok` / `Accepted` outcome |
56
+ | 201 / 204 | Resolves with the body, or with the response headers when the body is empty |
57
+ | any other | Rejects |
58
+
59
+ A rejection prefers the server's own error: a text body is rehydrated into the original
60
+ `ResilientError`, so a `handleError` reply on the far side arrives as the same class it was thrown
61
+ as. Failing that it becomes `ServerCrashedError` (500), `ServerAuthError` (401) or `ApiClientError`
62
+ (403 and the rest).
63
+
64
+ **A failure with no answer at all is a different family.** Suppressing status errors does not wrap
65
+ the call: an expired `timeout` (`ECONNABORTED`), an aborted `signal` (`CanceledError` /
66
+ `ERR_CANCELED`), a refused connection or a DNS failure rejects with the raw axios error, which is
67
+ none of the typed errors above. A caller that bounds a call has to catch that too — testing for
68
+ `ApiClientError` alone lets a timeout through as an unhandled rejection.
69
+
70
+ An `auth-token-refresh` response header is consumed here: when the context has an auth service, the
71
+ rotated token is handed to it, which is what keeps a long session alive without the caller doing
72
+ anything.
73
+
74
+ Building a URL without making the call is a different question — that is `entrypointUrl` from
75
+ `@owlmeans/client-entrypoint/utils`, or `ep.url(req, { absolute })`.
21
76
 
22
77
  ## Usage
23
78
 
24
- The api service backs `ctx.entrypoint<ClientEntrypoint<T>>(alias).call(...)` cross-service calls. Most apps don't import it directly — they register it once in their context factory:
79
+ Most apps don't import this directly — they register it once in their context factory:
25
80
 
26
81
  ```typescript
27
- import { makeApiService } from '@owlmeans/api'
28
- context.registerService(makeApiService())
82
+ import { appendApiClient } from '@owlmeans/api'
83
+ appendApiClient(context)
29
84
  ```
30
85
 
86
+ `appendApiClient` only claims `cfg.webService` when it is unset, so an app that has already named a
87
+ transport keeps it.
88
+
31
89
  ## Depends On
32
90
 
33
91
  - `@owlmeans/context` — service registration
34
- - `@owlmeans/error`, `@owlmeans/i18n`
35
- - `axios` (runtime)
92
+ - `@owlmeans/entrypoint` — the entrypoint being addressed, `@owlmeans/client-route` — `extractParams`
93
+ - `@owlmeans/config` — `makeSecurityHelper`, `@owlmeans/client-config` — the config shape
94
+ - `@owlmeans/auth-common` — the token-refresh header and the auth service it updates
95
+ - `@owlmeans/error`, `@owlmeans/route`
96
+ - `axios`, `qs` (runtime)
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAM3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAO3D,KAAK,MAAM,GAAG,iBAAiB,CAAA;AAE/B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAC;CACnE;AAED,eAAO,MAAM,gBAAgB,WAAW,MAAM,KAAmB,SA4EhE,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,UAAS,MAAM,KAAmB,CAU/G,CAAA"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAM3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAa3D,KAAK,MAAM,GAAG,iBAAiB,CAAA;AAE/B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAC;CACnE;AAED,eAAO,MAAM,gBAAgB,WAAW,MAAM,KAAmB,SA+FhE,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,UAAS,MAAM,KAAmB,CAU/G,CAAA"}
package/build/service.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { assertContext, createService } from '@owlmeans/context';
2
2
  import { extractParams } from '@owlmeans/client-route';
3
3
  import axios from 'axios';
4
- import { DEFAULT_ALIAS } from './consts.js';
4
+ import { DEFAULT_ALIAS, UNAUTHORIZED_ERROR } from './consts.js';
5
5
  import { processResponse } from './utils/handler.js';
6
6
  import qs from 'qs';
7
7
  import { makeSecurityHelper } from '@owlmeans/config';
8
8
  import { RouteMethod } from '@owlmeans/route';
9
- import { DEF_AUTH_SRV, TOKEN_UPDATE } from '@owlmeans/auth-common';
9
+ import { AUTH_HEADER, DEF_AUTH_SRV, TOKEN_UPDATE } from '@owlmeans/auth-common';
10
+ /** Whether a request carried an authentication header of its own. */
11
+ const presented = (headers) => headers != null && Object.entries(headers).some(([key, value]) => key.toLowerCase() === AUTH_HEADER && value != null && value !== '');
10
12
  export const createApiService = (alias = DEFAULT_ALIAS) => {
11
13
  const location = `api.service:${alias}`;
12
14
  const client = createService(alias, {
@@ -16,9 +18,8 @@ export const createApiService = (alias = DEFAULT_ALIAS) => {
16
18
  }
17
19
  const context = assertContext(client.ctx, location);
18
20
  const module = context.entrypoint(request.alias);
19
- await module.resolve();
20
21
  const route = module.route.route;
21
- let path = module.getPath();
22
+ let path = module.path();
22
23
  const params = extractParams(path);
23
24
  path = params.reduce((path, param) => {
24
25
  if (request.params[param] == null) {
@@ -26,11 +27,10 @@ export const createApiService = (alias = DEFAULT_ALIAS) => {
26
27
  }
27
28
  return path.replace(`:${param}`, `${request.params[param]}`);
28
29
  }, path);
29
- if (route.host == null && request.host == null) {
30
- throw new SyntaxError(`No host provided in ${module.alias} route`);
31
- }
32
30
  const helper = makeSecurityHelper(context);
33
- const url = helper.makeUrl(route, path, { host: request.host, base: request.base, forceUnsecure: request.unsecure });
31
+ // The entrypoint answers where it lives — host, port, base, protocol and whether the hop is
32
+ // TLS — from its declaration and the service it names. A per-request host still overrides it.
33
+ const url = helper.makeUrl(module.address(), path, { host: request.host, base: request.base, forceUnsecure: request.unsecure });
34
34
  let transformer = undefined;
35
35
  let body = request.body != null && Object.entries((request.headers ?? {})).find(([key, value]) => key.toLowerCase() === 'content-type' && value?.includes('application/x-www-form-urlencoded')) ? qs.stringify(request.body) : request.body;
36
36
  // @TODO Probably this hack needs to be made a little bit less dirty
@@ -62,6 +62,23 @@ export const createApiService = (alias = DEFAULT_ALIAS) => {
62
62
  const update = headers.get(TOKEN_UPDATE);
63
63
  await auth.update(update == null || update === '' ? undefined : update);
64
64
  }
65
+ else if (response.status === UNAUTHORIZED_ERROR
66
+ && presented(request.headers)
67
+ && context.hasService(DEF_AUTH_SRV)) {
68
+ /**
69
+ * A session the server has refused is not a session, and keeping it is worse than having
70
+ * none: the application goes on rendering its signed-in tree over a credential that fails
71
+ * every call it makes, and nothing on the client ever asks again — `authenticated()` reads
72
+ * storage and decodes an envelope, it does not consult the server. What that leaves is an
73
+ * app that looks signed in, works at nothing, and offers "Log out" where the control that
74
+ * would fix it should be.
75
+ *
76
+ * Narrow on purpose. Only a request that actually PRESENTED this context's bearer counts,
77
+ * so a 401 from an authentication attempt — bad code, wrong password, an expired challenge
78
+ * — never clears the session of whoever is already signed in.
79
+ */
80
+ await context.service(DEF_AUTH_SRV).update(undefined);
81
+ }
65
82
  processResponse(response, reply);
66
83
  return [reply.error ?? reply.value, reply.outcome];
67
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/api",
3
- "version": "0.1.18-rc.3",
3
+ "version": "0.1.18-rc.30",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,14 +21,14 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@owlmeans/auth-common": "^0.1.18-rc.3",
25
- "@owlmeans/client-config": "^0.1.18-rc.3",
26
- "@owlmeans/client-route": "^0.1.18-rc.3",
27
- "@owlmeans/config": "^0.1.18-rc.3",
28
- "@owlmeans/context": "^0.1.18-rc.3",
29
- "@owlmeans/error": "^0.1.18-rc.3",
30
- "@owlmeans/entrypoint": "^0.1.18-rc.3",
31
- "@owlmeans/route": "^0.1.18-rc.3",
24
+ "@owlmeans/auth-common": "^0.1.18-rc.30",
25
+ "@owlmeans/client-config": "^0.1.18-rc.30",
26
+ "@owlmeans/client-route": "^0.1.18-rc.31",
27
+ "@owlmeans/config": "^0.1.18-rc.30",
28
+ "@owlmeans/context": "^0.1.18-rc.26",
29
+ "@owlmeans/error": "^0.1.18-rc.28",
30
+ "@owlmeans/entrypoint": "^0.1.18-rc.29",
31
+ "@owlmeans/route": "^0.1.18-rc.27",
32
32
  "axios": "^1.7.7",
33
33
  "qs": "^6.13.0"
34
34
  },
package/src/service.ts CHANGED
@@ -5,15 +5,21 @@ import type { ApiClient } from './types.js'
5
5
  import axios, { AxiosHeaders } from 'axios'
6
6
  import type { AxiosRequestTransformer } from 'axios'
7
7
  import type { CommonEntrypoint } from '@owlmeans/entrypoint'
8
- import { DEFAULT_ALIAS } from './consts.js'
8
+ import { DEFAULT_ALIAS, UNAUTHORIZED_ERROR } from './consts.js'
9
9
  import { processResponse } from './utils/handler.js'
10
10
  import { BasicClientConfig } from '@owlmeans/client-config'
11
11
  import qs from 'qs'
12
12
  import { makeSecurityHelper } from '@owlmeans/config'
13
13
  import { RouteMethod } from '@owlmeans/route'
14
- import { DEF_AUTH_SRV, TOKEN_UPDATE } from '@owlmeans/auth-common'
14
+ import { AUTH_HEADER, DEF_AUTH_SRV, TOKEN_UPDATE } from '@owlmeans/auth-common'
15
15
  import type { AuthService } from '@owlmeans/auth-common'
16
16
 
17
+ /** Whether a request carried an authentication header of its own. */
18
+ const presented = (headers?: Record<string, unknown>): boolean =>
19
+ headers != null && Object.entries(headers).some(
20
+ ([key, value]) => key.toLowerCase() === AUTH_HEADER && value != null && value !== ''
21
+ )
22
+
17
23
  type Config = BasicClientConfig
18
24
 
19
25
  interface Context<C extends Config = Config> extends BasicContext<C> {
@@ -28,9 +34,8 @@ export const createApiService = (alias: string = DEFAULT_ALIAS): ApiClient => {
28
34
  }
29
35
  const context = assertContext<Config, Context>(client.ctx, location)
30
36
  const module = context.entrypoint<CommonEntrypoint>(request.alias)
31
- await module.resolve()
32
37
  const route = module.route.route
33
- let path = module.getPath()
38
+ let path = module.path()
34
39
  const params = extractParams(path)
35
40
  path = params.reduce((path, param) => {
36
41
  type Key = keyof typeof request.params
@@ -39,13 +44,15 @@ export const createApiService = (alias: string = DEFAULT_ALIAS): ApiClient => {
39
44
  }
40
45
  return path.replace(`:${param}`, `${request.params[param as Key]}`)
41
46
  }, path)
42
- if (route.host == null && request.host == null) {
43
- throw new SyntaxError(`No host provided in ${module.alias} route`)
44
- }
45
47
 
46
48
  const helper = makeSecurityHelper(context)
47
49
 
48
- const url = helper.makeUrl(route, path, { host: request.host, base: request.base, forceUnsecure: request.unsecure })
50
+ // The entrypoint answers where it lives — host, port, base, protocol and whether the hop is
51
+ // TLS — from its declaration and the service it names. A per-request host still overrides it.
52
+ const url = helper.makeUrl(
53
+ module.address(), path,
54
+ { host: request.host, base: request.base, forceUnsecure: request.unsecure }
55
+ )
49
56
 
50
57
  let transformer: AxiosRequestTransformer | undefined = undefined
51
58
 
@@ -84,6 +91,24 @@ export const createApiService = (alias: string = DEFAULT_ALIAS): ApiClient => {
84
91
  const auth = context.service<AuthService>(DEF_AUTH_SRV)
85
92
  const update = headers.get(TOKEN_UPDATE) as string
86
93
  await auth.update(update == null || update === '' ? undefined : update)
94
+ } else if (
95
+ response.status === UNAUTHORIZED_ERROR
96
+ && presented(request.headers)
97
+ && context.hasService(DEF_AUTH_SRV)
98
+ ) {
99
+ /**
100
+ * A session the server has refused is not a session, and keeping it is worse than having
101
+ * none: the application goes on rendering its signed-in tree over a credential that fails
102
+ * every call it makes, and nothing on the client ever asks again — `authenticated()` reads
103
+ * storage and decodes an envelope, it does not consult the server. What that leaves is an
104
+ * app that looks signed in, works at nothing, and offers "Log out" where the control that
105
+ * would fix it should be.
106
+ *
107
+ * Narrow on purpose. Only a request that actually PRESENTED this context's bearer counts,
108
+ * so a 401 from an authentication attempt — bad code, wrong password, an expired challenge
109
+ * — never clears the session of whoever is already signed in.
110
+ */
111
+ await context.service<AuthService>(DEF_AUTH_SRV).update(undefined)
87
112
  }
88
113
 
89
114
  processResponse(response, reply)
package/build/.gitkeep DELETED
File without changes