@structured-id/ui-core 4.12.0 → 4.14.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.
@@ -4,15 +4,28 @@
4
4
  * The account API through the BFF (`getAccountTransport`), which adds the
5
5
  * session's Bearer token. Shared between profile-ui-ce, profile-ui-ee, and
6
6
  * frontend.
7
+ *
8
+ * Self-service listings name no Profile: the server resolves the caller's
9
+ * own from its session, so the page never needs its own ProfileId.
7
10
  */
8
11
  import type { RpcTransport } from "@protobuf-ts/runtime-rpc";
12
+ import {
13
+ CredentialId,
14
+ DeviceId,
15
+ PrincipalId,
16
+ ProfileEmailId,
17
+ ProfileId,
18
+ SessionId,
19
+ optional,
20
+ required,
21
+ } from "@structured-id/ids";
9
22
  import { getAccountTransport } from "../../index";
10
23
  import { IdentityServiceClient } from "@structured-id/proto/sid/v1/identity/identity.client";
11
24
  import {
12
- type Profile,
13
- type Session,
14
- type Credential,
15
- type Principal,
25
+ type Profile as WireProfile,
26
+ type Session as WireSession,
27
+ type Credential as WireCredential,
28
+ type Principal as WirePrincipal,
16
29
  ProfileStatus,
17
30
  PrincipalType,
18
31
  CredentialType,
@@ -20,6 +33,7 @@ import {
20
33
  } from "@structured-id/proto/sid/v1/identity/identity";
21
34
  import { Timestamp } from "@structured-id/proto/google/protobuf/timestamp";
22
35
  import { authMeta } from "../auth";
36
+ import type { WithId } from "./useAuthApi";
23
37
 
24
38
  function client(transport?: RpcTransport): IdentityServiceClient {
25
39
  return new IdentityServiceClient(transport ?? getAccountTransport());
@@ -27,22 +41,36 @@ function client(transport?: RpcTransport): IdentityServiceClient {
27
41
 
28
42
  // ── Profile ──
29
43
 
30
- export type { Profile, Principal };
44
+ /** A Profile as answered, its id read into a typed identifier. */
45
+ export type Profile = WithId<WireProfile, "id", ProfileId>;
46
+
47
+ function profile(wire: WireProfile | undefined): Profile {
48
+ if (!wire) throw new TypeError("the answer carries no profile");
49
+ return { ...wire, id: required(ProfileId, wire.id) };
50
+ }
31
51
 
32
- export async function fetchCurrentProfile(): Promise<Profile> {
33
- const { response } = await client().getCurrentProfile(
52
+ /**
53
+ * The signed-in user's own profile. `transport`: the sign-in surface's
54
+ * transport when a sign-in step reads it; the account API otherwise.
55
+ */
56
+ export async function fetchCurrentProfile(
57
+ transport?: RpcTransport,
58
+ ): Promise<Profile> {
59
+ const { response } = await client(transport).getCurrentProfile(
34
60
  {},
35
61
  { meta: authMeta() },
36
62
  );
37
- return response.profile!;
63
+ return profile(response.profile);
38
64
  }
39
65
 
40
66
  /**
67
+ * Update the profile `id` (the one {@link fetchCurrentProfile} answered).
68
+ *
41
69
  * `transport`: the sign-in surface's transport when a sign-in step (a
42
70
  * required profile update) makes the call; the account API otherwise.
43
71
  */
44
72
  export async function updateProfile(
45
- id: string,
73
+ id: ProfileId,
46
74
  update: {
47
75
  givenName?: string;
48
76
  familyName?: string;
@@ -53,7 +81,7 @@ export async function updateProfile(
53
81
  ): Promise<Profile> {
54
82
  const { response } = await client(transport).updateProfile(
55
83
  {
56
- id,
84
+ id: id.toWire(),
57
85
  givenName: update.givenName,
58
86
  familyName: update.familyName,
59
87
  middleName: update.middleName,
@@ -61,66 +89,93 @@ export async function updateProfile(
61
89
  },
62
90
  { meta: authMeta() },
63
91
  );
64
- return response.profile!;
92
+ return profile(response.profile);
65
93
  }
66
94
 
67
95
  // ── Sessions ──
68
96
 
69
- export type { Session };
97
+ /** A session as answered, with its own and its device's typed ids. */
98
+ export type Session = WithId<
99
+ WithId<WireSession, "id", SessionId>,
100
+ "deviceId",
101
+ DeviceId | undefined
102
+ >;
103
+
104
+ /** The caller's own sessions. */
105
+ export async function fetchSessions(): Promise<Session[]> {
106
+ const { response } = await client().listSessions({}, { meta: authMeta() });
107
+ return response.sessions.map((s) => ({
108
+ ...s,
109
+ id: required(SessionId, s.id),
110
+ deviceId: optional(DeviceId, s.deviceId),
111
+ }));
112
+ }
70
113
 
71
- export async function fetchSessions(profileId: string): Promise<Session[]> {
72
- const { response } = await client().listSessions(
73
- { profileId },
114
+ export async function revokeSession(id: SessionId): Promise<void> {
115
+ await client().revokeSession(
116
+ { sessionId: id.toWire() },
74
117
  { meta: authMeta() },
75
118
  );
76
- return response.sessions;
77
- }
78
-
79
- export async function revokeSession(sessionId: string): Promise<void> {
80
- await client().revokeSession({ sessionId }, { meta: authMeta() });
81
119
  }
82
120
 
83
121
  // ── Credentials ──
84
122
 
85
- export type { Credential };
123
+ /** A credential as answered, its id read into a typed identifier. */
124
+ export type Credential = WithId<WireCredential, "id", CredentialId>;
86
125
 
87
- export async function fetchCredentials(
88
- profileId: string,
89
- ): Promise<Credential[]> {
90
- const { response } = await client().listCredentials(
91
- { profileId },
92
- { meta: authMeta() },
93
- );
94
- return response.credentials;
126
+ function credential(wire: WireCredential | undefined): Credential {
127
+ if (!wire) throw new TypeError("the answer carries no credential");
128
+ return { ...wire, id: required(CredentialId, wire.id) };
95
129
  }
96
130
 
97
- export async function revokeCredential(credentialId: string): Promise<void> {
98
- await client().revokeCredential({ credentialId }, { meta: authMeta() });
131
+ /** The caller's own credentials. */
132
+ export async function fetchCredentials(): Promise<Credential[]> {
133
+ const { response } = await client().listCredentials({}, { meta: authMeta() });
134
+ return response.credentials.map(credential);
135
+ }
136
+
137
+ export async function revokeCredential(id: CredentialId): Promise<void> {
138
+ await client().revokeCredential(
139
+ { credentialId: id.toWire() },
140
+ { meta: authMeta() },
141
+ );
99
142
  }
100
143
 
101
144
  export async function updateCredentialLabel(
102
- credentialId: string,
145
+ id: CredentialId,
103
146
  label: string,
104
147
  ): Promise<Credential> {
105
148
  const { response } = await client().updateCredential(
106
- { credentialId, label },
149
+ { credentialId: id.toWire(), label },
107
150
  { meta: authMeta() },
108
151
  );
109
- return response.credential!;
152
+ return credential(response.credential);
110
153
  }
111
154
 
112
155
  // ── Principals ──
113
156
 
114
- export async function fetchPrincipals(profileId: string): Promise<Principal[]> {
115
- const { response } = await client().listPrincipals(
116
- { profileId },
117
- { meta: authMeta() },
118
- );
119
- return response.principals;
157
+ /** A login handle as answered, with its own and its contact's typed ids. */
158
+ export type Principal = WithId<
159
+ WithId<WirePrincipal, "id", PrincipalId>,
160
+ "sourceEmailId",
161
+ ProfileEmailId | undefined
162
+ >;
163
+
164
+ /** The caller's own login handles. */
165
+ export async function fetchPrincipals(): Promise<Principal[]> {
166
+ const { response } = await client().listPrincipals({}, { meta: authMeta() });
167
+ return response.principals.map((p) => ({
168
+ ...p,
169
+ id: required(PrincipalId, p.id),
170
+ sourceEmailId: optional(ProfileEmailId, p.sourceEmailId),
171
+ }));
120
172
  }
121
173
 
122
- export async function removePrincipal(principalId: string): Promise<void> {
123
- await client().removePrincipal({ principalId }, { meta: authMeta() });
174
+ export async function removePrincipal(id: PrincipalId): Promise<void> {
175
+ await client().removePrincipal(
176
+ { principalId: id.toWire() },
177
+ { meta: authMeta() },
178
+ );
124
179
  }
125
180
 
126
181
  // Re-export enums and Timestamp for convenience
@@ -101,7 +101,7 @@ describe("useLoginSuccess (BFF cookie mode)", () => {
101
101
 
102
102
  it("calls checkBffSession and populates auth store on success", async () => {
103
103
  mockCheckBffSession.mockResolvedValue({
104
- sub: "profile-uuid-1",
104
+ sub: "subject-1",
105
105
  name: "Alice Smith",
106
106
  email: "alice@example.com",
107
107
  preferred_username: "alice",
@@ -114,7 +114,6 @@ describe("useLoginSuccess (BFF cookie mode)", () => {
114
114
 
115
115
  expect(mockCheckBffSession).toHaveBeenCalled();
116
116
  expect(auth.isAuthenticated).toBe(true);
117
- expect(auth.profileId).toBe("profile-uuid-1");
118
117
  expect(auth.displayName).toBe("Alice Smith");
119
118
  expect(auth.email).toBe("alice@example.com");
120
119
  });
@@ -10,12 +10,13 @@
10
10
  * Otherwise the app's own session comes from its BFF: the handler reads it
11
11
  * from the BFF's /auth/userinfo and navigates within the app.
12
12
  */
13
+ import type { SessionId } from "@structured-id/ids";
13
14
  import { useRouter } from "vue-router";
14
15
  import { useAuthStore } from "../../index";
15
16
  import { checkBffSession } from "../../index";
16
17
 
17
18
  export interface LoginSuccessResult {
18
- sessionId?: string;
19
+ sessionId?: SessionId;
19
20
  }
20
21
 
21
22
  /** Options of {@link useLoginSuccess}. */
@@ -20,8 +20,6 @@ function makeCred(
20
20
  opts?: { attachment?: AuthenticatorAttachment; backupState?: boolean },
21
21
  ): Credential {
22
22
  const cred: Credential = {
23
- id: "cred-1",
24
- profileId: "",
25
23
  type,
26
24
  data: new Uint8Array(),
27
25
  info: { oneofKind: undefined },
@@ -11,7 +11,6 @@ describe("useAuthStore (BFF cookie session)", () => {
11
11
  const store = useAuthStore();
12
12
  expect(store.isAuthenticated).toBe(false);
13
13
  expect(store.sessionValid).toBe(false);
14
- expect(store.profileId).toBeNull();
15
14
  expect(store.displayName).toBeNull();
16
15
  expect(store.email).toBeNull();
17
16
  });
@@ -19,7 +18,7 @@ describe("useAuthStore (BFF cookie session)", () => {
19
18
  it("setBffSession sets sessionValid and profile data", () => {
20
19
  const store = useAuthStore();
21
20
  store.setBffSession({
22
- sub: "profile-uuid-123",
21
+ sub: "subject-123",
23
22
  email: "alice@example.com",
24
23
  name: "Alice Smith",
25
24
  preferred_username: "alice",
@@ -28,18 +27,20 @@ describe("useAuthStore (BFF cookie session)", () => {
28
27
 
29
28
  expect(store.isAuthenticated).toBe(true);
30
29
  expect(store.sessionValid).toBe(true);
31
- expect(store.profileId).toBe("profile-uuid-123");
32
30
  expect(store.displayName).toBe("Alice Smith");
33
31
  expect(store.email).toBe("alice@example.com");
34
32
  expect(store.preferredUsername).toBe("alice");
35
33
  });
36
34
 
37
- it("setBffSession handles null/missing optional fields", () => {
35
+ // Regression: the userinfo `sub` was kept as the user's ProfileId, though
36
+ // it is the subject this application knows the user by. The store keeps
37
+ // no Profile id at all.
38
+ it("never reads the subject as a Profile id", () => {
38
39
  const store = useAuthStore();
39
- store.setBffSession({ sub: "uuid-456" });
40
+ store.setBffSession({ sub: "subject-456" });
40
41
 
41
42
  expect(store.isAuthenticated).toBe(true);
42
- expect(store.profileId).toBe("uuid-456");
43
+ expect("profileId" in store).toBe(false);
43
44
  expect(store.displayName).toBeNull();
44
45
  expect(store.email).toBeNull();
45
46
  expect(store.preferredUsername).toBeNull();
@@ -47,15 +48,13 @@ describe("useAuthStore (BFF cookie session)", () => {
47
48
 
48
49
  it("setProfile updates display data", () => {
49
50
  const store = useAuthStore();
50
- store.setBffSession({ sub: "uuid-1" });
51
+ store.setBffSession({ sub: "subject-1" });
51
52
 
52
53
  store.setProfile({
53
- id: "uuid-1",
54
54
  displayName: "Bob Jones",
55
55
  email: "bob@example.com",
56
56
  });
57
57
 
58
- expect(store.profileId).toBe("uuid-1");
59
58
  expect(store.displayName).toBe("Bob Jones");
60
59
  expect(store.email).toBe("bob@example.com");
61
60
  });
@@ -63,7 +62,7 @@ describe("useAuthStore (BFF cookie session)", () => {
63
62
  it("logout clears all state", () => {
64
63
  const store = useAuthStore();
65
64
  store.setBffSession({
66
- sub: "uuid-1",
65
+ sub: "subject-1",
67
66
  name: "Alice",
68
67
  email: "alice@example.com",
69
68
  preferred_username: "alice",
@@ -73,7 +72,6 @@ describe("useAuthStore (BFF cookie session)", () => {
73
72
 
74
73
  expect(store.isAuthenticated).toBe(false);
75
74
  expect(store.sessionValid).toBe(false);
76
- expect(store.profileId).toBeNull();
77
75
  expect(store.displayName).toBeNull();
78
76
  expect(store.email).toBeNull();
79
77
  expect(store.preferredUsername).toBeNull();
@@ -24,9 +24,11 @@ export const useAuthStore = defineStore("auth", () => {
24
24
  const sessionId = ref<string | null>(null);
25
25
 
26
26
  // ── Profile info (populated from BFF /auth/userinfo) ──────────────
27
+ //
28
+ // No Profile id: the userinfo `sub` is the subject this application
29
+ // knows the user by, not a ProfileId, and self-service calls name no
30
+ // Profile at all.
27
31
 
28
- /** Profile ID of the authenticated user (= sub from BFF userinfo). */
29
- const profileId = ref<string | null>(null);
30
32
  /** Display name. */
31
33
  const displayName = ref<string | null>(null);
32
34
  /** Email address. */
@@ -46,7 +48,6 @@ export const useAuthStore = defineStore("auth", () => {
46
48
  */
47
49
  function setBffSession(info: BffUserInfo): void {
48
50
  sessionValid.value = true;
49
- profileId.value = info.sub;
50
51
  displayName.value = info.name ?? null;
51
52
  email.value = info.email ?? null;
52
53
  preferredUsername.value = info.preferred_username ?? null;
@@ -56,11 +57,9 @@ export const useAuthStore = defineStore("auth", () => {
56
57
  * Update profile display data (e.g., after profile edit).
57
58
  */
58
59
  function setProfile(profile: {
59
- id: string;
60
60
  displayName?: string | null;
61
61
  email?: string | null;
62
62
  }): void {
63
- profileId.value = profile.id;
64
63
  displayName.value = profile.displayName ?? null;
65
64
  email.value = profile.email ?? null;
66
65
  }
@@ -71,7 +70,6 @@ export const useAuthStore = defineStore("auth", () => {
71
70
  function logout(): void {
72
71
  sessionValid.value = false;
73
72
  sessionId.value = null;
74
- profileId.value = null;
75
73
  displayName.value = null;
76
74
  email.value = null;
77
75
  preferredUsername.value = null;
@@ -80,7 +78,6 @@ export const useAuthStore = defineStore("auth", () => {
80
78
  return {
81
79
  sessionValid,
82
80
  sessionId,
83
- profileId,
84
81
  displayName,
85
82
  email,
86
83
  preferredUsername,
package/src/types/auth.ts CHANGED
@@ -4,8 +4,6 @@ export interface AuthState {
4
4
  sessionValid: boolean;
5
5
  /** Server-side session identifier (for step-up, session management). */
6
6
  sessionId: string | null;
7
- /** Profile ID of the authenticated user (UUID). */
8
- profileId: string | null;
9
7
  /** Display name of the authenticated user. */
10
8
  displayName: string | null;
11
9
  /** Email address (may be null if not in session claims). */