the plugin, start to finish: connection, pass-through, panels, skill
OpenSEO ships its own MCP server and its own web UI, so this plugin holds the address, holds the credential, and is the door. There are no curated routes in front of it and there should not be: proxy-manager's router already records where that leads — a curated surface cannot keep up, and the plugin becomes a worse version of the thing it wraps. The door IS the security model, not a convenience. Upstream self-hosts with AUTH_MODE=local_noauth, so every request reaching the instance is trusted by design and their docs say to put it behind auth. Officer's gate is that auth. Two things follow and neither is optional: nothing here may publish OpenSEO's own port, and /_config is owner-only, because the address alone is enough to bypass this plugin entirely. The iframe therefore points at /api/openseo/ — this Officer's origin — and never at the instance, since an iframe src is a URL anybody can read out of the page and open directly. The probe asks /mcp for tools/list rather than fetching the home page. A web root answers 200 for a great many things that are not this; a JSON-RPC reply carrying tool names is the instance identifying itself. Saving with an empty key field KEEPS the stored one — absent means keep, empty string means clear. Without that distinction, changing the address from a form that cannot display the credential would silently erase it, and it would surface days later as an instance that had stopped answering. One table, deliberately: every project, keyword and crawl lives in the instance, so uninstalling this cannot destroy anybody's research. The skill says so too, because the temptation to cache a project list is exactly what would break it. The skill leads with cost. This is free software over DataForSEO, metered and billed to the owner per lookup, so it tells the agent not to sweep and to say what a thorough answer would spend before spending it. Tags are `Development` and `Homelab`. `Marketing` and `Research` would both be more accurate and neither exists in the union — extending it is a platform change rather than a plugin's to make.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
|
||||
|
||||
/**
|
||||
* The connection to an OpenSEO instance. One row, and the only thing this plugin stores.
|
||||
*
|
||||
* ── Why there is nothing else here ──
|
||||
*
|
||||
* OpenSEO keeps its own data — projects, keywords, rankings, crawls — in its own SQLite database inside
|
||||
* its container. None of it belongs here, and the temptation to cache "just the project list" should be
|
||||
* resisted for a reason worth writing down: the plugin installer drops a plugin's tables when the
|
||||
* "delete its data" checkbox is ticked. So the strongest thing that can be said about this plugin is a
|
||||
* consequence of this file being one table long — **no operation on it can destroy anybody's research.**
|
||||
* Uninstalling removes a URL and a key; every project survives in the container.
|
||||
*
|
||||
* ── The DataForSEO key lives here, not in the container's env ──
|
||||
*
|
||||
* OpenSEO takes `DATAFORSEO_API_KEY` from its environment, so an instance somebody else started already
|
||||
* has one and this column stays null. It exists for the case where Officer provisions the container —
|
||||
* the direction `todo/infra/opm-officer-s-own-process-manager` is heading — and it is stored encrypted
|
||||
* from the start so that day needs no migration and no window where a key sits in plaintext.
|
||||
*
|
||||
* It is a real cost, not a free tier: DataForSEO is metered and billed directly by them.
|
||||
*/
|
||||
export const openseoConnection = pgTable('openseo_connection', {
|
||||
id: serial('id').primaryKey(),
|
||||
|
||||
/** Where the instance is — the origin its UI and `/mcp` are served from. No trailing slash. */
|
||||
url: text('url').notNull(),
|
||||
|
||||
/**
|
||||
* Base64 of `login:password`, encrypted with `encryptSecret('openseo', …)` — its own purpose key, so
|
||||
* a leak of that one opens this column and nothing else.
|
||||
*
|
||||
* **Never returned across the API boundary**, at any level, to anybody including the owner. A form
|
||||
* asking "is one set" gets a boolean.
|
||||
*/
|
||||
dataForSeoKey: text('data_for_seo_key'),
|
||||
|
||||
/** From the last successful probe — what that instance said about itself. Diagnostic only. */
|
||||
probedVersion: text('probed_version'),
|
||||
|
||||
lastProbeAt: timestamp('last_probe_at', { withTimezone: true }),
|
||||
/**
|
||||
* Three states, not two — the same reasoning proxy-manager gives for using an integer. Null means
|
||||
* never probed, which reads differently from probed-and-failed.
|
||||
*/
|
||||
lastProbeOk: integer('last_probe_ok'),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type OpenSeoConnection = typeof openseoConnection.$inferSelect;
|
||||
Reference in New Issue
Block a user