@elevasis/core 0.35.1 → 0.37.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.
Files changed (32) hide show
  1. package/dist/auth/index.d.ts +124 -2
  2. package/dist/auth/index.js +14 -1
  3. package/dist/index.d.ts +334 -3
  4. package/dist/index.js +21 -5
  5. package/dist/knowledge/index.d.ts +81 -0
  6. package/dist/organization-model/index.d.ts +334 -3
  7. package/dist/organization-model/index.js +21 -5
  8. package/dist/test-utils/index.d.ts +97 -0
  9. package/dist/test-utils/index.js +19 -4
  10. package/package.json +1 -1
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +67 -0
  12. package/src/auth/multi-tenancy/invitations/api-schemas.ts +24 -9
  13. package/src/auth/multi-tenancy/invitations/index.ts +11 -9
  14. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +39 -3
  15. package/src/auth/multi-tenancy/organizations/api-schemas.ts +36 -8
  16. package/src/auth/multi-tenancy/organizations/index.ts +13 -11
  17. package/src/business/acquisition/build-templates.test.ts +34 -0
  18. package/src/business/acquisition/build-templates.ts +8 -1
  19. package/src/organization-model/__tests__/domains/navigation-topbar.test.ts +282 -0
  20. package/src/organization-model/__tests__/migration-helpers.test.ts +11 -11
  21. package/src/organization-model/defaults.ts +2 -1
  22. package/src/organization-model/domains/navigation.ts +176 -139
  23. package/src/organization-model/icons.ts +1 -0
  24. package/src/organization-model/migration-helpers.ts +8 -1
  25. package/src/organization-model/published.ts +6 -6
  26. package/src/organization-model/types.ts +5 -1
  27. package/src/platform/constants/versions.ts +1 -1
  28. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2054
  29. package/src/reference/_generated/contracts.md +67 -0
  30. package/src/scaffold-registry/index.ts +4 -4
  31. package/src/supabase/database.types.ts +15 -0
  32. package/src/test-utils/rls/RLSTestContext.ts +3 -3
@@ -2011,7 +2011,7 @@ var RLSTestContext = class {
2011
2011
  if (roleErr || !roleDef) {
2012
2012
  throw new Error(`Failed to look up system role '${slug}': ${roleErr?.message ?? "not found"}`);
2013
2013
  }
2014
- const { error: assignErr } = await this.adminClient.from("org_rol_assignments").insert({ membership_id: membershipId, role_id: roleDef.id });
2014
+ const { error: assignErr } = await this.adminClient.from("org_rol_assignments").upsert({ membership_id: membershipId, role_id: roleDef.id }, { onConflict: "membership_id,role_id" });
2015
2015
  if (assignErr) {
2016
2016
  throw new Error(`Failed to assign system role '${slug}' to membership: ${assignErr.message}`);
2017
2017
  }
@@ -19817,6 +19817,7 @@ var ORGANIZATION_MODEL_ICON_TOKENS = [
19817
19817
  "view",
19818
19818
  "launch",
19819
19819
  "message",
19820
+ "message-plus",
19820
19821
  "escalate",
19821
19822
  "promote",
19822
19823
  "submit",
@@ -20001,9 +20002,22 @@ var SidebarNavigationSchema = z.object({
20001
20002
  primary: SidebarSectionSchema,
20002
20003
  bottom: SidebarSectionSchema
20003
20004
  }).default({ primary: {}, bottom: {} });
20005
+ var TopbarActionNodeSchema = z.object({
20006
+ id: ModelIdSchema,
20007
+ label: LabelSchema,
20008
+ tooltip: DescriptionSchema.optional(),
20009
+ icon: IconNameSchema.optional(),
20010
+ order: z.number().int().optional(),
20011
+ enabled: z.boolean().default(true),
20012
+ devOnly: z.boolean().optional(),
20013
+ requiresAdmin: z.boolean().optional(),
20014
+ targets: SidebarSurfaceTargetsSchema.optional()
20015
+ });
20016
+ var TopbarSectionSchema = z.record(z.string(), TopbarActionNodeSchema).default({});
20004
20017
  var OrganizationModelNavigationSchema = z.object({
20005
- sidebar: SidebarNavigationSchema
20006
- }).default({ sidebar: { primary: {}, bottom: {} } });
20018
+ sidebar: SidebarNavigationSchema,
20019
+ topbar: TopbarSectionSchema
20020
+ }).default({ sidebar: { primary: {}, bottom: {} }, topbar: {} });
20007
20021
  z.object({
20008
20022
  id: ModelIdSchema,
20009
20023
  label: LabelSchema,
@@ -21608,7 +21622,8 @@ var DEFAULT_ORGANIZATION_MODEL_NAVIGATION = {
21608
21622
  sidebar: {
21609
21623
  primary: {},
21610
21624
  bottom: {}
21611
- }
21625
+ },
21626
+ topbar: {}
21612
21627
  };
21613
21628
  var DEFAULT_ORGANIZATION_MODEL = {
21614
21629
  version: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/core",
3
- "version": "0.35.1",
3
+ "version": "0.37.0",
4
4
  "license": "MIT",
5
5
  "description": "Minimal shared constants across Elevasis monorepo",
6
6
  "sideEffects": false,
@@ -135,6 +135,18 @@ export type OrganizationModelSidebarSurfaceNode = Extract<OrganizationModelSideb
135
135
  export type OrganizationModelSidebarGroupNode = Extract<OrganizationModelSidebarNode, { type: 'group' }>
136
136
  ```
137
137
 
138
+ ### `OrganizationModelTopbarActionNode`
139
+
140
+ ```typescript
141
+ export type OrganizationModelTopbarActionNode = z.infer<typeof TopbarActionNodeSchema>
142
+ ```
143
+
144
+ ### `OrganizationModelTopbarSection`
145
+
146
+ ```typescript
147
+ export type OrganizationModelTopbarSection = z.infer<typeof TopbarSectionSchema>
148
+ ```
149
+
138
150
  ### `OrganizationModelTechStackEntry`
139
151
 
140
152
  ```typescript
@@ -845,6 +857,57 @@ export interface ShellRuntime {
845
857
  }
846
858
  ```
847
859
 
860
+ ### `ResolvedTopbarAction`
861
+
862
+ ```typescript
863
+ /**
864
+ * A resolved topbar action — the OM node after gating/filtering, with icon resolved
865
+ * from the semantic icon registry. Passed to `TopbarActionModule.render`.
866
+ */
867
+ export interface ResolvedTopbarAction {
868
+ id: string
869
+ label: string
870
+ tooltip?: string
871
+ icon: TablerIconComponent
872
+ order: number
873
+ }
874
+ ```
875
+
876
+ ### `TopbarActionModule`
877
+
878
+ ```typescript
879
+ /**
880
+ * A topbar action module — binds a registry key to UI behavior (a render callback).
881
+ * The key must match `navigation.topbar[id]`. The OM supplies data + visibility;
882
+ * the module supplies behavior.
883
+ */
884
+ export interface TopbarActionModule {
885
+ /** Stable key that matches the `id` of a `navigation.topbar` node. */
886
+ key: string
887
+ /** Render the topbar action. Receives the resolved OM node (icon pre-resolved). */
888
+ render: (ctx: { node: ResolvedTopbarAction }) => ReactNode
889
+ }
890
+ ```
891
+
892
+ ### `ResolvedTopbarActionEntry`
893
+
894
+ ```typescript
895
+ /** A joined entry emitted by `getTopbarActions`: resolved OM node + module behavior. */
896
+ export interface ResolvedTopbarActionEntry {
897
+ node: ResolvedTopbarAction
898
+ render: TopbarActionModule['render']
899
+ }
900
+ ```
901
+
902
+ ### `TopbarActionsProjectionOptions`
903
+
904
+ ```typescript
905
+ export interface TopbarActionsProjectionOptions {
906
+ isPlatformAdmin?: boolean
907
+ isDev?: boolean
908
+ }
909
+ ```
910
+
848
911
  ### `OrganizationGraphSystemBridge`
849
912
 
850
913
  ```typescript
@@ -868,6 +931,8 @@ export interface OrganizationGraphContextValue {
868
931
  ```typescript
869
932
  export interface ElevasisSystemsProviderProps {
870
933
  systems?: SystemModule[]
934
+ /** Registered topbar action modules. OM node presence controls visibility; module supplies behavior. */
935
+ topbarActions?: TopbarActionModule[]
871
936
  organizationModel?: ElevasisOrganizationModel
872
937
  timeRange?: TimeRange
873
938
  operationsApiUrl?: string
@@ -886,6 +951,8 @@ export interface ElevasisSystemsContextValue {
886
951
  shellModel: ResolvedShellModel
887
952
  shellRuntime: ShellRuntime
888
953
  getSidebarLinks: (options?: ShellSidebarProjectionOptions) => ShellSidebarLinkGroup[]
954
+ /** Returns the list of topbar actions visible to the current user, in order. */
955
+ getTopbarActions: (options?: TopbarActionsProjectionOptions) => ResolvedTopbarActionEntry[]
889
956
  enabledResolvedSystems: ResolvedSystemModule[]
890
957
  resolvedSystems: ResolvedSystemModule[]
891
958
  organizationGraph: OrganizationGraphContextValue
@@ -59,11 +59,25 @@ export const SendInvitationSchema = z
59
59
  * Security:
60
60
  * - Token validated (non-empty string)
61
61
  */
62
- export const AcceptInvitationSchema = z
63
- .object({
64
- invitation_token: z.string().min(1, 'Invitation token is required')
65
- })
66
- .strict()
62
+ export const AcceptInvitationSchema = z
63
+ .object({
64
+ invitation_token: z.string().min(1, 'Invitation token is required')
65
+ })
66
+ .strict()
67
+
68
+ /**
69
+ * Public invitation router query
70
+ * GET /api/invite
71
+ *
72
+ * Security:
73
+ * - Token validated as present only; service layer performs WorkOS lookup
74
+ * - Router redirects with only the token needed by tenant AuthKit login
75
+ */
76
+ export const InviteRouterQuerySchema = z
77
+ .object({
78
+ invitation_token: z.string().min(1, 'Invitation token is required')
79
+ })
80
+ .strict()
67
81
 
68
82
  // ============================================================================
69
83
  // Query Parameters
@@ -98,7 +112,8 @@ export const ListInvitationsQuerySchema = z
98
112
  // ============================================================================
99
113
 
100
114
  // Export inferred types for use in route handlers
101
- export type SendInvitationInput = z.infer<typeof SendInvitationSchema>
102
- export type AcceptInvitationInput = z.infer<typeof AcceptInvitationSchema>
103
- export type ListInvitationsQuery = z.infer<typeof ListInvitationsQuerySchema>
104
- export type InvitationIdParam = z.infer<typeof InvitationIdParamSchema>
115
+ export type SendInvitationInput = z.infer<typeof SendInvitationSchema>
116
+ export type AcceptInvitationInput = z.infer<typeof AcceptInvitationSchema>
117
+ export type InviteRouterQuery = z.infer<typeof InviteRouterQuerySchema>
118
+ export type ListInvitationsQuery = z.infer<typeof ListInvitationsQuerySchema>
119
+ export type InvitationIdParam = z.infer<typeof InvitationIdParamSchema>
@@ -27,12 +27,14 @@ export { transformSupabaseToInvitation } from './supabase'
27
27
 
28
28
  // Validation schemas
29
29
  export {
30
- SendInvitationSchema,
31
- AcceptInvitationSchema,
32
- ListInvitationsQuerySchema,
33
- InvitationIdParamSchema,
34
- type SendInvitationInput,
35
- type AcceptInvitationInput,
36
- type ListInvitationsQuery,
37
- type InvitationIdParam
38
- } from './api-schemas'
30
+ SendInvitationSchema,
31
+ AcceptInvitationSchema,
32
+ InviteRouterQuerySchema,
33
+ ListInvitationsQuerySchema,
34
+ InvitationIdParamSchema,
35
+ type SendInvitationInput,
36
+ type AcceptInvitationInput,
37
+ type InviteRouterQuery,
38
+ type ListInvitationsQuery,
39
+ type InvitationIdParam
40
+ } from './api-schemas'
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { CreateOrganizationSchema, ListOrganizationsQuerySchema, UpdateOrganizationSchema } from '../api-schemas'
2
+ import {
3
+ CreateOrganizationSchema,
4
+ ListOrganizationsQuerySchema,
5
+ UpdateOrganizationAuthConfigSchema,
6
+ UpdateOrganizationSchema
7
+ } from '../api-schemas'
3
8
 
4
9
  // ---------------------------------------------------------------------------
5
10
  // CreateOrganizationSchema
@@ -87,7 +92,7 @@ describe('CreateOrganizationSchema', () => {
87
92
  // UpdateOrganizationSchema
88
93
  // ---------------------------------------------------------------------------
89
94
 
90
- describe('UpdateOrganizationSchema', () => {
95
+ describe('UpdateOrganizationSchema', () => {
91
96
  it('rejects an empty object (at least one field required)', () => {
92
97
  // UpdateOrganizationSchema is CreateOrganizationSchema.partial().strict().refine(...)
93
98
  // The .refine() enforces the documented "at least one field" convention
@@ -135,7 +140,38 @@ describe('UpdateOrganizationSchema', () => {
135
140
  it('rejects unknown top-level fields (.strict() mode)', () => {
136
141
  expect(UpdateOrganizationSchema.safeParse({ name: 'Valid Name', unknownField: 'bad' }).success).toBe(false)
137
142
  })
138
- })
143
+ })
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // UpdateOrganizationAuthConfigSchema
147
+ // ---------------------------------------------------------------------------
148
+
149
+ describe('UpdateOrganizationAuthConfigSchema', () => {
150
+ it('accepts an HTTPS app origin', () => {
151
+ expect(UpdateOrganizationAuthConfigSchema.safeParse({ appOrigin: 'https://app.example.com' }).success).toBe(true)
152
+ })
153
+
154
+ it('rejects app origins with paths, query strings, or hashes', () => {
155
+ expect(UpdateOrganizationAuthConfigSchema.safeParse({ appOrigin: 'https://app.example.com/login' }).success).toBe(
156
+ false
157
+ )
158
+ expect(UpdateOrganizationAuthConfigSchema.safeParse({ appOrigin: 'https://app.example.com?x=1' }).success).toBe(
159
+ false
160
+ )
161
+ expect(UpdateOrganizationAuthConfigSchema.safeParse({ appOrigin: 'https://app.example.com#hash' }).success).toBe(
162
+ false
163
+ )
164
+ })
165
+
166
+ it('rejects unknown config fields', () => {
167
+ expect(
168
+ UpdateOrganizationAuthConfigSchema.safeParse({
169
+ appOrigin: 'https://app.example.com',
170
+ other: true
171
+ }).success
172
+ ).toBe(false)
173
+ })
174
+ })
139
175
 
140
176
  // ---------------------------------------------------------------------------
141
177
  // ListOrganizationsQuerySchema
@@ -101,11 +101,38 @@ export const CreateOrganizationSchema = z
101
101
  * - At least one field required (matches documented Update schema convention,
102
102
  * see .claude/rules/core-package.md → "api-schemas.ts Pattern")
103
103
  */
104
- export const UpdateOrganizationSchema = CreateOrganizationSchema.partial()
105
- .strict()
106
- .refine((data) => Object.keys(data).length > 0, {
107
- message: 'At least one field (name, domainData, or metadata) must be provided'
108
- })
104
+ export const UpdateOrganizationSchema = CreateOrganizationSchema.partial()
105
+ .strict()
106
+ .refine((data) => Object.keys(data).length > 0, {
107
+ message: 'At least one field (name, domainData, or metadata) must be provided'
108
+ })
109
+
110
+ /**
111
+ * Update organization auth config
112
+ * PATCH /organizations/:id/config/auth
113
+ *
114
+ * Security:
115
+ * - Narrow config surface: only appOrigin is accepted
116
+ * - App origin is an origin URL, not a route URL
117
+ * - Strict mode prevents accidental writes to unrelated config keys
118
+ */
119
+ export const UpdateOrganizationAuthConfigSchema = z
120
+ .object({
121
+ appOrigin: z
122
+ .string()
123
+ .trim()
124
+ .url('App origin must be a valid URL')
125
+ .max(2048, 'App origin must be at most 2048 characters')
126
+ .refine((value) => {
127
+ try {
128
+ const url = new URL(value)
129
+ return url.pathname === '/' && url.search === '' && url.hash === ''
130
+ } catch {
131
+ return false
132
+ }
133
+ }, 'App origin must not include a path, query string, or hash')
134
+ })
135
+ .strict()
109
136
 
110
137
  // ============================================================================
111
138
  // Query Parameters
@@ -131,6 +158,7 @@ export const ListOrganizationsQuerySchema = z.object({
131
158
 
132
159
  // Export inferred types for use in route handlers
133
160
  export type CreateOrganizationInput = z.infer<typeof CreateOrganizationSchema>
134
- export type UpdateOrganizationInput = z.infer<typeof UpdateOrganizationSchema>
135
- export type ListOrganizationsQuery = z.infer<typeof ListOrganizationsQuerySchema>
136
- export type OrganizationIdParam = z.infer<typeof OrganizationIdParamSchema>
161
+ export type UpdateOrganizationInput = z.infer<typeof UpdateOrganizationSchema>
162
+ export type UpdateOrganizationAuthConfigInput = z.infer<typeof UpdateOrganizationAuthConfigSchema>
163
+ export type ListOrganizationsQuery = z.infer<typeof ListOrganizationsQuerySchema>
164
+ export type OrganizationIdParam = z.infer<typeof OrganizationIdParamSchema>
@@ -8,16 +8,18 @@ export type { Organization, OrganizationSummary } from './organization'
8
8
 
9
9
  // Validation schemas
10
10
  export {
11
- CreateOrganizationSchema,
12
- UpdateOrganizationSchema,
13
- ListOrganizationsQuerySchema,
14
- OrganizationIdParamSchema,
15
- OrganizationNameSchema,
16
- OrganizationDomainSchema,
17
- type CreateOrganizationInput,
18
- type UpdateOrganizationInput,
19
- type ListOrganizationsQuery,
20
- type OrganizationIdParam
21
- } from './api-schemas'
11
+ CreateOrganizationSchema,
12
+ UpdateOrganizationSchema,
13
+ UpdateOrganizationAuthConfigSchema,
14
+ ListOrganizationsQuerySchema,
15
+ OrganizationIdParamSchema,
16
+ OrganizationNameSchema,
17
+ OrganizationDomainSchema,
18
+ type CreateOrganizationInput,
19
+ type UpdateOrganizationInput,
20
+ type UpdateOrganizationAuthConfigInput,
21
+ type ListOrganizationsQuery,
22
+ type OrganizationIdParam
23
+ } from './api-schemas'
22
24
 
23
25
  // Note: WorkOS types and transforms available via @repo/core/server
@@ -170,6 +170,40 @@ describe('createBuildPlanSnapshotFromTemplate', () => {
170
170
  ]
171
171
  })
172
172
  })
173
+
174
+ it('normalizes stage-key dependencies to step ids for snapshots', () => {
175
+ const snapshot = createBuildPlanSnapshotFromTemplate({
176
+ id: 'stage-key-dependency',
177
+ label: 'Stage Key Dependency',
178
+ steps: [
179
+ {
180
+ id: 'source-companies',
181
+ label: 'Companies found',
182
+ primaryEntity: 'company',
183
+ outputs: ['company'],
184
+ stageKey: 'populated',
185
+ dependencyMode: 'per-record-eligibility',
186
+ actionKey: 'lead-gen.company.source',
187
+ defaultBatchSize: 100,
188
+ maxBatchSize: 250
189
+ },
190
+ {
191
+ id: 'qualify-companies',
192
+ label: 'Companies qualified',
193
+ primaryEntity: 'company',
194
+ outputs: ['company'],
195
+ stageKey: 'qualified',
196
+ dependsOn: ['populated'],
197
+ dependencyMode: 'per-record-eligibility',
198
+ actionKey: 'lead-gen.company.qualify',
199
+ defaultBatchSize: 50,
200
+ maxBatchSize: 100
201
+ }
202
+ ]
203
+ })
204
+
205
+ expect(snapshot.steps[1]?.dependsOn).toEqual(['source-companies'])
206
+ })
173
207
  })
174
208
 
175
209
  describe('createBuildPlanSnapshotFromTemplateId', () => {
@@ -30,6 +30,9 @@ export function isProspectingBuildTemplateId(
30
30
  }
31
31
 
32
32
  export function createBuildPlanSnapshotFromTemplate(template: ProspectingBuildTemplate): BuildPlanSnapshot {
33
+ const stepIdByStageKey = new Map(template.steps.map((step) => [step.stageKey, step.id]))
34
+ const stepIds = new Set(template.steps.map((step) => step.id))
35
+
33
36
  return {
34
37
  templateId: template.id,
35
38
  templateLabel: template.label,
@@ -50,7 +53,11 @@ export function createBuildPlanSnapshotFromTemplate(template: ProspectingBuildTe
50
53
 
51
54
  if (step.description) snapshotStep.description = step.description
52
55
  if (step.recordEntity) snapshotStep.recordEntity = step.recordEntity
53
- if (step.dependsOn?.length) snapshotStep.dependsOn = [...step.dependsOn]
56
+ if (step.dependsOn?.length) {
57
+ snapshotStep.dependsOn = step.dependsOn.map((dependency) =>
58
+ stepIds.has(dependency) ? dependency : (stepIdByStageKey.get(dependency) ?? dependency)
59
+ )
60
+ }
54
61
  if (step.credentialRequirements?.length) {
55
62
  snapshotStep.credentialRequirements = step.credentialRequirements.map((requirement: CredentialRequirement) => ({
56
63
  ...requirement