@elevasis/core 0.17.0 → 0.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.
Files changed (44) hide show
  1. package/dist/index.d.ts +82 -1
  2. package/dist/index.js +291 -171
  3. package/dist/knowledge/index.d.ts +43 -0
  4. package/dist/organization-model/index.d.ts +82 -1
  5. package/dist/organization-model/index.js +291 -171
  6. package/dist/test-utils/index.d.ts +41 -12
  7. package/dist/test-utils/index.js +291 -171
  8. package/package.json +2 -1
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +78 -65
  10. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -0
  11. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -128
  12. package/src/business/acquisition/api-schemas.test.ts +100 -2
  13. package/src/business/acquisition/api-schemas.ts +81 -43
  14. package/src/business/acquisition/build-templates.test.ts +212 -0
  15. package/src/business/acquisition/types.ts +21 -38
  16. package/src/execution/engine/index.ts +436 -434
  17. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -0
  18. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -0
  19. package/src/execution/engine/tools/lead-service-types.ts +51 -9
  20. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -6
  21. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -5
  22. package/src/execution/engine/tools/platform/acquisition/types.ts +20 -9
  23. package/src/execution/engine/tools/registry.ts +700 -698
  24. package/src/execution/engine/tools/tool-maps.ts +10 -0
  25. package/src/execution/external/__tests__/api-schemas.test.ts +127 -0
  26. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -6
  27. package/src/integrations/oauth/provider-registry.ts +74 -61
  28. package/src/integrations/oauth/server/credentials.ts +43 -39
  29. package/src/knowledge/__tests__/queries.test.ts +89 -0
  30. package/src/organization-model/__tests__/icons.test.ts +61 -0
  31. package/src/organization-model/__tests__/knowledge.test.ts +118 -1
  32. package/src/organization-model/__tests__/prospecting-ssot.test.ts +94 -0
  33. package/src/organization-model/defaults.ts +8 -0
  34. package/src/organization-model/domains/knowledge.ts +9 -0
  35. package/src/organization-model/domains/prospecting.ts +272 -226
  36. package/src/organization-model/domains/sales.ts +32 -25
  37. package/src/organization-model/icons.ts +3 -0
  38. package/src/organization-model/types.ts +9 -1
  39. package/src/platform/constants/versions.ts +1 -1
  40. package/src/platform/utils/__tests__/validation.test.ts +1084 -1083
  41. package/src/platform/utils/validation.ts +425 -425
  42. package/src/reference/_generated/contracts.md +78 -65
  43. package/src/server.ts +6 -0
  44. package/src/supabase/database.types.ts +6 -12
@@ -195,6 +195,18 @@ export type OrgKnowledgeNode = z.infer<typeof OrgKnowledgeNodeSchema>
195
195
  export type OrgKnowledgeKind = z.infer<typeof OrgKnowledgeKindSchema>
196
196
  ```
197
197
 
198
+ ### `KnowledgeSkillBinding`
199
+
200
+ ```typescript
201
+ export type KnowledgeSkillBinding = z.infer<typeof KnowledgeSkillBindingSchema>
202
+ ```
203
+
204
+ ### `KnowledgeDomainBinding`
205
+
206
+ ```typescript
207
+ export type KnowledgeDomainBinding = z.infer<typeof KnowledgeDomainBindingSchema>
208
+ ```
209
+
198
210
  ### `OrganizationModelIconToken`
199
211
 
200
212
  ```typescript
@@ -866,7 +878,9 @@ export interface AcqCompany {
866
878
  category: string | null
867
879
  categoryPain: string | null
868
880
  segment: string | null
869
- pipelineStatus: CompanyPipelineStatus | null
881
+ processingState: CompanyProcessingState | null
882
+ /** @deprecated Use `processingState`. This legacy DB column has been removed; responses return null. */
883
+ pipelineStatus?: LegacyPipelineStatus | null
870
884
  enrichmentData: CompanyEnrichmentData | null
871
885
  source: string | null
872
886
  batchId: string | null
@@ -906,7 +920,9 @@ export interface AcqContact {
906
920
  openingLine: string | null
907
921
  source: string | null
908
922
  sourceId: string | null
909
- pipelineStatus: ContactPipelineStatus | null
923
+ processingState: ContactProcessingState | null
924
+ /** @deprecated Use `processingState`. This legacy DB column has been removed; responses return null. */
925
+ pipelineStatus?: LegacyPipelineStatus | null
910
926
  enrichmentData: ContactEnrichmentData | null
911
927
  /** Attio Person record ID - set when contact responds and is added to CRM */
912
928
  attioPersonId: string | null
@@ -969,7 +985,7 @@ export interface DealContact {
969
985
  title: string | null
970
986
  headline: string | null
971
987
  linkedin_url: string | null
972
- pipeline_status: Record<string, unknown> | null
988
+ processing_state: Record<string, unknown> | null
973
989
  enrichment_data: Record<string, unknown> | null
974
990
  company: {
975
991
  id: string
@@ -1242,7 +1258,7 @@ export const DealContactSummarySchema = z.object({
1242
1258
  title: z.string().nullable(),
1243
1259
  headline: z.string().nullable(),
1244
1260
  linkedin_url: z.string().nullable(),
1245
- pipeline_status: z.record(z.string(), z.unknown()).nullable(),
1261
+ processing_state: ProcessingStateSchema.nullable(),
1246
1262
  enrichment_data: z.record(z.string(), z.unknown()).nullable(),
1247
1263
  company: z
1248
1264
  .object({
@@ -1570,51 +1586,6 @@ export interface WebPost {
1570
1586
  }
1571
1587
  ```
1572
1588
 
1573
- ### `CompanyPipelineStatus`
1574
-
1575
- ```typescript
1576
- /**
1577
- * Tracks pipeline status for a company across all processing stages.
1578
- */
1579
- export interface CompanyPipelineStatus {
1580
- acquired: boolean
1581
- enrichment: {
1582
- [source: string]: {
1583
- status: 'pending' | 'complete' | 'failed' | 'skipped'
1584
- completedAt?: string
1585
- error?: string
1586
- }
1587
- }
1588
- }
1589
- ```
1590
-
1591
- ### `ContactPipelineStatus`
1592
-
1593
- ```typescript
1594
- /**
1595
- * Tracks pipeline status for a contact across all processing stages.
1596
- */
1597
- export interface ContactPipelineStatus {
1598
- enrichment: {
1599
- [source: string]: {
1600
- status: 'pending' | 'complete' | 'failed' | 'skipped'
1601
- completedAt?: string
1602
- error?: string
1603
- }
1604
- }
1605
- personalization: {
1606
- status: 'pending' | 'complete' | 'failed' | 'skipped'
1607
- completedAt?: string
1608
- }
1609
- outreach: {
1610
- status: 'pending' | 'sent' | 'replied' | 'bounced' | 'opted-out'
1611
- sentAt?: string
1612
- channel?: string
1613
- campaignId?: string
1614
- }
1615
- }
1616
- ```
1617
-
1618
1589
  ### `CompanyEnrichmentData`
1619
1590
 
1620
1591
  ```typescript
@@ -1730,7 +1701,9 @@ export interface AcqCompany {
1730
1701
  category: string | null
1731
1702
  categoryPain: string | null
1732
1703
  segment: string | null
1733
- pipelineStatus: CompanyPipelineStatus | null
1704
+ processingState: CompanyProcessingState | null
1705
+ /** @deprecated Use `processingState`. This legacy DB column has been removed; responses return null. */
1706
+ pipelineStatus?: LegacyPipelineStatus | null
1734
1707
  enrichmentData: CompanyEnrichmentData | null
1735
1708
  source: string | null
1736
1709
  batchId: string | null
@@ -1770,7 +1743,9 @@ export interface AcqContact {
1770
1743
  openingLine: string | null
1771
1744
  source: string | null
1772
1745
  sourceId: string | null
1773
- pipelineStatus: ContactPipelineStatus | null
1746
+ processingState: ContactProcessingState | null
1747
+ /** @deprecated Use `processingState`. This legacy DB column has been removed; responses return null. */
1748
+ pipelineStatus?: LegacyPipelineStatus | null
1774
1749
  enrichmentData: ContactEnrichmentData | null
1775
1750
  /** Attio Person record ID - set when contact responds and is added to CRM */
1776
1751
  attioPersonId: string | null
@@ -2081,6 +2056,7 @@ export const ListCompaniesQuerySchema = z
2081
2056
  website: z.string().trim().min(1).max(2048).optional(),
2082
2057
  segment: z.string().trim().min(1).max(255).optional(),
2083
2058
  category: z.string().trim().min(1).max(255).optional(),
2059
+ pipelineStatus: z.unknown().optional(),
2084
2060
  batchId: z.string().trim().min(1).max(255).optional(),
2085
2061
  status: AcqCompanyStatusSchema.optional(),
2086
2062
  includeAll: QueryBooleanSchema.optional(),
@@ -2122,6 +2098,7 @@ export const CreateCompanyRequestSchema = z
2122
2098
  category: z.string().trim().min(1).max(255).optional(),
2123
2099
  source: z.string().trim().min(1).max(255).optional(),
2124
2100
  batchId: z.string().trim().min(1).max(255).optional(),
2101
+ pipelineStatus: z.unknown().optional(),
2125
2102
  verticalResearch: z.string().trim().min(1).max(5000).optional()
2126
2103
  })
2127
2104
  .strict()
@@ -2142,7 +2119,8 @@ export const UpdateCompanyRequestSchema = z
2142
2119
  locationState: z.string().trim().min(1).max(255).optional(),
2143
2120
  category: z.string().trim().min(1).max(255).optional(),
2144
2121
  segment: z.string().trim().min(1).max(255).optional(),
2145
- pipelineStatus: z.record(z.string(), z.unknown()).optional(),
2122
+ processingState: CompanyProcessingStateSchema.optional(),
2123
+ pipelineStatus: z.unknown().optional(),
2146
2124
  enrichmentData: z.record(z.string(), z.unknown()).optional(),
2147
2125
  source: z.string().trim().min(1).max(255).optional(),
2148
2126
  batchId: z.string().trim().min(1).max(255).optional(),
@@ -2162,6 +2140,7 @@ export const UpdateCompanyRequestSchema = z
2162
2140
  data.locationState !== undefined ||
2163
2141
  data.category !== undefined ||
2164
2142
  data.segment !== undefined ||
2143
+ data.processingState !== undefined ||
2165
2144
  data.pipelineStatus !== undefined ||
2166
2145
  data.enrichmentData !== undefined ||
2167
2146
  data.source !== undefined ||
@@ -2187,7 +2166,8 @@ export const CreateContactRequestSchema = z
2187
2166
  title: z.string().trim().min(1).max(255).optional(),
2188
2167
  source: z.string().trim().min(1).max(255).optional(),
2189
2168
  sourceId: z.string().trim().min(1).max(255).optional(),
2190
- batchId: z.string().trim().min(1).max(255).optional()
2169
+ batchId: z.string().trim().min(1).max(255).optional(),
2170
+ pipelineStatus: z.unknown().optional()
2191
2171
  })
2192
2172
  .strict()
2193
2173
  ```
@@ -2206,7 +2186,8 @@ export const UpdateContactRequestSchema = z
2206
2186
  headline: z.string().trim().min(1).max(5000).optional(),
2207
2187
  filterReason: z.string().trim().min(1).max(5000).optional(),
2208
2188
  openingLine: z.string().trim().min(1).max(5000).optional(),
2209
- pipelineStatus: z.record(z.string(), z.unknown()).optional(),
2189
+ processingState: ContactProcessingStateSchema.optional(),
2190
+ pipelineStatus: z.unknown().optional(),
2210
2191
  enrichmentData: z.record(z.string(), z.unknown()).optional(),
2211
2192
  status: AcqContactStatusSchema.optional()
2212
2193
  })
@@ -2222,6 +2203,7 @@ export const UpdateContactRequestSchema = z
2222
2203
  data.headline !== undefined ||
2223
2204
  data.filterReason !== undefined ||
2224
2205
  data.openingLine !== undefined ||
2206
+ data.processingState !== undefined ||
2225
2207
  data.pipelineStatus !== undefined ||
2226
2208
  data.enrichmentData !== undefined ||
2227
2209
  data.status !== undefined,
@@ -2248,7 +2230,8 @@ export const AcqCompanyResponseSchema = z.object({
2248
2230
  category: z.string().nullable(),
2249
2231
  categoryPain: z.string().nullable(),
2250
2232
  segment: z.string().nullable(),
2251
- pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
2233
+ processingState: CompanyProcessingStateSchema.nullable(),
2234
+ pipelineStatus: z.unknown().nullable().optional(),
2252
2235
  enrichmentData: z.record(z.string(), z.unknown()).nullable(),
2253
2236
  source: z.string().nullable(),
2254
2237
  batchId: z.string().nullable(),
@@ -2314,7 +2297,8 @@ export const AcqContactResponseSchema = z.object({
2314
2297
  openingLine: z.string().nullable(),
2315
2298
  source: z.string().nullable(),
2316
2299
  sourceId: z.string().nullable(),
2317
- pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
2300
+ processingState: ContactProcessingStateSchema.nullable(),
2301
+ pipelineStatus: z.unknown().nullable().optional(),
2318
2302
  enrichmentData: z.record(z.string(), z.unknown()).nullable(),
2319
2303
  attioPersonId: z.string().nullable(),
2320
2304
  batchId: z.string().nullable(),
@@ -2480,6 +2464,7 @@ export const AcqListCompanyResponseSchema = z.object({
2480
2464
 
2481
2465
  ```typescript
2482
2466
  export const AcqCompanySchemas = {
2467
+ CompanyProcessingState: CompanyProcessingStateSchema,
2483
2468
  CompanyIdParams: CompanyIdParamsSchema,
2484
2469
  ListCompaniesQuery: ListCompaniesQuerySchema,
2485
2470
  CreateCompanyRequest: CreateCompanyRequestSchema,
@@ -2494,6 +2479,7 @@ export const AcqCompanySchemas = {
2494
2479
 
2495
2480
  ```typescript
2496
2481
  export const AcqContactSchemas = {
2482
+ ContactProcessingState: ContactProcessingStateSchema,
2497
2483
  ContactIdParams: ContactIdParamsSchema,
2498
2484
  ListContactsQuery: ListContactsQuerySchema,
2499
2485
  CreateContactRequest: CreateContactRequestSchema,
@@ -2519,7 +2505,10 @@ export const AcqListSchemas = {
2519
2505
  BuildPlanSnapshot: BuildPlanSnapshotSchema,
2520
2506
  BuildPlanSnapshotStep: BuildPlanSnapshotStepSchema,
2521
2507
  AcqListMetadata: AcqListMetadataSchema,
2508
+ LeadGenStageKey: LeadGenStageKeySchema,
2509
+ LeadGenCapabilityKey: LeadGenCapabilityKeySchema,
2522
2510
  ProcessingStageStatus: ProcessingStageStatusSchema,
2511
+ ProcessingState: ProcessingStateSchema,
2523
2512
  ListStageCounts: ListStageCountsSchema,
2524
2513
  ListTelemetry: ListTelemetrySchema,
2525
2514
 
@@ -2812,6 +2801,8 @@ export interface CreateCompanyParams {
2812
2801
  source?: string
2813
2802
  batchId?: string
2814
2803
  verticalResearch?: string
2804
+ /** @deprecated Use processingState. Accepted as a no-op compatibility bridge for external tenants. */
2805
+ pipelineStatus?: unknown
2815
2806
  }
2816
2807
  ```
2817
2808
 
@@ -2829,7 +2820,9 @@ export interface UpdateCompanyParams {
2829
2820
  locationState?: string
2830
2821
  category?: string
2831
2822
  segment?: string
2832
- pipelineStatus?: Record<string, unknown>
2823
+ processingState?: ProcessingState
2824
+ /** @deprecated Use processingState. Accepted as a no-op compatibility bridge for external tenants. */
2825
+ pipelineStatus?: unknown
2833
2826
  enrichmentData?: Record<string, unknown>
2834
2827
  source?: string
2835
2828
  batchId?: string
@@ -2837,7 +2830,7 @@ export interface UpdateCompanyParams {
2837
2830
  verticalResearch?: string | null
2838
2831
  /** Track A: flat qualification score column (null until a scoring rubric is defined) */
2839
2832
  qualificationScore?: number | null
2840
- /** Track A: flat qualification signals jsonb — mirrors the former pipeline_status.qualification shape */
2833
+ /** Track A: flat qualification signals jsonb */
2841
2834
  qualificationSignals?: Record<string, unknown> | null
2842
2835
  /** Track A: key identifying the rubric used for qualification */
2843
2836
  qualificationRubricKey?: string | null
@@ -2860,13 +2853,15 @@ export interface CompanyFilters {
2860
2853
  website?: string
2861
2854
  segment?: string
2862
2855
  category?: string
2863
- pipelineStatus?: Record<string, unknown>
2864
- /** Exclude companies whose pipeline_status contains this value (PostgREST NOT contains) */
2865
- pipelineStatusNot?: Record<string, unknown>
2856
+ processingState?: ProcessingState
2857
+ /** @deprecated Use processingState. Accepted as a no-op compatibility bridge for external tenants. */
2858
+ pipelineStatus?: unknown
2859
+ /** Exclude companies whose processing state contains this value (PostgREST NOT contains) */
2860
+ processingStateNot?: ProcessingState
2866
2861
  batchId?: string
2867
2862
  status?: 'active' | 'invalid'
2868
2863
  includeAll?: boolean
2869
- excludeColumns?: Array<'enrichmentData' | 'pipelineStatus'>
2864
+ excludeColumns?: Array<'enrichmentData' | 'processingState'>
2870
2865
  limit?: number
2871
2866
  }
2872
2867
  ```
@@ -2885,6 +2880,8 @@ export interface CreateContactParams {
2885
2880
  source?: string
2886
2881
  sourceId?: string
2887
2882
  batchId?: string
2883
+ /** @deprecated Use processingState. Accepted as a no-op compatibility bridge for external tenants. */
2884
+ pipelineStatus?: unknown
2888
2885
  }
2889
2886
  ```
2890
2887
 
@@ -2901,7 +2898,9 @@ export interface UpdateContactParams {
2901
2898
  headline?: string
2902
2899
  filterReason?: string
2903
2900
  openingLine?: string
2904
- pipelineStatus?: Record<string, unknown>
2901
+ processingState?: ProcessingState
2902
+ /** @deprecated Use processingState. Accepted as a no-op compatibility bridge for external tenants. */
2903
+ pipelineStatus?: unknown
2905
2904
  enrichmentData?: Record<string, unknown>
2906
2905
  status?: 'active' | 'invalid'
2907
2906
  }
@@ -2920,7 +2919,9 @@ export interface ContactFilters {
2920
2919
  listId?: string // Filter to contacts in a specific list (via acq_list_members)
2921
2920
  search?: string
2922
2921
  openingLineIsNull?: boolean // Filter to contacts without personalization
2923
- pipelineStatus?: Record<string, unknown>
2922
+ processingState?: ProcessingState
2923
+ /** @deprecated Use processingState. Accepted as a no-op compatibility bridge for external tenants. */
2924
+ pipelineStatus?: unknown
2924
2925
  batchId?: string
2925
2926
  contactStatus?: 'active' | 'invalid' // Filter by contact status (soft-delete flag)
2926
2927
  }
@@ -3119,7 +3120,7 @@ export interface BulkImportCompanyEntry {
3119
3120
  category?: string
3120
3121
  source?: string
3121
3122
  enrichmentData?: Record<string, unknown>
3122
- pipelineStatus?: Record<string, unknown>
3123
+ processingState?: ProcessingState
3123
3124
  }
3124
3125
  ```
3125
3126
 
@@ -3300,6 +3301,14 @@ export type ListToolMap = {
3300
3301
  params: Omit<UpdateContactStageParams, 'organizationId'>
3301
3302
  result: void
3302
3303
  }
3304
+ listPendingCompanyIds: {
3305
+ params: Omit<ListPendingCompanyIdsParams, 'organizationId'>
3306
+ result: string[]
3307
+ }
3308
+ listPendingContactIds: {
3309
+ params: Omit<ListPendingContactIdsParams, 'organizationId'>
3310
+ result: string[]
3311
+ }
3303
3312
  }
3304
3313
  ```
3305
3314
 
@@ -3335,6 +3344,10 @@ export const OrgKnowledgeNodeSchema = z.object({
3335
3344
  * Each link emits a `governs` edge: knowledge-node -> target node.
3336
3345
  */
3337
3346
  links: z.array(KnowledgeLinkSchema).default([]),
3347
+ /** Operator skill or command bindings relevant to this node. */
3348
+ skills: z.array(KnowledgeSkillBindingSchema).optional(),
3349
+ /** Domain key used to derive fast graph->skill registries. */
3350
+ domain: KnowledgeDomainBindingSchema.optional(),
3338
3351
  /** Identifiers of the roles or members who own this knowledge node. */
3339
3352
  ownerIds: z.array(ModelIdSchema).default([]),
3340
3353
  /** ISO date string (YYYY-MM-DD or full ISO 8601) of last meaningful update. */
@@ -0,0 +1,194 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { CreateOrganizationSchema, ListOrganizationsQuerySchema, UpdateOrganizationSchema } from '../api-schemas'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // CreateOrganizationSchema
6
+ // ---------------------------------------------------------------------------
7
+
8
+ describe('CreateOrganizationSchema', () => {
9
+ it('accepts a minimal valid payload (name only)', () => {
10
+ expect(CreateOrganizationSchema.safeParse({ name: 'Acme Corp' }).success).toBe(true)
11
+ })
12
+
13
+ it('accepts name at minimum length boundary (2 chars)', () => {
14
+ expect(CreateOrganizationSchema.safeParse({ name: 'AB' }).success).toBe(true)
15
+ })
16
+
17
+ it('accepts name at maximum length boundary (100 chars)', () => {
18
+ expect(CreateOrganizationSchema.safeParse({ name: 'A'.repeat(100) }).success).toBe(true)
19
+ })
20
+
21
+ it('rejects name shorter than 2 characters', () => {
22
+ expect(CreateOrganizationSchema.safeParse({ name: 'A' }).success).toBe(false)
23
+ expect(CreateOrganizationSchema.safeParse({ name: '' }).success).toBe(false)
24
+ })
25
+
26
+ it('rejects name longer than 100 characters', () => {
27
+ expect(CreateOrganizationSchema.safeParse({ name: 'A'.repeat(101) }).success).toBe(false)
28
+ })
29
+
30
+ it('accepts names with letters, numbers, spaces, hyphens, and underscores', () => {
31
+ expect(CreateOrganizationSchema.safeParse({ name: 'My Org-123_Test' }).success).toBe(true)
32
+ })
33
+
34
+ it('rejects names with disallowed characters (special symbols)', () => {
35
+ expect(CreateOrganizationSchema.safeParse({ name: 'Acme@Corp' }).success).toBe(false)
36
+ expect(CreateOrganizationSchema.safeParse({ name: 'Acme.Corp' }).success).toBe(false)
37
+ expect(CreateOrganizationSchema.safeParse({ name: 'Acme/Corp' }).success).toBe(false)
38
+ expect(CreateOrganizationSchema.safeParse({ name: 'Acme+Corp' }).success).toBe(false)
39
+ })
40
+
41
+ it('accepts optional domainData array with valid entries', () => {
42
+ const result = CreateOrganizationSchema.safeParse({
43
+ name: 'Acme Corp',
44
+ domainData: [{ domain: 'acme.com', state: 'verified' }]
45
+ })
46
+ expect(result.success).toBe(true)
47
+ })
48
+
49
+ it('accepts domainData up to 10 entries', () => {
50
+ const domains = Array.from({ length: 10 }, (_, i) => ({ domain: `domain${i}.com` }))
51
+ expect(CreateOrganizationSchema.safeParse({ name: 'Acme Corp', domainData: domains }).success).toBe(true)
52
+ })
53
+
54
+ it('rejects domainData with more than 10 entries', () => {
55
+ const domains = Array.from({ length: 11 }, (_, i) => ({ domain: `domain${i}.com` }))
56
+ expect(CreateOrganizationSchema.safeParse({ name: 'Acme Corp', domainData: domains }).success).toBe(false)
57
+ })
58
+
59
+ it('accepts optional metadata as a record under 10KB', () => {
60
+ const result = CreateOrganizationSchema.safeParse({
61
+ name: 'Acme Corp',
62
+ metadata: { plan: 'pro', region: 'us-west' }
63
+ })
64
+ expect(result.success).toBe(true)
65
+ })
66
+
67
+ it('rejects metadata exceeding 10KB (10240 bytes)', () => {
68
+ // Build a metadata object whose JSON representation exceeds 10240 bytes
69
+ const largeValue = 'x'.repeat(10241)
70
+ const result = CreateOrganizationSchema.safeParse({
71
+ name: 'Acme Corp',
72
+ metadata: { big: largeValue }
73
+ })
74
+ expect(result.success).toBe(false)
75
+ })
76
+
77
+ it('rejects unknown top-level fields (.strict() mode)', () => {
78
+ expect(CreateOrganizationSchema.safeParse({ name: 'Acme Corp', unknownField: 'value' }).success).toBe(false)
79
+ })
80
+
81
+ it('rejects missing name field', () => {
82
+ expect(CreateOrganizationSchema.safeParse({}).success).toBe(false)
83
+ })
84
+ })
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // UpdateOrganizationSchema
88
+ // ---------------------------------------------------------------------------
89
+
90
+ describe('UpdateOrganizationSchema', () => {
91
+ it('rejects an empty object (at least one field required)', () => {
92
+ // UpdateOrganizationSchema is CreateOrganizationSchema.partial().strict().refine(...)
93
+ // The .refine() enforces the documented "at least one field" convention
94
+ // (see .claude/rules/core-package.md → "api-schemas.ts Pattern")
95
+ const result = UpdateOrganizationSchema.safeParse({})
96
+ expect(result.success).toBe(false)
97
+ if (!result.success) {
98
+ expect(result.error.issues[0].message).toMatch(/at least one field/i)
99
+ }
100
+ })
101
+
102
+ it('accepts updating only name', () => {
103
+ expect(UpdateOrganizationSchema.safeParse({ name: 'New Name' }).success).toBe(true)
104
+ })
105
+
106
+ it('accepts updating only domainData', () => {
107
+ expect(UpdateOrganizationSchema.safeParse({ domainData: [{ domain: 'newdomain.com' }] }).success).toBe(true)
108
+ })
109
+
110
+ it('accepts updating only metadata', () => {
111
+ expect(UpdateOrganizationSchema.safeParse({ metadata: { key: 'value' } }).success).toBe(true)
112
+ })
113
+
114
+ it('applies same name validation as CreateOrganizationSchema when name is provided', () => {
115
+ // Too short
116
+ expect(UpdateOrganizationSchema.safeParse({ name: 'A' }).success).toBe(false)
117
+ // Too long
118
+ expect(UpdateOrganizationSchema.safeParse({ name: 'A'.repeat(101) }).success).toBe(false)
119
+ // Invalid charset
120
+ expect(UpdateOrganizationSchema.safeParse({ name: 'Bad@Name' }).success).toBe(false)
121
+ // Valid
122
+ expect(UpdateOrganizationSchema.safeParse({ name: 'Good Name-123' }).success).toBe(true)
123
+ })
124
+
125
+ it('applies same domainData max-10 validation when provided', () => {
126
+ const tooMany = Array.from({ length: 11 }, (_, i) => ({ domain: `d${i}.com` }))
127
+ expect(UpdateOrganizationSchema.safeParse({ domainData: tooMany }).success).toBe(false)
128
+ })
129
+
130
+ it('applies same metadata 10KB cap validation when provided', () => {
131
+ const largeValue = 'x'.repeat(10241)
132
+ expect(UpdateOrganizationSchema.safeParse({ metadata: { big: largeValue } }).success).toBe(false)
133
+ })
134
+
135
+ it('rejects unknown top-level fields (.strict() mode)', () => {
136
+ expect(UpdateOrganizationSchema.safeParse({ name: 'Valid Name', unknownField: 'bad' }).success).toBe(false)
137
+ })
138
+ })
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // ListOrganizationsQuerySchema
142
+ // ---------------------------------------------------------------------------
143
+
144
+ describe('ListOrganizationsQuerySchema', () => {
145
+ it('accepts an empty query and applies default limit of 20', () => {
146
+ const result = ListOrganizationsQuerySchema.safeParse({})
147
+ expect(result.success).toBe(true)
148
+ if (result.success) {
149
+ expect(result.data.limit).toBe(20)
150
+ }
151
+ })
152
+
153
+ it('coerces limit from string "50" to number 50', () => {
154
+ const result = ListOrganizationsQuerySchema.safeParse({ limit: '50' })
155
+ expect(result.success).toBe(true)
156
+ if (result.success) expect(result.data.limit).toBe(50)
157
+ })
158
+
159
+ it('accepts limit at upper boundary (100)', () => {
160
+ const result = ListOrganizationsQuerySchema.safeParse({ limit: '100' })
161
+ expect(result.success).toBe(true)
162
+ if (result.success) expect(result.data.limit).toBe(100)
163
+ })
164
+
165
+ it('accepts limit at lower boundary (1)', () => {
166
+ const result = ListOrganizationsQuerySchema.safeParse({ limit: '1' })
167
+ expect(result.success).toBe(true)
168
+ if (result.success) expect(result.data.limit).toBe(1)
169
+ })
170
+
171
+ it('rejects limit of 101 (above max 100)', () => {
172
+ expect(ListOrganizationsQuerySchema.safeParse({ limit: '101' }).success).toBe(false)
173
+ })
174
+
175
+ it('rejects limit of 0 (below min 1)', () => {
176
+ expect(ListOrganizationsQuerySchema.safeParse({ limit: '0' }).success).toBe(false)
177
+ })
178
+
179
+ it('accepts optional before cursor string', () => {
180
+ const result = ListOrganizationsQuerySchema.safeParse({ before: 'cursor_abc123' })
181
+ expect(result.success).toBe(true)
182
+ if (result.success) expect(result.data.before).toBe('cursor_abc123')
183
+ })
184
+
185
+ it('accepts optional after cursor string', () => {
186
+ const result = ListOrganizationsQuerySchema.safeParse({ after: 'cursor_xyz789' })
187
+ expect(result.success).toBe(true)
188
+ if (result.success) expect(result.data.after).toBe('cursor_xyz789')
189
+ })
190
+
191
+ it('accepts both before and after cursors together', () => {
192
+ expect(ListOrganizationsQuerySchema.safeParse({ before: 'a', after: 'b' }).success).toBe(true)
193
+ })
194
+ })