@structured-id/ui-core 4.11.0 → 4.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.
@@ -1,16 +1,15 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import {
3
- opaqueLoginStart,
4
- opaqueLoginFinish,
5
- opaqueRegistrationStart,
6
- opaqueRegistrationFinish,
7
- } from "./useAuthApi";
3
+ IdError,
4
+ MissingIdError,
5
+ OpaqueLoginId,
6
+ SessionId,
7
+ } from "@structured-id/ids";
8
+ import { opaqueLoginStart, opaqueLoginFinish } from "./useAuthApi";
8
9
 
9
10
  // Mock the gRPC transport + AuthServiceClient
10
11
  const mockOpaqueLoginStart = vi.fn();
11
12
  const mockOpaqueLoginFinish = vi.fn();
12
- const mockOpaqueRegistrationStart = vi.fn();
13
- const mockOpaqueRegistrationFinish = vi.fn();
14
13
 
15
14
  vi.mock("../../index", () => ({
16
15
  getTransport: vi.fn(),
@@ -23,8 +22,6 @@ vi.mock("@structured-id/proto/sid/v1/authn/auth.client", () => ({
23
22
  return {
24
23
  opaqueLoginStart: mockOpaqueLoginStart,
25
24
  opaqueLoginFinish: mockOpaqueLoginFinish,
26
- opaqueRegistrationStart: mockOpaqueRegistrationStart,
27
- opaqueRegistrationFinish: mockOpaqueRegistrationFinish,
28
25
  };
29
26
  }),
30
27
  }));
@@ -34,18 +31,23 @@ vi.mock("../auth", () => ({
34
31
  authMeta: () => ({}),
35
32
  }));
36
33
 
34
+ const LOGIN = OpaqueLoginId.parse("0192f3a4-7c1e-7b2a-9d4e-3f5a6b7c8d9e");
35
+ const SESSION = SessionId.parse("0192f3a4-7c1e-7b2a-9d4e-3f5a6b7c8d9f");
36
+
37
37
  describe("OPAQUE gRPC wrappers", () => {
38
38
  beforeEach(() => {
39
39
  vi.clearAllMocks();
40
40
  });
41
41
 
42
42
  describe("opaqueLoginStart", () => {
43
- it("sends principal and credentialRequest, returns response", async () => {
44
- const expected = {
45
- credentialResponse: new Uint8Array([1, 2, 3]),
46
- serverLoginState: "state-123",
47
- };
48
- mockOpaqueLoginStart.mockResolvedValue({ response: expected });
43
+ // The start names its login; the finish carries that name back.
44
+ it("sends principal and credentialRequest, reads the login it began", async () => {
45
+ mockOpaqueLoginStart.mockResolvedValue({
46
+ response: {
47
+ credentialResponse: new Uint8Array([1, 2, 3]),
48
+ loginId: LOGIN.toWire(),
49
+ },
50
+ });
49
51
 
50
52
  const result = await opaqueLoginStart(
51
53
  "alice@test.com",
@@ -56,7 +58,29 @@ describe("OPAQUE gRPC wrappers", () => {
56
58
  principal: "alice@test.com",
57
59
  credentialRequest: new Uint8Array([10, 20]),
58
60
  });
59
- expect(result).toBe(expected);
61
+ expect(result.credentialResponse).toEqual(new Uint8Array([1, 2, 3]));
62
+ expect(result.loginId.equals(LOGIN)).toBe(true);
63
+ });
64
+
65
+ // A start that names no login, or a malformed one, cannot be finished:
66
+ // it fails here rather than sending an unusable finish.
67
+ it("refuses an answer without a valid login", async () => {
68
+ mockOpaqueLoginStart.mockResolvedValue({
69
+ response: { credentialResponse: new Uint8Array([1]) },
70
+ });
71
+ await expect(
72
+ opaqueLoginStart("x", new Uint8Array()),
73
+ ).rejects.toBeInstanceOf(MissingIdError);
74
+
75
+ mockOpaqueLoginStart.mockResolvedValue({
76
+ response: {
77
+ credentialResponse: new Uint8Array([1]),
78
+ loginId: { value: new Uint8Array(15) },
79
+ },
80
+ });
81
+ await expect(
82
+ opaqueLoginStart("x", new Uint8Array()),
83
+ ).rejects.toBeInstanceOf(IdError);
60
84
  });
61
85
 
62
86
  it("propagates gRPC errors", async () => {
@@ -68,97 +92,35 @@ describe("OPAQUE gRPC wrappers", () => {
68
92
  });
69
93
 
70
94
  describe("opaqueLoginFinish", () => {
71
- it("sends principal, finalization, and serverLoginState", async () => {
72
- const expected = {
73
- sessionId: "sess-1",
74
- expiresIn: BigInt(3600),
75
- };
76
- mockOpaqueLoginFinish.mockResolvedValue({ response: expected });
95
+ it("sends principal, finalization and the login's bytes, reads the session", async () => {
96
+ mockOpaqueLoginFinish.mockResolvedValue({
97
+ response: { sessionId: SESSION.toWire(), expiresIn: BigInt(3600) },
98
+ });
77
99
 
78
100
  const result = await opaqueLoginFinish(
79
101
  "alice@test.com",
80
102
  new Uint8Array([7, 8, 9]),
81
- "state-123",
103
+ LOGIN,
82
104
  );
83
105
 
84
106
  expect(mockOpaqueLoginFinish).toHaveBeenCalledWith({
85
107
  principal: "alice@test.com",
86
108
  credentialFinalization: new Uint8Array([7, 8, 9]),
87
- serverLoginState: "state-123",
88
- });
89
- expect(result).toBe(expected);
90
- });
91
- });
92
-
93
- describe("opaqueRegistrationStart", () => {
94
- it("sends principal and registrationRequest", async () => {
95
- const expected = {
96
- registrationResponse: new Uint8Array([4, 5, 6]),
97
- serverSetup: "setup-abc",
98
- };
99
- mockOpaqueRegistrationStart.mockResolvedValue({ response: expected });
100
-
101
- const result = await opaqueRegistrationStart(
102
- "bob@test.com",
103
- new Uint8Array([11, 12]),
104
- );
105
-
106
- expect(mockOpaqueRegistrationStart).toHaveBeenCalledWith({
107
- principal: "bob@test.com",
108
- registrationRequest: new Uint8Array([11, 12]),
109
- });
110
- expect(result).toBe(expected);
111
- });
112
-
113
- // An unclaimed installation accepts only a registration carrying the
114
- // claim token; the wrapper must put it on the wire unchanged.
115
- it("sends the instance claim token when given", async () => {
116
- mockOpaqueRegistrationStart.mockResolvedValue({ response: {} });
117
-
118
- await opaqueRegistrationStart(
119
- "owner@test.com",
120
- new Uint8Array([1]),
121
- "sidclaim_abc",
122
- );
123
-
124
- expect(mockOpaqueRegistrationStart).toHaveBeenCalledWith({
125
- principal: "owner@test.com",
126
- registrationRequest: new Uint8Array([1]),
127
- claimToken: "sidclaim_abc",
109
+ loginId: { value: LOGIN.toBytes() },
128
110
  });
111
+ expect(result.sessionId.equals(SESSION)).toBe(true);
112
+ expect(result.expiresIn).toBe(BigInt(3600));
129
113
  });
130
- });
131
-
132
- describe("opaqueRegistrationFinish", () => {
133
- it("sends principal, record, and serverSetup", async () => {
134
- const expected = {
135
- profileId: "profile-uuid",
136
- credentialId: "cred-uuid",
137
- serverPepper: new Uint8Array(),
138
- };
139
- mockOpaqueRegistrationFinish.mockResolvedValue({ response: expected });
140
-
141
- const result = await opaqueRegistrationFinish(
142
- "bob@test.com",
143
- new Uint8Array([20, 21]),
144
- "setup-abc",
145
- );
146
114
 
147
- expect(mockOpaqueRegistrationFinish).toHaveBeenCalledWith({
148
- principal: "bob@test.com",
149
- registrationRecord: new Uint8Array([20, 21]),
150
- serverSetup: "setup-abc",
115
+ // Every completed sign-in names its session; an answer without one is
116
+ // not a sign-in the page can manage.
117
+ it("refuses an answer without its session", async () => {
118
+ mockOpaqueLoginFinish.mockResolvedValue({
119
+ response: { expiresIn: BigInt(3600) },
151
120
  });
152
- expect(result).toBe(expected);
153
- });
154
-
155
- it("propagates gRPC errors", async () => {
156
- mockOpaqueRegistrationFinish.mockRejectedValue(
157
- new Error("ALREADY_EXISTS"),
158
- );
159
121
  await expect(
160
- opaqueRegistrationFinish("x", new Uint8Array(), "s"),
161
- ).rejects.toThrow("ALREADY_EXISTS");
122
+ opaqueLoginFinish("x", new Uint8Array(), LOGIN),
123
+ ).rejects.toBeInstanceOf(MissingIdError);
162
124
  });
163
125
  });
164
126
  });