@eldrin-project/eldrin-app-core 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -957,14 +957,220 @@ function requireAllPermissions(request, permissions) {
957
957
  return auth;
958
958
  }
959
959
 
960
+ // src/middleware/matcher.ts
961
+ function compileRoutes(routes, apiPrefix) {
962
+ return routes.map((route) => {
963
+ const methods = new Set(
964
+ Array.isArray(route.method) ? route.method.map((m) => m.toUpperCase()) : [route.method.toUpperCase()]
965
+ );
966
+ const fullPath = `${apiPrefix}${route.path}`;
967
+ const paramNames = [];
968
+ let regexPattern = fullPath.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
969
+ paramNames.push(name);
970
+ return "([^/]+)";
971
+ }).replace(/\*/g, "[^/]+");
972
+ const pattern = new RegExp(`^${regexPattern}$`);
973
+ let permission = null;
974
+ if (route.permission) {
975
+ const colonIndex = route.permission.indexOf(":");
976
+ if (colonIndex > 0) {
977
+ permission = {
978
+ resource: route.permission.substring(0, colonIndex),
979
+ action: route.permission.substring(colonIndex + 1)
980
+ };
981
+ }
982
+ }
983
+ return {
984
+ methods,
985
+ pattern,
986
+ paramNames,
987
+ permission,
988
+ originalPath: route.path
989
+ };
990
+ });
991
+ }
992
+ function matchRoute(method, pathname, compiledRoutes) {
993
+ const upperMethod = method.toUpperCase();
994
+ for (const route of compiledRoutes) {
995
+ if (!route.methods.has(upperMethod)) continue;
996
+ const match = pathname.match(route.pattern);
997
+ if (match) {
998
+ const params = {};
999
+ route.paramNames.forEach((name, index) => {
1000
+ params[name] = match[index + 1];
1001
+ });
1002
+ return { route, params };
1003
+ }
1004
+ }
1005
+ return null;
1006
+ }
1007
+ function isPublicRoute(pathname, publicRoutes, apiPrefix) {
1008
+ for (const publicPath of publicRoutes) {
1009
+ const fullPath = `${apiPrefix}${publicPath}`;
1010
+ if (pathname === fullPath) return true;
1011
+ if (publicPath.endsWith("/*")) {
1012
+ const prefix = `${apiPrefix}${publicPath.slice(0, -2)}`;
1013
+ if (pathname.startsWith(prefix + "/")) return true;
1014
+ }
1015
+ }
1016
+ return false;
1017
+ }
1018
+
1019
+ // src/middleware/index.ts
1020
+ function createPermissionMiddleware(config) {
1021
+ const apiPrefix = config.apiPrefix ?? "/api";
1022
+ const manifest = config.manifest;
1023
+ const api = manifest.api;
1024
+ const compiledRoutes = api?.routes ? compileRoutes(api.routes, apiPrefix) : [];
1025
+ const defaultPolicy = api?.defaultPolicy ?? "deny";
1026
+ const publicRoutes = api?.publicRoutes ?? [];
1027
+ const emptyAuth = {
1028
+ userId: "",
1029
+ email: "",
1030
+ name: "",
1031
+ platformRoles: [],
1032
+ permissions: []
1033
+ };
1034
+ function createCorsHeaders() {
1035
+ if (!config.cors) return {};
1036
+ return {
1037
+ "Access-Control-Allow-Origin": config.cors.allowOrigin,
1038
+ "Access-Control-Allow-Methods": config.cors.allowMethods,
1039
+ "Access-Control-Allow-Headers": config.cors.allowHeaders
1040
+ };
1041
+ }
1042
+ function withCors(response) {
1043
+ if (!config.cors) return response;
1044
+ const newHeaders = new Headers(response.headers);
1045
+ Object.entries(createCorsHeaders()).forEach(([key, value]) => {
1046
+ newHeaders.set(key, value);
1047
+ });
1048
+ return new Response(response.body, {
1049
+ status: response.status,
1050
+ statusText: response.statusText,
1051
+ headers: newHeaders
1052
+ });
1053
+ }
1054
+ function errorResponse(error) {
1055
+ if (config.onError) {
1056
+ return withCors(config.onError(error));
1057
+ }
1058
+ switch (error.type) {
1059
+ case "unauthorized":
1060
+ return withCors(
1061
+ Response.json(
1062
+ { error: "Unauthorized", message: error.message },
1063
+ { status: 401 }
1064
+ )
1065
+ );
1066
+ case "forbidden":
1067
+ return withCors(
1068
+ Response.json(
1069
+ {
1070
+ error: "Forbidden",
1071
+ message: error.message,
1072
+ permission: error.permission
1073
+ },
1074
+ { status: 403 }
1075
+ )
1076
+ );
1077
+ case "route_not_found":
1078
+ return withCors(
1079
+ Response.json(
1080
+ { error: "Not Found", message: error.message },
1081
+ { status: 404 }
1082
+ )
1083
+ );
1084
+ }
1085
+ }
1086
+ async function verifyAndGetAuth(request, env) {
1087
+ const jwtOptions = {
1088
+ secret: config.getSecret(env),
1089
+ appId: manifest.id,
1090
+ developerId: manifest.developer_id
1091
+ };
1092
+ const result = await verifyJWT(request, jwtOptions);
1093
+ if (!result.success) {
1094
+ return errorResponse({
1095
+ type: "unauthorized",
1096
+ message: result.error
1097
+ });
1098
+ }
1099
+ return result.auth;
1100
+ }
1101
+ return {
1102
+ async handle(request, env) {
1103
+ const url = new URL(request.url);
1104
+ const method = request.method;
1105
+ const pathname = url.pathname;
1106
+ if (config.skipRoutes) {
1107
+ for (const skipPattern of config.skipRoutes) {
1108
+ if (pathname.startsWith(skipPattern)) {
1109
+ return { auth: emptyAuth, url, params: {} };
1110
+ }
1111
+ }
1112
+ }
1113
+ if (method === "OPTIONS") {
1114
+ return {
1115
+ response: new Response(null, {
1116
+ status: 204,
1117
+ headers: createCorsHeaders()
1118
+ })
1119
+ };
1120
+ }
1121
+ if (!pathname.startsWith(apiPrefix)) {
1122
+ return { auth: emptyAuth, url, params: {} };
1123
+ }
1124
+ if (isPublicRoute(pathname, publicRoutes, apiPrefix)) {
1125
+ return { auth: emptyAuth, url, params: {} };
1126
+ }
1127
+ const match = matchRoute(method, pathname, compiledRoutes);
1128
+ if (!match) {
1129
+ if (defaultPolicy === "allow") {
1130
+ return { auth: emptyAuth, url, params: {} };
1131
+ }
1132
+ const authResult2 = await verifyAndGetAuth(request, env);
1133
+ if (authResult2 instanceof Response) {
1134
+ return { response: authResult2 };
1135
+ }
1136
+ return { auth: authResult2, url, params: {} };
1137
+ }
1138
+ if (match.route.permission === null) {
1139
+ return { auth: emptyAuth, url, params: match.params };
1140
+ }
1141
+ const authResult = await verifyAndGetAuth(request, env);
1142
+ if (authResult instanceof Response) {
1143
+ return { response: authResult };
1144
+ }
1145
+ const auth = authResult;
1146
+ if (isPlatformAdmin(auth)) {
1147
+ return { auth, url, params: match.params };
1148
+ }
1149
+ const { resource, action } = match.route.permission;
1150
+ if (!hasPermission(auth, resource, action)) {
1151
+ return {
1152
+ response: errorResponse({
1153
+ type: "forbidden",
1154
+ message: `Missing permission: ${resource}:${action}`,
1155
+ permission: `${resource}:${action}`
1156
+ })
1157
+ };
1158
+ }
1159
+ return { auth, url, params: match.params };
1160
+ }
1161
+ };
1162
+ }
1163
+
960
1164
  exports.AUTH_HEADERS = AUTH_HEADERS;
961
1165
  exports.CHECKSUM_PREFIX = CHECKSUM_PREFIX;
962
1166
  exports.DatabaseProvider = DatabaseProvider;
963
1167
  exports.EldrinEventClient = EldrinEventClient;
964
1168
  exports.calculateChecksum = calculateChecksum;
965
1169
  exports.calculatePrefixedChecksum = calculatePrefixedChecksum;
1170
+ exports.compileRoutes = compileRoutes;
966
1171
  exports.createApp = createApp;
967
1172
  exports.createEventClient = createEventClient;
1173
+ exports.createPermissionMiddleware = createPermissionMiddleware;
968
1174
  exports.extractTimestamp = extractTimestamp;
969
1175
  exports.generateMigrationManifest = generateMigrationManifest;
970
1176
  exports.getAuthContext = getAuthContext;
@@ -974,8 +1180,10 @@ exports.getRollbackFilename = getRollbackFilename;
974
1180
  exports.hasPermission = hasPermission;
975
1181
  exports.hasPlatformRole = hasPlatformRole;
976
1182
  exports.isPlatformAdmin = isPlatformAdmin;
1183
+ exports.isPublicRoute = isPublicRoute;
977
1184
  exports.isValidMigrationFilename = isValidMigrationFilename;
978
1185
  exports.isValidRollbackFilename = isValidRollbackFilename;
1186
+ exports.matchRoute = matchRoute;
979
1187
  exports.parseSQLStatements = parseSQLStatements;
980
1188
  exports.requireAllPermissions = requireAllPermissions;
981
1189
  exports.requireAnyPermission = requireAnyPermission;