@grapity/grapity 0.3.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 +647 -69
- 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-DUPbMrAe.d.ts → index-Bx-7YlUF.d.ts} +15 -0
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +151 -3
- package/dist/registry/serve.d.ts +2 -1
- package/dist/registry/serve.js +202 -7
- package/package.json +13 -11
- 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/registry/serve.js
CHANGED
|
@@ -1756,6 +1756,7 @@ var pushRoute = new Hono().post("/", async (c) => {
|
|
|
1756
1756
|
}
|
|
1757
1757
|
const store = c.get("store");
|
|
1758
1758
|
const service = new RegistryService(store);
|
|
1759
|
+
const actor = c.get("actor") ?? body.pushedBy;
|
|
1759
1760
|
try {
|
|
1760
1761
|
const result = await service.pushSpec(body.content, body.name, {
|
|
1761
1762
|
type: body.type,
|
|
@@ -1764,7 +1765,7 @@ var pushRoute = new Hono().post("/", async (c) => {
|
|
|
1764
1765
|
sourceRepo: body.sourceRepo,
|
|
1765
1766
|
tags: Array.isArray(body.tags) ? body.tags : void 0,
|
|
1766
1767
|
gitRef: body.gitRef,
|
|
1767
|
-
pushedBy:
|
|
1768
|
+
pushedBy: actor,
|
|
1768
1769
|
prerelease: body.prerelease,
|
|
1769
1770
|
force: body.force,
|
|
1770
1771
|
reason: body.reason
|
|
@@ -1929,7 +1930,8 @@ var deleteSpecRoute = new Hono5().delete(
|
|
|
1929
1930
|
const name = c.req.param("name");
|
|
1930
1931
|
const store = c.get("store");
|
|
1931
1932
|
const service = new RegistryService(store);
|
|
1932
|
-
const
|
|
1933
|
+
const actor = c.get("actor");
|
|
1934
|
+
const deleted = await service.deleteSpec(name, actor);
|
|
1933
1935
|
if (!deleted) {
|
|
1934
1936
|
return c.json({ error: "not_found", message: `Spec "${name}" not found`, statusCode: 404 }, 404);
|
|
1935
1937
|
}
|
|
@@ -2670,6 +2672,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c) => {
|
|
|
2670
2672
|
}
|
|
2671
2673
|
const store = c.get("store");
|
|
2672
2674
|
const service = new GatewayService(store, store);
|
|
2675
|
+
const actor = c.get("actor") ?? body.pushedBy;
|
|
2673
2676
|
try {
|
|
2674
2677
|
const result = await service.pushGatewayConfig({
|
|
2675
2678
|
name: body.name,
|
|
@@ -2680,7 +2683,7 @@ var pushGatewayConfigRoute = new Hono13().post("/", async (c) => {
|
|
|
2680
2683
|
environments: body.environments ?? {},
|
|
2681
2684
|
callerIdentification: body.callerIdentification,
|
|
2682
2685
|
content: body.content,
|
|
2683
|
-
pushedBy:
|
|
2686
|
+
pushedBy: actor
|
|
2684
2687
|
});
|
|
2685
2688
|
return c.json({ data: result }, 201);
|
|
2686
2689
|
} catch (err) {
|
|
@@ -2945,7 +2948,137 @@ var gatewayLogStatsRoute = new Hono21().get("/stats", async (c) => {
|
|
|
2945
2948
|
});
|
|
2946
2949
|
});
|
|
2947
2950
|
|
|
2951
|
+
// src/registry/auth/middleware.ts
|
|
2952
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2953
|
+
var AuthError = class extends Error {
|
|
2954
|
+
constructor(statusCode, code, message) {
|
|
2955
|
+
super(message);
|
|
2956
|
+
this.statusCode = statusCode;
|
|
2957
|
+
this.code = code;
|
|
2958
|
+
this.name = "AuthError";
|
|
2959
|
+
}
|
|
2960
|
+
statusCode;
|
|
2961
|
+
code;
|
|
2962
|
+
};
|
|
2963
|
+
function buildKeycloakUrls(config) {
|
|
2964
|
+
const base = `${config.serverUrl}/realms/${config.realm}`;
|
|
2965
|
+
return {
|
|
2966
|
+
issuer: base,
|
|
2967
|
+
jwksUri: `${base}/protocol/openid-connect/certs`,
|
|
2968
|
+
tokenUrl: `${base}/protocol/openid-connect/token`
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function createAuthMiddleware(config, routeScopes) {
|
|
2972
|
+
if (config.auth?.mode !== "keycloak") {
|
|
2973
|
+
return async (_c, next) => await next();
|
|
2974
|
+
}
|
|
2975
|
+
const authConfig = config.auth;
|
|
2976
|
+
const { issuer, jwksUri } = buildKeycloakUrls(authConfig);
|
|
2977
|
+
const jwks = createRemoteJWKSet(new URL(jwksUri));
|
|
2978
|
+
const scopeByRoute = /* @__PURE__ */ new Map();
|
|
2979
|
+
for (const route of routeScopes) {
|
|
2980
|
+
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
2981
|
+
scopeByRoute.set(key, {
|
|
2982
|
+
operationId: route.operationId,
|
|
2983
|
+
scopes: route.scopes
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
return async (c, next) => {
|
|
2987
|
+
const matchedPath = c.req.matchedRoutes.map((r) => r.path).filter((p) => p !== "/*").pop();
|
|
2988
|
+
const routeKey = matchedPath ? `${c.req.method}:${matchedPath}` : `${c.req.method}:${c.req.routePath}`;
|
|
2989
|
+
const required = scopeByRoute.get(routeKey);
|
|
2990
|
+
if (!required || required.scopes.length === 0) {
|
|
2991
|
+
return await next();
|
|
2992
|
+
}
|
|
2993
|
+
const authHeader = c.req.header("Authorization");
|
|
2994
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
2995
|
+
throw new AuthError(401, "unauthorized", "Bearer token required");
|
|
2996
|
+
}
|
|
2997
|
+
const token = authHeader.slice("Bearer ".length).trim();
|
|
2998
|
+
if (!token) {
|
|
2999
|
+
throw new AuthError(401, "unauthorized", "Bearer token required");
|
|
3000
|
+
}
|
|
3001
|
+
let payload;
|
|
3002
|
+
try {
|
|
3003
|
+
const result = await jwtVerify(token, jwks, {
|
|
3004
|
+
issuer,
|
|
3005
|
+
audience: authConfig.audience
|
|
3006
|
+
});
|
|
3007
|
+
payload = result.payload;
|
|
3008
|
+
} catch (err) {
|
|
3009
|
+
const message = err instanceof Error ? err.message : "Invalid token";
|
|
3010
|
+
throw new AuthError(401, "unauthorized", `Invalid or expired token: ${message}`);
|
|
3011
|
+
}
|
|
3012
|
+
const subject = payload.sub;
|
|
3013
|
+
if (!subject) {
|
|
3014
|
+
throw new AuthError(401, "unauthorized", "Token missing subject claim");
|
|
3015
|
+
}
|
|
3016
|
+
const grantedScopes = extractScopes(payload, authConfig.roleSource ?? "scope");
|
|
3017
|
+
const missing = required.scopes.filter((s) => !grantedScopes.has(s));
|
|
3018
|
+
if (missing.length > 0) {
|
|
3019
|
+
throw new AuthError(
|
|
3020
|
+
403,
|
|
3021
|
+
"forbidden",
|
|
3022
|
+
`Missing required scope${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
c.set("actor", subject);
|
|
3026
|
+
c.set("claims", payload);
|
|
3027
|
+
await next();
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
function extractScopes(payload, source) {
|
|
3031
|
+
if (source === "realm_access.roles") {
|
|
3032
|
+
const roles = payload.realm_access?.roles;
|
|
3033
|
+
return new Set(roles ?? []);
|
|
3034
|
+
}
|
|
3035
|
+
const scopeValue = payload.scope;
|
|
3036
|
+
if (typeof scopeValue === "string") {
|
|
3037
|
+
return new Set(scopeValue.split(/\s+/).filter(Boolean));
|
|
3038
|
+
}
|
|
3039
|
+
return /* @__PURE__ */ new Set();
|
|
3040
|
+
}
|
|
3041
|
+
function parseRouteScopes(spec) {
|
|
3042
|
+
const routes = [];
|
|
3043
|
+
const paths = spec.paths;
|
|
3044
|
+
if (!paths) return routes;
|
|
3045
|
+
for (const [path2, operations] of Object.entries(paths)) {
|
|
3046
|
+
for (const [method, operation] of Object.entries(operations)) {
|
|
3047
|
+
if (typeof operation !== "object" || operation === null) continue;
|
|
3048
|
+
const op = operation;
|
|
3049
|
+
const operationId = op.operationId;
|
|
3050
|
+
if (!operationId) continue;
|
|
3051
|
+
const security = op.security;
|
|
3052
|
+
const scopes = [];
|
|
3053
|
+
if (security) {
|
|
3054
|
+
for (const sec of security) {
|
|
3055
|
+
for (const [name, required] of Object.entries(sec)) {
|
|
3056
|
+
if (name === "keycloak" && Array.isArray(required)) {
|
|
3057
|
+
scopes.push(...required);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
routes.push({
|
|
3063
|
+
method: method.toUpperCase(),
|
|
3064
|
+
path: path2.replace(/\{([^}]+)\}/g, ":$1"),
|
|
3065
|
+
operationId,
|
|
3066
|
+
scopes
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
return routes;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
2948
3073
|
// src/registry/server.ts
|
|
3074
|
+
import { readFileSync } from "fs";
|
|
3075
|
+
import { fileURLToPath } from "url";
|
|
3076
|
+
import yaml6 from "js-yaml";
|
|
3077
|
+
function loadOpenApiSpec() {
|
|
3078
|
+
const path2 = fileURLToPath(new URL("../../openapi.yaml", import.meta.url));
|
|
3079
|
+
const content = readFileSync(path2, "utf-8");
|
|
3080
|
+
return yaml6.load(content);
|
|
3081
|
+
}
|
|
2949
3082
|
function createApp(config, store) {
|
|
2950
3083
|
const app = new Hono22();
|
|
2951
3084
|
app.use("*", logger());
|
|
@@ -2956,6 +3089,21 @@ function createApp(config, store) {
|
|
|
2956
3089
|
c.set("config", config);
|
|
2957
3090
|
await next();
|
|
2958
3091
|
});
|
|
3092
|
+
const routeScopes = parseRouteScopes(loadOpenApiSpec());
|
|
3093
|
+
app.use("*", createAuthMiddleware(config, routeScopes));
|
|
3094
|
+
app.onError((err, c) => {
|
|
3095
|
+
if (err instanceof AuthError) {
|
|
3096
|
+
return c.json(
|
|
3097
|
+
{ error: err.code, message: err.message, statusCode: err.statusCode },
|
|
3098
|
+
err.statusCode
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
console.error("Unhandled error:", err);
|
|
3102
|
+
return c.json(
|
|
3103
|
+
{ error: "internal_error", message: "Internal server error", statusCode: 500 },
|
|
3104
|
+
500
|
|
3105
|
+
);
|
|
3106
|
+
});
|
|
2959
3107
|
app.route("/v1/specs", pushRoute);
|
|
2960
3108
|
app.route("/v1/specs", validateRoute);
|
|
2961
3109
|
app.route("/v1/specs", listRoute);
|
|
@@ -2983,7 +3131,8 @@ function createApp(config, store) {
|
|
|
2983
3131
|
// src/registry/config.ts
|
|
2984
3132
|
var defaultConfig = {
|
|
2985
3133
|
port: 3750,
|
|
2986
|
-
database: "sqlite"
|
|
3134
|
+
database: "sqlite",
|
|
3135
|
+
auth: { mode: "none" }
|
|
2987
3136
|
};
|
|
2988
3137
|
|
|
2989
3138
|
// src/registry/storage/sqlite.ts
|
|
@@ -3596,17 +3745,63 @@ var provisions2 = pgTable("provisions", {
|
|
|
3596
3745
|
// src/registry/storage/postgresql.ts
|
|
3597
3746
|
import { v4 as uuid6 } from "uuid";
|
|
3598
3747
|
var MIGRATIONS_FOLDER2 = PG_MIGRATIONS_FOLDER;
|
|
3748
|
+
var DatabaseConnectionError = class extends Error {
|
|
3749
|
+
constructor(message, postgresUrl, options) {
|
|
3750
|
+
super(message, options);
|
|
3751
|
+
this.postgresUrl = postgresUrl;
|
|
3752
|
+
this.name = "DatabaseConnectionError";
|
|
3753
|
+
}
|
|
3754
|
+
postgresUrl;
|
|
3755
|
+
};
|
|
3756
|
+
function isConnectionError(err) {
|
|
3757
|
+
if (typeof err !== "object" || err === null) return false;
|
|
3758
|
+
if ("code" in err) {
|
|
3759
|
+
const code = err.code;
|
|
3760
|
+
if (code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND") {
|
|
3761
|
+
return true;
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
if (err instanceof AggregateError) {
|
|
3765
|
+
if (err.errors.some(isConnectionError)) return true;
|
|
3766
|
+
}
|
|
3767
|
+
if ("cause" in err && err.cause) {
|
|
3768
|
+
return isConnectionError(err.cause);
|
|
3769
|
+
}
|
|
3770
|
+
return false;
|
|
3771
|
+
}
|
|
3772
|
+
function maskPostgresPassword(url) {
|
|
3773
|
+
try {
|
|
3774
|
+
const parsed = new URL(url);
|
|
3775
|
+
if (parsed.password) parsed.password = "***";
|
|
3776
|
+
return parsed.toString();
|
|
3777
|
+
} catch {
|
|
3778
|
+
return url;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3599
3781
|
var PostgreSQLSpecStore = class {
|
|
3600
3782
|
db;
|
|
3601
3783
|
pool;
|
|
3784
|
+
postgresUrl;
|
|
3602
3785
|
constructor(postgresUrl) {
|
|
3786
|
+
this.postgresUrl = postgresUrl;
|
|
3603
3787
|
this.pool = new Pool({ connectionString: postgresUrl });
|
|
3604
3788
|
this.db = drizzle2(this.pool);
|
|
3605
3789
|
}
|
|
3606
3790
|
async migrate() {
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3791
|
+
try {
|
|
3792
|
+
await migrate2(this.db, {
|
|
3793
|
+
migrationsFolder: MIGRATIONS_FOLDER2
|
|
3794
|
+
});
|
|
3795
|
+
} catch (err) {
|
|
3796
|
+
if (isConnectionError(err)) {
|
|
3797
|
+
throw new DatabaseConnectionError(
|
|
3798
|
+
`PostgreSQL is not reachable at ${maskPostgresPassword(this.postgresUrl)}`,
|
|
3799
|
+
this.postgresUrl,
|
|
3800
|
+
{ cause: err }
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
throw err;
|
|
3804
|
+
}
|
|
3610
3805
|
}
|
|
3611
3806
|
async end() {
|
|
3612
3807
|
await this.pool.end();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grapity/grapity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "grapity - API spec registry and compatibility guardian",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -66,26 +66,28 @@
|
|
|
66
66
|
"provenance": true
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"
|
|
70
|
-
"js-yaml": "^4.1.0",
|
|
71
|
-
"chalk": "^5.4.0",
|
|
72
|
-
"ora": "^8.2.0",
|
|
69
|
+
"@apidevtools/swagger-parser": "^10.1.0",
|
|
73
70
|
"@hono/node-server": "^2.0.4",
|
|
71
|
+
"better-sqlite3": "^12.10.0",
|
|
72
|
+
"chalk": "^5.4.0",
|
|
73
|
+
"commander": "^13.1.0",
|
|
74
|
+
"drizzle-orm": "^0.44.0",
|
|
74
75
|
"hono": "^4.12.22",
|
|
76
|
+
"jose": "^6.2.3",
|
|
77
|
+
"js-yaml": "^4.1.0",
|
|
75
78
|
"lucide-react": "^0.475.0",
|
|
79
|
+
"ora": "^8.2.0",
|
|
80
|
+
"pg": "^8.16.0",
|
|
76
81
|
"react": "^19.0.0",
|
|
77
82
|
"react-dom": "^19.0.0",
|
|
78
83
|
"react-router-dom": "^7.4.0",
|
|
79
84
|
"shiki": "^4.2.0",
|
|
80
|
-
"drizzle-orm": "^0.44.0",
|
|
81
|
-
"better-sqlite3": "^12.10.0",
|
|
82
|
-
"pg": "^8.16.0",
|
|
83
|
-
"@apidevtools/swagger-parser": "^10.1.0",
|
|
84
85
|
"uuid": "^11.1.0",
|
|
85
86
|
"zod": "^3.24.0"
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
88
89
|
"@tailwindcss/vite": "^4.3.0",
|
|
90
|
+
"@testcontainers/postgresql": "^11.14.0",
|
|
89
91
|
"@testing-library/dom": "^10.4.0",
|
|
90
92
|
"@testing-library/jest-dom": "^6.6.0",
|
|
91
93
|
"@testing-library/react": "^16.2.0",
|
|
@@ -103,10 +105,10 @@
|
|
|
103
105
|
"openapi-typescript": "^7.13.0",
|
|
104
106
|
"postcss": "^8.5.3",
|
|
105
107
|
"tailwindcss": "^4.0.0",
|
|
108
|
+
"testcontainers": "11.14.0",
|
|
106
109
|
"tsup": "^8.4.0",
|
|
107
110
|
"tsx": "^4.19.0",
|
|
108
111
|
"typescript": "^5.8.0",
|
|
109
|
-
"vite": "^6.3.0"
|
|
110
|
-
"@testcontainers/postgresql": "^11.14.0"
|
|
112
|
+
"vite": "^6.3.0"
|
|
111
113
|
}
|
|
112
114
|
}
|