@company-semantics/contracts 0.38.0 → 0.40.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- - `preferredName` is assistant-facing only, not user-edited label
12
- - `fullName` is user-editable override of derived firstName + lastName
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
- - `UserIdentity` — User identity with name fields and timestamps
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
- - `deriveFullName(firstName, lastName?)` — Derive full name from components
26
- - `resolveDisplayName(identity)` — Get display name (preferredName or firstName)
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
 
@@ -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 DECISIONS.md ADR-2026-01-020 for design rationale
7
+ * @see ADR-CONT-032 for design rationale
8
8
  */
9
9
 
10
10
  import type { UserIdentity } from './types';
11
11
 
12
12
  // =============================================================================
13
- // Full Name Derivation
13
+ // Helper Functions
14
14
  // =============================================================================
15
15
 
16
16
  /**
17
- * Derive fullName from firstName and lastName.
17
+ * Extract the first word from a full name.
18
+ * Used as fallback when preferredName is not set.
18
19
  *
19
- * Invariant: fullName MUST be derived automatically unless explicitly provided.
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
- * deriveFullName("Ian", "Heidt") // "Ian Heidt"
27
- * deriveFullName("Ian") // "Ian"
24
+ * extractFirstWord("Ian Heidt") // "Ian"
25
+ * extractFirstWord("Madonna") // "Madonna"
26
+ * extractFirstWord("Mary Jane") // "Mary"
27
+ * extractFirstWord("") // ""
28
28
  */
29
- export function deriveFullName(firstName: string, lastName?: string): string {
30
- return lastName ? `${firstName} ${lastName}` : firstName;
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 firstName (never blank)
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 firstName fields
50
+ * @param identity - Object with preferredName and fullName fields
49
51
  * @returns The resolved display name
50
52
  *
51
53
  * @example
52
- * resolveDisplayName({ firstName: "Ian", preferredName: "Heidt" }) // "Heidt"
53
- * resolveDisplayName({ firstName: "Ian", preferredName: "" }) // "Ian"
54
- * resolveDisplayName({ firstName: "Ian" }) // "Ian"
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' | 'firstName'>
60
+ identity: Pick<UserIdentity, 'preferredName' | 'fullName'>
58
61
  ): string {
59
- return identity.preferredName?.trim() || identity.firstName;
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
  }
@@ -6,7 +6,12 @@
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
11
  // Functions
12
- export { deriveFullName, resolveDisplayName } from './display-name';
12
+ export {
13
+ extractFirstWord,
14
+ resolveDisplayName,
15
+ /** @deprecated Use fullName directly */
16
+ deriveFullName,
17
+ } from './display-name';
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Canonical vocabulary for user identity across Company Semantics.
5
5
  *
6
- * @see DECISIONS.md ADR-2026-01-020 for design rationale
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,35 +38,34 @@ export type ISODateString = string;
25
38
  * Represents the canonical identity vocabulary for a user.
26
39
  *
27
40
  * Invariants:
28
- * - firstName is required (always addressable)
29
- * - lastName is optional (single-name users exist globally)
30
- * - fullName is STORED (derived by default, user-editable)
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
- * Derived by default from firstName + lastName, but user-editable.
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
- * Assistant-only field; may differ from firstName.
53
- * If not set, assistant falls back to firstName via resolveDisplayName().
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
 
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-2026-01-020 for design rationale
109
- export type { ISODateString, UserIdentity } from './identity/index'
110
- export { deriveFullName, resolveDisplayName } from './identity/index'
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'
@@ -152,9 +152,15 @@ export type {
152
152
  OrgDomain,
153
153
  Phase4AuditAction,
154
154
  UserOrgMembership,
155
+ // Authorization context types (Phase 5)
156
+ // @see ADR-CTRL-010 for design rationale
157
+ OrgScopedContext,
155
158
  } from './org/index'
156
159
 
157
- export { ROLE_DISPLAY_MAP, WORKSPACE_CAPABILITIES, ROLE_CAPABILITY_MAP } from './org/index'
160
+ export { ROLE_DISPLAY_MAP, WORKSPACE_CAPABILITIES, ROLE_CAPABILITY_MAP, VIEW_SCOPE_MAP, getViewScope } from './org/index'
161
+
162
+ // View authorization types (Phase 5 - ADR-APP-013)
163
+ export type { AuthorizableView } from './org/index'
158
164
 
159
165
  // MCP tool discovery types
160
166
  // @see company-semantics-backend/src/interfaces/mcp/ for implementation
package/src/org/index.ts CHANGED
@@ -34,6 +34,8 @@ export type {
34
34
  Phase3AuditAction,
35
35
  // Multi-org membership types (Phase 4)
36
36
  UserOrgMembership,
37
+ // Authorization context types (Phase 5 - ADR-CTRL-010)
38
+ OrgScopedContext,
37
39
  } from './types';
38
40
 
39
41
  export { ROLE_DISPLAY_MAP } from './types';
@@ -49,3 +51,7 @@ export type {
49
51
  OrgDomain,
50
52
  Phase4AuditAction,
51
53
  } from './domain';
54
+
55
+ // View authorization scopes (Phase 5 - ADR-APP-013)
56
+ export type { AuthorizableView } from './view-scopes';
57
+ export { VIEW_SCOPE_MAP, getViewScope } from './view-scopes';
package/src/org/types.ts CHANGED
@@ -329,3 +329,49 @@ export interface UserOrgMembership {
329
329
  /** Whether this membership is currently active. */
330
330
  isActive: boolean;
331
331
  }
332
+
333
+ // ─────────────────────────────────────────────────────────────────────────────
334
+ // Authorization Context Types (Phase 5 - ADR-CTRL-010)
335
+ // ─────────────────────────────────────────────────────────────────────────────
336
+
337
+ /**
338
+ * Validated organization-scoped context for policy implementations.
339
+ *
340
+ * This type provides compile-time proof that org membership was validated
341
+ * upstream. Policy implementations MUST accept this type instead of raw
342
+ * `orgId: string` to prevent user-controlled org bypass attacks.
343
+ *
344
+ * @invariant orgId comes from validated session, never user input
345
+ * @invariant userId is the authenticated user from session
346
+ * @invariant _orgValidated is a type-level marker (not runtime)
347
+ *
348
+ * @see ADR-CTRL-010 Org-Scoped Context Type requirement
349
+ * @see ADR-BE-060 Policy Delegation Pattern
350
+ *
351
+ * @example
352
+ * ```typescript
353
+ * // Policy implementation - accepts context, not raw orgId
354
+ * export const DomainViewPolicyImpl = {
355
+ * async getDomainById(
356
+ * db: Db,
357
+ * ctx: OrgScopedContext,
358
+ * domainId: string
359
+ * ): Promise<OrgDomain | null> {
360
+ * return db.select().from(orgDomains)
361
+ * .where(and(eq(orgDomains.orgId, ctx.orgId), eq(orgDomains.id, domainId)));
362
+ * }
363
+ * };
364
+ * ```
365
+ */
366
+ export interface OrgScopedContext {
367
+ /** Organization ID from validated session. */
368
+ readonly orgId: string;
369
+ /** Authenticated user ID from session. */
370
+ readonly userId: string;
371
+ /**
372
+ * Type-level marker proving org validation occurred.
373
+ * This is a compile-time constraint, not a runtime value.
374
+ * Set to `true as const` when constructing from validated session.
375
+ */
376
+ readonly _orgValidated: true;
377
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * View Authorization Scope Mapping
3
+ *
4
+ * Maps frontend views to required RBAC scopes.
5
+ * Used by useViewAuthorization hook for centralized view access control.
6
+ *
7
+ * See: ADR-APP-013
8
+ *
9
+ * INVARIANT: VIEW_SCOPE_MAP must stay in sync with AppView type in app-state.ts
10
+ */
11
+
12
+ /**
13
+ * View-to-scope mapping for frontend authorization.
14
+ *
15
+ * - Views with string values require that scope for access
16
+ * - Views with null values are public (require only authentication)
17
+ */
18
+ export const VIEW_SCOPE_MAP = {
19
+ // Protected views (require specific scope)
20
+ workspace: 'org.view_workspace',
21
+ timeline: 'org.view_timeline',
22
+ dashboard: 'org.view_dashboard',
23
+ 'organization-strategy': 'org.view_strategy',
24
+ 'system-snapshot': 'org.view_system',
25
+ // Public views (require only authentication)
26
+ chat: null,
27
+ settings: null,
28
+ chats: null,
29
+ upgrade: null,
30
+ } as const;
31
+
32
+ /**
33
+ * Type for views that can be checked against VIEW_SCOPE_MAP.
34
+ */
35
+ export type AuthorizableView = keyof typeof VIEW_SCOPE_MAP;
36
+
37
+ /**
38
+ * Get the required scope for a view, or null if public.
39
+ */
40
+ export function getViewScope(view: string): string | null {
41
+ return VIEW_SCOPE_MAP[view as AuthorizableView] ?? null;
42
+ }