@meistrari/chat-nuxt 3.17.0-rc.3 → 3.17.0-rc.5

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
@@ -34,6 +34,13 @@ interface ChatNuxtModuleOptions {
34
34
  * `NUXT_CHAT_NUXT_SECRETLESS_EXECUTION`.
35
35
  */
36
36
  secretlessExecution?: boolean;
37
+ /**
38
+ * Opt this deployment into the experimental agent-api v5 snapshot path: the
39
+ * server proxy adds `"agent-api-v5": true` to the `x-chat-config` header, so
40
+ * wake and execute prefer the published snapshot (with v4 fallback). Defaults
41
+ * to `false`; also settable via `NUXT_CHAT_NUXT_AGENT_API_V5`.
42
+ */
43
+ agentApiV5?: boolean;
37
44
  }
38
45
  declare const _default: any;
39
46
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "3.17.0-rc.3",
4
+ "version": "3.17.0-rc.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -451,7 +451,8 @@ const module$1 = defineNuxtModule({
451
451
  authTokenCookie: hostAuthComposablePath ? options.authTokenCookie || existingChatNuxtRuntimeConfig?.authTokenCookie : void 0,
452
452
  // Explicit boolean default keeps the key in runtime config so the
453
453
  // NUXT_CHAT_NUXT_SECRETLESS_EXECUTION env override can apply.
454
- secretlessExecution: options.secretlessExecution ?? existingChatNuxtRuntimeConfig?.secretlessExecution ?? false
454
+ secretlessExecution: options.secretlessExecution ?? existingChatNuxtRuntimeConfig?.secretlessExecution ?? false,
455
+ agentApiV5: options.agentApiV5 ?? existingChatNuxtRuntimeConfig?.agentApiV5 ?? false
455
456
  };
456
457
  if (hostAuthComposablePath && !rc.chatNuxt.authTokenCookie) {
457
458
  console.warn(
@@ -1,9 +1,24 @@
1
1
  <script setup>
2
+ import { loadCitationSiteName } from "../site-name";
2
3
  const props = defineProps({
3
4
  citation: { type: Object, required: true }
4
5
  });
5
6
  const faviconFailed = ref(false);
6
- const accessibleLabel = computed(() => `${props.citation.sourceLabel}: ${props.citation.title}`);
7
+ const siteName = ref(null);
8
+ const sourceLabel = computed(() => siteName.value || props.citation.sourceLabel);
9
+ onMounted(() => {
10
+ watch(() => props.citation.href, async (href, _previous, onCleanup) => {
11
+ let active = true;
12
+ onCleanup(() => {
13
+ active = false;
14
+ });
15
+ siteName.value = null;
16
+ const name = await loadCitationSiteName(href);
17
+ if (active)
18
+ siteName.value = name;
19
+ }, { immediate: true });
20
+ });
21
+ const accessibleLabel = computed(() => `${sourceLabel.value}: ${props.citation.title}`);
7
22
  </script>
8
23
 
9
24
  <template>
@@ -32,7 +47,7 @@ const accessibleLabel = computed(() => `${props.citation.sourceLabel}: ${props.c
32
47
  @error="faviconFailed = true"
33
48
  >
34
49
  <span v-else i-ph-globe-simple class="url-citation-fallback" aria-hidden="true" />
35
- <span>{{ citation.sourceLabel }}</span>
50
+ <span>{{ sourceLabel }}</span>
36
51
  </a>
37
52
 
38
53
  <template #content>
@@ -48,7 +63,7 @@ const accessibleLabel = computed(() => `${props.citation.sourceLabel}: ${props.c
48
63
  >
49
64
  <span v-else i-ph-globe-simple class="url-citation-card__fallback" aria-hidden="true" />
50
65
  <span body-12-medium text-secondary truncate>
51
- {{ citation.sourceLabel }}
66
+ {{ sourceLabel }}
52
67
  </span>
53
68
  </div>
54
69
 
@@ -0,0 +1,5 @@
1
+ export declare function createCitationSiteNameLoader(fetchMetadata: (url: string) => Promise<{
2
+ siteName?: string | null;
3
+ retryable?: boolean;
4
+ }>, now?: () => number): (href: string) => Promise<string | null>;
5
+ export declare const loadCitationSiteName: (href: string) => Promise<string | null>;
@@ -0,0 +1,36 @@
1
+ export function createCitationSiteNameLoader(fetchMetadata, now = Date.now) {
2
+ const cache = /* @__PURE__ */ new Map();
3
+ return (href) => {
4
+ let key;
5
+ try {
6
+ const url = new URL(href);
7
+ url.hash = "";
8
+ key = url.href;
9
+ } catch {
10
+ return Promise.resolve(null);
11
+ }
12
+ const cached = cache.get(key);
13
+ if (cached && cached.expires > now())
14
+ return cached.result;
15
+ const result = Promise.resolve().then(() => fetchMetadata(key)).then((value) => {
16
+ if (value.retryable)
17
+ throw new Error("Citation metadata temporarily unavailable");
18
+ return typeof value.siteName === "string" && value.siteName.trim() ? value.siteName.trim() : null;
19
+ }).catch(() => {
20
+ if (cache.get(key)?.result === result)
21
+ cache.delete(key);
22
+ return null;
23
+ });
24
+ cache.delete(key);
25
+ if (cache.size >= 512)
26
+ cache.delete(cache.keys().next().value);
27
+ cache.set(key, { expires: now() + 60 * 60 * 1e3, result });
28
+ return result;
29
+ };
30
+ }
31
+ export const loadCitationSiteName = createCitationSiteNameLoader(async (url) => {
32
+ const response = await fetch(`/api/citation-metadata?${new URLSearchParams({ url })}`);
33
+ if (!response.ok)
34
+ throw new Error("Citation metadata unavailable");
35
+ return response.json();
36
+ });
@@ -0,0 +1,8 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ siteName: string | null;
3
+ retryable?: undefined;
4
+ } | {
5
+ siteName: null;
6
+ retryable: boolean;
7
+ }>>;
8
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { defineEventHandler, getQuery, setResponseHeaders } from "h3";
2
+ import { createSiteNameCache } from "../utils/citation-metadata.js";
3
+ const getSiteName = createSiteNameCache();
4
+ export default defineEventHandler(async (event) => {
5
+ setResponseHeaders(event, { "cache-control": "private, no-store" });
6
+ const url = getQuery(event).url;
7
+ try {
8
+ return { siteName: typeof url === "string" ? await getSiteName(url) : null };
9
+ } catch {
10
+ return { siteName: null, retryable: true };
11
+ }
12
+ });
@@ -1,4 +1,4 @@
1
- import { PREPARED_AGENT_RUNTIME_CONFIG_KEY, SECRETLESS_EXECUTION_CONFIG_KEY } from "../../types/schemas/chat/chat-config.js";
1
+ import { AGENT_API_V5_CONFIG_KEY, PREPARED_AGENT_RUNTIME_CONFIG_KEY, SECRETLESS_EXECUTION_CONFIG_KEY } from "../../types/schemas/chat/chat-config.js";
2
2
  import { createError, defineEventHandler, getHeader, getRequestURL, getRouterParam, proxyRequest } from "h3";
3
3
  import { useRuntimeConfig } from "nitropack/runtime";
4
4
  import { resolveAuthToken } from "./auth-token.js";
@@ -64,6 +64,9 @@ function buildProxyHeaders(event) {
64
64
  if (config.chatNuxt?.secretlessExecution) {
65
65
  chatConfig[SECRETLESS_EXECUTION_CONFIG_KEY] = true;
66
66
  }
67
+ if (config.chatNuxt?.agentApiV5) {
68
+ chatConfig[AGENT_API_V5_CONFIG_KEY] = true;
69
+ }
67
70
  const headers = {
68
71
  "authorization": `Bearer ${token}`,
69
72
  "x-chat-client": "chat-nuxt",
@@ -0,0 +1,16 @@
1
+ export declare const MAX_HTML_BYTES: number;
2
+ export declare function publicCitationUrl(value: unknown): URL;
3
+ export declare function isPublicAddress(address: string): boolean;
4
+ export declare function parseCitationSiteName(html: string): string | null;
5
+ interface Address {
6
+ address: string;
7
+ family: number;
8
+ }
9
+ export declare function resolvePublicAddress(hostname: string, resolve: (host: string) => Promise<Address[]>): Promise<Address>;
10
+ declare function requestPage(url: URL, signal: AbortSignal): Promise<{
11
+ redirect?: string;
12
+ html?: string;
13
+ }>;
14
+ export declare function fetchCitationSiteName(input: string, request?: typeof requestPage): Promise<string | null>;
15
+ export declare function createSiteNameCache(load?: typeof fetchCitationSiteName, now?: () => number): (input: string) => Promise<string | null>;
16
+ export {};
@@ -0,0 +1,187 @@
1
+ import { Parser } from "htmlparser2";
2
+ import ipaddr from "ipaddr.js";
3
+ export const MAX_HTML_BYTES = 256 * 1024;
4
+ const TIMEOUT_MS = 5e3;
5
+ const MAX_REDIRECTS = 3;
6
+ class PermanentMetadataError extends Error {
7
+ }
8
+ export function publicCitationUrl(value) {
9
+ if (typeof value !== "string" || value.length > 2048)
10
+ throw new PermanentMetadataError("Invalid citation URL");
11
+ let url;
12
+ try {
13
+ url = new URL(value);
14
+ } catch {
15
+ throw new PermanentMetadataError("Invalid citation URL");
16
+ }
17
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.port)
18
+ throw new PermanentMetadataError("Invalid citation URL");
19
+ url.hash = "";
20
+ return url;
21
+ }
22
+ export function isPublicAddress(address) {
23
+ try {
24
+ return ipaddr.process(address.replace(/^\[|\]$/g, "")).range() === "unicast";
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ export function parseCitationSiteName(html) {
30
+ let siteName = null;
31
+ const parser = new Parser({
32
+ onopentag(name, attributes) {
33
+ if (name !== "meta" || siteName || (attributes.property || attributes.name || "").toLowerCase() !== "og:site_name")
34
+ return;
35
+ const value = (attributes.content || "").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
36
+ if (value && value.length <= 100)
37
+ siteName = value;
38
+ }
39
+ }, { decodeEntities: true });
40
+ parser.end(html);
41
+ return siteName;
42
+ }
43
+ export async function resolvePublicAddress(hostname, resolve) {
44
+ const host = hostname.replace(/^\[|\]$/g, "");
45
+ const addresses = ipaddr.isValid(host) ? [{ address: host, family: ipaddr.parse(host).kind() === "ipv4" ? 4 : 6 }] : await resolve(host);
46
+ if (!addresses.length || addresses.some((record) => !isPublicAddress(record.address)))
47
+ throw new PermanentMetadataError("Non-public citation address");
48
+ return addresses[0];
49
+ }
50
+ async function requestPage(url, signal) {
51
+ const [{ lookup }, http, https, { Buffer }] = await Promise.all([
52
+ import("node:dns/promises"),
53
+ import("node:http"),
54
+ import("node:https"),
55
+ import("node:buffer")
56
+ ]);
57
+ const address = await resolvePublicAddress(url.hostname, (host) => lookup(host, { all: true }));
58
+ signal.throwIfAborted();
59
+ return new Promise((resolve, reject) => {
60
+ const request = (url.protocol === "https:" ? https.request : http.request)(url, {
61
+ agent: false,
62
+ family: address.family,
63
+ signal,
64
+ // Preserve the original Host header and TLS server name. Only the
65
+ // socket destination is pinned; no cookies or auth are forwarded.
66
+ lookup: (_hostname, _options, callback) => callback(null, address.address, address.family),
67
+ headers: { "accept": "text/html,application/xhtml+xml", "accept-encoding": "identity" }
68
+ }, (response) => {
69
+ const status = response.statusCode || 0;
70
+ if ([301, 302, 303, 307, 308].includes(status)) {
71
+ response.destroy();
72
+ resolve({ redirect: response.headers.location });
73
+ return;
74
+ }
75
+ if (status < 200 || status >= 300) {
76
+ response.destroy();
77
+ const retryable = status >= 500 || [408, 425, 429].includes(status);
78
+ reject(retryable ? new Error("Citation page unavailable") : new PermanentMetadataError("Citation page unavailable"));
79
+ return;
80
+ }
81
+ if (!/^(?:text\/html|application\/xhtml\+xml)(?:;|$)/i.test(response.headers["content-type"] || "") || response.headers["content-encoding"] && response.headers["content-encoding"] !== "identity") {
82
+ response.destroy();
83
+ reject(new PermanentMetadataError("Unsupported citation content"));
84
+ return;
85
+ }
86
+ const chunks = [];
87
+ let size = 0;
88
+ response.on("data", (chunk) => {
89
+ size += chunk.length;
90
+ if (size > MAX_HTML_BYTES) {
91
+ const remaining = MAX_HTML_BYTES - (size - chunk.length);
92
+ chunks.push(chunk.subarray(0, remaining));
93
+ resolve({ html: Buffer.concat(chunks).toString("utf8") });
94
+ response.destroy();
95
+ return;
96
+ }
97
+ chunks.push(chunk);
98
+ });
99
+ response.on("end", () => resolve({ html: Buffer.concat(chunks).toString("utf8") }));
100
+ response.on("error", reject);
101
+ });
102
+ request.on("error", reject);
103
+ request.end();
104
+ });
105
+ }
106
+ export async function fetchCitationSiteName(input, request = requestPage) {
107
+ const controller = new AbortController();
108
+ let timer;
109
+ const timeout = new Promise((_resolve, reject) => {
110
+ timer = setTimeout(() => {
111
+ controller.abort();
112
+ reject(new Error("Citation metadata timed out"));
113
+ }, TIMEOUT_MS);
114
+ });
115
+ try {
116
+ return await Promise.race([timeout, (async () => {
117
+ let url = publicCitationUrl(input);
118
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
119
+ const response = await request(url, controller.signal);
120
+ if (!response.redirect)
121
+ return response.html ? parseCitationSiteName(response.html) : null;
122
+ let target;
123
+ try {
124
+ target = new URL(response.redirect, url);
125
+ } catch {
126
+ throw new PermanentMetadataError("Invalid citation redirect");
127
+ }
128
+ url = publicCitationUrl(target.href);
129
+ }
130
+ throw new PermanentMetadataError("Too many citation redirects");
131
+ })()]);
132
+ } catch (error) {
133
+ if (error instanceof PermanentMetadataError)
134
+ return null;
135
+ throw error;
136
+ } finally {
137
+ clearTimeout(timer);
138
+ controller.abort();
139
+ }
140
+ }
141
+ export function createSiteNameCache(load = fetchCitationSiteName, now = Date.now) {
142
+ const cache = /* @__PURE__ */ new Map();
143
+ const queue = [];
144
+ let active = 0;
145
+ return (input) => {
146
+ let key;
147
+ try {
148
+ key = publicCitationUrl(input).href;
149
+ } catch {
150
+ return Promise.resolve(null);
151
+ }
152
+ const cached = cache.get(key);
153
+ if (cached && cached.expires > now())
154
+ return cached.result;
155
+ if (active >= 8 && queue.length >= 64)
156
+ return Promise.reject(new Error("Citation metadata queue full"));
157
+ const entry = { expires: Number.POSITIVE_INFINITY, result: Promise.resolve(null) };
158
+ entry.result = new Promise((resolve, reject) => {
159
+ const start = () => {
160
+ active++;
161
+ void Promise.resolve().then(() => load(key)).then((name) => {
162
+ entry.expires = now() + 60 * 60 * 1e3;
163
+ resolve(name);
164
+ }, (error) => {
165
+ if (cache.get(key) === entry)
166
+ cache.delete(key);
167
+ reject(error);
168
+ }).finally(() => {
169
+ active--;
170
+ queue.shift()?.();
171
+ });
172
+ };
173
+ if (active < 8)
174
+ start();
175
+ else
176
+ queue.push(start);
177
+ });
178
+ cache.delete(key);
179
+ if (cache.size >= 512) {
180
+ const oldestCompleted = [...cache].find(([, value]) => Number.isFinite(value.expires));
181
+ if (oldestCompleted)
182
+ cache.delete(oldestCompleted[0]);
183
+ }
184
+ cache.set(key, entry);
185
+ return entry.result;
186
+ };
187
+ }
@@ -1,2 +1,3 @@
1
1
  export declare const PREPARED_AGENT_RUNTIME_CONFIG_KEY = "prepared-agent-runtime";
2
2
  export declare const SECRETLESS_EXECUTION_CONFIG_KEY = "secretless-execution";
3
+ export declare const AGENT_API_V5_CONFIG_KEY = "agent-api-v5";
@@ -1,2 +1,3 @@
1
1
  export const PREPARED_AGENT_RUNTIME_CONFIG_KEY = 'prepared-agent-runtime';
2
2
  export const SECRETLESS_EXECUTION_CONFIG_KEY = 'secretless-execution';
3
+ export const AGENT_API_V5_CONFIG_KEY = 'agent-api-v5';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "3.17.0-rc.3",
3
+ "version": "3.17.0-rc.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -51,6 +51,8 @@
51
51
  "@vueuse/components": "^12.8.0",
52
52
  "@vueuse/core": "^12.8.0",
53
53
  "h3": "^1.15.0",
54
+ "htmlparser2": "12.0.0",
55
+ "ipaddr.js": "2.5.0",
54
56
  "markstream-vue": "2.0.7",
55
57
  "mermaid": "^11.13.0",
56
58
  "motion": "^11.13.0",