@vertesia/ui 1.6.0-dev.20261009.095813Z → 1.6.0-dev.20261014.015055Z
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/lib/boot/index.d.ts +12 -1
- package/lib/boot/index.d.ts.map +1 -1
- package/lib/boot/index.js +65 -10
- package/lib/boot/index.js.map +1 -1
- package/lib/boot/text.d.ts +13 -0
- package/lib/boot/text.d.ts.map +1 -0
- package/lib/boot/text.js +68 -0
- package/lib/boot/text.js.map +1 -0
- package/lib/features/agent/PayloadBuilder.d.ts +6 -0
- package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +6 -4
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js +58 -0
- package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js.map +1 -1
- package/lib/features/agent/chat/SkillWidgetProvider.d.ts.map +1 -1
- package/lib/features/agent/chat/SkillWidgetProvider.js +3 -2
- package/lib/features/agent/chat/SkillWidgetProvider.js.map +1 -1
- package/lib/i18n/LanguageProvider.d.ts +9 -1
- package/lib/i18n/LanguageProvider.d.ts.map +1 -1
- package/lib/i18n/LanguageProvider.js +7 -4
- package/lib/i18n/LanguageProvider.js.map +1 -1
- package/lib/i18n/locales/ar.json +5 -1
- package/lib/i18n/locales/de.json +5 -1
- package/lib/i18n/locales/en.json +5 -1
- package/lib/i18n/locales/es.json +5 -1
- package/lib/i18n/locales/fr.json +5 -1
- package/lib/i18n/locales/it.json +5 -1
- package/lib/i18n/locales/ja.json +5 -1
- package/lib/i18n/locales/ko.json +5 -1
- package/lib/i18n/locales/pt.json +5 -1
- package/lib/i18n/locales/ru.json +5 -1
- package/lib/i18n/locales/tr.json +5 -1
- package/lib/i18n/locales/zh-TW.json +5 -1
- package/lib/i18n/locales/zh.json +5 -1
- package/lib/i18n/rtl.d.ts +4 -2
- package/lib/i18n/rtl.d.ts.map +1 -1
- package/lib/i18n/rtl.js +4 -2
- package/lib/i18n/rtl.js.map +1 -1
- package/lib/session/UserSessionProvider.d.ts.map +1 -1
- package/lib/session/UserSessionProvider.js +10 -4
- package/lib/session/UserSessionProvider.js.map +1 -1
- package/lib/session/auth/oauth.d.ts.map +1 -1
- package/lib/session/auth/oauth.js +15 -11
- package/lib/session/auth/oauth.js.map +1 -1
- package/lib/shell/VertesiaShell.d.ts +7 -2
- package/lib/shell/VertesiaShell.d.ts.map +1 -1
- package/lib/shell/VertesiaShell.js +2 -2
- package/lib/shell/VertesiaShell.js.map +1 -1
- package/lib/shell/login/InviteAcceptModal.d.ts.map +1 -1
- package/lib/shell/login/InviteAcceptModal.js +83 -42
- package/lib/shell/login/InviteAcceptModal.js.map +1 -1
- package/lib/vertesia-ui-boot.js +52 -15
- package/lib/vertesia-ui-boot.js.map +1 -1
- package/lib/vertesia-ui-core.js +43 -43
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-features.js +334 -334
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.B1Nd8-dE.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B1Nd8-dE.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.B2Z7mx1x.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B2Z7mx1x.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.B756-YtS.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B756-YtS.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BICdgAEn.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BICdgAEn.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.B_yx0c2A.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B_yx0c2A.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BwPrGiQn.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BwPrGiQn.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CfQbOHm3.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CfQbOHm3.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DMISlF16.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DMISlF16.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.D_WA36Yq.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.D_WA36Yq.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Dp3KuMs-.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Dp3KuMs-.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DsioKoQ3.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DsioKoQ3.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.QEIqEPlv.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.QEIqEPlv.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +2 -2
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-layout.js +15 -15
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +44 -44
- package/lib/vertesia-ui-rich-text.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +51 -51
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +51 -51
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +20 -20
- package/src/boot/index.ts +74 -11
- package/src/boot/text.ts +80 -0
- package/src/features/agent/chat/ModernAgentConversation.tsx +7 -4
- package/src/features/agent/chat/ModernAgentOutput/SummaryConversation.ts +60 -0
- package/src/features/agent/chat/SkillWidgetProvider.tsx +3 -2
- package/src/i18n/LanguageProvider.tsx +16 -4
- package/src/i18n/locales/ar.json +5 -1
- package/src/i18n/locales/de.json +5 -1
- package/src/i18n/locales/en.json +5 -1
- package/src/i18n/locales/es.json +5 -1
- package/src/i18n/locales/fr.json +5 -1
- package/src/i18n/locales/it.json +5 -1
- package/src/i18n/locales/ja.json +5 -1
- package/src/i18n/locales/ko.json +5 -1
- package/src/i18n/locales/pt.json +5 -1
- package/src/i18n/locales/ru.json +5 -1
- package/src/i18n/locales/tr.json +5 -1
- package/src/i18n/locales/zh-TW.json +5 -1
- package/src/i18n/locales/zh.json +5 -1
- package/src/i18n/rtl.ts +5 -2
- package/src/session/UserSessionProvider.tsx +11 -4
- package/src/session/auth/oauth.ts +26 -12
- package/src/shell/VertesiaShell.tsx +8 -2
- package/src/shell/login/InviteAcceptModal.tsx +112 -65
- package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js.map +0 -1
package/src/i18n/locales/ko.json
CHANGED
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"agent.toggleRightSidebar": "오른쪽 사이드바 전환",
|
|
252
252
|
"agent.typeDifferentResponse": "또는 다른 응답을 입력하세요...",
|
|
253
253
|
"agent.typeYourMessage": "메시지를 입력하세요...",
|
|
254
|
-
"agent.typeYourResponse": "응답을 입력하세요...",
|
|
254
|
+
"agent.typeYourResponse": "또는 다른 응답을 입력하세요...",
|
|
255
255
|
"agent.unableToOpenPrint": "인쇄 미리보기를 열 수 없습니다",
|
|
256
256
|
"agent.unknownError": "알 수 없는 오류",
|
|
257
257
|
"agent.unknownTime": "알 수 없는 시간",
|
|
@@ -379,6 +379,10 @@
|
|
|
379
379
|
"layout.searchPlaceholder": "검색...",
|
|
380
380
|
"layout.toggleNavigationMenu": "탐색 메뉴 전환",
|
|
381
381
|
"login.accept": "수락",
|
|
382
|
+
"login.inviteActionFailed": "이 초대를 업데이트할 수 없습니다. 목록을 새로 고쳤습니다.",
|
|
383
|
+
"login.inviteRole": "역할: {{role}}",
|
|
384
|
+
"login.invitedBy": "초대한 사람: {{name}}",
|
|
385
|
+
"login.invitesIntro": "다음 계정에 참여하도록 초대받았습니다. 각 초대를 수락하거나 거절하세요.",
|
|
382
386
|
"login.reject": "거부",
|
|
383
387
|
"login.reviewInvites": "초대 검토",
|
|
384
388
|
"login.terminal.account": "계정",
|
package/src/i18n/locales/pt.json
CHANGED
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
"agent.toggleRightSidebar": "Alternar painel lateral",
|
|
254
254
|
"agent.typeDifferentResponse": "Ou digite uma resposta diferente...",
|
|
255
255
|
"agent.typeYourMessage": "Digite sua mensagem...",
|
|
256
|
-
"agent.typeYourResponse": "
|
|
256
|
+
"agent.typeYourResponse": "Ou digite uma resposta diferente...",
|
|
257
257
|
"agent.unableToOpenPrint": "Não foi possível abrir a visualização de impressão",
|
|
258
258
|
"agent.unknownError": "Erro desconhecido",
|
|
259
259
|
"agent.unknownTime": "Hora desconhecida",
|
|
@@ -381,6 +381,10 @@
|
|
|
381
381
|
"layout.searchPlaceholder": "Pesquisar...",
|
|
382
382
|
"layout.toggleNavigationMenu": "Alternar menu de navegação",
|
|
383
383
|
"login.accept": "Aceitar",
|
|
384
|
+
"login.inviteActionFailed": "Não foi possível atualizar este convite. A lista foi atualizada.",
|
|
385
|
+
"login.inviteRole": "Função: {{role}}",
|
|
386
|
+
"login.invitedBy": "Convidado por {{name}}",
|
|
387
|
+
"login.invitesIntro": "Você foi convidado a participar das seguintes contas. Aceite ou recuse cada convite.",
|
|
384
388
|
"login.reject": "Rejeitar",
|
|
385
389
|
"login.reviewInvites": "Revisar convites",
|
|
386
390
|
"login.terminal.account": "Conta",
|
package/src/i18n/locales/ru.json
CHANGED
|
@@ -257,7 +257,7 @@
|
|
|
257
257
|
"agent.toggleRightSidebar": "Переключить правую панель",
|
|
258
258
|
"agent.typeDifferentResponse": "Или введите другой ответ...",
|
|
259
259
|
"agent.typeYourMessage": "Введите сообщение...",
|
|
260
|
-
"agent.typeYourResponse": "
|
|
260
|
+
"agent.typeYourResponse": "Или введите другой ответ...",
|
|
261
261
|
"agent.unableToOpenPrint": "Не удалось открыть предпросмотр печати",
|
|
262
262
|
"agent.unknownError": "Неизвестная ошибка",
|
|
263
263
|
"agent.unknownTime": "Неизвестное время",
|
|
@@ -385,6 +385,10 @@
|
|
|
385
385
|
"layout.searchPlaceholder": "Поиск...",
|
|
386
386
|
"layout.toggleNavigationMenu": "Переключить меню навигации",
|
|
387
387
|
"login.accept": "Принять",
|
|
388
|
+
"login.inviteActionFailed": "Не удалось обновить это приглашение. Список обновлён.",
|
|
389
|
+
"login.inviteRole": "Роль: {{role}}",
|
|
390
|
+
"login.invitedBy": "Пригласил(а): {{name}}",
|
|
391
|
+
"login.invitesIntro": "Вас пригласили присоединиться к следующим аккаунтам. Примите или отклоните каждое приглашение.",
|
|
388
392
|
"login.reject": "Отклонить",
|
|
389
393
|
"login.reviewInvites": "Просмотреть приглашения",
|
|
390
394
|
"login.terminal.account": "Аккаунт",
|
package/src/i18n/locales/tr.json
CHANGED
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"agent.toggleRightSidebar": "Sağ kenar çubuğunu aç/kapat",
|
|
252
252
|
"agent.typeDifferentResponse": "Ya da farklı bir yanıt yazın...",
|
|
253
253
|
"agent.typeYourMessage": "Mesajınızı yazın...",
|
|
254
|
-
"agent.typeYourResponse": "
|
|
254
|
+
"agent.typeYourResponse": "Ya da farklı bir yanıt yazın...",
|
|
255
255
|
"agent.unableToOpenPrint": "Yazdırma önizlemesi açılamadı",
|
|
256
256
|
"agent.unknownError": "Bilinmeyen hata",
|
|
257
257
|
"agent.unknownTime": "Bilinmeyen zaman",
|
|
@@ -379,6 +379,10 @@
|
|
|
379
379
|
"layout.searchPlaceholder": "Ara...",
|
|
380
380
|
"layout.toggleNavigationMenu": "Gezinme menüsünü aç veya kapat",
|
|
381
381
|
"login.accept": "Kabul Et",
|
|
382
|
+
"login.inviteActionFailed": "Bu davet güncellenemedi. Liste yenilendi.",
|
|
383
|
+
"login.inviteRole": "Rol: {{role}}",
|
|
384
|
+
"login.invitedBy": "Davet eden: {{name}}",
|
|
385
|
+
"login.invitesIntro": "Aşağıdaki hesaplara katılmaya davet edildiniz. Her daveti kabul edin veya reddedin.",
|
|
382
386
|
"login.reject": "Reddet",
|
|
383
387
|
"login.reviewInvites": "Davetleri İncele",
|
|
384
388
|
"login.terminal.account": "Hesap",
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"agent.toggleRightSidebar": "切換右側邊欄",
|
|
252
252
|
"agent.typeDifferentResponse": "或輸入其他回覆...",
|
|
253
253
|
"agent.typeYourMessage": "輸入訊息...",
|
|
254
|
-
"agent.typeYourResponse": "
|
|
254
|
+
"agent.typeYourResponse": "或輸入其他回覆...",
|
|
255
255
|
"agent.unableToOpenPrint": "無法開啟列印預覽",
|
|
256
256
|
"agent.unknownError": "未知錯誤",
|
|
257
257
|
"agent.unknownTime": "未知時間",
|
|
@@ -379,6 +379,10 @@
|
|
|
379
379
|
"layout.searchPlaceholder": "搜尋...",
|
|
380
380
|
"layout.toggleNavigationMenu": "切換導覽選單",
|
|
381
381
|
"login.accept": "接受",
|
|
382
|
+
"login.inviteActionFailed": "無法更新此邀請。清單已重新整理。",
|
|
383
|
+
"login.inviteRole": "角色:{{role}}",
|
|
384
|
+
"login.invitedBy": "邀請人:{{name}}",
|
|
385
|
+
"login.invitesIntro": "您已受邀加入以下帳戶。請接受或拒絕每個邀請。",
|
|
382
386
|
"login.reject": "拒絕",
|
|
383
387
|
"login.reviewInvites": "查看邀請",
|
|
384
388
|
"login.terminal.account": "帳戶",
|
package/src/i18n/locales/zh.json
CHANGED
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"agent.toggleRightSidebar": "切换右侧边栏",
|
|
252
252
|
"agent.typeDifferentResponse": "或输入其他回复...",
|
|
253
253
|
"agent.typeYourMessage": "输入消息...",
|
|
254
|
-
"agent.typeYourResponse": "
|
|
254
|
+
"agent.typeYourResponse": "或输入其他回复...",
|
|
255
255
|
"agent.unableToOpenPrint": "无法打开打印预览",
|
|
256
256
|
"agent.unknownError": "未知错误",
|
|
257
257
|
"agent.unknownTime": "未知时间",
|
|
@@ -379,6 +379,10 @@
|
|
|
379
379
|
"layout.searchPlaceholder": "搜索...",
|
|
380
380
|
"layout.toggleNavigationMenu": "切换导航菜单",
|
|
381
381
|
"login.accept": "接受",
|
|
382
|
+
"login.inviteActionFailed": "无法更新此邀请。列表已刷新。",
|
|
383
|
+
"login.inviteRole": "角色:{{role}}",
|
|
384
|
+
"login.invitedBy": "邀请人:{{name}}",
|
|
385
|
+
"login.invitesIntro": "您已被邀请加入以下账户。请接受或拒绝每个邀请。",
|
|
382
386
|
"login.reject": "拒绝",
|
|
383
387
|
"login.reviewInvites": "查看邀请",
|
|
384
388
|
"login.terminal.account": "账户",
|
package/src/i18n/rtl.ts
CHANGED
|
@@ -29,6 +29,9 @@ export const SUPPORTED_LANGUAGES = [
|
|
|
29
29
|
|
|
30
30
|
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
|
31
31
|
|
|
32
|
+
/** Where `LanguageProvider` persists the chosen language; the boot runtime reads it too. */
|
|
33
|
+
export const LANGUAGE_STORAGE_KEY = 'vertesia-ui-language';
|
|
34
|
+
|
|
32
35
|
export const RTL_LANGUAGES: ReadonlySet<string> = new Set(['ar', 'he', 'fa', 'ur']);
|
|
33
36
|
|
|
34
37
|
export function isRTL(lng?: string): boolean {
|
|
@@ -42,8 +45,8 @@ export function isRTL(lng?: string): boolean {
|
|
|
42
45
|
* 2. base-language fallback (`pt-BR` -> `pt`)
|
|
43
46
|
* 3. `'en'` fallback
|
|
44
47
|
*
|
|
45
|
-
* The
|
|
46
|
-
*
|
|
48
|
+
* The pre-React boot runtime (`../boot/index.ts`) reimplements this same logic
|
|
49
|
+
* in its inline script, which runs before any bundle loads.
|
|
47
50
|
*/
|
|
48
51
|
export function resolveLanguage(raw: string | undefined | null): SupportedLanguage {
|
|
49
52
|
if (!raw) return 'en';
|
|
@@ -280,10 +280,17 @@ export function UserSessionProvider({ children, loadOnboardingStatus = false }:
|
|
|
280
280
|
})
|
|
281
281
|
.catch((err) => {
|
|
282
282
|
if (surfaceAuthError(err)) return;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
// A customer-domain user without an invite is an expected sign-in outcome, already
|
|
284
|
+
// logged at warn where the 403 is received.
|
|
285
|
+
const inviteRequired =
|
|
286
|
+
err instanceof Error &&
|
|
287
|
+
err.message === 'Customer-domain user requires an invite to join';
|
|
288
|
+
if (!inviteRequired) {
|
|
289
|
+
console.error('Unexpected failure while fetching a user token', err);
|
|
290
|
+
Env.logger.error('Unexpected failure while fetching a user token', {
|
|
291
|
+
vertesia: { account_id: selectedAccount, project_id: selectedProject, error: err },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
287
294
|
session.logout();
|
|
288
295
|
session.isLoading = false;
|
|
289
296
|
session.authError = err;
|
|
@@ -13,7 +13,14 @@ const MAX_TRANSACTION_AGE = 10 * 60_000;
|
|
|
13
13
|
let pending: Promise<string> | undefined;
|
|
14
14
|
// Long-lived credentials never enter browser storage. A reload resumes via the broker when access expires.
|
|
15
15
|
let refreshSession:
|
|
16
|
-
| {
|
|
16
|
+
| {
|
|
17
|
+
token: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
issuer: string;
|
|
20
|
+
resource?: string;
|
|
21
|
+
tokenEndpoint: string;
|
|
22
|
+
revocationEndpoint?: string;
|
|
23
|
+
}
|
|
17
24
|
| undefined;
|
|
18
25
|
|
|
19
26
|
export class OAuthLoginError extends Error {}
|
|
@@ -61,6 +68,7 @@ interface Transaction {
|
|
|
61
68
|
clientId: string;
|
|
62
69
|
redirectUri: string;
|
|
63
70
|
issuer: string;
|
|
71
|
+
resource?: string;
|
|
64
72
|
target: string;
|
|
65
73
|
rememberedScope?: ScopeSelection;
|
|
66
74
|
retryScope?: ScopeSelection;
|
|
@@ -90,6 +98,12 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
90
98
|
const redirectUri = redirect.toString();
|
|
91
99
|
if (new URL(redirectUri).origin !== window.location.origin) throw new Error('OAuth callback must be same-origin');
|
|
92
100
|
const issuer = (config.issuer ? httpsUrl(config.issuer).toString() : Env.endpoints.sts).replace(/\/+$/, '');
|
|
101
|
+
// A token is issued for one audience. The default audience follows the issuer, which is already part of every
|
|
102
|
+
// stored credential's identity; a configured one is recorded alongside it, so changing it never reuses a token,
|
|
103
|
+
// refresh session or login transaction issued for the previous audience.
|
|
104
|
+
const configuredResource = config.resource ? httpsUrl(config.resource).toString() : undefined;
|
|
105
|
+
const sameClient = (stored: { clientId: string; issuer: string; resource?: string }) =>
|
|
106
|
+
stored.clientId === clientId && stored.issuer === issuer && stored.resource === configuredResource;
|
|
93
107
|
let current = new URL(window.location.href);
|
|
94
108
|
let { accountId, projectId } = resolveAuthSelection(current);
|
|
95
109
|
const callback = current.origin + current.pathname === new URL(redirectUri).origin + new URL(redirectUri).pathname;
|
|
@@ -105,9 +119,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
105
119
|
if (
|
|
106
120
|
!transaction ||
|
|
107
121
|
transaction.state !== state ||
|
|
108
|
-
transaction
|
|
122
|
+
!sameClient(transaction) ||
|
|
109
123
|
transaction.redirectUri !== redirectUri ||
|
|
110
|
-
transaction.issuer !== issuer ||
|
|
111
124
|
Date.now() - transaction.created > MAX_TRANSACTION_AGE ||
|
|
112
125
|
transaction.created > Date.now()
|
|
113
126
|
) {
|
|
@@ -133,25 +146,21 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
133
146
|
token: string;
|
|
134
147
|
clientId: string;
|
|
135
148
|
issuer: string;
|
|
149
|
+
resource?: string;
|
|
136
150
|
refreshToken?: string;
|
|
137
151
|
};
|
|
138
152
|
const claims = jwtDecode<AuthTokenPayload>(cached.token);
|
|
139
153
|
if (
|
|
140
154
|
config.offlineAccess &&
|
|
141
|
-
cached
|
|
142
|
-
cached.issuer === issuer &&
|
|
155
|
+
sameClient(cached) &&
|
|
143
156
|
(!projectId || claims.project?.id === projectId) &&
|
|
144
157
|
(!accountId || claims.account?.id === accountId)
|
|
145
158
|
)
|
|
146
|
-
refreshCredential =
|
|
147
|
-
refreshSession?.clientId === clientId && refreshSession.issuer === issuer
|
|
148
|
-
? refreshSession.token
|
|
149
|
-
: undefined;
|
|
159
|
+
refreshCredential = refreshSession && sameClient(refreshSession) ? refreshSession.token : undefined;
|
|
150
160
|
// Discard credentials persisted by older SDK versions rather than loading them into the new session.
|
|
151
161
|
if (cached.refreshToken) sessionStorage.removeItem(TOKEN_KEY);
|
|
152
162
|
if (
|
|
153
|
-
cached
|
|
154
|
-
cached.issuer === issuer &&
|
|
163
|
+
sameClient(cached) &&
|
|
155
164
|
!forceRefresh &&
|
|
156
165
|
claims.exp > Date.now() / 1000 + (config.offlineAccess ? 300 : 30) &&
|
|
157
166
|
(!projectId || claims.project?.id === projectId) &&
|
|
@@ -179,7 +188,7 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
179
188
|
const tokenEndpoint = httpsUrl(metadata.token_endpoint);
|
|
180
189
|
const revocationEndpoint =
|
|
181
190
|
metadata.revocation_endpoint === undefined ? undefined : httpsUrl(metadata.revocation_endpoint).toString();
|
|
182
|
-
const resource =
|
|
191
|
+
const resource = configuredResource ?? new URL(metadata.issuer).toString();
|
|
183
192
|
if (refreshCredential) {
|
|
184
193
|
try {
|
|
185
194
|
const response = await fetch(tokenEndpoint, {
|
|
@@ -215,12 +224,14 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
215
224
|
token: grant.access_token,
|
|
216
225
|
clientId,
|
|
217
226
|
issuer,
|
|
227
|
+
resource: configuredResource,
|
|
218
228
|
}),
|
|
219
229
|
);
|
|
220
230
|
refreshSession = {
|
|
221
231
|
token: grant.refresh_token ?? refreshCredential,
|
|
222
232
|
clientId,
|
|
223
233
|
issuer,
|
|
234
|
+
resource: configuredResource,
|
|
224
235
|
tokenEndpoint: tokenEndpoint.toString(),
|
|
225
236
|
revocationEndpoint,
|
|
226
237
|
};
|
|
@@ -262,6 +273,7 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
262
273
|
token: grant.access_token,
|
|
263
274
|
clientId,
|
|
264
275
|
issuer,
|
|
276
|
+
resource: configuredResource,
|
|
265
277
|
}),
|
|
266
278
|
);
|
|
267
279
|
refreshSession =
|
|
@@ -270,6 +282,7 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
270
282
|
token: grant.refresh_token,
|
|
271
283
|
clientId,
|
|
272
284
|
issuer,
|
|
285
|
+
resource: configuredResource,
|
|
273
286
|
tokenEndpoint: tokenEndpoint.toString(),
|
|
274
287
|
revocationEndpoint,
|
|
275
288
|
}
|
|
@@ -307,6 +320,7 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
307
320
|
clientId,
|
|
308
321
|
redirectUri,
|
|
309
322
|
issuer,
|
|
323
|
+
resource: configuredResource,
|
|
310
324
|
target: current.toString(),
|
|
311
325
|
rememberedScope:
|
|
312
326
|
accountId && explicitScope?.projectId === undefined && (!explicitScope?.accountId || projectId)
|
|
@@ -28,8 +28,13 @@ export interface VertesiaShellProps {
|
|
|
28
28
|
suppressSignInErrorPrefixes?: string | string[];
|
|
29
29
|
/** Use an already-issued Vertesia token instead of starting the normal sign-in flow. */
|
|
30
30
|
authToken?: string;
|
|
31
|
-
/**
|
|
31
|
+
/** Language used only when there is neither a saved choice nor a browser language. */
|
|
32
32
|
defaultLanguage?: SupportedLanguage;
|
|
33
|
+
/**
|
|
34
|
+
* Pin the language the shell renders in, whatever the saved choice or browser language.
|
|
35
|
+
* Embedded apps still receive the user's own language (`useLanguage().preferredLanguage`).
|
|
36
|
+
*/
|
|
37
|
+
language?: SupportedLanguage;
|
|
33
38
|
}
|
|
34
39
|
export function VertesiaShell({
|
|
35
40
|
children,
|
|
@@ -41,6 +46,7 @@ export function VertesiaShell({
|
|
|
41
46
|
suppressSignInErrorPrefixes,
|
|
42
47
|
authToken,
|
|
43
48
|
defaultLanguage,
|
|
49
|
+
language,
|
|
44
50
|
authScreens,
|
|
45
51
|
branding,
|
|
46
52
|
}: VertesiaShellProps) {
|
|
@@ -48,7 +54,7 @@ export function VertesiaShell({
|
|
|
48
54
|
const brandedContent = (
|
|
49
55
|
<TypeRegistryProvider>
|
|
50
56
|
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
|
51
|
-
<LanguageProvider defaultLanguage={defaultLanguage}>
|
|
57
|
+
<LanguageProvider defaultLanguage={defaultLanguage} language={language}>
|
|
52
58
|
<LanguageBoundI18nProvider>
|
|
53
59
|
<AppBrandingBoundary branding={branding}>
|
|
54
60
|
<IframeAppContextSync />
|
|
@@ -2,7 +2,12 @@ import type { UserInviteToken } from '@vertesia/common';
|
|
|
2
2
|
import { Button, Modal, ModalBody, ModalTitle } from '@vertesia/ui/core';
|
|
3
3
|
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
4
4
|
import { useUserSession } from '@vertesia/ui/session';
|
|
5
|
-
import { useEffect, useState } from 'react';
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
// Legacy invites have no account data and cannot be shown or accepted here.
|
|
8
|
+
function withAccount(invites: UserInviteToken[]) {
|
|
9
|
+
return invites.filter((i) => i.data.account);
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
export function InviteAcceptModal() {
|
|
8
13
|
const session = useUserSession();
|
|
@@ -10,95 +15,137 @@ export function InviteAcceptModal() {
|
|
|
10
15
|
const { t } = useUITranslation();
|
|
11
16
|
const [showModal, setShowModal] = useState(false);
|
|
12
17
|
const [invites, setInvites] = useState<UserInviteToken[]>([]);
|
|
18
|
+
const [pendingId, setPendingId] = useState<string>();
|
|
19
|
+
const [error, setError] = useState<string>();
|
|
20
|
+
const [acceptedAccountIds, setAcceptedAccountIds] = useState<string[]>([]);
|
|
21
|
+
const isOpen = showModal && invites.length > 0;
|
|
22
|
+
|
|
23
|
+
const loadInvites = useCallback(async () => {
|
|
24
|
+
const valid = withAccount(await client.account.listInvites());
|
|
25
|
+
setInvites(valid);
|
|
26
|
+
return valid;
|
|
27
|
+
}, [client]);
|
|
13
28
|
|
|
14
29
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Filter out legacy invites that do not have account data
|
|
20
|
-
const notLegacyInvites = invites.filter((i) => i.data.account);
|
|
21
|
-
if (notLegacyInvites.length === 0) {
|
|
22
|
-
console.log('No valid invites found, closing modal');
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
// If we have valid invites, show the modal
|
|
26
|
-
console.log('Found valid invites', notLegacyInvites.length);
|
|
30
|
+
loadInvites()
|
|
31
|
+
.then((valid) => {
|
|
32
|
+
if (valid.length > 0) {
|
|
33
|
+
console.log('Found valid invites', valid.length);
|
|
27
34
|
setShowModal(true);
|
|
28
|
-
setInvites(notLegacyInvites);
|
|
29
|
-
} else {
|
|
30
|
-
console.log('No invites found, closing modal');
|
|
31
|
-
setShowModal(false);
|
|
32
35
|
}
|
|
33
36
|
})
|
|
34
37
|
.catch((err) => {
|
|
35
38
|
console.error('Error fetching invites', err);
|
|
36
39
|
});
|
|
37
|
-
}, [
|
|
40
|
+
}, [loadInvites]);
|
|
38
41
|
|
|
39
42
|
const closeModal = () => setShowModal(false);
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// Refreshing the session re-renders the app around the modal, so it waits until the modal has closed and
|
|
45
|
+
// then picks up every accepted invite at once.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (isOpen || acceptedAccountIds.length === 0) return;
|
|
48
|
+
setAcceptedAccountIds([]);
|
|
49
|
+
void (async () => {
|
|
50
|
+
try {
|
|
51
|
+
// The current token predates the new memberships and does not list the new accounts, so the
|
|
52
|
+
// server rejects reads of them until a fresh token is minted.
|
|
53
|
+
await session.refreshAuthToken();
|
|
54
|
+
await session.fetchAccounts();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('Failed to refresh the session after accepting invites', err);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Each account loads on its own, so one failure does not hide the others.
|
|
60
|
+
for (const accountId of new Set(acceptedAccountIds)) {
|
|
61
|
+
await session.fetchProjects(accountId).catch((err) => {
|
|
62
|
+
console.error('Failed to load projects for an accepted invite', err);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
}, [isOpen, acceptedAccountIds, session]);
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
const removeInvite = (inviteId: string) => setInvites((current) => current.filter((i) => i.id !== inviteId));
|
|
69
|
+
|
|
70
|
+
// An accept or reject can fail because the invite was already used, e.g. from another tab: show the server's list.
|
|
71
|
+
const recoverFromFailure = async (action: string, err: unknown) => {
|
|
72
|
+
console.error(`Failed to ${action} invite`, err);
|
|
73
|
+
setError(t('login.inviteActionFailed'));
|
|
74
|
+
return loadInvites().catch((reloadErr) => {
|
|
75
|
+
console.error('Error fetching invites', reloadErr);
|
|
76
|
+
return undefined;
|
|
77
|
+
});
|
|
54
78
|
};
|
|
55
79
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
const accept = async (invite: UserInviteToken) => {
|
|
81
|
+
setPendingId(invite.id);
|
|
82
|
+
setError(undefined);
|
|
83
|
+
try {
|
|
84
|
+
await client.account.acceptInvite(invite.id);
|
|
85
|
+
removeInvite(invite.id);
|
|
86
|
+
setAcceptedAccountIds((current) => [...current, invite.data.account.id]);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const remaining = await recoverFromFailure('accept', err);
|
|
89
|
+
// A gone invite was most likely accepted after all (e.g. the response was lost), so its account
|
|
90
|
+
// still needs loading. If it was rejected elsewhere instead, that load fails harmlessly.
|
|
91
|
+
if (remaining && !remaining.some((i) => i.id === invite.id)) {
|
|
92
|
+
setAcceptedAccountIds((current) => [...current, invite.data.account.id]);
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
setPendingId(undefined);
|
|
62
96
|
}
|
|
63
97
|
};
|
|
64
98
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
const reject = async (invite: UserInviteToken) => {
|
|
100
|
+
setPendingId(invite.id);
|
|
101
|
+
setError(undefined);
|
|
102
|
+
try {
|
|
103
|
+
await client.account.rejectInvite(invite.id);
|
|
104
|
+
removeInvite(invite.id);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
await recoverFromFailure('reject', err);
|
|
107
|
+
} finally {
|
|
108
|
+
setPendingId(undefined);
|
|
69
109
|
}
|
|
110
|
+
};
|
|
70
111
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
</div>
|
|
81
|
-
<div className="flex flex-col gap-4">
|
|
82
|
-
<Button size={'xs'} onClick={() => accept(invite)}>
|
|
83
|
-
{t('login.accept')}
|
|
84
|
-
</Button>{' '}
|
|
85
|
-
<Button size={'xs'} variant="secondary" onClick={() => reject(invite)}>
|
|
86
|
-
{t('login.reject')}
|
|
87
|
-
</Button>
|
|
88
|
-
</div>
|
|
112
|
+
const inviteList = invites.map((invite) => (
|
|
113
|
+
<div key={invite.id} className="flex flex-row w-full justify-between border rounded-sm px-2 py-2 ">
|
|
114
|
+
<div className="flex flex-col">
|
|
115
|
+
<div className="w-full font-semibold">{invite.data.account.name}</div>
|
|
116
|
+
{invite.data.project && <div className="w-full text-base">- {invite.data.project.name}</div>}
|
|
117
|
+
<div className="text-xs">{t('login.inviteRole', { role: invite.data.role })}</div>
|
|
118
|
+
{invite.data.invited_by && (
|
|
119
|
+
<div className="text-xs">{t('login.invitedBy', { name: invite.data.invited_by.name })}</div>
|
|
120
|
+
)}
|
|
89
121
|
</div>
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
<div className="flex flex-col gap-4">
|
|
123
|
+
<Button
|
|
124
|
+
size={'xs'}
|
|
125
|
+
isDisabled={!!pendingId}
|
|
126
|
+
isLoading={pendingId === invite.id}
|
|
127
|
+
onClick={() => void accept(invite)}
|
|
128
|
+
>
|
|
129
|
+
{t('login.accept')}
|
|
130
|
+
</Button>{' '}
|
|
131
|
+
<Button size={'xs'} variant="secondary" isDisabled={!!pendingId} onClick={() => void reject(invite)}>
|
|
132
|
+
{t('login.reject')}
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
));
|
|
92
137
|
|
|
93
138
|
return (
|
|
94
139
|
<div>
|
|
95
|
-
<Modal isOpen={
|
|
140
|
+
<Modal isOpen={isOpen} onClose={closeModal}>
|
|
96
141
|
<ModalTitle>{t('login.reviewInvites')}</ModalTitle>
|
|
97
142
|
<ModalBody>
|
|
98
|
-
<div className="text-sm pb-4">
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
<div className="text-sm pb-4">{t('login.invitesIntro')}</div>
|
|
144
|
+
{error && (
|
|
145
|
+
<div role="alert" className="text-sm text-destructive pb-4">
|
|
146
|
+
{error}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
102
149
|
{inviteList}
|
|
103
150
|
</ModalBody>
|
|
104
151
|
</Modal>
|