Skip to content

Commit 57019c7

Browse files
authored
Merge pull request #9 from Unity-Lab-AI/codex/fix-pollinations-token-endpoint-issue
Support Pollinations tokens from URL parameters
2 parents 36c8532 + a8798c8 commit 57019c7

3 files changed

Lines changed: 309 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,9 @@ expects the token to be provided at runtime so it is never bundled into the stat
4242
- **Local development** – Either define `POLLI_TOKEN`/`VITE_POLLI_TOKEN` in your shell when running
4343
`npm run dev`, add a `<meta name="pollinations-token" ...>` tag to `index.html`, or inject
4444
`window.__POLLINATIONS_TOKEN__` before the application bootstraps.
45+
- **Static overrides** – When a dynamic endpoint is unavailable, append a `token` query parameter
46+
to the page URL (e.g. `https://example.github.io/chatdemo/?token=your-secret`). The application
47+
will capture the token, remove it from the visible URL, and apply it to subsequent Pollinations
48+
requests.
4549

4650
If the token cannot be resolved the UI remains disabled and an error is shown in the status banner.

src/pollinations-client.js

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ async function ensureToken() {
3232
}
3333

3434
async function resolveToken() {
35-
const attempts = [fetchTokenFromApi, readTokenFromMeta, readTokenFromWindow, readTokenFromEnv];
35+
const attempts = [
36+
readTokenFromUrl,
37+
readTokenFromMeta,
38+
readTokenFromWindow,
39+
readTokenFromEnv,
40+
fetchTokenFromApi,
41+
];
3642
const errors = [];
3743

3844
for (const attempt of attempts) {
@@ -101,6 +107,40 @@ async function fetchTokenFromApi() {
101107
}
102108
}
103109

110+
function readTokenFromUrl() {
111+
const location = getCurrentLocation();
112+
if (!location) {
113+
return { token: null, source: 'url', error: new Error('Location is unavailable.') };
114+
}
115+
116+
const { url, searchParams, hashParams, rawFragments } = parseLocation(location);
117+
const tokenKeys = new Set();
118+
const candidates = [];
119+
120+
collectTokenCandidates(searchParams, tokenKeys, candidates);
121+
collectTokenCandidates(hashParams, tokenKeys, candidates);
122+
123+
if (candidates.length === 0 && rawFragments.length > 0) {
124+
const regex = /(token[^=:#/?&]*)([:=])([^#&/?]+)/gi;
125+
for (const fragment of rawFragments) {
126+
let match;
127+
while ((match = regex.exec(fragment))) {
128+
tokenKeys.add(match[1]);
129+
candidates.push(match[3]);
130+
}
131+
}
132+
}
133+
134+
const token = extractTokenValue(candidates);
135+
if (!token) {
136+
return { token: null, source: 'url' };
137+
}
138+
139+
sanitizeUrlToken(location, url, tokenKeys);
140+
141+
return { token, source: 'url' };
142+
}
143+
104144
function readTokenFromMeta() {
105145
if (typeof document === 'undefined') {
106146
return { token: null, source: 'meta', error: new Error('Document is unavailable.') };
@@ -170,6 +210,156 @@ function readTokenFromEnv() {
170210
return { token, source: 'env' };
171211
}
172212

213+
function getCurrentLocation() {
214+
if (typeof window !== 'undefined' && window?.location) {
215+
return window.location;
216+
}
217+
if (typeof globalThis !== 'undefined' && globalThis?.location) {
218+
return globalThis.location;
219+
}
220+
return null;
221+
}
222+
223+
function parseLocation(location) {
224+
const result = {
225+
url: null,
226+
searchParams: new URLSearchParams(),
227+
hashParams: new URLSearchParams(),
228+
rawFragments: [],
229+
};
230+
231+
let baseHref = '';
232+
if (typeof location.href === 'string' && location.href) {
233+
baseHref = location.href;
234+
} else {
235+
const origin = typeof location.origin === 'string' ? location.origin : 'http://localhost';
236+
const path = typeof location.pathname === 'string' ? location.pathname : '/';
237+
const search = typeof location.search === 'string' ? location.search : '';
238+
const hash = typeof location.hash === 'string' ? location.hash : '';
239+
baseHref = `${origin.replace(/\/?$/, '')}${path.startsWith('/') ? path : `/${path}`}${search}${hash}`;
240+
}
241+
242+
try {
243+
const base = typeof location.origin === 'string' && location.origin ? location.origin : undefined;
244+
result.url = base ? new URL(baseHref, base) : new URL(baseHref);
245+
} catch {
246+
try {
247+
result.url = new URL(baseHref, 'http://localhost');
248+
} catch {
249+
result.url = null;
250+
}
251+
}
252+
253+
if (result.url) {
254+
result.searchParams = new URLSearchParams(result.url.searchParams);
255+
const hash = typeof result.url.hash === 'string' ? result.url.hash.replace(/^#/, '') : '';
256+
if (hash) {
257+
result.hashParams = new URLSearchParams(hash);
258+
result.rawFragments.push(hash);
259+
}
260+
} else {
261+
const search = typeof location.search === 'string' ? location.search.replace(/^\?/, '') : '';
262+
const hash = typeof location.hash === 'string' ? location.hash.replace(/^#/, '') : '';
263+
result.searchParams = new URLSearchParams(search);
264+
result.hashParams = new URLSearchParams(hash);
265+
if (hash) {
266+
result.rawFragments.push(hash);
267+
}
268+
}
269+
270+
const hrefFragment = typeof location.href === 'string' ? location.href : '';
271+
if (hrefFragment) {
272+
result.rawFragments.push(hrefFragment);
273+
}
274+
275+
return result;
276+
}
277+
278+
function collectTokenCandidates(params, tokenKeys, candidates) {
279+
if (!params) return;
280+
for (const key of params.keys()) {
281+
if (typeof key !== 'string') continue;
282+
if (!key.toLowerCase().includes('token')) continue;
283+
tokenKeys.add(key);
284+
const values = params.getAll(key);
285+
for (const value of values) {
286+
candidates.push(value);
287+
}
288+
}
289+
}
290+
291+
function sanitizeUrlToken(location, url, tokenKeys) {
292+
if (!location || !tokenKeys || tokenKeys.size === 0) {
293+
return;
294+
}
295+
296+
const effectiveUrl = url ?? parseLocation(location).url;
297+
if (!effectiveUrl) {
298+
return;
299+
}
300+
301+
let modified = false;
302+
for (const key of tokenKeys) {
303+
if (effectiveUrl.searchParams.has(key)) {
304+
effectiveUrl.searchParams.delete(key);
305+
modified = true;
306+
}
307+
}
308+
309+
const originalHash = effectiveUrl.hash;
310+
if (typeof originalHash === 'string' && originalHash.length > 1) {
311+
const hashParams = new URLSearchParams(originalHash.slice(1));
312+
let hashModified = false;
313+
for (const key of tokenKeys) {
314+
if (hashParams.has(key)) {
315+
hashParams.delete(key);
316+
hashModified = true;
317+
}
318+
}
319+
if (hashModified) {
320+
const nextHash = hashParams.toString();
321+
effectiveUrl.hash = nextHash ? `#${nextHash}` : '';
322+
modified = true;
323+
}
324+
}
325+
326+
if (!modified) {
327+
return;
328+
}
329+
330+
const history =
331+
(typeof window !== 'undefined' && window?.history) ||
332+
(typeof globalThis !== 'undefined' && globalThis?.history) ||
333+
null;
334+
const nextUrl = effectiveUrl.toString();
335+
336+
if (history?.replaceState) {
337+
try {
338+
history.replaceState(history.state ?? null, '', nextUrl);
339+
return;
340+
} catch {
341+
// ignore history errors
342+
}
343+
}
344+
345+
if (typeof location.assign === 'function') {
346+
try {
347+
location.assign(nextUrl);
348+
return;
349+
} catch {
350+
// ignore assignment errors
351+
}
352+
}
353+
354+
if ('href' in location) {
355+
try {
356+
location.href = nextUrl;
357+
} catch {
358+
// ignore inability to mutate href
359+
}
360+
}
361+
}
362+
173363
function determineDevelopmentEnvironment(importMetaEnv, processEnv) {
174364
if (importMetaEnv && typeof importMetaEnv.DEV !== 'undefined') {
175365
return !!importMetaEnv.DEV;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import assert from 'node:assert/strict';
2+
import { createPollinationsClient, __testing } from '../src/pollinations-client.js';
3+
4+
export const name = 'Pollinations client resolves tokens from URL parameters';
5+
6+
function createStubResponse(status = 404) {
7+
return {
8+
status,
9+
ok: status >= 200 && status < 300,
10+
headers: {
11+
get() {
12+
return null;
13+
},
14+
},
15+
async json() {
16+
return {};
17+
},
18+
async text() {
19+
return '';
20+
},
21+
};
22+
}
23+
24+
export async function run() {
25+
const originalFetch = globalThis.fetch;
26+
const originalWindow = globalThis.window;
27+
const originalDocument = globalThis.document;
28+
const originalLocation = globalThis.location;
29+
const originalHistory = globalThis.history;
30+
31+
try {
32+
globalThis.fetch = async () => createStubResponse(404);
33+
34+
const url = new URL('https://demo.example.com/chat/?foo=bar&token=url-token#pane');
35+
36+
const location = {
37+
href: url.toString(),
38+
origin: url.origin,
39+
pathname: url.pathname,
40+
search: url.search,
41+
hash: url.hash,
42+
};
43+
44+
const historyCalls = [];
45+
const history = {
46+
state: null,
47+
replaceState(state, _title, newUrl) {
48+
this.state = state;
49+
historyCalls.push(newUrl);
50+
const parsed = new URL(newUrl);
51+
location.href = parsed.toString();
52+
location.search = parsed.search;
53+
location.hash = parsed.hash;
54+
},
55+
};
56+
57+
globalThis.window = {
58+
location,
59+
history,
60+
};
61+
globalThis.location = location;
62+
globalThis.history = history;
63+
globalThis.document = {
64+
querySelector() {
65+
return null;
66+
},
67+
location: { origin: url.origin },
68+
};
69+
70+
__testing.resetTokenCache();
71+
72+
const { client, tokenSource } = await createPollinationsClient();
73+
assert.equal(tokenSource, 'url');
74+
75+
const token = await client._auth.getToken();
76+
assert.equal(token, 'url-token');
77+
78+
assert.ok(historyCalls.length >= 1, 'history.replaceState should be invoked to clean the URL');
79+
const cleanedUrl = new URL(location.href);
80+
assert.equal(cleanedUrl.searchParams.has('token'), false, 'token should be stripped from the query string');
81+
} finally {
82+
if (originalFetch) {
83+
globalThis.fetch = originalFetch;
84+
} else {
85+
delete globalThis.fetch;
86+
}
87+
88+
if (typeof originalWindow === 'undefined') {
89+
delete globalThis.window;
90+
} else {
91+
globalThis.window = originalWindow;
92+
}
93+
94+
if (typeof originalLocation === 'undefined') {
95+
delete globalThis.location;
96+
} else {
97+
globalThis.location = originalLocation;
98+
}
99+
100+
if (typeof originalDocument === 'undefined') {
101+
delete globalThis.document;
102+
} else {
103+
globalThis.document = originalDocument;
104+
}
105+
106+
if (typeof originalHistory === 'undefined') {
107+
delete globalThis.history;
108+
} else {
109+
globalThis.history = originalHistory;
110+
}
111+
112+
__testing.resetTokenCache();
113+
}
114+
}

0 commit comments

Comments
 (0)