@o3co/auth-provider-foundation 0.5.1 → 0.5.2
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/dist/repositories/HttpUserRepository.d.mts.map +1 -1
- package/dist/repositories/HttpUserRepository.mjs +30 -1
- package/package.json +5 -2
- package/dist/repositories/__tests__/HttpUserRepository.test.d.mts +0 -2
- package/dist/repositories/__tests__/HttpUserRepository.test.d.mts.map +0 -1
- package/dist/repositories/__tests__/HttpUserRepository.test.mjs +0 -89
- package/dist/repositories/__tests__/registerBuiltinAdapters.test.d.mts +0 -2
- package/dist/repositories/__tests__/registerBuiltinAdapters.test.d.mts.map +0 -1
- package/dist/repositories/__tests__/registerBuiltinAdapters.test.mjs +0 -92
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpUserRepository.d.mts","sourceRoot":"","sources":["../../src/repositories/HttpUserRepository.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"HttpUserRepository.d.mts","sourceRoot":"","sources":["../../src/repositories/HttpUserRepository.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAuBrE,qBAAa,kBAAmB,YAAW,cAAc;IACxD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,OAAO,CAAS;gBAEZ,EACX,eAAe,EACf,sBAAsB,EACtB,OAAO,GACP,EAAE;QACF,eAAe,EAAE,MAAM,CAAC;QACxB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,OAAO,EAAE,MAAM,CAAC;KAChB;IAMK,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAItE,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;YAIhD,IAAI;CAiClB"}
|
|
@@ -13,6 +13,27 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* Runtime guard for the upstream user-service response. The previous
|
|
18
|
+
* `(await res.json()) as User` was a compile-time cast only — a malformed
|
|
19
|
+
* upstream payload (`{ status: "ok" }`, schema migration, tampered
|
|
20
|
+
* response) silently produced a `User` with `undefined` required fields,
|
|
21
|
+
* leaking `sub: undefined` into the authentication flow.
|
|
22
|
+
*
|
|
23
|
+
* The guard accepts any object with string `id` and `username`,
|
|
24
|
+
* preserving the index-signature `[key: string]: unknown` extras that
|
|
25
|
+
* `User` allows. Empty strings pass — bcrypt compare and downstream
|
|
26
|
+
* gates prevent empty-credential authentication in practice; tightening
|
|
27
|
+
* to `.min(1)` is a Phase F follow-up if needed.
|
|
28
|
+
*
|
|
29
|
+
* Per TS-2 (Wave 5g).
|
|
30
|
+
*/
|
|
31
|
+
function isUser(v) {
|
|
32
|
+
if (typeof v !== "object" || v === null)
|
|
33
|
+
return false;
|
|
34
|
+
const o = v;
|
|
35
|
+
return typeof o.id === "string" && typeof o.username === "string";
|
|
36
|
+
}
|
|
16
37
|
export class HttpUserRepository {
|
|
17
38
|
authenticateUrl;
|
|
18
39
|
authenticateByTokenUrl;
|
|
@@ -39,7 +60,15 @@ export class HttpUserRepository {
|
|
|
39
60
|
signal: controller.signal,
|
|
40
61
|
});
|
|
41
62
|
if (res.ok) {
|
|
42
|
-
|
|
63
|
+
const parsed = await res.json();
|
|
64
|
+
if (!isUser(parsed)) {
|
|
65
|
+
// Upstream returned 2xx with an unexpected shape — this is an
|
|
66
|
+
// "upstream is broken" case, not a "user not found" case, so
|
|
67
|
+
// throw rather than return null. The thrown error propagates
|
|
68
|
+
// as a 500 to the client (correct: upstream-service failure).
|
|
69
|
+
throw new Error(`HttpUserRepository: upstream ${url} returned an invalid User shape`);
|
|
70
|
+
}
|
|
71
|
+
return parsed;
|
|
43
72
|
}
|
|
44
73
|
if (res.status === 401 || res.status === 403) {
|
|
45
74
|
return null;
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth-provider-foundation",
|
|
3
3
|
"description": "Production HTTP user-authentication adapter for auth.provider",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.19.0"
|
|
8
|
+
},
|
|
6
9
|
"type": "module",
|
|
7
10
|
"main": "./dist/index.mjs",
|
|
8
11
|
"types": "./dist/index.d.mts",
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
"pino": "^10.3.1",
|
|
38
41
|
"vitest": "^4.1.2",
|
|
39
42
|
"@vitest/coverage-v8": "^4.1.2",
|
|
40
|
-
"@o3co/auth-provider-core": "0.5.
|
|
43
|
+
"@o3co/auth-provider-core": "0.5.2"
|
|
41
44
|
},
|
|
42
45
|
"scripts": {
|
|
43
46
|
"prebuild": "rimraf dist",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"HttpUserRepository.test.d.mts","sourceRoot":"","sources":["../../../src/repositories/__tests__/HttpUserRepository.test.mts"],"names":[],"mappings":""}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { HttpResponse, http } from "msw";
|
|
17
|
-
import { setupServer } from "msw/node";
|
|
18
|
-
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
19
|
-
import { HttpUserRepository } from "../HttpUserRepository.mjs";
|
|
20
|
-
const BASE_URL = "http://localhost:18080";
|
|
21
|
-
const mockUser = { id: "user-1", username: "alice" };
|
|
22
|
-
const handlers = [
|
|
23
|
-
http.post(`${BASE_URL}/user/authenticate`, async ({ request }) => {
|
|
24
|
-
const body = (await request.json());
|
|
25
|
-
if (body.email === "alice@example.com" && body.password === "correct-pass") {
|
|
26
|
-
return HttpResponse.json(mockUser, { status: 200 });
|
|
27
|
-
}
|
|
28
|
-
return new HttpResponse(null, { status: 401 });
|
|
29
|
-
}),
|
|
30
|
-
http.post(`${BASE_URL}/user/authenticate/token`, async ({ request }) => {
|
|
31
|
-
const body = (await request.json());
|
|
32
|
-
if (body.token === "valid-token") {
|
|
33
|
-
return HttpResponse.json(mockUser, { status: 200 });
|
|
34
|
-
}
|
|
35
|
-
return new HttpResponse(null, { status: 401 });
|
|
36
|
-
}),
|
|
37
|
-
];
|
|
38
|
-
const server = setupServer(...handlers);
|
|
39
|
-
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
|
40
|
-
afterEach(() => server.resetHandlers());
|
|
41
|
-
afterAll(() => server.close());
|
|
42
|
-
describe("HttpUserRepository", () => {
|
|
43
|
-
const repo = new HttpUserRepository({
|
|
44
|
-
authenticateUrl: `${BASE_URL}/user/authenticate`,
|
|
45
|
-
authenticateByTokenUrl: `${BASE_URL}/user/authenticate/token`,
|
|
46
|
-
timeout: 5000,
|
|
47
|
-
});
|
|
48
|
-
describe("authenticate", () => {
|
|
49
|
-
it("returns user on success", async () => {
|
|
50
|
-
const user = await repo.authenticate("alice@example.com", "correct-pass");
|
|
51
|
-
expect(user).not.toBeNull();
|
|
52
|
-
expect(user?.id).toBe("user-1");
|
|
53
|
-
expect(user?.username).toBe("alice");
|
|
54
|
-
});
|
|
55
|
-
it("sends email field (not username) in request body", async () => {
|
|
56
|
-
let capturedBody = null;
|
|
57
|
-
server.use(http.post(`${BASE_URL}/user/authenticate`, async ({ request }) => {
|
|
58
|
-
capturedBody = (await request.json());
|
|
59
|
-
return HttpResponse.json(mockUser, { status: 200 });
|
|
60
|
-
}));
|
|
61
|
-
await repo.authenticate("alice@example.com", "correct-pass");
|
|
62
|
-
expect(capturedBody).not.toBeNull();
|
|
63
|
-
expect(capturedBody).toHaveProperty("email", "alice@example.com");
|
|
64
|
-
expect(capturedBody).not.toHaveProperty("username");
|
|
65
|
-
});
|
|
66
|
-
it("returns null on 401", async () => {
|
|
67
|
-
const user = await repo.authenticate("alice@example.com", "wrong-pass");
|
|
68
|
-
expect(user).toBeNull();
|
|
69
|
-
});
|
|
70
|
-
it("throws on unexpected HTTP status", async () => {
|
|
71
|
-
server.use(http.post(`${BASE_URL}/user/authenticate`, () => {
|
|
72
|
-
return new HttpResponse(null, { status: 500 });
|
|
73
|
-
}));
|
|
74
|
-
await expect(repo.authenticate("alice@example.com", "pass")).rejects.toThrow("Unexpected HTTP status 500");
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
describe("authenticateByToken", () => {
|
|
78
|
-
it("returns user on success", async () => {
|
|
79
|
-
const user = await repo.authenticateByToken("valid-token");
|
|
80
|
-
expect(user).not.toBeNull();
|
|
81
|
-
expect(user?.id).toBe("user-1");
|
|
82
|
-
expect(user?.username).toBe("alice");
|
|
83
|
-
});
|
|
84
|
-
it("returns null on 401", async () => {
|
|
85
|
-
const user = await repo.authenticateByToken("invalid-token");
|
|
86
|
-
expect(user).toBeNull();
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registerBuiltinAdapters.test.d.mts","sourceRoot":"","sources":["../../../src/repositories/__tests__/registerBuiltinAdapters.test.mts"],"names":[],"mappings":""}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { AdapterFactoryError, createAdapterFactory, } from "@o3co/auth-provider-core";
|
|
17
|
-
import { HttpResponse, http } from "msw";
|
|
18
|
-
import { setupServer } from "msw/node";
|
|
19
|
-
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
20
|
-
import { registerBuiltinAdapters } from "#/index.mjs";
|
|
21
|
-
const BASE_URL = "http://localhost:19090";
|
|
22
|
-
const mockUser = { id: "u1", username: "alice" };
|
|
23
|
-
const server = setupServer(http.post(`${BASE_URL}/auth`, async ({ request }) => {
|
|
24
|
-
const body = (await request.json());
|
|
25
|
-
if (body.email === "alice" && body.password === "pass") {
|
|
26
|
-
return HttpResponse.json(mockUser, { status: 200 });
|
|
27
|
-
}
|
|
28
|
-
return new HttpResponse(null, { status: 401 });
|
|
29
|
-
}), http.post(`${BASE_URL}/auth/token`, async ({ request }) => {
|
|
30
|
-
const body = (await request.json());
|
|
31
|
-
if (body.token === "valid") {
|
|
32
|
-
return HttpResponse.json(mockUser, { status: 200 });
|
|
33
|
-
}
|
|
34
|
-
return new HttpResponse(null, { status: 401 });
|
|
35
|
-
}));
|
|
36
|
-
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
|
37
|
-
afterEach(() => server.resetHandlers());
|
|
38
|
-
afterAll(() => server.close());
|
|
39
|
-
describe("registerBuiltinAdapters", () => {
|
|
40
|
-
it("registers 'http' type in userFactory", async () => {
|
|
41
|
-
const userFactory = createAdapterFactory("UserRepository");
|
|
42
|
-
registerBuiltinAdapters({ userFactory });
|
|
43
|
-
try {
|
|
44
|
-
await userFactory.create({ type: "unknown" });
|
|
45
|
-
throw new Error("should have thrown");
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
expect(err).toBeInstanceOf(AdapterFactoryError);
|
|
49
|
-
const e = err;
|
|
50
|
-
expect(e.reason).toBe("unknown");
|
|
51
|
-
expect(e.kind).toBe("UserRepository");
|
|
52
|
-
expect(e.registered).toContain("http");
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
it("http builder creates a working HttpUserRepository", async () => {
|
|
56
|
-
const userFactory = createAdapterFactory("UserRepository");
|
|
57
|
-
registerBuiltinAdapters({ userFactory });
|
|
58
|
-
const repo = await userFactory.create({
|
|
59
|
-
type: "http",
|
|
60
|
-
authenticateUrl: `${BASE_URL}/auth`,
|
|
61
|
-
authenticateByTokenUrl: `${BASE_URL}/auth/token`,
|
|
62
|
-
timeout: 5000,
|
|
63
|
-
});
|
|
64
|
-
const user = await repo.authenticate("alice", "pass");
|
|
65
|
-
expect(user).not.toBeNull();
|
|
66
|
-
expect(user?.username).toBe("alice");
|
|
67
|
-
const byToken = await repo.authenticateByToken("valid");
|
|
68
|
-
expect(byToken).not.toBeNull();
|
|
69
|
-
const invalid = await repo.authenticate("alice", "wrong");
|
|
70
|
-
expect(invalid).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
it("http builder coerces string timeout to number (env-override path)", async () => {
|
|
73
|
-
const userFactory = createAdapterFactory("UserRepository");
|
|
74
|
-
registerBuiltinAdapters({ userFactory });
|
|
75
|
-
const repo = await userFactory.create({
|
|
76
|
-
type: "http",
|
|
77
|
-
authenticateUrl: `${BASE_URL}/auth`,
|
|
78
|
-
authenticateByTokenUrl: `${BASE_URL}/auth/token`,
|
|
79
|
-
timeout: "1234", // string, as HOCON env override produces
|
|
80
|
-
});
|
|
81
|
-
expect(repo).toBeDefined();
|
|
82
|
-
// HttpUserRepository doesn't expose timeout publicly; we verify it didn't throw
|
|
83
|
-
// and works via the MSW fixture. The coercion path is internal.
|
|
84
|
-
const user = await repo.authenticate("alice", "pass");
|
|
85
|
-
expect(user).not.toBeNull();
|
|
86
|
-
});
|
|
87
|
-
it("http builder throws when authenticateUrl is missing", async () => {
|
|
88
|
-
const userFactory = createAdapterFactory("UserRepository");
|
|
89
|
-
registerBuiltinAdapters({ userFactory });
|
|
90
|
-
await expect(userFactory.create({ type: "http", timeout: 5000 })).rejects.toThrow(/authenticateUrl/);
|
|
91
|
-
});
|
|
92
|
-
});
|