FreeLattice · Celeste reading home

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

Archive pages whose extracted text matched this section: 1427, 1428, 1429. Unmatched pages stay on the source record with the checked page method. They are not guessed into a section.

No topic tag was earned for this page under the recorded evidence rule.

 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');
+