@meistrari/chat-nuxt 3.12.1 → 3.13.0-rc.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/dist/module.d.mts CHANGED
@@ -13,6 +13,20 @@ interface ChatNuxtModuleOptions {
13
13
  * required runtime config keys are unset.
14
14
  */
15
15
  validateConfig?: ChatNuxtConfigValidationMode;
16
+ /**
17
+ * Path, relative to `srcDir`, of a module default-exporting the host app's
18
+ * auth bridge built with `defineChatAuth()` (from `@meistrari/chat-nuxt/auth`).
19
+ * Setting it switches `#chat-auth` to the host implementation and lifts the
20
+ * `@meistrari/auth-nuxt` module requirement.
21
+ */
22
+ authComposable?: string;
23
+ /**
24
+ * Name of the cookie holding the bearer token the server proxy routes
25
+ * forward to the chat-api. Only read when `authComposable` is set; without
26
+ * it the proxies keep relying on `event.context.auth` populated by
27
+ * `@meistrari/auth-nuxt`.
28
+ */
29
+ authTokenCookie?: string;
16
30
  }
17
31
  declare const _default: any;
18
32
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "3.12.1-rc.0",
4
+ "version": "3.13.0-rc.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, mkdirSync, symlinkSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
- import { join, dirname } from 'node:path';
3
+ import { join, dirname, isAbsolute } from 'node:path';
4
4
  import { defineNuxtModule, createResolver, addTemplate, addTypeTemplate, addPlugin, addImportsDir, addComponent, addComponentsDir } from 'nuxt/kit';
5
5
  import { TOOL_ICON_SAFELIST } from '../dist/runtime/messages/tool-registry.js';
6
6
  export { ChatMessageStatus } from '../dist/runtime/messages/composables/chat-message-terminal.js';
@@ -193,7 +193,33 @@ function resolveChatAuthMode(runtimeConfig) {
193
193
  const telaAuth = publicConfig?.telaAuth;
194
194
  return telaAuth?.application?.enabled ? "application" : "first-party";
195
195
  }
196
- function buildChatAuthTemplate(mode) {
196
+ const HOST_AUTH_COMPOSABLE_EXTENSIONS = ["", ".ts", ".mts", ".js", ".mjs"];
197
+ function resolveHostAuthComposable(srcDir, authComposable) {
198
+ const base = isAbsolute(authComposable) ? authComposable : join(srcDir, authComposable);
199
+ for (const extension of HOST_AUTH_COMPOSABLE_EXTENSIONS) {
200
+ const candidate = `${base}${extension}`;
201
+ if (existsSync(candidate)) {
202
+ return candidate;
203
+ }
204
+ }
205
+ throw new Error(
206
+ `[@meistrari/chat-nuxt] authComposable not found: ${base}
207
+ Create it with:
208
+
209
+ import { defineChatAuth } from '@meistrari/chat-nuxt/auth'
210
+
211
+ export default defineChatAuth(() => ({
212
+ user, activeOrganization, logout, getToken, getAvailableOrganizations, switchOrganization,
213
+ }))
214
+
215
+ See docs/guides/host-auth.md in @meistrari/chat-nuxt.`
216
+ );
217
+ }
218
+ function buildChatAuthTemplate(mode, hostAuthComposablePath) {
219
+ if (mode === "host") {
220
+ return `export { default as useChatAuth } from ${JSON.stringify((hostAuthComposablePath ?? "").replace(/\\/g, "/"))}
221
+ `;
222
+ }
197
223
  if (mode === "application") {
198
224
  return `
199
225
  import { useTelaApplicationAuth } from '#imports'
@@ -333,10 +359,11 @@ const module$1 = defineNuxtModule({
333
359
  validateConfig: false
334
360
  },
335
361
  setup(options, nuxt) {
362
+ const hostAuthComposablePath = options.authComposable ? resolveHostAuthComposable(nuxt.options.srcDir, options.authComposable) : void 0;
336
363
  const hasAuthModule = (nuxt.options.modules ?? []).some(isAuthModuleEntry);
337
- if (!hasAuthModule) {
364
+ if (!hasAuthModule && !hostAuthComposablePath) {
338
365
  throw new Error(
339
- '[@meistrari/chat-nuxt] Missing required module "@meistrari/auth-nuxt". Install and enable it so chat auth composables and event.context.auth are available.'
366
+ `[@meistrari/chat-nuxt] Missing required module "@meistrari/auth-nuxt". Install and enable it so chat auth composables and event.context.auth are available, or set chatNuxt.authComposable to provide the host app's own auth bridge.`
340
367
  );
341
368
  }
342
369
  ensureIconifyPackages(nuxt.options.rootDir);
@@ -356,10 +383,14 @@ const module$1 = defineNuxtModule({
356
383
  );
357
384
  const telaBuildPath = resolveTelaBuildLayer();
358
385
  const runtimeAliases = resolveRuntimeAliases(telaBuildPath);
359
- const chatAuthMode = resolveChatAuthMode(nuxt.options.runtimeConfig);
386
+ const chatAuthMode = hostAuthComposablePath ? "host" : resolveChatAuthMode(nuxt.options.runtimeConfig);
387
+ console.info(
388
+ { mode: chatAuthMode, ...options.authComposable ? { authComposable: options.authComposable } : {} },
389
+ "[@meistrari/chat-nuxt] auth bridge resolved"
390
+ );
360
391
  const chatAuthTemplate = addTemplate({
361
392
  filename: "chat-nuxt/auth.mjs",
362
- getContents: () => buildChatAuthTemplate(chatAuthMode)
393
+ getContents: () => buildChatAuthTemplate(chatAuthMode, hostAuthComposablePath)
363
394
  });
364
395
  const chatAuthTypeTemplate = addTypeTemplate({
365
396
  filename: "chat-nuxt/chat-auth.d.ts",
@@ -412,8 +443,14 @@ const module$1 = defineNuxtModule({
412
443
  rc.chatNuxt = {
413
444
  chatApiUrl: existingChatNuxtRuntimeConfig?.chatApiUrl || rc.chatApiUrl,
414
445
  source: options.source || existingChatNuxtRuntimeConfig?.source,
415
- clientVersion: _chatNuxtPackageVersion
446
+ clientVersion: _chatNuxtPackageVersion,
447
+ authTokenCookie: hostAuthComposablePath ? options.authTokenCookie || existingChatNuxtRuntimeConfig?.authTokenCookie : void 0
416
448
  };
449
+ if (hostAuthComposablePath && !rc.chatNuxt.authTokenCookie) {
450
+ console.warn(
451
+ "[@meistrari/chat-nuxt] authComposable is set but authTokenCookie is not. Server proxy routes will only authenticate if the host populates event.context.auth.token through its own server middleware."
452
+ );
453
+ }
417
454
  validateRuntimeConfig(rc, options.validateConfig ?? false);
418
455
  nuxt.options.css = nuxt.options.css || [];
419
456
  for (const css of [
@@ -427,6 +464,14 @@ const module$1 = defineNuxtModule({
427
464
  }
428
465
  addPlugin(join(runtimeDir, "plugins/markstream"));
429
466
  addImportsDir(FEATURE_DIRS.map((feature) => join(runtimeDir, feature, "composables")));
467
+ const importsExtendNuxt = nuxt;
468
+ importsExtendNuxt.hook("imports:extend", (imports) => {
469
+ for (const entry of imports) {
470
+ if (entry.from?.startsWith(runtimeDir)) {
471
+ entry.priority ||= -100;
472
+ }
473
+ }
474
+ });
430
475
  addComponent({
431
476
  name: "MeistrariChatEmbed",
432
477
  filePath: join(runtimeDir, "embed/components/meistrari-chat-embed.vue"),
@@ -0,0 +1,37 @@
1
+ import type { Ref } from 'vue';
2
+ export type ChatAuthUser = {
3
+ id: string;
4
+ email?: string | null;
5
+ name?: string | null;
6
+ image?: string | null;
7
+ };
8
+ export type ChatAuthMemberUser = {
9
+ name: string;
10
+ email: string;
11
+ image?: string;
12
+ };
13
+ export type ChatAuthMember = {
14
+ id: string;
15
+ userId: string;
16
+ user: ChatAuthMemberUser;
17
+ };
18
+ export type ChatAuthOrganization = {
19
+ id: string;
20
+ name?: string;
21
+ logo?: string | null;
22
+ members?: ChatAuthMember[];
23
+ };
24
+ export type ChatAuthBridge = {
25
+ user: Ref<ChatAuthUser | null>;
26
+ activeOrganization: Ref<ChatAuthOrganization | null>;
27
+ logout: () => Promise<void>;
28
+ getToken: () => Promise<string | null | undefined>;
29
+ getAvailableOrganizations: () => Promise<ChatAuthOrganization[]>;
30
+ switchOrganization: (organizationId: string) => Promise<void>;
31
+ };
32
+ /**
33
+ * Identity helper that types a host app's auth bridge against the `#chat-auth`
34
+ * contract at the definition site. Default-export the result from the file
35
+ * referenced by the `chatNuxt.authComposable` module option.
36
+ */
37
+ export declare function defineChatAuth(bridge: () => ChatAuthBridge): () => ChatAuthBridge;
@@ -0,0 +1,3 @@
1
+ export function defineChatAuth(bridge) {
2
+ return bridge;
3
+ }
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { onClickOutside } from "@vueuse/core";
2
+ import { onClickOutside, useElementBounding, useWindowSize } from "@vueuse/core";
3
3
  import { useEmbedConfig } from "../../embed/composables/embed-config";
4
4
  import { useCitationPanel } from "../composables/citation-panel";
5
5
  import { useCitations } from "../composables/citations";
@@ -20,9 +20,55 @@ const status = computed(() => entry.value?.status.value ?? "loading");
20
20
  const meta = computed(() => entry.value?.meta.value ?? null);
21
21
  const displayLabel = computed(() => props.label?.trim() || meta.value?.identifier || meta.value?.title || "Documento");
22
22
  const rootEl = ref(null);
23
+ const popoverEl = ref(null);
23
24
  const popoverOpen = ref(false);
24
25
  onClickOutside(rootEl, () => {
25
26
  popoverOpen.value = false;
27
+ }, {
28
+ ignore: [popoverEl]
29
+ });
30
+ const POPOVER_WIDTH = 300;
31
+ const POPOVER_GAP = 6;
32
+ const VIEWPORT_PADDING = 8;
33
+ const triggerBounds = useElementBounding(rootEl);
34
+ const popoverBounds = useElementBounding(popoverEl);
35
+ const viewport = useWindowSize();
36
+ const popoverStyle = computed(() => {
37
+ const maxHeight = Math.max(
38
+ 0,
39
+ viewport.height.value - VIEWPORT_PADDING * 2
40
+ );
41
+ const maxLeft = Math.max(
42
+ VIEWPORT_PADDING,
43
+ viewport.width.value - POPOVER_WIDTH - VIEWPORT_PADDING
44
+ );
45
+ const left = Math.min(
46
+ Math.max(triggerBounds.left.value, VIEWPORT_PADDING),
47
+ maxLeft
48
+ );
49
+ const fitsAbove = triggerBounds.top.value >= popoverBounds.height.value + POPOVER_GAP + VIEWPORT_PADDING;
50
+ const fitsBelow = viewport.height.value - triggerBounds.bottom.value >= popoverBounds.height.value + POPOVER_GAP + VIEWPORT_PADDING;
51
+ const constrainedStyle = {
52
+ left: `${left}px`,
53
+ maxHeight: `${maxHeight}px`,
54
+ overflowY: "auto"
55
+ };
56
+ if (fitsAbove) {
57
+ return {
58
+ ...constrainedStyle,
59
+ bottom: `${viewport.height.value - triggerBounds.top.value + POPOVER_GAP}px`
60
+ };
61
+ }
62
+ if (fitsBelow) {
63
+ return {
64
+ ...constrainedStyle,
65
+ top: `${triggerBounds.bottom.value + POPOVER_GAP}px`
66
+ };
67
+ }
68
+ return {
69
+ ...constrainedStyle,
70
+ top: `${VIEWPORT_PADDING}px`
71
+ };
26
72
  });
27
73
  function togglePopover() {
28
74
  if (status.value === "loading")
@@ -99,88 +145,92 @@ const badgeClasses = computed(() => BADGE_TONE_CLASSES[meta.value?.badge?.tone ?
99
145
  @click="togglePopover"
100
146
  ><span :class="chipIcon" class="citation-ref-icon" />{{ displayLabel }}</button>
101
147
 
102
- <div
103
- v-if="popoverOpen"
104
- absolute bottom-full left-0 mb-6px z-50
105
- w-300px
106
- bg-white
107
- border-0.5px border-solid border-neutral-300
108
- rounded-12px p-12px
109
- shadow-lg
110
- cursor-default
111
- flex="~ col" gap-8px
112
- >
113
- <template v-if="status === 'unresolved'">
114
- <div flex items-start gap-6px>
115
- <span i-ph-warning-circle text-14px text-amber-600 mt-2px flex-shrink-0 />
116
- <p text-12px leading-16px text-neutral-700>
117
- Não foi possível verificar esta referência. Ela pode não existir ou não estar disponível para o seu perfil.
118
- </p>
119
- </div>
120
- <button
121
- type="button"
122
- self-start text-12px leading-16px text-neutral-700 underline cursor-pointer
123
- @click="retry"
124
- >
125
- Tentar novamente
126
- </button>
127
- </template>
128
-
129
- <template v-else-if="meta">
130
- <div flex="~ col" gap-2px>
131
- <p text-13px leading-18px font-580 text-neutral-900>
132
- {{ meta.title }}
133
- </p>
134
- <p v-if="meta.sourceLabel || meta.identifier" text-12px leading-16px text-neutral-600>
135
- {{ [meta.sourceLabel, meta.identifier].filter(Boolean).join(" \xB7 ") }}
136
- </p>
137
- <p v-if="formattedDate" text-12px leading-16px text-neutral-500>
138
- {{ meta.dateLabel ? `${meta.dateLabel}: ` : "" }}{{ formattedDate }}
139
- </p>
140
- </div>
141
-
142
- <span
143
- v-if="meta.badge"
144
- self-start px-6px py-1px rounded-4px text-11px leading-15px font-580
145
- :class="badgeClasses"
146
- >
147
- {{ meta.badge.label }}
148
- </span>
149
-
150
- <p v-if="meta.snippet" text-12px leading-17px text-neutral-600 line-clamp-3>
151
- {{ meta.snippet }}
152
- </p>
153
-
154
- <div v-if="canOpenDocument || safeSourceUrl" flex items-center gap-12px pt-2px>
148
+ <Teleport to="body">
149
+ <div
150
+ v-if="popoverOpen"
151
+ ref="popoverEl"
152
+ class="citation-ref-popover"
153
+ :style="popoverStyle"
154
+ w-300px
155
+ bg-white
156
+ border-0.5px border-solid border-neutral-300
157
+ rounded-12px p-12px
158
+ shadow-lg
159
+ cursor-default
160
+ flex="~ col" gap-8px
161
+ >
162
+ <template v-if="status === 'unresolved'">
163
+ <div flex items-start gap-6px>
164
+ <span i-ph-warning-circle text-14px text-amber-600 mt-2px flex-shrink-0 />
165
+ <p text-12px leading-16px text-neutral-700>
166
+ Não foi possível verificar esta referência. Ela pode não existir ou não estar disponível para o seu perfil.
167
+ </p>
168
+ </div>
155
169
  <button
156
- v-if="canOpenDocument"
157
170
  type="button"
158
- inline-flex items-center gap-4px
159
- text-12px leading-16px font-580 text-neutral-900
160
- cursor-pointer hover:underline
161
- @click="openDocument"
171
+ self-start text-12px leading-16px text-neutral-700 underline cursor-pointer
172
+ @click="retry"
162
173
  >
163
- <span i-ph-file-magnifying-glass text-13px />
164
- Ver documento
174
+ Tentar novamente
165
175
  </button>
166
- <a
167
- v-if="safeSourceUrl"
168
- :href="safeSourceUrl"
169
- target="_blank"
170
- rel="noopener noreferrer"
171
- inline-flex items-center gap-4px
172
- text-12px leading-16px text-neutral-600
173
- hover:underline
176
+ </template>
177
+
178
+ <template v-else-if="meta">
179
+ <div flex="~ col" gap-2px>
180
+ <p text-13px leading-18px font-580 text-neutral-900>
181
+ {{ meta.title }}
182
+ </p>
183
+ <p v-if="meta.sourceLabel || meta.identifier" text-12px leading-16px text-neutral-600>
184
+ {{ [meta.sourceLabel, meta.identifier].filter(Boolean).join(" \xB7 ") }}
185
+ </p>
186
+ <p v-if="formattedDate" text-12px leading-16px text-neutral-500>
187
+ {{ meta.dateLabel ? `${meta.dateLabel}: ` : "" }}{{ formattedDate }}
188
+ </p>
189
+ </div>
190
+
191
+ <span
192
+ v-if="meta.badge"
193
+ self-start px-6px py-1px rounded-4px text-11px leading-15px font-580
194
+ :class="badgeClasses"
174
195
  >
175
- <span i-ph-arrow-square-out text-13px />
176
- Abrir na fonte
177
- </a>
178
- </div>
179
- </template>
180
- </div>
196
+ {{ meta.badge.label }}
197
+ </span>
198
+
199
+ <p v-if="meta.snippet" text-12px leading-17px text-neutral-600 line-clamp-3>
200
+ {{ meta.snippet }}
201
+ </p>
202
+
203
+ <div v-if="canOpenDocument || safeSourceUrl" flex items-center gap-12px pt-2px>
204
+ <button
205
+ v-if="canOpenDocument"
206
+ type="button"
207
+ inline-flex items-center gap-4px
208
+ text-12px leading-16px font-580 text-neutral-900
209
+ cursor-pointer hover:underline
210
+ @click="openDocument"
211
+ >
212
+ <span i-ph-file-magnifying-glass text-13px />
213
+ Ver documento
214
+ </button>
215
+ <a
216
+ v-if="safeSourceUrl"
217
+ :href="safeSourceUrl"
218
+ target="_blank"
219
+ rel="noopener noreferrer"
220
+ inline-flex items-center gap-4px
221
+ text-12px leading-16px text-neutral-600
222
+ hover:underline
223
+ >
224
+ <span i-ph-arrow-square-out text-13px />
225
+ Abrir na fonte
226
+ </a>
227
+ </div>
228
+ </template>
229
+ </div>
230
+ </Teleport>
181
231
  </span>
182
232
  </template>
183
233
 
184
234
  <style scoped>
185
- .citation-ref{display:inline;position:relative}.citation-ref-anchor{background:transparent;border:0;border-radius:5px;color:inherit;cursor:pointer;display:inline;font:inherit;margin:0 -2px;padding:1px 3px;text-decoration:underline;text-decoration-color:#a3a3a3;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px;transition:background-color .15s ease,text-decoration-color .15s ease}.citation-ref-anchor:hover{background-color:#f5f5f5;text-decoration-color:#525252}.citation-ref-anchor--loading{cursor:default;opacity:.6;text-decoration-color:#d4d4d4}.citation-ref-anchor--unresolved{text-decoration-color:#f59e0b}.citation-ref-anchor--unresolved:hover{background-color:#fffbeb}.citation-ref-icon{display:inline-block;font-size:.85em;margin-right:.25em;vertical-align:-.08em}
235
+ .citation-ref{display:inline;position:relative}.citation-ref-popover{position:fixed;z-index:50}.citation-ref-anchor{background:transparent;border:0;border-radius:5px;color:inherit;cursor:pointer;display:inline;font:inherit;margin:0 -2px;padding:1px 3px;text-decoration:underline;text-decoration-color:#a3a3a3;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px;transition:background-color .15s ease,text-decoration-color .15s ease}.citation-ref-anchor:hover{background-color:#f5f5f5;text-decoration-color:#525252}.citation-ref-anchor--loading{cursor:default;opacity:.6;text-decoration-color:#d4d4d4}.citation-ref-anchor--unresolved{text-decoration-color:#f59e0b}.citation-ref-anchor--unresolved:hover{background-color:#fffbeb}.citation-ref-icon{display:inline-block;font-size:.85em;margin-right:.25em;vertical-align:-.08em}
186
236
  </style>
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { useChatAuth } from "#chat-auth";
3
3
  import { useBreakpoints } from "@vueuse/core";
4
+ import { useCitationPanel } from "../../citations/composables/citation-panel";
4
5
  import { exportConversationToMarkdown } from "../../conversations/export";
5
6
  import { CHAT_MOBILE_BREAKPOINT } from "../../shared/breakpoints";
6
7
  import ChatMobileFilePreview from "./mobile/chat-mobile-file-preview.vue";
@@ -1,15 +1,40 @@
1
1
  import { getCookie } from "h3";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { parseAllowedEmailDomains } from "./domain-allow.js";
4
+ import { logger } from "./logger.js";
3
5
  const DEFAULT_FIRST_PARTY_JWT_COOKIE_NAME = "tela-jwt";
4
6
  function resolveFirstPartyJwtCookieName(event) {
5
7
  const config = useRuntimeConfig(event);
6
8
  return config.public?.telaAuth?.jwtCookieName || DEFAULT_FIRST_PARTY_JWT_COOKIE_NAME;
7
9
  }
10
+ function resolveHostAuthTokenCookieName(event) {
11
+ const config = useRuntimeConfig(event);
12
+ return config.chatNuxt?.authTokenCookie || void 0;
13
+ }
14
+ function hasEmailDomainRestrictions(event) {
15
+ const config = useRuntimeConfig(event);
16
+ return parseAllowedEmailDomains(config.auth?.allowedEmailDomains).length > 0;
17
+ }
8
18
  export function resolveAuthToken(event) {
9
19
  const auth = event.context.auth;
10
20
  if (auth?.token) {
11
21
  return auth.token;
12
22
  }
23
+ const hostCookieName = resolveHostAuthTokenCookieName(event);
24
+ if (hostCookieName) {
25
+ if (hasEmailDomainRestrictions(event)) {
26
+ logger.warn(
27
+ { cookie: hostCookieName },
28
+ "authTokenCookie ignored: auth.allowedEmailDomains is set and the host cookie carries no user context for domain validation"
29
+ );
30
+ return void 0;
31
+ }
32
+ const token2 = getCookie(event, hostCookieName);
33
+ if (token2 && auth) {
34
+ auth.token = token2;
35
+ }
36
+ return token2 || void 0;
37
+ }
13
38
  if (!auth?.user || !auth.workspace) {
14
39
  return void 0;
15
40
  }
@@ -1,3 +1,5 @@
1
+ // Keep these types in sync with ../auth/define.ts — the exported ChatAuthBridge
2
+ // there is the same contract host apps implement via defineChatAuth().
1
3
  declare module '#chat-auth' {
2
4
  type ChatAuthUser = {
3
5
  id: string
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "3.12.1",
3
+ "version": "3.13.0-rc.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/types.d.mts",
8
8
  "import": "./dist/module.mjs"
9
9
  },
10
+ "./auth": {
11
+ "types": "./dist/runtime/auth/define.d.ts",
12
+ "import": "./dist/runtime/auth/define.js"
13
+ },
10
14
  "./components/MeistrariChatEmbed": {
11
15
  "types": "./dist/runtime/embed/components/meistrari-chat-embed.vue.d.ts",
12
16
  "import": "./dist/runtime/embed/components/meistrari-chat-embed.vue"