@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.
@@ -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;AAErE,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;CAyBlB"}
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
- return (await res.json());
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.1",
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.1"
43
+ "@o3co/auth-provider-core": "0.5.2"
41
44
  },
42
45
  "scripts": {
43
46
  "prebuild": "rimraf dist",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=HttpUserRepository.test.d.mts.map
@@ -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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=registerBuiltinAdapters.test.d.mts.map
@@ -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
- });