Public, free to read, reuse, and adapt — Kirk Patrick Miller's notice for this edition. Not a patent grant.
FLINT_NEXT_TREE_GATHERING_API_KEYS_v0.1.txt · c07
44d8662586 · words 1200 · characters 58353–68516
Previous section · Next section
Exact section text · Full source · Source index
keyTail(id);
+ kIn.value = '';
+ kIn.placeholder = tail ? ('saved, ends in ' + tail + ' (leave empty to keep it)') : ('paste your ' + p.name + ' key');
+ getLink.textContent = 'Get your ' + p.name + ' key (opens ' + p.name + ')';
+ getLink.href = p.keyUrl;
+ note.textContent = honestNote(id);
+ }
+
+ function paintSaved() {
+ while (saved.firstChild) saved.removeChild(saved.firstChild);
+ savedProviders().forEach(function (id) {
+ var row = el('p', 'cloud-key-row');
+ var place = keyPlace(id) === 'visit' ? 'this visit only' : 'on this device';
+ row.appendChild(document.createTextNode(PROVIDERS[id].name + ' key, ends in ' + keyTail(id) + ', kept ' + place + '. '));
+ var f = el('button', 'cloud-key-forget', 'Forget');
+ f.type = 'button';
+ f.setAttribute('data-cloud-forget', id);
+ f.setAttribute('aria-label', 'Forget the ' + PROVIDERS[id].name + ' key');
+ f.addEventListener('click', function () {
+ forgetKey(id);
+ status.textContent = 'Forgotten. The ' + PROVIDERS[id].name + ' key is gone from this browser.';
+ paintSaved();
+ paintProvider();
+ });
+ row.appendChild(f);
+ saved.appendChild(row);
+ });
+ }
+
+ pSel.addEventListener('change', paintProvider);
+ save.addEventListener('click', function () {
+ var id = pSel.value;
+ var p = PROVIDERS[id];
+ var model = cleanModel(mIn.value);
+ if (!model) { status.textContent = 'Type a model name for ' + p.name + ' (letters, numbers, . _ : / - only).'; return; }
+ var typed = String(kIn.value || '');
+ if (typed) {
+ var r = saveKey(id, typed, !!visit.checked);
+ kIn.value = '';
+ if (!r.ok) {
+ status.textContent = r.reason === 'key'
+ ? 'That does not look like a key (no spaces, at least 8 characters). Nothing was saved.'
+ : 'This browser would not keep the key. Nothing was saved.';
+ return;
+ }
+ } else if (!getKey(id)) {
+ status.textContent = 'Paste your ' + p.name + ' key first. Nothing was saved.';
+ return;
+ }
+ var seat = addSeat(id, model);
+ paintSaved();
+ paintProvider();
+ status.textContent = 'Saved. ' + model + ' through ' + p.name + ' can sit in any chair. Nothing was sent yet.';
+ if (seat && typeof onSeat === 'function') onSeat(seat);
+ });
+
+ paintProvider();
+ paintSaved();
+ host.appendChild(box);
+ return box;
+ }
+
+ window.TreeCloudMinds = {
+ MARK: MARK,
+ PROVIDERS: PROVIDERS,
+ ORDER: ORDER,
+ isCloud: isCloud,
+ providerIdOf: providerIdOf,
+ providerName: providerName,
+ saveKey: saveKey,
+ forgetKey: forgetKey,
+ keyPlace: keyPlace,
+ keyTail: keyTail,
+ savedProviders: savedProviders,
+ addSeat: addSeat,
+ getSeats: getSeats,
+ seatOptions: seatOptions,
+ buildRequest: buildRequest,
+ parseReply: parseReply,
+ send: send,
+ heartLine: heartLine,
+ honestNote: honestNote,
+ mountKeyPanel: mountKeyPanel
+ };
+})();
diff --git a/docs/modules/tree-gathering-api-keys.test.js b/docs/modules/tree-gathering-api-keys.test.js
new file mode 100644
index 0000000..b593da8
--- /dev/null
+++ b/docs/modules/tree-gathering-api-keys.test.js
@@ -0,0 +1,249 @@
+#!/usr/bin/env node
+// v-tree-gathering-api-keys-v0: bring your own key, seat a cloud mind in a Gathering chair.
+// Guards: the provider list is fixed and https only; the key lives only in this browser
+// (localStorage, or sessionStorage for this visit) and goes only to the chosen provider,
+// in its own header, never in a URL; seats and binds never hold the key; a cloud chair
+// says where words go; files stay on this machine; no confirm(), no innerHTML.
+// No real network: fetch is a spy.
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var vm = require('vm');
+var assert = require('assert');
+
+var dir = __dirname;
+var src = fs.readFileSync(path.join(dir, 'tree-cloud-minds.js'), 'utf8');
+var thread = fs.readFileSync(path.join(dir, 'garden-thread.js'), 'utf8');
+var rooms = fs.readFileSync(path.join(dir, 'garden-rooms.js'), 'utf8');
+var css = fs.readFileSync(path.join(dir, 'garden-rooms.css'), 'utf8');
+
+function Store() { this.m = {}; }
+Store.prototype.getItem = function (k) { return Object.prototype.hasOwnProperty.call(this.m, k) ? this.m[k] : null; };
+Store.prototype.setItem = function (k, v) { this.m[k] = String(v); };
+Store.prototype.removeItem = function (k) { delete this.m[k]; };
+
+var calls = [];
+var nextReply = null;
+function fakeFetch(url, opts) {
+ calls.push({ url: String(url), opts: opts });
+ var r = nextReply || { status: 200, json: {} };
+ return Promise.resolve({
+ ok: r.status >= 200 && r.status < 300,
+ status: r.status,
+ json: function () { return r.throwJson ? Promise.reject(new Error('bad')) : Promise.resolve(r.json); }
+ });
+}
+
+var local = new Store();
+var session = new Store();
+var sandbox = { console: console, setTimeout: setTimeout, clearTimeout: clearTimeout, JSON: JSON, Promise: Promise,
+ encodeURIComponent: encodeURIComponent, fetch: fakeFetch };
+sandbox.window = sandbox;
+sandbox.localStorage = local;
+sandbox.sessionStorage = session;
+vm.createContext(sandbox);
+vm.runInContext(src, sandbox, { filename: 'tree-cloud-minds.js' });
+var T = sandbox.TreeCloudMinds;
+assert.ok(T, 'TreeCloudMinds is on window');
+
+// 1. Fixed list, https only, exact hosts.
+var HOSTS = {
+ groq: 'api.groq.com', openrouter: 'openrouter.ai', openai: 'api.openai.com', anthropic: 'api.anthropic.com',
+ google: 'generativelanguage.googleapis.com', xai: 'api.x.ai', mistral: 'api.mistral.ai',
+ deepseek: 'api.deepseek.com', together: 'api.together.xyz'
+};
+assert.deepStrictEqual(Object.keys(T.PROVIDERS).sort(), Object.keys(HOSTS).sort(), 'fixed provider list');
+Object.keys(HOSTS).forEach(function (id) {
+ var u = new URL(T.PROVIDERS[id].url);
+ assert.strictEqual(u.protocol, 'https:', id + ' is https');
+ assert.strictEqual(u.host, HOSTS[id], id + ' host');
+ assert.ok(T.PROVIDERS[id].models.length >= 1, id + ' has suggestions');
+});
+assert.ok(!/custom/i.test(Object.keys(T.PROVIDERS).join(',')), 'no custom hosts in v0.1');
+
+// 2. Keys: device vs this visit; forget clears both; junk refused.
+assert.strictEqual(T.saveKey('groq', 'has space key', false).ok, false, 'a key with spaces is refused');
+assert.strictEqual(T.saveKey('nope', 'gsk_abcdefgh1234', false).ok, false, 'unknown provider refused');
+assert.strictEqual(T.saveKey('groq', ' gsk_abcdefgh1234 ', false).ok, true);
+assert.strictEqual(T.keyPlace('groq'), 'device');
+assert.ok(local.getItem('fl_tree_cloud_keys').indexOf('gsk_abcdefgh1234') !== -1, 'device key in localStorage');
+assert.strictEqual(T.saveKey('groq', 'gsk_visitonly9999', true).ok, true);
+assert.strictEqual(T.keyPlace('groq'), 'visit', 'visit-only moves the key to sessionStorage');
+assert.ok(local.getItem('fl_tree_cloud_keys').indexOf('gsk_') === -1, 'and out of localStorage');
+assert.strictEqual(T.keyTail('groq'), '9999');
+T.forgetKey('groq');
+assert.strictEqual(T.keyPlace('groq'), '', 'forget clears it');
+assert.ok((local.getItem('fl_tree_cloud_keys') + session.getItem('fl_tree_cloud_keys')).indexOf('gsk_') === -1, 'gone from both stores');
+
+// 3. Seats never hold a key; only providers with a key are offered.
+T.saveKey('anthropic', 'sk-ant-test-0000aaaa', false);
+T.saveKey('openai', 'sk-test-openai-1111', false);
+T.saveKey('google', 'AIza-test-google-2222', false);
+assert.ok(T.addSeat('anthropic', 'claude-sonnet-4-20250514'));
+assert.ok(T.addSeat('openai', 'gpt-4.1-mini'));
+assert.ok(T.addSeat('google', 'gemini-2.5-flash'));
+assert.ok(T.addSeat('groq', 'llama-3.3-70b-versatile'), 'a seat may be saved before its key');
+assert.strictEqual(T.addSeat('openai', 'bad name with spaces'), null, 'model names are checked');
+var seatsRaw = local.getItem('fl_tree_cloud_seats');
+assert.ok(!/sk-|AIza|gsk_/.test(seatsRaw), 'no key in the seats store');
+var opts = T.seatOptions();
+assert.deepStrictEqual(opts.map(function (s) { return s.url; }).sort(), ['cloud:anthropic', 'cloud:google', 'cloud:openai'], 'groq seat waits for its key');
+opts.forEach(function (s) {
+ assert.ok(!/sk-|AIza|gsk_/.test(JSON.stringify(s)), 'no key in a seat option');
+ assert.strictEqual(s.cloud, true);
+ assert.strictEqual(s.tag, 'general');
+});
+assert.strictEqual(T.isCloud({ url: 'cloud:openai' }), true);
+assert.strictEqual(T.isCloud({ url: 'http://127.0.0.1:11434/api/tags' }), false);
+assert.strictEqual(T.isCloud({ url: 'cloud:evil.example' }), false, 'unknown cloud ids are not cloud minds');
+
+var msgs = [{ role: 'system', content: 'Just talk.' }, { role: 'user', content: 'hello' }, { role: 'assistant', content: 'hi' }, { role: 'user', content: 'again' }];
+
+(async function () {
+ // 4. OpenAI shape.
+ calls = []; nextReply = { status: 200, json: { choices: [{ message: { content: 'from openai' } }] } };
+ var r1 = await T.send({ url: 'cloud:openai', model: 'gpt-4.1-mini' }, msgs);
+ assert.strictEqual(r1, 'from openai');
+ assert.strictEqual(calls.length, 1);
+ assert.strictEqual(calls[0].url, 'https://api.openai.com/v1/chat/completions');
+ assert.strictEqual(calls[0].opts.headers.Authorization, 'Bearer sk-test-openai-1111');
+ assert.strictEqual(calls[0].opts.credentials, 'omit');
+ assert.strictEqual(calls[0].opts.referrerPolicy, 'no-referrer');
+ var b1 = JSON.parse(calls[0].opts.body);
+ assert.strictEqual(b1.model, 'gpt-4.1-mini');
+ assert.strictEqual(b1.messages.length, 4);
+ assert.ok(calls[0].opts.body.indexOf('sk-') === -1, 'key never in the body');
+
+ // 5. Anthropic shape: x-api-key, browser header, system split, max_tokens.
+ calls = []; nextReply = { status: 200, json: { content: [{ type: 'text', text: 'from claude' }] } };
+ var r2 = await T.send({ url: 'cloud:anthropic', model: 'claude-sonnet-4-20250514' }, msgs);
+ assert.strictEqual(r2, 'from claude');
+ assert.strictEqual(calls[0].url, 'https://api.anthropic.com/v1/messages');
+