@o3co/auth-provider-foundation 0.12.1 → 0.13.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.md
CHANGED
|
@@ -40,6 +40,7 @@ class HttpUserRepository implements UserRepository {
|
|
|
40
40
|
constructor(options: {
|
|
41
41
|
authenticateUrl: string; // POST endpoint for username/password auth
|
|
42
42
|
authenticateByTokenUrl: string; // POST endpoint for token-based auth
|
|
43
|
+
linkFederatedIdentityUrl?: string; // POST endpoint that links a federated identity (#482)
|
|
43
44
|
timeout: number; // request timeout in milliseconds
|
|
44
45
|
maxResponseBytes?: number; // response body cap, default 1 MiB
|
|
45
46
|
});
|
|
@@ -49,6 +50,10 @@ class HttpUserRepository implements UserRepository {
|
|
|
49
50
|
|
|
50
51
|
// POST authenticateByTokenUrl with body: { token }
|
|
51
52
|
authenticateByToken(token: string): Promise<User | null>;
|
|
53
|
+
|
|
54
|
+
// POST linkFederatedIdentityUrl with body: { userId, provider, sub, token, claims }
|
|
55
|
+
// — present only when the URL is configured (#482)
|
|
56
|
+
linkFederatedIdentity?(userId: string, identity: FederatedIdentityLink): Promise<LinkFederatedIdentityResult>;
|
|
52
57
|
}
|
|
53
58
|
```
|
|
54
59
|
|
|
@@ -57,13 +62,21 @@ class HttpUserRepository implements UserRepository {
|
|
|
57
62
|
- Throws when the upstream returns 2xx with a body that is not a JSON `User`
|
|
58
63
|
(`{ id: string, username: string, … }`) — an upstream failure, not a
|
|
59
64
|
"user not found".
|
|
65
|
+
- `linkFederatedIdentity` (#482): a `2xx` `User` is `{ ok: true, user }`;
|
|
66
|
+
`401` / `403` is `{ ok: false, reason: "refused" }`; `409` is
|
|
67
|
+
`{ ok: false, reason: "conflict" }`; anything else throws. The method is
|
|
68
|
+
absent when `linkFederatedIdentityUrl` is not configured, which is how the
|
|
69
|
+
federation routes know to refuse `?link=1` up front. What a Store checks
|
|
70
|
+
before answering `2xx` — never on an unverified or relay address, never by
|
|
71
|
+
e-mail alone — is in the
|
|
72
|
+
[session package README](../session/README.md#account-linking-across-federations-482).
|
|
60
73
|
|
|
61
74
|
### Constructor validation
|
|
62
75
|
|
|
63
76
|
Every option is validated in the **constructor**, so a misconfigured deployment
|
|
64
77
|
fails at boot rather than at the first login attempt.
|
|
65
78
|
|
|
66
|
-
**
|
|
79
|
+
**Every URL must use `https://`** (the link endpoint included). They carry plaintext user credentials — a
|
|
67
80
|
password on `authenticateUrl`, a token on `authenticateByTokenUrl` — so an
|
|
68
81
|
`http://` URL does not merely weaken the connection, it publishes the credential
|
|
69
82
|
to every hop on the path.
|
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;AA6B/E,eAAO,MAAM,uBAAuB,GAAI,WAAW;IAClD,WAAW,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;CAC5C,KAAG,
|
|
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;AA6B/E,eAAO,MAAM,uBAAuB,GAAI,WAAW;IAClD,WAAW,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;CAC5C,KAAG,IAsBH,CAAC;AAEF,OAAO,EACN,0BAA0B,EAC1B,kBAAkB,GAClB,MAAM,uCAAuC,CAAC"}
|
package/dist/index.mjs
CHANGED
|
@@ -52,6 +52,10 @@ export const registerBuiltinAdapters = (factories) => {
|
|
|
52
52
|
return new HttpUserRepository({
|
|
53
53
|
authenticateUrl: config.authenticateUrl,
|
|
54
54
|
authenticateByTokenUrl: config.authenticateByTokenUrl,
|
|
55
|
+
// #482: optional. Present → the repository can link a federated identity.
|
|
56
|
+
...(typeof config.linkFederatedIdentityUrl === "string"
|
|
57
|
+
? { linkFederatedIdentityUrl: config.linkFederatedIdentityUrl }
|
|
58
|
+
: {}),
|
|
55
59
|
timeout: toNumber(config.timeout, DEFAULT_TIMEOUT_MS),
|
|
56
60
|
maxResponseBytes: toNumber(config.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES),
|
|
57
61
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { User, UserRepository } from "@o3co/auth-provider-core";
|
|
1
|
+
import type { FederatedIdentityLink, LinkFederatedIdentityResult, User, UserRepository } from "@o3co/auth-provider-core";
|
|
2
2
|
/**
|
|
3
3
|
* Default ceiling on an upstream response body, in bytes.
|
|
4
4
|
*
|
|
@@ -22,14 +22,30 @@ export declare class HttpUserRepository implements UserRepository {
|
|
|
22
22
|
private authenticateByTokenUrl;
|
|
23
23
|
private timeout;
|
|
24
24
|
private maxResponseBytes;
|
|
25
|
-
|
|
25
|
+
private linkFederatedIdentityUrl?;
|
|
26
|
+
/**
|
|
27
|
+
* #482 — see {@link UserRepository.linkFederatedIdentity}. Present only when
|
|
28
|
+
* `linkFederatedIdentityUrl` is configured, which is how the federation routes
|
|
29
|
+
* know to refuse `?link=1` up front instead of at the callback.
|
|
30
|
+
*/
|
|
31
|
+
readonly linkFederatedIdentity?: (userId: string, identity: FederatedIdentityLink) => Promise<LinkFederatedIdentityResult>;
|
|
32
|
+
constructor({ authenticateUrl, authenticateByTokenUrl, linkFederatedIdentityUrl, timeout, maxResponseBytes, }: {
|
|
26
33
|
authenticateUrl: string;
|
|
27
34
|
authenticateByTokenUrl: string;
|
|
35
|
+
/** #482: optional; the Store's link endpoint. Same https rule as the other two. */
|
|
36
|
+
linkFederatedIdentityUrl?: string;
|
|
28
37
|
timeout: number;
|
|
29
38
|
maxResponseBytes?: number;
|
|
30
39
|
});
|
|
31
40
|
authenticate(username: string, password: string): Promise<User | null>;
|
|
32
41
|
authenticateByToken(token: string): Promise<User | null>;
|
|
42
|
+
/**
|
|
43
|
+
* POST `{ userId, provider, sub, token, claims }` to the Store's link endpoint
|
|
44
|
+
* (#482). A `2xx` `User` is the linked account; `401` / `403` is the Store's
|
|
45
|
+
* refusal (policy — an unverified or relay address, one identity per
|
|
46
|
+
* provider, …); `409` says the identity is already someone else's.
|
|
47
|
+
*/
|
|
48
|
+
private linkViaHttp;
|
|
33
49
|
private post;
|
|
34
50
|
}
|
|
35
51
|
//# sourceMappingURL=HttpUserRepository.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpUserRepository.d.mts","sourceRoot":"","sources":["../../src/repositories/HttpUserRepository.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"HttpUserRepository.d.mts","sourceRoot":"","sources":["../../src/repositories/HttpUserRepository.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACX,qBAAqB,EACrB,2BAA2B,EAC3B,IAAI,EACJ,cAAc,EACd,MAAM,0BAA0B,CAAC;AAMlC;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,QAAc,CAAC;AAkItD;;;;;;;;;GASG;AACH,qBAAa,kBAAmB,YAAW,cAAc;IACxD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,wBAAwB,CAAC,CAAS;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAChC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,qBAAqB,KAC3B,OAAO,CAAC,2BAA2B,CAAC,CAAC;gBAE9B,EACX,eAAe,EACf,sBAAsB,EACtB,wBAAwB,EACxB,OAAO,EACP,gBAA6C,GAC7C,EAAE;QACF,eAAe,EAAE,MAAM,CAAC;QACxB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,mFAAmF;QACnF,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,OAAO,EAAE,MAAM,CAAC;QAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC1B;IA4BK,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAKtE,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAI9D;;;;;OAKG;YACW,WAAW;YAWX,IAAI;CA4ElB"}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { assertSecureEndpoint } from "../endpointUrl.mjs";
|
|
17
|
+
/** The Store answered 409 to a link request: the identity is already someone else's (#482). */
|
|
18
|
+
const CONFLICT = Symbol("conflict");
|
|
17
19
|
/**
|
|
18
20
|
* Default ceiling on an upstream response body, in bytes.
|
|
19
21
|
*
|
|
@@ -152,9 +154,20 @@ export class HttpUserRepository {
|
|
|
152
154
|
authenticateByTokenUrl;
|
|
153
155
|
timeout;
|
|
154
156
|
maxResponseBytes;
|
|
155
|
-
|
|
157
|
+
linkFederatedIdentityUrl;
|
|
158
|
+
/**
|
|
159
|
+
* #482 — see {@link UserRepository.linkFederatedIdentity}. Present only when
|
|
160
|
+
* `linkFederatedIdentityUrl` is configured, which is how the federation routes
|
|
161
|
+
* know to refuse `?link=1` up front instead of at the callback.
|
|
162
|
+
*/
|
|
163
|
+
linkFederatedIdentity;
|
|
164
|
+
constructor({ authenticateUrl, authenticateByTokenUrl, linkFederatedIdentityUrl, timeout, maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, }) {
|
|
156
165
|
this.authenticateUrl = assertSecureEndpoint(authenticateUrl, "authenticateUrl");
|
|
157
166
|
this.authenticateByTokenUrl = assertSecureEndpoint(authenticateByTokenUrl, "authenticateByTokenUrl");
|
|
167
|
+
if (linkFederatedIdentityUrl !== undefined) {
|
|
168
|
+
this.linkFederatedIdentityUrl = assertSecureEndpoint(linkFederatedIdentityUrl, "linkFederatedIdentityUrl");
|
|
169
|
+
this.linkFederatedIdentity = (userId, identity) => this.linkViaHttp(userId, identity);
|
|
170
|
+
}
|
|
158
171
|
if (!isPositiveIntegerWithin(timeout, MAX_TIMEOUT_MS)) {
|
|
159
172
|
throw new Error(`HttpUserRepository: "timeout" must be a positive integer no greater than ` +
|
|
160
173
|
`${MAX_TIMEOUT_MS} milliseconds`);
|
|
@@ -166,12 +179,28 @@ export class HttpUserRepository {
|
|
|
166
179
|
this.maxResponseBytes = maxResponseBytes;
|
|
167
180
|
}
|
|
168
181
|
async authenticate(username, password) {
|
|
182
|
+
// Without `acceptConflict` the sentinel is never produced — a 409 throws.
|
|
169
183
|
return this.post(this.authenticateUrl, { email: username, password });
|
|
170
184
|
}
|
|
171
185
|
async authenticateByToken(token) {
|
|
172
186
|
return this.post(this.authenticateByTokenUrl, { token });
|
|
173
187
|
}
|
|
174
|
-
|
|
188
|
+
/**
|
|
189
|
+
* POST `{ userId, provider, sub, token, claims }` to the Store's link endpoint
|
|
190
|
+
* (#482). A `2xx` `User` is the linked account; `401` / `403` is the Store's
|
|
191
|
+
* refusal (policy — an unverified or relay address, one identity per
|
|
192
|
+
* provider, …); `409` says the identity is already someone else's.
|
|
193
|
+
*/
|
|
194
|
+
async linkViaHttp(userId, identity) {
|
|
195
|
+
const url = this.linkFederatedIdentityUrl;
|
|
196
|
+
const answer = await this.post(url, { userId, ...identity }, { acceptConflict: true });
|
|
197
|
+
if (answer === CONFLICT)
|
|
198
|
+
return { ok: false, reason: "conflict" };
|
|
199
|
+
if (answer === null)
|
|
200
|
+
return { ok: false, reason: "refused" };
|
|
201
|
+
return { ok: true, user: answer };
|
|
202
|
+
}
|
|
203
|
+
async post(url, body, options = {}) {
|
|
175
204
|
const controller = new AbortController();
|
|
176
205
|
let timedOut = false;
|
|
177
206
|
// One absolute deadline for the whole exchange, expressed twice: as the
|
|
@@ -218,6 +247,9 @@ export class HttpUserRepository {
|
|
|
218
247
|
return parsed;
|
|
219
248
|
}
|
|
220
249
|
discardBody(res);
|
|
250
|
+
if (options.acceptConflict && res.status === 409) {
|
|
251
|
+
return CONFLICT;
|
|
252
|
+
}
|
|
221
253
|
if (res.status === 401 || res.status === 403) {
|
|
222
254
|
return null;
|
|
223
255
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth-provider-foundation",
|
|
3
3
|
"description": "Production HTTP user-authentication adapter for auth.provider",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@o3co/auth-provider-core": "^0.
|
|
35
|
+
"@o3co/auth-provider-core": "^0.13.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^26.
|
|
38
|
+
"@types/node": "^26.5.0",
|
|
39
39
|
"msw": "^2.15.0",
|
|
40
40
|
"pino": "^10.3.1",
|
|
41
|
-
"vitest": "^
|
|
42
|
-
"@vitest/coverage-v8": "^
|
|
43
|
-
"@o3co/auth-provider-core": "0.
|
|
41
|
+
"vitest": "^5.0.0",
|
|
42
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
43
|
+
"@o3co/auth-provider-core": "0.13.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"prebuild": "rimraf dist",
|