@grapity/grapity 0.3.0 → 0.4.1

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/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { Command as Command20 } from "commander";
4
+ import { Command as Command21 } from "commander";
5
5
 
6
6
  // src/cli/commands/registry/index.ts
7
7
  import { Command as Command8 } from "commander";
@@ -18,6 +18,7 @@ import fs from "fs";
18
18
  import os from "os";
19
19
  import path from "path";
20
20
  import yaml from "js-yaml";
21
+ var CONFIG_PATH = () => path.join(os.homedir(), ".grapity", "config.yaml");
21
22
  var DEFAULT_CONFIG = {
22
23
  mode: "local",
23
24
  local: {
@@ -25,8 +26,11 @@ var DEFAULT_CONFIG = {
25
26
  database: "sqlite"
26
27
  }
27
28
  };
29
+ function configExists() {
30
+ return fs.existsSync(CONFIG_PATH());
31
+ }
28
32
  function getConfig() {
29
- const configPath = path.join(os.homedir(), ".grapity", "config.yaml");
33
+ const configPath = CONFIG_PATH();
30
34
  if (!fs.existsSync(configPath)) {
31
35
  return DEFAULT_CONFIG;
32
36
  }
@@ -52,7 +56,8 @@ function getConfig() {
52
56
  port: config.local?.port ?? DEFAULT_CONFIG.local.port,
53
57
  database,
54
58
  sqlitePath: config.local?.sqlitePath,
55
- postgresUrl: config.local?.postgresUrl
59
+ postgresUrl: config.local?.postgresUrl,
60
+ auth: config.local?.auth
56
61
  }
57
62
  };
58
63
  }
@@ -67,6 +72,169 @@ function isPostgresqlUrl(value) {
67
72
  return value.startsWith("postgresql://") || value.startsWith("postgres://");
68
73
  }
69
74
 
75
+ // src/cli/auth.ts
76
+ import { decodeJwt } from "jose";
77
+
78
+ // src/registry/auth/middleware.ts
79
+ import { createRemoteJWKSet, jwtVerify } from "jose";
80
+ var AuthError = class extends Error {
81
+ constructor(statusCode, code, message) {
82
+ super(message);
83
+ this.statusCode = statusCode;
84
+ this.code = code;
85
+ this.name = "AuthError";
86
+ }
87
+ statusCode;
88
+ code;
89
+ };
90
+ function buildKeycloakUrls(config) {
91
+ const base = `${config.serverUrl}/realms/${config.realm}`;
92
+ return {
93
+ issuer: base,
94
+ jwksUri: `${base}/protocol/openid-connect/certs`,
95
+ tokenUrl: `${base}/protocol/openid-connect/token`
96
+ };
97
+ }
98
+ function createAuthMiddleware(config, routeScopes2) {
99
+ if (config.auth?.mode !== "keycloak") {
100
+ return async (_c, next) => await next();
101
+ }
102
+ const authConfig = config.auth;
103
+ const { issuer, jwksUri } = buildKeycloakUrls(authConfig);
104
+ const jwks = createRemoteJWKSet(new URL(jwksUri));
105
+ const scopeByRoute = /* @__PURE__ */ new Map();
106
+ for (const route of routeScopes2) {
107
+ const key = `${route.method.toUpperCase()}:${route.path}`;
108
+ scopeByRoute.set(key, {
109
+ operationId: route.operationId,
110
+ scopes: route.scopes
111
+ });
112
+ }
113
+ return async (c2, next) => {
114
+ const matchedPath = c2.req.matchedRoutes.map((r) => r.path).filter((p) => p !== "/*").pop();
115
+ const routeKey = matchedPath ? `${c2.req.method}:${matchedPath}` : `${c2.req.method}:${c2.req.routePath}`;
116
+ const required = scopeByRoute.get(routeKey);
117
+ if (!required || required.scopes.length === 0) {
118
+ return await next();
119
+ }
120
+ const authHeader = c2.req.header("Authorization");
121
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
122
+ throw new AuthError(401, "unauthorized", "Bearer token required");
123
+ }
124
+ const token = authHeader.slice("Bearer ".length).trim();
125
+ if (!token) {
126
+ throw new AuthError(401, "unauthorized", "Bearer token required");
127
+ }
128
+ let payload;
129
+ try {
130
+ const result = await jwtVerify(token, jwks, {
131
+ issuer,
132
+ audience: authConfig.audience
133
+ });
134
+ payload = result.payload;
135
+ } catch (err) {
136
+ const message = err instanceof Error ? err.message : "Invalid token";
137
+ throw new AuthError(401, "unauthorized", `Invalid or expired token: ${message}`);
138
+ }
139
+ const subject = payload.sub;
140
+ if (!subject) {
141
+ throw new AuthError(401, "unauthorized", "Token missing subject claim");
142
+ }
143
+ const grantedScopes = extractScopes(payload, authConfig.roleSource ?? "scope");
144
+ const missing = required.scopes.filter((s) => !grantedScopes.has(s));
145
+ if (missing.length > 0) {
146
+ throw new AuthError(
147
+ 403,
148
+ "forbidden",
149
+ `Missing required scope${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`
150
+ );
151
+ }
152
+ c2.set("actor", subject);
153
+ c2.set("claims", payload);
154
+ await next();
155
+ };
156
+ }
157
+ function extractScopes(payload, source) {
158
+ if (source === "realm_access.roles") {
159
+ const roles = payload.realm_access?.roles;
160
+ return new Set(roles ?? []);
161
+ }
162
+ const scopeValue = payload.scope;
163
+ if (typeof scopeValue === "string") {
164
+ return new Set(scopeValue.split(/\s+/).filter(Boolean));
165
+ }
166
+ return /* @__PURE__ */ new Set();
167
+ }
168
+
169
+ // src/cli/auth.ts
170
+ var cachedToken = null;
171
+ function resetTokenCache() {
172
+ cachedToken = null;
173
+ }
174
+ async function getAccessToken(config) {
175
+ const staticToken = process.env.GRAPITY_TOKEN;
176
+ if (staticToken) {
177
+ return staticToken;
178
+ }
179
+ const now = Math.floor(Date.now() / 1e3);
180
+ if (cachedToken && cachedToken.expiresAt > now + 60) {
181
+ return cachedToken.accessToken;
182
+ }
183
+ const clientSecret = process.env.GRAPITY_CLIENT_SECRET;
184
+ if (!clientSecret) {
185
+ throw new Error(
186
+ "Keycloak client secret not found. Set GRAPITY_CLIENT_SECRET or provide a static token via GRAPITY_TOKEN."
187
+ );
188
+ }
189
+ const { tokenUrl } = buildKeycloakUrls({ serverUrl: config.serverUrl, realm: config.realm });
190
+ const params = new URLSearchParams();
191
+ params.set("grant_type", "client_credentials");
192
+ params.set("client_id", config.clientId);
193
+ params.set("client_secret", clientSecret);
194
+ if (config.audience) {
195
+ params.set("audience", config.audience);
196
+ }
197
+ const response = await fetch(tokenUrl, {
198
+ method: "POST",
199
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
200
+ body: params.toString()
201
+ });
202
+ if (!response.ok) {
203
+ const text3 = await response.text().catch(() => "Unknown error");
204
+ throw new Error(`Keycloak token request failed: ${response.status} ${text3}`);
205
+ }
206
+ const data = await response.json();
207
+ cachedToken = {
208
+ accessToken: data.access_token,
209
+ expiresAt: now + data.expires_in
210
+ };
211
+ return data.access_token;
212
+ }
213
+ function decodeToken(token) {
214
+ try {
215
+ const payload = decodeJwt(token);
216
+ return {
217
+ sub: payload.sub,
218
+ exp: payload.exp,
219
+ iss: payload.iss,
220
+ scope: typeof payload.scope === "string" ? payload.scope : void 0,
221
+ roles: payload.realm_access?.roles
222
+ };
223
+ } catch {
224
+ return {};
225
+ }
226
+ }
227
+ function formatTokenStatus(token) {
228
+ const decoded = decodeToken(token);
229
+ const now = Math.floor(Date.now() / 1e3);
230
+ return {
231
+ valid: !!decoded.sub,
232
+ sub: decoded.sub,
233
+ exp: decoded.exp,
234
+ expired: decoded.exp ? decoded.exp < now : void 0
235
+ };
236
+ }
237
+
70
238
  // src/cli/client.ts
71
239
  var BreakingChangeError = class extends Error {
72
240
  constructor(compatReport) {
@@ -76,13 +244,30 @@ var BreakingChangeError = class extends Error {
76
244
  }
77
245
  compatReport;
78
246
  };
247
+ async function getAuthHeaders() {
248
+ const config = getConfig();
249
+ const authConfig = config.mode === "remote" ? config.remote?.auth : config.local?.auth;
250
+ const headers = {};
251
+ if (authConfig?.mode === "keycloak") {
252
+ if (!authConfig.clientId) {
253
+ throw new Error(
254
+ "Keycloak auth is configured but clientId is missing. Run grapity init with --keycloak-client-id."
255
+ );
256
+ }
257
+ const token = await getAccessToken(authConfig);
258
+ headers["Authorization"] = `Bearer ${token}`;
259
+ }
260
+ return headers;
261
+ }
79
262
  async function request(method, path12, body) {
80
263
  const baseUrl = getRegistryUrl();
81
264
  const url = `${baseUrl}${path12}`;
265
+ const authHeaders = await getAuthHeaders();
82
266
  const response = await fetch(url, {
83
267
  method,
84
268
  headers: {
85
- "Content-Type": "application/json"
269
+ "Content-Type": "application/json",
270
+ ...authHeaders
86
271
  },
87
272
  body: body ? JSON.stringify(body) : void 0
88
273
  });
@@ -99,7 +284,11 @@ async function request(method, path12, body) {
99
284
  async function requestText(method, path12) {
100
285
  const baseUrl = getRegistryUrl();
101
286
  const url = `${baseUrl}${path12}`;
102
- const response = await fetch(url, { method });
287
+ const authHeaders = await getAuthHeaders();
288
+ const response = await fetch(url, {
289
+ method,
290
+ headers: authHeaders
291
+ });
103
292
  if (!response.ok) {
104
293
  const error = await response.json();
105
294
  throw new Error(error.message ?? `Request failed: ${response.status}`);
@@ -534,12 +723,24 @@ function formatInitSuccess(params) {
534
723
  if (params.database) lines.push(` ${c.label("Database")} ${c.primary(params.database)}`);
535
724
  if (params.dbPath) lines.push(` ${c.label("Path")} ${c.dim(params.dbPath)}`);
536
725
  if (params.postgresUrl) lines.push(` ${c.label("PostgreSQL")} ${c.dim(params.postgresUrl)}`);
537
- lines.push("");
538
- lines.push(` ${c.dim("\u203A")} Start the server with: ${c.primary("grapity serve")}`);
539
726
  } else {
540
727
  if (params.url) lines.push(` ${c.label("URL")} ${c.cyan(params.url)}`);
728
+ }
729
+ if (params.authMode && params.authMode !== "none") {
730
+ lines.push("");
731
+ lines.push(` ${c.label("Auth")} ${c.primary(params.authMode)}`);
732
+ if (params.keycloakServer) lines.push(` ${c.label("Keycloak")} ${c.dim(params.keycloakServer)}`);
733
+ if (params.keycloakRealm) lines.push(` ${c.label("Realm")} ${c.dim(params.keycloakRealm)}`);
734
+ if (params.keycloakClientId) lines.push(` ${c.label("Client ID")} ${c.dim(params.keycloakClientId)}`);
735
+ lines.push("");
736
+ lines.push(` ${c.dim("\u203A")} Set the client secret in GRAPITY_CLIENT_SECRET before running commands.`);
737
+ } else {
541
738
  lines.push("");
542
- lines.push(` ${c.dim("\u203A")} Push a spec with: ${c.primary("grapity registry push ./openapi.yaml --name my-api")}`);
739
+ if (params.mode === "local") {
740
+ lines.push(` ${c.dim("\u203A")} Start the server with: ${c.primary("grapity serve")}`);
741
+ } else {
742
+ lines.push(` ${c.dim("\u203A")} Push a spec with: ${c.primary("grapity registry push ./openapi.yaml --name my-api")}`);
743
+ }
543
744
  }
544
745
  return lines.join("\n");
545
746
  }
@@ -555,6 +756,16 @@ function formatServeConfig(params) {
555
756
  if (params.database === "postgresql" && params.postgresUrl) {
556
757
  lines.push(` ${c.label("PostgreSQL")} ${c.dim(maskPostgresUrl(params.postgresUrl))}`);
557
758
  }
759
+ lines.push(` ${c.label("Auth")} ${c.primary(params.authMode ?? "none")}`);
760
+ if (params.keycloakServer) {
761
+ lines.push(` ${c.label("Keycloak")} ${c.dim(params.keycloakServer)}`);
762
+ }
763
+ if (params.keycloakRealm) {
764
+ lines.push(` ${c.label("Realm")} ${c.dim(params.keycloakRealm)}`);
765
+ }
766
+ if (params.keycloakAudience) {
767
+ lines.push(` ${c.label("Audience")} ${c.dim(params.keycloakAudience)}`);
768
+ }
558
769
  return lines.join("\n");
559
770
  }
560
771
  function maskPostgresUrl(url) {
@@ -598,6 +809,39 @@ function formatEmptyState(message, hints) {
598
809
  function formatReady(port) {
599
810
  return ` ${c.success("\u25CF")} Server ready ${c.label("\xB7")} ${c.cyan(`http://localhost:${port}`)}`;
600
811
  }
812
+ function formatAuthStatus(params) {
813
+ const lines = [];
814
+ if (params.cleared) {
815
+ lines.push(` ${c.success("\u2713")} Cached access token cleared`);
816
+ return lines.join("\n");
817
+ }
818
+ lines.push(` ${c.label("Mode")} ${c.primary(params.mode)}`);
819
+ if (!params.configured) {
820
+ lines.push(` ${c.dim("\xB7")} No remote authentication configured`);
821
+ return lines.join("\n");
822
+ }
823
+ if (params.serverUrl) lines.push(` ${c.label("Server")} ${c.dim(params.serverUrl)}`);
824
+ if (params.realm) lines.push(` ${c.label("Realm")} ${c.dim(params.realm)}`);
825
+ if (params.clientId) lines.push(` ${c.label("Client ID")} ${c.dim(params.clientId)}`);
826
+ if (params.audience) lines.push(` ${c.label("Audience")} ${c.dim(params.audience)}`);
827
+ lines.push("");
828
+ if (params.valid === false) {
829
+ lines.push(` ${c.error("\u2717")} Token is not a valid JWT`);
830
+ } else if (params.sub) {
831
+ lines.push(` ${c.success("\u2713")} Authenticated as ${c.primary(params.sub)}`);
832
+ if (params.exp) {
833
+ const date = new Date(params.exp * 1e3).toISOString();
834
+ if (params.expired) {
835
+ lines.push(` ${c.error("\u2717")} Token expired at ${c.dim(date)}`);
836
+ } else {
837
+ lines.push(` ${c.success("\u2713")} Token valid until ${c.dim(date)}`);
838
+ }
839
+ }
840
+ } else {
841
+ lines.push(` ${c.dim("\xB7")} No token fetched yet`);
842
+ }
843
+ return lines.join("\n");
844
+ }
601
845
  function formatHubReady(port) {
602
846
  return ` ${c.success("\u25CF")} Hub ready ${c.label("\xB7")} ${c.cyan(`http://localhost:${port}`)}`;
603
847
  }
@@ -1396,7 +1640,7 @@ import fs8 from "fs";
1396
1640
  import os3 from "os";
1397
1641
  import path8 from "path";
1398
1642
  import yaml4 from "js-yaml";
1399
- var initCommand = new Command18("init").description("Configure grapity registry (local or remote mode)").option("--local", "Use local mode (SQLite or PostgreSQL)").option("--remote", "Use remote mode (connect to a grapity server)").option("--url <url>", "Registry URL (for remote mode)").option("--port <port>", "Port for local server (default: 3750)").option("--db <path-or-url>", "SQLite path or postgresql:// URL (for local mode)").action(async (options) => {
1643
+ var initCommand = new Command18("init").description("Configure grapity registry (local or remote mode)").option("--local", "Use local mode (SQLite or PostgreSQL)").option("--remote", "Use remote mode (connect to a grapity server)").option("--url <url>", "Registry URL (for remote mode)").option("--port <port>", "Port for local server (default: 3750)").option("--db <path-or-url>", "SQLite path or postgresql:// URL (for local mode)").option("--auth <mode>", "Auth mode: none | keycloak (default: none)").option("--keycloak-server <url>", "Keycloak server URL").option("--keycloak-realm <realm>", "Keycloak realm").option("--keycloak-client-id <id>", "Keycloak client ID (for CLI client credentials)").option("--keycloak-audience <audience>", "Keycloak token audience to validate").option("--keycloak-role-source <source>", "Where to read roles from: scope | realm_access.roles (default: scope)").action(async (options) => {
1400
1644
  const configDir = path8.join(os3.homedir(), ".grapity");
1401
1645
  const configPath = path8.join(configDir, "config.yaml");
1402
1646
  let mode;
@@ -1421,6 +1665,12 @@ var initCommand = new Command18("init").description("Configure grapity registry
1421
1665
  );
1422
1666
  process.exit(1);
1423
1667
  }
1668
+ const authMode = options.auth ?? "none";
1669
+ if (authMode !== "none" && authMode !== "keycloak") {
1670
+ console.error(formatError("invalid auth mode", "--auth must be one of: none, keycloak"));
1671
+ process.exit(1);
1672
+ }
1673
+ const keycloakAuth = authMode === "keycloak" ? parseKeycloakOptions(options, mode) : void 0;
1424
1674
  const config = { mode };
1425
1675
  if (mode === "local") {
1426
1676
  const dbValue = options.db ?? process.env.GRAPITY_DATABASE_URL;
@@ -1434,6 +1684,22 @@ var initCommand = new Command18("init").description("Configure grapity registry
1434
1684
  } else {
1435
1685
  config.local.sqlitePath = dbValue ?? path8.join(os3.homedir(), ".grapity", "registry.db");
1436
1686
  }
1687
+ if (keycloakAuth) {
1688
+ if (!options.keycloakClientId) {
1689
+ console.error(
1690
+ formatError(
1691
+ "missing flag",
1692
+ "--keycloak-client-id is required when using Keycloak auth."
1693
+ )
1694
+ );
1695
+ process.exit(1);
1696
+ }
1697
+ config.local.auth = {
1698
+ mode: "keycloak",
1699
+ clientId: options.keycloakClientId,
1700
+ ...keycloakAuth
1701
+ };
1702
+ }
1437
1703
  } else {
1438
1704
  if (!options.url) {
1439
1705
  console.error(
@@ -1448,6 +1714,16 @@ var initCommand = new Command18("init").description("Configure grapity registry
1448
1714
  config.remote = {
1449
1715
  url: options.url.replace(/\/$/, "")
1450
1716
  };
1717
+ if (keycloakAuth) {
1718
+ config.remote.auth = {
1719
+ mode: "keycloak",
1720
+ serverUrl: keycloakAuth.serverUrl,
1721
+ realm: keycloakAuth.realm,
1722
+ clientId: keycloakAuth.clientId,
1723
+ audience: keycloakAuth.audience,
1724
+ roleSource: keycloakAuth.roleSource
1725
+ };
1726
+ }
1451
1727
  }
1452
1728
  if (!fs8.existsSync(configDir)) {
1453
1729
  fs8.mkdirSync(configDir, { recursive: true });
@@ -1462,16 +1738,107 @@ var initCommand = new Command18("init").description("Configure grapity registry
1462
1738
  database: config.local?.database,
1463
1739
  dbPath: config.local?.sqlitePath,
1464
1740
  postgresUrl: config.local?.postgresUrl,
1465
- url: config.remote?.url
1741
+ url: config.remote?.url,
1742
+ authMode,
1743
+ keycloakServer: keycloakAuth?.serverUrl,
1744
+ keycloakRealm: keycloakAuth?.realm,
1745
+ keycloakClientId: config.local?.auth?.clientId ?? config.remote?.auth?.clientId
1466
1746
  })
1467
1747
  );
1468
1748
  });
1749
+ function parseKeycloakOptions(options, _mode) {
1750
+ if (!options.keycloakServer) {
1751
+ console.error(
1752
+ formatError(
1753
+ "missing flag",
1754
+ "--keycloak-server is required when auth mode is keycloak.",
1755
+ ["Example: --keycloak-server https://keycloak.example.com"]
1756
+ )
1757
+ );
1758
+ process.exit(1);
1759
+ }
1760
+ if (!options.keycloakRealm) {
1761
+ console.error(
1762
+ formatError(
1763
+ "missing flag",
1764
+ "--keycloak-realm is required when auth mode is keycloak.",
1765
+ ["Example: --keycloak-realm grapity"]
1766
+ )
1767
+ );
1768
+ process.exit(1);
1769
+ }
1770
+ const roleSource = options.keycloakRoleSource;
1771
+ if (roleSource && roleSource !== "scope" && roleSource !== "realm_access.roles") {
1772
+ console.error(formatError("invalid role source", "--keycloak-role-source must be one of: scope, realm_access.roles"));
1773
+ process.exit(1);
1774
+ }
1775
+ return {
1776
+ serverUrl: options.keycloakServer.replace(/\/$/, ""),
1777
+ realm: options.keycloakRealm,
1778
+ clientId: options.keycloakClientId,
1779
+ audience: options.keycloakAudience,
1780
+ roleSource
1781
+ };
1782
+ }
1469
1783
 
1470
- // src/cli/commands/serve.ts
1784
+ // src/cli/commands/auth.ts
1471
1785
  import { Command as Command19 } from "commander";
1786
+ var authCommand = new Command19("auth").description("Manage authentication for remote registry mode").addCommand(
1787
+ new Command19("status").description("Show current authentication status").action(async () => {
1788
+ const config = getConfig();
1789
+ const auth = config.mode === "remote" ? config.remote?.auth : config.local?.auth;
1790
+ if (!auth) {
1791
+ console.log(formatAuthStatus({ mode: "none", configured: false }));
1792
+ return;
1793
+ }
1794
+ if (auth.mode !== "keycloak") {
1795
+ console.log(formatAuthStatus({ mode: auth.mode, configured: true }));
1796
+ return;
1797
+ }
1798
+ if (!auth.clientId) {
1799
+ console.error(
1800
+ formatError(
1801
+ "auth misconfigured",
1802
+ "Keycloak auth is configured but clientId is missing. Run grapity init with --keycloak-client-id."
1803
+ )
1804
+ );
1805
+ process.exit(1);
1806
+ }
1807
+ try {
1808
+ const token = await getAccessToken(auth);
1809
+ const status = formatTokenStatus(token);
1810
+ console.log(
1811
+ formatAuthStatus({
1812
+ mode: auth.mode,
1813
+ configured: true,
1814
+ serverUrl: auth.serverUrl,
1815
+ realm: auth.realm,
1816
+ clientId: auth.clientId,
1817
+ audience: auth.audience,
1818
+ ...status
1819
+ })
1820
+ );
1821
+ } catch (err) {
1822
+ console.error(
1823
+ formatError(
1824
+ "auth failed",
1825
+ err instanceof Error ? err.message : "Unable to fetch access token"
1826
+ )
1827
+ );
1828
+ process.exit(1);
1829
+ }
1830
+ })
1831
+ ).addCommand(
1832
+ new Command19("clear").description("Clear the cached access token").action(() => {
1833
+ resetTokenCache();
1834
+ console.log(formatAuthStatus({ mode: "none", configured: false, cleared: true }));
1835
+ })
1836
+ );
1837
+
1838
+ // src/cli/commands/serve.ts
1839
+ import { Command as Command20 } from "commander";
1472
1840
  import os4 from "os";
1473
1841
  import path11 from "path";
1474
- import { readFileSync } from "fs";
1475
1842
 
1476
1843
  // src/registry/serve.ts
1477
1844
  import path9 from "path";
@@ -3231,6 +3598,7 @@ var pushRoute = new Hono().post("/", async (c2) => {
3231
3598
  }
3232
3599
  const store = c2.get("store");
3233
3600
  const service = new RegistryService(store);
3601
+ const actor = c2.get("actor") ?? body.pushedBy;
3234
3602
  try {
3235
3603
  const result = await service.pushSpec(body.content, body.name, {
3236
3604
  type: body.type,
@@ -3239,7 +3607,7 @@ var pushRoute = new Hono().post("/", async (c2) => {
3239
3607
  sourceRepo: body.sourceRepo,
3240
3608
  tags: Array.isArray(body.tags) ? body.tags : void 0,
3241
3609
  gitRef: body.gitRef,
3242
- pushedBy: body.pushedBy,
3610
+ pushedBy: actor,
3243
3611
  prerelease: body.prerelease,
3244
3612
  force: body.force,
3245
3613
  reason: body.reason
@@ -3404,7 +3772,8 @@ var deleteSpecRoute = new Hono5().delete(
3404
3772
  const name = c2.req.param("name");
3405
3773
  const store = c2.get("store");
3406
3774
  const service = new RegistryService(store);
3407
- const deleted = await service.deleteSpec(name);
3775
+ const actor = c2.get("actor");
3776
+ const deleted = await service.deleteSpec(name, actor);
3408
3777
  if (!deleted) {
3409
3778
  return c2.json({ error: "not_found", message: `Spec "${name}" not found`, statusCode: 404 }, 404);
3410
3779
  }
@@ -4145,6 +4514,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c2) => {
4145
4514
  }
4146
4515
  const store = c2.get("store");
4147
4516
  const service = new GatewayService(store, store);
4517
+ const actor = c2.get("actor") ?? body.pushedBy;
4148
4518
  try {
4149
4519
  const result = await service.pushGatewayConfig({
4150
4520
  name: body.name,
@@ -4155,7 +4525,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c2) => {
4155
4525
  environments: body.environments ?? {},
4156
4526
  callerIdentification: body.callerIdentification,
4157
4527
  content: body.content,
4158
- pushedBy: body.pushedBy
4528
+ pushedBy: actor
4159
4529
  });
4160
4530
  return c2.json({ data: result }, 201);
4161
4531
  } catch (err) {
@@ -4420,6 +4790,192 @@ var gatewayLogStatsRoute = new Hono21().get("/stats", async (c2) => {
4420
4790
  });
4421
4791
  });
4422
4792
 
4793
+ // src/registry/generated/route-scopes.ts
4794
+ var routeScopes = [
4795
+ {
4796
+ "method": "GET",
4797
+ "path": "/v1/health",
4798
+ "operationId": "getHealth",
4799
+ "scopes": []
4800
+ },
4801
+ {
4802
+ "method": "GET",
4803
+ "path": "/v1/specs",
4804
+ "operationId": "listSpecs",
4805
+ "scopes": [
4806
+ "specs:read"
4807
+ ]
4808
+ },
4809
+ {
4810
+ "method": "POST",
4811
+ "path": "/v1/specs",
4812
+ "operationId": "pushSpec",
4813
+ "scopes": [
4814
+ "specs:write"
4815
+ ]
4816
+ },
4817
+ {
4818
+ "method": "GET",
4819
+ "path": "/v1/specs/:name",
4820
+ "operationId": "getSpec",
4821
+ "scopes": [
4822
+ "specs:read"
4823
+ ]
4824
+ },
4825
+ {
4826
+ "method": "DELETE",
4827
+ "path": "/v1/specs/:name",
4828
+ "operationId": "deleteSpec",
4829
+ "scopes": [
4830
+ "specs:write"
4831
+ ]
4832
+ },
4833
+ {
4834
+ "method": "POST",
4835
+ "path": "/v1/specs/:name/validate",
4836
+ "operationId": "validateSpec",
4837
+ "scopes": [
4838
+ "specs:write"
4839
+ ]
4840
+ },
4841
+ {
4842
+ "method": "GET",
4843
+ "path": "/v1/specs/:name/versions",
4844
+ "operationId": "listVersions",
4845
+ "scopes": [
4846
+ "specs:read"
4847
+ ]
4848
+ },
4849
+ {
4850
+ "method": "GET",
4851
+ "path": "/v1/specs/:name/versions/:semver",
4852
+ "operationId": "getVersion",
4853
+ "scopes": [
4854
+ "specs:read"
4855
+ ]
4856
+ },
4857
+ {
4858
+ "method": "GET",
4859
+ "path": "/v1/specs/:name/spec.json",
4860
+ "operationId": "getSpecJson",
4861
+ "scopes": [
4862
+ "specs:read"
4863
+ ]
4864
+ },
4865
+ {
4866
+ "method": "GET",
4867
+ "path": "/v1/specs/:name/spec.yaml",
4868
+ "operationId": "getSpecYaml",
4869
+ "scopes": [
4870
+ "specs:read"
4871
+ ]
4872
+ },
4873
+ {
4874
+ "method": "GET",
4875
+ "path": "/v1/specs/:name/versions/:semver/spec.json",
4876
+ "operationId": "getVersionSpecJson",
4877
+ "scopes": [
4878
+ "specs:read"
4879
+ ]
4880
+ },
4881
+ {
4882
+ "method": "GET",
4883
+ "path": "/v1/specs/:name/versions/:semver/spec.yaml",
4884
+ "operationId": "getVersionSpecYaml",
4885
+ "scopes": [
4886
+ "specs:read"
4887
+ ]
4888
+ },
4889
+ {
4890
+ "method": "GET",
4891
+ "path": "/v1/specs/:name/compat/:semver",
4892
+ "operationId": "getCompatReport",
4893
+ "scopes": [
4894
+ "specs:read"
4895
+ ]
4896
+ },
4897
+ {
4898
+ "method": "GET",
4899
+ "path": "/v1/specs/:name/compare",
4900
+ "operationId": "compareVersions",
4901
+ "scopes": [
4902
+ "specs:read"
4903
+ ]
4904
+ },
4905
+ {
4906
+ "method": "GET",
4907
+ "path": "/v1/gateway-configs",
4908
+ "operationId": "listGatewayConfigs",
4909
+ "scopes": [
4910
+ "gateway-configs:read"
4911
+ ]
4912
+ },
4913
+ {
4914
+ "method": "POST",
4915
+ "path": "/v1/gateway-configs",
4916
+ "operationId": "pushGatewayConfig",
4917
+ "scopes": [
4918
+ "gateway-configs:write"
4919
+ ]
4920
+ },
4921
+ {
4922
+ "method": "GET",
4923
+ "path": "/v1/gateway-configs/:name",
4924
+ "operationId": "getGatewayConfig",
4925
+ "scopes": [
4926
+ "gateway-configs:read"
4927
+ ]
4928
+ },
4929
+ {
4930
+ "method": "GET",
4931
+ "path": "/v1/gateway-configs/:name/versions",
4932
+ "operationId": "listGatewayConfigVersions",
4933
+ "scopes": [
4934
+ "gateway-configs:read"
4935
+ ]
4936
+ },
4937
+ {
4938
+ "method": "GET",
4939
+ "path": "/v1/gateway-configs/:name/versions/:versionId",
4940
+ "operationId": "getGatewayConfigVersion",
4941
+ "scopes": [
4942
+ "gateway-configs:read"
4943
+ ]
4944
+ },
4945
+ {
4946
+ "method": "POST",
4947
+ "path": "/v1/gateway-logs/ingest/:provider/:environment",
4948
+ "operationId": "ingestGatewayLog",
4949
+ "scopes": [
4950
+ "gateway-logs:write"
4951
+ ]
4952
+ },
4953
+ {
4954
+ "method": "GET",
4955
+ "path": "/v1/gateway-logs",
4956
+ "operationId": "listGatewayLogs",
4957
+ "scopes": [
4958
+ "gateway-logs:read"
4959
+ ]
4960
+ },
4961
+ {
4962
+ "method": "GET",
4963
+ "path": "/v1/gateway-logs/stats",
4964
+ "operationId": "getGatewayLogStats",
4965
+ "scopes": [
4966
+ "gateway-logs:read"
4967
+ ]
4968
+ },
4969
+ {
4970
+ "method": "GET",
4971
+ "path": "/v1/gateway-logs/:id",
4972
+ "operationId": "getGatewayLog",
4973
+ "scopes": [
4974
+ "gateway-logs:read"
4975
+ ]
4976
+ }
4977
+ ];
4978
+
4423
4979
  // src/registry/server.ts
4424
4980
  function createApp(config, store) {
4425
4981
  const app = new Hono22();
@@ -4431,6 +4987,21 @@ function createApp(config, store) {
4431
4987
  c2.set("config", config);
4432
4988
  await next();
4433
4989
  });
4990
+ const authRouteScopes = config.auth?.mode === "keycloak" ? routeScopes : [];
4991
+ app.use("*", createAuthMiddleware(config, authRouteScopes));
4992
+ app.onError((err, c2) => {
4993
+ if (err instanceof AuthError) {
4994
+ return c2.json(
4995
+ { error: err.code, message: err.message, statusCode: err.statusCode },
4996
+ err.statusCode
4997
+ );
4998
+ }
4999
+ console.error("Unhandled error:", err);
5000
+ return c2.json(
5001
+ { error: "internal_error", message: "Internal server error", statusCode: 500 },
5002
+ 500
5003
+ );
5004
+ });
4434
5005
  app.route("/v1/specs", pushRoute);
4435
5006
  app.route("/v1/specs", validateRoute);
4436
5007
  app.route("/v1/specs", listRoute);
@@ -4458,7 +5029,8 @@ function createApp(config, store) {
4458
5029
  // src/registry/config.ts
4459
5030
  var defaultConfig = {
4460
5031
  port: 3750,
4461
- database: "sqlite"
5032
+ database: "sqlite",
5033
+ auth: { mode: "none" }
4462
5034
  };
4463
5035
 
4464
5036
  // src/registry/storage/sqlite.ts
@@ -5071,17 +5643,63 @@ var provisions2 = pgTable("provisions", {
5071
5643
  // src/registry/storage/postgresql.ts
5072
5644
  import { v4 as uuid6 } from "uuid";
5073
5645
  var MIGRATIONS_FOLDER2 = PG_MIGRATIONS_FOLDER;
5646
+ var DatabaseConnectionError = class extends Error {
5647
+ constructor(message, postgresUrl, options) {
5648
+ super(message, options);
5649
+ this.postgresUrl = postgresUrl;
5650
+ this.name = "DatabaseConnectionError";
5651
+ }
5652
+ postgresUrl;
5653
+ };
5654
+ function isConnectionError(err) {
5655
+ if (typeof err !== "object" || err === null) return false;
5656
+ if ("code" in err) {
5657
+ const code = err.code;
5658
+ if (code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND") {
5659
+ return true;
5660
+ }
5661
+ }
5662
+ if (err instanceof AggregateError) {
5663
+ if (err.errors.some(isConnectionError)) return true;
5664
+ }
5665
+ if ("cause" in err && err.cause) {
5666
+ return isConnectionError(err.cause);
5667
+ }
5668
+ return false;
5669
+ }
5670
+ function maskPostgresPassword(url) {
5671
+ try {
5672
+ const parsed = new URL(url);
5673
+ if (parsed.password) parsed.password = "***";
5674
+ return parsed.toString();
5675
+ } catch {
5676
+ return url;
5677
+ }
5678
+ }
5074
5679
  var PostgreSQLSpecStore = class {
5075
5680
  db;
5076
5681
  pool;
5682
+ postgresUrl;
5077
5683
  constructor(postgresUrl) {
5684
+ this.postgresUrl = postgresUrl;
5078
5685
  this.pool = new Pool({ connectionString: postgresUrl });
5079
5686
  this.db = drizzle2(this.pool);
5080
5687
  }
5081
5688
  async migrate() {
5082
- await migrate2(this.db, {
5083
- migrationsFolder: MIGRATIONS_FOLDER2
5084
- });
5689
+ try {
5690
+ await migrate2(this.db, {
5691
+ migrationsFolder: MIGRATIONS_FOLDER2
5692
+ });
5693
+ } catch (err) {
5694
+ if (isConnectionError(err)) {
5695
+ throw new DatabaseConnectionError(
5696
+ `PostgreSQL is not reachable at ${maskPostgresPassword(this.postgresUrl)}`,
5697
+ this.postgresUrl,
5698
+ { cause: err }
5699
+ );
5700
+ }
5701
+ throw err;
5702
+ }
5085
5703
  }
5086
5704
  async end() {
5087
5705
  await this.pool.end();
@@ -5511,15 +6129,34 @@ var DEFAULT_REGISTRY_URL = "http://localhost:3750";
5511
6129
  async function startHubServer(userConfig) {
5512
6130
  const config = {
5513
6131
  port: userConfig?.port ?? DEFAULT_PORT,
5514
- registryUrl: userConfig?.registryUrl ?? DEFAULT_REGISTRY_URL
6132
+ registryUrl: userConfig?.registryUrl ?? DEFAULT_REGISTRY_URL,
6133
+ auth: userConfig?.auth
5515
6134
  };
5516
6135
  const app = new Hono23();
6136
+ app.get("/config.js", (c2) => {
6137
+ const clientConfig = {
6138
+ registryUrl: config.registryUrl,
6139
+ auth: config.auth ? {
6140
+ mode: config.auth.mode,
6141
+ serverUrl: config.auth.serverUrl,
6142
+ realm: config.auth.realm,
6143
+ clientId: config.auth.clientId,
6144
+ audience: config.auth.audience
6145
+ } : void 0
6146
+ };
6147
+ c2.header("Content-Type", "application/javascript");
6148
+ return c2.body(
6149
+ `window.__GRAPITY_CONFIG__ = ${JSON.stringify(clientConfig)};`
6150
+ );
6151
+ });
5517
6152
  app.use("/v1/*", async (c2) => {
5518
6153
  const url = new URL(c2.req.url);
5519
6154
  const targetUrl = config.registryUrl + url.pathname + url.search;
6155
+ const headers = new Headers(c2.req.raw.headers);
6156
+ headers.delete("host");
5520
6157
  const response = await fetch(targetUrl, {
5521
6158
  method: c2.req.method,
5522
- headers: c2.req.raw.headers,
6159
+ headers,
5523
6160
  body: c2.req.raw.body
5524
6161
  });
5525
6162
  return response;
@@ -5527,13 +6164,21 @@ async function startHubServer(userConfig) {
5527
6164
  app.use("/*", serveStatic({ root: HUB_DIST_PATH }));
5528
6165
  app.get("/*", async (c2) => {
5529
6166
  const indexPath = path10.join(HUB_DIST_PATH, "index.html");
5530
- if (fs10.existsSync(indexPath)) {
5531
- return c2.html(fs10.readFileSync(indexPath, "utf-8"));
6167
+ if (!fs10.existsSync(indexPath)) {
6168
+ return c2.text(
6169
+ "index.html not found. Build the project with 'bun run build' first.",
6170
+ 404
6171
+ );
5532
6172
  }
5533
- return c2.text(
5534
- "index.html not found. Build the project with 'bun run build' first.",
5535
- 404
6173
+ const html = fs10.readFileSync(indexPath, "utf-8");
6174
+ const configScript = `
6175
+ <script src="/config.js"></script>
6176
+ `;
6177
+ const injected = html.replace(
6178
+ "</head>",
6179
+ `${configScript}</head>`
5536
6180
  );
6181
+ return c2.html(injected);
5537
6182
  });
5538
6183
  serve2({
5539
6184
  fetch: app.fetch,
@@ -5545,76 +6190,148 @@ if (process.argv[1] === new URL(import.meta.url).pathname) {
5545
6190
  }
5546
6191
 
5547
6192
  // src/cli/commands/serve.ts
5548
- function getPackageVersion() {
5549
- try {
5550
- const pkgPath = new URL("../../../../package.json", import.meta.url);
5551
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
5552
- return pkg.version;
5553
- } catch {
5554
- return "unknown";
5555
- }
5556
- }
5557
6193
  function resolveServerConfig(cliOptions, cliConfig) {
6194
+ if (cliConfig.mode === "remote") {
6195
+ throw new Error(
6196
+ `grapity serve is for local mode only. Your config is set to remote (${cliConfig.remote?.url ?? "no URL"}). Use grapity serve on the machine running the local registry.`
6197
+ );
6198
+ }
5558
6199
  const envUrl = process.env.GRAPITY_DATABASE_URL;
5559
- const dbValue = envUrl ?? cliOptions.db;
5560
- if (dbValue) {
5561
- if (isPostgresqlUrl(dbValue)) {
5562
- return { port: cliOptions.port, database: "postgresql", postgresUrl: dbValue };
5563
- }
5564
- return { port: cliOptions.port, database: "sqlite", sqlitePath: dbValue };
6200
+ const local = cliConfig.local;
6201
+ const dbValue = envUrl ?? local?.postgresUrl;
6202
+ const base = { port: cliOptions.port };
6203
+ if (dbValue && isPostgresqlUrl(dbValue)) {
6204
+ base.database = "postgresql";
6205
+ base.postgresUrl = dbValue;
6206
+ } else {
6207
+ base.database = "sqlite";
6208
+ base.sqlitePath = dbValue ?? local?.sqlitePath ?? path11.join(os4.homedir(), ".grapity", "registry.db");
5565
6209
  }
5566
- if (cliConfig.mode === "local") {
5567
- const local = cliConfig.local;
5568
- if (local?.database === "postgresql") {
5569
- if (!local.postgresUrl) {
5570
- throw new Error(
5571
- "PostgreSQL is configured but no postgresUrl is set. Run grapity init --local --db postgresql://... or set GRAPITY_DATABASE_URL."
5572
- );
5573
- }
5574
- return { port: cliOptions.port, database: "postgresql", postgresUrl: local.postgresUrl };
5575
- }
5576
- return {
5577
- port: cliOptions.port,
5578
- database: "sqlite",
5579
- sqlitePath: local?.sqlitePath ?? path11.join(os4.homedir(), ".grapity", "registry.db")
5580
- };
6210
+ if (cliOptions.noAuth) {
6211
+ base.auth = { mode: "none" };
6212
+ } else {
6213
+ base.auth = resolveAuthConfig(local?.auth);
6214
+ }
6215
+ return base;
6216
+ }
6217
+ function resolveAuthConfig(authConfig) {
6218
+ if (!authConfig || authConfig.mode === "none") {
6219
+ throw new Error(
6220
+ "Authentication is required by default. Configure it with: grapity init --local --auth keycloak ...\nOr run with: grapity serve --no-auth"
6221
+ );
6222
+ }
6223
+ if (!authConfig.serverUrl) {
6224
+ throw new Error(
6225
+ "Keycloak auth is configured but serverUrl is missing. Run grapity init --local --auth keycloak --keycloak-server <url> ..."
6226
+ );
6227
+ }
6228
+ if (!authConfig.realm) {
6229
+ throw new Error(
6230
+ "Keycloak auth is configured but realm is missing. Run grapity init --local --auth keycloak --keycloak-realm <realm> ..."
6231
+ );
5581
6232
  }
5582
6233
  return {
5583
- port: cliOptions.port,
5584
- database: "sqlite",
5585
- sqlitePath: path11.join(os4.homedir(), ".grapity", "registry.db")
6234
+ mode: "keycloak",
6235
+ serverUrl: authConfig.serverUrl.replace(/\/$/, ""),
6236
+ realm: authConfig.realm,
6237
+ audience: authConfig.audience,
6238
+ roleSource: authConfig.roleSource
5586
6239
  };
5587
6240
  }
5588
- function createServeCommand(_version) {
5589
- return new Command19("serve").description("Start the local grapity registry server").option("-p, --port <port>", "Port to listen on", "3750").option("--hub-port <port>", "Port for the developer portal (Hub)", "3000").option("--no-hub", "Skip starting the developer portal").option("--db <path-or-url>", "SQLite path or postgresql:// URL").action(async (options) => {
6241
+ async function verifyKeycloakReachable(serverUrl, realm) {
6242
+ const url = `${serverUrl}/realms/${realm}/.well-known/openid-configuration`;
6243
+ try {
6244
+ const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
6245
+ if (!res.ok) {
6246
+ throw new Error(`Keycloak returned ${res.status}`);
6247
+ }
6248
+ } catch (err) {
6249
+ const message = err instanceof Error ? err.message : String(err);
6250
+ throw new Error(
6251
+ `Keycloak is not reachable at ${url}: ${message}
6252
+ See https://grapity.dev/docs/cli-reference/init#local-mode-with-keycloak to set up a local Keycloak server.
6253
+ Or run with: grapity serve --no-auth`
6254
+ );
6255
+ }
6256
+ }
6257
+ function formatDatabaseConnectionError(err) {
6258
+ return formatError(
6259
+ "PostgreSQL is not reachable",
6260
+ err.message,
6261
+ [
6262
+ "Make sure PostgreSQL is running and accessible.",
6263
+ "Check the database URL in ~/.grapity/config.yaml or GRAPITY_DATABASE_URL."
6264
+ ]
6265
+ );
6266
+ }
6267
+ function createServeCommand(version2) {
6268
+ return new Command20("serve").description("Start the local grapity registry server").option("-p, --port <port>", "Port to listen on", "3750").option("--hub-port <port>", "Port for the developer portal (Hub)", "3000").option("--no-hub", "Skip starting the developer portal").option("--no-auth", "Start without authentication").action(async (options) => {
5590
6269
  const port = parseInt(options.port, 10);
5591
6270
  const hubPort = parseInt(options.hubPort, 10);
5592
6271
  const startHub = options.hub !== false;
5593
- const version2 = getPackageVersion();
6272
+ const noAuth = options.auth === false;
6273
+ if (!configExists()) {
6274
+ console.error(
6275
+ formatError(
6276
+ "not initialized",
6277
+ "Run grapity init first to configure the local registry.",
6278
+ ["Example: grapity init --local"]
6279
+ )
6280
+ );
6281
+ process.exit(1);
6282
+ }
5594
6283
  const cliConfig = getConfig();
5595
6284
  let serverConfig;
5596
6285
  try {
5597
- serverConfig = resolveServerConfig({ port, db: options.db }, cliConfig);
6286
+ serverConfig = resolveServerConfig({ port, noAuth }, cliConfig);
5598
6287
  } catch (err) {
5599
6288
  console.error(formatError("invalid config", err.message));
5600
6289
  process.exit(1);
5601
6290
  }
5602
- console.log(formatHeader("grapity Registry", `v${version2}`));
6291
+ if (serverConfig.auth?.mode === "keycloak" && !noAuth) {
6292
+ try {
6293
+ await verifyKeycloakReachable(
6294
+ serverConfig.auth.serverUrl,
6295
+ serverConfig.auth.realm
6296
+ );
6297
+ } catch (err) {
6298
+ console.error(formatError("keycloak unreachable", err.message));
6299
+ process.exit(1);
6300
+ }
6301
+ }
6302
+ console.log(formatHeader("grapity", `v${version2}`));
6303
+ console.log("");
6304
+ console.log(formatHeader("grapity Registry"));
5603
6305
  console.log("");
5604
6306
  console.log(
5605
6307
  formatServeConfig({
5606
6308
  port,
5607
6309
  database: serverConfig.database,
5608
6310
  dbPath: serverConfig.sqlitePath,
5609
- postgresUrl: serverConfig.postgresUrl
6311
+ postgresUrl: serverConfig.postgresUrl,
6312
+ authMode: serverConfig.auth?.mode,
6313
+ keycloakServer: serverConfig.auth?.mode === "keycloak" ? serverConfig.auth.serverUrl : void 0,
6314
+ keycloakRealm: serverConfig.auth?.mode === "keycloak" ? serverConfig.auth.realm : void 0,
6315
+ keycloakAudience: serverConfig.auth?.mode === "keycloak" ? serverConfig.auth.audience : void 0
5610
6316
  })
5611
6317
  );
5612
6318
  console.log("");
5613
- const { store } = await startServer(serverConfig);
6319
+ let store;
6320
+ try {
6321
+ const started = await startServer(serverConfig);
6322
+ store = started.store;
6323
+ } catch (err) {
6324
+ if (err instanceof DatabaseConnectionError) {
6325
+ console.error(formatDatabaseConnectionError(err));
6326
+ } else {
6327
+ console.error(formatError("failed to start server", err.message));
6328
+ }
6329
+ process.exit(1);
6330
+ }
5614
6331
  console.log(formatReady(port));
5615
6332
  if (startHub) {
5616
6333
  console.log("");
5617
- console.log(formatHeader("grapity Hub", `v${version2}`));
6334
+ console.log(formatHeader("grapity Hub"));
5618
6335
  console.log("");
5619
6336
  console.log(
5620
6337
  formatHubConfig({
@@ -5625,7 +6342,14 @@ function createServeCommand(_version) {
5625
6342
  console.log("");
5626
6343
  await startHubServer({
5627
6344
  port: hubPort,
5628
- registryUrl: `http://localhost:${port}`
6345
+ registryUrl: `http://localhost:${port}`,
6346
+ auth: serverConfig.auth?.mode === "keycloak" ? {
6347
+ mode: "keycloak",
6348
+ serverUrl: serverConfig.auth.serverUrl,
6349
+ realm: serverConfig.auth.realm,
6350
+ clientId: "grapity-hub",
6351
+ audience: serverConfig.auth.audience
6352
+ } : void 0
5629
6353
  });
5630
6354
  console.log(formatHubReady(hubPort));
5631
6355
  }
@@ -5644,7 +6368,7 @@ function createServeCommand(_version) {
5644
6368
  import { createRequire } from "module";
5645
6369
  var require2 = createRequire(import.meta.url);
5646
6370
  var { version } = require2("../../package.json");
5647
- var program = new Command20();
6371
+ var program = new Command21();
5648
6372
  program.name("grapity").description("grapity - API spec registry and compatibility guardian").version(version).addHelpText(
5649
6373
  "after",
5650
6374
  "\nDocumentation: https://grapity.dev/docs/getting-started/quickstart"
@@ -5652,5 +6376,6 @@ program.name("grapity").description("grapity - API spec registry and compatibili
5652
6376
  program.addCommand(registryCommand);
5653
6377
  program.addCommand(gatewayCommand);
5654
6378
  program.addCommand(initCommand);
6379
+ program.addCommand(authCommand);
5655
6380
  program.addCommand(createServeCommand(version));
5656
6381
  program.parse();