Skip to content

Commit 5ab16c7

Browse files
committed
feat(extensions): unified extension store backend (bundle installer + catalog + trust)
Implements Phases A+B of the unified extension store (docs/plans/2026-06-26-unified-extension-store-design.md): one catalog, trust model, and install record over the two existing backends (in-process integration artifacts + containerized service repos). Phase A — bundle install foundation: - extension.json schema + validator (packages/core extension-schema.ts), the bundle primitive: N integrations (sha256-pinned) + N services (git ref-pinned) as one versioned unit. - service --ref pinning + managed_by_extension on service_repository; fixes a latent bug where the strip-.git clone made refreshRepo's fetch/reset/rev-parse resolve against the monorepo, not the clone — refresh now re-clones into a tmp dir, validates, then atomically swaps. - orchestrated atomic installer (extension-installer.ts): register+pin service repos -> enable -> render -> start; install integration artifacts -> hot reload; record installed_extension(+components); best-effort rollback; remove uninstalls exactly what it placed. Phase B — catalog + trust backend: - deterministic service security rating from declared capabilities (security-rating.ts), on top of the existing community sandbox. - extension-store.ts: multi-source catalog, source-anchored trust tiers (verified vs community), removed/critical kill-switch, manifest resolution, folds the legacy integration catalog as degenerate extensions. - /admin/extensions routes (catalog/installed/install/update/remove/ sources/refresh) + extension.install/extension.remove jobs. - installed_extension + installed_extension_component tables; managed_by_extension on installed_integration. Migrations 0008 (service repo pinning) + 0009 (installed_extension).
1 parent 612f696 commit 5ab16c7

24 files changed

Lines changed: 7111 additions & 87 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { index, jsonb, pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core";
2+
import { user } from "./auth-schema";
3+
4+
// An installed Extension *bundle* — the parent record that ties together the
5+
// per-component substrate (installed_integration rows + service_repository rows)
6+
// so a bundle installs/updates/removes as one version-coupled unit. A degenerate
7+
// single-component extension is the common case (a lone integration or service).
8+
export const installedExtension = pgTable(
9+
"installed_extension",
10+
{
11+
id: text("id").primaryKey(),
12+
name: text("name").notNull(),
13+
// Catalog/source URL the bundle was resolved from (null for raw-URL installs).
14+
sourceUrl: text("source_url"),
15+
// Effective trust tier resolved at install: built-in | verified | community.
16+
sourceTrust: text("source_trust").notNull().default("community"),
17+
installedVersion: text("installed_version").notNull(),
18+
// Snapshot of the resolved extension.json we installed (what to remove/update).
19+
manifest: jsonb("manifest"),
20+
installedAt: timestamp("installed_at").defaultNow().notNull(),
21+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
22+
installedBy: text("installed_by").references(() => user.id, { onDelete: "set null" }),
23+
},
24+
(table) => [index("installedExtension_sourceTrust_idx").on(table.sourceTrust)],
25+
);
26+
27+
// One row per component an extension installed, linking to the per-component
28+
// substrate (installed_integration.id for integrations, the service id for
29+
// services). Lets uninstall remove exactly what the extension placed.
30+
export const installedExtensionComponent = pgTable(
31+
"installed_extension_component",
32+
{
33+
extensionId: text("extension_id")
34+
.notNull()
35+
.references(() => installedExtension.id, { onDelete: "cascade" }),
36+
kind: text("kind").notNull(), // "integration" | "service"
37+
componentId: text("component_id").notNull(),
38+
},
39+
(table) => [
40+
primaryKey({ columns: [table.extensionId, table.kind, table.componentId] }),
41+
index("installedExtensionComponent_componentId_idx").on(table.componentId),
42+
],
43+
);
44+
45+
export type InstalledExtensionRow = typeof installedExtension.$inferSelect;
46+
export type InstalledExtensionComponentRow = typeof installedExtensionComponent.$inferSelect;

apps/api/src/db/installed-integration-schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export const installedIntegration = pgTable(
1111
installedAt: timestamp("installed_at").defaultNow().notNull(),
1212
updatedAt: timestamp("updated_at").defaultNow().notNull(),
1313
installedBy: text("installed_by").references(() => user.id, { onDelete: "set null" }),
14+
// Extension id that owns this integration (installed as part of a bundle).
15+
// The standalone integration store skips update/remove of managed rows.
16+
managedByExtension: text("managed_by_extension"),
1417
},
1518
(table) => [index("installedIntegration_sourceType_idx").on(table.sourceType)],
1619
);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "service_repository" ADD COLUMN "pinned_ref" text;--> statement-breakpoint
2+
ALTER TABLE "service_repository" ADD COLUMN "managed_by_extension" text;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
CREATE TABLE "installed_extension" (
2+
"id" text PRIMARY KEY NOT NULL,
3+
"name" text NOT NULL,
4+
"source_url" text,
5+
"source_trust" text DEFAULT 'community' NOT NULL,
6+
"installed_version" text NOT NULL,
7+
"manifest" jsonb,
8+
"installed_at" timestamp DEFAULT now() NOT NULL,
9+
"updated_at" timestamp DEFAULT now() NOT NULL,
10+
"installed_by" text
11+
);
12+
--> statement-breakpoint
13+
CREATE TABLE "installed_extension_component" (
14+
"extension_id" text NOT NULL,
15+
"kind" text NOT NULL,
16+
"component_id" text NOT NULL,
17+
CONSTRAINT "installed_extension_component_extension_id_kind_component_id_pk" PRIMARY KEY("extension_id","kind","component_id")
18+
);
19+
--> statement-breakpoint
20+
ALTER TABLE "installed_integration" ADD COLUMN "managed_by_extension" text;--> statement-breakpoint
21+
ALTER TABLE "installed_extension" ADD CONSTRAINT "installed_extension_installed_by_user_id_fk" FOREIGN KEY ("installed_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
22+
ALTER TABLE "installed_extension_component" ADD CONSTRAINT "installed_extension_component_extension_id_installed_extension_id_fk" FOREIGN KEY ("extension_id") REFERENCES "public"."installed_extension"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
23+
CREATE INDEX "installedExtension_sourceTrust_idx" ON "installed_extension" USING btree ("source_trust");--> statement-breakpoint
24+
CREATE INDEX "installedExtensionComponent_componentId_idx" ON "installed_extension_component" USING btree ("component_id");

0 commit comments

Comments
 (0)