@company-semantics/contracts 1.17.0 → 1.19.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": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -112,18 +112,18 @@
112
112
  "node": "22.x"
113
113
  },
114
114
  "dependencies": {
115
- "zod": "^4.2.1"
115
+ "zod": "^4.4.3"
116
116
  },
117
117
  "devDependencies": {
118
118
  "@types/node": "^25.6.0",
119
119
  "husky": "^9.1.7",
120
120
  "lint-staged": "^16.4.0",
121
- "markdownlint-cli2": "^0.22.0",
121
+ "markdownlint-cli2": "^0.22.1",
122
122
  "openapi-typescript": "^7.13.0",
123
123
  "tsx": "^4.21.0",
124
124
  "typescript": "^5",
125
- "vitest": "^4.1.4",
126
- "yaml": "^2.8.3"
125
+ "vitest": "^4.1.5",
126
+ "yaml": "^2.8.4"
127
127
  },
128
128
  "pnpm": {
129
129
  "overrides": {
@@ -29,9 +29,14 @@ export {
29
29
  ImpersonationSessionResponseSchema,
30
30
  ImpersonationSessionNullableResponseSchema,
31
31
  EndImpersonationResponseSchema,
32
+ ImpersonationSessionWireDtoSchema,
32
33
  } from './schemas'
33
34
 
34
35
  export type {
35
36
  ImpersonationSessionResponse,
36
37
  EndImpersonationResponse,
38
+ ImpersonationSessionWireDtoParsed,
37
39
  } from './schemas'
40
+
41
+ // Wire DTO for SSE projection (PRD-00557 F5)
42
+ export type { ImpersonationSessionWireDto } from '../impersonation-events'
@@ -36,5 +36,24 @@ export const EndImpersonationResponseSchema = z.object({
36
36
  ok: z.literal(true),
37
37
  })
38
38
 
39
+ /**
40
+ * Wire-only DTO for SSE snapshot/started events (PRD-00557 F5).
41
+ *
42
+ * .strict() rejects extraneous keys so a backend regression that leaks
43
+ * reason / reasonHash / ipAddress / userAgent is caught at the contract
44
+ * boundary, not at the network. This complements the type-level
45
+ * narrowing in src/impersonation-events.ts.
46
+ */
47
+ export const ImpersonationSessionWireDtoSchema = z
48
+ .object({
49
+ id: z.string(),
50
+ targetUserId: z.string(),
51
+ startedAt: z.string().datetime({ offset: true }),
52
+ expiresAt: z.string().datetime({ offset: true }),
53
+ endedAt: z.string().datetime({ offset: true }).nullable(),
54
+ })
55
+ .strict()
56
+
39
57
  export type ImpersonationSessionResponse = z.infer<typeof ImpersonationSessionResponseSchema>
40
58
  export type EndImpersonationResponse = z.infer<typeof EndImpersonationResponseSchema>
59
+ export type ImpersonationSessionWireDtoParsed = z.infer<typeof ImpersonationSessionWireDtoSchema>
@@ -7,7 +7,19 @@
7
7
  */
8
8
 
9
9
  import type { BaseEvent } from './chat/types'
10
- import type { ImpersonationSession } from './impersonation'
10
+
11
+ /**
12
+ * Minimal projection of an impersonation session for over-the-wire delivery.
13
+ * Excludes reason, reasonHash, ipAddress, userAgent — those stay server-side
14
+ * (PRD-00557 F5: type-impossible wire exposure of admin-private fields).
15
+ */
16
+ export interface ImpersonationSessionWireDto {
17
+ readonly id: string
18
+ readonly targetUserId: string
19
+ readonly startedAt: string
20
+ readonly expiresAt: string
21
+ readonly endedAt: string | null
22
+ }
11
23
 
12
24
  /**
13
25
  * Sent as the first frame on SSE connection.
@@ -17,7 +29,7 @@ import type { ImpersonationSession } from './impersonation'
17
29
  export interface ImpersonationSessionSnapshotEvent extends BaseEvent {
18
30
  type: 'impersonation.session.snapshot'
19
31
  data: {
20
- session: ImpersonationSession | null
32
+ session: ImpersonationSessionWireDto | null
21
33
  }
22
34
  }
23
35
 
@@ -25,7 +37,7 @@ export interface ImpersonationSessionSnapshotEvent extends BaseEvent {
25
37
  export interface ImpersonationSessionStartedEvent extends BaseEvent {
26
38
  type: 'impersonation.session.started'
27
39
  data: {
28
- session: ImpersonationSession
40
+ session: ImpersonationSessionWireDto
29
41
  }
30
42
  }
31
43
 
@@ -78,7 +78,13 @@ describe('OrgUnitSchema', () => {
78
78
 
79
79
  describe('OrgUnitTreeNodeSchema', () => {
80
80
  it('requires depth 1..5', () => {
81
- const base = { ...makeUnit(), depth: 3, hasChildren: true, memberCount: 5 };
81
+ const base = {
82
+ ...makeUnit(),
83
+ depth: 3,
84
+ hasChildren: true,
85
+ memberCount: 5,
86
+ missingAtNextLevel: null,
87
+ };
82
88
  expect(() => OrgUnitTreeNodeSchema.parse(base)).not.toThrow();
83
89
  expect(() => OrgUnitTreeNodeSchema.parse({ ...base, depth: 0 })).toThrow();
84
90
  expect(() => OrgUnitTreeNodeSchema.parse({ ...base, depth: 6 })).toThrow();
@@ -154,7 +160,10 @@ describe('OrgLevelConfigSchema', () => {
154
160
  describe('Response schemas', () => {
155
161
  it('OrgUnitTreeResponseSchema accepts empty tree', () => {
156
162
  expect(() =>
157
- OrgUnitTreeResponseSchema.parse({ nodes: [], levelConfig: [] })
163
+ OrgUnitTreeResponseSchema.parse({
164
+ nodes: [],
165
+ levelConfig: [],
166
+ })
158
167
  ).not.toThrow();
159
168
  });
160
169
 
package/src/org/index.ts CHANGED
@@ -234,6 +234,7 @@ export {
234
234
  OrgLevelIconSchema,
235
235
  OrgUnitResponseSchema,
236
236
  OrgUnitTreeResponseSchema,
237
+ MissingAtNextLevelSchema,
237
238
  OrgUnitChildrenResponseSchema,
238
239
  OrgUnitAncestorsResponseSchema,
239
240
  OrgUnitDescendantsResponseSchema,
@@ -255,6 +256,7 @@ export type {
255
256
  OrgLevelIcon,
256
257
  OrgUnitResponse,
257
258
  OrgUnitTreeResponse,
259
+ MissingAtNextLevel,
258
260
  OrgUnitChildrenResponse,
259
261
  OrgUnitAncestorsResponse,
260
262
  OrgUnitDescendantsResponse,
@@ -32,6 +32,7 @@ const WorkspaceMemberSchema = z.object({
32
32
  roleNames: z.array(z.string()),
33
33
  joinedAt: z.string(),
34
34
  lastActiveAt: z.string().nullable(),
35
+ primaryUnitId: z.string().uuid().nullable(),
35
36
  unitMemberships: z.array(WorkspaceMemberUnitSummarySchema),
36
37
  unitMembershipsTruncated: z.boolean(),
37
38
  inviteStatus: z.enum(['active', 'pending', 'expired']).nullable(),
@@ -712,10 +713,23 @@ export const OrgUnitSchema = z.object({
712
713
  updatedAt: z.string(),
713
714
  });
714
715
 
716
+ /**
717
+ * Active users home directly at this unit who have no level-(depth+1) home
718
+ * assignment within its subtree. The settings UI surfaces this as a
719
+ * synthetic "[child level label] assignment missing" row above the
720
+ * children list at every level. `null` for leaf-level units (no children
721
+ * configured below them) where the concept is meaningless.
722
+ */
723
+ export const MissingAtNextLevelSchema = z.object({
724
+ count: z.number().int().min(0),
725
+ userIds: z.array(z.string().uuid()),
726
+ });
727
+
715
728
  export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
716
729
  depth: z.number().int().min(1).max(5),
717
730
  hasChildren: z.boolean(),
718
731
  memberCount: z.number().int().min(0),
732
+ missingAtNextLevel: MissingAtNextLevelSchema.nullable(),
719
733
  });
720
734
 
721
735
  export const OrgUnitMembershipSchema = z.object({
@@ -852,6 +866,7 @@ export type OrgLevelConfig = z.infer<typeof OrgLevelConfigSchema>;
852
866
  export type OrgLevelIcon = z.infer<typeof OrgLevelIconSchema>;
853
867
  export type OrgUnitResponse = z.infer<typeof OrgUnitResponseSchema>;
854
868
  export type OrgUnitTreeResponse = z.infer<typeof OrgUnitTreeResponseSchema>;
869
+ export type MissingAtNextLevel = z.infer<typeof MissingAtNextLevelSchema>;
855
870
  export type OrgUnitChildrenResponse = z.infer<typeof OrgUnitChildrenResponseSchema>;
856
871
  export type OrgUnitAncestorsResponse = z.infer<typeof OrgUnitAncestorsResponseSchema>;
857
872
  export type OrgUnitDescendantsResponse = z.infer<typeof OrgUnitDescendantsResponseSchema>;
package/src/org/types.ts CHANGED
@@ -125,6 +125,12 @@ export interface WorkspaceMember {
125
125
  joinedAt: string;
126
126
  /** ISO timestamp of last activity; null if never recorded. */
127
127
  lastActiveAt: string | null;
128
+ /**
129
+ * Home org-unit (`users.primary_unit_id`). Null for org-scoped users
130
+ * (no level-2 home assignment). Used by the unit-detail Members table to
131
+ * roll up "people in this org-unit" across the subtree.
132
+ */
133
+ primaryUnitId: string | null;
128
134
  /** OrgUnit memberships for this member, capped at a small number server-side. */
129
135
  unitMemberships: WorkspaceMemberUnitSummary[];
130
136
  /** True when server truncated `unitMemberships`; detail endpoint returns the full list. */