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

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
@@ -1,12 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var react = require('react');
4
3
  var promises = require('fs/promises');
5
4
  var fs = require('fs');
6
5
  var path = require('path');
7
- var jsxRuntime = require('react/jsx-runtime');
8
-
9
- // src/app/createApp.tsx
10
6
 
11
7
  // src/migrations/checksum.ts
12
8
  var CHECKSUM_PREFIX = "sha256:";
@@ -444,120 +440,6 @@ async function validateMigrationManifest(manifest, files) {
444
440
  errors
445
441
  };
446
442
  }
447
- var EldrinDatabaseContext = react.createContext(null);
448
- function DatabaseProvider({
449
- db,
450
- migrationsComplete,
451
- migrationResult,
452
- children
453
- }) {
454
- const value = {
455
- db,
456
- migrationsComplete,
457
- migrationResult
458
- };
459
- return /* @__PURE__ */ jsxRuntime.jsx(EldrinDatabaseContext.Provider, { value, children });
460
- }
461
- function useDatabase() {
462
- const context = react.useContext(EldrinDatabaseContext);
463
- if (context === null) {
464
- throw new Error("useDatabase must be used within a DatabaseProvider (via createApp)");
465
- }
466
- return context.db;
467
- }
468
- function useDatabaseContext() {
469
- const context = react.useContext(EldrinDatabaseContext);
470
- if (context === null) {
471
- throw new Error("useDatabaseContext must be used within a DatabaseProvider (via createApp)");
472
- }
473
- return context;
474
- }
475
- function useMigrationsComplete() {
476
- const context = react.useContext(EldrinDatabaseContext);
477
- return context?.migrationsComplete ?? false;
478
- }
479
-
480
- // src/app/createApp.tsx
481
- function createApp(options) {
482
- const { name, root: RootComponent, migrations = [], onMigrationsComplete, onMigrationError } = options;
483
- const state = {
484
- db: null,
485
- migrationsComplete: false,
486
- migrationResult: void 0,
487
- mountedElement: void 0,
488
- reactRoot: void 0
489
- };
490
- async function bootstrap(props) {
491
- const db = props.db ?? null;
492
- state.db = db;
493
- if (db && migrations.length > 0) {
494
- try {
495
- const result = await runMigrations(db, {
496
- migrations,
497
- onLog: (message, level) => {
498
- const prefix = `[${name}]`;
499
- if (level === "error") {
500
- console.error(prefix, message);
501
- } else if (level === "warn") {
502
- console.warn(prefix, message);
503
- } else {
504
- console.log(prefix, message);
505
- }
506
- }
507
- });
508
- state.migrationResult = result;
509
- if (result.success) {
510
- state.migrationsComplete = true;
511
- onMigrationsComplete?.(result);
512
- } else {
513
- const error = new Error(result.error?.message ?? "Migration failed");
514
- onMigrationError?.(error);
515
- throw error;
516
- }
517
- } catch (error) {
518
- state.migrationsComplete = false;
519
- const err = error instanceof Error ? error : new Error(String(error));
520
- onMigrationError?.(err);
521
- throw err;
522
- }
523
- } else {
524
- state.migrationsComplete = true;
525
- }
526
- }
527
- async function mount(props) {
528
- const domElement = props.domElement ?? document.getElementById(`app-${name}`);
529
- if (!domElement) {
530
- throw new Error(`No DOM element found for app "${name}". Expected element with id="app-${name}" or domElement prop.`);
531
- }
532
- state.mountedElement = domElement;
533
- const rootElement = react.createElement(RootComponent, props.customProps ?? {});
534
- const appElement = react.createElement(DatabaseProvider, {
535
- db: state.db,
536
- migrationsComplete: state.migrationsComplete,
537
- migrationResult: state.migrationResult,
538
- children: rootElement
539
- });
540
- const ReactDOM = await import('react-dom/client');
541
- const root = ReactDOM.createRoot(domElement);
542
- root.render(appElement);
543
- state.reactRoot = root;
544
- }
545
- async function unmount(_props) {
546
- if (state.reactRoot) {
547
- state.reactRoot.unmount();
548
- state.reactRoot = void 0;
549
- }
550
- if (state.mountedElement) {
551
- state.mountedElement.innerHTML = "";
552
- state.mountedElement = void 0;
553
- }
554
- }
555
- return {
556
- bootstrap,
557
- mount,
558
- unmount
559
- };
560
- }
561
443
 
562
444
  // src/events/client.ts
563
445
  var EldrinEventClient = class {
@@ -957,14 +839,218 @@ function requireAllPermissions(request, permissions) {
957
839
  return auth;
958
840
  }
959
841
 
842
+ // src/middleware/matcher.ts
843
+ function compileRoutes(routes, apiPrefix) {
844
+ return routes.map((route) => {
845
+ const methods = new Set(
846
+ Array.isArray(route.method) ? route.method.map((m) => m.toUpperCase()) : [route.method.toUpperCase()]
847
+ );
848
+ const fullPath = `${apiPrefix}${route.path}`;
849
+ const paramNames = [];
850
+ let regexPattern = fullPath.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
851
+ paramNames.push(name);
852
+ return "([^/]+)";
853
+ }).replace(/\*/g, "[^/]+");
854
+ const pattern = new RegExp(`^${regexPattern}$`);
855
+ let permission = null;
856
+ if (route.permission) {
857
+ const colonIndex = route.permission.indexOf(":");
858
+ if (colonIndex > 0) {
859
+ permission = {
860
+ resource: route.permission.substring(0, colonIndex),
861
+ action: route.permission.substring(colonIndex + 1)
862
+ };
863
+ }
864
+ }
865
+ return {
866
+ methods,
867
+ pattern,
868
+ paramNames,
869
+ permission,
870
+ originalPath: route.path
871
+ };
872
+ });
873
+ }
874
+ function matchRoute(method, pathname, compiledRoutes) {
875
+ const upperMethod = method.toUpperCase();
876
+ for (const route of compiledRoutes) {
877
+ if (!route.methods.has(upperMethod)) continue;
878
+ const match = pathname.match(route.pattern);
879
+ if (match) {
880
+ const params = {};
881
+ route.paramNames.forEach((name, index) => {
882
+ params[name] = match[index + 1];
883
+ });
884
+ return { route, params };
885
+ }
886
+ }
887
+ return null;
888
+ }
889
+ function isPublicRoute(pathname, publicRoutes, apiPrefix) {
890
+ for (const publicPath of publicRoutes) {
891
+ const fullPath = `${apiPrefix}${publicPath}`;
892
+ if (pathname === fullPath) return true;
893
+ if (publicPath.endsWith("/*")) {
894
+ const prefix = `${apiPrefix}${publicPath.slice(0, -2)}`;
895
+ if (pathname.startsWith(prefix + "/")) return true;
896
+ }
897
+ }
898
+ return false;
899
+ }
900
+
901
+ // src/middleware/index.ts
902
+ function createPermissionMiddleware(config) {
903
+ const apiPrefix = config.apiPrefix ?? "/api";
904
+ const manifest = config.manifest;
905
+ const api = manifest.api;
906
+ const compiledRoutes = api?.routes ? compileRoutes(api.routes, apiPrefix) : [];
907
+ const defaultPolicy = api?.defaultPolicy ?? "deny";
908
+ const publicRoutes = api?.publicRoutes ?? [];
909
+ const emptyAuth = {
910
+ userId: "",
911
+ email: "",
912
+ name: "",
913
+ platformRoles: [],
914
+ permissions: []
915
+ };
916
+ function createCorsHeaders() {
917
+ if (!config.cors) return {};
918
+ return {
919
+ "Access-Control-Allow-Origin": config.cors.allowOrigin,
920
+ "Access-Control-Allow-Methods": config.cors.allowMethods,
921
+ "Access-Control-Allow-Headers": config.cors.allowHeaders
922
+ };
923
+ }
924
+ function withCors(response) {
925
+ if (!config.cors) return response;
926
+ const newHeaders = new Headers(response.headers);
927
+ Object.entries(createCorsHeaders()).forEach(([key, value]) => {
928
+ newHeaders.set(key, value);
929
+ });
930
+ return new Response(response.body, {
931
+ status: response.status,
932
+ statusText: response.statusText,
933
+ headers: newHeaders
934
+ });
935
+ }
936
+ function errorResponse(error) {
937
+ if (config.onError) {
938
+ return withCors(config.onError(error));
939
+ }
940
+ switch (error.type) {
941
+ case "unauthorized":
942
+ return withCors(
943
+ Response.json(
944
+ { error: "Unauthorized", message: error.message },
945
+ { status: 401 }
946
+ )
947
+ );
948
+ case "forbidden":
949
+ return withCors(
950
+ Response.json(
951
+ {
952
+ error: "Forbidden",
953
+ message: error.message,
954
+ permission: error.permission
955
+ },
956
+ { status: 403 }
957
+ )
958
+ );
959
+ case "route_not_found":
960
+ return withCors(
961
+ Response.json(
962
+ { error: "Not Found", message: error.message },
963
+ { status: 404 }
964
+ )
965
+ );
966
+ }
967
+ }
968
+ async function verifyAndGetAuth(request, env) {
969
+ const jwtOptions = {
970
+ secret: config.getSecret(env),
971
+ appId: manifest.id,
972
+ developerId: manifest.developer_id
973
+ };
974
+ const result = await verifyJWT(request, jwtOptions);
975
+ if (!result.success) {
976
+ return errorResponse({
977
+ type: "unauthorized",
978
+ message: result.error
979
+ });
980
+ }
981
+ return result.auth;
982
+ }
983
+ return {
984
+ async handle(request, env) {
985
+ const url = new URL(request.url);
986
+ const method = request.method;
987
+ const pathname = url.pathname;
988
+ if (config.skipRoutes) {
989
+ for (const skipPattern of config.skipRoutes) {
990
+ if (pathname.startsWith(skipPattern)) {
991
+ return { auth: emptyAuth, url, params: {} };
992
+ }
993
+ }
994
+ }
995
+ if (method === "OPTIONS") {
996
+ return {
997
+ response: new Response(null, {
998
+ status: 204,
999
+ headers: createCorsHeaders()
1000
+ })
1001
+ };
1002
+ }
1003
+ if (!pathname.startsWith(apiPrefix)) {
1004
+ return { auth: emptyAuth, url, params: {} };
1005
+ }
1006
+ if (isPublicRoute(pathname, publicRoutes, apiPrefix)) {
1007
+ return { auth: emptyAuth, url, params: {} };
1008
+ }
1009
+ const match = matchRoute(method, pathname, compiledRoutes);
1010
+ if (!match) {
1011
+ if (defaultPolicy === "allow") {
1012
+ return { auth: emptyAuth, url, params: {} };
1013
+ }
1014
+ const authResult2 = await verifyAndGetAuth(request, env);
1015
+ if (authResult2 instanceof Response) {
1016
+ return { response: authResult2 };
1017
+ }
1018
+ return { auth: authResult2, url, params: {} };
1019
+ }
1020
+ if (match.route.permission === null) {
1021
+ return { auth: emptyAuth, url, params: match.params };
1022
+ }
1023
+ const authResult = await verifyAndGetAuth(request, env);
1024
+ if (authResult instanceof Response) {
1025
+ return { response: authResult };
1026
+ }
1027
+ const auth = authResult;
1028
+ if (isPlatformAdmin(auth)) {
1029
+ return { auth, url, params: match.params };
1030
+ }
1031
+ const { resource, action } = match.route.permission;
1032
+ if (!hasPermission(auth, resource, action)) {
1033
+ return {
1034
+ response: errorResponse({
1035
+ type: "forbidden",
1036
+ message: `Missing permission: ${resource}:${action}`,
1037
+ permission: `${resource}:${action}`
1038
+ })
1039
+ };
1040
+ }
1041
+ return { auth, url, params: match.params };
1042
+ }
1043
+ };
1044
+ }
1045
+
960
1046
  exports.AUTH_HEADERS = AUTH_HEADERS;
961
1047
  exports.CHECKSUM_PREFIX = CHECKSUM_PREFIX;
962
- exports.DatabaseProvider = DatabaseProvider;
963
1048
  exports.EldrinEventClient = EldrinEventClient;
964
1049
  exports.calculateChecksum = calculateChecksum;
965
1050
  exports.calculatePrefixedChecksum = calculatePrefixedChecksum;
966
- exports.createApp = createApp;
1051
+ exports.compileRoutes = compileRoutes;
967
1052
  exports.createEventClient = createEventClient;
1053
+ exports.createPermissionMiddleware = createPermissionMiddleware;
968
1054
  exports.extractTimestamp = extractTimestamp;
969
1055
  exports.generateMigrationManifest = generateMigrationManifest;
970
1056
  exports.getAuthContext = getAuthContext;
@@ -974,8 +1060,10 @@ exports.getRollbackFilename = getRollbackFilename;
974
1060
  exports.hasPermission = hasPermission;
975
1061
  exports.hasPlatformRole = hasPlatformRole;
976
1062
  exports.isPlatformAdmin = isPlatformAdmin;
1063
+ exports.isPublicRoute = isPublicRoute;
977
1064
  exports.isValidMigrationFilename = isValidMigrationFilename;
978
1065
  exports.isValidRollbackFilename = isValidRollbackFilename;
1066
+ exports.matchRoute = matchRoute;
979
1067
  exports.parseSQLStatements = parseSQLStatements;
980
1068
  exports.requireAllPermissions = requireAllPermissions;
981
1069
  exports.requireAnyPermission = requireAnyPermission;
@@ -985,9 +1073,6 @@ exports.requireJWTPermission = requireJWTPermission;
985
1073
  exports.requirePermission = requirePermission;
986
1074
  exports.rollbackMigrations = rollbackMigrations;
987
1075
  exports.runMigrations = runMigrations;
988
- exports.useDatabase = useDatabase;
989
- exports.useDatabaseContext = useDatabaseContext;
990
- exports.useMigrationsComplete = useMigrationsComplete;
991
1076
  exports.validateMigrationManifest = validateMigrationManifest;
992
1077
  exports.verifyChecksum = verifyChecksum;
993
1078
  exports.verifyJWT = verifyJWT;