@nuxt-customer-portal/core 0.3.2 → 0.4.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/CHANGELOG.md +37 -0
- package/README.md +4 -0
- package/app/components/PortalTimezoneSelect.vue +28 -0
- package/app/composables/useClientConfiguration.ts +8 -0
- package/app/composables/usePaginatedResource.ts +2 -1
- package/app/composables/usePortalSession.ts +2 -0
- package/app/composables/useUserStore.ts +5 -0
- package/app/utils/auth-client.ts +4 -1
- package/i18n/locales/en.json +22 -0
- package/i18n/locales/nl.json +22 -0
- package/migrations/0006_timezone_preferences.sql +22 -0
- package/migrations/0007_user_names.sql +2 -0
- package/migrations/0008_better_auth_api_keys.sql +27 -0
- package/nuxt.config.ts +16 -5
- package/package.json +8 -3
- package/portal.manifest.mjs +1 -1
- package/server/api/auth/permissions.get.ts +4 -1
- package/server/api/timezones.get.ts +9 -0
- package/server/db/schema/auth-schema.ts +46 -1
- package/server/middleware/auth.ts +10 -0
- package/server/utils/auth.ts +150 -12
- package/server/utils/business-hooks.ts +34 -0
- package/server/utils/client-account-policy.ts +15 -0
- package/server/utils/client-configuration.ts +9 -0
- package/server/utils/email-texts.ts +27 -0
- package/server/utils/email.ts +25 -2
- package/server/utils/invitation-management.ts +4 -0
- package/server/utils/openapi-contracts.ts +8 -0
- package/server/utils/permissions.ts +4 -0
- package/server/utils/portal-email.ts +12 -4
- package/server/utils/public-api-route.ts +14 -0
- package/server/utils/public-invitation-route.ts +3 -0
- package/server/utils/timezone-validation.ts +4 -0
- package/server/utils/timezones.ts +28 -0
- package/shared/appointment-time.ts +43 -0
- package/shared/core-feature.ts +82 -29
- package/shared/countries.ts +18 -0
- package/shared/email-recipient.ts +7 -0
- package/shared/languages.ts +7 -0
- package/shared/permissions.ts +25 -0
- package/shared/portal-session.ts +2 -0
- package/shared/social-auth.ts +5 -0
- package/shared/timezone.ts +21 -0
- package/shared/types/feature.ts +11 -0
- package/shared/types/sections/database.ts +3 -0
package/shared/core-feature.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PortalFeatureDefinition } from './types/feature'
|
|
2
2
|
|
|
3
|
+
const recipientNamePlaceholder = {
|
|
4
|
+
key: 'recipient_name',
|
|
5
|
+
labelKey: 'admin.email.placeholders.recipientName',
|
|
6
|
+
example: 'Alex'
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export const coreFeature: PortalFeatureDefinition = {
|
|
4
10
|
id: 'portal-core',
|
|
5
11
|
navigation: [
|
|
@@ -39,16 +45,21 @@ export const coreFeature: PortalFeatureDefinition = {
|
|
|
39
45
|
defaults: {
|
|
40
46
|
en: {
|
|
41
47
|
subject: 'Verify your {{brand_name}} email address',
|
|
42
|
-
body: '
|
|
43
|
-
footer:
|
|
48
|
+
body: 'Dear {{recipient_name}},\n\nWelcome to **{{brand_name}}**. To finish verifying your email address, enter the following code:\n\n`{{otp}}`\n\nOnce your email address is verified, you can continue setting up and using your account.',
|
|
49
|
+
footer:
|
|
50
|
+
'For your security, this code will expire soon and can only be used once. If you did not create or verify an account, you can safely ignore this email.'
|
|
44
51
|
},
|
|
45
52
|
nl: {
|
|
46
53
|
subject: 'Verifieer uw e-mailadres voor {{brand_name}}',
|
|
47
|
-
body: '
|
|
48
|
-
footer:
|
|
54
|
+
body: 'Beste {{recipient_name}},\n\nWelkom bij **{{brand_name}}**. Voer de volgende code in om de verificatie van uw e-mailadres af te ronden:\n\n`{{otp}}`\n\nNa de verificatie kunt u doorgaan met het instellen en gebruiken van uw account.',
|
|
55
|
+
footer:
|
|
56
|
+
'Voor uw veiligheid verloopt deze code binnenkort en kan deze maar één keer worden gebruikt. Heeft u geen account aangemaakt of geverifieerd? Dan kunt u deze e-mail veilig negeren.'
|
|
49
57
|
}
|
|
50
58
|
},
|
|
51
|
-
placeholders: [
|
|
59
|
+
placeholders: [
|
|
60
|
+
recipientNamePlaceholder,
|
|
61
|
+
{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }
|
|
62
|
+
]
|
|
52
63
|
},
|
|
53
64
|
{
|
|
54
65
|
id: 'sign-in',
|
|
@@ -56,16 +67,21 @@ export const coreFeature: PortalFeatureDefinition = {
|
|
|
56
67
|
defaults: {
|
|
57
68
|
en: {
|
|
58
69
|
subject: 'Your {{brand_name}} sign-in code',
|
|
59
|
-
body: '
|
|
60
|
-
footer:
|
|
70
|
+
body: 'Dear {{recipient_name}},\n\nWe received a request to sign in to your **{{brand_name}}** account. Use the code below to continue:\n\n`{{otp}}`\n\nReturn to the sign-in screen and enter this code to securely access your account.',
|
|
71
|
+
footer:
|
|
72
|
+
'This code will expire soon and can only be used once. If you did not try to sign in, you can ignore this email; no changes will be made to your account.'
|
|
61
73
|
},
|
|
62
74
|
nl: {
|
|
63
75
|
subject: 'Uw inlogcode voor {{brand_name}}',
|
|
64
|
-
body: '
|
|
65
|
-
footer:
|
|
76
|
+
body: 'Beste {{recipient_name}},\n\nWe hebben een verzoek ontvangen om in te loggen op uw **{{brand_name}}**-account. Gebruik de onderstaande code om verder te gaan:\n\n`{{otp}}`\n\nGa terug naar het inlogscherm en voer deze code in om veilig toegang te krijgen tot uw account.',
|
|
77
|
+
footer:
|
|
78
|
+
'Deze code verloopt binnenkort en kan maar één keer worden gebruikt. Heeft u niet geprobeerd in te loggen? Dan kunt u deze e-mail negeren; er wordt niets aan uw account gewijzigd.'
|
|
66
79
|
}
|
|
67
80
|
},
|
|
68
|
-
placeholders: [
|
|
81
|
+
placeholders: [
|
|
82
|
+
recipientNamePlaceholder,
|
|
83
|
+
{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }
|
|
84
|
+
]
|
|
69
85
|
},
|
|
70
86
|
{
|
|
71
87
|
id: 'forget-password',
|
|
@@ -73,16 +89,21 @@ export const coreFeature: PortalFeatureDefinition = {
|
|
|
73
89
|
defaults: {
|
|
74
90
|
en: {
|
|
75
91
|
subject: 'Reset your {{brand_name}} password',
|
|
76
|
-
body: '
|
|
77
|
-
footer:
|
|
92
|
+
body: 'Dear {{recipient_name}},\n\nWe received a request to reset the password for your **{{brand_name}}** account. Enter the following code on the password reset screen:\n\n`{{otp}}`\n\nAfter verification, you will be able to choose a new password.',
|
|
93
|
+
footer:
|
|
94
|
+
'For your security, this code will expire soon and can only be used once. If you did not request a password reset, ignore this email and your current password will remain unchanged.'
|
|
78
95
|
},
|
|
79
96
|
nl: {
|
|
80
97
|
subject: 'Stel uw wachtwoord voor {{brand_name}} opnieuw in',
|
|
81
|
-
body: '
|
|
82
|
-
footer:
|
|
98
|
+
body: 'Beste {{recipient_name}},\n\nWe hebben een verzoek ontvangen om het wachtwoord van uw **{{brand_name}}**-account opnieuw in te stellen. Voer de volgende code in op het scherm voor wachtwoordherstel:\n\n`{{otp}}`\n\nNa de verificatie kunt u een nieuw wachtwoord kiezen.',
|
|
99
|
+
footer:
|
|
100
|
+
'Voor uw veiligheid verloopt deze code binnenkort en kan deze maar één keer worden gebruikt. Heeft u geen wachtwoordherstel aangevraagd? Negeer dan deze e-mail; uw huidige wachtwoord blijft ongewijzigd.'
|
|
83
101
|
}
|
|
84
102
|
},
|
|
85
|
-
placeholders: [
|
|
103
|
+
placeholders: [
|
|
104
|
+
recipientNamePlaceholder,
|
|
105
|
+
{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }
|
|
106
|
+
]
|
|
86
107
|
},
|
|
87
108
|
{
|
|
88
109
|
id: 'change-email',
|
|
@@ -90,16 +111,21 @@ export const coreFeature: PortalFeatureDefinition = {
|
|
|
90
111
|
defaults: {
|
|
91
112
|
en: {
|
|
92
113
|
subject: 'Confirm your new {{brand_name}} email address',
|
|
93
|
-
body: '
|
|
94
|
-
footer:
|
|
114
|
+
body: 'Dear {{recipient_name}},\n\nYou are changing the email address connected to your **{{brand_name}}** account. Enter the following code to confirm that this new address belongs to you:\n\n`{{otp}}`\n\nAfter confirmation, future account messages will be sent to this address.',
|
|
115
|
+
footer:
|
|
116
|
+
'This code will expire soon and can only be used once. If you did not request this change, ignore this email and review your account security.'
|
|
95
117
|
},
|
|
96
118
|
nl: {
|
|
97
119
|
subject: 'Bevestig uw nieuwe e-mailadres voor {{brand_name}}',
|
|
98
|
-
body: '
|
|
99
|
-
footer:
|
|
120
|
+
body: 'Beste {{recipient_name}},\n\nU wijzigt het e-mailadres dat aan uw **{{brand_name}}**-account is gekoppeld. Voer de volgende code in om te bevestigen dat dit nieuwe adres van u is:\n\n`{{otp}}`\n\nNa de bevestiging worden toekomstige accountberichten naar dit adres verzonden.',
|
|
121
|
+
footer:
|
|
122
|
+
'Deze code verloopt binnenkort en kan maar één keer worden gebruikt. Heeft u deze wijziging niet aangevraagd? Negeer dan deze e-mail en controleer de beveiliging van uw account.'
|
|
100
123
|
}
|
|
101
124
|
},
|
|
102
|
-
placeholders: [
|
|
125
|
+
placeholders: [
|
|
126
|
+
recipientNamePlaceholder,
|
|
127
|
+
{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }
|
|
128
|
+
]
|
|
103
129
|
},
|
|
104
130
|
{
|
|
105
131
|
id: 'invitation',
|
|
@@ -107,38 +133,65 @@ export const coreFeature: PortalFeatureDefinition = {
|
|
|
107
133
|
defaults: {
|
|
108
134
|
en: {
|
|
109
135
|
subject: "You've been invited to join {{organization_name}}",
|
|
110
|
-
body: '{{inviter_name}} has invited you to join
|
|
111
|
-
footer:
|
|
136
|
+
body: 'Dear {{recipient_name}},\n\n**{{inviter_name}}** has invited you to join **{{organization_name}}** as **{{role}}**. By accepting, you will gain access to the organization and the features available for your role.\n\n[Review and accept the invitation]({{action_url}})\n\nWe look forward to welcoming you.',
|
|
137
|
+
footer:
|
|
138
|
+
'This personal invitation will expire soon. If you were not expecting it, you can safely ignore this email.'
|
|
112
139
|
},
|
|
113
140
|
nl: {
|
|
114
|
-
subject: '
|
|
115
|
-
body: '{{inviter_name}} heeft
|
|
116
|
-
footer:
|
|
141
|
+
subject: 'Je bent uitgenodigd voor {{organization_name}}',
|
|
142
|
+
body: 'Beste {{recipient_name}},\n\n**{{inviter_name}}** heeft je uitgenodigd om als **{{role}}** lid te worden van **{{organization_name}}**. Na acceptatie krijg je toegang tot de organisatie en de functies die bij je rol horen.\n\n[Uitnodiging bekijken en accepteren]({{action_url}})\n\nWe heten je graag welkom.',
|
|
143
|
+
footer:
|
|
144
|
+
'Deze persoonlijke uitnodiging verloopt binnenkort. Verwachtte je deze uitnodiging niet? Dan kun je deze e-mail veilig negeren.'
|
|
117
145
|
}
|
|
118
146
|
},
|
|
119
147
|
placeholders: [
|
|
148
|
+
recipientNamePlaceholder,
|
|
120
149
|
{ key: 'inviter_name', labelKey: 'admin.email.placeholders.inviterName', example: 'Alex Morgan' },
|
|
121
150
|
{ key: 'organization_name', labelKey: 'admin.email.placeholders.organizationName', example: 'Example Company' },
|
|
122
151
|
{ key: 'role', labelKey: 'admin.email.placeholders.role', example: 'member' },
|
|
123
152
|
{ key: 'action_url', labelKey: 'admin.email.placeholders.actionUrl', example: 'https://example.com/signup' }
|
|
124
153
|
]
|
|
125
154
|
},
|
|
155
|
+
{
|
|
156
|
+
id: 'personal-account-invitation',
|
|
157
|
+
labelKey: 'admin.email.messages.personalAccountInvitation',
|
|
158
|
+
defaults: {
|
|
159
|
+
en: {
|
|
160
|
+
subject: 'Set up your personal portal account',
|
|
161
|
+
body: 'Dear {{recipient_name}},\n\nA personal portal profile has been created for you. Set up your account to access your purchases, appointments, invoices, and shared files.\n\n[Set up your account]({{action_url}})\n\nUse the same email address that received this message.',
|
|
162
|
+
footer: 'This account setup link expires soon. If you did not expect this message, you can safely ignore it.'
|
|
163
|
+
},
|
|
164
|
+
nl: {
|
|
165
|
+
subject: 'Stel je persoonlijke portaalaccount in',
|
|
166
|
+
body: 'Beste {{recipient_name}},\n\nEr is een persoonlijk portaalprofiel voor je aangemaakt. Stel je account in om toegang te krijgen tot je aankopen, afspraken, facturen en gedeelde bestanden.\n\n[Je account instellen]({{action_url}})\n\nGebruik hetzelfde e-mailadres waarop je dit bericht hebt ontvangen.',
|
|
167
|
+
footer:
|
|
168
|
+
'Deze link om je account in te stellen verloopt binnenkort. Verwachtte je dit bericht niet? Dan kun je het veilig negeren.'
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
placeholders: [
|
|
172
|
+
recipientNamePlaceholder,
|
|
173
|
+
{ key: 'action_url', labelKey: 'admin.email.placeholders.actionUrl', example: 'https://example.com/signup' }
|
|
174
|
+
]
|
|
175
|
+
},
|
|
126
176
|
{
|
|
127
177
|
id: 'account-deletion',
|
|
128
178
|
labelKey: 'admin.email.messages.accountDeletion',
|
|
129
179
|
defaults: {
|
|
130
180
|
en: {
|
|
131
181
|
subject: 'Confirm account deletion',
|
|
132
|
-
body: '
|
|
133
|
-
footer:
|
|
182
|
+
body: 'Dear {{recipient_name}},\n\nWe received a request to permanently delete your account. Because this action cannot be undone, please confirm that you want to continue.\n\n[Confirm account deletion]({{action_url}})\n\nYour account will remain active until you complete this confirmation.',
|
|
183
|
+
footer:
|
|
184
|
+
'If you did not request account deletion, do not use the link. You can safely ignore this email and your account will remain active.'
|
|
134
185
|
},
|
|
135
186
|
nl: {
|
|
136
187
|
subject: 'Bevestig het verwijderen van uw account',
|
|
137
|
-
body: '
|
|
138
|
-
footer:
|
|
188
|
+
body: 'Beste {{recipient_name}},\n\nWe hebben een verzoek ontvangen om uw account definitief te verwijderen. Omdat dit niet ongedaan kan worden gemaakt, vragen we u te bevestigen dat u wilt doorgaan.\n\n[Verwijderen van account bevestigen]({{action_url}})\n\nUw account blijft actief totdat u deze bevestiging heeft voltooid.',
|
|
189
|
+
footer:
|
|
190
|
+
'Heeft u niet gevraagd om uw account te verwijderen? Gebruik de link dan niet. U kunt deze e-mail veilig negeren; uw account blijft actief.'
|
|
139
191
|
}
|
|
140
192
|
},
|
|
141
193
|
placeholders: [
|
|
194
|
+
recipientNamePlaceholder,
|
|
142
195
|
{ key: 'user_name', labelKey: 'admin.email.placeholders.userName', example: 'Alex Morgan' },
|
|
143
196
|
{
|
|
144
197
|
key: 'action_url',
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// ISO 3166-1 alpha-2 codes; display names are supplied by the browser's Intl data.
|
|
2
|
+
export const countryCodes =
|
|
3
|
+
'AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW'.split(
|
|
4
|
+
' '
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
export function browserCountry(languages: readonly string[]): string | undefined {
|
|
8
|
+
for (const language of languages) {
|
|
9
|
+
try {
|
|
10
|
+
const region = new Intl.Locale(language).region
|
|
11
|
+
if (region && countryCodes.includes(region)) {
|
|
12
|
+
return region
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
// Ignore unsupported language tags.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Languages with bundled portal interface translations. */
|
|
2
|
+
export const portalLanguageCodes = ['en', 'nl'] as const
|
|
3
|
+
export type PortalLanguage = (typeof portalLanguageCodes)[number]
|
|
4
|
+
export const portalLanguages = [
|
|
5
|
+
{ value: 'en' as const, label: '🇺🇸 English' },
|
|
6
|
+
{ value: 'nl' as const, label: '🇳🇱 Nederlands' }
|
|
7
|
+
]
|
package/shared/permissions.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { createAccessControl } from 'better-auth/plugins/access'
|
|
2
2
|
import { defaultStatements, adminAc } from 'better-auth/plugins/admin/access'
|
|
3
|
+
import {
|
|
4
|
+
defaultStatements as organizationDefaultStatements,
|
|
5
|
+
adminAc as organizationAdminAc,
|
|
6
|
+
memberAc as organizationMemberAc,
|
|
7
|
+
ownerAc as organizationOwnerAc
|
|
8
|
+
} from 'better-auth/plugins/organization/access'
|
|
3
9
|
|
|
4
10
|
// Core portal statements. Feature authorization is owned by each feature policy.
|
|
5
11
|
export const statement = {
|
|
@@ -24,3 +30,22 @@ export const admin = ac.newRole({
|
|
|
24
30
|
|
|
25
31
|
// Export the access control instance and roles
|
|
26
32
|
export { ac }
|
|
33
|
+
|
|
34
|
+
export const organizationStatement = {
|
|
35
|
+
...organizationDefaultStatements,
|
|
36
|
+
apiKey: ['create', 'read', 'update', 'delete']
|
|
37
|
+
} as const
|
|
38
|
+
|
|
39
|
+
export const organizationAc = createAccessControl(organizationStatement)
|
|
40
|
+
export const organizationOwner = organizationAc.newRole({
|
|
41
|
+
...organizationOwnerAc.statements,
|
|
42
|
+
apiKey: ['create', 'read', 'update', 'delete']
|
|
43
|
+
})
|
|
44
|
+
export const organizationAdmin = organizationAc.newRole({
|
|
45
|
+
...organizationAdminAc.statements,
|
|
46
|
+
apiKey: ['create', 'read', 'update', 'delete']
|
|
47
|
+
})
|
|
48
|
+
export const organizationMember = organizationAc.newRole({
|
|
49
|
+
...organizationMemberAc.statements,
|
|
50
|
+
apiKey: []
|
|
51
|
+
})
|
package/shared/portal-session.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Named zones retain daylight-saving rules; offsets do not. */
|
|
2
|
+
export const DEFAULT_TIMEZONE = 'Europe/Amsterdam'
|
|
3
|
+
export const isValidTimezone = (value: string): boolean => {
|
|
4
|
+
if (!value || /^[+-]/.test(value)) {
|
|
5
|
+
return false
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
new Intl.DateTimeFormat('en', { timeZone: value }).format()
|
|
9
|
+
return true
|
|
10
|
+
} catch {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const resolveTimezones = (input: {
|
|
15
|
+
providerTimezone: string
|
|
16
|
+
clientTimezone?: string | null
|
|
17
|
+
userTimezone?: string | null
|
|
18
|
+
}) => {
|
|
19
|
+
const schedulingTimezone = input.clientTimezone || input.providerTimezone
|
|
20
|
+
return { ...input, schedulingTimezone, displayTimezone: input.userTimezone || schedulingTimezone }
|
|
21
|
+
}
|
package/shared/types/feature.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type PortalAudience =
|
|
|
7
7
|
| 'providerAdmin'
|
|
8
8
|
| 'clientAuthenticated'
|
|
9
9
|
| 'clientAdmin'
|
|
10
|
+
| 'clientOrganizationAdmin'
|
|
10
11
|
export type PortalOrganizationRole = 'owner' | 'admin' | 'member'
|
|
11
12
|
export type PortalOrganizationType = 'PROVIDER' | 'CLIENT'
|
|
12
13
|
|
|
@@ -58,6 +59,7 @@ export interface PortalModuleContribution {
|
|
|
58
59
|
to: string
|
|
59
60
|
routePrefixes: string[]
|
|
60
61
|
audiences: PortalAudience[]
|
|
62
|
+
navigationAudiences?: PortalAudience[]
|
|
61
63
|
order?: number
|
|
62
64
|
menuItems?: readonly PortalModuleMenuItem[]
|
|
63
65
|
}
|
|
@@ -117,6 +119,13 @@ export interface PortalEmailDefinition {
|
|
|
117
119
|
placeholders: readonly PortalEmailPlaceholder[]
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
export interface PortalApiScopeDefinition {
|
|
123
|
+
id: string
|
|
124
|
+
action: string
|
|
125
|
+
labelKey: string
|
|
126
|
+
descriptionKey?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
export interface PortalFeatureDefinition<Action extends string = string> {
|
|
121
130
|
id: string
|
|
122
131
|
navigation?: readonly PortalNavigationItem[]
|
|
@@ -126,5 +135,7 @@ export interface PortalFeatureDefinition<Action extends string = string> {
|
|
|
126
135
|
surfaces?: readonly PortalSurfaceContribution[]
|
|
127
136
|
clientIntegration?: PortalClientIntegration
|
|
128
137
|
emails?: readonly PortalEmailDefinition[]
|
|
138
|
+
/** API capabilities this module intentionally exposes to organization API keys. */
|
|
139
|
+
apiScopes?: readonly PortalApiScopeDefinition[]
|
|
129
140
|
policy: PortalFeaturePolicy<Action>
|
|
130
141
|
}
|