@crombolo/api-management 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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/package.json +32 -4
  4. package/src/authorization.ts +453 -0
  5. package/src/handlers/content-type.ts +784 -0
  6. package/src/handlers/data-type.ts +267 -0
  7. package/src/handlers/dictionary.ts +245 -0
  8. package/src/handlers/document-blueprint.ts +193 -0
  9. package/src/handlers/document.ts +961 -0
  10. package/src/handlers/editor-services.ts +59 -0
  11. package/src/handlers/element.ts +492 -0
  12. package/src/handlers/file-systems.ts +207 -0
  13. package/src/handlers/import.ts +46 -0
  14. package/src/handlers/localization.ts +116 -0
  15. package/src/handlers/log-viewer.ts +125 -0
  16. package/src/handlers/manifest.ts +15 -0
  17. package/src/handlers/media.ts +453 -0
  18. package/src/handlers/member-group.ts +87 -0
  19. package/src/handlers/member.ts +189 -0
  20. package/src/handlers/models-builder.ts +29 -0
  21. package/src/handlers/package.ts +189 -0
  22. package/src/handlers/public-access.ts +107 -0
  23. package/src/handlers/published-cache.ts +27 -0
  24. package/src/handlers/redirects.ts +78 -0
  25. package/src/handlers/references.ts +44 -0
  26. package/src/handlers/server.ts +25 -0
  27. package/src/handlers/settings-stubs.ts +76 -0
  28. package/src/handlers/start-node-trees.ts +84 -0
  29. package/src/handlers/template-query.ts +191 -0
  30. package/src/handlers/template.ts +150 -0
  31. package/src/handlers/temporary-file.ts +62 -0
  32. package/src/handlers/user.ts +604 -0
  33. package/src/index.ts +17 -0
  34. package/src/ports-content.ts +564 -0
  35. package/src/ports-files.ts +51 -0
  36. package/src/ports-logs.ts +38 -0
  37. package/src/ports-members.ts +111 -0
  38. package/src/ports-models-builder.ts +26 -0
  39. package/src/ports-packages.ts +58 -0
  40. package/src/ports-users.ts +230 -0
  41. package/src/ports.ts +225 -0
  42. package/src/router.ts +216 -0
  43. package/src/routes.ts +99 -0
  44. package/src/server-events.ts +71 -0
  45. package/README.md +0 -3
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Members and the groups they belong to. The response types come from the
3
+ * generated contract, so an implementation that drifts from the wire shape is a
4
+ * compile error rather than a broken editor.
5
+ */
6
+ import type { ResponseOf } from '@crombolo/contracts'
7
+ import type { DocumentValidationError, DocumentValue, Page, SkipTake } from '@crombolo/core'
8
+ import type { PortFailure } from './ports-content.ts'
9
+ import type { Principal } from './router.ts'
10
+
11
+ export type MemberGroupResponse = ResponseOf<'GetMemberGroupById'>
12
+ export type MemberGroupItem = ResponseOf<'GetItemMemberGroup'>[number]
13
+ export type MemberGroupTreeItem = ResponseOf<'GetTreeMemberGroupRoot'>['items'][number]
14
+
15
+ /** A name already taken is Umbraco's only failure here besides a missing group. */
16
+ export type MemberGroupStatus = 'ok' | 'not-found' | 'duplicate-name'
17
+
18
+ export type MemberResponse = ResponseOf<'GetMemberById'>
19
+ export type MemberItem = ResponseOf<'GetItemMember'>[number]
20
+
21
+ /**
22
+ * What a create or a save carries. `password` is plaintext here and hashed by
23
+ * the adapter: hashing belongs to `@crombolo/auth`, and a handler should not be
24
+ * able to store one by accident.
25
+ */
26
+ export interface SaveMember {
27
+ key: string
28
+ memberTypeKey: string
29
+ email: string
30
+ username: string
31
+ password?: string | null
32
+ oldPassword?: string | null
33
+ isApproved: boolean
34
+ isLockedOut: boolean
35
+ isTwoFactorEnabled: boolean
36
+ groupKeys: string[] | undefined
37
+ values: DocumentValue[]
38
+ variants: Array<{ culture: string | null; segment: string | null; name: string }>
39
+ }
40
+
41
+ export interface MemberFilterCriteria extends SkipTake {
42
+ memberTypeKey?: string | null
43
+ memberGroupName?: string | null
44
+ isApproved?: boolean
45
+ isLockedOut?: boolean
46
+ filter?: string
47
+ orderBy?: string
48
+ orderDirection?: 'Ascending' | 'Descending'
49
+ }
50
+
51
+ export interface MemberPort {
52
+ byKey(key: string, viewer: Principal): Promise<MemberResponse | undefined>
53
+ /** The rules the server would enforce on a save, for the editor to show first. */
54
+ validate(input: SaveMember): Promise<DocumentValidationError[]>
55
+ create(input: SaveMember, principal: Principal): Promise<{ ok: true; key: string } | PortFailure>
56
+ update(key: string, input: SaveMember, principal: Principal): Promise<{ ok: true } | PortFailure>
57
+ remove(key: string): Promise<boolean>
58
+ /** The Members collection. */
59
+ filter(criteria: MemberFilterCriteria, viewer: Principal): Promise<Page<MemberResponse>>
60
+ items(keys: readonly string[]): Promise<MemberItem[]>
61
+ search(
62
+ query: string,
63
+ options: { allowedMemberTypeKeys: readonly string[] } & SkipTake,
64
+ ): Promise<Page<MemberItem>>
65
+ }
66
+
67
+ export type PublicAccessResponse = ResponseOf<'GetDocumentByIdPublicAccess'>
68
+
69
+ /**
70
+ * Why a public-access write was refused, in Umbraco's own vocabulary — the
71
+ * client keys its messages off these.
72
+ */
73
+ export type PublicAccessStatus =
74
+ | 'ok'
75
+ | 'content-not-found'
76
+ | 'login-node-not-found'
77
+ | 'error-node-not-found'
78
+ | 'entry-not-found'
79
+
80
+ export interface PublicAccessInput {
81
+ loginDocumentKey: string
82
+ errorDocumentKey: string
83
+ memberGroupNames: string[]
84
+ memberUserNames: string[]
85
+ }
86
+
87
+ export interface PublicAccessPort {
88
+ /**
89
+ * The entry protecting a document. `includeAncestors` follows the branch up,
90
+ * which is how the editor tells "protected here" from "protected above".
91
+ */
92
+ entry(
93
+ documentKey: string,
94
+ includeAncestors: boolean,
95
+ ): Promise<{ status: PublicAccessStatus; entry?: PublicAccessResponse }>
96
+ save(documentKey: string, input: PublicAccessInput): Promise<PublicAccessStatus>
97
+ remove(documentKey: string): Promise<PublicAccessStatus>
98
+ }
99
+
100
+ export interface MemberGroupPort {
101
+ list(paging: SkipTake): Promise<Page<MemberGroupResponse>>
102
+ byKey(key: string): Promise<MemberGroupResponse | undefined>
103
+ items(keys: readonly string[]): Promise<MemberGroupItem[]>
104
+ tree(paging: SkipTake): Promise<Page<MemberGroupTreeItem>>
105
+ create(input: {
106
+ key?: string
107
+ name: string
108
+ }): Promise<{ status: MemberGroupStatus; key?: string }>
109
+ rename(key: string, name: string): Promise<MemberGroupStatus>
110
+ remove(key: string): Promise<MemberGroupStatus>
111
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Umbraco's ModelsBuilder, which generates strongly-typed models from the
3
+ * content types. Crombolo's models are the TypeScript `crombolo generate`
4
+ * writes from the TOML schema, so the same three operations describe it.
5
+ */
6
+
7
+ /** Umbraco's `OutOfDateType`: `Unknown` when staleness cannot be determined. */
8
+ export type OutOfDateStatus = 'OutOfDate' | 'Current' | 'Unknown'
9
+
10
+ export interface ModelsBuilderInfo {
11
+ /** One of Umbraco's `ModelsMode` strings; the client explains only those. */
12
+ mode: string
13
+ canGenerate: boolean
14
+ outOfDateModels: boolean
15
+ trackingOutOfDateModels: boolean
16
+ lastError: string | null
17
+ version: string | null
18
+ modelsNamespace: string | null
19
+ }
20
+
21
+ export interface ModelsBuilderPort {
22
+ info(): Promise<ModelsBuilderInfo>
23
+ status(): Promise<OutOfDateStatus>
24
+ /** Writes the models; the message of a failure is kept for `info().lastError`. */
25
+ build(): Promise<{ ok: true } | { ok: false; error: string }>
26
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Created packages: the definitions the Packages section builds, and the zip a
3
+ * download turns one into. `docs/17-bundles.md`.
4
+ *
5
+ * The port hands back built bytes rather than a path, because the artifact is
6
+ * never stored — it is serialized when it is asked for, so it cannot be stale.
7
+ */
8
+ import type { Page, SkipTake } from '@crombolo/core'
9
+
10
+ /** What a package would contain, in the aliases and keys the contract uses. */
11
+ export interface PackageDefinitionInput {
12
+ name: string
13
+ /** The content subtree's root, by key, or null when none is picked. */
14
+ contentNodeId: string | null
15
+ contentLoadChildNodes: boolean
16
+ mediaIds: string[]
17
+ mediaLoadChildNodes: boolean
18
+ /** Null and empty mean the same thing; the contract allows both. */
19
+ elementIds: string[] | null
20
+ contentTypes: string[]
21
+ mediaTypes: string[]
22
+ dataTypes: string[]
23
+ templates: string[]
24
+ partialViews: string[]
25
+ stylesheets: string[]
26
+ scripts: string[]
27
+ languages: string[]
28
+ dictionaryItems: string[]
29
+ }
30
+
31
+ export interface PackageDefinition extends PackageDefinitionInput {
32
+ id: string
33
+ }
34
+
35
+ export type PackageWriteResult =
36
+ | { ok: true; id: string }
37
+ | { ok: false; status: PackageWriteStatus }
38
+
39
+ export type PackageWriteStatus = 'NotFound' | 'DuplicateName' | 'InvalidName'
40
+
41
+ /** A built package, ready to send. */
42
+ export interface BuiltPackage {
43
+ fileName: string
44
+ /** Owns its buffer, so it can be a response body without a copy. */
45
+ bytes: Uint8Array<ArrayBuffer>
46
+ }
47
+
48
+ export interface PackagePort {
49
+ /** Where the Marketplace view points; from the site's configuration. */
50
+ marketplaceUrl(): string
51
+ list(paging: SkipTake): Promise<Page<PackageDefinition>>
52
+ byId(id: string): Promise<PackageDefinition | undefined>
53
+ create(input: PackageDefinitionInput, id: string | undefined): Promise<PackageWriteResult>
54
+ update(id: string, input: PackageDefinitionInput): Promise<PackageWriteResult>
55
+ remove(id: string): Promise<'deleted' | 'notFound'>
56
+ /** Serializes and zips the definition now; undefined when it is gone. */
57
+ build(id: string): Promise<BuiltPackage | undefined>
58
+ }
@@ -0,0 +1,230 @@
1
+ /** Ports for backoffice users, user groups and user data. */
2
+ import type { ResponseOf } from '@crombolo/contracts'
3
+ import type { Page } from '@crombolo/core'
4
+ import type { Principal } from './router.ts'
5
+
6
+ export type UserResponse = ResponseOf<'GetUserById'>
7
+ export type UserGroupResponse = ResponseOf<'GetUserGroupById'>
8
+
9
+ /** Umbraco's `UserOperationStatus` names, as its problem details report them. */
10
+ export type UserStatus =
11
+ | 'Success'
12
+ | 'UserNotFound'
13
+ | 'MissingUserGroup'
14
+ | 'NoUserGroup'
15
+ | 'DuplicateUserName'
16
+ | 'DuplicateEmail'
17
+ | 'InvalidEmail'
18
+ | 'InvalidUserName'
19
+ | 'UserNameIsNotEmail'
20
+ | 'Unauthorized'
21
+ | 'Forbidden'
22
+ | 'CannotDeleteSelf'
23
+ | 'CannotDisableSelf'
24
+ | 'CannotDeleteUserWithLoginHistory'
25
+ | 'CannotDisableInvitedUser'
26
+ | 'CannotInvite'
27
+ | 'InvalidPassword'
28
+ | 'SelfOldPasswordRequired'
29
+ | 'InvalidIsoCode'
30
+ | 'ContentStartNodeNotFound'
31
+ | 'MediaStartNodeNotFound'
32
+ | 'ElementStartNodeNotFound'
33
+ | 'InvalidInviteToken'
34
+ | 'NotInInviteState'
35
+ | 'AvatarFileNotFound'
36
+ | 'InvalidAvatar'
37
+ | 'InvalidUserType'
38
+ | 'DuplicateClientId'
39
+ | 'InvalidClientId'
40
+ | 'InvalidResetCode'
41
+
42
+ export type UserOrder =
43
+ | 'UserName'
44
+ | 'Language'
45
+ | 'Name'
46
+ | 'Email'
47
+ | 'Id'
48
+ | 'CreateDate'
49
+ | 'UpdateDate'
50
+ | 'IsApproved'
51
+ | 'IsLockedOut'
52
+ | 'LastLoginDate'
53
+
54
+ export interface UserFilter {
55
+ filter?: string
56
+ groupKeys: string[]
57
+ states: string[]
58
+ orderBy: UserOrder
59
+ direction: 'Ascending' | 'Descending'
60
+ skip: number
61
+ take: number
62
+ }
63
+
64
+ export interface UserInput {
65
+ key?: string
66
+ kind: 'Default' | 'Api'
67
+ email: string
68
+ userName: string
69
+ name: string
70
+ groupKeys: string[]
71
+ }
72
+
73
+ export interface UserUpdateInput {
74
+ email: string
75
+ userName: string
76
+ name: string
77
+ languageIsoCode: string
78
+ groupKeys: string[]
79
+ documentStartNodeKeys: string[]
80
+ hasDocumentRootAccess: boolean
81
+ mediaStartNodeKeys: string[]
82
+ hasMediaRootAccess: boolean
83
+ elementStartNodeKeys: string[]
84
+ hasElementRootAccess: boolean
85
+ }
86
+
87
+ export type Result<T> = { status: 'Success'; value: T } | { status: Exclude<UserStatus, 'Success'> }
88
+
89
+ export interface UserPort {
90
+ list(principal: Principal, skip: number, take: number): Promise<Page<UserResponse>>
91
+ filter(principal: Principal, query: UserFilter): Promise<Page<UserResponse>>
92
+ get(principal: Principal, key: string): Promise<Result<UserResponse>>
93
+ batch(principal: Principal, keys: readonly string[]): Promise<UserResponse[]>
94
+ create(principal: Principal, input: UserInput): Promise<Result<string>>
95
+ invite(principal: Principal, input: UserInput, message: string | null): Promise<Result<string>>
96
+ resendInvite(principal: Principal, key: string, message: string | null): Promise<UserStatus>
97
+ /** Anonymous: whether an invitation token is still good. */
98
+ verifyInvite(key: string, token: string): Promise<UserStatus>
99
+ /** Anonymous: accepts an invitation by setting the first password. */
100
+ createInitialPassword(key: string, token: string, password: string): Promise<UserStatus>
101
+ update(principal: Principal, key: string, input: UserUpdateInput): Promise<UserStatus>
102
+ delete(principal: Principal, keys: readonly string[]): Promise<UserStatus>
103
+ setApproved(principal: Principal, keys: readonly string[], approved: boolean): Promise<UserStatus>
104
+ unlock(principal: Principal, keys: readonly string[]): Promise<UserStatus>
105
+ setGroups(
106
+ principal: Principal,
107
+ userKeys: readonly string[],
108
+ groupKeys: readonly string[],
109
+ ): Promise<UserStatus>
110
+ changePassword(
111
+ principal: Principal,
112
+ key: string,
113
+ newPassword: string,
114
+ oldPassword: string | null,
115
+ ): Promise<UserStatus>
116
+ /** A generated password, which the backoffice shows once. */
117
+ resetPassword(principal: Principal, key: string): Promise<Result<string>>
118
+ setAvatar(principal: Principal, key: string, temporaryFileId: string): Promise<UserStatus>
119
+ removeAvatar(principal: Principal, key: string): Promise<UserStatus>
120
+ calculateStartNodes(
121
+ principal: Principal,
122
+ key: string,
123
+ ): Promise<Result<ResponseOf<'GetUserByIdCalculateStartNodes'>>>
124
+ clientCredentials(principal: Principal, key: string): Promise<Result<string[]>>
125
+ addClientCredential(
126
+ principal: Principal,
127
+ key: string,
128
+ clientId: string,
129
+ secret: string,
130
+ ): Promise<UserStatus>
131
+ removeClientCredential(principal: Principal, key: string, clientId: string): Promise<UserStatus>
132
+ setLanguage(principal: Principal, isoCode: string): Promise<UserStatus>
133
+ /** The caller's verbs on each node, as the permission rules calculate them. */
134
+ permissions(
135
+ principal: Principal,
136
+ tree: 'document' | 'media' | 'element',
137
+ keys: readonly string[],
138
+ ): Promise<Array<{ nodeKey: string; permissions: string[] }>>
139
+ configuration(): ResponseOf<'GetUserConfiguration'>
140
+ currentConfiguration(): ResponseOf<'GetUserCurrentConfiguration'>
141
+ passwordConfiguration(): ResponseOf<'GetSecurityConfiguration'>['passwordConfiguration']
142
+ /** Anonymous: sends a reset link when the address belongs to a user; says nothing either way. */
143
+ forgotPassword(email: string): Promise<void>
144
+ verifyResetCode(key: string, code: string): Promise<UserStatus>
145
+ resetWithCode(key: string, code: string, password: string): Promise<UserStatus>
146
+ }
147
+
148
+ export interface UserGroupInput {
149
+ key?: string
150
+ name: string
151
+ alias: string
152
+ description: string | null
153
+ icon: string | null
154
+ sections: string[]
155
+ languages: string[]
156
+ hasAccessToAllLanguages: boolean
157
+ documentStartNode: string | null
158
+ documentRootAccess: boolean
159
+ mediaStartNode: string | null
160
+ mediaRootAccess: boolean
161
+ elementStartNode: string | null
162
+ elementRootAccess: boolean
163
+ fallbackPermissions: string[]
164
+ permissions: UserGroupResponse['permissions']
165
+ }
166
+
167
+ /** Umbraco's `UserGroupOperationStatus` names. */
168
+ export type UserGroupStatus =
169
+ | 'Success'
170
+ | 'NotFound'
171
+ | 'Unauthorized'
172
+ | 'Forbidden'
173
+ | 'DuplicateAlias'
174
+ | 'DuplicateName'
175
+ | 'MissingName'
176
+ | 'NameTooLong'
177
+ | 'AliasTooLong'
178
+ | 'IsSystemUserGroup'
179
+ | 'CanNotUpdateAliasIsSystemUserGroup'
180
+ | 'DocumentStartNodeKeyNotFound'
181
+ | 'MediaStartNodeKeyNotFound'
182
+ | 'ElementStartNodeKeyNotFound'
183
+ | 'LanguageNotFound'
184
+ | 'AdminGroupCannotBeEmpty'
185
+ | 'UserNotFound'
186
+
187
+ export interface UserGroupPort {
188
+ list(skip: number, take: number): Promise<Page<UserGroupResponse>>
189
+ filter(
190
+ principal: Principal,
191
+ filter: string,
192
+ skip: number,
193
+ take: number,
194
+ ): Promise<Page<UserGroupResponse>>
195
+ get(principal: Principal, key: string): Promise<UserGroupResponse | 'NotFound' | 'Forbidden'>
196
+ items(keys: readonly string[]): Promise<ResponseOf<'GetItemUserGroup'>>
197
+ create(
198
+ principal: Principal,
199
+ input: UserGroupInput,
200
+ ): Promise<{ status: UserGroupStatus; key?: string }>
201
+ update(principal: Principal, key: string, input: UserGroupInput): Promise<UserGroupStatus>
202
+ delete(principal: Principal, keys: readonly string[]): Promise<UserGroupStatus>
203
+ addUsers(principal: Principal, key: string, userKeys: readonly string[]): Promise<UserGroupStatus>
204
+ removeUsers(
205
+ principal: Principal,
206
+ key: string,
207
+ userKeys: readonly string[],
208
+ ): Promise<UserGroupStatus>
209
+ }
210
+
211
+ export interface UserDataEntryModel {
212
+ key: string
213
+ group: string
214
+ identifier: string
215
+ value: string
216
+ }
217
+
218
+ /** The current user's key/value data. */
219
+ export interface UserDataPort {
220
+ list(
221
+ principal: Principal,
222
+ filter: { groups: string[]; identifiers: string[] },
223
+ skip: number,
224
+ take: number,
225
+ ): Promise<Page<UserDataEntryModel>>
226
+ get(principal: Principal, key: string): Promise<UserDataEntryModel | undefined>
227
+ create(principal: Principal, entry: UserDataEntryModel): Promise<'Success' | 'AlreadyExists'>
228
+ update(principal: Principal, entry: UserDataEntryModel): Promise<'Success' | 'NotFound'>
229
+ delete(principal: Principal, key: string): Promise<'Success' | 'NotFound'>
230
+ }
package/src/ports.ts ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Ports the Management API needs from the rest of the application.
3
+ *
4
+ * Declared here as interfaces so this package depends only on @crombolo/core and
5
+ * @crombolo/contracts; the composition root supplies the implementations. The
6
+ * return types come from the generated contract, so an implementation that
7
+ * drifts from the wire shape fails to compile.
8
+ */
9
+
10
+ import type { ResponseOf } from '@crombolo/contracts'
11
+ import type { UploadSettings } from '@crombolo/core'
12
+ import type {
13
+ BlueprintPort,
14
+ ContentTypePort,
15
+ DataTypePort,
16
+ DocumentPort,
17
+ ElementPort,
18
+ MediaPort,
19
+ RedirectPort,
20
+ ReferencePort,
21
+ TemplatePort,
22
+ TemplateQueryPort,
23
+ } from './ports-content.ts'
24
+ import type { FileSystemPort, Snippet } from './ports-files.ts'
25
+ import type { LogViewerPort } from './ports-logs.ts'
26
+ import type { MemberGroupPort, MemberPort, PublicAccessPort } from './ports-members.ts'
27
+ import type { ModelsBuilderPort } from './ports-models-builder.ts'
28
+ import type { PackagePort } from './ports-packages.ts'
29
+ import type { UserDataPort, UserGroupPort, UserPort } from './ports-users.ts'
30
+ import type { Principal } from './router.ts'
31
+
32
+ export interface ServerPort {
33
+ status(): ResponseOf<'GetServerStatus'>
34
+ configuration(): ResponseOf<'GetServerConfiguration'>
35
+ information(): ResponseOf<'GetServerInformation'>
36
+ /** What may be uploaded; Umbraco's defaults when omitted. */
37
+ uploadSettings?(): UploadSettings
38
+ }
39
+
40
+ export type ManifestVisibility = 'all' | 'public' | 'private'
41
+
42
+ export interface ManifestPort {
43
+ list(visibility: ManifestVisibility): ResponseOf<'GetManifestManifest'>
44
+ }
45
+
46
+ export interface LanguageInput {
47
+ isoCode: string
48
+ name: string
49
+ isDefault: boolean
50
+ isMandatory: boolean
51
+ fallbackIsoCode: string | null
52
+ }
53
+
54
+ export interface LocalizationPort {
55
+ /** Languages live in the database (and `schema/languages.toml`); cultures are the fixed list. */
56
+ allLanguages(): Promise<LanguageInput[]>
57
+ language(isoCode: string): Promise<LanguageInput | undefined>
58
+ saveLanguage(language: LanguageInput): Promise<void>
59
+ deleteLanguage(isoCode: string): Promise<'deleted' | 'default' | 'in-use' | 'not-found'>
60
+ cultures(): ResponseOf<'GetCulture'>['items']
61
+ }
62
+
63
+ export interface CurrentUserPort {
64
+ get(principal: Principal): ResponseOf<'GetUserCurrent'> | Promise<ResponseOf<'GetUserCurrent'>>
65
+ }
66
+
67
+ /** Backoffice users by key, for anything that names who did something. */
68
+ export interface UserItemPort {
69
+ items(
70
+ keys: readonly string[],
71
+ ): Promise<Array<{ key: string; name: string; avatarUrls: string[]; kind?: 'Default' | 'Api' }>>
72
+ }
73
+
74
+ /** Uploads waiting to be saved into a value (Umbraco's temporary files). */
75
+ export interface TemporaryFilePort {
76
+ save(
77
+ id: string,
78
+ file: File,
79
+ ): Promise<
80
+ | { ok: true; file: { id: string; fileName: string; availableUntil: Date } }
81
+ | { ok: false; reason: string }
82
+ >
83
+ get(id: string): Promise<{ id: string; fileName: string; availableUntil: Date } | undefined>
84
+ /** The upload's name and bytes, for files that are read rather than placed. */
85
+ read(id: string): Promise<{ fileName: string; bytes: Uint8Array } | undefined>
86
+ remove(id: string): Promise<boolean>
87
+ }
88
+
89
+ /** Tags in use, for the tags editor's suggestions. */
90
+ export interface TagPort {
91
+ list(filter: {
92
+ query?: string
93
+ group?: string
94
+ culture?: string | null
95
+ }): Promise<Array<{ id: string; text: string; group: string; nodeCount: number }>>
96
+ }
97
+
98
+ /**
99
+ * The published content cache behind the Published Status dashboard.
100
+ *
101
+ * Umbraco distinguishes reloading the in-memory cache from rebuilding the
102
+ * database cache it reads (`cmsContentNu`). There is no such table here — the
103
+ * content tables are the source — so the difference is when the cost is paid:
104
+ * a reload drops the snapshot and lets the next request rebuild it, a rebuild
105
+ * builds it before answering.
106
+ */
107
+ export interface PublishedCachePort {
108
+ reload(): void
109
+ rebuild(): Promise<void>
110
+ /** Ours completes within the request, so this is only ever false afterwards. */
111
+ isRebuilding(): boolean
112
+ }
113
+
114
+ /** Embed markup for a URL, from its oEmbed provider. */
115
+ export interface OEmbedPort {
116
+ markup(
117
+ url: string,
118
+ maxWidth?: number,
119
+ maxHeight?: number,
120
+ ): Promise<
121
+ { ok: true; markup: string } | { ok: false; status: 'unsupported' | 'failed'; reason: string }
122
+ >
123
+ }
124
+
125
+ /** Preview mode is a cookie; the server owns its name and attributes. */
126
+ export interface PreviewPort {
127
+ /** `Set-Cookie` values that enter preview for the signed-in editor. */
128
+ enter(): string[]
129
+ /** `Set-Cookie` values that leave it. */
130
+ exit(): string[]
131
+ }
132
+
133
+ export interface DictionaryItemModel {
134
+ key: string
135
+ name: string
136
+ parentKey: string | null
137
+ translations: Array<{ isoCode: string; translation: string }>
138
+ }
139
+
140
+ export type DictionaryWriteStatus =
141
+ | 'Success'
142
+ | 'DuplicateItemKey'
143
+ | 'DuplicateKey'
144
+ | 'ParentNotFound'
145
+ | 'NotFound'
146
+ | 'InvalidParent'
147
+ | 'InvalidLanguage'
148
+
149
+ /** Dictionary items and their translations. */
150
+ export interface DictionaryPort {
151
+ all(filter?: string): Promise<DictionaryItemModel[]>
152
+ get(key: string): Promise<DictionaryItemModel | undefined>
153
+ items(keys: readonly string[]): Promise<DictionaryItemModel[]>
154
+ children(
155
+ parentKey: string | null,
156
+ ): Promise<Array<{ key: string; name: string; parentKey: string | null; hasChildren: boolean }>>
157
+ /** The item and its ancestors, root first. */
158
+ ancestry(key: string): Promise<DictionaryItemModel[]>
159
+ create(item: DictionaryItemModel): Promise<DictionaryWriteStatus>
160
+ update(
161
+ key: string,
162
+ change: { name: string; translations: DictionaryItemModel['translations'] },
163
+ ): Promise<DictionaryWriteStatus>
164
+ move(key: string, parentKey: string | null): Promise<DictionaryWriteStatus>
165
+ delete(key: string): Promise<DictionaryWriteStatus>
166
+ /** The item, with its descendants when asked, as a `.udt` file. */
167
+ export(
168
+ key: string,
169
+ includeChildren: boolean,
170
+ ): Promise<{ fileName: string; content: string } | undefined>
171
+ /** Creates or updates the items in an uploaded `.udt` file, under `parentKey`. */
172
+ import(
173
+ temporaryFileId: string,
174
+ parentKey: string | null,
175
+ ): Promise<
176
+ | { ok: true; key: string }
177
+ | { ok: false; status: 'NotFound' | 'ParentNotFound' | 'Invalid'; reason: string }
178
+ >
179
+ }
180
+
181
+ export interface AuthPort {
182
+ /** Ends the caller's session. */
183
+ signOut(principal: Principal): Promise<void>
184
+ }
185
+
186
+ export interface ManagementApiDeps {
187
+ server: ServerPort
188
+ manifests: ManifestPort
189
+ localization: LocalizationPort
190
+ currentUser: CurrentUserPort
191
+ users?: UserItemPort
192
+ preview?: PreviewPort
193
+ temporaryFiles?: TemporaryFilePort
194
+ media?: MediaPort
195
+ tags?: TagPort
196
+ oembed?: OEmbedPort
197
+ blueprints?: BlueprintPort
198
+ contentTypes?: ContentTypePort
199
+ mediaTypes?: ContentTypePort
200
+ memberTypes?: ContentTypePort
201
+ dataTypes?: DataTypePort
202
+ templates?: TemplatePort
203
+ documents?: DocumentPort
204
+ dictionary?: DictionaryPort
205
+ userManagement?: UserPort
206
+ userGroups?: UserGroupPort
207
+ userData?: UserDataPort
208
+ /** The site's `components/` tree: one root, folders and all. */
209
+ components?: FileSystemPort
210
+ componentSnippets?: readonly Snippet[]
211
+ stylesheets?: FileSystemPort
212
+ scripts?: FileSystemPort
213
+ logViewer?: LogViewerPort
214
+ modelsBuilder?: ModelsBuilderPort
215
+ publishedCache?: PublishedCachePort
216
+ /** The template editor's query builder. */
217
+ templateQuery?: TemplateQueryPort
218
+ memberGroups?: MemberGroupPort
219
+ members?: MemberPort
220
+ publicAccess?: PublicAccessPort
221
+ elements?: ElementPort
222
+ references?: ReferencePort
223
+ redirects?: RedirectPort
224
+ packages?: PackagePort
225
+ }