@company-semantics/contracts 0.39.0 → 0.41.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/package.json +1 -1
- package/src/identity/README.md +10 -5
- package/src/identity/avatar.ts +120 -0
- package/src/identity/display-name.ts +34 -19
- package/src/identity/index.ts +14 -3
- package/src/identity/types.ts +48 -14
- package/src/index.ts +3 -3
package/package.json
CHANGED
package/src/identity/README.md
CHANGED
|
@@ -8,22 +8,27 @@ TypeScript types and functions for user identity and display name resolution.
|
|
|
8
8
|
|
|
9
9
|
- Types with minimal runtime code (pure functions only, no side effects)
|
|
10
10
|
- No external dependencies (contracts policy)
|
|
11
|
-
- `
|
|
12
|
-
- `
|
|
11
|
+
- `fullName` is the only stored name field (no firstName/lastName)
|
|
12
|
+
- `preferredName` is assistant-facing only, always user-editable
|
|
13
|
+
- `fullName` is user-editable only when `nameSource === 'self'`
|
|
14
|
+
- `fullName` is read-only when `nameSource === 'sso'` (managed by organization)
|
|
13
15
|
- `displayName` is NEVER stored — always derived at read time via `resolveDisplayName()`
|
|
14
16
|
- `resolveDisplayName()` is the ONLY approved way to determine assistant addressing
|
|
17
|
+
- Display name fallback: `preferredName ?? extractFirstWord(fullName)`
|
|
15
18
|
|
|
16
19
|
## Public API
|
|
17
20
|
|
|
18
21
|
### Types
|
|
19
22
|
|
|
20
23
|
- `ISODateString` — ISO 8601 date-time string alias
|
|
21
|
-
- `
|
|
24
|
+
- `NameSource` — `'self' | 'sso'` indicating name data source
|
|
25
|
+
- `UserIdentity` — User identity with name fields, source, and timestamps
|
|
22
26
|
|
|
23
27
|
### Functions
|
|
24
28
|
|
|
25
|
-
- `
|
|
26
|
-
- `resolveDisplayName(identity)` — Get display name (preferredName or
|
|
29
|
+
- `extractFirstWord(fullName)` — Extract first word from full name
|
|
30
|
+
- `resolveDisplayName(identity)` — Get display name (preferredName or first word of fullName)
|
|
31
|
+
- `deriveFullName(firstName, lastName?)` — **[DEPRECATED]** Kept for migration only
|
|
27
32
|
|
|
28
33
|
## Dependencies
|
|
29
34
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avatar Resolution Functions
|
|
3
|
+
*
|
|
4
|
+
* Canonical functions for resolving user avatars.
|
|
5
|
+
* Determines whether to use Slack profile photo or initials fallback.
|
|
6
|
+
*
|
|
7
|
+
* @see ADR-BE-063 for design rationale
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { UserIdentity } from './types';
|
|
11
|
+
import { extractFirstWord } from './display-name';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Source of the resolved avatar.
|
|
19
|
+
* - 'slack': Using Slack profile photo
|
|
20
|
+
* - 'initials': Using generated initials (fallback)
|
|
21
|
+
*/
|
|
22
|
+
export type AvatarSource = 'slack' | 'initials';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolved avatar for UI rendering.
|
|
26
|
+
*
|
|
27
|
+
* INVARIANT: initials is ALWAYS populated, regardless of source.
|
|
28
|
+
* This prevents UI regressions if Slack avatar fails to load.
|
|
29
|
+
*/
|
|
30
|
+
export interface ResolvedAvatar {
|
|
31
|
+
/** Source of the avatar (slack or initials) */
|
|
32
|
+
source: AvatarSource;
|
|
33
|
+
|
|
34
|
+
/** Slack profile image URL (present only if source === 'slack') */
|
|
35
|
+
url?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* User initials for fallback display.
|
|
39
|
+
* REQUIRED - always populated to ensure graceful degradation.
|
|
40
|
+
*/
|
|
41
|
+
initials: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Helper Functions
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate initials from a full name.
|
|
50
|
+
*
|
|
51
|
+
* Takes the first letter of the first word and optionally the last word.
|
|
52
|
+
* Always returns uppercase initials (1-2 characters).
|
|
53
|
+
*
|
|
54
|
+
* @param fullName - The user's full name
|
|
55
|
+
* @returns Uppercase initials (e.g., "IH" for "Ian Heidt", "M" for "Madonna")
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* generateInitials("Ian Heidt") // "IH"
|
|
59
|
+
* generateInitials("Madonna") // "M"
|
|
60
|
+
* generateInitials("Mary Jane Doe") // "MD"
|
|
61
|
+
* generateInitials("") // ""
|
|
62
|
+
*/
|
|
63
|
+
export function generateInitials(fullName: string): string {
|
|
64
|
+
const trimmed = fullName.trim();
|
|
65
|
+
if (!trimmed) return '';
|
|
66
|
+
|
|
67
|
+
const words = trimmed.split(/\s+/);
|
|
68
|
+
const firstInitial = words[0]?.[0]?.toUpperCase() ?? '';
|
|
69
|
+
|
|
70
|
+
if (words.length === 1) {
|
|
71
|
+
return firstInitial;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lastInitial = words[words.length - 1]?.[0]?.toUpperCase() ?? '';
|
|
75
|
+
return `${firstInitial}${lastInitial}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Avatar Resolution
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve avatar from user identity.
|
|
84
|
+
*
|
|
85
|
+
* This is the ONLY approved way to determine which avatar to display.
|
|
86
|
+
* Returns a ResolvedAvatar with source, optional URL, and required initials.
|
|
87
|
+
*
|
|
88
|
+
* INVARIANT: initials is ALWAYS populated, regardless of source.
|
|
89
|
+
* This ensures the UI always has a fallback if the Slack image fails to load.
|
|
90
|
+
*
|
|
91
|
+
* @param identity - Object with slackAvatarUrl and fullName fields
|
|
92
|
+
* @returns ResolvedAvatar with source, url (if slack), and initials
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Slack avatar available
|
|
96
|
+
* resolveAvatar({ slackAvatarUrl: "https://...", fullName: "Ian Heidt" })
|
|
97
|
+
* // { source: 'slack', url: 'https://...', initials: 'IH' }
|
|
98
|
+
*
|
|
99
|
+
* // No Slack avatar (initials fallback)
|
|
100
|
+
* resolveAvatar({ fullName: "Ian Heidt" })
|
|
101
|
+
* // { source: 'initials', initials: 'IH' }
|
|
102
|
+
*/
|
|
103
|
+
export function resolveAvatar(
|
|
104
|
+
identity: Pick<UserIdentity, 'slackAvatarUrl' | 'fullName'>
|
|
105
|
+
): ResolvedAvatar {
|
|
106
|
+
const initials = generateInitials(identity.fullName);
|
|
107
|
+
|
|
108
|
+
if (identity.slackAvatarUrl) {
|
|
109
|
+
return {
|
|
110
|
+
source: 'slack',
|
|
111
|
+
url: identity.slackAvatarUrl,
|
|
112
|
+
initials,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
source: 'initials',
|
|
118
|
+
initials,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -4,30 +4,32 @@
|
|
|
4
4
|
* Canonical functions for deriving and resolving user display names.
|
|
5
5
|
* These are the ONLY approved ways to handle display name logic.
|
|
6
6
|
*
|
|
7
|
-
* @see
|
|
7
|
+
* @see ADR-CONT-032 for design rationale
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { UserIdentity } from './types';
|
|
11
11
|
|
|
12
12
|
// =============================================================================
|
|
13
|
-
//
|
|
13
|
+
// Helper Functions
|
|
14
14
|
// =============================================================================
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Extract the first word from a full name.
|
|
18
|
+
* Used as fallback when preferredName is not set.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @param firstName - User's first name (required)
|
|
22
|
-
* @param lastName - User's last name (optional)
|
|
23
|
-
* @returns Combined full name
|
|
20
|
+
* @param fullName - The user's full name
|
|
21
|
+
* @returns The first word (before first space), or entire string if no spaces
|
|
24
22
|
*
|
|
25
23
|
* @example
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* extractFirstWord("Ian Heidt") // "Ian"
|
|
25
|
+
* extractFirstWord("Madonna") // "Madonna"
|
|
26
|
+
* extractFirstWord("Mary Jane") // "Mary"
|
|
27
|
+
* extractFirstWord("") // ""
|
|
28
28
|
*/
|
|
29
|
-
export function
|
|
30
|
-
|
|
29
|
+
export function extractFirstWord(fullName: string): string {
|
|
30
|
+
const trimmed = fullName.trim();
|
|
31
|
+
const spaceIndex = trimmed.indexOf(' ');
|
|
32
|
+
return spaceIndex === -1 ? trimmed : trimmed.slice(0, spaceIndex);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
// =============================================================================
|
|
@@ -42,19 +44,32 @@ export function deriveFullName(firstName: string, lastName?: string): string {
|
|
|
42
44
|
*
|
|
43
45
|
* Invariants:
|
|
44
46
|
* - preferredName overrides if present and non-empty
|
|
45
|
-
* - Falls back to
|
|
47
|
+
* - Falls back to first word of fullName (never blank if fullName is set)
|
|
46
48
|
* - displayName is NEVER stored, always derived at read time
|
|
47
49
|
*
|
|
48
|
-
* @param identity - Object with preferredName and
|
|
50
|
+
* @param identity - Object with preferredName and fullName fields
|
|
49
51
|
* @returns The resolved display name
|
|
50
52
|
*
|
|
51
53
|
* @example
|
|
52
|
-
* resolveDisplayName({
|
|
53
|
-
* resolveDisplayName({
|
|
54
|
-
* resolveDisplayName({
|
|
54
|
+
* resolveDisplayName({ fullName: "Ian Heidt", preferredName: "Heidt" }) // "Heidt"
|
|
55
|
+
* resolveDisplayName({ fullName: "Ian Heidt", preferredName: "" }) // "Ian"
|
|
56
|
+
* resolveDisplayName({ fullName: "Ian Heidt" }) // "Ian"
|
|
57
|
+
* resolveDisplayName({ fullName: "Madonna" }) // "Madonna"
|
|
55
58
|
*/
|
|
56
59
|
export function resolveDisplayName(
|
|
57
|
-
identity: Pick<UserIdentity, 'preferredName' | '
|
|
60
|
+
identity: Pick<UserIdentity, 'preferredName' | 'fullName'>
|
|
58
61
|
): string {
|
|
59
|
-
return identity.preferredName?.trim() || identity.
|
|
62
|
+
return identity.preferredName?.trim() || extractFirstWord(identity.fullName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Deprecated Functions
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Use fullName directly. firstName/lastName are no longer stored.
|
|
71
|
+
* This function is retained for migration compatibility only.
|
|
72
|
+
*/
|
|
73
|
+
export function deriveFullName(firstName: string, lastName?: string): string {
|
|
74
|
+
return lastName ? `${firstName} ${lastName}` : firstName;
|
|
60
75
|
}
|
package/src/identity/index.ts
CHANGED
|
@@ -6,7 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Types
|
|
9
|
-
export type { ISODateString, UserIdentity } from './types';
|
|
9
|
+
export type { ISODateString, NameSource, UserIdentity } from './types';
|
|
10
10
|
|
|
11
|
-
// Functions
|
|
12
|
-
export {
|
|
11
|
+
// Functions - Display Name
|
|
12
|
+
export {
|
|
13
|
+
extractFirstWord,
|
|
14
|
+
resolveDisplayName,
|
|
15
|
+
/** @deprecated Use fullName directly */
|
|
16
|
+
deriveFullName,
|
|
17
|
+
} from './display-name';
|
|
18
|
+
|
|
19
|
+
// Types - Avatar
|
|
20
|
+
export type { AvatarSource, ResolvedAvatar } from './avatar';
|
|
21
|
+
|
|
22
|
+
// Functions - Avatar
|
|
23
|
+
export { generateInitials, resolveAvatar } from './avatar';
|
package/src/identity/types.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Canonical vocabulary for user identity across Company Semantics.
|
|
5
5
|
*
|
|
6
|
-
* @see
|
|
6
|
+
* @see ADR-CONT-032 for design rationale
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// =============================================================================
|
|
@@ -16,6 +16,19 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export type ISODateString = string;
|
|
18
18
|
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Name Source
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Source of the user's name data.
|
|
25
|
+
* Determines who controls the fullName field.
|
|
26
|
+
*
|
|
27
|
+
* - 'self': User-provided (editable by user)
|
|
28
|
+
* - 'sso': SSO/IdP-provided (read-only, managed by organization)
|
|
29
|
+
*/
|
|
30
|
+
export type NameSource = 'self' | 'sso';
|
|
31
|
+
|
|
19
32
|
// =============================================================================
|
|
20
33
|
// User Identity
|
|
21
34
|
// =============================================================================
|
|
@@ -25,38 +38,59 @@ export type ISODateString = string;
|
|
|
25
38
|
* Represents the canonical identity vocabulary for a user.
|
|
26
39
|
*
|
|
27
40
|
* Invariants:
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
30
|
-
* -
|
|
31
|
-
* - preferredName is STORED (assistant-only addressing)
|
|
41
|
+
* - fullName is the only stored name field (no firstName/lastName)
|
|
42
|
+
* - preferredName is optional and always user-controlled
|
|
43
|
+
* - nameSource determines whether fullName is user-editable
|
|
32
44
|
* - displayName is NEVER stored (derived at read time via resolveDisplayName)
|
|
33
45
|
*/
|
|
34
46
|
export type UserIdentity = {
|
|
35
47
|
/** Unique user identifier */
|
|
36
48
|
userId: string;
|
|
37
49
|
|
|
38
|
-
/** User's first name (required) */
|
|
39
|
-
firstName: string;
|
|
40
|
-
|
|
41
|
-
/** User's last name (optional) */
|
|
42
|
-
lastName?: string;
|
|
43
|
-
|
|
44
50
|
/**
|
|
45
51
|
* Full name for display in UI, exports, and audit logs.
|
|
46
|
-
*
|
|
52
|
+
* Editable when nameSource === 'self', read-only when nameSource === 'sso'.
|
|
47
53
|
*/
|
|
48
54
|
fullName: string;
|
|
49
55
|
|
|
50
56
|
/**
|
|
51
57
|
* Preferred name for AI assistant to use.
|
|
52
|
-
*
|
|
53
|
-
* If not set, assistant falls back to
|
|
58
|
+
* Always user-editable regardless of nameSource.
|
|
59
|
+
* If not set, assistant falls back to first word of fullName via resolveDisplayName().
|
|
54
60
|
*/
|
|
55
61
|
preferredName?: string;
|
|
56
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Source of the name data.
|
|
65
|
+
* Determines whether fullName is editable by the user.
|
|
66
|
+
*/
|
|
67
|
+
nameSource: NameSource;
|
|
68
|
+
|
|
57
69
|
/** When the identity was created */
|
|
58
70
|
createdAt: ISODateString;
|
|
59
71
|
|
|
60
72
|
/** When the identity was last updated */
|
|
61
73
|
updatedAt: ISODateString;
|
|
74
|
+
|
|
75
|
+
// ==========================================================================
|
|
76
|
+
// Slack Avatar Fields (ADR-BE-063)
|
|
77
|
+
// ==========================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Slack user ID (set when Slack is connected and email matches).
|
|
81
|
+
* Used for efficient lookups in user_change event handling.
|
|
82
|
+
*/
|
|
83
|
+
slackUserId?: string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Slack profile image URL (synced from Slack events).
|
|
87
|
+
* May be undefined if Slack not connected or email mismatch.
|
|
88
|
+
*/
|
|
89
|
+
slackAvatarUrl?: string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* When the Slack avatar was last synced.
|
|
93
|
+
* Used for debugging and audit purposes.
|
|
94
|
+
*/
|
|
95
|
+
slackAvatarLastSyncedAt?: ISODateString;
|
|
62
96
|
};
|
package/src/index.ts
CHANGED
|
@@ -105,9 +105,9 @@ export { COMPATIBILITY } from './compatibility'
|
|
|
105
105
|
export type { Compatibility, Deprecation } from './compatibility'
|
|
106
106
|
|
|
107
107
|
// User identity types and functions
|
|
108
|
-
// @see ADR-
|
|
109
|
-
export type { ISODateString, UserIdentity } from './identity/index'
|
|
110
|
-
export {
|
|
108
|
+
// @see ADR-CONT-032 for design rationale
|
|
109
|
+
export type { ISODateString, NameSource, UserIdentity } from './identity/index'
|
|
110
|
+
export { extractFirstWord, resolveDisplayName, deriveFullName } from './identity/index'
|
|
111
111
|
|
|
112
112
|
// Auth domain types
|
|
113
113
|
export { OTPErrorCode } from './auth/index'
|