ugly-app 0.1.619 → 0.1.621

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.
Files changed (79) hide show
  1. package/dist/cli/version.d.ts +1 -1
  2. package/dist/cli/version.js +1 -1
  3. package/dist/native/contract-meta.d.ts +7 -0
  4. package/dist/native/contract-meta.d.ts.map +1 -0
  5. package/dist/native/contract-meta.js +105 -0
  6. package/dist/native/contract-meta.js.map +1 -0
  7. package/dist/native/contract.d.ts +254 -24
  8. package/dist/native/contract.d.ts.map +1 -1
  9. package/dist/native/contract.js +4 -15
  10. package/dist/native/contract.js.map +1 -1
  11. package/dist/native/facades/net.d.ts +26 -0
  12. package/dist/native/facades/net.d.ts.map +1 -0
  13. package/dist/native/facades/net.js +22 -0
  14. package/dist/native/facades/net.js.map +1 -0
  15. package/dist/native/facades/process.d.ts +20 -0
  16. package/dist/native/facades/process.d.ts.map +1 -0
  17. package/dist/native/facades/process.js +15 -0
  18. package/dist/native/facades/process.js.map +1 -0
  19. package/dist/native/facades/studio.d.ts +47 -0
  20. package/dist/native/facades/studio.d.ts.map +1 -0
  21. package/dist/native/facades/studio.js +38 -0
  22. package/dist/native/facades/studio.js.map +1 -0
  23. package/dist/native/index.d.ts +10 -3
  24. package/dist/native/index.d.ts.map +1 -1
  25. package/dist/native/index.js +4 -2
  26. package/dist/native/index.js.map +1 -1
  27. package/dist/native/install.d.ts.map +1 -1
  28. package/dist/native/install.js +10 -8
  29. package/dist/native/install.js.map +1 -1
  30. package/dist/native/permissions.d.ts +12 -0
  31. package/dist/native/permissions.d.ts.map +1 -0
  32. package/dist/native/permissions.js +47 -0
  33. package/dist/native/permissions.js.map +1 -0
  34. package/dist/native/protocol.d.ts +5 -1
  35. package/dist/native/protocol.d.ts.map +1 -1
  36. package/dist/native/protocol.js +8 -0
  37. package/dist/native/protocol.js.map +1 -1
  38. package/dist/native/web-adapter.d.ts +5 -0
  39. package/dist/native/web-adapter.d.ts.map +1 -0
  40. package/dist/native/web-adapter.js +111 -0
  41. package/dist/native/web-adapter.js.map +1 -0
  42. package/dist/native/wrapper.d.ts +80 -6
  43. package/dist/native/wrapper.d.ts.map +1 -1
  44. package/dist/native/wrapper.js +149 -12
  45. package/dist/native/wrapper.js.map +1 -1
  46. package/dist/search/server/WebRetriever.d.ts +7 -0
  47. package/dist/search/server/WebRetriever.d.ts.map +1 -1
  48. package/dist/search/server/WebRetriever.js +2 -2
  49. package/dist/search/server/WebRetriever.js.map +1 -1
  50. package/dist/server/ai/providers/UglyBotWebSearchProvider.d.ts.map +1 -1
  51. package/dist/server/ai/providers/UglyBotWebSearchProvider.js +11 -8
  52. package/dist/server/ai/providers/UglyBotWebSearchProvider.js.map +1 -1
  53. package/dist/server/ai/types.d.ts +7 -0
  54. package/dist/server/ai/types.d.ts.map +1 -1
  55. package/dist/server/ai/types.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/cli/version.ts +1 -1
  58. package/src/native/contract-meta.test.ts +23 -0
  59. package/src/native/contract-meta.ts +114 -0
  60. package/src/native/contract.test.ts +13 -7
  61. package/src/native/contract.ts +130 -32
  62. package/src/native/facades/facades.test.ts +35 -0
  63. package/src/native/facades/net.ts +50 -0
  64. package/src/native/facades/process.ts +37 -0
  65. package/src/native/facades/studio.ts +73 -0
  66. package/src/native/index.ts +24 -3
  67. package/src/native/install.test.ts +8 -0
  68. package/src/native/install.ts +12 -8
  69. package/src/native/permissions.test.ts +42 -0
  70. package/src/native/permissions.ts +53 -0
  71. package/src/native/protocol.test.ts +15 -0
  72. package/src/native/protocol.ts +10 -1
  73. package/src/native/web-adapter.test.ts +27 -0
  74. package/src/native/web-adapter.ts +118 -0
  75. package/src/native/wrapper.test.ts +22 -0
  76. package/src/native/wrapper.ts +163 -13
  77. package/src/search/server/WebRetriever.ts +9 -4
  78. package/src/server/ai/providers/UglyBotWebSearchProvider.ts +13 -2
  79. package/src/server/ai/types.ts +7 -0
@@ -0,0 +1,118 @@
1
+ import type { Channel, NativeEvent, DataOf, SafeAreaInsets } from './contract.js';
2
+ import { fallbackFor } from './contract-meta.js';
3
+ import { NativeUnavailableError } from './protocol.js';
4
+
5
+ type Emit = <E extends NativeEvent>(event: E, data: DataOf<E>) => void;
6
+
7
+ // In-tab alarm timers (web fallback only; cleared on tab close).
8
+ const alarmTimers = new Map<string, ReturnType<typeof setTimeout>>();
9
+ let gpsWatchId: number | null = null;
10
+ let webAudio: HTMLAudioElement | null = null;
11
+
12
+ function readSafeArea(): SafeAreaInsets {
13
+ const cs = getComputedStyle(document.documentElement);
14
+ const px = (v: string): number => parseInt(v || '0', 10) || 0;
15
+ return {
16
+ top: px(cs.getPropertyValue('--sat')),
17
+ right: 0,
18
+ bottom: 0,
19
+ left: 0,
20
+ };
21
+ }
22
+
23
+ export async function webInvoke(channel: Channel, payload: unknown, emit: Emit): Promise<unknown> {
24
+ switch (channel) {
25
+ case 'clipboard.write':
26
+ await navigator.clipboard.writeText((payload as { text: string }).text);
27
+ return undefined;
28
+ case 'clipboard.read':
29
+ return { text: await navigator.clipboard.readText() };
30
+ case 'system.openExternal':
31
+ window.open((payload as { url: string }).url, '_blank', 'noopener');
32
+ return undefined;
33
+ case 'system.share':
34
+ if (navigator.share) await navigator.share(payload as ShareData);
35
+ return undefined;
36
+ case 'badge.set': {
37
+ const n = (payload as { count: number }).count;
38
+ const navAny = navigator as unknown as {
39
+ setAppBadge?: (n?: number) => Promise<void>;
40
+ clearAppBadge?: () => Promise<void>;
41
+ };
42
+ if (n > 0) await navAny.setAppBadge?.(n);
43
+ else await navAny.clearAppBadge?.();
44
+ return undefined;
45
+ }
46
+ case 'ui.getSafeArea':
47
+ return readSafeArea();
48
+ case 'gps.start': {
49
+ if (gpsWatchId != null) navigator.geolocation.clearWatch(gpsWatchId);
50
+ gpsWatchId = navigator.geolocation.watchPosition(
51
+ (pos) =>
52
+ emit('gps.position', {
53
+ lat: pos.coords.latitude,
54
+ lng: pos.coords.longitude,
55
+ accuracy: pos.coords.accuracy,
56
+ timestamp: pos.timestamp,
57
+ altitude: pos.coords.altitude ?? undefined,
58
+ speed: pos.coords.speed ?? undefined,
59
+ course: pos.coords.heading ?? undefined,
60
+ }),
61
+ () => {},
62
+ { enableHighAccuracy: true },
63
+ );
64
+ return undefined;
65
+ }
66
+ case 'gps.stop':
67
+ if (gpsWatchId != null) {
68
+ navigator.geolocation.clearWatch(gpsWatchId);
69
+ gpsWatchId = null;
70
+ }
71
+ return undefined;
72
+ case 'alarm.schedule': {
73
+ const p = payload as { id: string; delaySeconds: number; title?: string; body?: string };
74
+ const existing = alarmTimers.get(p.id);
75
+ if (existing) clearTimeout(existing);
76
+ alarmTimers.set(
77
+ p.id,
78
+ setTimeout(() => {
79
+ alarmTimers.delete(p.id);
80
+ if ('Notification' in window && Notification.permission === 'granted') {
81
+ new Notification(p.title ?? 'Alarm', { body: p.body });
82
+ }
83
+ emit('alarm.fired', { id: p.id });
84
+ }, p.delaySeconds * 1000),
85
+ );
86
+ return undefined;
87
+ }
88
+ case 'alarm.cancel': {
89
+ const id = (payload as { id?: string }).id;
90
+ if (id) {
91
+ const t = alarmTimers.get(id);
92
+ if (t) clearTimeout(t);
93
+ alarmTimers.delete(id);
94
+ } else {
95
+ for (const t of alarmTimers.values()) clearTimeout(t);
96
+ alarmTimers.clear();
97
+ }
98
+ return undefined;
99
+ }
100
+ case 'alarm.playAudio': {
101
+ const p = payload as { base64: string; mimeType?: string };
102
+ webAudio = new Audio(`data:${p.mimeType ?? 'audio/wav'};base64,${p.base64}`);
103
+ await webAudio.play().catch(() => {});
104
+ return undefined;
105
+ }
106
+ case 'capture.screenshot': {
107
+ // Best-effort capture requires a user-gesture-driven display capture; declared
108
+ // web-capable but gated by a gesture, so treat as unavailable until wired by a caller.
109
+ throw new NativeUnavailableError(channel);
110
+ }
111
+ default: {
112
+ const fb = fallbackFor(channel, 'web');
113
+ if (fb === 'throw') throw new NativeUnavailableError(channel);
114
+ // 'noop' / 'web'-but-unimplemented (liveActivity, tts, push, …) resolve silently.
115
+ return undefined;
116
+ }
117
+ }
118
+ }
@@ -30,4 +30,26 @@ describe('native wrapper', () => {
30
30
  native.gps.onPosition(cb);
31
31
  expect(subscribe).toHaveBeenCalledWith('gps.position', cb);
32
32
  });
33
+
34
+ it('capture/clipboard (non-gated) invoke the right channels', async () => {
35
+ const { invoke } = installFakeNative();
36
+ const { native } = await import('./wrapper');
37
+ await native.capture.screenshot();
38
+ await native.clipboard.write({ text: 'a' });
39
+ expect(invoke).toHaveBeenCalledWith('capture.screenshot', undefined);
40
+ expect(invoke).toHaveBeenCalledWith('clipboard.write', { text: 'a' });
41
+ });
42
+
43
+ it('fs.readFile ensures the fs permission then invokes', async () => {
44
+ const granted: Record<string, string> = { fs: 'full' };
45
+ const invoke = vi.fn(async (channel: string) => {
46
+ if (channel === 'permissions.query') return { granted: { ...granted } };
47
+ if (channel === 'fs.readFile') return { content: 'hello' };
48
+ return undefined;
49
+ });
50
+ (globalThis as unknown as { UglyNative: unknown }).UglyNative = { platform: 'desktop', invoke, subscribe: () => () => {} };
51
+ const { native } = await import('./wrapper');
52
+ expect(await native.fs.readFile('/x')).toBe('hello');
53
+ expect(invoke).toHaveBeenCalledWith('fs.readFile', { path: '/x' });
54
+ });
33
55
  });
@@ -1,28 +1,178 @@
1
- import type { DataOf, PayloadOf } from './contract.js';
1
+ import type { Channel, DataOf, PayloadOf, ResultOf } from './contract.js';
2
2
  import type { NativePlatform } from './protocol.js';
3
+ import { NativeUnavailableError } from './protocol.js';
3
4
  import { installUglyNative } from './install.js';
5
+ import { createPermissions } from './permissions.js';
6
+ import { createProcessFacade } from './facades/process.js';
7
+ import { createNetFacade } from './facades/net.js';
8
+ import { createStudioFacade } from './facades/studio.js';
4
9
 
5
10
  const n = () => installUglyNative();
11
+ const inv = <C extends Channel>(c: C, p: PayloadOf<C>): Promise<ResultOf<C>> =>
12
+ n().invoke(c, p) as Promise<ResultOf<C>>;
13
+
14
+ export const permissions = createPermissions((c, p) => n().invoke(c as never, p as never), n().platform);
15
+
16
+ // Desktop daemon bridges (process/net/studio + fs bytes) hang off window.UglyDesktop.
17
+ interface DesktopBridges {
18
+ process?: unknown;
19
+ net?: unknown;
20
+ studio?: unknown;
21
+ fsBytes?: {
22
+ readFileBytes(path: string): Promise<Uint8Array>;
23
+ writeFileBytes(path: string, bytes: Uint8Array): Promise<void>;
24
+ };
25
+ }
26
+ const desktopBridge = (): DesktopBridges | undefined =>
27
+ (globalThis as unknown as { UglyDesktop?: DesktopBridges }).UglyDesktop;
6
28
 
7
29
  export const native = {
30
+ permissions,
31
+ capture: {
32
+ screenshot: () => inv('capture.screenshot', undefined),
33
+ },
8
34
  gps: {
9
- start: (p: PayloadOf<'gps.start'>) => n().invoke('gps.start', p),
10
- stop: () => n().invoke('gps.stop', undefined),
35
+ start: (p: PayloadOf<'gps.start'>) => inv('gps.start', p),
36
+ stop: () => inv('gps.stop', undefined),
11
37
  onPosition: (cb: (d: DataOf<'gps.position'>) => void) => n().subscribe('gps.position', cb),
12
38
  },
39
+ liveActivity: {
40
+ startWorkout: (p: PayloadOf<'liveActivity.startWorkout'>) => inv('liveActivity.startWorkout', p),
41
+ updateWorkout: (p: PayloadOf<'liveActivity.updateWorkout'>) => inv('liveActivity.updateWorkout', p),
42
+ endWorkout: () => inv('liveActivity.endWorkout', undefined),
43
+ startCardio: (p: PayloadOf<'liveActivity.startCardio'>) => inv('liveActivity.startCardio', p),
44
+ updateCardio: (p: PayloadOf<'liveActivity.updateCardio'>) => inv('liveActivity.updateCardio', p),
45
+ endCardio: () => inv('liveActivity.endCardio', undefined),
46
+ },
13
47
  alarm: {
14
- schedule: (p: PayloadOf<'alarm.schedule'>) => n().invoke('alarm.schedule', p),
15
- cancel: (p: PayloadOf<'alarm.cancel'>) => n().invoke('alarm.cancel', p),
16
- playAudio: (p: PayloadOf<'alarm.playAudio'>) => n().invoke('alarm.playAudio', p),
48
+ schedule: (p: PayloadOf<'alarm.schedule'>) => inv('alarm.schedule', p),
49
+ cancel: (p: PayloadOf<'alarm.cancel'>) => inv('alarm.cancel', p),
50
+ playAudio: (p: PayloadOf<'alarm.playAudio'>) => inv('alarm.playAudio', p),
17
51
  onFired: (cb: (d: DataOf<'alarm.fired'>) => void) => n().subscribe('alarm.fired', cb),
18
52
  },
19
- liveActivity: {
20
- startWorkout: (p: PayloadOf<'liveActivity.startWorkout'>) => n().invoke('liveActivity.startWorkout', p),
21
- updateWorkout: (p: PayloadOf<'liveActivity.updateWorkout'>) => n().invoke('liveActivity.updateWorkout', p),
22
- endWorkout: () => n().invoke('liveActivity.endWorkout', undefined),
23
- startCardio: (p: PayloadOf<'liveActivity.startCardio'>) => n().invoke('liveActivity.startCardio', p),
24
- updateCardio: (p: PayloadOf<'liveActivity.updateCardio'>) => n().invoke('liveActivity.updateCardio', p),
25
- endCardio: () => n().invoke('liveActivity.endCardio', undefined),
53
+ tts: {
54
+ start: (p: PayloadOf<'tts.start'>) => inv('tts.start', p),
55
+ pause: () => inv('tts.pause', undefined),
56
+ resume: () => inv('tts.resume', undefined),
57
+ stop: () => inv('tts.stop', undefined),
58
+ onState: (cb: (d: DataOf<'tts.state'>) => void) => n().subscribe('tts.state', cb),
59
+ },
60
+ push: {
61
+ register: () => inv('push.register', undefined),
62
+ onToken: (cb: (d: DataOf<'push.token'>) => void) => n().subscribe('push.token', cb),
63
+ onNotificationClick: (cb: (d: DataOf<'push.notificationClick'>) => void) =>
64
+ n().subscribe('push.notificationClick', cb),
65
+ },
66
+ clipboard: {
67
+ write: (p: PayloadOf<'clipboard.write'>) => inv('clipboard.write', p),
68
+ read: () => inv('clipboard.read', undefined),
69
+ },
70
+ system: {
71
+ openExternal: (p: PayloadOf<'system.openExternal'>) => inv('system.openExternal', p),
72
+ share: (p: PayloadOf<'system.share'>) => inv('system.share', p),
73
+ },
74
+ badge: {
75
+ set: (p: PayloadOf<'badge.set'>) => inv('badge.set', p),
76
+ },
77
+ ui: {
78
+ getSafeArea: () => inv('ui.getSafeArea', undefined),
79
+ onKeyboardHeight: (cb: (d: DataOf<'keyboard.height'>) => void) => n().subscribe('keyboard.height', cb),
80
+ onSafeAreaInsets: (cb: (d: DataOf<'safeArea.insets'>) => void) => n().subscribe('safeArea.insets', cb),
81
+ },
82
+ fs: {
83
+ readFile: async (path: string) => {
84
+ await permissions.ensure('fs');
85
+ return (await inv('fs.readFile', { path })).content;
86
+ },
87
+ writeFile: async (path: string, content: string) => {
88
+ await permissions.ensure('fs');
89
+ return inv('fs.writeFile', { path, content });
90
+ },
91
+ mkdir: async (path: string, recursive = false) => {
92
+ await permissions.ensure('fs');
93
+ return inv('fs.mkdir', { path, recursive });
94
+ },
95
+ rm: async (path: string, opts: { recursive?: boolean; force?: boolean } = {}) => {
96
+ await permissions.ensure('fs');
97
+ return inv('fs.rm', { path, ...opts });
98
+ },
99
+ rename: async (from: string, to: string) => {
100
+ await permissions.ensure('fs');
101
+ return inv('fs.rename', { from, to });
102
+ },
103
+ readdir: async (path: string) => {
104
+ await permissions.ensure('fs');
105
+ return (await inv('fs.readdir', { path })).entries;
106
+ },
107
+ stat: async (path: string) => {
108
+ await permissions.ensure('fs');
109
+ return inv('fs.stat', { path });
110
+ },
111
+ exists: async (path: string) => {
112
+ await permissions.ensure('fs');
113
+ return (await inv('fs.exists', { path })).exists;
114
+ },
115
+ realpath: async (path: string) => {
116
+ await permissions.ensure('fs');
117
+ return (await inv('fs.realpath', { path })).path;
118
+ },
119
+ readFileBytes: async (path: string): Promise<Uint8Array> => {
120
+ await permissions.ensure('fs');
121
+ const fb = desktopBridge()?.fsBytes;
122
+ if (!fb) throw new NativeUnavailableError('fs.readFileBytes');
123
+ return fb.readFileBytes(path);
124
+ },
125
+ writeFileBytes: async (path: string, bytes: Uint8Array): Promise<void> => {
126
+ await permissions.ensure('fs');
127
+ const fb = desktopBridge()?.fsBytes;
128
+ if (!fb) throw new NativeUnavailableError('fs.writeFileBytes');
129
+ return fb.writeFileBytes(path, bytes);
130
+ },
131
+ },
132
+ process: createProcessFacade(() => desktopBridge()?.process as never, n().platform, () => permissions.ensure('process')),
133
+ net: createNetFacade(() => desktopBridge()?.net as never, n().platform, () => permissions.ensure('net')),
134
+ studio: createStudioFacade(() => desktopBridge()?.studio as never, n().platform),
135
+ secrets: {
136
+ get: async (key: string) => {
137
+ await permissions.ensure('secrets');
138
+ return (await inv('secrets.get', { key })).value;
139
+ },
140
+ set: async (key: string, value: string) => {
141
+ await permissions.ensure('secrets');
142
+ return inv('secrets.set', { key, value });
143
+ },
144
+ delete: async (key: string) => {
145
+ await permissions.ensure('secrets');
146
+ return inv('secrets.delete', { key });
147
+ },
148
+ list: async () => {
149
+ await permissions.ensure('secrets');
150
+ return (await inv('secrets.list', undefined)).keys;
151
+ },
152
+ },
153
+ serve: {
154
+ start: async (dir?: string) => {
155
+ await permissions.ensure('serve');
156
+ return inv('serve.start', { dir });
157
+ },
158
+ stop: async (port: number) => {
159
+ await permissions.ensure('serve');
160
+ return inv('serve.stop', { port });
161
+ },
162
+ },
163
+ workers: {
164
+ register: async (path: string, boot = false) => {
165
+ await permissions.ensure('workers');
166
+ return (await inv('workers.register', { path, boot })).id;
167
+ },
168
+ unregister: async (id: string) => {
169
+ await permissions.ensure('workers');
170
+ return inv('workers.unregister', { id });
171
+ },
172
+ list: async () => {
173
+ await permissions.ensure('workers');
174
+ return (await inv('workers.list', undefined)).ids;
175
+ },
26
176
  },
27
177
  };
28
178
 
@@ -11,17 +11,22 @@ import type { Retriever, RetrievalResult, RetrieveOptions } from '../shared/Retr
11
11
  export interface WebSearchLike {
12
12
  search(
13
13
  params: { query: string; limit?: number },
14
- options?: { userId?: string },
14
+ options?: { userId?: string; token?: string },
15
15
  ): Promise<{ items: { url: string; title: string; snippet: string }[] }>;
16
16
  summarize?(
17
17
  params: { url?: string; text?: string },
18
- options?: { userId?: string },
18
+ options?: { userId?: string; token?: string },
19
19
  ): Promise<{ output: string }>;
20
20
  }
21
21
 
22
22
  export interface WebRetrieverOptions {
23
23
  /** Bill the search proxy to this user (charge context). */
24
24
  userId?: string;
25
+ /**
26
+ * The end user's ugly.bot session JWT. When set, the search proxy bills the
27
+ * USER (not the app owner). Omit for the default owner-billed behavior.
28
+ */
29
+ userToken?: string;
25
30
  /** Default result count. */
26
31
  limit?: number;
27
32
  }
@@ -46,7 +51,7 @@ export class WebRetriever implements Retriever {
46
51
  const limit = o?.limit ?? this.opts.limit ?? 8;
47
52
  const { items } = await this.provider.search(
48
53
  { query, limit },
49
- { userId: this.opts.userId },
54
+ { userId: this.opts.userId, token: this.opts.userToken },
50
55
  );
51
56
  const n = Math.max(items.length, 1);
52
57
  return items.map((it, i) => ({
@@ -69,7 +74,7 @@ export class WebRetriever implements Retriever {
69
74
  try {
70
75
  const { output } = await this.provider.summarize(
71
76
  { url: result.source.url },
72
- { userId: this.opts.userId },
77
+ { userId: this.opts.userId, token: this.opts.userToken },
73
78
  );
74
79
  if (output) return output;
75
80
  } catch {
@@ -23,9 +23,15 @@ import type { WebSearchOptions, WebSearchProvider } from '../types.js';
23
23
 
24
24
  const DEFAULT_SEARCH_PROXY_URL = 'https://api.ugly.bot/v1/search';
25
25
 
26
- function resolveSearchEndpoint(op: string): { url: string; token: string } {
26
+ function resolveSearchEndpoint(
27
+ op: string,
28
+ userToken?: string,
29
+ ): { url: string; token: string } {
27
30
  const base = process.env['SEARCH_PROXY_URL'] ?? DEFAULT_SEARCH_PROXY_URL;
31
+ // A user session JWT (when supplied) bills the user via resolveChargePayer;
32
+ // otherwise fall back to the app's owner-billed proxy token.
28
33
  const token =
34
+ userToken ??
29
35
  process.env['SEARCH_PROXY_TOKEN'] ??
30
36
  process.env['UGLY_BOT_TOKEN'] ??
31
37
  '';
@@ -42,8 +48,9 @@ function resolveSearchEndpoint(op: string): { url: string; token: string } {
42
48
  async function searchProxyRequest<T>(
43
49
  op: string,
44
50
  body: Record<string, unknown>,
51
+ userToken?: string,
45
52
  ): Promise<T> {
46
- const { url, token } = resolveSearchEndpoint(op);
53
+ const { url, token } = resolveSearchEndpoint(op, userToken);
47
54
  const controller = new AbortController();
48
55
  const timer = setTimeout(() => controller.abort(), 30_000);
49
56
  try {
@@ -90,6 +97,7 @@ export const uglyBotWebSearchProvider: WebSearchProvider = {
90
97
  { query: params.query, limit: params.limit },
91
98
  options?.userId,
92
99
  ),
100
+ options?.token,
93
101
  );
94
102
  },
95
103
 
@@ -103,6 +111,7 @@ export const uglyBotWebSearchProvider: WebSearchProvider = {
103
111
  { url: params.url, text: params.text },
104
112
  options?.userId,
105
113
  ),
114
+ options?.token,
106
115
  );
107
116
  return {
108
117
  output: result.summary,
@@ -117,6 +126,7 @@ export const uglyBotWebSearchProvider: WebSearchProvider = {
117
126
  return searchProxyRequest<WebSearchOutput>(
118
127
  'enrich-web',
119
128
  withChargeContext({ query: params.query }, options?.userId),
129
+ options?.token,
120
130
  );
121
131
  },
122
132
 
@@ -127,6 +137,7 @@ export const uglyBotWebSearchProvider: WebSearchProvider = {
127
137
  return searchProxyRequest<WebSearchOutput>(
128
138
  'enrich-news',
129
139
  withChargeContext({ query: params.query }, options?.userId),
140
+ options?.token,
130
141
  );
131
142
  },
132
143
  };
@@ -210,6 +210,13 @@ export interface ImageGenProvider {
210
210
 
211
211
  export interface WebSearchOptions {
212
212
  userId?: string;
213
+ /**
214
+ * The end user's ugly.bot session JWT. When set, the search proxy is called
215
+ * with THIS token instead of the app's `SEARCH_PROXY_TOKEN`, so ugly.bot bills
216
+ * the user (via `resolveChargePayer`) instead of the app owner. Omit to keep
217
+ * the default owner-billed behavior.
218
+ */
219
+ token?: string;
213
220
  }
214
221
 
215
222
  export interface WebSearchProvider {