@o3co/auth-provider-foundation 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +147 -61
- package/README.md +368 -81
- package/dist/endpointUrl.d.mts +20 -0
- package/dist/endpointUrl.d.mts.map +1 -1
- package/dist/endpointUrl.mjs +30 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +20 -0
- package/dist/repositories/HttpUserRepository.d.mts +111 -7
- package/dist/repositories/HttpUserRepository.d.mts.map +1 -1
- package/dist/repositories/HttpUserRepository.mjs +434 -30
- package/dist/repositories/storeErrors.d.mts +81 -0
- package/dist/repositories/storeErrors.d.mts.map +1 -0
- package/dist/repositories/storeErrors.mjs +193 -0
- package/dist/repositories/wwwAuthenticate.d.mts +10 -0
- package/dist/repositories/wwwAuthenticate.d.mts.map +1 -0
- package/dist/repositories/wwwAuthenticate.mjs +58 -0
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -1,85 +1,334 @@
|
|
|
1
1
|
# @o3co/auth-provider-foundation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Last updated: 2026-09-26
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The HTTP client of "the Store" — the deployment's own user service — for
|
|
6
|
+
auth.provider. `HttpUserRepository` implements core's `UserRepository` port over
|
|
7
|
+
HTTPS: it authenticates users, links federated identities, and answers the
|
|
8
|
+
identity lookup federation grants ask for. `registerBuiltinAdapters` registers
|
|
9
|
+
it as the `"http"` user adapter.
|
|
10
|
+
|
|
11
|
+
## Responsibility
|
|
12
|
+
|
|
13
|
+
**Role.** An adapter for one core port,
|
|
14
|
+
[`UserRepository`](../core/src/repositories/UserRepository.mts) (see
|
|
15
|
+
[`core/src/repositories`](../core/src/repositories/README.md)). Despite the
|
|
16
|
+
package name it is not a base layer: no other package imports it at runtime
|
|
17
|
+
(`federation-grants` uses it in a test). A composition root selects it —
|
|
18
|
+
`repositories.user.type = "http"` in the standalone template.
|
|
19
|
+
|
|
20
|
+
**Owns:**
|
|
21
|
+
|
|
22
|
+
- the wire contract a Store implements: the requests `HttpUserRepository` sends
|
|
23
|
+
and what each answer means (below);
|
|
24
|
+
- the transport rules on the Store's URLs — `https`, or `http` to a loopback
|
|
25
|
+
host only ([`src/endpointUrl.mts`](src/endpointUrl.mts)) — and that no
|
|
26
|
+
request follows a redirect away from them;
|
|
27
|
+
- the credential it presents to the Store (`bearerToken`), the floor that
|
|
28
|
+
credential is held to, and how the Store's refusal of it reads;
|
|
29
|
+
- that nothing a transport reports — which may quote the request — is thrown;
|
|
30
|
+
- the request deadline and the response-size cap;
|
|
31
|
+
- the coverage declaration the identity lookup is judged by at boot.
|
|
32
|
+
|
|
33
|
+
**Does not own:** the port and the `User` shape (core); when a user is
|
|
34
|
+
authenticated, an identity linked or an owner looked up — the session routes
|
|
35
|
+
([`@o3co/auth-provider-session`](../session/README.md)), `oauth`'s jwt-bearer
|
|
36
|
+
grant ([`@o3co/auth-provider-oauth`](../oauth/README.md)) and federation grants
|
|
37
|
+
([`@o3co/auth-provider-federation-grants`](../federation-grants/README.md)); the
|
|
38
|
+
Store itself; core's development user adapters (`yaml` / `static`); Redis-backed
|
|
39
|
+
stores ([`@o3co/auth-provider-redis`](../redis/README.md), which also holds the
|
|
40
|
+
Redis authorization-code store).
|
|
41
|
+
|
|
42
|
+
**Why a separate package.** Core ships only development user adapters, read from
|
|
43
|
+
a file; a production deployment keeps its users in a service of its own, and
|
|
44
|
+
this is the client for one. It is the replaceable piece — a deployment with its
|
|
45
|
+
own `UserRepository` implementation does not install it — and it depends on
|
|
46
|
+
nothing but core (the global `fetch`, no other dependency).
|
|
6
47
|
|
|
7
48
|
## Install
|
|
8
49
|
|
|
9
50
|
```sh
|
|
10
|
-
npm install @o3co/auth-provider-foundation
|
|
11
|
-
# Peer dependency (required):
|
|
12
|
-
npm install @o3co/auth-provider-core
|
|
51
|
+
npm install @o3co/auth-provider-foundation @o3co/auth-provider-core
|
|
13
52
|
```
|
|
14
53
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
### `registerBuiltinAdapters`
|
|
54
|
+
Peer dependency: `@o3co/auth-provider-core`. The package has no dependencies
|
|
55
|
+
of its own.
|
|
18
56
|
|
|
19
|
-
|
|
57
|
+
## Usage
|
|
20
58
|
|
|
21
59
|
```typescript
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
import { createRepositoryFactories } from "@o3co/auth-provider-core";
|
|
61
|
+
import { registerBuiltinAdapters } from "@o3co/auth-provider-foundation";
|
|
62
|
+
|
|
63
|
+
const { userFactory } = createRepositoryFactories();
|
|
26
64
|
|
|
27
|
-
|
|
65
|
+
registerBuiltinAdapters({ userFactory });
|
|
28
66
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
67
|
+
// Create an HTTP user repository via the factory
|
|
68
|
+
const userRepo = await userFactory.create({
|
|
69
|
+
type: "http",
|
|
70
|
+
authenticateUrl: "https://users.example.com/authenticate",
|
|
71
|
+
authenticateByTokenUrl: "https://users.example.com/authenticate-by-token",
|
|
72
|
+
timeout: 5000,
|
|
73
|
+
});
|
|
32
74
|
```
|
|
33
75
|
|
|
34
|
-
|
|
76
|
+
`registerBuiltinAdapters` ([`src/index.mts`](src/index.mts)) registers the
|
|
77
|
+
`"http"` type on a `UserRepository` factory; the builder reads the keys below,
|
|
78
|
+
defaults `timeout` to 5000 ms and `maxResponseBytes` to
|
|
79
|
+
`DEFAULT_MAX_RESPONSE_BYTES` (1 MiB) when absent, and accepts numbers given as
|
|
80
|
+
strings (an environment override). `HttpUserRepository`
|
|
81
|
+
([`src/repositories/HttpUserRepository.mts`](src/repositories/HttpUserRepository.mts))
|
|
82
|
+
can also be constructed directly with the same options; there `timeout` is
|
|
83
|
+
required, since the 5000 ms default is the builder's.
|
|
35
84
|
|
|
36
|
-
|
|
85
|
+
### Configuration
|
|
37
86
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
Under `repositories.user` (`type = "http"`, `CLIENT_USER_TYPE`), in its `http`
|
|
88
|
+
block; defaults are in [`reference.conf`](../core/config/reference.conf):
|
|
89
|
+
|
|
90
|
+
| Key | Env | |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| `authenticateUrl` | `CLIENT_USER_AUTHENTICATE_URL` | Required. Password login. |
|
|
93
|
+
| `authenticateByTokenUrl` | `CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL` | Required. Resolves an opaque handle: federated login, the jwt-bearer grant. |
|
|
94
|
+
| `linkFederatedIdentityUrl` | `CLIENT_USER_LINK_FEDERATED_IDENTITY_URL` | Optional. Enables account linking. |
|
|
95
|
+
| `findSubjectByFederatedIdentityUrl` | `CLIENT_USER_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL` | Optional. The identity lookup. |
|
|
96
|
+
| `federatedIdentityLookupCoverage` | — (a list; HOCON only) | What the lookup covers. Default `[]`. |
|
|
97
|
+
| `bearerToken` | `CLIENT_USER_BEARER_TOKEN` | Optional. Sent on every request as `Authorization: Bearer <token>`; at least 32 bytes of key material. Unset, no `Authorization` header is sent. See [who may call the Store](#what-the-store-must-enforce-itself). |
|
|
98
|
+
| `timeout` | `CLIENT_USER_TIMEOUT` | Milliseconds. Default 5000. |
|
|
99
|
+
| `maxResponseBytes` | `CLIENT_USER_MAX_RESPONSE_BYTES` | Default 1048576. |
|
|
100
|
+
|
|
101
|
+
## The wire contract
|
|
102
|
+
|
|
103
|
+
Every request is a `POST` with a JSON body, and — when `bearerToken` is
|
|
104
|
+
configured — an `Authorization: Bearer <token>` header. The user a Store
|
|
105
|
+
answers with is core's [`User`](../core/src/repositories/types.mts).
|
|
106
|
+
|
|
107
|
+
**`authenticate`** posts `{ email, password }` to `authenticateUrl` (the
|
|
108
|
+
username arrives as `email`); **`authenticateByToken`** posts `{ token }` to
|
|
109
|
+
`authenticateByTokenUrl`, where `token` is an opaque handle the Store resolves
|
|
110
|
+
to a user — `<provider>:<sub>` from the federation callback, the verified
|
|
111
|
+
assertion's subject handle from `oauth`'s jwt-bearer grant. For both:
|
|
112
|
+
|
|
113
|
+
- a `2xx` whose body is a JSON `User` (`{ id: string, username: string, … }`) is
|
|
114
|
+
the user;
|
|
115
|
+
- `401` or `403` is `null` — no such user, or wrong credentials;
|
|
116
|
+
- a `2xx` with a body that is not a `User` throws — an upstream failure, not a
|
|
117
|
+
"user not found";
|
|
118
|
+
- any other status throws.
|
|
119
|
+
|
|
120
|
+
The body of a non-`2xx` answer is discarded unread, for these and for linking.
|
|
121
|
+
No request follows a redirect, including the identity lookup: a `3xx` is one
|
|
122
|
+
more status that throws, and its `Location` is never contacted.
|
|
123
|
+
|
|
124
|
+
**A `401` or `403` with a `Bearer` challenge is a refused credential.** When
|
|
125
|
+
`bearerToken` is configured, a `401` or `403` carrying
|
|
126
|
+
`WWW-Authenticate: Bearer …` (RFC 6750 §3 — `invalid_token`,
|
|
127
|
+
`insufficient_scope`) from any of the four endpoints throws a
|
|
128
|
+
`StoreCredentialRefusedError`, `HttpUserRepository: the Store at <url> refused
|
|
129
|
+
this deployment's credential (HTTP <status> with a Bearer challenge) — …`,
|
|
130
|
+
instead of the reading this section otherwise gives that status: not "no such
|
|
131
|
+
user", not a refused link, not the lookup's `answered HTTP <status>`. What each
|
|
132
|
+
caller then answers and logs is in
|
|
133
|
+
[What the Store must enforce itself](#what-the-store-must-enforce-itself). The
|
|
134
|
+
challenge is found in any case, among other challenges or on a header line of
|
|
135
|
+
its own, and never inside a quoted string; a quoted string left unterminated
|
|
136
|
+
runs to the end of the value and hides whatever follows it, which then reads
|
|
137
|
+
as no challenge. Nothing the Store wrote after the scheme is repeated. A `401`
|
|
138
|
+
or `403` without a `Bearer` challenge, and any `401` or `403` when no token is
|
|
139
|
+
configured, keeps the meaning this section gives it, so a Store that does not
|
|
140
|
+
check the token is unaffected.
|
|
141
|
+
|
|
142
|
+
**A transport failure carries nothing of the exchange.** When a request cannot
|
|
143
|
+
be made or its answer cannot be read, the error is a `StoreTransportError`
|
|
144
|
+
whose `reason` and message say which:
|
|
145
|
+
|
|
146
|
+
| `reason` | When | Message (the lookup: `identity lookup at <url> …`) |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| `unreachable` | no connection to exchange on: a refused connection, DNS, TLS, a route or host the network cannot reach — the network path, or TLS | `HttpUserRepository: request to <url> could not be reached` |
|
|
149
|
+
| `connection_closed` | a connection closed or reset before a complete response arrived: before any byte, after an interim `1xx`, mid-head, or a pooled keep-alive connection the Store, a proxy or an idle timeout closed between two requests — the transport cannot tell which | `HttpUserRepository: the connection to <url> closed before a complete response arrived` (the lookup: `identity lookup at <url>: the connection closed …`) |
|
|
150
|
+
| `malformed_response` | the Store sent a response head the transport cannot take: the parser refused the status line or a header, or the head outgrew the size limit — the Store's answer, or a proxy's | `HttpUserRepository: the Store at <url> answered with a malformed HTTP response` |
|
|
151
|
+
| `unreadable` | an HTTP answer whose body broke mid-read | `HttpUserRepository: response from <url> could not be read` |
|
|
152
|
+
|
|
153
|
+
Each carries at most a transport code an operator can act on, in the message
|
|
154
|
+
and as `code`: `ECONNREFUSED`, `ENOTFOUND`, `ECONNRESET`, `EPROTO`, an
|
|
155
|
+
`UND_ERR_*` (`UND_ERR_HEADERS_OVERFLOW`, `UND_ERR_SOCKET`, …), an `HPE_*` where
|
|
156
|
+
the runtime sets one (Node 26's undici sets no code on its parser errors), an
|
|
157
|
+
`ERR_SSL_*` (`ERR_SSL_WRONG_VERSION_NUMBER` is an https URL on a port that
|
|
158
|
+
speaks plain HTTP; `ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE` is a TLS 1.2
|
|
159
|
+
handshake the Store refused — mutual TLS it expects a client certificate for,
|
|
160
|
+
or no shared cipher), a certificate code. It never
|
|
161
|
+
carries the transport's own error or a `cause`: undici's parser errors quote
|
|
162
|
+
the bytes they rejected, and a peer that reflects the request — a broken
|
|
163
|
+
proxy, a debugging echo — puts the `Authorization` header, or a password,
|
|
164
|
+
among them. A request that outlives the deadline is a `TimeoutError` instead
|
|
165
|
+
(see Constructor validation).
|
|
166
|
+
|
|
167
|
+
**`linkFederatedIdentity`** posts `{ userId, provider, sub, token, claims }` to
|
|
168
|
+
`linkFederatedIdentityUrl`: a `2xx` `User` is `{ ok: true, user }`; `401` / `403`
|
|
169
|
+
is `{ ok: false, reason: "refused" }`; `409` is
|
|
170
|
+
`{ ok: false, reason: "conflict" }`; anything else throws. Because the body of a
|
|
171
|
+
refusal is not read, a refusal carries no description from the Store. The
|
|
172
|
+
method is absent when `linkFederatedIdentityUrl` is not configured, which is
|
|
173
|
+
how the federation
|
|
174
|
+
routes know to refuse `?link=1` up front. What a Store checks before answering
|
|
175
|
+
`2xx` — never on an unverified or relay address, never by e-mail alone — is in
|
|
176
|
+
the
|
|
177
|
+
[session package README](../session/README.md#account-linking-across-federations-482).
|
|
178
|
+
|
|
179
|
+
#### The identity lookup (#613)
|
|
180
|
+
|
|
181
|
+
What a federation-grant deployment (`@o3co/auth-provider-federation-grants`,
|
|
182
|
+
the connect callback's check 5 in the ADR's
|
|
183
|
+
[D7](../core/docs/adr/2026-09-17-federation-grants-offline-delegation.md#d7--the-connect-flow-binds-its-callback-and-refuses-on-any-mismatch))
|
|
184
|
+
asks of the Store: who holds an upstream identity, so that a
|
|
185
|
+
delegation is refused when the account belongs to another local user. The
|
|
186
|
+
Store answers about ownership across **every** registration of the IdP, not
|
|
187
|
+
only the one the identity came through — a login links under the federation
|
|
188
|
+
the user signed in through, and an IdP whose `sub` is pairwise per app
|
|
189
|
+
registration (Entra's is) gives the same person another `sub` under each. For
|
|
190
|
+
a separate registration for grants (the ADR's
|
|
191
|
+
[D19](../core/docs/adr/2026-09-17-federation-grants-offline-delegation.md#d19--entra-on-behalf-of-is-not-implemented-and-consent-accumulates)),
|
|
192
|
+
that means: to answer `unlinked`
|
|
193
|
+
honestly, your Store must resolve the verified `(tid, oid)` against an
|
|
194
|
+
authoritative directory complete for the relevant tenant and all local
|
|
195
|
+
ownership links across registrations; no match in a partial directory or an
|
|
196
|
+
unfamiliar identity is `identity_not_resolvable`, and multiple distinct local
|
|
197
|
+
owners are a server error. Declaring coverage attests to that strategy; boot
|
|
198
|
+
neither discovers nor proves the remote directory's completeness.
|
|
199
|
+
|
|
200
|
+
The request is `POST findSubjectByFederatedIdentityUrl` with
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{ "provider": "entra-files", "issuer": "https://login.microsoftonline.com/<tenant>/v2.0",
|
|
204
|
+
"clientId": "<the grants app registration>", "sub": "<verified sub>",
|
|
205
|
+
"claims": { "tid": "<verified>", "oid": "<verified>" } }
|
|
58
206
|
```
|
|
59
207
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
208
|
+
— the registration exactly as the connection is configured, the verified
|
|
209
|
+
`sub`, and every claim the connection's `identityClaims` named (verified,
|
|
210
|
+
from the id_token; `{}` when it named none). The Store must not log, persist
|
|
211
|
+
or echo those claims — core's
|
|
212
|
+
[`FederatedIdentityLookup`](../core/src/repositories/UserRepository.mts) marks
|
|
213
|
+
them transient. The lookup must change nothing:
|
|
214
|
+
no login stamped, no link made, no user provisioned. The answer is a `2xx`
|
|
215
|
+
JSON body, one of
|
|
216
|
+
|
|
217
|
+
| Body | Meaning |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `{ "kind": "linked", "subject": "<local User.id>" }` | exactly one local user holds it. `subject` is that user's `id` **byte for byte** — the callback compares it with the signed-in user's `sub` exactly, so a padded or otherwise normalised id reads as another user's |
|
|
220
|
+
| `{ "kind": "unlinked" }` | a **complete** resolution found nobody |
|
|
221
|
+
| `{ "kind": "indeterminate", "reason": "registration_not_covered" }` | no strategy for this registration |
|
|
222
|
+
| `{ "kind": "indeterminate", "reason": "identity_not_resolvable" }` | a strategy, and this identity is not in it |
|
|
223
|
+
|
|
224
|
+
Fields beyond those are ignored. **Everything else is an outage, never
|
|
225
|
+
"nobody"**: any status but a `2xx` — `404`, `401`, `403`, `409`, `5xx` — a
|
|
226
|
+
`2xx` whose body is empty or not one of the four, a redirect (never followed:
|
|
227
|
+
the body carries a verified identity), a timeout, or a body over the cap all
|
|
228
|
+
throw, and the callback answers `temporarily_unavailable`. More than one
|
|
229
|
+
distinct local owner is a `500` from the Store. What is thrown names the
|
|
230
|
+
endpoint — and, for an answer with a status, the status — never the body,
|
|
231
|
+
the identity, a status text or an underlying cause.
|
|
232
|
+
|
|
233
|
+
**Coverage.** The probe the grants module asks at boot is synchronous and
|
|
234
|
+
cannot reach the Store, so `federatedIdentityLookupCoverage` relays what the
|
|
235
|
+
Store's lookup covers: one entry per registration,
|
|
236
|
+
`{ provider, issuer, clientId, requiredClaims }`, all four fields, compared
|
|
237
|
+
exactly (no trimming, no case folding, no trailing-slash tolerance), and
|
|
238
|
+
`requiredClaims` the claim names the strategy needs — `[]` for one on the
|
|
239
|
+
registration and `sub` alone. `supportsFederatedIdentityLookup(registration,
|
|
240
|
+
identityClaims)` is `true` when an entry equals the registration and every
|
|
241
|
+
`requiredClaims` name is among `identityClaims`. When federation grants are enabled and have at
|
|
242
|
+
least one connection and `federationGrants.identityLookup` is `"required"` (the
|
|
243
|
+
default), the grants module refuses at boot every connection it is `false` for;
|
|
244
|
+
under `"unsupported"`, or with no connection, it asks nothing. A registration nobody declared is
|
|
245
|
+
answered `registration_not_covered` locally, and a declared one arriving
|
|
246
|
+
without a required claim `identity_not_resolvable`, with no request either
|
|
247
|
+
way. The declaration is snapshotted at construction; a duplicate registration,
|
|
248
|
+
a malformed entry, or coverage without the URL is a construction error. It has
|
|
249
|
+
no environment-variable form (a list is HOCON's), as `federationGrants.connections`
|
|
250
|
+
has none. `supportsFederatedIdentityLookup` and `findSubjectByFederatedIdentity`
|
|
251
|
+
are absent when `findSubjectByFederatedIdentityUrl` is not configured, and a
|
|
252
|
+
deployment that then has a connection under `"required"` is refused at boot.
|
|
253
|
+
|
|
254
|
+
## What the Store must enforce itself
|
|
255
|
+
|
|
256
|
+
- **Who may call it.** What `authenticateByToken` and linking carry is not a
|
|
257
|
+
secret — the federation callback's `<provider>:<sub>` is an identifier — so a
|
|
258
|
+
Store that answers any caller lets anyone who can reach
|
|
259
|
+
`authenticateByTokenUrl` resolve a known identity to its user, and anyone who
|
|
260
|
+
can reach an open `linkFederatedIdentityUrl` bind any identity to any
|
|
261
|
+
`userId`. Configure `bearerToken` (`CLIENT_USER_BEARER_TOKEN`, from
|
|
262
|
+
`openssl rand -hex 32`) and have the Store refuse every request on all four
|
|
263
|
+
endpoints whose `Authorization` is not exactly `Bearer <that token>`,
|
|
264
|
+
compared in constant time, and never log the header. One token goes to all
|
|
265
|
+
four URLs, so they must be one trust domain: whoever runs any of those
|
|
266
|
+
endpoints holds a credential the others accept. Refuse with `401` and
|
|
267
|
+
`WWW-Authenticate: Bearer error="invalid_token"` (RFC 6750 §3), or `403` and
|
|
268
|
+
`error="insufficient_scope"` for a token that is valid but not enough. With
|
|
269
|
+
that challenge, a token the Store does not accept — a typo, a half-finished
|
|
270
|
+
rotation — is an outage on every call, which each caller reports as the
|
|
271
|
+
table below shows. Without the challenge the `401` or `403` keeps its wire
|
|
272
|
+
meaning, "no such user" or a refused link, and a mismatch shows only as
|
|
273
|
+
every login failing. For the same reason, never send a `Bearer` challenge
|
|
274
|
+
with a user's wrong password, an unknown identity or a link your policy
|
|
275
|
+
refuses: that answer would read as the Store refusing auth.provider, and the
|
|
276
|
+
user's failed login or link as an outage. To rotate, have the Store accept the old token and the new, move
|
|
277
|
+
auth.provider to the new, then retire the old.
|
|
278
|
+
Without `bearerToken` no request carries an `Authorization` header, and the
|
|
279
|
+
Store must admit only auth.provider some other way — a network policy or a
|
|
280
|
+
private network, or mutual TLS provided by the platform in front of the
|
|
281
|
+
Store (a sidecar on a loopback address, which the `http` carve-out admits).
|
|
282
|
+
This adapter offers no client certificate of its own: Node's `fetch` takes
|
|
283
|
+
one only through an `undici` dispatcher, a dependency this package does not
|
|
284
|
+
carry. A URL carrying `user:password@` is refused.
|
|
285
|
+
- **No secret in the URL.** A query-string token would not stay secret: it
|
|
286
|
+
travels in every request line, to the Store's own access logs and to any
|
|
287
|
+
proxy in between. The errors this adapter throws — which the session routes
|
|
288
|
+
log — name an endpoint by its origin and path only, never its query or
|
|
289
|
+
fragment, so the query at least does not reach this deployment's log. The
|
|
290
|
+
caller's credential belongs in `bearerToken`, which nothing this adapter
|
|
291
|
+
throws carries.
|
|
292
|
+
- **Answer without redirecting.** No request follows a redirect, so a
|
|
293
|
+
password, a token, a link request or an identity goes only to the configured
|
|
294
|
+
URL — the one the `https` rule below checks — and no answer from anywhere
|
|
295
|
+
else is taken as the user, the link or the lookup's answer. A `3xx` from any
|
|
296
|
+
of the four endpoints throws like any other unexpected status (the session
|
|
297
|
+
routes and the jwt-bearer grant answer `503 temporarily_unavailable`, the
|
|
298
|
+
grants callback `temporarily_unavailable`), so a Store behind a URL that
|
|
299
|
+
redirects — a host alias redirecting to the canonical host, an added
|
|
300
|
+
trailing slash, a moved path — fails every call. Configure each URL as the
|
|
301
|
+
endpoint that answers, not one that redirects.
|
|
302
|
+
|
|
303
|
+
**What a refused token looks like, per caller.** Each caller answers a
|
|
304
|
+
`StoreCredentialRefusedError` as it answers any Store failure — a
|
|
305
|
+
`StoreTransportError` or a `TimeoutError` too; what differs is what it logs:
|
|
306
|
+
|
|
307
|
+
| Caller | Answers | Logs |
|
|
308
|
+
| --- | --- | --- |
|
|
309
|
+
| Password login, `POST /session/login` ([`@o3co/auth-provider-session`](../session/README.md)) | `503 temporarily_unavailable` | `login_store_unavailable` (error, `store: "user_repository"`), with `err` |
|
|
310
|
+
| Federation login and `?link=1` callbacks (session) | `503 temporarily_unavailable` | `federation_callback_store_unavailable` or `federation_link_store_unavailable` (error, `store: "user_repository"`), with `err` |
|
|
311
|
+
| The jwt-bearer grant ([`@o3co/auth-provider-oauth`](../oauth/README.md)) | `503 temporarily_unavailable` | `jwt_bearer_user_repository_unavailable` (error), with `err` |
|
|
312
|
+
| The federation-grants connect callback — the identity lookup ([`@o3co/auth-provider-federation-grants`](../federation-grants/README.md)) | redirect with `error=temporarily_unavailable` | `federation_grant_callback_unavailable` (error), `store: "user_directory"`, with `err` |
|
|
313
|
+
|
|
314
|
+
Where `err` is logged, a `StoreCredentialRefusedError`'s message names the
|
|
315
|
+
Store endpoint (its origin and path), the status and
|
|
316
|
+
`CLIENT_USER_BEARER_TOKEN` — never the token; a `StoreTransportError`'s names
|
|
317
|
+
the endpoint the same way, what failed and at most a transport code. No
|
|
318
|
+
message this adapter throws quotes a URL's query string or fragment.
|
|
319
|
+
|
|
320
|
+
## Constructor validation
|
|
75
321
|
|
|
76
322
|
Every option is validated in the **constructor**, so a misconfigured deployment
|
|
77
323
|
fails at boot rather than at the first login attempt.
|
|
78
324
|
|
|
79
|
-
**Every URL must use `https://`** (the link
|
|
80
|
-
password on `authenticateUrl`, a token on `authenticateByTokenUrl
|
|
325
|
+
**Every URL must use `https://`** (the link and lookup endpoints included). They carry plaintext user credentials — a
|
|
326
|
+
password on `authenticateUrl`, a token on `authenticateByTokenUrl`, a verified
|
|
327
|
+
upstream identity on `findSubjectByFederatedIdentityUrl` — so an
|
|
81
328
|
`http://` URL does not merely weaken the connection, it publishes the credential
|
|
82
|
-
to every hop on the path.
|
|
329
|
+
to every hop on the path. The URL checked here is the only place a request goes
|
|
330
|
+
and the only one whose answer is taken: no request follows a redirect, so a
|
|
331
|
+
`307` or `308` cannot re-send the body to, and no `301`, `302` or `303` can fetch an answer from, a `Location` this rule never saw.
|
|
83
332
|
|
|
84
333
|
**The one carve-out is loopback:** `http://` is accepted when the host is
|
|
85
334
|
`localhost`, an address in `127.0.0.0/8`, or `[::1]`. That traffic never leaves
|
|
@@ -94,44 +343,82 @@ This is the same rule `oauth.jwt.issuer` applies in
|
|
|
94
343
|
[`@o3co/auth-provider-core`](../core/README.md), with the carve-out widened from
|
|
95
344
|
the single address `127.0.0.1` to the whole `127.0.0.0/8` block, and query
|
|
96
345
|
strings allowed (an issuer may not carry one; a POST endpoint legitimately may).
|
|
346
|
+
A query is sent, and never quoted: every error names the endpoint by origin
|
|
347
|
+
and path.
|
|
97
348
|
|
|
98
349
|
**`timeout` must be a positive integer** no greater than `2147483647`
|
|
99
350
|
milliseconds. `0`, a negative number, and `NaN` all clamp to "fire immediately"
|
|
100
351
|
in `setTimeout` — which would abort every request — and a value above Node's
|
|
101
352
|
timer range clamps to 1ms, turning "be patient" into the most impatient setting
|
|
102
|
-
available.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
—
|
|
107
|
-
|
|
353
|
+
available. A blank environment override is a boot failure, not the default. The
|
|
354
|
+
deadline covers the whole exchange, **body included**: the body read is raced
|
|
355
|
+
against it rather than relying on the abort signal, because aborting a request
|
|
356
|
+
does not reliably interrupt a read already in flight. That is the slow-loris
|
|
357
|
+
shape — headers arrive promptly, then the body dribbles or stops — and without
|
|
358
|
+
the race it hangs forever. A request that outlives the deadline rejects with a
|
|
359
|
+
`timed out after <n>ms` error naming the endpoint, named `TimeoutError` on all
|
|
360
|
+
four requests, so a reporter that classifies by name reads it as a timeout.
|
|
108
361
|
|
|
109
362
|
**`maxResponseBytes` must be a positive integer**, defaulting to
|
|
110
363
|
`DEFAULT_MAX_RESPONSE_BYTES` (1 MiB). The cap is enforced against
|
|
111
364
|
`Content-Length` *and* while streaming, so a Store that omits the header — or
|
|
112
365
|
lies in it — is still cut off rather than allowed to exhaust memory.
|
|
113
366
|
|
|
114
|
-
|
|
367
|
+
**`bearerToken`, when set, must be a bare RFC 6750 token of at least 32 bytes
|
|
368
|
+
of key material.** Unset (absent) sends no `Authorization` header. Set, it is
|
|
369
|
+
refused unless it is a string, not blank (a blank environment override is a
|
|
370
|
+
boot failure, not "no token"), made only of letters, digits, `-._~+/` and
|
|
371
|
+
trailing `=` padding — no whitespace, no line break, and no `Bearer ` prefix,
|
|
372
|
+
which the adapter adds — and it clears core's shared-secret floor
|
|
373
|
+
(`MIN_SECRET_ENTROPY_BYTES`, the one `SESSION_SECRET` and `OAUTH_JWT_SECRET`
|
|
374
|
+
clear), measured on the decoded length of a hex or base64 value, so
|
|
375
|
+
`openssl rand -hex 16` is 16 bytes however long it looks. Whoever holds the
|
|
376
|
+
token speaks to the Store as auth.provider. The shape is checked here because
|
|
377
|
+
a header value `fetch` refuses is one it quotes in the error it throws. No
|
|
378
|
+
refusal quotes the value, no error from a request carries it — a transport
|
|
379
|
+
failure is thrown without the transport's error (see the wire contract) — and
|
|
380
|
+
it is held in an ECMAScript private field, so it is absent from `inspect()`
|
|
381
|
+
and `JSON.stringify` of the repository. These checks run when the repository
|
|
382
|
+
is built, which a deployment does under `repositories.user.type = "http"`
|
|
383
|
+
(the standalone template's default); under core's `reference.conf` default,
|
|
384
|
+
`yaml`, the `http` block — and a token in it — is not read at all.
|
|
115
385
|
|
|
116
|
-
|
|
117
|
-
import { createRepositoryFactories } from "@o3co/auth-provider-core";
|
|
118
|
-
import { registerBuiltinAdapters } from "@o3co/auth-provider-foundation";
|
|
386
|
+
## Public API
|
|
119
387
|
|
|
120
|
-
|
|
388
|
+
Exported from [`src/index.mts`](src/index.mts):
|
|
121
389
|
|
|
122
|
-
registerBuiltinAdapters({ userFactory })
|
|
390
|
+
- `registerBuiltinAdapters({ userFactory })` — registers the `"http"` type.
|
|
391
|
+
- `HttpUserRepository` — the repository
|
|
392
|
+
([`src/repositories/HttpUserRepository.mts`](src/repositories/HttpUserRepository.mts)).
|
|
393
|
+
- `StoreCredentialRefusedError` — what a refused credential throws; its
|
|
394
|
+
`name` and `storeStatus` (`401` or `403`) are part of the contract. Not
|
|
395
|
+
`status`: Express, http-errors and the standalone's terminal handler read
|
|
396
|
+
that as the status to answer with.
|
|
397
|
+
- `StoreTransportError`, `StoreTransportFailure` — what a transport failure
|
|
398
|
+
throws; its `name`, `reason` and `code` are part of the contract, and it has
|
|
399
|
+
no `status` either
|
|
400
|
+
([`src/repositories/storeErrors.mts`](src/repositories/storeErrors.mts)).
|
|
401
|
+
- `DEFAULT_MAX_RESPONSE_BYTES` — the default response cap.
|
|
402
|
+
- `FederatedIdentityLookupCoverage` — the type of one coverage entry.
|
|
123
403
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
404
|
+
## Tests
|
|
405
|
+
|
|
406
|
+
| Test file | Pins |
|
|
407
|
+
| --- | --- |
|
|
408
|
+
| [`HttpUserRepository.test.mts`](src/repositories/__tests__/HttpUserRepository.test.mts) | authentication and its answers, the `User` shape check, the https rule, the timeout and the response cap, linking, and the identity lookup's presence, probe and wire |
|
|
409
|
+
| [`HttpUserRepository.transport.test.mts`](src/repositories/__tests__/HttpUserRepository.transport.test.mts) | against real HTTP servers: the identity lookup releasing a refused answer's connection, and a redirect refused on each of the four requests — to another origin, to the same origin, or with no `Location` — with nothing sent to a redirect target |
|
|
410
|
+
| [`HttpUserRepository.credential.test.mts`](src/repositories/__tests__/HttpUserRepository.credential.test.mts) | against real HTTP servers: `Authorization: Bearer <token>` on each of the four requests when `bearerToken` is set and no `Authorization` header when it is not, by hand and through the `"http"` builder; a weak, malformed, blank or non-string token refused at construction; the token in no failure and no inspection of the repository; a transport failure a `StoreTransportError` that says what failed — a refused connection and an https URL on a plain-HTTP port (not reached), a peer that reflects the token into a status line or header, a head over the size limit (malformed response), a close after a `1xx`, mid-head, before any byte, or of a pooled keep-alive connection between two requests (connection closed), a chunked body (not read), a TLS 1.2 server that refuses the handshake (not reached, `ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE`) — with its code and no cause; a timeout a `TimeoutError` whether the headers or the body stall; with a token sent, a `401` or `403` with a `Bearer` challenge a `StoreCredentialRefusedError` (`storeStatus`, no `status`) on each of the four, and one without — or with no token sent — read as before |
|
|
411
|
+
| [`storeErrors.test.mts`](src/repositories/__tests__/storeErrors.test.mts) | which transport codes are kept, and the two named errors' shape — no `status`, no cause |
|
|
412
|
+
| [`wwwAuthenticate.test.mts`](src/repositories/__tests__/wwwAuthenticate.test.mts) | which `WWW-Authenticate` values carry a `Bearer` challenge, and a hostile 64 KiB value read in one pass |
|
|
413
|
+
| [`registerBuiltinAdapters.test.mts`](src/repositories/__tests__/registerBuiltinAdapters.test.mts) | the `"http"` builder, its defaults and string coercion, and configuration refused at build time |
|
|
414
|
+
| [`endpointUrl.test.mts`](src/__tests__/endpointUrl.test.mts) | the https-or-loopback rule |
|
|
132
415
|
|
|
133
|
-
## See
|
|
416
|
+
## See also
|
|
134
417
|
|
|
135
|
-
- [`@o3co/auth-provider-core`](../core/README.md) —
|
|
136
|
-
|
|
137
|
-
- [auth
|
|
418
|
+
- [`@o3co/auth-provider-core`](../core/README.md) — the `UserRepository` port,
|
|
419
|
+
`createRepositoryFactories` and the development user adapters
|
|
420
|
+
- [`@o3co/auth-provider-session`](../session/README.md) — the routes that call
|
|
421
|
+
`authenticate`, `authenticateByToken` and `linkFederatedIdentity`
|
|
422
|
+
- [`@o3co/auth-provider-federation-grants`](../federation-grants/README.md) — the
|
|
423
|
+
caller of the identity lookup
|
|
424
|
+
- [auth.provider](../../README.md) — top-level repository documentation
|
package/dist/endpointUrl.d.mts
CHANGED
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
* runs on, though, is shared vocabulary: `isLoopbackHostname` comes from
|
|
28
28
|
* `@o3co/auth-provider-core` (`net/loopback`, #364), the same definition the
|
|
29
29
|
* session redirect policy consumes, so the carve-outs cannot drift apart.
|
|
30
|
+
*
|
|
31
|
+
* ## What a message names of an endpoint
|
|
32
|
+
*
|
|
33
|
+
* Every failure the repository throws names the endpoint it was talking to,
|
|
34
|
+
* and every caller logs what it throws. A message names the endpoint by its
|
|
35
|
+
* origin and path alone ({@link endpointForMessage}): a query string is
|
|
36
|
+
* accepted in a Store URL, and a deployment may carry a credential there
|
|
37
|
+
* (`?api_key=…`) despite the README's advice, so the query never reaches a
|
|
38
|
+
* message — nor does a fragment, which no request sends anyway.
|
|
30
39
|
*/
|
|
31
40
|
import { isLoopbackHostname } from "@o3co/auth-provider-core";
|
|
32
41
|
export { isLoopbackHostname };
|
|
@@ -56,4 +65,15 @@ export declare function describeEndpointRejection(reason: EndpointRejection): st
|
|
|
56
65
|
* lands in logs.
|
|
57
66
|
*/
|
|
58
67
|
export declare function assertSecureEndpoint(value: unknown, field: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* `url` as a message names it: its origin and path — `https://store.example
|
|
70
|
+
* /authenticate`, as the WHATWG URL parser normalises them (a default port
|
|
71
|
+
* dropped, scheme and host lower-cased, an IPv6 literal in brackets) — and
|
|
72
|
+
* never its query or fragment, which may carry a credential. Anything that is
|
|
73
|
+
* not an http or https URL is named by what it is not: its origin would be
|
|
74
|
+
* `"null"` and its "path" the rest of it (`data:…`, `mailto:…`), and the
|
|
75
|
+
* constructor refuses such a Store URL anyway — but a caller can hand
|
|
76
|
+
* `StoreCredentialRefusedError` anything.
|
|
77
|
+
*/
|
|
78
|
+
export declare function endpointForMessage(url: string): string;
|
|
59
79
|
//# sourceMappingURL=endpointUrl.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpointUrl.d.mts","sourceRoot":"","sources":["../src/endpointUrl.mts"],"names":[],"mappings":"AAgBA
|
|
1
|
+
{"version":3,"file":"endpointUrl.d.mts","sourceRoot":"","sources":["../src/endpointUrl.mts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAC1B,cAAc,GACd,OAAO,GACP,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,iBAAiB,CAAC;AAKrB;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAyB5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAwB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAS1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQtD"}
|
package/dist/endpointUrl.mjs
CHANGED
|
@@ -42,6 +42,15 @@
|
|
|
42
42
|
* runs on, though, is shared vocabulary: `isLoopbackHostname` comes from
|
|
43
43
|
* `@o3co/auth-provider-core` (`net/loopback`, #364), the same definition the
|
|
44
44
|
* session redirect policy consumes, so the carve-outs cannot drift apart.
|
|
45
|
+
*
|
|
46
|
+
* ## What a message names of an endpoint
|
|
47
|
+
*
|
|
48
|
+
* Every failure the repository throws names the endpoint it was talking to,
|
|
49
|
+
* and every caller logs what it throws. A message names the endpoint by its
|
|
50
|
+
* origin and path alone ({@link endpointForMessage}): a query string is
|
|
51
|
+
* accepted in a Store URL, and a deployment may carry a credential there
|
|
52
|
+
* (`?api_key=…`) despite the README's advice, so the query never reaches a
|
|
53
|
+
* message — nor does a fragment, which no request sends anyway.
|
|
45
54
|
*/
|
|
46
55
|
import { isLoopbackHostname } from "@o3co/auth-provider-core";
|
|
47
56
|
// Re-exported unchanged: this module's callers (and its tests) read the
|
|
@@ -127,3 +136,24 @@ export function assertSecureEndpoint(value, field) {
|
|
|
127
136
|
}
|
|
128
137
|
return value;
|
|
129
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* `url` as a message names it: its origin and path — `https://store.example
|
|
141
|
+
* /authenticate`, as the WHATWG URL parser normalises them (a default port
|
|
142
|
+
* dropped, scheme and host lower-cased, an IPv6 literal in brackets) — and
|
|
143
|
+
* never its query or fragment, which may carry a credential. Anything that is
|
|
144
|
+
* not an http or https URL is named by what it is not: its origin would be
|
|
145
|
+
* `"null"` and its "path" the rest of it (`data:…`, `mailto:…`), and the
|
|
146
|
+
* constructor refuses such a Store URL anyway — but a caller can hand
|
|
147
|
+
* `StoreCredentialRefusedError` anything.
|
|
148
|
+
*/
|
|
149
|
+
export function endpointForMessage(url) {
|
|
150
|
+
try {
|
|
151
|
+
const { protocol, origin, pathname } = new URL(url);
|
|
152
|
+
if (protocol === "http:" || protocol === "https:")
|
|
153
|
+
return `${origin}${pathname}`;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Named below, like any other value that is not an http or https URL.
|
|
157
|
+
}
|
|
158
|
+
return "(an endpoint that is not an http or https URL)";
|
|
159
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -2,5 +2,6 @@ import type { AdapterFactory, UserRepository } from "@o3co/auth-provider-core";
|
|
|
2
2
|
export declare const registerBuiltinAdapters: (factories: {
|
|
3
3
|
userFactory: AdapterFactory<UserRepository>;
|
|
4
4
|
}) => void;
|
|
5
|
-
export { DEFAULT_MAX_RESPONSE_BYTES, HttpUserRepository, } from "./repositories/HttpUserRepository.mjs";
|
|
5
|
+
export { DEFAULT_MAX_RESPONSE_BYTES, type FederatedIdentityLookupCoverage, HttpUserRepository, } from "./repositories/HttpUserRepository.mjs";
|
|
6
|
+
export { StoreCredentialRefusedError, StoreTransportError, type StoreTransportFailure, } from "./repositories/storeErrors.mjs";
|
|
6
7
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AA8B/E,eAAO,MAAM,uBAAuB,GAAI,WAAW;IAClD,WAAW,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;CAC5C,KAAG,IA0CH,CAAC;AAEF,OAAO,EACN,0BAA0B,EAC1B,KAAK,+BAA+B,EACpC,kBAAkB,GAClB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACN,2BAA2B,EAC3B,mBAAmB,EACnB,KAAK,qBAAqB,GAC1B,MAAM,gCAAgC,CAAC"}
|
package/dist/index.mjs
CHANGED
|
@@ -56,9 +56,29 @@ export const registerBuiltinAdapters = (factories) => {
|
|
|
56
56
|
...(typeof config.linkFederatedIdentityUrl === "string"
|
|
57
57
|
? { linkFederatedIdentityUrl: config.linkFederatedIdentityUrl }
|
|
58
58
|
: {}),
|
|
59
|
+
// #613: optional. Forwarded whenever SET, not only when well-typed —
|
|
60
|
+
// the link URL above vanishes when misspelt, and for the lookup a value
|
|
61
|
+
// that vanishes is a deployment that believes itself covered and is
|
|
62
|
+
// not. The constructor refuses what is not a URL, or not a list.
|
|
63
|
+
...(config.findSubjectByFederatedIdentityUrl !== undefined
|
|
64
|
+
? {
|
|
65
|
+
findSubjectByFederatedIdentityUrl: config.findSubjectByFederatedIdentityUrl,
|
|
66
|
+
}
|
|
67
|
+
: {}),
|
|
68
|
+
...(config.federatedIdentityLookupCoverage !== undefined
|
|
69
|
+
? {
|
|
70
|
+
federatedIdentityLookupCoverage: config.federatedIdentityLookupCoverage,
|
|
71
|
+
}
|
|
72
|
+
: {}),
|
|
73
|
+
// Optional, and forwarded whenever SET for the lookup URL's reason: a
|
|
74
|
+
// token that vanished would be a deployment that believes its Store
|
|
75
|
+
// calls authenticated and sends them bare. The constructor refuses
|
|
76
|
+
// what is not a string, and what is blank, malformed or too weak.
|
|
77
|
+
...(config.bearerToken !== undefined ? { bearerToken: config.bearerToken } : {}),
|
|
59
78
|
timeout: toNumber(config.timeout, DEFAULT_TIMEOUT_MS),
|
|
60
79
|
maxResponseBytes: toNumber(config.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES),
|
|
61
80
|
});
|
|
62
81
|
});
|
|
63
82
|
};
|
|
64
83
|
export { DEFAULT_MAX_RESPONSE_BYTES, HttpUserRepository, } from "./repositories/HttpUserRepository.mjs";
|
|
84
|
+
export { StoreCredentialRefusedError, StoreTransportError, } from "./repositories/storeErrors.mjs";
|