marketplace.officer.dev: the catalogue becomes a service
The plugins page read a TypeScript constant. It now reads a public API backed by its own database, and falls back to that constant when it cannot. ── Four pieces ── `src/databases/marketplace_db` — a second Postgres database, `officer_marketplace`. Two tables, split by owner and lifetime: `plugins` is the registry (app name, developer, clone URL) and is authoritative; `plugin_metadata` is a CACHE of what each repository says about itself, so the website can render without cloning three repos per page load. Keeping them apart is what makes the cache safe to be wrong — a refresh can never lose the fact that a plugin exists. `scripts/marketplace-sync.ts` — fills it from the plugin repositories. Reads each `manifest.ts` (imported, since a manifest is TypeScript), derives `has` from the directory rather than trusting a declaration, and rewrites SSH remotes to HTTPS: `example` and `offscale` had `git@gitea…` origins, which would have handed every installer a URL that only works on the machine that published it. `src/servers/marketplace/server.tsx` — its own process, its own port, two GET routes and no others. Not part of the platform server: it has no users, no sessions and no request it answers differently per caller, and mounting a public unauthenticated surface inside the process holding mail, a wallet and passkeys means writing permission-gate exemptions that later turn out to be too wide. `src/apps/marketplace` — the public site. A window, deliberately: nothing here installs anything, because installing means cloning a repo and starting a process on someone else's machine. Every plugin page ends at "install it from your own Officer". ── The platform side is the part that had to be careful ── `fetchCatalogue()` now answers live → cached → bundled, in that order, and never throws. The plugins page is where an owner manages plugins they ALREADY have, so the store being down must not take that page with it. Network responses are validated per entry — not against a hostile marketplace, which has already won if the owner installs what it lists, but against a wrong one: a proxy serving HTML, a half-deployed API, a field renamed on one side. A non-https `source` is refused outright because that string reaches `git clone`. 8 tests on the degradation path. Verified end to end against the running service: live catalogue, cached answer surviving the store dying, bundled fallback. ── One thing found on the way ── `bunfig.toml` and CLAUDE.md both document `bun install --no-frozen-lockfile` as the way to change a dependency. Bun 1.3.14 has no such flag — only `--frozen-lockfile` — so the documented escape hatch does not work. Sidestepped by not making marketplace_db a workspace member (relative imports, zero lockfile change), and `bun install` is frozen-clean again. Worth fixing the docs. tsgo clean, 873 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
// Same .env loading as officer_db's config: drizzle-kit runs as its own process from this directory and
|
||||
// inherits nothing from the server.
|
||||
const envPath = resolve(__dirname, '../../../.env');
|
||||
try {
|
||||
const text = readFileSync(envPath, 'utf-8');
|
||||
for (const line of text.split('\n')) {
|
||||
const match = line.match(/^(\w+)=(.*)$/);
|
||||
if (match) {
|
||||
const [, key, val] = match;
|
||||
if (!process.env[key!]) process.env[key!] = val!.replace(/^["']|["']$/g, '');
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Derived exactly the way the runtime derives it — see src/db.ts. Duplicated rather than imported
|
||||
// because drizzle-kit loads this file outside the workspace's module resolution.
|
||||
const url = (() => {
|
||||
if (process.env.MARKETPLACE_POSTGRES_URL) return process.env.MARKETPLACE_POSTGRES_URL;
|
||||
const base = process.env.POSTGRES_URL;
|
||||
if (!base) throw new Error('MARKETPLACE_POSTGRES_URL or POSTGRES_URL is required');
|
||||
const parsed = new URL(base);
|
||||
parsed.pathname = '/officer_marketplace';
|
||||
return parsed.toString();
|
||||
})();
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/schema.ts',
|
||||
out: './migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: { url },
|
||||
});
|
||||
Reference in New Issue
Block a user