@company-semantics/contracts 18.1.1 → 19.0.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/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +5 -0
- package/src/identity/README.md +6 -0
- package/src/identity/__tests__/identity-link.test.ts +52 -0
- package/src/identity/__tests__/person.test.ts +40 -0
- package/src/identity/identity-link.ts +38 -0
- package/src/identity/index.ts +10 -0
- package/src/identity/people-org-chart.ts +18 -0
- package/src/identity/person.ts +28 -0
- package/src/index.ts +10 -0
- package/src/org/structure-facts.ts +9 -0
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '814c99a18543' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '814c99a18543f448daaed5312db276f7704a9b63710c512b1a298be5fc652105' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -4745,6 +4745,11 @@ export interface components {
|
|
|
4745
4745
|
jobTitle: string | null;
|
|
4746
4746
|
avatarUrl: string | null;
|
|
4747
4747
|
primaryUnitId: string | null;
|
|
4748
|
+
ownedUnits: {
|
|
4749
|
+
/** Format: uuid */
|
|
4750
|
+
id: string;
|
|
4751
|
+
name: string;
|
|
4752
|
+
}[];
|
|
4748
4753
|
}[];
|
|
4749
4754
|
edges: {
|
|
4750
4755
|
/** Format: uuid */
|
package/src/identity/README.md
CHANGED
|
@@ -44,6 +44,8 @@ TypeScript types and functions for user identity and display name resolution.
|
|
|
44
44
|
- `BannerDismissedListResponseSchema`
|
|
45
45
|
- `DeletionBlocker` _(type)_ — Conditions that block account deletion.
|
|
46
46
|
- `ISODateString` _(type)_ — ISO 8601 date-time string.
|
|
47
|
+
- `IdentityLink` _(type)_
|
|
48
|
+
- `IdentityLinkSchema`
|
|
47
49
|
- `MeResponse` _(type)_
|
|
48
50
|
- `MeResponseSchema` — Full identity context returned by GET /api/me.
|
|
49
51
|
- `NameSource` _(type)_ — Source of the user's name data.
|
|
@@ -53,8 +55,12 @@ TypeScript types and functions for user identity and display name resolution.
|
|
|
53
55
|
- `PeopleOrgChartNodeSchema`
|
|
54
56
|
- `PeopleOrgChartOpenRole` _(type)_
|
|
55
57
|
- `PeopleOrgChartOpenRoleSchema`
|
|
58
|
+
- `PeopleOrgChartOwnedUnit` _(type)_
|
|
59
|
+
- `PeopleOrgChartOwnedUnitSchema`
|
|
56
60
|
- `PeopleOrgChartResponse` _(type)_
|
|
57
61
|
- `PeopleOrgChartResponseSchema`
|
|
62
|
+
- `Person` _(type)_
|
|
63
|
+
- `PersonSchema`
|
|
58
64
|
- `ProfileResponse` _(type)_
|
|
59
65
|
- `ProfileResponseSchema`
|
|
60
66
|
- `ReportingRelationshipType` _(type)_
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { IdentityLinkSchema } from "../identity-link.js";
|
|
3
|
+
|
|
4
|
+
const validLink = {
|
|
5
|
+
id: "11111111-1111-4111-8111-111111111111",
|
|
6
|
+
orgId: "22222222-2222-4222-8222-222222222222",
|
|
7
|
+
provider: "bamboohr",
|
|
8
|
+
externalId: "emp-4081",
|
|
9
|
+
personId: "33333333-3333-4333-8333-333333333333",
|
|
10
|
+
createdAt: "2026-06-21T10:00:00.000Z",
|
|
11
|
+
updatedAt: "2026-06-21T10:00:00.000Z",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("IdentityLinkSchema", () => {
|
|
15
|
+
it("accepts a valid identity link record", () => {
|
|
16
|
+
expect(IdentityLinkSchema.parse(validLink)).toEqual(validLink);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("accepts arbitrary provider strings (open vocabulary)", () => {
|
|
20
|
+
for (const provider of ["bamboohr", "workday", "scim", "custom-hris"]) {
|
|
21
|
+
expect(
|
|
22
|
+
IdentityLinkSchema.safeParse({ ...validLink, provider }).success,
|
|
23
|
+
).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects an empty provider", () => {
|
|
28
|
+
expect(
|
|
29
|
+
IdentityLinkSchema.safeParse({ ...validLink, provider: "" }).success,
|
|
30
|
+
).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("rejects an empty externalId", () => {
|
|
34
|
+
expect(
|
|
35
|
+
IdentityLinkSchema.safeParse({ ...validLink, externalId: "" }).success,
|
|
36
|
+
).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects a non-uuid personId", () => {
|
|
40
|
+
expect(
|
|
41
|
+
IdentityLinkSchema.safeParse({ ...validLink, personId: "not-a-uuid" })
|
|
42
|
+
.success,
|
|
43
|
+
).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects a non-datetime createdAt", () => {
|
|
47
|
+
expect(
|
|
48
|
+
IdentityLinkSchema.safeParse({ ...validLink, createdAt: "yesterday" })
|
|
49
|
+
.success,
|
|
50
|
+
).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { PersonSchema } from "../person.js";
|
|
4
|
+
|
|
5
|
+
describe("PersonSchema", () => {
|
|
6
|
+
const base = {
|
|
7
|
+
id: "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
|
8
|
+
orgId: "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b",
|
|
9
|
+
displayName: "Ada Lovelace",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
it("parses a minimal accountless person", () => {
|
|
13
|
+
const person = PersonSchema.parse(base);
|
|
14
|
+
expect(person.displayName).toBe("Ada Lovelace");
|
|
15
|
+
expect(person.userId).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("accepts a null userId (accountless) and a uuid userId (linked)", () => {
|
|
19
|
+
expect(PersonSchema.parse({ ...base, userId: null }).userId).toBeNull();
|
|
20
|
+
const linked = PersonSchema.parse({
|
|
21
|
+
...base,
|
|
22
|
+
userId: "9c5b94b1-35ad-49bb-b118-8e8fc24abf80",
|
|
23
|
+
});
|
|
24
|
+
expect(linked.userId).toBe("9c5b94b1-35ad-49bb-b118-8e8fc24abf80");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("accepts a null primaryEmail and validates email format", () => {
|
|
28
|
+
expect(
|
|
29
|
+
PersonSchema.parse({ ...base, primaryEmail: null }).primaryEmail,
|
|
30
|
+
).toBeNull();
|
|
31
|
+
expect(() =>
|
|
32
|
+
PersonSchema.parse({ ...base, primaryEmail: "not-an-email" }),
|
|
33
|
+
).toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects an empty displayName and non-uuid id", () => {
|
|
37
|
+
expect(() => PersonSchema.parse({ ...base, displayName: "" })).toThrow();
|
|
38
|
+
expect(() => PersonSchema.parse({ ...base, id: "nope" })).toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity Link Vocabulary
|
|
3
|
+
*
|
|
4
|
+
* An IdentityLink is a durable map from an external system's identifier to a
|
|
5
|
+
* Person in the org graph. It generalizes the legacy
|
|
6
|
+
* `users.external_source_id` / `users.external_source_system` pair into a
|
|
7
|
+
* first-class, per-source record: a BambooHR link is simply an IdentityLink
|
|
8
|
+
* with `provider: 'bamboohr'`.
|
|
9
|
+
*
|
|
10
|
+
* `provider` is an OPEN string (e.g. 'bamboohr', 'workday', 'scim') rather than
|
|
11
|
+
* an enum, so onboarding a new identity source needs no schema change.
|
|
12
|
+
*
|
|
13
|
+
* Logical uniqueness is the tuple (orgId, provider, externalId): a given
|
|
14
|
+
* external id from a given provider, within a given org, maps to exactly one
|
|
15
|
+
* Person. The (orgId, provider, externalId) tuple is the natural key.
|
|
16
|
+
*
|
|
17
|
+
* @see ADR-CONT-032 for identity vocabulary design rationale
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// IdentityLink — external system id → Person
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export const IdentityLinkSchema = z.object({
|
|
26
|
+
id: z.string().uuid(),
|
|
27
|
+
orgId: z.string().uuid(),
|
|
28
|
+
// Open string (NOT an enum) so new sources need no schema change.
|
|
29
|
+
provider: z.string().min(1),
|
|
30
|
+
// The opaque identifier the external system uses for this person.
|
|
31
|
+
externalId: z.string().min(1),
|
|
32
|
+
// The Person this external identity resolves to (Person.id).
|
|
33
|
+
personId: z.string().uuid(),
|
|
34
|
+
createdAt: z.string().datetime(),
|
|
35
|
+
updatedAt: z.string().datetime(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type IdentityLink = z.infer<typeof IdentityLinkSchema>;
|
package/src/identity/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ export type {
|
|
|
65
65
|
export {
|
|
66
66
|
ReportingRelationshipTypeSchema,
|
|
67
67
|
PeopleOrgChartNodeSchema,
|
|
68
|
+
PeopleOrgChartOwnedUnitSchema,
|
|
68
69
|
PeopleOrgChartEdgeSchema,
|
|
69
70
|
PeopleOrgChartOpenRoleSchema,
|
|
70
71
|
PeopleOrgChartResponseSchema,
|
|
@@ -72,7 +73,16 @@ export {
|
|
|
72
73
|
export type {
|
|
73
74
|
ReportingRelationshipType,
|
|
74
75
|
PeopleOrgChartNode,
|
|
76
|
+
PeopleOrgChartOwnedUnit,
|
|
75
77
|
PeopleOrgChartEdge,
|
|
76
78
|
PeopleOrgChartOpenRole,
|
|
77
79
|
PeopleOrgChartResponse,
|
|
78
80
|
} from "./people-org-chart";
|
|
81
|
+
|
|
82
|
+
// Person — the org-graph node for a human (0..1 User link), per ADR-CTRL-182
|
|
83
|
+
export { PersonSchema } from "./person";
|
|
84
|
+
export type { Person } from "./person";
|
|
85
|
+
|
|
86
|
+
// Identity Link — external system id → Person (provider is an open string)
|
|
87
|
+
export { IdentityLinkSchema } from "./identity-link";
|
|
88
|
+
export type { IdentityLink } from "./identity-link";
|
|
@@ -26,12 +26,30 @@ export type ReportingRelationshipType = z.infer<
|
|
|
26
26
|
// Node — a person in the chart
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
|
+
// A unit this person is a *local structural owner* of — i.e. holds a grant with
|
|
30
|
+
// `authority_source = 'structural'` directly on that unit (ADR-BE-168). Carries
|
|
31
|
+
// the unit name so the chart can render an owner tooltip ("Unit owner of …")
|
|
32
|
+
// without a second lookup. The org root unit appears here for the account
|
|
33
|
+
// owner(s) (the CEO), whom the chart marks with the owner icon instead.
|
|
34
|
+
export const PeopleOrgChartOwnedUnitSchema = z.object({
|
|
35
|
+
id: z.string().uuid(),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type PeopleOrgChartOwnedUnit = z.infer<
|
|
40
|
+
typeof PeopleOrgChartOwnedUnitSchema
|
|
41
|
+
>;
|
|
42
|
+
|
|
29
43
|
export const PeopleOrgChartNodeSchema = z.object({
|
|
30
44
|
id: z.string().uuid(),
|
|
31
45
|
fullName: z.string(),
|
|
32
46
|
jobTitle: z.string().nullable(),
|
|
33
47
|
avatarUrl: z.string().nullable(),
|
|
34
48
|
primaryUnitId: z.string().uuid().nullable(),
|
|
49
|
+
// Units this person is a local structural owner of. Empty ⇒ not a unit owner.
|
|
50
|
+
// Drives the chart's owner key + tooltip; the account owner (root-unit owner)
|
|
51
|
+
// is rendered with the org owner icon (crown) instead.
|
|
52
|
+
ownedUnits: z.array(PeopleOrgChartOwnedUnitSchema),
|
|
35
53
|
});
|
|
36
54
|
|
|
37
55
|
export type PeopleOrgChartNode = z.infer<typeof PeopleOrgChartNodeSchema>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Person Identity Vocabulary
|
|
3
|
+
*
|
|
4
|
+
* Person: the human in the org — the org-graph node. Distinct from `User` (an
|
|
5
|
+
* authenticated account). A Person has 0..1 linked User (`userId`); accountless
|
|
6
|
+
* People are valid from day one. Identity vocabulary, NOT a structure fact, so it
|
|
7
|
+
* carries no FactProvenance envelope. See control ADR-CTRL-182.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Person — the org-graph node for a human, with an optional 0..1 User link
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export const PersonSchema = z.object({
|
|
16
|
+
/** Unique identifier for the person within the org graph. */
|
|
17
|
+
id: z.string().uuid(),
|
|
18
|
+
/** Owning organization (tenant) this person belongs to. */
|
|
19
|
+
orgId: z.string().uuid(),
|
|
20
|
+
/** Human-facing name of the person in the org. */
|
|
21
|
+
displayName: z.string().min(1),
|
|
22
|
+
/** Primary email if known; null for people imported without one. */
|
|
23
|
+
primaryEmail: z.string().email().nullable().optional(),
|
|
24
|
+
/** The authenticated account this person is, if any (0..1). Null when accountless. */
|
|
25
|
+
userId: z.string().uuid().nullable().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type Person = z.infer<typeof PersonSchema>;
|
package/src/index.ts
CHANGED
|
@@ -171,6 +171,7 @@ export type {
|
|
|
171
171
|
export {
|
|
172
172
|
ReportingRelationshipTypeSchema,
|
|
173
173
|
PeopleOrgChartNodeSchema,
|
|
174
|
+
PeopleOrgChartOwnedUnitSchema,
|
|
174
175
|
PeopleOrgChartEdgeSchema,
|
|
175
176
|
PeopleOrgChartOpenRoleSchema,
|
|
176
177
|
PeopleOrgChartResponseSchema,
|
|
@@ -178,11 +179,20 @@ export {
|
|
|
178
179
|
export type {
|
|
179
180
|
ReportingRelationshipType,
|
|
180
181
|
PeopleOrgChartNode,
|
|
182
|
+
PeopleOrgChartOwnedUnit,
|
|
181
183
|
PeopleOrgChartEdge,
|
|
182
184
|
PeopleOrgChartOpenRole,
|
|
183
185
|
PeopleOrgChartResponse,
|
|
184
186
|
} from "./identity/index";
|
|
185
187
|
|
|
188
|
+
// Person — the org-graph node for a human (0..1 User link), per ADR-CTRL-182
|
|
189
|
+
export { PersonSchema } from "./identity/index";
|
|
190
|
+
export type { Person } from "./identity/index";
|
|
191
|
+
|
|
192
|
+
// Identity Link — external system id → Person (provider is an open string)
|
|
193
|
+
export { IdentityLinkSchema } from "./identity/index";
|
|
194
|
+
export type { IdentityLink } from "./identity/index";
|
|
195
|
+
|
|
186
196
|
// Auth domain types
|
|
187
197
|
export type { AuthStartMode, AuthStartResponse } from "./auth/index";
|
|
188
198
|
export { OTPErrorCode } from "./auth/index";
|
|
@@ -76,6 +76,15 @@ export const FactProvenanceSchema = z.object({
|
|
|
76
76
|
* for an original. Enables append+supersede without destructive updates.
|
|
77
77
|
*/
|
|
78
78
|
supersedesFactId: z.string().uuid().nullable().optional(),
|
|
79
|
+
/**
|
|
80
|
+
* Human-meaningful WHY for a deliberate human edit — a free-text audit
|
|
81
|
+
* record of intent, capped at 2000 chars to mirror the delegation-note /
|
|
82
|
+
* unit-owner reason API cap. `null`/absent for machine-sourced facts (sync,
|
|
83
|
+
* import, inference). It is an audit record of intent, NOT an authorization
|
|
84
|
+
* grant: it explains why a fact was set, never who may act on it
|
|
85
|
+
* (ADR-CTRL-180).
|
|
86
|
+
*/
|
|
87
|
+
reason: z.string().max(2000).nullable().optional(),
|
|
79
88
|
});
|
|
80
89
|
export type FactProvenance = z.infer<typeof FactProvenanceSchema>;
|
|
81
90
|
|