@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.
package/dist/hub/index.js CHANGED
@@ -17,15 +17,34 @@ var DEFAULT_REGISTRY_URL = "http://localhost:3750";
17
17
  async function startHubServer(userConfig) {
18
18
  const config = {
19
19
  port: userConfig?.port ?? DEFAULT_PORT,
20
- registryUrl: userConfig?.registryUrl ?? DEFAULT_REGISTRY_URL
20
+ registryUrl: userConfig?.registryUrl ?? DEFAULT_REGISTRY_URL,
21
+ auth: userConfig?.auth
21
22
  };
22
23
  const app = new Hono();
24
+ app.get("/config.js", (c) => {
25
+ const clientConfig = {
26
+ registryUrl: config.registryUrl,
27
+ auth: config.auth ? {
28
+ mode: config.auth.mode,
29
+ serverUrl: config.auth.serverUrl,
30
+ realm: config.auth.realm,
31
+ clientId: config.auth.clientId,
32
+ audience: config.auth.audience
33
+ } : void 0
34
+ };
35
+ c.header("Content-Type", "application/javascript");
36
+ return c.body(
37
+ `window.__GRAPITY_CONFIG__ = ${JSON.stringify(clientConfig)};`
38
+ );
39
+ });
23
40
  app.use("/v1/*", async (c) => {
24
41
  const url = new URL(c.req.url);
25
42
  const targetUrl = config.registryUrl + url.pathname + url.search;
43
+ const headers = new Headers(c.req.raw.headers);
44
+ headers.delete("host");
26
45
  const response = await fetch(targetUrl, {
27
46
  method: c.req.method,
28
- headers: c.req.raw.headers,
47
+ headers,
29
48
  body: c.req.raw.body
30
49
  });
31
50
  return response;
@@ -33,13 +52,21 @@ async function startHubServer(userConfig) {
33
52
  app.use("/*", serveStatic({ root: HUB_DIST_PATH }));
34
53
  app.get("/*", async (c) => {
35
54
  const indexPath = path.join(HUB_DIST_PATH, "index.html");
36
- if (fs.existsSync(indexPath)) {
37
- return c.html(fs.readFileSync(indexPath, "utf-8"));
55
+ if (!fs.existsSync(indexPath)) {
56
+ return c.text(
57
+ "index.html not found. Build the project with 'bun run build' first.",
58
+ 404
59
+ );
38
60
  }
39
- return c.text(
40
- "index.html not found. Build the project with 'bun run build' first.",
41
- 404
61
+ const html = fs.readFileSync(indexPath, "utf-8");
62
+ const configScript = `
63
+ <script src="/config.js"></script>
64
+ `;
65
+ const injected = html.replace(
66
+ "</head>",
67
+ `${configScript}</head>`
42
68
  );
69
+ return c.html(injected);
43
70
  });
44
71
  serve({
45
72
  fetch: app.fetch,
@@ -1,7 +1,15 @@
1
+ interface HubAuthConfig {
2
+ mode: "keycloak";
3
+ serverUrl: string;
4
+ realm: string;
5
+ clientId: string;
6
+ audience?: string;
7
+ }
1
8
  interface HubConfig {
2
9
  port?: number;
3
10
  registryUrl?: string;
11
+ auth?: HubAuthConfig;
4
12
  }
5
13
  declare function startHubServer(userConfig?: Partial<HubConfig>): Promise<void>;
6
14
 
7
- export { type HubConfig, startHubServer };
15
+ export { type HubAuthConfig, type HubConfig, startHubServer };
package/dist/hub/serve.js CHANGED
@@ -17,15 +17,34 @@ var DEFAULT_REGISTRY_URL = "http://localhost:3750";
17
17
  async function startHubServer(userConfig) {
18
18
  const config = {
19
19
  port: userConfig?.port ?? DEFAULT_PORT,
20
- registryUrl: userConfig?.registryUrl ?? DEFAULT_REGISTRY_URL
20
+ registryUrl: userConfig?.registryUrl ?? DEFAULT_REGISTRY_URL,
21
+ auth: userConfig?.auth
21
22
  };
22
23
  const app = new Hono();
24
+ app.get("/config.js", (c) => {
25
+ const clientConfig = {
26
+ registryUrl: config.registryUrl,
27
+ auth: config.auth ? {
28
+ mode: config.auth.mode,
29
+ serverUrl: config.auth.serverUrl,
30
+ realm: config.auth.realm,
31
+ clientId: config.auth.clientId,
32
+ audience: config.auth.audience
33
+ } : void 0
34
+ };
35
+ c.header("Content-Type", "application/javascript");
36
+ return c.body(
37
+ `window.__GRAPITY_CONFIG__ = ${JSON.stringify(clientConfig)};`
38
+ );
39
+ });
23
40
  app.use("/v1/*", async (c) => {
24
41
  const url = new URL(c.req.url);
25
42
  const targetUrl = config.registryUrl + url.pathname + url.search;
43
+ const headers = new Headers(c.req.raw.headers);
44
+ headers.delete("host");
26
45
  const response = await fetch(targetUrl, {
27
46
  method: c.req.method,
28
- headers: c.req.raw.headers,
47
+ headers,
29
48
  body: c.req.raw.body
30
49
  });
31
50
  return response;
@@ -33,13 +52,21 @@ async function startHubServer(userConfig) {
33
52
  app.use("/*", serveStatic({ root: HUB_DIST_PATH }));
34
53
  app.get("/*", async (c) => {
35
54
  const indexPath = path.join(HUB_DIST_PATH, "index.html");
36
- if (fs.existsSync(indexPath)) {
37
- return c.html(fs.readFileSync(indexPath, "utf-8"));
55
+ if (!fs.existsSync(indexPath)) {
56
+ return c.text(
57
+ "index.html not found. Build the project with 'bun run build' first.",
58
+ 404
59
+ );
38
60
  }
39
- return c.text(
40
- "index.html not found. Build the project with 'bun run build' first.",
41
- 404
61
+ const html = fs.readFileSync(indexPath, "utf-8");
62
+ const configScript = `
63
+ <script src="/config.js"></script>
64
+ `;
65
+ const injected = html.replace(
66
+ "</head>",
67
+ `${configScript}</head>`
42
68
  );
69
+ return c.html(injected);
43
70
  });
44
71
  serve({
45
72
  fetch: app.fetch,
package/dist/index.html CHANGED
@@ -9,8 +9,9 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com" />
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
11
  <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
12
- <script type="module" crossorigin src="/assets/index-Dq5tdnlb.js"></script>
13
- <link rel="stylesheet" crossorigin href="/assets/index-NJpHAonA.css">
12
+ <script src="/config.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-JAhtTTW2.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/assets/index-LDlidn22.css">
14
15
  </head>
15
16
  <body>
16
17
  <div id="root"></div>
@@ -191,28 +191,35 @@ interface GatewayConfigStore {
191
191
  deleteGatewayLogsOlderThan(days: number): Promise<void>;
192
192
  }
193
193
 
194
+ type RoleSource = "scope" | "realm_access.roles";
195
+ interface KeycloakAuthConfig {
196
+ mode: "keycloak";
197
+ serverUrl: string;
198
+ realm: string;
199
+ audience?: string;
200
+ roleSource?: RoleSource;
201
+ }
202
+ type AuthConfig = {
203
+ mode: "none";
204
+ } | KeycloakAuthConfig;
205
+
206
+ type DatabaseBackend = "sqlite" | "postgresql";
194
207
  interface ServerConfig {
195
208
  port: number;
196
- database: "sqlite" | "postgresql";
209
+ database: DatabaseBackend;
197
210
  sqlitePath?: string;
198
211
  postgresUrl?: string;
199
- gracePeriodDays: number;
200
- auth?: {
201
- mode: "none" | "api-key" | "jwt";
202
- apiKeyHashes?: string[];
203
- jwtSecret?: string;
204
- };
205
- audit?: {
206
- enabled: boolean;
207
- };
212
+ auth: AuthConfig;
208
213
  }
209
214
 
210
215
  type AppEnv = {
211
216
  Variables: {
212
217
  store: SpecStore & GatewayConfigStore;
213
218
  config: ServerConfig;
219
+ actor?: string;
220
+ claims?: Record<string, unknown>;
214
221
  };
215
222
  };
216
223
  declare function createApp(config: ServerConfig, store: SpecStore & GatewayConfigStore): Hono<AppEnv, hono_types.BlankSchema, "/">;
217
224
 
218
- export { type AppEnv as A, type ServerConfig as S, createApp as c };
225
+ export { type AuditAction as A, type CompatReport as C, type GatewayConfigStore as G, type Provision as P, type SpecStore as S, type Spec as a, type SpecVersion as b, type SpecFilters as c, type GatewayConfig as d, type GatewayConfigVersion as e, type GatewayLog as f, type GatewayLogFilters as g, type GatewayLogStats as h, createApp as i, type ServerConfig as j };
@@ -1,3 +1,3 @@
1
- export { S as ServerConfig, c as createApp } from './index-Baj_sSgl.js';
1
+ export { j as ServerConfig, i as createApp } from './index-Bx-7YlUF.js';
2
2
  import 'hono/types';
3
3
  import 'hono';
@@ -1751,6 +1751,7 @@ var pushRoute = new Hono().post("/", async (c) => {
1751
1751
  }
1752
1752
  const store = c.get("store");
1753
1753
  const service = new RegistryService(store);
1754
+ const actor = c.get("actor") ?? body.pushedBy;
1754
1755
  try {
1755
1756
  const result = await service.pushSpec(body.content, body.name, {
1756
1757
  type: body.type,
@@ -1759,7 +1760,7 @@ var pushRoute = new Hono().post("/", async (c) => {
1759
1760
  sourceRepo: body.sourceRepo,
1760
1761
  tags: Array.isArray(body.tags) ? body.tags : void 0,
1761
1762
  gitRef: body.gitRef,
1762
- pushedBy: body.pushedBy,
1763
+ pushedBy: actor,
1763
1764
  prerelease: body.prerelease,
1764
1765
  force: body.force,
1765
1766
  reason: body.reason
@@ -1924,7 +1925,8 @@ var deleteSpecRoute = new Hono5().delete(
1924
1925
  const name = c.req.param("name");
1925
1926
  const store = c.get("store");
1926
1927
  const service = new RegistryService(store);
1927
- const deleted = await service.deleteSpec(name);
1928
+ const actor = c.get("actor");
1929
+ const deleted = await service.deleteSpec(name, actor);
1928
1930
  if (!deleted) {
1929
1931
  return c.json({ error: "not_found", message: `Spec "${name}" not found`, statusCode: 404 }, 404);
1930
1932
  }
@@ -2525,8 +2527,7 @@ function switchTab(tab) {
2525
2527
  }
2526
2528
  var welcomeRoute = new Hono12().get("/", (c) => {
2527
2529
  const config = c.get("config");
2528
- const mode = config.database === "sqlite" ? "local" : "remote";
2529
- return c.html(buildPage(config.port, mode));
2530
+ return c.html(buildPage(config.port, "local"));
2530
2531
  });
2531
2532
 
2532
2533
  // src/registry/routes/push-gateway-config.ts
@@ -2666,6 +2667,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c) => {
2666
2667
  }
2667
2668
  const store = c.get("store");
2668
2669
  const service = new GatewayService(store, store);
2670
+ const actor = c.get("actor") ?? body.pushedBy;
2669
2671
  try {
2670
2672
  const result = await service.pushGatewayConfig({
2671
2673
  name: body.name,
@@ -2676,7 +2678,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c) => {
2676
2678
  environments: body.environments ?? {},
2677
2679
  callerIdentification: body.callerIdentification,
2678
2680
  content: body.content,
2679
- pushedBy: body.pushedBy
2681
+ pushedBy: actor
2680
2682
  });
2681
2683
  return c.json({ data: result }, 201);
2682
2684
  } catch (err) {
@@ -2867,7 +2869,6 @@ var ingestGatewayLogRoute = new Hono18().post("/ingest/:provider/:environment",
2867
2869
  await service.ingestLog(provider, environment, payload);
2868
2870
  return c.json({ status: "ok" }, 201);
2869
2871
  } catch (err) {
2870
- console.error("Gateway log ingest error:", err);
2871
2872
  return c.json({
2872
2873
  error: "bad_request",
2873
2874
  message: err instanceof Error ? err.message : "Invalid log payload",
@@ -2942,7 +2943,137 @@ var gatewayLogStatsRoute = new Hono21().get("/stats", async (c) => {
2942
2943
  });
2943
2944
  });
2944
2945
 
2946
+ // src/registry/auth/middleware.ts
2947
+ import { createRemoteJWKSet, jwtVerify } from "jose";
2948
+ var AuthError = class extends Error {
2949
+ constructor(statusCode, code, message) {
2950
+ super(message);
2951
+ this.statusCode = statusCode;
2952
+ this.code = code;
2953
+ this.name = "AuthError";
2954
+ }
2955
+ statusCode;
2956
+ code;
2957
+ };
2958
+ function buildKeycloakUrls(config) {
2959
+ const base = `${config.serverUrl}/realms/${config.realm}`;
2960
+ return {
2961
+ issuer: base,
2962
+ jwksUri: `${base}/protocol/openid-connect/certs`,
2963
+ tokenUrl: `${base}/protocol/openid-connect/token`
2964
+ };
2965
+ }
2966
+ function createAuthMiddleware(config, routeScopes) {
2967
+ if (config.auth?.mode !== "keycloak") {
2968
+ return async (_c, next) => await next();
2969
+ }
2970
+ const authConfig = config.auth;
2971
+ const { issuer, jwksUri } = buildKeycloakUrls(authConfig);
2972
+ const jwks = createRemoteJWKSet(new URL(jwksUri));
2973
+ const scopeByRoute = /* @__PURE__ */ new Map();
2974
+ for (const route of routeScopes) {
2975
+ const key = `${route.method.toUpperCase()}:${route.path}`;
2976
+ scopeByRoute.set(key, {
2977
+ operationId: route.operationId,
2978
+ scopes: route.scopes
2979
+ });
2980
+ }
2981
+ return async (c, next) => {
2982
+ const matchedPath = c.req.matchedRoutes.map((r) => r.path).filter((p) => p !== "/*").pop();
2983
+ const routeKey = matchedPath ? `${c.req.method}:${matchedPath}` : `${c.req.method}:${c.req.routePath}`;
2984
+ const required = scopeByRoute.get(routeKey);
2985
+ if (!required || required.scopes.length === 0) {
2986
+ return await next();
2987
+ }
2988
+ const authHeader = c.req.header("Authorization");
2989
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
2990
+ throw new AuthError(401, "unauthorized", "Bearer token required");
2991
+ }
2992
+ const token = authHeader.slice("Bearer ".length).trim();
2993
+ if (!token) {
2994
+ throw new AuthError(401, "unauthorized", "Bearer token required");
2995
+ }
2996
+ let payload;
2997
+ try {
2998
+ const result = await jwtVerify(token, jwks, {
2999
+ issuer,
3000
+ audience: authConfig.audience
3001
+ });
3002
+ payload = result.payload;
3003
+ } catch (err) {
3004
+ const message = err instanceof Error ? err.message : "Invalid token";
3005
+ throw new AuthError(401, "unauthorized", `Invalid or expired token: ${message}`);
3006
+ }
3007
+ const subject = payload.sub;
3008
+ if (!subject) {
3009
+ throw new AuthError(401, "unauthorized", "Token missing subject claim");
3010
+ }
3011
+ const grantedScopes = extractScopes(payload, authConfig.roleSource ?? "scope");
3012
+ const missing = required.scopes.filter((s) => !grantedScopes.has(s));
3013
+ if (missing.length > 0) {
3014
+ throw new AuthError(
3015
+ 403,
3016
+ "forbidden",
3017
+ `Missing required scope${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`
3018
+ );
3019
+ }
3020
+ c.set("actor", subject);
3021
+ c.set("claims", payload);
3022
+ await next();
3023
+ };
3024
+ }
3025
+ function extractScopes(payload, source) {
3026
+ if (source === "realm_access.roles") {
3027
+ const roles = payload.realm_access?.roles;
3028
+ return new Set(roles ?? []);
3029
+ }
3030
+ const scopeValue = payload.scope;
3031
+ if (typeof scopeValue === "string") {
3032
+ return new Set(scopeValue.split(/\s+/).filter(Boolean));
3033
+ }
3034
+ return /* @__PURE__ */ new Set();
3035
+ }
3036
+ function parseRouteScopes(spec) {
3037
+ const routes = [];
3038
+ const paths = spec.paths;
3039
+ if (!paths) return routes;
3040
+ for (const [path, operations] of Object.entries(paths)) {
3041
+ for (const [method, operation] of Object.entries(operations)) {
3042
+ if (typeof operation !== "object" || operation === null) continue;
3043
+ const op = operation;
3044
+ const operationId = op.operationId;
3045
+ if (!operationId) continue;
3046
+ const security = op.security;
3047
+ const scopes = [];
3048
+ if (security) {
3049
+ for (const sec of security) {
3050
+ for (const [name, required] of Object.entries(sec)) {
3051
+ if (name === "keycloak" && Array.isArray(required)) {
3052
+ scopes.push(...required);
3053
+ }
3054
+ }
3055
+ }
3056
+ }
3057
+ routes.push({
3058
+ method: method.toUpperCase(),
3059
+ path: path.replace(/\{([^}]+)\}/g, ":$1"),
3060
+ operationId,
3061
+ scopes
3062
+ });
3063
+ }
3064
+ }
3065
+ return routes;
3066
+ }
3067
+
2945
3068
  // src/registry/server.ts
3069
+ import { readFileSync } from "fs";
3070
+ import { fileURLToPath } from "url";
3071
+ import yaml6 from "js-yaml";
3072
+ function loadOpenApiSpec() {
3073
+ const path = fileURLToPath(new URL("../../openapi.yaml", import.meta.url));
3074
+ const content = readFileSync(path, "utf-8");
3075
+ return yaml6.load(content);
3076
+ }
2946
3077
  function createApp(config, store) {
2947
3078
  const app = new Hono22();
2948
3079
  app.use("*", logger());
@@ -2953,6 +3084,21 @@ function createApp(config, store) {
2953
3084
  c.set("config", config);
2954
3085
  await next();
2955
3086
  });
3087
+ const routeScopes = parseRouteScopes(loadOpenApiSpec());
3088
+ app.use("*", createAuthMiddleware(config, routeScopes));
3089
+ app.onError((err, c) => {
3090
+ if (err instanceof AuthError) {
3091
+ return c.json(
3092
+ { error: err.code, message: err.message, statusCode: err.statusCode },
3093
+ err.statusCode
3094
+ );
3095
+ }
3096
+ console.error("Unhandled error:", err);
3097
+ return c.json(
3098
+ { error: "internal_error", message: "Internal server error", statusCode: 500 },
3099
+ 500
3100
+ );
3101
+ });
2956
3102
  app.route("/v1/specs", pushRoute);
2957
3103
  app.route("/v1/specs", validateRoute);
2958
3104
  app.route("/v1/specs", listRoute);
@@ -1,7 +1,98 @@
1
- import * as hono from 'hono';
2
- import * as hono_types from 'hono/types';
3
- import { S as ServerConfig, A as AppEnv } from './index-Baj_sSgl.js';
1
+ import { ServerType } from '@hono/node-server';
2
+ import { S as SpecStore, G as GatewayConfigStore, a as Spec, b as SpecVersion, c as SpecFilters, C as CompatReport, A as AuditAction, d as GatewayConfig, e as GatewayConfigVersion, P as Provision, f as GatewayLog, g as GatewayLogFilters, h as GatewayLogStats, i as createApp, j as ServerConfig } from './index-Bx-7YlUF.js';
3
+ import 'hono/types';
4
+ import 'hono';
4
5
 
5
- declare function startServer(userConfig?: Partial<ServerConfig>): Promise<hono.Hono<AppEnv, hono_types.BlankSchema, "/">>;
6
+ declare class SQLiteSpecStore implements SpecStore, GatewayConfigStore {
7
+ private db;
8
+ constructor(dbPath: string);
9
+ migrate(): Promise<void>;
10
+ getSpec(name: string): Promise<Spec | null>;
11
+ getSpecVersion(name: string, semver: string): Promise<SpecVersion | null>;
12
+ getLatestVersion(name: string): Promise<SpecVersion | null>;
13
+ listSpecs(filters?: SpecFilters): Promise<Spec[]>;
14
+ listVersions(name: string, options?: {
15
+ limit?: number;
16
+ offset?: number;
17
+ }): Promise<{
18
+ versions: SpecVersion[];
19
+ total: number;
20
+ }>;
21
+ pushSpecVersion(spec: Spec, version: SpecVersion): Promise<SpecVersion>;
22
+ deleteSpec(name: string): Promise<boolean>;
23
+ getCompatReport(name: string, semver: string): Promise<CompatReport | null>;
24
+ logAudit(action: AuditAction, actor: string, specName: string, version: string | undefined, details: Record<string, unknown> | undefined): Promise<void>;
25
+ private mapSpecRow;
26
+ private mapVersionRow;
27
+ getGatewayConfig(name: string): Promise<GatewayConfig | null>;
28
+ listGatewayConfigs(): Promise<GatewayConfig[]>;
29
+ getGatewayConfigVersion(name: string, versionId: string): Promise<GatewayConfigVersion | null>;
30
+ getLatestGatewayConfigVersion(name: string): Promise<GatewayConfigVersion | null>;
31
+ listGatewayConfigVersions(name: string): Promise<GatewayConfigVersion[]>;
32
+ pushGatewayConfigVersion(config: GatewayConfig, version: GatewayConfigVersion): Promise<GatewayConfigVersion>;
33
+ recordProvision(provision: Provision): Promise<void>;
34
+ listProvisions(gatewayConfigName?: string): Promise<Provision[]>;
35
+ private mapGatewayConfigRow;
36
+ private mapGatewayConfigVersionRow;
37
+ recordGatewayLog(log: GatewayLog): Promise<void>;
38
+ listGatewayLogs(filters: GatewayLogFilters): Promise<{
39
+ logs: GatewayLog[];
40
+ total: number;
41
+ }>;
42
+ getGatewayLog(id: string): Promise<GatewayLog | null>;
43
+ getGatewayLogStats(_filters: GatewayLogFilters): Promise<GatewayLogStats[]>;
44
+ deleteGatewayLogsOlderThan(days: number): Promise<void>;
45
+ }
6
46
 
7
- export { ServerConfig, startServer };
47
+ declare class PostgreSQLSpecStore implements SpecStore, GatewayConfigStore {
48
+ private db;
49
+ private pool;
50
+ private postgresUrl;
51
+ constructor(postgresUrl: string);
52
+ migrate(): Promise<void>;
53
+ end(): Promise<void>;
54
+ getSpec(name: string): Promise<Spec | null>;
55
+ getSpecVersion(name: string, semver: string): Promise<SpecVersion | null>;
56
+ getLatestVersion(name: string): Promise<SpecVersion | null>;
57
+ listSpecs(filters?: SpecFilters): Promise<Spec[]>;
58
+ listVersions(name: string, options?: {
59
+ limit?: number;
60
+ offset?: number;
61
+ }): Promise<{
62
+ versions: SpecVersion[];
63
+ total: number;
64
+ }>;
65
+ pushSpecVersion(spec: Spec, version: SpecVersion): Promise<SpecVersion>;
66
+ deleteSpec(name: string): Promise<boolean>;
67
+ getCompatReport(name: string, semver: string): Promise<CompatReport | null>;
68
+ logAudit(action: AuditAction, actor: string, specName: string, version: string | undefined, details: Record<string, unknown> | undefined): Promise<void>;
69
+ private mapSpecRow;
70
+ private mapVersionRow;
71
+ getGatewayConfig(name: string): Promise<GatewayConfig | null>;
72
+ listGatewayConfigs(): Promise<GatewayConfig[]>;
73
+ getGatewayConfigVersion(name: string, versionId: string): Promise<GatewayConfigVersion | null>;
74
+ getLatestGatewayConfigVersion(name: string): Promise<GatewayConfigVersion | null>;
75
+ listGatewayConfigVersions(name: string): Promise<GatewayConfigVersion[]>;
76
+ pushGatewayConfigVersion(config: GatewayConfig, version: GatewayConfigVersion): Promise<GatewayConfigVersion>;
77
+ recordProvision(provision: Provision): Promise<void>;
78
+ listProvisions(gatewayConfigName?: string): Promise<Provision[]>;
79
+ private mapGatewayConfigRow;
80
+ private mapGatewayConfigVersionRow;
81
+ recordGatewayLog(log: GatewayLog): Promise<void>;
82
+ listGatewayLogs(filters: GatewayLogFilters): Promise<{
83
+ logs: GatewayLog[];
84
+ total: number;
85
+ }>;
86
+ getGatewayLog(id: string): Promise<GatewayLog | null>;
87
+ getGatewayLogStats(_filters: GatewayLogFilters): Promise<GatewayLogStats[]>;
88
+ deleteGatewayLogsOlderThan(days: number): Promise<void>;
89
+ }
90
+
91
+ interface RunningServer {
92
+ app: ReturnType<typeof createApp>;
93
+ store: SQLiteSpecStore | PostgreSQLSpecStore;
94
+ server: ServerType;
95
+ }
96
+ declare function startServer(userConfig?: Partial<ServerConfig>): Promise<RunningServer>;
97
+
98
+ export { type RunningServer, ServerConfig, startServer };