@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/README.md +47 -14
- package/dist/assets/index-JAhtTTW2.js +336 -0
- package/dist/assets/index-JAhtTTW2.js.map +1 -0
- package/dist/assets/index-LDlidn22.css +1 -0
- package/dist/cli/index.js +1315 -181
- package/dist/core/index.d.ts +391 -23
- package/dist/hub/index.js +34 -7
- package/dist/hub/serve.d.ts +9 -1
- package/dist/hub/serve.js +34 -7
- package/dist/index.html +3 -2
- package/dist/registry/{index-Baj_sSgl.d.ts → index-Bx-7YlUF.d.ts} +18 -11
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +152 -6
- package/dist/registry/serve.d.ts +96 -5
- package/dist/registry/serve.js +714 -21
- package/package.json +13 -15
- package/dist/assets/index-Dq5tdnlb.js +0 -326
- package/dist/assets/index-Dq5tdnlb.js.map +0 -1
- package/dist/assets/index-NJpHAonA.css +0 -1
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
|
|
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,14 +18,19 @@ 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: {
|
|
24
|
-
port: 3750
|
|
25
|
+
port: 3750,
|
|
26
|
+
database: "sqlite"
|
|
25
27
|
}
|
|
26
28
|
};
|
|
29
|
+
function configExists() {
|
|
30
|
+
return fs.existsSync(CONFIG_PATH());
|
|
31
|
+
}
|
|
27
32
|
function getConfig() {
|
|
28
|
-
const configPath =
|
|
33
|
+
const configPath = CONFIG_PATH();
|
|
29
34
|
if (!fs.existsSync(configPath)) {
|
|
30
35
|
return DEFAULT_CONFIG;
|
|
31
36
|
}
|
|
@@ -40,12 +45,19 @@ function getConfig() {
|
|
|
40
45
|
return DEFAULT_CONFIG;
|
|
41
46
|
}
|
|
42
47
|
const config = parsed;
|
|
48
|
+
let database = config.local?.database ?? DEFAULT_CONFIG.local.database;
|
|
49
|
+
if (!config.local?.database && config.local?.sqlitePath) {
|
|
50
|
+
database = "sqlite";
|
|
51
|
+
}
|
|
43
52
|
return {
|
|
44
53
|
mode: config.mode ?? DEFAULT_CONFIG.mode,
|
|
45
54
|
remote: config.remote,
|
|
46
55
|
local: {
|
|
47
56
|
port: config.local?.port ?? DEFAULT_CONFIG.local.port,
|
|
48
|
-
|
|
57
|
+
database,
|
|
58
|
+
sqlitePath: config.local?.sqlitePath,
|
|
59
|
+
postgresUrl: config.local?.postgresUrl,
|
|
60
|
+
auth: config.local?.auth
|
|
49
61
|
}
|
|
50
62
|
};
|
|
51
63
|
}
|
|
@@ -56,6 +68,203 @@ function getRegistryUrl() {
|
|
|
56
68
|
}
|
|
57
69
|
return `http://localhost:${config.local?.port ?? 3750}`;
|
|
58
70
|
}
|
|
71
|
+
function isPostgresqlUrl(value) {
|
|
72
|
+
return value.startsWith("postgresql://") || value.startsWith("postgres://");
|
|
73
|
+
}
|
|
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, routeScopes) {
|
|
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 routeScopes) {
|
|
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
|
+
function parseRouteScopes(spec) {
|
|
169
|
+
const routes = [];
|
|
170
|
+
const paths = spec.paths;
|
|
171
|
+
if (!paths) return routes;
|
|
172
|
+
for (const [path12, operations] of Object.entries(paths)) {
|
|
173
|
+
for (const [method, operation] of Object.entries(operations)) {
|
|
174
|
+
if (typeof operation !== "object" || operation === null) continue;
|
|
175
|
+
const op = operation;
|
|
176
|
+
const operationId = op.operationId;
|
|
177
|
+
if (!operationId) continue;
|
|
178
|
+
const security = op.security;
|
|
179
|
+
const scopes = [];
|
|
180
|
+
if (security) {
|
|
181
|
+
for (const sec of security) {
|
|
182
|
+
for (const [name, required] of Object.entries(sec)) {
|
|
183
|
+
if (name === "keycloak" && Array.isArray(required)) {
|
|
184
|
+
scopes.push(...required);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
routes.push({
|
|
190
|
+
method: method.toUpperCase(),
|
|
191
|
+
path: path12.replace(/\{([^}]+)\}/g, ":$1"),
|
|
192
|
+
operationId,
|
|
193
|
+
scopes
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return routes;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/cli/auth.ts
|
|
201
|
+
var cachedToken = null;
|
|
202
|
+
function resetTokenCache() {
|
|
203
|
+
cachedToken = null;
|
|
204
|
+
}
|
|
205
|
+
async function getAccessToken(config) {
|
|
206
|
+
const staticToken = process.env.GRAPITY_TOKEN;
|
|
207
|
+
if (staticToken) {
|
|
208
|
+
return staticToken;
|
|
209
|
+
}
|
|
210
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
211
|
+
if (cachedToken && cachedToken.expiresAt > now + 60) {
|
|
212
|
+
return cachedToken.accessToken;
|
|
213
|
+
}
|
|
214
|
+
const clientSecret = process.env.GRAPITY_CLIENT_SECRET;
|
|
215
|
+
if (!clientSecret) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"Keycloak client secret not found. Set GRAPITY_CLIENT_SECRET or provide a static token via GRAPITY_TOKEN."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const { tokenUrl } = buildKeycloakUrls({ serverUrl: config.serverUrl, realm: config.realm });
|
|
221
|
+
const params = new URLSearchParams();
|
|
222
|
+
params.set("grant_type", "client_credentials");
|
|
223
|
+
params.set("client_id", config.clientId);
|
|
224
|
+
params.set("client_secret", clientSecret);
|
|
225
|
+
if (config.audience) {
|
|
226
|
+
params.set("audience", config.audience);
|
|
227
|
+
}
|
|
228
|
+
const response = await fetch(tokenUrl, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
231
|
+
body: params.toString()
|
|
232
|
+
});
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
const text3 = await response.text().catch(() => "Unknown error");
|
|
235
|
+
throw new Error(`Keycloak token request failed: ${response.status} ${text3}`);
|
|
236
|
+
}
|
|
237
|
+
const data = await response.json();
|
|
238
|
+
cachedToken = {
|
|
239
|
+
accessToken: data.access_token,
|
|
240
|
+
expiresAt: now + data.expires_in
|
|
241
|
+
};
|
|
242
|
+
return data.access_token;
|
|
243
|
+
}
|
|
244
|
+
function decodeToken(token) {
|
|
245
|
+
try {
|
|
246
|
+
const payload = decodeJwt(token);
|
|
247
|
+
return {
|
|
248
|
+
sub: payload.sub,
|
|
249
|
+
exp: payload.exp,
|
|
250
|
+
iss: payload.iss,
|
|
251
|
+
scope: typeof payload.scope === "string" ? payload.scope : void 0,
|
|
252
|
+
roles: payload.realm_access?.roles
|
|
253
|
+
};
|
|
254
|
+
} catch {
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function formatTokenStatus(token) {
|
|
259
|
+
const decoded = decodeToken(token);
|
|
260
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
261
|
+
return {
|
|
262
|
+
valid: !!decoded.sub,
|
|
263
|
+
sub: decoded.sub,
|
|
264
|
+
exp: decoded.exp,
|
|
265
|
+
expired: decoded.exp ? decoded.exp < now : void 0
|
|
266
|
+
};
|
|
267
|
+
}
|
|
59
268
|
|
|
60
269
|
// src/cli/client.ts
|
|
61
270
|
var BreakingChangeError = class extends Error {
|
|
@@ -66,19 +275,31 @@ var BreakingChangeError = class extends Error {
|
|
|
66
275
|
}
|
|
67
276
|
compatReport;
|
|
68
277
|
};
|
|
278
|
+
async function getAuthHeaders() {
|
|
279
|
+
const config = getConfig();
|
|
280
|
+
const authConfig = config.mode === "remote" ? config.remote?.auth : config.local?.auth;
|
|
281
|
+
const headers = {};
|
|
282
|
+
if (authConfig?.mode === "keycloak") {
|
|
283
|
+
if (!authConfig.clientId) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
"Keycloak auth is configured but clientId is missing. Run grapity init with --keycloak-client-id."
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const token = await getAccessToken(authConfig);
|
|
289
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
290
|
+
}
|
|
291
|
+
return headers;
|
|
292
|
+
}
|
|
69
293
|
async function request(method, path12, body) {
|
|
70
294
|
const baseUrl = getRegistryUrl();
|
|
71
295
|
const url = `${baseUrl}${path12}`;
|
|
72
|
-
const
|
|
73
|
-
const headers = {
|
|
74
|
-
"Content-Type": "application/json"
|
|
75
|
-
};
|
|
76
|
-
if (config.mode === "remote" && config.remote?.apiKey) {
|
|
77
|
-
headers["X-API-Key"] = config.remote.apiKey;
|
|
78
|
-
}
|
|
296
|
+
const authHeaders = await getAuthHeaders();
|
|
79
297
|
const response = await fetch(url, {
|
|
80
298
|
method,
|
|
81
|
-
headers
|
|
299
|
+
headers: {
|
|
300
|
+
"Content-Type": "application/json",
|
|
301
|
+
...authHeaders
|
|
302
|
+
},
|
|
82
303
|
body: body ? JSON.stringify(body) : void 0
|
|
83
304
|
});
|
|
84
305
|
if (!response.ok) {
|
|
@@ -88,18 +309,17 @@ async function request(method, path12, body) {
|
|
|
88
309
|
}
|
|
89
310
|
throw new Error(error.message ?? `Request failed: ${response.status}`);
|
|
90
311
|
}
|
|
91
|
-
const
|
|
92
|
-
return
|
|
312
|
+
const text3 = await response.text();
|
|
313
|
+
return text3 ? JSON.parse(text3) : void 0;
|
|
93
314
|
}
|
|
94
315
|
async function requestText(method, path12) {
|
|
95
316
|
const baseUrl = getRegistryUrl();
|
|
96
317
|
const url = `${baseUrl}${path12}`;
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
headers
|
|
101
|
-
}
|
|
102
|
-
const response = await fetch(url, { method, headers });
|
|
318
|
+
const authHeaders = await getAuthHeaders();
|
|
319
|
+
const response = await fetch(url, {
|
|
320
|
+
method,
|
|
321
|
+
headers: authHeaders
|
|
322
|
+
});
|
|
103
323
|
if (!response.ok) {
|
|
104
324
|
const error = await response.json();
|
|
105
325
|
throw new Error(error.message ?? `Request failed: ${response.status}`);
|
|
@@ -150,9 +370,9 @@ var client = {
|
|
|
150
370
|
fetchSpec: async (name, options) => {
|
|
151
371
|
const format = options.format ?? "yaml";
|
|
152
372
|
const path12 = options.semver ? `/v1/specs/${name}/versions/${options.semver}/spec.${format}` : `/v1/specs/${name}/spec.${format}`;
|
|
153
|
-
const { text:
|
|
373
|
+
const { text: text3, headers } = await requestText("GET", path12);
|
|
154
374
|
return {
|
|
155
|
-
content:
|
|
375
|
+
content: text3,
|
|
156
376
|
resolvedVersion: headers.get("Grapity-Resolved-Version") ?? void 0
|
|
157
377
|
};
|
|
158
378
|
},
|
|
@@ -531,27 +751,65 @@ function formatInitSuccess(params) {
|
|
|
531
751
|
];
|
|
532
752
|
if (params.mode === "local") {
|
|
533
753
|
if (params.port) lines.push(` ${c.label("Port")} ${c.cyan(String(params.port))}`);
|
|
534
|
-
if (params.
|
|
535
|
-
lines.push("");
|
|
536
|
-
lines.push(` ${c.
|
|
754
|
+
if (params.database) lines.push(` ${c.label("Database")} ${c.primary(params.database)}`);
|
|
755
|
+
if (params.dbPath) lines.push(` ${c.label("Path")} ${c.dim(params.dbPath)}`);
|
|
756
|
+
if (params.postgresUrl) lines.push(` ${c.label("PostgreSQL")} ${c.dim(params.postgresUrl)}`);
|
|
537
757
|
} else {
|
|
538
758
|
if (params.url) lines.push(` ${c.label("URL")} ${c.cyan(params.url)}`);
|
|
539
|
-
|
|
759
|
+
}
|
|
760
|
+
if (params.authMode && params.authMode !== "none") {
|
|
761
|
+
lines.push("");
|
|
762
|
+
lines.push(` ${c.label("Auth")} ${c.primary(params.authMode)}`);
|
|
763
|
+
if (params.keycloakServer) lines.push(` ${c.label("Keycloak")} ${c.dim(params.keycloakServer)}`);
|
|
764
|
+
if (params.keycloakRealm) lines.push(` ${c.label("Realm")} ${c.dim(params.keycloakRealm)}`);
|
|
765
|
+
if (params.keycloakClientId) lines.push(` ${c.label("Client ID")} ${c.dim(params.keycloakClientId)}`);
|
|
540
766
|
lines.push("");
|
|
541
|
-
lines.push(` ${c.dim("\u203A")}
|
|
767
|
+
lines.push(` ${c.dim("\u203A")} Set the client secret in GRAPITY_CLIENT_SECRET before running commands.`);
|
|
768
|
+
} else {
|
|
769
|
+
lines.push("");
|
|
770
|
+
if (params.mode === "local") {
|
|
771
|
+
lines.push(` ${c.dim("\u203A")} Start the server with: ${c.primary("grapity serve")}`);
|
|
772
|
+
} else {
|
|
773
|
+
lines.push(` ${c.dim("\u203A")} Push a spec with: ${c.primary("grapity registry push ./openapi.yaml --name my-api")}`);
|
|
774
|
+
}
|
|
542
775
|
}
|
|
543
776
|
return lines.join("\n");
|
|
544
777
|
}
|
|
545
778
|
function formatServeConfig(params) {
|
|
546
|
-
const modeLabel = `local ${c.label("\xB7")} ${params.mode}`;
|
|
547
779
|
const lines = [
|
|
548
|
-
` ${c.label("Mode")} ${c.primary(
|
|
549
|
-
` ${c.label("Port")} ${c.cyan(String(params.port))}
|
|
780
|
+
` ${c.label("Mode")} ${c.primary("local")}`,
|
|
781
|
+
` ${c.label("Port")} ${c.cyan(String(params.port))}`,
|
|
782
|
+
` ${c.label("Database")} ${c.primary(params.database)}`
|
|
550
783
|
];
|
|
551
|
-
if (params.
|
|
552
|
-
|
|
784
|
+
if (params.database === "sqlite" && params.dbPath) {
|
|
785
|
+
lines.push(` ${c.label("Path")} ${c.dim(params.dbPath)}`);
|
|
786
|
+
}
|
|
787
|
+
if (params.database === "postgresql" && params.postgresUrl) {
|
|
788
|
+
lines.push(` ${c.label("PostgreSQL")} ${c.dim(maskPostgresUrl(params.postgresUrl))}`);
|
|
789
|
+
}
|
|
790
|
+
lines.push(` ${c.label("Auth")} ${c.primary(params.authMode ?? "none")}`);
|
|
791
|
+
if (params.keycloakServer) {
|
|
792
|
+
lines.push(` ${c.label("Keycloak")} ${c.dim(params.keycloakServer)}`);
|
|
793
|
+
}
|
|
794
|
+
if (params.keycloakRealm) {
|
|
795
|
+
lines.push(` ${c.label("Realm")} ${c.dim(params.keycloakRealm)}`);
|
|
796
|
+
}
|
|
797
|
+
if (params.keycloakAudience) {
|
|
798
|
+
lines.push(` ${c.label("Audience")} ${c.dim(params.keycloakAudience)}`);
|
|
799
|
+
}
|
|
553
800
|
return lines.join("\n");
|
|
554
801
|
}
|
|
802
|
+
function maskPostgresUrl(url) {
|
|
803
|
+
try {
|
|
804
|
+
const parsed = new URL(url);
|
|
805
|
+
if (parsed.password) {
|
|
806
|
+
parsed.password = "***";
|
|
807
|
+
}
|
|
808
|
+
return parsed.toString();
|
|
809
|
+
} catch {
|
|
810
|
+
return url;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
555
813
|
function formatHubConfig(params) {
|
|
556
814
|
const lines = [
|
|
557
815
|
` ${c.label("Port")} ${c.cyan(String(params.port))}`,
|
|
@@ -582,6 +840,39 @@ function formatEmptyState(message, hints) {
|
|
|
582
840
|
function formatReady(port) {
|
|
583
841
|
return ` ${c.success("\u25CF")} Server ready ${c.label("\xB7")} ${c.cyan(`http://localhost:${port}`)}`;
|
|
584
842
|
}
|
|
843
|
+
function formatAuthStatus(params) {
|
|
844
|
+
const lines = [];
|
|
845
|
+
if (params.cleared) {
|
|
846
|
+
lines.push(` ${c.success("\u2713")} Cached access token cleared`);
|
|
847
|
+
return lines.join("\n");
|
|
848
|
+
}
|
|
849
|
+
lines.push(` ${c.label("Mode")} ${c.primary(params.mode)}`);
|
|
850
|
+
if (!params.configured) {
|
|
851
|
+
lines.push(` ${c.dim("\xB7")} No remote authentication configured`);
|
|
852
|
+
return lines.join("\n");
|
|
853
|
+
}
|
|
854
|
+
if (params.serverUrl) lines.push(` ${c.label("Server")} ${c.dim(params.serverUrl)}`);
|
|
855
|
+
if (params.realm) lines.push(` ${c.label("Realm")} ${c.dim(params.realm)}`);
|
|
856
|
+
if (params.clientId) lines.push(` ${c.label("Client ID")} ${c.dim(params.clientId)}`);
|
|
857
|
+
if (params.audience) lines.push(` ${c.label("Audience")} ${c.dim(params.audience)}`);
|
|
858
|
+
lines.push("");
|
|
859
|
+
if (params.valid === false) {
|
|
860
|
+
lines.push(` ${c.error("\u2717")} Token is not a valid JWT`);
|
|
861
|
+
} else if (params.sub) {
|
|
862
|
+
lines.push(` ${c.success("\u2713")} Authenticated as ${c.primary(params.sub)}`);
|
|
863
|
+
if (params.exp) {
|
|
864
|
+
const date = new Date(params.exp * 1e3).toISOString();
|
|
865
|
+
if (params.expired) {
|
|
866
|
+
lines.push(` ${c.error("\u2717")} Token expired at ${c.dim(date)}`);
|
|
867
|
+
} else {
|
|
868
|
+
lines.push(` ${c.success("\u2713")} Token valid until ${c.dim(date)}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
lines.push(` ${c.dim("\xB7")} No token fetched yet`);
|
|
873
|
+
}
|
|
874
|
+
return lines.join("\n");
|
|
875
|
+
}
|
|
585
876
|
function formatHubReady(port) {
|
|
586
877
|
return ` ${c.success("\u25CF")} Hub ready ${c.label("\xB7")} ${c.cyan(`http://localhost:${port}`)}`;
|
|
587
878
|
}
|
|
@@ -744,18 +1035,18 @@ var validateCommand = new Command2("validate").description("Validate a spec agai
|
|
|
744
1035
|
import { Command as Command3 } from "commander";
|
|
745
1036
|
var listCommand = new Command3("list").description("List all specs in the registry").option("--type <type>", "Filter by spec type").option("--owner <owner>", "Filter by owner").option("--tags <tags>", "Filter by tags (comma-separated)").action(async (options) => {
|
|
746
1037
|
try {
|
|
747
|
-
const
|
|
1038
|
+
const specs3 = await client.listSpecs({
|
|
748
1039
|
type: options.type,
|
|
749
1040
|
owner: options.owner,
|
|
750
1041
|
tags: options.tags?.split(",")
|
|
751
1042
|
});
|
|
752
|
-
if (
|
|
1043
|
+
if (specs3.length === 0) {
|
|
753
1044
|
console.log(formatEmptyState("No specs in the registry.", [
|
|
754
1045
|
"Push one with: grapity registry push ./openapi.yaml --name my-api"
|
|
755
1046
|
]));
|
|
756
1047
|
return;
|
|
757
1048
|
}
|
|
758
|
-
for (const spec of
|
|
1049
|
+
for (const spec of specs3) {
|
|
759
1050
|
console.log(formatSpec(spec));
|
|
760
1051
|
}
|
|
761
1052
|
} catch (err) {
|
|
@@ -1145,10 +1436,10 @@ function convertPathToKong(path12) {
|
|
|
1145
1436
|
}
|
|
1146
1437
|
return converted;
|
|
1147
1438
|
}
|
|
1148
|
-
function routeName(serviceName,
|
|
1439
|
+
function routeName(serviceName, index3, path12, methods) {
|
|
1149
1440
|
const cleanPath = path12.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1150
1441
|
const methodList = methods.join("-").toLowerCase();
|
|
1151
|
-
return `${serviceName}-${cleanPath}-${methodList}-${
|
|
1442
|
+
return `${serviceName}-${cleanPath}-${methodList}-${index3}`;
|
|
1152
1443
|
}
|
|
1153
1444
|
function generateDeckYaml(config, envName) {
|
|
1154
1445
|
const env = config.environments[envName];
|
|
@@ -1380,7 +1671,7 @@ import fs8 from "fs";
|
|
|
1380
1671
|
import os3 from "os";
|
|
1381
1672
|
import path8 from "path";
|
|
1382
1673
|
import yaml4 from "js-yaml";
|
|
1383
|
-
var initCommand = new Command18("init").description("Configure grapity registry (local or remote mode)").option("--local", "Use local mode (SQLite)").option("--remote", "Use remote mode (connect to a grapity server)").option("--url <url>", "Registry URL (for remote mode)").option("--
|
|
1674
|
+
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) => {
|
|
1384
1675
|
const configDir = path8.join(os3.homedir(), ".grapity");
|
|
1385
1676
|
const configPath = path8.join(configDir, "config.yaml");
|
|
1386
1677
|
let mode;
|
|
@@ -1398,21 +1689,47 @@ var initCommand = new Command18("init").description("Configure grapity registry
|
|
|
1398
1689
|
"missing flag",
|
|
1399
1690
|
"Select registry mode: use --local or --remote.",
|
|
1400
1691
|
[
|
|
1401
|
-
"--local Run a registry server on this machine
|
|
1692
|
+
"--local Run a registry server on this machine",
|
|
1402
1693
|
"--remote Connect to an existing grapity server"
|
|
1403
1694
|
]
|
|
1404
1695
|
)
|
|
1405
1696
|
);
|
|
1406
1697
|
process.exit(1);
|
|
1407
1698
|
}
|
|
1699
|
+
const authMode = options.auth ?? "none";
|
|
1700
|
+
if (authMode !== "none" && authMode !== "keycloak") {
|
|
1701
|
+
console.error(formatError("invalid auth mode", "--auth must be one of: none, keycloak"));
|
|
1702
|
+
process.exit(1);
|
|
1703
|
+
}
|
|
1704
|
+
const keycloakAuth = authMode === "keycloak" ? parseKeycloakOptions(options, mode) : void 0;
|
|
1408
1705
|
const config = { mode };
|
|
1409
1706
|
if (mode === "local") {
|
|
1707
|
+
const dbValue = options.db ?? process.env.GRAPITY_DATABASE_URL;
|
|
1708
|
+
const database = dbValue && isPostgresqlUrl(dbValue) ? "postgresql" : "sqlite";
|
|
1410
1709
|
config.local = {
|
|
1411
1710
|
port: options.port ? parseInt(options.port, 10) : 3750,
|
|
1412
|
-
|
|
1711
|
+
database
|
|
1413
1712
|
};
|
|
1414
|
-
if (
|
|
1415
|
-
config.local.
|
|
1713
|
+
if (database === "postgresql") {
|
|
1714
|
+
config.local.postgresUrl = dbValue;
|
|
1715
|
+
} else {
|
|
1716
|
+
config.local.sqlitePath = dbValue ?? path8.join(os3.homedir(), ".grapity", "registry.db");
|
|
1717
|
+
}
|
|
1718
|
+
if (keycloakAuth) {
|
|
1719
|
+
if (!options.keycloakClientId) {
|
|
1720
|
+
console.error(
|
|
1721
|
+
formatError(
|
|
1722
|
+
"missing flag",
|
|
1723
|
+
"--keycloak-client-id is required when using Keycloak auth."
|
|
1724
|
+
)
|
|
1725
|
+
);
|
|
1726
|
+
process.exit(1);
|
|
1727
|
+
}
|
|
1728
|
+
config.local.auth = {
|
|
1729
|
+
mode: "keycloak",
|
|
1730
|
+
clientId: options.keycloakClientId,
|
|
1731
|
+
...keycloakAuth
|
|
1732
|
+
};
|
|
1416
1733
|
}
|
|
1417
1734
|
} else {
|
|
1418
1735
|
if (!options.url) {
|
|
@@ -1426,9 +1743,18 @@ var initCommand = new Command18("init").description("Configure grapity registry
|
|
|
1426
1743
|
process.exit(1);
|
|
1427
1744
|
}
|
|
1428
1745
|
config.remote = {
|
|
1429
|
-
url: options.url.replace(/\/$/, "")
|
|
1430
|
-
apiKey: options.apiKey
|
|
1746
|
+
url: options.url.replace(/\/$/, "")
|
|
1431
1747
|
};
|
|
1748
|
+
if (keycloakAuth) {
|
|
1749
|
+
config.remote.auth = {
|
|
1750
|
+
mode: "keycloak",
|
|
1751
|
+
serverUrl: keycloakAuth.serverUrl,
|
|
1752
|
+
realm: keycloakAuth.realm,
|
|
1753
|
+
clientId: keycloakAuth.clientId,
|
|
1754
|
+
audience: keycloakAuth.audience,
|
|
1755
|
+
roleSource: keycloakAuth.roleSource
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1432
1758
|
}
|
|
1433
1759
|
if (!fs8.existsSync(configDir)) {
|
|
1434
1760
|
fs8.mkdirSync(configDir, { recursive: true });
|
|
@@ -1440,18 +1766,110 @@ var initCommand = new Command18("init").description("Configure grapity registry
|
|
|
1440
1766
|
configPath,
|
|
1441
1767
|
mode,
|
|
1442
1768
|
port: config.local?.port,
|
|
1769
|
+
database: config.local?.database,
|
|
1443
1770
|
dbPath: config.local?.sqlitePath,
|
|
1771
|
+
postgresUrl: config.local?.postgresUrl,
|
|
1444
1772
|
url: config.remote?.url,
|
|
1445
|
-
|
|
1773
|
+
authMode,
|
|
1774
|
+
keycloakServer: keycloakAuth?.serverUrl,
|
|
1775
|
+
keycloakRealm: keycloakAuth?.realm,
|
|
1776
|
+
keycloakClientId: config.local?.auth?.clientId ?? config.remote?.auth?.clientId
|
|
1446
1777
|
})
|
|
1447
1778
|
);
|
|
1448
1779
|
});
|
|
1780
|
+
function parseKeycloakOptions(options, _mode) {
|
|
1781
|
+
if (!options.keycloakServer) {
|
|
1782
|
+
console.error(
|
|
1783
|
+
formatError(
|
|
1784
|
+
"missing flag",
|
|
1785
|
+
"--keycloak-server is required when auth mode is keycloak.",
|
|
1786
|
+
["Example: --keycloak-server https://keycloak.example.com"]
|
|
1787
|
+
)
|
|
1788
|
+
);
|
|
1789
|
+
process.exit(1);
|
|
1790
|
+
}
|
|
1791
|
+
if (!options.keycloakRealm) {
|
|
1792
|
+
console.error(
|
|
1793
|
+
formatError(
|
|
1794
|
+
"missing flag",
|
|
1795
|
+
"--keycloak-realm is required when auth mode is keycloak.",
|
|
1796
|
+
["Example: --keycloak-realm grapity"]
|
|
1797
|
+
)
|
|
1798
|
+
);
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
const roleSource = options.keycloakRoleSource;
|
|
1802
|
+
if (roleSource && roleSource !== "scope" && roleSource !== "realm_access.roles") {
|
|
1803
|
+
console.error(formatError("invalid role source", "--keycloak-role-source must be one of: scope, realm_access.roles"));
|
|
1804
|
+
process.exit(1);
|
|
1805
|
+
}
|
|
1806
|
+
return {
|
|
1807
|
+
serverUrl: options.keycloakServer.replace(/\/$/, ""),
|
|
1808
|
+
realm: options.keycloakRealm,
|
|
1809
|
+
clientId: options.keycloakClientId,
|
|
1810
|
+
audience: options.keycloakAudience,
|
|
1811
|
+
roleSource
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1449
1814
|
|
|
1450
|
-
// src/cli/commands/
|
|
1815
|
+
// src/cli/commands/auth.ts
|
|
1451
1816
|
import { Command as Command19 } from "commander";
|
|
1817
|
+
var authCommand = new Command19("auth").description("Manage authentication for remote registry mode").addCommand(
|
|
1818
|
+
new Command19("status").description("Show current authentication status").action(async () => {
|
|
1819
|
+
const config = getConfig();
|
|
1820
|
+
const auth = config.mode === "remote" ? config.remote?.auth : config.local?.auth;
|
|
1821
|
+
if (!auth) {
|
|
1822
|
+
console.log(formatAuthStatus({ mode: "none", configured: false }));
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (auth.mode !== "keycloak") {
|
|
1826
|
+
console.log(formatAuthStatus({ mode: auth.mode, configured: true }));
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (!auth.clientId) {
|
|
1830
|
+
console.error(
|
|
1831
|
+
formatError(
|
|
1832
|
+
"auth misconfigured",
|
|
1833
|
+
"Keycloak auth is configured but clientId is missing. Run grapity init with --keycloak-client-id."
|
|
1834
|
+
)
|
|
1835
|
+
);
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
}
|
|
1838
|
+
try {
|
|
1839
|
+
const token = await getAccessToken(auth);
|
|
1840
|
+
const status = formatTokenStatus(token);
|
|
1841
|
+
console.log(
|
|
1842
|
+
formatAuthStatus({
|
|
1843
|
+
mode: auth.mode,
|
|
1844
|
+
configured: true,
|
|
1845
|
+
serverUrl: auth.serverUrl,
|
|
1846
|
+
realm: auth.realm,
|
|
1847
|
+
clientId: auth.clientId,
|
|
1848
|
+
audience: auth.audience,
|
|
1849
|
+
...status
|
|
1850
|
+
})
|
|
1851
|
+
);
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
console.error(
|
|
1854
|
+
formatError(
|
|
1855
|
+
"auth failed",
|
|
1856
|
+
err instanceof Error ? err.message : "Unable to fetch access token"
|
|
1857
|
+
)
|
|
1858
|
+
);
|
|
1859
|
+
process.exit(1);
|
|
1860
|
+
}
|
|
1861
|
+
})
|
|
1862
|
+
).addCommand(
|
|
1863
|
+
new Command19("clear").description("Clear the cached access token").action(() => {
|
|
1864
|
+
resetTokenCache();
|
|
1865
|
+
console.log(formatAuthStatus({ mode: "none", configured: false, cleared: true }));
|
|
1866
|
+
})
|
|
1867
|
+
);
|
|
1868
|
+
|
|
1869
|
+
// src/cli/commands/serve.ts
|
|
1870
|
+
import { Command as Command20 } from "commander";
|
|
1452
1871
|
import os4 from "os";
|
|
1453
1872
|
import path11 from "path";
|
|
1454
|
-
import { readFileSync } from "fs";
|
|
1455
1873
|
|
|
1456
1874
|
// src/registry/serve.ts
|
|
1457
1875
|
import path9 from "path";
|
|
@@ -3090,9 +3508,9 @@ var RegistryService = class {
|
|
|
3090
3508
|
return { spec, version: version2, compatReport, isNewSpec };
|
|
3091
3509
|
}
|
|
3092
3510
|
async listSpecs(filters) {
|
|
3093
|
-
const
|
|
3511
|
+
const specs3 = await this.store.listSpecs(filters);
|
|
3094
3512
|
return Promise.all(
|
|
3095
|
-
|
|
3513
|
+
specs3.map(async (spec) => {
|
|
3096
3514
|
const latestVersion = await this.store.getLatestVersion(spec.name);
|
|
3097
3515
|
return { ...spec, latestVersion: latestVersion ?? void 0 };
|
|
3098
3516
|
})
|
|
@@ -3211,6 +3629,7 @@ var pushRoute = new Hono().post("/", async (c2) => {
|
|
|
3211
3629
|
}
|
|
3212
3630
|
const store = c2.get("store");
|
|
3213
3631
|
const service = new RegistryService(store);
|
|
3632
|
+
const actor = c2.get("actor") ?? body.pushedBy;
|
|
3214
3633
|
try {
|
|
3215
3634
|
const result = await service.pushSpec(body.content, body.name, {
|
|
3216
3635
|
type: body.type,
|
|
@@ -3219,7 +3638,7 @@ var pushRoute = new Hono().post("/", async (c2) => {
|
|
|
3219
3638
|
sourceRepo: body.sourceRepo,
|
|
3220
3639
|
tags: Array.isArray(body.tags) ? body.tags : void 0,
|
|
3221
3640
|
gitRef: body.gitRef,
|
|
3222
|
-
pushedBy:
|
|
3641
|
+
pushedBy: actor,
|
|
3223
3642
|
prerelease: body.prerelease,
|
|
3224
3643
|
force: body.force,
|
|
3225
3644
|
reason: body.reason
|
|
@@ -3350,8 +3769,8 @@ var listRoute = new Hono3().get("/", async (c2) => {
|
|
|
3350
3769
|
const type = c2.req.query("type");
|
|
3351
3770
|
const owner = c2.req.query("owner");
|
|
3352
3771
|
const tags = c2.req.query("tags")?.split(",");
|
|
3353
|
-
const
|
|
3354
|
-
return c2.json({ data:
|
|
3772
|
+
const specs3 = await service.listSpecs({ type, owner, tags });
|
|
3773
|
+
return c2.json({ data: specs3 });
|
|
3355
3774
|
});
|
|
3356
3775
|
|
|
3357
3776
|
// src/registry/routes/get-spec.ts
|
|
@@ -3384,7 +3803,8 @@ var deleteSpecRoute = new Hono5().delete(
|
|
|
3384
3803
|
const name = c2.req.param("name");
|
|
3385
3804
|
const store = c2.get("store");
|
|
3386
3805
|
const service = new RegistryService(store);
|
|
3387
|
-
const
|
|
3806
|
+
const actor = c2.get("actor");
|
|
3807
|
+
const deleted = await service.deleteSpec(name, actor);
|
|
3388
3808
|
if (!deleted) {
|
|
3389
3809
|
return c2.json({ error: "not_found", message: `Spec "${name}" not found`, statusCode: 404 }, 404);
|
|
3390
3810
|
}
|
|
@@ -3985,8 +4405,7 @@ function switchTab(tab) {
|
|
|
3985
4405
|
}
|
|
3986
4406
|
var welcomeRoute = new Hono12().get("/", (c2) => {
|
|
3987
4407
|
const config = c2.get("config");
|
|
3988
|
-
|
|
3989
|
-
return c2.html(buildPage(config.port, mode));
|
|
4408
|
+
return c2.html(buildPage(config.port, "local"));
|
|
3990
4409
|
});
|
|
3991
4410
|
|
|
3992
4411
|
// src/registry/routes/push-gateway-config.ts
|
|
@@ -4126,6 +4545,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c2) => {
|
|
|
4126
4545
|
}
|
|
4127
4546
|
const store = c2.get("store");
|
|
4128
4547
|
const service = new GatewayService(store, store);
|
|
4548
|
+
const actor = c2.get("actor") ?? body.pushedBy;
|
|
4129
4549
|
try {
|
|
4130
4550
|
const result = await service.pushGatewayConfig({
|
|
4131
4551
|
name: body.name,
|
|
@@ -4136,7 +4556,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c2) => {
|
|
|
4136
4556
|
environments: body.environments ?? {},
|
|
4137
4557
|
callerIdentification: body.callerIdentification,
|
|
4138
4558
|
content: body.content,
|
|
4139
|
-
pushedBy:
|
|
4559
|
+
pushedBy: actor
|
|
4140
4560
|
});
|
|
4141
4561
|
return c2.json({ data: result }, 201);
|
|
4142
4562
|
} catch (err) {
|
|
@@ -4327,7 +4747,6 @@ var ingestGatewayLogRoute = new Hono18().post("/ingest/:provider/:environment",
|
|
|
4327
4747
|
await service.ingestLog(provider, environment, payload);
|
|
4328
4748
|
return c2.json({ status: "ok" }, 201);
|
|
4329
4749
|
} catch (err) {
|
|
4330
|
-
console.error("Gateway log ingest error:", err);
|
|
4331
4750
|
return c2.json({
|
|
4332
4751
|
error: "bad_request",
|
|
4333
4752
|
message: err instanceof Error ? err.message : "Invalid log payload",
|
|
@@ -4403,6 +4822,14 @@ var gatewayLogStatsRoute = new Hono21().get("/stats", async (c2) => {
|
|
|
4403
4822
|
});
|
|
4404
4823
|
|
|
4405
4824
|
// src/registry/server.ts
|
|
4825
|
+
import { readFileSync } from "fs";
|
|
4826
|
+
import { fileURLToPath } from "url";
|
|
4827
|
+
import yaml10 from "js-yaml";
|
|
4828
|
+
function loadOpenApiSpec() {
|
|
4829
|
+
const path12 = fileURLToPath(new URL("../../openapi.yaml", import.meta.url));
|
|
4830
|
+
const content = readFileSync(path12, "utf-8");
|
|
4831
|
+
return yaml10.load(content);
|
|
4832
|
+
}
|
|
4406
4833
|
function createApp(config, store) {
|
|
4407
4834
|
const app = new Hono22();
|
|
4408
4835
|
app.use("*", logger());
|
|
@@ -4413,6 +4840,21 @@ function createApp(config, store) {
|
|
|
4413
4840
|
c2.set("config", config);
|
|
4414
4841
|
await next();
|
|
4415
4842
|
});
|
|
4843
|
+
const routeScopes = parseRouteScopes(loadOpenApiSpec());
|
|
4844
|
+
app.use("*", createAuthMiddleware(config, routeScopes));
|
|
4845
|
+
app.onError((err, c2) => {
|
|
4846
|
+
if (err instanceof AuthError) {
|
|
4847
|
+
return c2.json(
|
|
4848
|
+
{ error: err.code, message: err.message, statusCode: err.statusCode },
|
|
4849
|
+
err.statusCode
|
|
4850
|
+
);
|
|
4851
|
+
}
|
|
4852
|
+
console.error("Unhandled error:", err);
|
|
4853
|
+
return c2.json(
|
|
4854
|
+
{ error: "internal_error", message: "Internal server error", statusCode: 500 },
|
|
4855
|
+
500
|
|
4856
|
+
);
|
|
4857
|
+
});
|
|
4416
4858
|
app.route("/v1/specs", pushRoute);
|
|
4417
4859
|
app.route("/v1/specs", validateRoute);
|
|
4418
4860
|
app.route("/v1/specs", listRoute);
|
|
@@ -4441,8 +4883,7 @@ function createApp(config, store) {
|
|
|
4441
4883
|
var defaultConfig = {
|
|
4442
4884
|
port: 3750,
|
|
4443
4885
|
database: "sqlite",
|
|
4444
|
-
|
|
4445
|
-
gracePeriodDays: 30
|
|
4886
|
+
auth: { mode: "none" }
|
|
4446
4887
|
};
|
|
4447
4888
|
|
|
4448
4889
|
// src/registry/storage/sqlite.ts
|
|
@@ -4948,137 +5389,829 @@ var SQLiteSpecStore = class {
|
|
|
4948
5389
|
}
|
|
4949
5390
|
};
|
|
4950
5391
|
|
|
4951
|
-
// src/registry/
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
config.sqlitePath = path9.join(homeDir, ".grapity", "registry.db");
|
|
4957
|
-
}
|
|
4958
|
-
if (config.database === "sqlite" && config.sqlitePath) {
|
|
4959
|
-
const dir = path9.dirname(config.sqlitePath);
|
|
4960
|
-
if (!fs9.existsSync(dir)) {
|
|
4961
|
-
fs9.mkdirSync(dir, { recursive: true });
|
|
4962
|
-
}
|
|
4963
|
-
}
|
|
4964
|
-
const store = new SQLiteSpecStore(config.sqlitePath);
|
|
4965
|
-
await store.migrate();
|
|
4966
|
-
const app = createApp(config, store);
|
|
4967
|
-
serve({
|
|
4968
|
-
fetch: app.fetch,
|
|
4969
|
-
port: config.port
|
|
4970
|
-
});
|
|
4971
|
-
return app;
|
|
4972
|
-
}
|
|
4973
|
-
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
|
4974
|
-
startServer();
|
|
4975
|
-
}
|
|
4976
|
-
|
|
4977
|
-
// src/hub/serve.ts
|
|
4978
|
-
import { Hono as Hono23 } from "hono";
|
|
4979
|
-
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4980
|
-
import { serve as serve2 } from "@hono/node-server";
|
|
4981
|
-
import path10 from "path";
|
|
4982
|
-
import fs10 from "fs";
|
|
5392
|
+
// src/registry/storage/postgresql.ts
|
|
5393
|
+
import { Pool } from "pg";
|
|
5394
|
+
import { drizzle as drizzle2 } from "drizzle-orm/node-postgres";
|
|
5395
|
+
import { migrate as migrate2 } from "drizzle-orm/node-postgres/migrator";
|
|
5396
|
+
import { eq as eq2, and as and2, desc as desc2, sql as sql2 } from "drizzle-orm";
|
|
4983
5397
|
|
|
4984
|
-
// src/
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
).
|
|
5398
|
+
// src/registry/storage/schema-pg.ts
|
|
5399
|
+
import { pgTable, text as text2, timestamp, boolean, jsonb, index as index2, integer as integer2 } from "drizzle-orm/pg-core";
|
|
5400
|
+
var specs2 = pgTable("specs", {
|
|
5401
|
+
id: text2("id").primaryKey(),
|
|
5402
|
+
name: text2("name").notNull().unique(),
|
|
5403
|
+
type: text2("type", { enum: ["openapi", "asyncapi"] }).notNull(),
|
|
5404
|
+
description: text2("description"),
|
|
5405
|
+
owner: text2("owner"),
|
|
5406
|
+
sourceRepo: text2("source_repo"),
|
|
5407
|
+
tags: jsonb("tags").$type().default([]),
|
|
5408
|
+
createdAt: timestamp("created_at").notNull(),
|
|
5409
|
+
updatedAt: timestamp("updated_at").notNull()
|
|
5410
|
+
});
|
|
5411
|
+
var specVersions2 = pgTable("spec_versions", {
|
|
5412
|
+
id: text2("id").primaryKey(),
|
|
5413
|
+
specId: text2("spec_id").notNull().references(() => specs2.id),
|
|
5414
|
+
semver: text2("semver").notNull(),
|
|
5415
|
+
content: text2("content").notNull(),
|
|
5416
|
+
checksum: text2("checksum").notNull(),
|
|
5417
|
+
gitRef: text2("git_ref"),
|
|
5418
|
+
pushedBy: text2("pushed_by"),
|
|
5419
|
+
compatibility: jsonb("compatibility").$type(),
|
|
5420
|
+
previousVersion: text2("previous_version"),
|
|
5421
|
+
forceReason: text2("force_reason"),
|
|
5422
|
+
isPrerelease: boolean("is_prerelease").notNull().default(false),
|
|
5423
|
+
createdAt: timestamp("created_at").notNull()
|
|
5424
|
+
}, (table) => [
|
|
5425
|
+
index2("idx_spec_versions_spec_id").on(table.specId),
|
|
5426
|
+
index2("idx_spec_versions_semver").on(table.specId, table.semver)
|
|
5427
|
+
]);
|
|
5428
|
+
var auditLog2 = pgTable("audit_log", {
|
|
5429
|
+
id: text2("id").primaryKey(),
|
|
5430
|
+
action: text2("action", { enum: ["spec.push", "spec.push.force", "spec.delete"] }).notNull(),
|
|
5431
|
+
actor: text2("actor").notNull(),
|
|
5432
|
+
specName: text2("spec_name").notNull(),
|
|
5433
|
+
version: text2("version"),
|
|
5434
|
+
details: jsonb("details").$type(),
|
|
5435
|
+
createdAt: timestamp("created_at").notNull()
|
|
5436
|
+
}, (table) => [
|
|
5437
|
+
index2("idx_audit_log_spec_name").on(table.specName),
|
|
5438
|
+
index2("idx_audit_log_created_at").on(table.createdAt)
|
|
5439
|
+
]);
|
|
5440
|
+
var gatewayConfigs2 = pgTable("gateway_configs", {
|
|
5441
|
+
id: text2("id").primaryKey(),
|
|
5442
|
+
name: text2("name").notNull().unique(),
|
|
5443
|
+
provider: text2("provider", { enum: ["kong"] }).notNull(),
|
|
5444
|
+
specName: text2("spec_name").notNull(),
|
|
5445
|
+
specSemver: text2("spec_semver").notNull(),
|
|
5446
|
+
createdAt: timestamp("created_at").notNull(),
|
|
5447
|
+
updatedAt: timestamp("updated_at").notNull()
|
|
5448
|
+
});
|
|
5449
|
+
var gatewayConfigVersions2 = pgTable("gateway_config_versions", {
|
|
5450
|
+
id: text2("id").primaryKey(),
|
|
5451
|
+
gatewayConfigId: text2("gateway_config_id").notNull().references(() => gatewayConfigs2.id),
|
|
5452
|
+
routes: jsonb("routes").$type().notNull(),
|
|
5453
|
+
environments: jsonb("environments").$type().notNull(),
|
|
5454
|
+
callerIdentification: jsonb("caller_identification").$type(),
|
|
5455
|
+
content: text2("content").notNull(),
|
|
5456
|
+
checksum: text2("checksum").notNull(),
|
|
5457
|
+
pushedBy: text2("pushed_by"),
|
|
5458
|
+
createdAt: timestamp("created_at").notNull()
|
|
5459
|
+
}, (table) => [
|
|
5460
|
+
index2("idx_gateway_config_versions_config_id").on(table.gatewayConfigId)
|
|
5461
|
+
]);
|
|
5462
|
+
var httpLogs2 = pgTable("http_logs", {
|
|
5463
|
+
id: text2("id").primaryKey(),
|
|
5464
|
+
provider: text2("provider").notNull(),
|
|
5465
|
+
gatewayConfigName: text2("gateway_config_name").notNull(),
|
|
5466
|
+
environment: text2("environment").notNull(),
|
|
5467
|
+
method: text2("method").notNull(),
|
|
5468
|
+
path: text2("path").notNull(),
|
|
5469
|
+
routePath: text2("route_path"),
|
|
5470
|
+
status: integer2("status").notNull(),
|
|
5471
|
+
callerId: text2("caller_id"),
|
|
5472
|
+
callerSource: text2("caller_source"),
|
|
5473
|
+
callerConfidence: text2("caller_confidence").notNull(),
|
|
5474
|
+
occurredAt: timestamp("occurred_at").notNull(),
|
|
5475
|
+
createdAt: timestamp("created_at").notNull()
|
|
5476
|
+
}, (table) => [
|
|
5477
|
+
index2("idx_http_logs_config_env").on(table.gatewayConfigName, table.environment),
|
|
5478
|
+
index2("idx_http_logs_occurred_at").on(table.occurredAt),
|
|
5479
|
+
index2("idx_http_logs_caller").on(table.gatewayConfigName, table.environment, table.callerId)
|
|
5480
|
+
]);
|
|
5481
|
+
var provisions2 = pgTable("provisions", {
|
|
5482
|
+
id: text2("id").primaryKey(),
|
|
5483
|
+
gatewayConfigName: text2("gateway_config_name").notNull(),
|
|
5484
|
+
gatewayConfigVersion: text2("gateway_config_version").notNull(),
|
|
5485
|
+
environment: text2("environment").notNull(),
|
|
5486
|
+
provider: text2("provider", { enum: ["kong"] }).notNull(),
|
|
5487
|
+
synced: boolean("synced").notNull().default(false),
|
|
5488
|
+
actor: text2("actor").notNull(),
|
|
5489
|
+
details: jsonb("details").$type(),
|
|
5490
|
+
createdAt: timestamp("created_at").notNull()
|
|
5491
|
+
}, (table) => [
|
|
5492
|
+
index2("idx_provisions_config_name").on(table.gatewayConfigName),
|
|
5493
|
+
index2("idx_provisions_created_at").on(table.createdAt)
|
|
5494
|
+
]);
|
|
4989
5495
|
|
|
4990
|
-
// src/
|
|
4991
|
-
|
|
4992
|
-
var
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
return response;
|
|
5008
|
-
});
|
|
5009
|
-
app.use("/*", serveStatic({ root: HUB_DIST_PATH }));
|
|
5010
|
-
app.get("/*", async (c2) => {
|
|
5011
|
-
const indexPath = path10.join(HUB_DIST_PATH, "index.html");
|
|
5012
|
-
if (fs10.existsSync(indexPath)) {
|
|
5013
|
-
return c2.html(fs10.readFileSync(indexPath, "utf-8"));
|
|
5496
|
+
// src/registry/storage/postgresql.ts
|
|
5497
|
+
import { v4 as uuid6 } from "uuid";
|
|
5498
|
+
var MIGRATIONS_FOLDER2 = PG_MIGRATIONS_FOLDER;
|
|
5499
|
+
var DatabaseConnectionError = class extends Error {
|
|
5500
|
+
constructor(message, postgresUrl, options) {
|
|
5501
|
+
super(message, options);
|
|
5502
|
+
this.postgresUrl = postgresUrl;
|
|
5503
|
+
this.name = "DatabaseConnectionError";
|
|
5504
|
+
}
|
|
5505
|
+
postgresUrl;
|
|
5506
|
+
};
|
|
5507
|
+
function isConnectionError(err) {
|
|
5508
|
+
if (typeof err !== "object" || err === null) return false;
|
|
5509
|
+
if ("code" in err) {
|
|
5510
|
+
const code = err.code;
|
|
5511
|
+
if (code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND") {
|
|
5512
|
+
return true;
|
|
5014
5513
|
}
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
});
|
|
5024
|
-
}
|
|
5025
|
-
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
|
5026
|
-
startHubServer();
|
|
5514
|
+
}
|
|
5515
|
+
if (err instanceof AggregateError) {
|
|
5516
|
+
if (err.errors.some(isConnectionError)) return true;
|
|
5517
|
+
}
|
|
5518
|
+
if ("cause" in err && err.cause) {
|
|
5519
|
+
return isConnectionError(err.cause);
|
|
5520
|
+
}
|
|
5521
|
+
return false;
|
|
5027
5522
|
}
|
|
5028
|
-
|
|
5029
|
-
// src/cli/commands/serve.ts
|
|
5030
|
-
function getPackageVersion() {
|
|
5523
|
+
function maskPostgresPassword(url) {
|
|
5031
5524
|
try {
|
|
5032
|
-
const
|
|
5033
|
-
|
|
5034
|
-
return
|
|
5525
|
+
const parsed = new URL(url);
|
|
5526
|
+
if (parsed.password) parsed.password = "***";
|
|
5527
|
+
return parsed.toString();
|
|
5035
5528
|
} catch {
|
|
5036
|
-
return
|
|
5529
|
+
return url;
|
|
5037
5530
|
}
|
|
5038
5531
|
}
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5532
|
+
var PostgreSQLSpecStore = class {
|
|
5533
|
+
db;
|
|
5534
|
+
pool;
|
|
5535
|
+
postgresUrl;
|
|
5536
|
+
constructor(postgresUrl) {
|
|
5537
|
+
this.postgresUrl = postgresUrl;
|
|
5538
|
+
this.pool = new Pool({ connectionString: postgresUrl });
|
|
5539
|
+
this.db = drizzle2(this.pool);
|
|
5540
|
+
}
|
|
5541
|
+
async migrate() {
|
|
5542
|
+
try {
|
|
5543
|
+
await migrate2(this.db, {
|
|
5544
|
+
migrationsFolder: MIGRATIONS_FOLDER2
|
|
5545
|
+
});
|
|
5546
|
+
} catch (err) {
|
|
5547
|
+
if (isConnectionError(err)) {
|
|
5548
|
+
throw new DatabaseConnectionError(
|
|
5549
|
+
`PostgreSQL is not reachable at ${maskPostgresPassword(this.postgresUrl)}`,
|
|
5550
|
+
this.postgresUrl,
|
|
5551
|
+
{ cause: err }
|
|
5552
|
+
);
|
|
5553
|
+
}
|
|
5554
|
+
throw err;
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
async end() {
|
|
5558
|
+
await this.pool.end();
|
|
5559
|
+
}
|
|
5560
|
+
async getSpec(name) {
|
|
5561
|
+
const rows = await this.db.select().from(specs2).where(eq2(specs2.name, name)).limit(1);
|
|
5562
|
+
if (rows.length === 0) return null;
|
|
5563
|
+
return this.mapSpecRow(rows[0]);
|
|
5564
|
+
}
|
|
5565
|
+
async getSpecVersion(name, semver) {
|
|
5566
|
+
const spec = await this.getSpec(name);
|
|
5567
|
+
if (!spec) return null;
|
|
5568
|
+
const rows = await this.db.select().from(specVersions2).where(and2(eq2(specVersions2.specId, spec.id), eq2(specVersions2.semver, semver))).limit(1);
|
|
5569
|
+
if (rows.length === 0) return null;
|
|
5570
|
+
return this.mapVersionRow(rows[0]);
|
|
5571
|
+
}
|
|
5572
|
+
async getLatestVersion(name) {
|
|
5573
|
+
const spec = await this.getSpec(name);
|
|
5574
|
+
if (!spec) return null;
|
|
5575
|
+
const rows = await this.db.select().from(specVersions2).where(eq2(specVersions2.specId, spec.id)).orderBy(desc2(specVersions2.createdAt), desc2(specVersions2.id)).limit(1);
|
|
5576
|
+
if (rows.length === 0) return null;
|
|
5577
|
+
return this.mapVersionRow(rows[0]);
|
|
5578
|
+
}
|
|
5579
|
+
async listSpecs(filters) {
|
|
5580
|
+
const conditions = [];
|
|
5581
|
+
if (filters?.type) conditions.push(eq2(specs2.type, filters.type));
|
|
5582
|
+
if (filters?.owner) conditions.push(eq2(specs2.owner, filters.owner));
|
|
5583
|
+
let rows = conditions.length > 0 ? await this.db.select().from(specs2).where(and2(...conditions)) : await this.db.select().from(specs2);
|
|
5584
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
5585
|
+
rows = rows.filter((row) => {
|
|
5586
|
+
const rowTags = row.tags ?? [];
|
|
5587
|
+
return filters.tags.every((tag) => rowTags.includes(tag));
|
|
5588
|
+
});
|
|
5589
|
+
}
|
|
5590
|
+
return rows.map((r) => this.mapSpecRow(r));
|
|
5591
|
+
}
|
|
5592
|
+
async listVersions(name, options) {
|
|
5593
|
+
const spec = await this.getSpec(name);
|
|
5594
|
+
if (!spec) return { versions: [], total: 0 };
|
|
5595
|
+
const limit = options?.limit ?? 10;
|
|
5596
|
+
const offset = options?.offset ?? 0;
|
|
5597
|
+
const [countRow] = await this.db.select({ count: sql2`count(*)` }).from(specVersions2).where(eq2(specVersions2.specId, spec.id));
|
|
5598
|
+
const total = Number(countRow?.count ?? 0);
|
|
5599
|
+
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);
|
|
5600
|
+
return { versions: rows.map((r) => this.mapVersionRow(r)), total };
|
|
5601
|
+
}
|
|
5602
|
+
async pushSpecVersion(spec, version2) {
|
|
5603
|
+
const existingSpec = await this.getSpec(spec.name);
|
|
5604
|
+
if (!existingSpec) {
|
|
5605
|
+
await this.db.insert(specs2).values({
|
|
5606
|
+
id: spec.id,
|
|
5607
|
+
name: spec.name,
|
|
5608
|
+
type: spec.type,
|
|
5609
|
+
description: spec.description ?? null,
|
|
5610
|
+
owner: spec.owner ?? null,
|
|
5611
|
+
sourceRepo: spec.sourceRepo ?? null,
|
|
5612
|
+
tags: spec.tags ?? [],
|
|
5613
|
+
createdAt: spec.createdAt,
|
|
5614
|
+
updatedAt: spec.updatedAt
|
|
5615
|
+
});
|
|
5616
|
+
} else {
|
|
5617
|
+
await this.db.update(specs2).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq2(specs2.id, existingSpec.id));
|
|
5618
|
+
}
|
|
5619
|
+
const specId = existingSpec?.id ?? spec.id;
|
|
5620
|
+
await this.db.insert(specVersions2).values({
|
|
5621
|
+
id: version2.id,
|
|
5622
|
+
specId,
|
|
5623
|
+
semver: version2.semver,
|
|
5624
|
+
content: version2.content,
|
|
5625
|
+
checksum: version2.checksum,
|
|
5626
|
+
gitRef: version2.gitRef ?? null,
|
|
5627
|
+
pushedBy: version2.pushedBy ?? null,
|
|
5628
|
+
compatibility: version2.compatibility ?? null,
|
|
5629
|
+
previousVersion: version2.previousVersion ?? null,
|
|
5630
|
+
forceReason: version2.forceReason ?? null,
|
|
5631
|
+
isPrerelease: version2.isPrerelease,
|
|
5632
|
+
createdAt: version2.createdAt
|
|
5633
|
+
});
|
|
5634
|
+
return version2;
|
|
5635
|
+
}
|
|
5636
|
+
async deleteSpec(name) {
|
|
5637
|
+
const existingSpec = await this.getSpec(name);
|
|
5638
|
+
if (!existingSpec) return false;
|
|
5639
|
+
await this.db.delete(specVersions2).where(eq2(specVersions2.specId, existingSpec.id));
|
|
5640
|
+
await this.db.delete(specs2).where(eq2(specs2.id, existingSpec.id));
|
|
5641
|
+
return true;
|
|
5642
|
+
}
|
|
5643
|
+
async getCompatReport(name, semver) {
|
|
5644
|
+
const version2 = await this.getSpecVersion(name, semver);
|
|
5645
|
+
return version2?.compatibility ?? null;
|
|
5646
|
+
}
|
|
5647
|
+
async logAudit(action, actor, specName, version2, details) {
|
|
5648
|
+
await this.db.insert(auditLog2).values({
|
|
5649
|
+
id: uuid6(),
|
|
5650
|
+
action,
|
|
5651
|
+
actor,
|
|
5652
|
+
specName,
|
|
5653
|
+
version: version2 ?? null,
|
|
5654
|
+
details: details ?? null,
|
|
5655
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
5656
|
+
});
|
|
5657
|
+
}
|
|
5658
|
+
mapSpecRow(row) {
|
|
5659
|
+
return {
|
|
5660
|
+
id: row.id,
|
|
5661
|
+
name: row.name,
|
|
5662
|
+
type: row.type,
|
|
5663
|
+
description: row.description ?? void 0,
|
|
5664
|
+
owner: row.owner ?? void 0,
|
|
5665
|
+
sourceRepo: row.sourceRepo ?? void 0,
|
|
5666
|
+
tags: row.tags ?? [],
|
|
5667
|
+
createdAt: row.createdAt,
|
|
5668
|
+
updatedAt: row.updatedAt
|
|
5669
|
+
};
|
|
5670
|
+
}
|
|
5671
|
+
mapVersionRow(row) {
|
|
5672
|
+
return {
|
|
5673
|
+
id: row.id,
|
|
5674
|
+
specId: row.specId,
|
|
5675
|
+
semver: row.semver,
|
|
5676
|
+
content: row.content,
|
|
5677
|
+
checksum: row.checksum,
|
|
5678
|
+
gitRef: row.gitRef ?? void 0,
|
|
5679
|
+
pushedBy: row.pushedBy ?? void 0,
|
|
5680
|
+
compatibility: row.compatibility ?? void 0,
|
|
5681
|
+
previousVersion: row.previousVersion ?? void 0,
|
|
5682
|
+
forceReason: row.forceReason ?? void 0,
|
|
5683
|
+
isPrerelease: row.isPrerelease,
|
|
5684
|
+
createdAt: row.createdAt
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
// GatewayConfigStore implementation
|
|
5688
|
+
async getGatewayConfig(name) {
|
|
5689
|
+
const rows = await this.db.select().from(gatewayConfigs2).where(eq2(gatewayConfigs2.name, name)).limit(1);
|
|
5690
|
+
if (rows.length === 0) return null;
|
|
5691
|
+
return this.mapGatewayConfigRow(rows[0]);
|
|
5692
|
+
}
|
|
5693
|
+
async listGatewayConfigs() {
|
|
5694
|
+
const rows = await this.db.select().from(gatewayConfigs2);
|
|
5695
|
+
return rows.map((r) => this.mapGatewayConfigRow(r));
|
|
5696
|
+
}
|
|
5697
|
+
async getGatewayConfigVersion(name, versionId) {
|
|
5698
|
+
const config = await this.getGatewayConfig(name);
|
|
5699
|
+
if (!config) return null;
|
|
5700
|
+
const rows = await this.db.select().from(gatewayConfigVersions2).where(and2(eq2(gatewayConfigVersions2.gatewayConfigId, config.id), eq2(gatewayConfigVersions2.id, versionId))).limit(1);
|
|
5701
|
+
if (rows.length === 0) return null;
|
|
5702
|
+
return this.mapGatewayConfigVersionRow(rows[0]);
|
|
5703
|
+
}
|
|
5704
|
+
async getLatestGatewayConfigVersion(name) {
|
|
5705
|
+
const config = await this.getGatewayConfig(name);
|
|
5706
|
+
if (!config) return null;
|
|
5707
|
+
const rows = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, config.id)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id)).limit(1);
|
|
5708
|
+
if (rows.length === 0) return null;
|
|
5709
|
+
return this.mapGatewayConfigVersionRow(rows[0]);
|
|
5710
|
+
}
|
|
5711
|
+
async listGatewayConfigVersions(name) {
|
|
5712
|
+
const config = await this.getGatewayConfig(name);
|
|
5713
|
+
if (!config) return [];
|
|
5714
|
+
const rows = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, config.id)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id)).limit(5);
|
|
5715
|
+
return rows.map((r) => this.mapGatewayConfigVersionRow(r));
|
|
5716
|
+
}
|
|
5717
|
+
async pushGatewayConfigVersion(config, version2) {
|
|
5718
|
+
const existingConfig = await this.getGatewayConfig(config.name);
|
|
5719
|
+
if (!existingConfig) {
|
|
5720
|
+
await this.db.insert(gatewayConfigs2).values({
|
|
5721
|
+
id: config.id,
|
|
5722
|
+
name: config.name,
|
|
5723
|
+
provider: config.provider,
|
|
5724
|
+
specName: config.specName,
|
|
5725
|
+
specSemver: config.specSemver,
|
|
5726
|
+
createdAt: config.createdAt,
|
|
5727
|
+
updatedAt: config.updatedAt
|
|
5728
|
+
});
|
|
5729
|
+
} else {
|
|
5730
|
+
await this.db.update(gatewayConfigs2).set({ updatedAt: /* @__PURE__ */ new Date(), specSemver: config.specSemver }).where(eq2(gatewayConfigs2.id, existingConfig.id));
|
|
5731
|
+
}
|
|
5732
|
+
const configId = existingConfig?.id ?? config.id;
|
|
5733
|
+
await this.db.insert(gatewayConfigVersions2).values({
|
|
5734
|
+
id: version2.id,
|
|
5735
|
+
gatewayConfigId: configId,
|
|
5736
|
+
routes: version2.routes,
|
|
5737
|
+
environments: version2.environments,
|
|
5738
|
+
callerIdentification: version2.callerIdentification ?? null,
|
|
5739
|
+
content: version2.content,
|
|
5740
|
+
checksum: version2.checksum,
|
|
5741
|
+
pushedBy: version2.pushedBy ?? null,
|
|
5742
|
+
createdAt: version2.createdAt
|
|
5743
|
+
});
|
|
5744
|
+
const versions = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, configId)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id));
|
|
5745
|
+
if (versions.length > 5) {
|
|
5746
|
+
const toDelete = versions.slice(5);
|
|
5747
|
+
for (const v of toDelete) {
|
|
5748
|
+
await this.db.delete(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.id, v.id));
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
return version2;
|
|
5752
|
+
}
|
|
5753
|
+
async recordProvision(provision) {
|
|
5754
|
+
await this.db.insert(provisions2).values({
|
|
5755
|
+
id: provision.id,
|
|
5756
|
+
gatewayConfigName: provision.gatewayConfigName,
|
|
5757
|
+
gatewayConfigVersion: provision.gatewayConfigVersion,
|
|
5758
|
+
environment: provision.environment,
|
|
5759
|
+
provider: provision.provider,
|
|
5760
|
+
synced: provision.synced,
|
|
5761
|
+
actor: provision.actor,
|
|
5762
|
+
details: provision.details ?? null,
|
|
5763
|
+
createdAt: provision.createdAt
|
|
5764
|
+
});
|
|
5765
|
+
}
|
|
5766
|
+
async listProvisions(gatewayConfigName) {
|
|
5767
|
+
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));
|
|
5768
|
+
return rows.map((r) => ({
|
|
5769
|
+
id: r.id,
|
|
5770
|
+
gatewayConfigName: r.gatewayConfigName,
|
|
5771
|
+
gatewayConfigVersion: r.gatewayConfigVersion,
|
|
5772
|
+
environment: r.environment,
|
|
5773
|
+
provider: r.provider,
|
|
5774
|
+
synced: r.synced,
|
|
5775
|
+
actor: r.actor,
|
|
5776
|
+
details: r.details ?? void 0,
|
|
5777
|
+
createdAt: r.createdAt
|
|
5778
|
+
}));
|
|
5779
|
+
}
|
|
5780
|
+
mapGatewayConfigRow(row) {
|
|
5781
|
+
return {
|
|
5782
|
+
id: row.id,
|
|
5783
|
+
name: row.name,
|
|
5784
|
+
provider: row.provider,
|
|
5785
|
+
specName: row.specName,
|
|
5786
|
+
specSemver: row.specSemver,
|
|
5787
|
+
createdAt: row.createdAt,
|
|
5788
|
+
updatedAt: row.updatedAt
|
|
5789
|
+
};
|
|
5790
|
+
}
|
|
5791
|
+
mapGatewayConfigVersionRow(row) {
|
|
5792
|
+
return {
|
|
5793
|
+
id: row.id,
|
|
5794
|
+
gatewayConfigId: row.gatewayConfigId,
|
|
5795
|
+
routes: row.routes,
|
|
5796
|
+
environments: row.environments,
|
|
5797
|
+
callerIdentification: row.callerIdentification ?? void 0,
|
|
5798
|
+
content: row.content,
|
|
5799
|
+
checksum: row.checksum,
|
|
5800
|
+
pushedBy: row.pushedBy ?? void 0,
|
|
5801
|
+
createdAt: row.createdAt
|
|
5802
|
+
};
|
|
5803
|
+
}
|
|
5804
|
+
async recordGatewayLog(log) {
|
|
5805
|
+
await this.db.insert(httpLogs2).values({
|
|
5806
|
+
id: log.id,
|
|
5807
|
+
provider: log.provider,
|
|
5808
|
+
gatewayConfigName: log.gatewayConfigName,
|
|
5809
|
+
environment: log.environment,
|
|
5810
|
+
method: log.method,
|
|
5811
|
+
path: log.path,
|
|
5812
|
+
routePath: log.routePath ?? null,
|
|
5813
|
+
status: log.status,
|
|
5814
|
+
callerId: log.callerId ?? null,
|
|
5815
|
+
callerSource: log.callerSource ?? null,
|
|
5816
|
+
callerConfidence: log.callerConfidence,
|
|
5817
|
+
occurredAt: log.occurredAt,
|
|
5818
|
+
createdAt: log.createdAt
|
|
5819
|
+
});
|
|
5820
|
+
}
|
|
5821
|
+
async listGatewayLogs(filters) {
|
|
5822
|
+
const limit = filters.limit ?? 50;
|
|
5823
|
+
const offset = filters.offset ?? 0;
|
|
5824
|
+
let query = this.db.select().from(httpLogs2);
|
|
5825
|
+
const conditions = [];
|
|
5826
|
+
if (filters.gatewayConfigName) {
|
|
5827
|
+
conditions.push(eq2(httpLogs2.gatewayConfigName, filters.gatewayConfigName));
|
|
5828
|
+
}
|
|
5829
|
+
if (filters.environment) {
|
|
5830
|
+
conditions.push(eq2(httpLogs2.environment, filters.environment));
|
|
5831
|
+
}
|
|
5832
|
+
if (filters.path) {
|
|
5833
|
+
conditions.push(eq2(httpLogs2.path, filters.path));
|
|
5834
|
+
}
|
|
5835
|
+
if (filters.method) {
|
|
5836
|
+
conditions.push(eq2(httpLogs2.method, filters.method));
|
|
5837
|
+
}
|
|
5838
|
+
if (filters.status !== void 0) {
|
|
5839
|
+
conditions.push(eq2(httpLogs2.status, filters.status));
|
|
5840
|
+
}
|
|
5841
|
+
if (filters.from) {
|
|
5842
|
+
conditions.push(sql2`${httpLogs2.occurredAt} >= ${filters.from}`);
|
|
5843
|
+
}
|
|
5844
|
+
if (filters.to) {
|
|
5845
|
+
conditions.push(sql2`${httpLogs2.occurredAt} <= ${filters.to}`);
|
|
5846
|
+
}
|
|
5847
|
+
if (conditions.length > 0) {
|
|
5848
|
+
query = query.where(and2(...conditions));
|
|
5849
|
+
}
|
|
5850
|
+
const countResult = await this.db.select({ count: sql2`count(*)` }).from(httpLogs2).where(conditions.length > 0 ? and2(...conditions) : void 0);
|
|
5851
|
+
const total = countResult[0]?.count ?? 0;
|
|
5852
|
+
const rows = await query.orderBy(desc2(httpLogs2.occurredAt)).limit(limit).offset(offset);
|
|
5853
|
+
return {
|
|
5854
|
+
logs: rows.map((r) => ({
|
|
5855
|
+
id: r.id,
|
|
5856
|
+
provider: r.provider,
|
|
5857
|
+
gatewayConfigName: r.gatewayConfigName,
|
|
5858
|
+
environment: r.environment,
|
|
5859
|
+
method: r.method,
|
|
5860
|
+
path: r.path,
|
|
5861
|
+
routePath: r.routePath ?? void 0,
|
|
5862
|
+
status: r.status,
|
|
5863
|
+
callerId: r.callerId ?? void 0,
|
|
5864
|
+
callerSource: r.callerSource ?? void 0,
|
|
5865
|
+
callerConfidence: r.callerConfidence,
|
|
5866
|
+
occurredAt: r.occurredAt,
|
|
5867
|
+
createdAt: r.createdAt
|
|
5868
|
+
})),
|
|
5869
|
+
total
|
|
5870
|
+
};
|
|
5871
|
+
}
|
|
5872
|
+
async getGatewayLog(id) {
|
|
5873
|
+
const rows = await this.db.select().from(httpLogs2).where(eq2(httpLogs2.id, id)).limit(1);
|
|
5874
|
+
if (rows.length === 0) return null;
|
|
5875
|
+
const r = rows[0];
|
|
5876
|
+
return {
|
|
5877
|
+
id: r.id,
|
|
5878
|
+
provider: r.provider,
|
|
5879
|
+
gatewayConfigName: r.gatewayConfigName,
|
|
5880
|
+
environment: r.environment,
|
|
5881
|
+
method: r.method,
|
|
5882
|
+
path: r.path,
|
|
5883
|
+
routePath: r.routePath ?? void 0,
|
|
5884
|
+
status: r.status,
|
|
5885
|
+
callerId: r.callerId ?? void 0,
|
|
5886
|
+
callerSource: r.callerSource ?? void 0,
|
|
5887
|
+
callerConfidence: r.callerConfidence,
|
|
5888
|
+
occurredAt: r.occurredAt,
|
|
5889
|
+
createdAt: r.createdAt
|
|
5890
|
+
};
|
|
5891
|
+
}
|
|
5892
|
+
async getGatewayLogStats(_filters) {
|
|
5893
|
+
const conditions = [];
|
|
5894
|
+
if (_filters.gatewayConfigName) {
|
|
5895
|
+
conditions.push(eq2(httpLogs2.gatewayConfigName, _filters.gatewayConfigName));
|
|
5896
|
+
}
|
|
5897
|
+
if (_filters.environment) {
|
|
5898
|
+
conditions.push(eq2(httpLogs2.environment, _filters.environment));
|
|
5899
|
+
}
|
|
5900
|
+
if (_filters.from) {
|
|
5901
|
+
conditions.push(sql2`${httpLogs2.occurredAt} >= ${_filters.from}`);
|
|
5902
|
+
}
|
|
5903
|
+
if (_filters.to) {
|
|
5904
|
+
conditions.push(sql2`${httpLogs2.occurredAt} <= ${_filters.to}`);
|
|
5905
|
+
}
|
|
5906
|
+
const whereClause = conditions.length > 0 ? and2(...conditions) : void 0;
|
|
5907
|
+
const rows = await this.db.select({
|
|
5908
|
+
gatewayConfigName: httpLogs2.gatewayConfigName,
|
|
5909
|
+
environment: httpLogs2.environment,
|
|
5910
|
+
method: httpLogs2.method,
|
|
5911
|
+
routePath: httpLogs2.routePath,
|
|
5912
|
+
lastSeenAt: sql2`max(${httpLogs2.occurredAt})`,
|
|
5913
|
+
totalCalls: sql2`count(*)`,
|
|
5914
|
+
uniqueCallerIds: sql2`count(distinct ${httpLogs2.callerId})`
|
|
5915
|
+
}).from(httpLogs2).where(whereClause).groupBy(httpLogs2.gatewayConfigName, httpLogs2.environment, httpLogs2.method, httpLogs2.routePath);
|
|
5916
|
+
return rows.map((r) => ({
|
|
5917
|
+
gatewayConfigName: r.gatewayConfigName,
|
|
5918
|
+
environment: r.environment,
|
|
5919
|
+
method: r.method,
|
|
5920
|
+
routePath: r.routePath ?? "/",
|
|
5921
|
+
lastSeenAt: new Date(r.lastSeenAt),
|
|
5922
|
+
totalCalls: r.totalCalls,
|
|
5923
|
+
uniqueCallerIds: r.uniqueCallerIds
|
|
5924
|
+
}));
|
|
5925
|
+
}
|
|
5926
|
+
async deleteGatewayLogsOlderThan(days) {
|
|
5927
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
5928
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
5929
|
+
await this.db.delete(httpLogs2).where(sql2`${httpLogs2.occurredAt} < ${cutoff}`);
|
|
5930
|
+
}
|
|
5931
|
+
};
|
|
5932
|
+
|
|
5933
|
+
// src/registry/serve.ts
|
|
5934
|
+
async function startServer(userConfig) {
|
|
5935
|
+
const config = { ...defaultConfig, ...userConfig };
|
|
5936
|
+
let store;
|
|
5937
|
+
if (config.database === "postgresql") {
|
|
5938
|
+
if (!config.postgresUrl) {
|
|
5939
|
+
throw new Error("PostgreSQL database requested but no postgresUrl provided.");
|
|
5940
|
+
}
|
|
5941
|
+
store = new PostgreSQLSpecStore(config.postgresUrl);
|
|
5942
|
+
} else {
|
|
5943
|
+
const sqlitePath = config.sqlitePath ?? path9.join(
|
|
5944
|
+
process.env.HOME || process.env.USERPROFILE || ".",
|
|
5945
|
+
".grapity",
|
|
5946
|
+
"registry.db"
|
|
5947
|
+
);
|
|
5948
|
+
const dir = path9.dirname(sqlitePath);
|
|
5949
|
+
if (!fs9.existsSync(dir)) {
|
|
5950
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
5951
|
+
}
|
|
5952
|
+
store = new SQLiteSpecStore(sqlitePath);
|
|
5953
|
+
}
|
|
5954
|
+
await store.migrate();
|
|
5955
|
+
const app = createApp(config, store);
|
|
5956
|
+
const server = serve({
|
|
5957
|
+
fetch: app.fetch,
|
|
5958
|
+
port: config.port
|
|
5959
|
+
});
|
|
5960
|
+
return { app, store, server };
|
|
5961
|
+
}
|
|
5962
|
+
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
|
5963
|
+
startServer();
|
|
5964
|
+
}
|
|
5965
|
+
|
|
5966
|
+
// src/hub/serve.ts
|
|
5967
|
+
import { Hono as Hono23 } from "hono";
|
|
5968
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5969
|
+
import { serve as serve2 } from "@hono/node-server";
|
|
5970
|
+
import path10 from "path";
|
|
5971
|
+
import fs10 from "fs";
|
|
5972
|
+
|
|
5973
|
+
// src/hub/paths.ts
|
|
5974
|
+
var HUB_DIST_PATH = new URL(
|
|
5975
|
+
"../../dist",
|
|
5976
|
+
import.meta.url
|
|
5977
|
+
).pathname;
|
|
5978
|
+
|
|
5979
|
+
// src/hub/serve.ts
|
|
5980
|
+
var DEFAULT_PORT = 3e3;
|
|
5981
|
+
var DEFAULT_REGISTRY_URL = "http://localhost:3750";
|
|
5982
|
+
async function startHubServer(userConfig) {
|
|
5983
|
+
const config = {
|
|
5984
|
+
port: userConfig?.port ?? DEFAULT_PORT,
|
|
5985
|
+
registryUrl: userConfig?.registryUrl ?? DEFAULT_REGISTRY_URL,
|
|
5986
|
+
auth: userConfig?.auth
|
|
5987
|
+
};
|
|
5988
|
+
const app = new Hono23();
|
|
5989
|
+
app.get("/config.js", (c2) => {
|
|
5990
|
+
const clientConfig = {
|
|
5991
|
+
registryUrl: config.registryUrl,
|
|
5992
|
+
auth: config.auth ? {
|
|
5993
|
+
mode: config.auth.mode,
|
|
5994
|
+
serverUrl: config.auth.serverUrl,
|
|
5995
|
+
realm: config.auth.realm,
|
|
5996
|
+
clientId: config.auth.clientId,
|
|
5997
|
+
audience: config.auth.audience
|
|
5998
|
+
} : void 0
|
|
5999
|
+
};
|
|
6000
|
+
c2.header("Content-Type", "application/javascript");
|
|
6001
|
+
return c2.body(
|
|
6002
|
+
`window.__GRAPITY_CONFIG__ = ${JSON.stringify(clientConfig)};`
|
|
6003
|
+
);
|
|
6004
|
+
});
|
|
6005
|
+
app.use("/v1/*", async (c2) => {
|
|
6006
|
+
const url = new URL(c2.req.url);
|
|
6007
|
+
const targetUrl = config.registryUrl + url.pathname + url.search;
|
|
6008
|
+
const headers = new Headers(c2.req.raw.headers);
|
|
6009
|
+
headers.delete("host");
|
|
6010
|
+
const response = await fetch(targetUrl, {
|
|
6011
|
+
method: c2.req.method,
|
|
6012
|
+
headers,
|
|
6013
|
+
body: c2.req.raw.body
|
|
6014
|
+
});
|
|
6015
|
+
return response;
|
|
6016
|
+
});
|
|
6017
|
+
app.use("/*", serveStatic({ root: HUB_DIST_PATH }));
|
|
6018
|
+
app.get("/*", async (c2) => {
|
|
6019
|
+
const indexPath = path10.join(HUB_DIST_PATH, "index.html");
|
|
6020
|
+
if (!fs10.existsSync(indexPath)) {
|
|
6021
|
+
return c2.text(
|
|
6022
|
+
"index.html not found. Build the project with 'bun run build' first.",
|
|
6023
|
+
404
|
|
6024
|
+
);
|
|
6025
|
+
}
|
|
6026
|
+
const html = fs10.readFileSync(indexPath, "utf-8");
|
|
6027
|
+
const configScript = `
|
|
6028
|
+
<script src="/config.js"></script>
|
|
6029
|
+
`;
|
|
6030
|
+
const injected = html.replace(
|
|
6031
|
+
"</head>",
|
|
6032
|
+
`${configScript}</head>`
|
|
6033
|
+
);
|
|
6034
|
+
return c2.html(injected);
|
|
6035
|
+
});
|
|
6036
|
+
serve2({
|
|
6037
|
+
fetch: app.fetch,
|
|
6038
|
+
port: config.port
|
|
6039
|
+
});
|
|
6040
|
+
}
|
|
6041
|
+
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
|
6042
|
+
startHubServer();
|
|
6043
|
+
}
|
|
6044
|
+
|
|
6045
|
+
// src/cli/commands/serve.ts
|
|
6046
|
+
function resolveServerConfig(cliOptions, cliConfig) {
|
|
6047
|
+
if (cliConfig.mode === "remote") {
|
|
6048
|
+
throw new Error(
|
|
6049
|
+
`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.`
|
|
6050
|
+
);
|
|
6051
|
+
}
|
|
6052
|
+
const envUrl = process.env.GRAPITY_DATABASE_URL;
|
|
6053
|
+
const local = cliConfig.local;
|
|
6054
|
+
const dbValue = envUrl ?? local?.postgresUrl;
|
|
6055
|
+
const base = { port: cliOptions.port };
|
|
6056
|
+
if (dbValue && isPostgresqlUrl(dbValue)) {
|
|
6057
|
+
base.database = "postgresql";
|
|
6058
|
+
base.postgresUrl = dbValue;
|
|
6059
|
+
} else {
|
|
6060
|
+
base.database = "sqlite";
|
|
6061
|
+
base.sqlitePath = dbValue ?? local?.sqlitePath ?? path11.join(os4.homedir(), ".grapity", "registry.db");
|
|
6062
|
+
}
|
|
6063
|
+
if (cliOptions.noAuth) {
|
|
6064
|
+
base.auth = { mode: "none" };
|
|
6065
|
+
} else {
|
|
6066
|
+
base.auth = resolveAuthConfig(local?.auth);
|
|
6067
|
+
}
|
|
6068
|
+
return base;
|
|
6069
|
+
}
|
|
6070
|
+
function resolveAuthConfig(authConfig) {
|
|
6071
|
+
if (!authConfig || authConfig.mode === "none") {
|
|
6072
|
+
throw new Error(
|
|
6073
|
+
"Authentication is required by default. Configure it with: grapity init --local --auth keycloak ...\nOr run with: grapity serve --no-auth"
|
|
6074
|
+
);
|
|
6075
|
+
}
|
|
6076
|
+
if (!authConfig.serverUrl) {
|
|
6077
|
+
throw new Error(
|
|
6078
|
+
"Keycloak auth is configured but serverUrl is missing. Run grapity init --local --auth keycloak --keycloak-server <url> ..."
|
|
6079
|
+
);
|
|
6080
|
+
}
|
|
6081
|
+
if (!authConfig.realm) {
|
|
6082
|
+
throw new Error(
|
|
6083
|
+
"Keycloak auth is configured but realm is missing. Run grapity init --local --auth keycloak --keycloak-realm <realm> ..."
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
6086
|
+
return {
|
|
6087
|
+
mode: "keycloak",
|
|
6088
|
+
serverUrl: authConfig.serverUrl.replace(/\/$/, ""),
|
|
6089
|
+
realm: authConfig.realm,
|
|
6090
|
+
audience: authConfig.audience,
|
|
6091
|
+
roleSource: authConfig.roleSource
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
async function verifyKeycloakReachable(serverUrl, realm) {
|
|
6095
|
+
const url = `${serverUrl}/realms/${realm}/.well-known/openid-configuration`;
|
|
6096
|
+
try {
|
|
6097
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
6098
|
+
if (!res.ok) {
|
|
6099
|
+
throw new Error(`Keycloak returned ${res.status}`);
|
|
6100
|
+
}
|
|
6101
|
+
} catch (err) {
|
|
6102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6103
|
+
throw new Error(
|
|
6104
|
+
`Keycloak is not reachable at ${url}: ${message}
|
|
6105
|
+
See https://grapity.dev/docs/cli-reference/init#local-mode-with-keycloak to set up a local Keycloak server.
|
|
6106
|
+
Or run with: grapity serve --no-auth`
|
|
6107
|
+
);
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
function formatDatabaseConnectionError(err) {
|
|
6111
|
+
return formatError(
|
|
6112
|
+
"PostgreSQL is not reachable",
|
|
6113
|
+
err.message,
|
|
6114
|
+
[
|
|
6115
|
+
"Make sure PostgreSQL is running and accessible.",
|
|
6116
|
+
"Check the database URL in ~/.grapity/config.yaml or GRAPITY_DATABASE_URL."
|
|
6117
|
+
]
|
|
6118
|
+
);
|
|
6119
|
+
}
|
|
6120
|
+
function createServeCommand(version2) {
|
|
6121
|
+
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) => {
|
|
6122
|
+
const port = parseInt(options.port, 10);
|
|
6123
|
+
const hubPort = parseInt(options.hubPort, 10);
|
|
6124
|
+
const startHub = options.hub !== false;
|
|
6125
|
+
const noAuth = options.auth === false;
|
|
6126
|
+
if (!configExists()) {
|
|
6127
|
+
console.error(
|
|
6128
|
+
formatError(
|
|
6129
|
+
"not initialized",
|
|
6130
|
+
"Run grapity init first to configure the local registry.",
|
|
6131
|
+
["Example: grapity init --local"]
|
|
6132
|
+
)
|
|
6133
|
+
);
|
|
6134
|
+
process.exit(1);
|
|
6135
|
+
}
|
|
6136
|
+
const cliConfig = getConfig();
|
|
6137
|
+
let serverConfig;
|
|
6138
|
+
try {
|
|
6139
|
+
serverConfig = resolveServerConfig({ port, noAuth }, cliConfig);
|
|
6140
|
+
} catch (err) {
|
|
6141
|
+
console.error(formatError("invalid config", err.message));
|
|
6142
|
+
process.exit(1);
|
|
6143
|
+
}
|
|
6144
|
+
if (serverConfig.auth?.mode === "keycloak" && !noAuth) {
|
|
6145
|
+
try {
|
|
6146
|
+
await verifyKeycloakReachable(
|
|
6147
|
+
serverConfig.auth.serverUrl,
|
|
6148
|
+
serverConfig.auth.realm
|
|
6149
|
+
);
|
|
6150
|
+
} catch (err) {
|
|
6151
|
+
console.error(formatError("keycloak unreachable", err.message));
|
|
6152
|
+
process.exit(1);
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
console.log(formatHeader("grapity", `v${version2}`));
|
|
6156
|
+
console.log("");
|
|
6157
|
+
console.log(formatHeader("grapity Registry"));
|
|
6158
|
+
console.log("");
|
|
6159
|
+
console.log(
|
|
6160
|
+
formatServeConfig({
|
|
6161
|
+
port,
|
|
6162
|
+
database: serverConfig.database,
|
|
6163
|
+
dbPath: serverConfig.sqlitePath,
|
|
6164
|
+
postgresUrl: serverConfig.postgresUrl,
|
|
6165
|
+
authMode: serverConfig.auth?.mode,
|
|
6166
|
+
keycloakServer: serverConfig.auth?.mode === "keycloak" ? serverConfig.auth.serverUrl : void 0,
|
|
6167
|
+
keycloakRealm: serverConfig.auth?.mode === "keycloak" ? serverConfig.auth.realm : void 0,
|
|
6168
|
+
keycloakAudience: serverConfig.auth?.mode === "keycloak" ? serverConfig.auth.audience : void 0
|
|
6169
|
+
})
|
|
6170
|
+
);
|
|
6171
|
+
console.log("");
|
|
6172
|
+
let store;
|
|
6173
|
+
try {
|
|
6174
|
+
const started = await startServer(serverConfig);
|
|
6175
|
+
store = started.store;
|
|
6176
|
+
} catch (err) {
|
|
6177
|
+
if (err instanceof DatabaseConnectionError) {
|
|
6178
|
+
console.error(formatDatabaseConnectionError(err));
|
|
6179
|
+
} else {
|
|
6180
|
+
console.error(formatError("failed to start server", err.message));
|
|
6181
|
+
}
|
|
6182
|
+
process.exit(1);
|
|
6183
|
+
}
|
|
6184
|
+
console.log(formatReady(port));
|
|
6185
|
+
if (startHub) {
|
|
6186
|
+
console.log("");
|
|
6187
|
+
console.log(formatHeader("grapity Hub"));
|
|
6188
|
+
console.log("");
|
|
6189
|
+
console.log(
|
|
6190
|
+
formatHubConfig({
|
|
6191
|
+
port: hubPort,
|
|
6192
|
+
registryUrl: `http://localhost:${port}`
|
|
6193
|
+
})
|
|
6194
|
+
);
|
|
6195
|
+
console.log("");
|
|
6196
|
+
await startHubServer({
|
|
6197
|
+
port: hubPort,
|
|
6198
|
+
registryUrl: `http://localhost:${port}`,
|
|
6199
|
+
auth: serverConfig.auth?.mode === "keycloak" ? {
|
|
6200
|
+
mode: "keycloak",
|
|
6201
|
+
serverUrl: serverConfig.auth.serverUrl,
|
|
6202
|
+
realm: serverConfig.auth.realm,
|
|
6203
|
+
clientId: "grapity-hub",
|
|
6204
|
+
audience: serverConfig.auth.audience
|
|
6205
|
+
} : void 0
|
|
6206
|
+
});
|
|
6207
|
+
console.log(formatHubReady(hubPort));
|
|
6208
|
+
}
|
|
6209
|
+
process.on("SIGINT", async () => {
|
|
5080
6210
|
console.log("");
|
|
5081
6211
|
console.log(formatShutdown());
|
|
6212
|
+
if ("end" in store && typeof store.end === "function") {
|
|
6213
|
+
await store.end();
|
|
6214
|
+
}
|
|
5082
6215
|
process.exit(0);
|
|
5083
6216
|
});
|
|
5084
6217
|
});
|
|
@@ -5088,7 +6221,7 @@ function createServeCommand(_version) {
|
|
|
5088
6221
|
import { createRequire } from "module";
|
|
5089
6222
|
var require2 = createRequire(import.meta.url);
|
|
5090
6223
|
var { version } = require2("../../package.json");
|
|
5091
|
-
var program = new
|
|
6224
|
+
var program = new Command21();
|
|
5092
6225
|
program.name("grapity").description("grapity - API spec registry and compatibility guardian").version(version).addHelpText(
|
|
5093
6226
|
"after",
|
|
5094
6227
|
"\nDocumentation: https://grapity.dev/docs/getting-started/quickstart"
|
|
@@ -5096,5 +6229,6 @@ program.name("grapity").description("grapity - API spec registry and compatibili
|
|
|
5096
6229
|
program.addCommand(registryCommand);
|
|
5097
6230
|
program.addCommand(gatewayCommand);
|
|
5098
6231
|
program.addCommand(initCommand);
|
|
6232
|
+
program.addCommand(authCommand);
|
|
5099
6233
|
program.addCommand(createServeCommand(version));
|
|
5100
6234
|
program.parse();
|