@crombolo/core 0.0.0-stage → 0.9.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kineco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,69 @@
1
+ crombolo
2
+ Copyright (c) 2026 Kineco
3
+
4
+ This product includes third-party software. crombolo is an independent project.
5
+ It is not affiliated with, endorsed by, or sponsored by Umbraco A/S, which owns
6
+ the Umbraco trademark.
7
+
8
+ There are three places third-party material is redistributed, rather than merely
9
+ depended on:
10
+
11
+ @crombolo/backoffice-dist the built Umbraco backoffice and the browser
12
+ packages bundled into it, produced by
13
+ `bun run vendor:backoffice`
14
+ @crombolo/backoffice-dist three CSS files copied unmodified from the Umbraco
15
+ client source tree
16
+ @crombolo/contracts the Umbraco Management API OpenAPI document,
17
+ copied unmodified
18
+
19
+ No Umbraco trademark is redistributed. The Umbraco wordmark, the "U" roundel
20
+ favicon and the installer illustration are deliberately not carried in this
21
+ repository: MIT grants copyright permission and says nothing about trademarks, so
22
+ crombolo's own marks are served at those paths instead. See `BRANDED_ASSETS` in
23
+ `@crombolo/backoffice-host`.
24
+
25
+ The Umbraco backoffice
26
+ ----------------------
27
+ @umbraco-cms/backoffice 18.2.0 — MIT — https://github.com/umbraco/Umbraco-CMS
28
+ @umbraco-ui/uui 2.0.2 — MIT — https://github.com/umbraco/Umbraco.UI
29
+
30
+ Static files from the Umbraco client source tree
31
+ ------------------------------------------------
32
+ css/umb-css.css, css/rte-content.css, css/umbraco-blockgridlayout.css
33
+ — copied unmodified from Umbraco-CMS release-18.2.0 — MIT
34
+ — https://github.com/umbraco/Umbraco-CMS
35
+ Shipped in @crombolo/backoffice-dist under `upstream-static/`.
36
+
37
+ `rte-content.css` itself carries Tiptap's default editor styles, copied from
38
+ tiptap v2.11.7 (MIT); the file records this in its own header.
39
+
40
+ The Umbraco Management API contract
41
+ -----------------------------------
42
+ OpenApi.json — copied unmodified from Umbraco-CMS release-18.2.0
43
+ (src/Umbraco.Cms.Api.Management/OpenApi.json) — MIT
44
+ — https://github.com/umbraco/Umbraco-CMS
45
+ Shipped in @crombolo/contracts, which also ships the TypeScript types generated
46
+ from it.
47
+
48
+ Browser runtime dependencies bundled into the backoffice build
49
+ --------------------------------------------------------------
50
+ @heximal/expressions 0.1.5 — BSD-3-Clause
51
+ @microsoft/signalr 10.0.11 — MIT
52
+ @tiptap/* 3.31.3 — MIT
53
+ diff 9.0.0 — BSD-3-Clause
54
+ dompurify 3.4.16 — Apache-2.0, elected from "MPL-2.0 OR Apache-2.0"
55
+ element-internals-polyfill 3.0.2 — MIT
56
+ lit 3.3.3 — BSD-3-Clause
57
+ luxon 3.7.2 — MIT
58
+ marked 18.0.14 — MIT
59
+ monaco-editor 0.57.0 — MIT
60
+ rxjs 7.8.2 — Apache-2.0
61
+ uuid 14.0.2 — MIT
62
+
63
+ dompurify is offered under either licence; Apache-2.0 is elected here, so no
64
+ MPL-2.0 obligation attaches to the copy bundled into the backoffice build. Of the
65
+ Apache-2.0 packages above, none ships a NOTICE file of its own, so Apache-2.0
66
+ section 4(d) adds nothing to this file.
67
+
68
+ Each package's own licence text ships inside its npm distribution and applies
69
+ unchanged to the copies bundled here.
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "@crombolo/core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.9.1",
4
+ "description": "Domain model, entity keys and shared contracts for crombolo. No I/O.",
5
+ "license": "MIT",
6
+ "author": "Crombolo",
7
+ "homepage": "https://github.com/kineco-au/crombolo-cms#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kineco-au/crombolo-cms.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/kineco-au/crombolo-cms/issues"
15
+ },
16
+ "engines": {
17
+ "bun": ">=1.3.0"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": "./src/index.ts"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "NOTICE"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The paths the vendored client hard-codes.
3
+ *
4
+ * Umbraco's `UmbracoPath` moves the editor, not its API: the Management API is
5
+ * declared by the contract and the client's generated SDK calls it at
6
+ * `/umbraco/management/api/v1/...` however the backoffice is mounted. The two
7
+ * SignalR hubs are built the same way in the client's own source. So these stay
8
+ * put while `backOfficePath` moves the shell, the login page, the static assets
9
+ * and the SPA's own routes.
10
+ */
11
+ export const MANAGEMENT_API_PREFIX = '/umbraco/management/api/'
12
+
13
+ export const MANAGEMENT_API_PATH = `${MANAGEMENT_API_PREFIX}v1`
14
+
15
+ /** `server-event.context.js`: `${serverURL}/umbraco/serverEventHub`. */
16
+ export const SERVER_EVENT_HUB_PATH = '/umbraco/serverEventHub'
17
+
18
+ /** `preview.context.js`: `${serverUrl}/umbraco/PreviewHub`. */
19
+ export const PREVIEW_HUB_PATH = '/umbraco/PreviewHub'
20
+
21
+ /** Where the editor is mounted unless a site overrides `backOfficePath`. */
22
+ export const DEFAULT_BACKOFFICE_PATH = '/crombolo'
@@ -0,0 +1,257 @@
1
+ /**
2
+ * The content-type aggregate: a content type plus its property groups, property
3
+ * types, compositions, allowed children and templates.
4
+ *
5
+ * Kept as one aggregate because the editor saves it as one document, and because
6
+ * the property set is only meaningful together with the compositions it inherits.
7
+ */
8
+ export interface PropertyTypeModel {
9
+ key: string
10
+ alias: string
11
+ name: string
12
+ description: string | null
13
+ dataTypeKey: string
14
+ /** The group this property renders in, or null for the generic tab. */
15
+ containerKey: string | null
16
+ sortOrder: number
17
+ variesByCulture: boolean
18
+ variesBySegment: boolean
19
+ mandatory: boolean
20
+ mandatoryMessage: string | null
21
+ regEx: string | null
22
+ regExMessage: string | null
23
+ labelOnTop: boolean
24
+ /** The file's `since`, when written. */
25
+ sinceVersion?: string | null
26
+ /** Set when the element is not yet live on the reading node: the state it goes live in. */
27
+ pending?: { version: string; revision: string } | null
28
+ /** Member types only: shown to the member on their profile. */
29
+ memberCanView?: boolean
30
+ /** Member types only: editable by the member on their profile. */
31
+ memberCanEdit?: boolean
32
+ /** Member types only: hidden from users without sensitive-data access. */
33
+ isSensitive?: boolean
34
+ }
35
+
36
+ export interface PropertyGroupModel {
37
+ key: string
38
+ name: string | null
39
+ alias: string | null
40
+ /** 'Tab' or 'Group'; the wire format is a string. */
41
+ type: string
42
+ sortOrder: number
43
+ parentKey: string | null
44
+ }
45
+
46
+ export type CompositionType = 'Composition' | 'Inheritance'
47
+
48
+ export interface CompositionModel {
49
+ contentTypeKey: string
50
+ compositionType: CompositionType
51
+ }
52
+
53
+ export interface AllowedChildModel {
54
+ contentTypeKey: string
55
+ sortOrder: number
56
+ }
57
+
58
+ export interface CleanupModel {
59
+ preventCleanup: boolean
60
+ keepAllVersionsNewerThanDays: number | null
61
+ keepLatestVersionPerDayForDays: number | null
62
+ }
63
+
64
+ export interface ContentTypeAggregate {
65
+ key: string
66
+ alias: string
67
+ name: string
68
+ description: string | null
69
+ icon: string
70
+ allowedAsRoot: boolean
71
+ variesByCulture: boolean
72
+ variesBySegment: boolean
73
+ isElement: boolean
74
+ allowedInLibrary: boolean
75
+ collectionKey: string | null
76
+ cleanup: CleanupModel
77
+ properties: PropertyTypeModel[]
78
+ containers: PropertyGroupModel[]
79
+ compositions: CompositionModel[]
80
+ allowedContentTypes: AllowedChildModel[]
81
+ allowedComponentKeys: string[]
82
+ defaultComponentKey: string | null
83
+ /** Folder placement in the tree, which is not the composition graph. */
84
+ parentKey: string | null
85
+ sinceVersion?: string | null
86
+ pending?: { version: string; revision: string } | null
87
+ }
88
+
89
+ /**
90
+ * Umbraco's system media types (`IsSystemMediaType`): always present, not
91
+ * deletable, alias fixed. Keys from its installer.
92
+ */
93
+ export const SYSTEM_MEDIA_TYPE_KEYS: Readonly<Record<'Folder' | 'Image' | 'File', string>> = {
94
+ Folder: 'f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d',
95
+ Image: 'cc07b313-0843-4aa8-bbda-871c8da728c8',
96
+ File: '4c52d8ab-54e6-40cd-999c-7a5f24903e4d',
97
+ }
98
+
99
+ export function isSystemMediaType(key: string): boolean {
100
+ return Object.values(SYSTEM_MEDIA_TYPE_KEYS).includes(key.toLowerCase())
101
+ }
102
+
103
+ /**
104
+ * Umbraco's one built-in member type, "Member", by the key its installer uses:
105
+ * always present, so a fresh install can create a member without a schema file.
106
+ */
107
+ export const SYSTEM_MEMBER_TYPE_KEY = 'd59be02f-1df9-4228-aa1e-01917d806cda'
108
+
109
+ export function isSystemMemberType(key: string): boolean {
110
+ return key.toLowerCase() === SYSTEM_MEMBER_TYPE_KEY
111
+ }
112
+
113
+ /**
114
+ * Content types this CMS ships: always present, not deletable, alias fixed. A
115
+ * site may add fields to one, but not remove or change the ones it ships with.
116
+ */
117
+ export const SYSTEM_CONTENT_TYPE_KEYS: Readonly<Record<'SeoFields', string>> = {
118
+ SeoFields: '5e0f1c2a-7b3d-4c8e-9a61-0d2b4f6e8a13',
119
+ }
120
+
121
+ export function isSystemContentType(kind: 'document' | 'media' | 'member', key: string): boolean {
122
+ if (kind === 'media') return isSystemMediaType(key)
123
+ if (kind === 'member') return isSystemMemberType(key)
124
+ return Object.values(SYSTEM_CONTENT_TYPE_KEYS).includes(key.toLowerCase())
125
+ }
126
+
127
+ /** A field a system content type ships with, which a site may not remove or change. */
128
+ export interface SystemPropertyModel {
129
+ key: string
130
+ alias: string
131
+ name: string
132
+ description: string
133
+ dataTypeKey: string
134
+ }
135
+
136
+ /**
137
+ * The "Search & sharing" composition's own fields. Invariant as shipped: the data
138
+ * layer does not mask a property's variance by the composing type's, so a site
139
+ * that wants per-language values turns variance on in its own file.
140
+ */
141
+ export const SEO_FIELDS_PROPERTIES: readonly SystemPropertyModel[] = [
142
+ {
143
+ key: '8d1f3a52-6c0e-4b7a-9e24-1a5c7d9f0b31',
144
+ alias: 'seoTitle',
145
+ name: 'Search title',
146
+ description: 'Shown in search results and browser tabs. Leave empty to use the page name.',
147
+ dataTypeKey: '0cc0eba1-9960-42c9-bf9b-60e150b429ae',
148
+ },
149
+ {
150
+ key: '2b7e9c41-0d5f-4a63-8b18-6e4c2a0f7d52',
151
+ alias: 'seoDescription',
152
+ name: 'Search description',
153
+ description: 'A sentence or two shown under the title in search results and when shared.',
154
+ dataTypeKey: 'c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3',
155
+ },
156
+ {
157
+ key: 'f4a60d83-9e2b-4c15-a7d0-3b8e1f5c9a64',
158
+ alias: 'seoImage',
159
+ name: 'Sharing image',
160
+ description: 'Shown when the page is shared. Leave empty to use the site image.',
161
+ dataTypeKey: 'ad9f0cf2-bda2-45d5-9ea1-a63cfc873fd3',
162
+ },
163
+ {
164
+ key: '6c3b8e14-2f7a-4d90-b5e1-9a0d4c7f2e75',
165
+ alias: 'seoCanonical',
166
+ name: 'Preferred address',
167
+ description:
168
+ 'Only when the same content lives at another address search engines should use instead.',
169
+ dataTypeKey: '0cc0eba1-9960-42c9-bf9b-60e150b429ae',
170
+ },
171
+ {
172
+ key: '0e9d5a27-4b1c-4f86-8c32-7d6f0a3e1b86',
173
+ alias: 'seoNoindex',
174
+ name: 'Hide from search engines',
175
+ description: 'Asks search engines to leave this page out of their results.',
176
+ dataTypeKey: '92897bc6-a5f3-4ffe-ae27-f2e7e33dda49',
177
+ },
178
+ {
179
+ key: 'a7f2c6e9-8d4b-4e31-b0a5-2c9e6f1d3a97',
180
+ alias: 'seoSitemapHide',
181
+ name: 'Hide from sitemap',
182
+ description: 'Leaves this page out of the list of pages offered to search engines.',
183
+ dataTypeKey: '92897bc6-a5f3-4ffe-ae27-f2e7e33dda49',
184
+ },
185
+ ]
186
+
187
+ /** The fields a system content type ships with, by the type's key. */
188
+ export function systemContentTypeProperties(key: string): readonly SystemPropertyModel[] {
189
+ return key.toLowerCase() === SYSTEM_CONTENT_TYPE_KEYS.SeoFields ? SEO_FIELDS_PROPERTIES : []
190
+ }
191
+
192
+ /**
193
+ * Why a change to a system content type's fields is refused, or undefined when
194
+ * it keeps every shipped field's key, alias and data type. Fields may be added,
195
+ * relabelled, moved, and switched to vary by culture.
196
+ */
197
+ export function systemFieldViolation(
198
+ key: string,
199
+ properties: ReadonlyArray<{ key: string; alias: string; dataTypeKey: string }>,
200
+ ): string | undefined {
201
+ for (const shipped of systemContentTypeProperties(key)) {
202
+ const found = properties.find((p) => p.key.toLowerCase() === shipped.key)
203
+ if (
204
+ !found ||
205
+ found.alias !== shipped.alias ||
206
+ found.dataTypeKey.toLowerCase() !== shipped.dataTypeKey
207
+ )
208
+ return `The '${shipped.name}' field is provided by Crombolo and cannot be removed or changed. You can add your own fields beside it.`
209
+ }
210
+ return undefined
211
+ }
212
+
213
+ /** How the editor labels a pending element. */
214
+ export function pendingLabel(until: { version: string }): string {
215
+ return `goes live in ${until.version}`
216
+ }
217
+
218
+ export interface DataTypeModel {
219
+ key: string
220
+ /** Schema files reference a data type by this; Umbraco itself has only key and name. */
221
+ alias: string | null
222
+ name: string
223
+ editorAlias: string
224
+ editorUiAlias: string | null
225
+ /** One of ValueStorageType. */
226
+ dbType: string
227
+ values: Array<{ alias: string; value: unknown }>
228
+ parentKey: string | null
229
+ }
230
+
231
+ export interface ComponentModel {
232
+ key: string
233
+ name: string
234
+ alias: string
235
+ content: string | null
236
+ /** Derived from `export const layout = '…'` in the view; the tree hangs children under it. */
237
+ masterKey?: string | null
238
+ }
239
+
240
+ /** One entry in a backoffice tree. */
241
+ export interface TreeItem {
242
+ key: string
243
+ name: string
244
+ hasChildren: boolean
245
+ parentKey: string | null
246
+ icon: string | null
247
+ isFolder: boolean
248
+ isElement?: boolean
249
+ editorUiAlias?: string | null
250
+ /** The content type of a blueprint in the blueprint tree. */
251
+ contentTypeKey?: string | null
252
+ }
253
+
254
+ export interface Page<T> {
255
+ total: number
256
+ items: T[]
257
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The document aggregate.
3
+ *
4
+ * Values are a flat list keyed by (alias, culture, segment), matching the wire
5
+ * format, because that is also how they are stored: one `property_data` row per
6
+ * combination.
7
+ */
8
+ export interface DocumentValue {
9
+ alias: string
10
+ culture: string | null
11
+ segment: string | null
12
+ value: unknown
13
+ /** Present on responses so the editor knows which editor rendered the value. */
14
+ editorAlias?: string
15
+ /** The data type's configuration, on reads; converters that depend on it use it. */
16
+ config?: Record<string, unknown>
17
+ }
18
+
19
+ /**
20
+ * Per-culture state. `NotCreated` means the culture has no name yet;
21
+ * `PublishedPendingChanges` means the draft differs from what is published.
22
+ */
23
+ export type VariantState =
24
+ | 'NotCreated'
25
+ | 'Draft'
26
+ | 'Published'
27
+ | 'PublishedPendingChanges'
28
+ | 'Trashed'
29
+
30
+ export interface DocumentVariant {
31
+ culture: string | null
32
+ segment: string | null
33
+ name: string
34
+ state: VariantState
35
+ createDate: Date
36
+ updateDate: Date
37
+ publishDate: Date | null
38
+ /** When a scheduled publish of this variant will run. */
39
+ scheduledPublishDate?: Date | null
40
+ scheduledUnpublishDate?: Date | null
41
+ }
42
+
43
+ export interface DocumentAggregate {
44
+ key: string
45
+ contentTypeKey: string
46
+ contentTypeAlias: string
47
+ contentTypeIcon: string
48
+ /** The list view the type shows its children in, if any. */
49
+ contentTypeCollectionKey?: string | null
50
+ componentKey: string | null
51
+ parentKey: string | null
52
+ sortOrder: number
53
+ isTrashed: boolean
54
+ /** True when any culture is published. */
55
+ published: boolean
56
+ /** True when the draft differs from the published version. */
57
+ edited: boolean
58
+ values: DocumentValue[]
59
+ variants: DocumentVariant[]
60
+ }
61
+
62
+ /** One property that failed validation, in the terms the editor highlights. */
63
+ export interface DocumentValidationError {
64
+ alias: string
65
+ culture: string | null
66
+ segment: string | null
67
+ messages: string[]
68
+ }
69
+
70
+ /** What the content tree shows for a document, beyond the generic tree item. */
71
+ export interface DocumentTreeItem {
72
+ key: string
73
+ name: string
74
+ hasChildren: boolean
75
+ parentKey: string | null
76
+ contentTypeKey: string
77
+ contentTypeCollectionKey?: string | null
78
+ icon: string
79
+ isTrashed: boolean
80
+ createDate: Date
81
+ ancestorKeys: string[]
82
+ variants: Array<{ culture: string | null; name: string; state: VariantState }>
83
+ }
84
+
85
+ /**
86
+ * A row in the Library tree, which mixes folders and elements: a folder carries
87
+ * the name and nothing else, so `documentType` is reported as null and its single
88
+ * variant as `NotCreated` — the contract's own way of saying there is no content
89
+ * here.
90
+ */
91
+ export interface ElementTreeItem extends DocumentTreeItem {
92
+ isFolder: boolean
93
+ }
94
+
95
+ export interface DocumentVersionSummary {
96
+ id: string
97
+ documentKey: string
98
+ versionDate: Date
99
+ /** The draft that is currently being edited. */
100
+ isCurrentDraft: boolean
101
+ isCurrentPublished: boolean
102
+ preventCleanup: boolean
103
+ userKey: string | null
104
+ culture: string | null
105
+ /** What made this version: a save, publish, rollback or schema migration. */
106
+ kind?: 'save' | 'publish' | 'rollback' | 'migrate'
107
+ }
108
+
109
+ /** A value keyed for lookup, so variance handling stays explicit. */
110
+ export function valueKey(alias: string, culture: string | null, segment: string | null): string {
111
+ return `${alias}|${culture ?? ''}|${segment ?? ''}`
112
+ }
113
+
114
+ /**
115
+ * The culture a value belongs to, honouring the effective variance: a value sent
116
+ * with a culture for an invariant property is stored invariant, which is what
117
+ * keeps a type change from orphaning data.
118
+ */
119
+ export function normaliseValueCulture(
120
+ culture: string | null,
121
+ propertyVariesByCulture: boolean,
122
+ ): string | null {
123
+ return propertyVariesByCulture ? culture : null
124
+ }