@grapity/grapity 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1635,9 +1635,9 @@ var RegistryService = class {
1635
1635
  return { spec, version, compatReport, isNewSpec };
1636
1636
  }
1637
1637
  async listSpecs(filters) {
1638
- const specs2 = await this.store.listSpecs(filters);
1638
+ const specs3 = await this.store.listSpecs(filters);
1639
1639
  return Promise.all(
1640
- specs2.map(async (spec) => {
1640
+ specs3.map(async (spec) => {
1641
1641
  const latestVersion = await this.store.getLatestVersion(spec.name);
1642
1642
  return { ...spec, latestVersion: latestVersion ?? void 0 };
1643
1643
  })
@@ -1756,6 +1756,7 @@ var pushRoute = new Hono().post("/", async (c) => {
1756
1756
  }
1757
1757
  const store = c.get("store");
1758
1758
  const service = new RegistryService(store);
1759
+ const actor = c.get("actor") ?? body.pushedBy;
1759
1760
  try {
1760
1761
  const result = await service.pushSpec(body.content, body.name, {
1761
1762
  type: body.type,
@@ -1764,7 +1765,7 @@ var pushRoute = new Hono().post("/", async (c) => {
1764
1765
  sourceRepo: body.sourceRepo,
1765
1766
  tags: Array.isArray(body.tags) ? body.tags : void 0,
1766
1767
  gitRef: body.gitRef,
1767
- pushedBy: body.pushedBy,
1768
+ pushedBy: actor,
1768
1769
  prerelease: body.prerelease,
1769
1770
  force: body.force,
1770
1771
  reason: body.reason
@@ -1895,8 +1896,8 @@ var listRoute = new Hono3().get("/", async (c) => {
1895
1896
  const type = c.req.query("type");
1896
1897
  const owner = c.req.query("owner");
1897
1898
  const tags = c.req.query("tags")?.split(",");
1898
- const specs2 = await service.listSpecs({ type, owner, tags });
1899
- return c.json({ data: specs2 });
1899
+ const specs3 = await service.listSpecs({ type, owner, tags });
1900
+ return c.json({ data: specs3 });
1900
1901
  });
1901
1902
 
1902
1903
  // src/registry/routes/get-spec.ts
@@ -1929,7 +1930,8 @@ var deleteSpecRoute = new Hono5().delete(
1929
1930
  const name = c.req.param("name");
1930
1931
  const store = c.get("store");
1931
1932
  const service = new RegistryService(store);
1932
- const deleted = await service.deleteSpec(name);
1933
+ const actor = c.get("actor");
1934
+ const deleted = await service.deleteSpec(name, actor);
1933
1935
  if (!deleted) {
1934
1936
  return c.json({ error: "not_found", message: `Spec "${name}" not found`, statusCode: 404 }, 404);
1935
1937
  }
@@ -2530,8 +2532,7 @@ function switchTab(tab) {
2530
2532
  }
2531
2533
  var welcomeRoute = new Hono12().get("/", (c) => {
2532
2534
  const config = c.get("config");
2533
- const mode = config.database === "sqlite" ? "local" : "remote";
2534
- return c.html(buildPage(config.port, mode));
2535
+ return c.html(buildPage(config.port, "local"));
2535
2536
  });
2536
2537
 
2537
2538
  // src/registry/routes/push-gateway-config.ts
@@ -2671,6 +2672,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c) => {
2671
2672
  }
2672
2673
  const store = c.get("store");
2673
2674
  const service = new GatewayService(store, store);
2675
+ const actor = c.get("actor") ?? body.pushedBy;
2674
2676
  try {
2675
2677
  const result = await service.pushGatewayConfig({
2676
2678
  name: body.name,
@@ -2681,7 +2683,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c) => {
2681
2683
  environments: body.environments ?? {},
2682
2684
  callerIdentification: body.callerIdentification,
2683
2685
  content: body.content,
2684
- pushedBy: body.pushedBy
2686
+ pushedBy: actor
2685
2687
  });
2686
2688
  return c.json({ data: result }, 201);
2687
2689
  } catch (err) {
@@ -2872,7 +2874,6 @@ var ingestGatewayLogRoute = new Hono18().post("/ingest/:provider/:environment",
2872
2874
  await service.ingestLog(provider, environment, payload);
2873
2875
  return c.json({ status: "ok" }, 201);
2874
2876
  } catch (err) {
2875
- console.error("Gateway log ingest error:", err);
2876
2877
  return c.json({
2877
2878
  error: "bad_request",
2878
2879
  message: err instanceof Error ? err.message : "Invalid log payload",
@@ -2947,7 +2948,137 @@ var gatewayLogStatsRoute = new Hono21().get("/stats", async (c) => {
2947
2948
  });
2948
2949
  });
2949
2950
 
2951
+ // src/registry/auth/middleware.ts
2952
+ import { createRemoteJWKSet, jwtVerify } from "jose";
2953
+ var AuthError = class extends Error {
2954
+ constructor(statusCode, code, message) {
2955
+ super(message);
2956
+ this.statusCode = statusCode;
2957
+ this.code = code;
2958
+ this.name = "AuthError";
2959
+ }
2960
+ statusCode;
2961
+ code;
2962
+ };
2963
+ function buildKeycloakUrls(config) {
2964
+ const base = `${config.serverUrl}/realms/${config.realm}`;
2965
+ return {
2966
+ issuer: base,
2967
+ jwksUri: `${base}/protocol/openid-connect/certs`,
2968
+ tokenUrl: `${base}/protocol/openid-connect/token`
2969
+ };
2970
+ }
2971
+ function createAuthMiddleware(config, routeScopes) {
2972
+ if (config.auth?.mode !== "keycloak") {
2973
+ return async (_c, next) => await next();
2974
+ }
2975
+ const authConfig = config.auth;
2976
+ const { issuer, jwksUri } = buildKeycloakUrls(authConfig);
2977
+ const jwks = createRemoteJWKSet(new URL(jwksUri));
2978
+ const scopeByRoute = /* @__PURE__ */ new Map();
2979
+ for (const route of routeScopes) {
2980
+ const key = `${route.method.toUpperCase()}:${route.path}`;
2981
+ scopeByRoute.set(key, {
2982
+ operationId: route.operationId,
2983
+ scopes: route.scopes
2984
+ });
2985
+ }
2986
+ return async (c, next) => {
2987
+ const matchedPath = c.req.matchedRoutes.map((r) => r.path).filter((p) => p !== "/*").pop();
2988
+ const routeKey = matchedPath ? `${c.req.method}:${matchedPath}` : `${c.req.method}:${c.req.routePath}`;
2989
+ const required = scopeByRoute.get(routeKey);
2990
+ if (!required || required.scopes.length === 0) {
2991
+ return await next();
2992
+ }
2993
+ const authHeader = c.req.header("Authorization");
2994
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
2995
+ throw new AuthError(401, "unauthorized", "Bearer token required");
2996
+ }
2997
+ const token = authHeader.slice("Bearer ".length).trim();
2998
+ if (!token) {
2999
+ throw new AuthError(401, "unauthorized", "Bearer token required");
3000
+ }
3001
+ let payload;
3002
+ try {
3003
+ const result = await jwtVerify(token, jwks, {
3004
+ issuer,
3005
+ audience: authConfig.audience
3006
+ });
3007
+ payload = result.payload;
3008
+ } catch (err) {
3009
+ const message = err instanceof Error ? err.message : "Invalid token";
3010
+ throw new AuthError(401, "unauthorized", `Invalid or expired token: ${message}`);
3011
+ }
3012
+ const subject = payload.sub;
3013
+ if (!subject) {
3014
+ throw new AuthError(401, "unauthorized", "Token missing subject claim");
3015
+ }
3016
+ const grantedScopes = extractScopes(payload, authConfig.roleSource ?? "scope");
3017
+ const missing = required.scopes.filter((s) => !grantedScopes.has(s));
3018
+ if (missing.length > 0) {
3019
+ throw new AuthError(
3020
+ 403,
3021
+ "forbidden",
3022
+ `Missing required scope${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`
3023
+ );
3024
+ }
3025
+ c.set("actor", subject);
3026
+ c.set("claims", payload);
3027
+ await next();
3028
+ };
3029
+ }
3030
+ function extractScopes(payload, source) {
3031
+ if (source === "realm_access.roles") {
3032
+ const roles = payload.realm_access?.roles;
3033
+ return new Set(roles ?? []);
3034
+ }
3035
+ const scopeValue = payload.scope;
3036
+ if (typeof scopeValue === "string") {
3037
+ return new Set(scopeValue.split(/\s+/).filter(Boolean));
3038
+ }
3039
+ return /* @__PURE__ */ new Set();
3040
+ }
3041
+ function parseRouteScopes(spec) {
3042
+ const routes = [];
3043
+ const paths = spec.paths;
3044
+ if (!paths) return routes;
3045
+ for (const [path2, operations] of Object.entries(paths)) {
3046
+ for (const [method, operation] of Object.entries(operations)) {
3047
+ if (typeof operation !== "object" || operation === null) continue;
3048
+ const op = operation;
3049
+ const operationId = op.operationId;
3050
+ if (!operationId) continue;
3051
+ const security = op.security;
3052
+ const scopes = [];
3053
+ if (security) {
3054
+ for (const sec of security) {
3055
+ for (const [name, required] of Object.entries(sec)) {
3056
+ if (name === "keycloak" && Array.isArray(required)) {
3057
+ scopes.push(...required);
3058
+ }
3059
+ }
3060
+ }
3061
+ }
3062
+ routes.push({
3063
+ method: method.toUpperCase(),
3064
+ path: path2.replace(/\{([^}]+)\}/g, ":$1"),
3065
+ operationId,
3066
+ scopes
3067
+ });
3068
+ }
3069
+ }
3070
+ return routes;
3071
+ }
3072
+
2950
3073
  // src/registry/server.ts
3074
+ import { readFileSync } from "fs";
3075
+ import { fileURLToPath } from "url";
3076
+ import yaml6 from "js-yaml";
3077
+ function loadOpenApiSpec() {
3078
+ const path2 = fileURLToPath(new URL("../../openapi.yaml", import.meta.url));
3079
+ const content = readFileSync(path2, "utf-8");
3080
+ return yaml6.load(content);
3081
+ }
2951
3082
  function createApp(config, store) {
2952
3083
  const app = new Hono22();
2953
3084
  app.use("*", logger());
@@ -2958,6 +3089,21 @@ function createApp(config, store) {
2958
3089
  c.set("config", config);
2959
3090
  await next();
2960
3091
  });
3092
+ const routeScopes = parseRouteScopes(loadOpenApiSpec());
3093
+ app.use("*", createAuthMiddleware(config, routeScopes));
3094
+ app.onError((err, c) => {
3095
+ if (err instanceof AuthError) {
3096
+ return c.json(
3097
+ { error: err.code, message: err.message, statusCode: err.statusCode },
3098
+ err.statusCode
3099
+ );
3100
+ }
3101
+ console.error("Unhandled error:", err);
3102
+ return c.json(
3103
+ { error: "internal_error", message: "Internal server error", statusCode: 500 },
3104
+ 500
3105
+ );
3106
+ });
2961
3107
  app.route("/v1/specs", pushRoute);
2962
3108
  app.route("/v1/specs", validateRoute);
2963
3109
  app.route("/v1/specs", listRoute);
@@ -2986,8 +3132,7 @@ function createApp(config, store) {
2986
3132
  var defaultConfig = {
2987
3133
  port: 3750,
2988
3134
  database: "sqlite",
2989
- sqlitePath: void 0,
2990
- gracePeriodDays: 30
3135
+ auth: { mode: "none" }
2991
3136
  };
2992
3137
 
2993
3138
  // src/registry/storage/sqlite.ts
@@ -3493,27 +3638,575 @@ var SQLiteSpecStore = class {
3493
3638
  }
3494
3639
  };
3495
3640
 
3641
+ // src/registry/storage/postgresql.ts
3642
+ import { Pool } from "pg";
3643
+ import { drizzle as drizzle2 } from "drizzle-orm/node-postgres";
3644
+ import { migrate as migrate2 } from "drizzle-orm/node-postgres/migrator";
3645
+ import { eq as eq2, and as and2, desc as desc2, sql as sql2 } from "drizzle-orm";
3646
+
3647
+ // src/registry/storage/schema-pg.ts
3648
+ import { pgTable, text as text2, timestamp, boolean, jsonb, index as index2, integer as integer2 } from "drizzle-orm/pg-core";
3649
+ var specs2 = pgTable("specs", {
3650
+ id: text2("id").primaryKey(),
3651
+ name: text2("name").notNull().unique(),
3652
+ type: text2("type", { enum: ["openapi", "asyncapi"] }).notNull(),
3653
+ description: text2("description"),
3654
+ owner: text2("owner"),
3655
+ sourceRepo: text2("source_repo"),
3656
+ tags: jsonb("tags").$type().default([]),
3657
+ createdAt: timestamp("created_at").notNull(),
3658
+ updatedAt: timestamp("updated_at").notNull()
3659
+ });
3660
+ var specVersions2 = pgTable("spec_versions", {
3661
+ id: text2("id").primaryKey(),
3662
+ specId: text2("spec_id").notNull().references(() => specs2.id),
3663
+ semver: text2("semver").notNull(),
3664
+ content: text2("content").notNull(),
3665
+ checksum: text2("checksum").notNull(),
3666
+ gitRef: text2("git_ref"),
3667
+ pushedBy: text2("pushed_by"),
3668
+ compatibility: jsonb("compatibility").$type(),
3669
+ previousVersion: text2("previous_version"),
3670
+ forceReason: text2("force_reason"),
3671
+ isPrerelease: boolean("is_prerelease").notNull().default(false),
3672
+ createdAt: timestamp("created_at").notNull()
3673
+ }, (table) => [
3674
+ index2("idx_spec_versions_spec_id").on(table.specId),
3675
+ index2("idx_spec_versions_semver").on(table.specId, table.semver)
3676
+ ]);
3677
+ var auditLog2 = pgTable("audit_log", {
3678
+ id: text2("id").primaryKey(),
3679
+ action: text2("action", { enum: ["spec.push", "spec.push.force", "spec.delete"] }).notNull(),
3680
+ actor: text2("actor").notNull(),
3681
+ specName: text2("spec_name").notNull(),
3682
+ version: text2("version"),
3683
+ details: jsonb("details").$type(),
3684
+ createdAt: timestamp("created_at").notNull()
3685
+ }, (table) => [
3686
+ index2("idx_audit_log_spec_name").on(table.specName),
3687
+ index2("idx_audit_log_created_at").on(table.createdAt)
3688
+ ]);
3689
+ var gatewayConfigs2 = pgTable("gateway_configs", {
3690
+ id: text2("id").primaryKey(),
3691
+ name: text2("name").notNull().unique(),
3692
+ provider: text2("provider", { enum: ["kong"] }).notNull(),
3693
+ specName: text2("spec_name").notNull(),
3694
+ specSemver: text2("spec_semver").notNull(),
3695
+ createdAt: timestamp("created_at").notNull(),
3696
+ updatedAt: timestamp("updated_at").notNull()
3697
+ });
3698
+ var gatewayConfigVersions2 = pgTable("gateway_config_versions", {
3699
+ id: text2("id").primaryKey(),
3700
+ gatewayConfigId: text2("gateway_config_id").notNull().references(() => gatewayConfigs2.id),
3701
+ routes: jsonb("routes").$type().notNull(),
3702
+ environments: jsonb("environments").$type().notNull(),
3703
+ callerIdentification: jsonb("caller_identification").$type(),
3704
+ content: text2("content").notNull(),
3705
+ checksum: text2("checksum").notNull(),
3706
+ pushedBy: text2("pushed_by"),
3707
+ createdAt: timestamp("created_at").notNull()
3708
+ }, (table) => [
3709
+ index2("idx_gateway_config_versions_config_id").on(table.gatewayConfigId)
3710
+ ]);
3711
+ var httpLogs2 = pgTable("http_logs", {
3712
+ id: text2("id").primaryKey(),
3713
+ provider: text2("provider").notNull(),
3714
+ gatewayConfigName: text2("gateway_config_name").notNull(),
3715
+ environment: text2("environment").notNull(),
3716
+ method: text2("method").notNull(),
3717
+ path: text2("path").notNull(),
3718
+ routePath: text2("route_path"),
3719
+ status: integer2("status").notNull(),
3720
+ callerId: text2("caller_id"),
3721
+ callerSource: text2("caller_source"),
3722
+ callerConfidence: text2("caller_confidence").notNull(),
3723
+ occurredAt: timestamp("occurred_at").notNull(),
3724
+ createdAt: timestamp("created_at").notNull()
3725
+ }, (table) => [
3726
+ index2("idx_http_logs_config_env").on(table.gatewayConfigName, table.environment),
3727
+ index2("idx_http_logs_occurred_at").on(table.occurredAt),
3728
+ index2("idx_http_logs_caller").on(table.gatewayConfigName, table.environment, table.callerId)
3729
+ ]);
3730
+ var provisions2 = pgTable("provisions", {
3731
+ id: text2("id").primaryKey(),
3732
+ gatewayConfigName: text2("gateway_config_name").notNull(),
3733
+ gatewayConfigVersion: text2("gateway_config_version").notNull(),
3734
+ environment: text2("environment").notNull(),
3735
+ provider: text2("provider", { enum: ["kong"] }).notNull(),
3736
+ synced: boolean("synced").notNull().default(false),
3737
+ actor: text2("actor").notNull(),
3738
+ details: jsonb("details").$type(),
3739
+ createdAt: timestamp("created_at").notNull()
3740
+ }, (table) => [
3741
+ index2("idx_provisions_config_name").on(table.gatewayConfigName),
3742
+ index2("idx_provisions_created_at").on(table.createdAt)
3743
+ ]);
3744
+
3745
+ // src/registry/storage/postgresql.ts
3746
+ import { v4 as uuid6 } from "uuid";
3747
+ var MIGRATIONS_FOLDER2 = PG_MIGRATIONS_FOLDER;
3748
+ var DatabaseConnectionError = class extends Error {
3749
+ constructor(message, postgresUrl, options) {
3750
+ super(message, options);
3751
+ this.postgresUrl = postgresUrl;
3752
+ this.name = "DatabaseConnectionError";
3753
+ }
3754
+ postgresUrl;
3755
+ };
3756
+ function isConnectionError(err) {
3757
+ if (typeof err !== "object" || err === null) return false;
3758
+ if ("code" in err) {
3759
+ const code = err.code;
3760
+ if (code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND") {
3761
+ return true;
3762
+ }
3763
+ }
3764
+ if (err instanceof AggregateError) {
3765
+ if (err.errors.some(isConnectionError)) return true;
3766
+ }
3767
+ if ("cause" in err && err.cause) {
3768
+ return isConnectionError(err.cause);
3769
+ }
3770
+ return false;
3771
+ }
3772
+ function maskPostgresPassword(url) {
3773
+ try {
3774
+ const parsed = new URL(url);
3775
+ if (parsed.password) parsed.password = "***";
3776
+ return parsed.toString();
3777
+ } catch {
3778
+ return url;
3779
+ }
3780
+ }
3781
+ var PostgreSQLSpecStore = class {
3782
+ db;
3783
+ pool;
3784
+ postgresUrl;
3785
+ constructor(postgresUrl) {
3786
+ this.postgresUrl = postgresUrl;
3787
+ this.pool = new Pool({ connectionString: postgresUrl });
3788
+ this.db = drizzle2(this.pool);
3789
+ }
3790
+ async migrate() {
3791
+ try {
3792
+ await migrate2(this.db, {
3793
+ migrationsFolder: MIGRATIONS_FOLDER2
3794
+ });
3795
+ } catch (err) {
3796
+ if (isConnectionError(err)) {
3797
+ throw new DatabaseConnectionError(
3798
+ `PostgreSQL is not reachable at ${maskPostgresPassword(this.postgresUrl)}`,
3799
+ this.postgresUrl,
3800
+ { cause: err }
3801
+ );
3802
+ }
3803
+ throw err;
3804
+ }
3805
+ }
3806
+ async end() {
3807
+ await this.pool.end();
3808
+ }
3809
+ async getSpec(name) {
3810
+ const rows = await this.db.select().from(specs2).where(eq2(specs2.name, name)).limit(1);
3811
+ if (rows.length === 0) return null;
3812
+ return this.mapSpecRow(rows[0]);
3813
+ }
3814
+ async getSpecVersion(name, semver) {
3815
+ const spec = await this.getSpec(name);
3816
+ if (!spec) return null;
3817
+ const rows = await this.db.select().from(specVersions2).where(and2(eq2(specVersions2.specId, spec.id), eq2(specVersions2.semver, semver))).limit(1);
3818
+ if (rows.length === 0) return null;
3819
+ return this.mapVersionRow(rows[0]);
3820
+ }
3821
+ async getLatestVersion(name) {
3822
+ const spec = await this.getSpec(name);
3823
+ if (!spec) return null;
3824
+ const rows = await this.db.select().from(specVersions2).where(eq2(specVersions2.specId, spec.id)).orderBy(desc2(specVersions2.createdAt), desc2(specVersions2.id)).limit(1);
3825
+ if (rows.length === 0) return null;
3826
+ return this.mapVersionRow(rows[0]);
3827
+ }
3828
+ async listSpecs(filters) {
3829
+ const conditions = [];
3830
+ if (filters?.type) conditions.push(eq2(specs2.type, filters.type));
3831
+ if (filters?.owner) conditions.push(eq2(specs2.owner, filters.owner));
3832
+ let rows = conditions.length > 0 ? await this.db.select().from(specs2).where(and2(...conditions)) : await this.db.select().from(specs2);
3833
+ if (filters?.tags && filters.tags.length > 0) {
3834
+ rows = rows.filter((row) => {
3835
+ const rowTags = row.tags ?? [];
3836
+ return filters.tags.every((tag) => rowTags.includes(tag));
3837
+ });
3838
+ }
3839
+ return rows.map((r) => this.mapSpecRow(r));
3840
+ }
3841
+ async listVersions(name, options) {
3842
+ const spec = await this.getSpec(name);
3843
+ if (!spec) return { versions: [], total: 0 };
3844
+ const limit = options?.limit ?? 10;
3845
+ const offset = options?.offset ?? 0;
3846
+ const [countRow] = await this.db.select({ count: sql2`count(*)` }).from(specVersions2).where(eq2(specVersions2.specId, spec.id));
3847
+ const total = Number(countRow?.count ?? 0);
3848
+ const rows = await this.db.select().from(specVersions2).where(eq2(specVersions2.specId, spec.id)).orderBy(desc2(specVersions2.createdAt), desc2(specVersions2.id)).limit(limit).offset(offset);
3849
+ return { versions: rows.map((r) => this.mapVersionRow(r)), total };
3850
+ }
3851
+ async pushSpecVersion(spec, version) {
3852
+ const existingSpec = await this.getSpec(spec.name);
3853
+ if (!existingSpec) {
3854
+ await this.db.insert(specs2).values({
3855
+ id: spec.id,
3856
+ name: spec.name,
3857
+ type: spec.type,
3858
+ description: spec.description ?? null,
3859
+ owner: spec.owner ?? null,
3860
+ sourceRepo: spec.sourceRepo ?? null,
3861
+ tags: spec.tags ?? [],
3862
+ createdAt: spec.createdAt,
3863
+ updatedAt: spec.updatedAt
3864
+ });
3865
+ } else {
3866
+ await this.db.update(specs2).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq2(specs2.id, existingSpec.id));
3867
+ }
3868
+ const specId = existingSpec?.id ?? spec.id;
3869
+ await this.db.insert(specVersions2).values({
3870
+ id: version.id,
3871
+ specId,
3872
+ semver: version.semver,
3873
+ content: version.content,
3874
+ checksum: version.checksum,
3875
+ gitRef: version.gitRef ?? null,
3876
+ pushedBy: version.pushedBy ?? null,
3877
+ compatibility: version.compatibility ?? null,
3878
+ previousVersion: version.previousVersion ?? null,
3879
+ forceReason: version.forceReason ?? null,
3880
+ isPrerelease: version.isPrerelease,
3881
+ createdAt: version.createdAt
3882
+ });
3883
+ return version;
3884
+ }
3885
+ async deleteSpec(name) {
3886
+ const existingSpec = await this.getSpec(name);
3887
+ if (!existingSpec) return false;
3888
+ await this.db.delete(specVersions2).where(eq2(specVersions2.specId, existingSpec.id));
3889
+ await this.db.delete(specs2).where(eq2(specs2.id, existingSpec.id));
3890
+ return true;
3891
+ }
3892
+ async getCompatReport(name, semver) {
3893
+ const version = await this.getSpecVersion(name, semver);
3894
+ return version?.compatibility ?? null;
3895
+ }
3896
+ async logAudit(action, actor, specName, version, details) {
3897
+ await this.db.insert(auditLog2).values({
3898
+ id: uuid6(),
3899
+ action,
3900
+ actor,
3901
+ specName,
3902
+ version: version ?? null,
3903
+ details: details ?? null,
3904
+ createdAt: /* @__PURE__ */ new Date()
3905
+ });
3906
+ }
3907
+ mapSpecRow(row) {
3908
+ return {
3909
+ id: row.id,
3910
+ name: row.name,
3911
+ type: row.type,
3912
+ description: row.description ?? void 0,
3913
+ owner: row.owner ?? void 0,
3914
+ sourceRepo: row.sourceRepo ?? void 0,
3915
+ tags: row.tags ?? [],
3916
+ createdAt: row.createdAt,
3917
+ updatedAt: row.updatedAt
3918
+ };
3919
+ }
3920
+ mapVersionRow(row) {
3921
+ return {
3922
+ id: row.id,
3923
+ specId: row.specId,
3924
+ semver: row.semver,
3925
+ content: row.content,
3926
+ checksum: row.checksum,
3927
+ gitRef: row.gitRef ?? void 0,
3928
+ pushedBy: row.pushedBy ?? void 0,
3929
+ compatibility: row.compatibility ?? void 0,
3930
+ previousVersion: row.previousVersion ?? void 0,
3931
+ forceReason: row.forceReason ?? void 0,
3932
+ isPrerelease: row.isPrerelease,
3933
+ createdAt: row.createdAt
3934
+ };
3935
+ }
3936
+ // GatewayConfigStore implementation
3937
+ async getGatewayConfig(name) {
3938
+ const rows = await this.db.select().from(gatewayConfigs2).where(eq2(gatewayConfigs2.name, name)).limit(1);
3939
+ if (rows.length === 0) return null;
3940
+ return this.mapGatewayConfigRow(rows[0]);
3941
+ }
3942
+ async listGatewayConfigs() {
3943
+ const rows = await this.db.select().from(gatewayConfigs2);
3944
+ return rows.map((r) => this.mapGatewayConfigRow(r));
3945
+ }
3946
+ async getGatewayConfigVersion(name, versionId) {
3947
+ const config = await this.getGatewayConfig(name);
3948
+ if (!config) return null;
3949
+ const rows = await this.db.select().from(gatewayConfigVersions2).where(and2(eq2(gatewayConfigVersions2.gatewayConfigId, config.id), eq2(gatewayConfigVersions2.id, versionId))).limit(1);
3950
+ if (rows.length === 0) return null;
3951
+ return this.mapGatewayConfigVersionRow(rows[0]);
3952
+ }
3953
+ async getLatestGatewayConfigVersion(name) {
3954
+ const config = await this.getGatewayConfig(name);
3955
+ if (!config) return null;
3956
+ const rows = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, config.id)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id)).limit(1);
3957
+ if (rows.length === 0) return null;
3958
+ return this.mapGatewayConfigVersionRow(rows[0]);
3959
+ }
3960
+ async listGatewayConfigVersions(name) {
3961
+ const config = await this.getGatewayConfig(name);
3962
+ if (!config) return [];
3963
+ const rows = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, config.id)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id)).limit(5);
3964
+ return rows.map((r) => this.mapGatewayConfigVersionRow(r));
3965
+ }
3966
+ async pushGatewayConfigVersion(config, version) {
3967
+ const existingConfig = await this.getGatewayConfig(config.name);
3968
+ if (!existingConfig) {
3969
+ await this.db.insert(gatewayConfigs2).values({
3970
+ id: config.id,
3971
+ name: config.name,
3972
+ provider: config.provider,
3973
+ specName: config.specName,
3974
+ specSemver: config.specSemver,
3975
+ createdAt: config.createdAt,
3976
+ updatedAt: config.updatedAt
3977
+ });
3978
+ } else {
3979
+ await this.db.update(gatewayConfigs2).set({ updatedAt: /* @__PURE__ */ new Date(), specSemver: config.specSemver }).where(eq2(gatewayConfigs2.id, existingConfig.id));
3980
+ }
3981
+ const configId = existingConfig?.id ?? config.id;
3982
+ await this.db.insert(gatewayConfigVersions2).values({
3983
+ id: version.id,
3984
+ gatewayConfigId: configId,
3985
+ routes: version.routes,
3986
+ environments: version.environments,
3987
+ callerIdentification: version.callerIdentification ?? null,
3988
+ content: version.content,
3989
+ checksum: version.checksum,
3990
+ pushedBy: version.pushedBy ?? null,
3991
+ createdAt: version.createdAt
3992
+ });
3993
+ const versions = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, configId)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id));
3994
+ if (versions.length > 5) {
3995
+ const toDelete = versions.slice(5);
3996
+ for (const v of toDelete) {
3997
+ await this.db.delete(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.id, v.id));
3998
+ }
3999
+ }
4000
+ return version;
4001
+ }
4002
+ async recordProvision(provision) {
4003
+ await this.db.insert(provisions2).values({
4004
+ id: provision.id,
4005
+ gatewayConfigName: provision.gatewayConfigName,
4006
+ gatewayConfigVersion: provision.gatewayConfigVersion,
4007
+ environment: provision.environment,
4008
+ provider: provision.provider,
4009
+ synced: provision.synced,
4010
+ actor: provision.actor,
4011
+ details: provision.details ?? null,
4012
+ createdAt: provision.createdAt
4013
+ });
4014
+ }
4015
+ async listProvisions(gatewayConfigName) {
4016
+ const rows = gatewayConfigName ? await this.db.select().from(provisions2).where(eq2(provisions2.gatewayConfigName, gatewayConfigName)).orderBy(desc2(provisions2.createdAt)) : await this.db.select().from(provisions2).orderBy(desc2(provisions2.createdAt));
4017
+ return rows.map((r) => ({
4018
+ id: r.id,
4019
+ gatewayConfigName: r.gatewayConfigName,
4020
+ gatewayConfigVersion: r.gatewayConfigVersion,
4021
+ environment: r.environment,
4022
+ provider: r.provider,
4023
+ synced: r.synced,
4024
+ actor: r.actor,
4025
+ details: r.details ?? void 0,
4026
+ createdAt: r.createdAt
4027
+ }));
4028
+ }
4029
+ mapGatewayConfigRow(row) {
4030
+ return {
4031
+ id: row.id,
4032
+ name: row.name,
4033
+ provider: row.provider,
4034
+ specName: row.specName,
4035
+ specSemver: row.specSemver,
4036
+ createdAt: row.createdAt,
4037
+ updatedAt: row.updatedAt
4038
+ };
4039
+ }
4040
+ mapGatewayConfigVersionRow(row) {
4041
+ return {
4042
+ id: row.id,
4043
+ gatewayConfigId: row.gatewayConfigId,
4044
+ routes: row.routes,
4045
+ environments: row.environments,
4046
+ callerIdentification: row.callerIdentification ?? void 0,
4047
+ content: row.content,
4048
+ checksum: row.checksum,
4049
+ pushedBy: row.pushedBy ?? void 0,
4050
+ createdAt: row.createdAt
4051
+ };
4052
+ }
4053
+ async recordGatewayLog(log) {
4054
+ await this.db.insert(httpLogs2).values({
4055
+ id: log.id,
4056
+ provider: log.provider,
4057
+ gatewayConfigName: log.gatewayConfigName,
4058
+ environment: log.environment,
4059
+ method: log.method,
4060
+ path: log.path,
4061
+ routePath: log.routePath ?? null,
4062
+ status: log.status,
4063
+ callerId: log.callerId ?? null,
4064
+ callerSource: log.callerSource ?? null,
4065
+ callerConfidence: log.callerConfidence,
4066
+ occurredAt: log.occurredAt,
4067
+ createdAt: log.createdAt
4068
+ });
4069
+ }
4070
+ async listGatewayLogs(filters) {
4071
+ const limit = filters.limit ?? 50;
4072
+ const offset = filters.offset ?? 0;
4073
+ let query = this.db.select().from(httpLogs2);
4074
+ const conditions = [];
4075
+ if (filters.gatewayConfigName) {
4076
+ conditions.push(eq2(httpLogs2.gatewayConfigName, filters.gatewayConfigName));
4077
+ }
4078
+ if (filters.environment) {
4079
+ conditions.push(eq2(httpLogs2.environment, filters.environment));
4080
+ }
4081
+ if (filters.path) {
4082
+ conditions.push(eq2(httpLogs2.path, filters.path));
4083
+ }
4084
+ if (filters.method) {
4085
+ conditions.push(eq2(httpLogs2.method, filters.method));
4086
+ }
4087
+ if (filters.status !== void 0) {
4088
+ conditions.push(eq2(httpLogs2.status, filters.status));
4089
+ }
4090
+ if (filters.from) {
4091
+ conditions.push(sql2`${httpLogs2.occurredAt} >= ${filters.from}`);
4092
+ }
4093
+ if (filters.to) {
4094
+ conditions.push(sql2`${httpLogs2.occurredAt} <= ${filters.to}`);
4095
+ }
4096
+ if (conditions.length > 0) {
4097
+ query = query.where(and2(...conditions));
4098
+ }
4099
+ const countResult = await this.db.select({ count: sql2`count(*)` }).from(httpLogs2).where(conditions.length > 0 ? and2(...conditions) : void 0);
4100
+ const total = countResult[0]?.count ?? 0;
4101
+ const rows = await query.orderBy(desc2(httpLogs2.occurredAt)).limit(limit).offset(offset);
4102
+ return {
4103
+ logs: rows.map((r) => ({
4104
+ id: r.id,
4105
+ provider: r.provider,
4106
+ gatewayConfigName: r.gatewayConfigName,
4107
+ environment: r.environment,
4108
+ method: r.method,
4109
+ path: r.path,
4110
+ routePath: r.routePath ?? void 0,
4111
+ status: r.status,
4112
+ callerId: r.callerId ?? void 0,
4113
+ callerSource: r.callerSource ?? void 0,
4114
+ callerConfidence: r.callerConfidence,
4115
+ occurredAt: r.occurredAt,
4116
+ createdAt: r.createdAt
4117
+ })),
4118
+ total
4119
+ };
4120
+ }
4121
+ async getGatewayLog(id) {
4122
+ const rows = await this.db.select().from(httpLogs2).where(eq2(httpLogs2.id, id)).limit(1);
4123
+ if (rows.length === 0) return null;
4124
+ const r = rows[0];
4125
+ return {
4126
+ id: r.id,
4127
+ provider: r.provider,
4128
+ gatewayConfigName: r.gatewayConfigName,
4129
+ environment: r.environment,
4130
+ method: r.method,
4131
+ path: r.path,
4132
+ routePath: r.routePath ?? void 0,
4133
+ status: r.status,
4134
+ callerId: r.callerId ?? void 0,
4135
+ callerSource: r.callerSource ?? void 0,
4136
+ callerConfidence: r.callerConfidence,
4137
+ occurredAt: r.occurredAt,
4138
+ createdAt: r.createdAt
4139
+ };
4140
+ }
4141
+ async getGatewayLogStats(_filters) {
4142
+ const conditions = [];
4143
+ if (_filters.gatewayConfigName) {
4144
+ conditions.push(eq2(httpLogs2.gatewayConfigName, _filters.gatewayConfigName));
4145
+ }
4146
+ if (_filters.environment) {
4147
+ conditions.push(eq2(httpLogs2.environment, _filters.environment));
4148
+ }
4149
+ if (_filters.from) {
4150
+ conditions.push(sql2`${httpLogs2.occurredAt} >= ${_filters.from}`);
4151
+ }
4152
+ if (_filters.to) {
4153
+ conditions.push(sql2`${httpLogs2.occurredAt} <= ${_filters.to}`);
4154
+ }
4155
+ const whereClause = conditions.length > 0 ? and2(...conditions) : void 0;
4156
+ const rows = await this.db.select({
4157
+ gatewayConfigName: httpLogs2.gatewayConfigName,
4158
+ environment: httpLogs2.environment,
4159
+ method: httpLogs2.method,
4160
+ routePath: httpLogs2.routePath,
4161
+ lastSeenAt: sql2`max(${httpLogs2.occurredAt})`,
4162
+ totalCalls: sql2`count(*)`,
4163
+ uniqueCallerIds: sql2`count(distinct ${httpLogs2.callerId})`
4164
+ }).from(httpLogs2).where(whereClause).groupBy(httpLogs2.gatewayConfigName, httpLogs2.environment, httpLogs2.method, httpLogs2.routePath);
4165
+ return rows.map((r) => ({
4166
+ gatewayConfigName: r.gatewayConfigName,
4167
+ environment: r.environment,
4168
+ method: r.method,
4169
+ routePath: r.routePath ?? "/",
4170
+ lastSeenAt: new Date(r.lastSeenAt),
4171
+ totalCalls: r.totalCalls,
4172
+ uniqueCallerIds: r.uniqueCallerIds
4173
+ }));
4174
+ }
4175
+ async deleteGatewayLogsOlderThan(days) {
4176
+ const cutoff = /* @__PURE__ */ new Date();
4177
+ cutoff.setDate(cutoff.getDate() - days);
4178
+ await this.db.delete(httpLogs2).where(sql2`${httpLogs2.occurredAt} < ${cutoff}`);
4179
+ }
4180
+ };
4181
+
3496
4182
  // src/registry/serve.ts
3497
4183
  async function startServer(userConfig) {
3498
4184
  const config = { ...defaultConfig, ...userConfig };
3499
- if (!config.sqlitePath && config.database === "sqlite") {
3500
- const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
3501
- config.sqlitePath = path.join(homeDir, ".grapity", "registry.db");
3502
- }
3503
- if (config.database === "sqlite" && config.sqlitePath) {
3504
- const dir = path.dirname(config.sqlitePath);
4185
+ let store;
4186
+ if (config.database === "postgresql") {
4187
+ if (!config.postgresUrl) {
4188
+ throw new Error("PostgreSQL database requested but no postgresUrl provided.");
4189
+ }
4190
+ store = new PostgreSQLSpecStore(config.postgresUrl);
4191
+ } else {
4192
+ const sqlitePath = config.sqlitePath ?? path.join(
4193
+ process.env.HOME || process.env.USERPROFILE || ".",
4194
+ ".grapity",
4195
+ "registry.db"
4196
+ );
4197
+ const dir = path.dirname(sqlitePath);
3505
4198
  if (!fs.existsSync(dir)) {
3506
4199
  fs.mkdirSync(dir, { recursive: true });
3507
4200
  }
4201
+ store = new SQLiteSpecStore(sqlitePath);
3508
4202
  }
3509
- const store = new SQLiteSpecStore(config.sqlitePath);
3510
4203
  await store.migrate();
3511
4204
  const app = createApp(config, store);
3512
- serve({
4205
+ const server = serve({
3513
4206
  fetch: app.fetch,
3514
4207
  port: config.port
3515
4208
  });
3516
- return app;
4209
+ return { app, store, server };
3517
4210
  }
3518
4211
  if (process.argv[1] === new URL(import.meta.url).pathname) {
3519
4212
  startServer();