@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 +208 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -168
- package/dist/index.d.ts +240 -168
- package/dist/index.js +205 -119
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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.
|
|
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;
|