@elevasis/core 0.31.0 → 0.33.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/dist/auth/index.d.ts +60 -2
- package/dist/auth/index.js +7 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.js +69 -4
- package/dist/knowledge/index.d.ts +10 -2
- package/dist/organization-model/index.d.ts +15 -3
- package/dist/organization-model/index.js +69 -4
- package/dist/test-utils/index.d.ts +55 -2
- package/dist/test-utils/index.js +69 -4
- package/package.json +1 -1
- package/src/auth/__tests__/access-keys.test.ts +16 -0
- package/src/auth/__tests__/access-model.test.ts +31 -0
- package/src/auth/access-keys.ts +1 -0
- package/src/auth/multi-tenancy/permissions.ts +19 -13
- package/src/organization-model/__tests__/define-domain-record.test.ts +31 -34
- package/src/organization-model/__tests__/domains/passthrough-extensibility.test.ts +199 -0
- package/src/organization-model/domains/branding.ts +58 -16
- package/src/organization-model/domains/identity.ts +122 -94
- package/src/supabase/database.types.ts +45 -0
|
@@ -17,6 +17,12 @@ const organizationModel = {
|
|
|
17
17
|
label: 'CRM',
|
|
18
18
|
lifecycle: 'active',
|
|
19
19
|
order: 10
|
|
20
|
+
},
|
|
21
|
+
'lead-gen': {
|
|
22
|
+
id: 'lead-gen',
|
|
23
|
+
label: 'Lead Gen',
|
|
24
|
+
lifecycle: 'active',
|
|
25
|
+
order: 20
|
|
20
26
|
}
|
|
21
27
|
}
|
|
22
28
|
},
|
|
@@ -179,6 +185,31 @@ describe('checkAccess', () => {
|
|
|
179
185
|
})
|
|
180
186
|
})
|
|
181
187
|
|
|
188
|
+
it('allows Lead Gen manage only with the explicit OM System permission', () => {
|
|
189
|
+
expect(
|
|
190
|
+
checkAccess(
|
|
191
|
+
AccessKeys.leadGenManage,
|
|
192
|
+
context({
|
|
193
|
+
membership: {
|
|
194
|
+
id: 'membership-1',
|
|
195
|
+
organizationId: 'org-1',
|
|
196
|
+
effectivePermissions: ['sales.lead-gen.manage']
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
).toEqual({
|
|
201
|
+
allowed: true,
|
|
202
|
+
restrictedBy: null,
|
|
203
|
+
reason: 'allowed'
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
expect(checkAccess(AccessKeys.leadGenManage, context())).toEqual({
|
|
207
|
+
allowed: false,
|
|
208
|
+
restrictedBy: 'role-permission',
|
|
209
|
+
reason: 'role-permission-denied'
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
182
213
|
it('requires explicit role permissions for permission-only access keys', () => {
|
|
183
214
|
expect(
|
|
184
215
|
checkAccess(
|
package/src/auth/access-keys.ts
CHANGED
|
@@ -54,6 +54,7 @@ export const AccessKeys = {
|
|
|
54
54
|
secretsManage: { systemPath: 'permission.secrets', action: 'manage' },
|
|
55
55
|
operationsRead: { systemPath: 'permission.operations', action: DEFAULT_ACCESS_ACTION },
|
|
56
56
|
operationsManage: { systemPath: 'permission.operations', action: 'manage' },
|
|
57
|
+
leadGenManage: { systemPath: 'sales.lead-gen', action: 'manage' },
|
|
57
58
|
acquisitionManage: { systemPath: 'permission.acquisition', action: 'manage' },
|
|
58
59
|
projectsManage: { systemPath: 'permission.projects', action: 'manage' },
|
|
59
60
|
clientsManage: { systemPath: 'permission.clients', action: 'manage' },
|
|
@@ -32,12 +32,13 @@ export const PERMISSIONS = {
|
|
|
32
32
|
MEMBERS_MANAGE: 'members.manage',
|
|
33
33
|
ROLES_MANAGE: 'roles.manage',
|
|
34
34
|
SECRETS_MANAGE: 'secrets.manage',
|
|
35
|
-
OPERATIONS_READ: 'operations.read',
|
|
36
|
-
OPERATIONS_MANAGE: 'operations.manage',
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
OPERATIONS_READ: 'operations.read',
|
|
36
|
+
OPERATIONS_MANAGE: 'operations.manage',
|
|
37
|
+
SALES_LEAD_GEN_MANAGE: 'sales.lead-gen.manage',
|
|
38
|
+
ACQUISITION_MANAGE: 'acquisition.manage',
|
|
39
|
+
PROJECTS_MANAGE: 'projects.manage',
|
|
40
|
+
CLIENTS_MANAGE: 'clients.manage'
|
|
41
|
+
} as const
|
|
41
42
|
|
|
42
43
|
export type PermissionKey = (typeof PERMISSIONS)[keyof typeof PERMISSIONS]
|
|
43
44
|
|
|
@@ -89,13 +90,18 @@ export const PERMISSION_CATALOG: readonly PermissionDescriptor[] = [
|
|
|
89
90
|
description: 'View executions, sessions, schedules, and command queue',
|
|
90
91
|
isOrgGrantable: true
|
|
91
92
|
},
|
|
92
|
-
{
|
|
93
|
-
key: 'operations.manage',
|
|
94
|
-
description: 'Run and modify executions, sessions, schedules, queue',
|
|
95
|
-
isOrgGrantable: true
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
key: '
|
|
93
|
+
{
|
|
94
|
+
key: 'operations.manage',
|
|
95
|
+
description: 'Run and modify executions, sessions, schedules, queue',
|
|
96
|
+
isOrgGrantable: true
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: 'sales.lead-gen.manage',
|
|
100
|
+
description: 'Operate Lead Gen lists and list-builder workflows',
|
|
101
|
+
isOrgGrantable: true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: 'acquisition.manage',
|
|
99
105
|
description:
|
|
100
106
|
'Create, update, and delete acquisition records (acq_companies, acq_contacts, acq_deals, acq_lists*, acq_content*, acquisition storage files)',
|
|
101
107
|
isOrgGrantable: false
|
|
@@ -216,45 +216,42 @@ describe.each([
|
|
|
216
216
|
idField: 'id' as const,
|
|
217
217
|
invalidOverride: { kind: 'invalid-kind' }
|
|
218
218
|
}
|
|
219
|
-
])(
|
|
220
|
-
'
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
expect((result as Record<string, unknown>)[idField]).toBe((valid as Record<string, unknown>)[idField])
|
|
226
|
-
})
|
|
219
|
+
])('define$domain — $domain', ({ domain: _domain, defineSingle, defineMultiple, valid, idField, invalidOverride }) => {
|
|
220
|
+
it('defineX — validates and returns a parsed entry', () => {
|
|
221
|
+
const result = defineSingle(valid as never)
|
|
222
|
+
expect(result).toHaveProperty(idField)
|
|
223
|
+
expect((result as Record<string, unknown>)[idField]).toBe((valid as Record<string, unknown>)[idField])
|
|
224
|
+
})
|
|
227
225
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
226
|
+
it('defineX — throws ZodError on invalid input', () => {
|
|
227
|
+
const bad = { ...valid, ...invalidOverride } as never
|
|
228
|
+
expect(() => defineSingle(bad)).toThrow(z.ZodError)
|
|
229
|
+
})
|
|
232
230
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
it('defineXs — produces an id-keyed map', () => {
|
|
232
|
+
const result = defineMultiple([valid] as never[])
|
|
233
|
+
const id = (valid as Record<string, unknown>)[idField] as string
|
|
234
|
+
expect(result).toHaveProperty(id)
|
|
235
|
+
expect((result[id] as Record<string, unknown>)[idField]).toBe(id)
|
|
236
|
+
})
|
|
239
237
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
it('defineXs — throws ZodError on invalid input', () => {
|
|
239
|
+
const bad = { ...valid, ...invalidOverride } as never
|
|
240
|
+
expect(() => defineMultiple([bad] as never[])).toThrow(z.ZodError)
|
|
241
|
+
})
|
|
244
242
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
243
|
+
it('defineXs — returns empty map for empty array', () => {
|
|
244
|
+
const result = defineMultiple([] as never[])
|
|
245
|
+
expect(result).toEqual({})
|
|
246
|
+
})
|
|
249
247
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
)
|
|
248
|
+
it('defineXs — validates through schema (no bypass)', () => {
|
|
249
|
+
// Confirm that defineMultiple uses schema.parse, not a pass-through.
|
|
250
|
+
// Missing required fields should throw even if the object looks similar.
|
|
251
|
+
const missingRequired = { [idField]: (valid as Record<string, unknown>)[idField] } as never
|
|
252
|
+
expect(() => defineMultiple([missingRequired] as never[])).toThrow(z.ZodError)
|
|
253
|
+
})
|
|
254
|
+
})
|
|
258
255
|
|
|
259
256
|
// ---------------------------------------------------------------------------
|
|
260
257
|
// Topology — existing helpers (defineTopology, defineTopologyRelationship)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { IdentityDomainSchema } from '../../domains/identity'
|
|
3
|
+
import { resolveOrganizationModel } from '../../resolve'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Profile-singleton domains (`branding`, `identity`) use `.passthrough()` so that
|
|
7
|
+
* tenant-authored direct properties survive `resolveOrganizationModel` instead of
|
|
8
|
+
* being silently stripped by Zod's default `.strip()`. These tests lock that
|
|
9
|
+
* behavior in and verify the broadened typed branding fields round-trip.
|
|
10
|
+
*/
|
|
11
|
+
describe('profile-singleton passthrough extensibility', () => {
|
|
12
|
+
it('keeps an unknown direct property on branding through resolve', () => {
|
|
13
|
+
const model = resolveOrganizationModel({
|
|
14
|
+
branding: {
|
|
15
|
+
organizationName: 'Acme',
|
|
16
|
+
productName: 'AcmeOS',
|
|
17
|
+
shortName: 'Acme',
|
|
18
|
+
// Unknown tenant-authored direct property — must survive passthrough.
|
|
19
|
+
brandVibe: 'warm'
|
|
20
|
+
} as never
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect((model.branding as Record<string, unknown>).brandVibe).toBe('warm')
|
|
24
|
+
// Known fields still resolve normally.
|
|
25
|
+
expect(model.branding.organizationName).toBe('Acme')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps an unknown direct property on identity through resolve', () => {
|
|
29
|
+
const model = resolveOrganizationModel({
|
|
30
|
+
identity: {
|
|
31
|
+
// Unknown tenant-authored direct property — must survive passthrough.
|
|
32
|
+
founderNote: 'bootstrapped'
|
|
33
|
+
} as never
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect((model.identity as Record<string, unknown>).founderNote).toBe('bootstrapped')
|
|
37
|
+
// Known identity fields still resolve from defaults.
|
|
38
|
+
expect(model.identity.timeZone).toBe('UTC')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('parses and round-trips the broadened typed branding fields', () => {
|
|
42
|
+
const model = resolveOrganizationModel({
|
|
43
|
+
branding: {
|
|
44
|
+
organizationName: 'Acme',
|
|
45
|
+
productName: 'AcmeOS',
|
|
46
|
+
shortName: 'Acme',
|
|
47
|
+
voice: 'Direct and human — no jargon',
|
|
48
|
+
tagline: 'Automation that works while you sleep',
|
|
49
|
+
values: ['Transparency', 'Craftsmanship', 'Velocity'],
|
|
50
|
+
themePresetId: 'midnight'
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
expect(model.branding.voice).toBe('Direct and human — no jargon')
|
|
55
|
+
expect(model.branding.tagline).toBe('Automation that works while you sleep')
|
|
56
|
+
expect(model.branding.values).toEqual(['Transparency', 'Craftsmanship', 'Velocity'])
|
|
57
|
+
expect(model.branding.themePresetId).toBe('midnight')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('leaves the broadened branding fields undefined when not authored', () => {
|
|
61
|
+
const model = resolveOrganizationModel()
|
|
62
|
+
|
|
63
|
+
expect(model.branding.voice).toBeUndefined()
|
|
64
|
+
expect(model.branding.tagline).toBeUndefined()
|
|
65
|
+
expect(model.branding.values).toBeUndefined()
|
|
66
|
+
expect(model.branding.themePresetId).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Names → identity: the four display-name fields (organizationName, productName,
|
|
72
|
+
* shortName, description) are now the recommended placement on `identity`. They are
|
|
73
|
+
* optional so that legacy tenants can continue to rely on `branding.*` fallbacks
|
|
74
|
+
* without breaking. These tests lock the contract: fields round-trip, are truly
|
|
75
|
+
* optional (no default), and branding fields still validate unchanged.
|
|
76
|
+
*/
|
|
77
|
+
describe('identity display-name fields — recommended placement (Step 5)', () => {
|
|
78
|
+
it('accepts all four name fields on identity and round-trips them through resolveOrganizationModel', () => {
|
|
79
|
+
const model = resolveOrganizationModel({
|
|
80
|
+
identity: {
|
|
81
|
+
organizationName: 'Acme Corp',
|
|
82
|
+
productName: 'AcmeOS',
|
|
83
|
+
shortName: 'Acme',
|
|
84
|
+
description: 'AI orchestration for modern teams.'
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(model.identity.organizationName).toBe('Acme Corp')
|
|
89
|
+
expect(model.identity.productName).toBe('AcmeOS')
|
|
90
|
+
expect(model.identity.shortName).toBe('Acme')
|
|
91
|
+
expect(model.identity.description).toBe('AI orchestration for modern teams.')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('identity name fields are optional — a model with none set still resolves, and resolved values are undefined', () => {
|
|
95
|
+
const model = resolveOrganizationModel()
|
|
96
|
+
|
|
97
|
+
expect(model.identity.organizationName).toBeUndefined()
|
|
98
|
+
expect(model.identity.productName).toBeUndefined()
|
|
99
|
+
expect(model.identity.shortName).toBeUndefined()
|
|
100
|
+
expect(model.identity.description).toBeUndefined()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('identity name fields parse as undefined through IdentityDomainSchema directly (no defaults injected)', () => {
|
|
104
|
+
const result = IdentityDomainSchema.safeParse({})
|
|
105
|
+
expect(result.success).toBe(true)
|
|
106
|
+
if (result.success) {
|
|
107
|
+
expect(result.data.organizationName).toBeUndefined()
|
|
108
|
+
expect(result.data.productName).toBeUndefined()
|
|
109
|
+
expect(result.data.shortName).toBeUndefined()
|
|
110
|
+
expect(result.data.description).toBeUndefined()
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('identity.shortName rejects values exceeding 40 characters', () => {
|
|
115
|
+
expect(() =>
|
|
116
|
+
resolveOrganizationModel({
|
|
117
|
+
identity: { shortName: 'a'.repeat(41) } as never
|
|
118
|
+
})
|
|
119
|
+
).toThrow()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('identity.organizationName rejects empty string (LabelSchema min 1)', () => {
|
|
123
|
+
expect(() =>
|
|
124
|
+
resolveOrganizationModel({
|
|
125
|
+
identity: { organizationName: '' } as never
|
|
126
|
+
})
|
|
127
|
+
).toThrow()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('identity.description rejects empty string (DescriptionSchema min 1)', () => {
|
|
131
|
+
expect(() =>
|
|
132
|
+
resolveOrganizationModel({
|
|
133
|
+
identity: { description: '' } as never
|
|
134
|
+
})
|
|
135
|
+
).toThrow()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('identity name fields trim whitespace', () => {
|
|
139
|
+
const model = resolveOrganizationModel({
|
|
140
|
+
identity: {
|
|
141
|
+
organizationName: ' Acme Corp ',
|
|
142
|
+
shortName: ' Acme '
|
|
143
|
+
} as never
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(model.identity.organizationName).toBe('Acme Corp')
|
|
147
|
+
expect(model.identity.shortName).toBe('Acme')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('branding still validates with its name fields — @deprecated tags change nothing at runtime', () => {
|
|
151
|
+
const model = resolveOrganizationModel({
|
|
152
|
+
branding: {
|
|
153
|
+
organizationName: 'Legacy Org',
|
|
154
|
+
productName: 'LegacyOS',
|
|
155
|
+
shortName: 'LGO',
|
|
156
|
+
description: 'Legacy description for back-compat.'
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
expect(model.branding.organizationName).toBe('Legacy Org')
|
|
161
|
+
expect(model.branding.productName).toBe('LegacyOS')
|
|
162
|
+
expect(model.branding.shortName).toBe('LGO')
|
|
163
|
+
expect(model.branding.description).toBe('Legacy description for back-compat.')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The broadened branding fields are *typed*, not passthrough — their constraints
|
|
169
|
+
* are part of the published contract. These tests lock those bounds so a future
|
|
170
|
+
* edit can't silently widen/drop them, and confirm `.passthrough()` does NOT
|
|
171
|
+
* bypass validation for known fields (only unknown ones are let through).
|
|
172
|
+
*/
|
|
173
|
+
describe('broadened branding typed-field validation', () => {
|
|
174
|
+
it('rejects a voice longer than 280 characters', () => {
|
|
175
|
+
expect(() => resolveOrganizationModel({ branding: { voice: 'a'.repeat(281) } as never })).toThrow()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('rejects a tagline longer than 200 characters', () => {
|
|
179
|
+
expect(() => resolveOrganizationModel({ branding: { tagline: 'a'.repeat(201) } as never })).toThrow()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('rejects an empty-string entry in values (each entry must be non-empty)', () => {
|
|
183
|
+
expect(() => resolveOrganizationModel({ branding: { values: ['Valid', ''] } as never })).toThrow()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('rejects a themePresetId outside the 1–64 character bounds', () => {
|
|
187
|
+
expect(() => resolveOrganizationModel({ branding: { themePresetId: '' } as never })).toThrow()
|
|
188
|
+
expect(() => resolveOrganizationModel({ branding: { themePresetId: 'a'.repeat(65) } as never })).toThrow()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('trims whitespace on string fields', () => {
|
|
192
|
+
const model = resolveOrganizationModel({
|
|
193
|
+
branding: { voice: ' trimmed voice ', tagline: ' trimmed tagline ' } as never
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(model.branding.voice).toBe('trimmed voice')
|
|
197
|
+
expect(model.branding.tagline).toBe('trimmed tagline')
|
|
198
|
+
})
|
|
199
|
+
})
|
|
@@ -1,19 +1,61 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { DescriptionSchema, LabelSchema } from './shared'
|
|
3
|
-
|
|
4
|
-
export const OrganizationModelBrandingSchema = z
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { DescriptionSchema, LabelSchema } from './shared'
|
|
3
|
+
|
|
4
|
+
export const OrganizationModelBrandingSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Prefer `identity.organizationName`; branding retains it for back-compat.
|
|
8
|
+
* Legacy tenants that have not set `identity.organizationName` fall back to this field.
|
|
9
|
+
*/
|
|
10
|
+
organizationName: LabelSchema,
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Prefer `identity.productName`; branding retains it for back-compat.
|
|
13
|
+
* Legacy tenants that have not set `identity.productName` fall back to this field.
|
|
14
|
+
*/
|
|
15
|
+
productName: LabelSchema,
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated Prefer `identity.shortName`; branding retains it for back-compat.
|
|
18
|
+
* Legacy tenants that have not set `identity.shortName` fall back to this field.
|
|
19
|
+
*/
|
|
20
|
+
shortName: z.string().trim().min(1).max(40),
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Prefer `identity.description`; branding retains it for back-compat.
|
|
23
|
+
* Legacy tenants that have not set `identity.description` fall back to this field.
|
|
24
|
+
*/
|
|
25
|
+
description: DescriptionSchema.optional(),
|
|
26
|
+
logos: z
|
|
27
|
+
.object({
|
|
28
|
+
light: z.string().trim().min(1).max(2048).optional(),
|
|
29
|
+
dark: z.string().trim().min(1).max(2048).optional()
|
|
30
|
+
})
|
|
31
|
+
.default({}),
|
|
32
|
+
/**
|
|
33
|
+
* Brand voice — how the organization communicates. Plain-language description
|
|
34
|
+
* of tone, register, and personality (e.g. "Direct and human — no jargon").
|
|
35
|
+
* Max 280 characters.
|
|
36
|
+
*/
|
|
37
|
+
voice: z.string().trim().max(280).optional(),
|
|
38
|
+
/**
|
|
39
|
+
* Brand tagline or positioning statement — the memorable one-liner that
|
|
40
|
+
* captures the brand's promise or differentiator. Max 200 characters.
|
|
41
|
+
*/
|
|
42
|
+
tagline: z.string().trim().max(200).optional(),
|
|
43
|
+
/**
|
|
44
|
+
* Core brand values — an ordered list of principles the organization stands
|
|
45
|
+
* for (e.g. ["Transparency", "Craftsmanship", "Velocity"]). Each entry must
|
|
46
|
+
* be a non-empty trimmed string.
|
|
47
|
+
*/
|
|
48
|
+
values: z.array(z.string().trim().min(1)).optional(),
|
|
49
|
+
/**
|
|
50
|
+
* ID of the active UI theme preset from the UI theme-presets registry.
|
|
51
|
+
* The UI layer resolves this to colors and typography via `usePresetsContext()`.
|
|
52
|
+
* Free-form string — validation and fallback state are handled at the UI layer.
|
|
53
|
+
* Min 1, max 64 characters.
|
|
54
|
+
*/
|
|
55
|
+
themePresetId: z.string().trim().min(1).max(64).optional()
|
|
56
|
+
})
|
|
57
|
+
.passthrough()
|
|
58
|
+
|
|
17
59
|
export const DEFAULT_ORGANIZATION_MODEL_BRANDING: z.infer<typeof OrganizationModelBrandingSchema> = {
|
|
18
60
|
organizationName: 'Default Organization',
|
|
19
61
|
productName: 'Organization OS',
|