@crombolo/api-delivery 0.0.0-stage → 0.13.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 +21 -0
- package/NOTICE +69 -0
- package/package.json +33 -4
- package/src/coverage.ts +27 -0
- package/src/expansion.ts +81 -0
- package/src/handler.ts +487 -0
- package/src/index.ts +7 -0
- package/src/output.ts +363 -0
- package/src/protection.ts +51 -0
- package/src/query.ts +198 -0
- package/src/settings.ts +137 -0
- package/README.md +0 -3
package/src/settings.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Umbraco's `DeliveryApiSettings`, with its defaults: off until a site turns
|
|
3
|
+
* it on. docs/delivery-api.md, "Settings".
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DeliveryApiClientMember {
|
|
7
|
+
/** The member this client signs in as. */
|
|
8
|
+
userName: string
|
|
9
|
+
/** Sent as `umbraco-member-<clientId>`; the prefix is added when missing. */
|
|
10
|
+
clientId: string
|
|
11
|
+
clientSecret: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DeliveryApiSettings {
|
|
15
|
+
enabled: boolean
|
|
16
|
+
/** False: every content request needs the API key. */
|
|
17
|
+
publicAccess: boolean
|
|
18
|
+
/** Unlocks non-public access and preview. Unset, nothing can preview. */
|
|
19
|
+
apiKey: string | undefined
|
|
20
|
+
/** When any are listed, only these content types are served. */
|
|
21
|
+
allowedContentTypeAliases: string[]
|
|
22
|
+
/** Never served; ignored when the allow list has entries. */
|
|
23
|
+
disallowedContentTypeAliases: string[]
|
|
24
|
+
media: { enabled: boolean; publicAccess: boolean }
|
|
25
|
+
memberAuthorization: {
|
|
26
|
+
authorizationCodeFlow: {
|
|
27
|
+
enabled: boolean
|
|
28
|
+
/** Absolute URLs an app may be sent back to after signing in. */
|
|
29
|
+
loginRedirectUrls: string[]
|
|
30
|
+
logoutRedirectUrls: string[]
|
|
31
|
+
/**
|
|
32
|
+
* The site page holding the member login form, e.g. `/login`. A member who
|
|
33
|
+
* is not signed in is sent there with `returnUrl`, and comes back to finish.
|
|
34
|
+
*/
|
|
35
|
+
loginPage: string | undefined
|
|
36
|
+
}
|
|
37
|
+
clientCredentialsFlow: { enabled: boolean; associatedMembers: DeliveryApiClientMember[] }
|
|
38
|
+
/** Umbraco's `Global:TimeOut`: a refresh token lasts this long, an access token a quarter of it. */
|
|
39
|
+
timeoutMinutes: number
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }
|
|
44
|
+
|
|
45
|
+
/** What a site may set: any part of the settings. */
|
|
46
|
+
export type DeliveryApiOptions = DeepPartial<DeliveryApiSettings>
|
|
47
|
+
|
|
48
|
+
export const CLIENT_ID_PREFIX = 'umbraco-member-'
|
|
49
|
+
|
|
50
|
+
/** The client id a caller sends for a configured member client. Pure. */
|
|
51
|
+
export const memberClientId = (id: string): string =>
|
|
52
|
+
id.startsWith(CLIENT_ID_PREFIX) ? id : `${CLIENT_ID_PREFIX}${id}`
|
|
53
|
+
|
|
54
|
+
/** The settings with every default filled in. Pure. */
|
|
55
|
+
export function resolveDeliveryApiSettings(options: DeliveryApiOptions = {}): DeliveryApiSettings {
|
|
56
|
+
const code = options.memberAuthorization?.authorizationCodeFlow ?? {}
|
|
57
|
+
const credentials = options.memberAuthorization?.clientCredentialsFlow ?? {}
|
|
58
|
+
return {
|
|
59
|
+
enabled: options.enabled ?? false,
|
|
60
|
+
publicAccess: options.publicAccess ?? true,
|
|
61
|
+
apiKey: options.apiKey?.trim() ? options.apiKey : undefined,
|
|
62
|
+
allowedContentTypeAliases: [...(options.allowedContentTypeAliases ?? [])].filter(
|
|
63
|
+
(alias): alias is string => typeof alias === 'string',
|
|
64
|
+
),
|
|
65
|
+
disallowedContentTypeAliases: [...(options.disallowedContentTypeAliases ?? [])].filter(
|
|
66
|
+
(alias): alias is string => typeof alias === 'string',
|
|
67
|
+
),
|
|
68
|
+
media: {
|
|
69
|
+
enabled: options.media?.enabled ?? false,
|
|
70
|
+
publicAccess: options.media?.publicAccess ?? true,
|
|
71
|
+
},
|
|
72
|
+
memberAuthorization: {
|
|
73
|
+
authorizationCodeFlow: {
|
|
74
|
+
enabled: code.enabled ?? false,
|
|
75
|
+
loginRedirectUrls: [...(code.loginRedirectUrls ?? [])].filter(
|
|
76
|
+
(url): url is string => typeof url === 'string',
|
|
77
|
+
),
|
|
78
|
+
logoutRedirectUrls: [...(code.logoutRedirectUrls ?? [])].filter(
|
|
79
|
+
(url): url is string => typeof url === 'string',
|
|
80
|
+
),
|
|
81
|
+
loginPage: code.loginPage,
|
|
82
|
+
},
|
|
83
|
+
clientCredentialsFlow: {
|
|
84
|
+
enabled: credentials.enabled ?? false,
|
|
85
|
+
associatedMembers: [...(credentials.associatedMembers ?? [])].flatMap((member) =>
|
|
86
|
+
member?.userName && member.clientId && member.clientSecret
|
|
87
|
+
? [
|
|
88
|
+
{
|
|
89
|
+
userName: member.userName,
|
|
90
|
+
clientId: memberClientId(member.clientId),
|
|
91
|
+
clientSecret: member.clientSecret,
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
: [],
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
timeoutMinutes: options.memberAuthorization?.timeoutMinutes ?? 20,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Either member flow switched on: only then is a bearer token looked at. Pure. */
|
|
103
|
+
export const memberAuthorizationEnabled = (settings: DeliveryApiSettings): boolean =>
|
|
104
|
+
settings.memberAuthorization.authorizationCodeFlow.enabled ||
|
|
105
|
+
settings.memberAuthorization.clientCredentialsFlow.enabled
|
|
106
|
+
|
|
107
|
+
/** Whether a content type is served, by Umbraco's allow-then-disallow rule. Pure. */
|
|
108
|
+
export function isAllowedContentType(settings: DeliveryApiSettings, alias: string): boolean {
|
|
109
|
+
const lower = alias.toLowerCase()
|
|
110
|
+
if (settings.allowedContentTypeAliases.length > 0)
|
|
111
|
+
return settings.allowedContentTypeAliases.some((a) => a.toLowerCase() === lower)
|
|
112
|
+
return !settings.disallowedContentTypeAliases.some((a) => a.toLowerCase() === lower)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The configuration problems that switch a member flow off, said once at boot. Pure. */
|
|
116
|
+
export function memberAuthorizationProblems(settings: DeliveryApiSettings): string[] {
|
|
117
|
+
const problems: string[] = []
|
|
118
|
+
const code = settings.memberAuthorization.authorizationCodeFlow
|
|
119
|
+
const absolute = (url: string) => /^[a-z][a-z0-9+.-]*:\/\//i.test(url)
|
|
120
|
+
if (code.enabled) {
|
|
121
|
+
if (code.loginRedirectUrls.length === 0)
|
|
122
|
+
problems.push('No login redirect URLs are set, so member sign-in from an app is off.')
|
|
123
|
+
else if (!code.loginRedirectUrls.every(absolute))
|
|
124
|
+
problems.push(
|
|
125
|
+
'Every login redirect URL must be absolute, so member sign-in from an app is off.',
|
|
126
|
+
)
|
|
127
|
+
if (!code.logoutRedirectUrls.every(absolute))
|
|
128
|
+
problems.push(
|
|
129
|
+
'Every logout redirect URL must be absolute, so member sign-in from an app is off.',
|
|
130
|
+
)
|
|
131
|
+
if (!code.loginPage)
|
|
132
|
+
problems.push(
|
|
133
|
+
'No member login page is set, so a member who is not signed in cannot sign in from an app.',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return problems
|
|
137
|
+
}
|
package/README.md
DELETED