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

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
@@ -559,18 +559,430 @@ function createApp(options) {
559
559
  };
560
560
  }
561
561
 
562
+ // src/events/client.ts
563
+ var EldrinEventClient = class {
564
+ appId;
565
+ coreApiUrl;
566
+ constructor(config) {
567
+ this.appId = config.appId;
568
+ this.coreApiUrl = config.coreApiUrl.replace(/\/$/, "");
569
+ }
570
+ /**
571
+ * Emit an event to the event bus
572
+ *
573
+ * @param type - Event type (must be declared in app's manifest emits)
574
+ * @param payload - Event payload data
575
+ * @param options - Optional emit configuration
576
+ * @returns Result containing the event ID
577
+ * @throws Error if app is not authorized to emit this event type
578
+ */
579
+ async emit(type, payload, options = {}) {
580
+ const response = await fetch(`${this.coreApiUrl}/api/events/emit`, {
581
+ method: "POST",
582
+ headers: {
583
+ "Content-Type": "application/json",
584
+ "X-Eldrin-App-Id": this.appId
585
+ },
586
+ body: JSON.stringify({
587
+ type,
588
+ payload,
589
+ version: options.version ?? 1,
590
+ idempotencyKey: options.idempotencyKey
591
+ })
592
+ });
593
+ if (!response.ok) {
594
+ const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
595
+ throw new Error(errorData.error || `Failed to emit event: ${response.status}`);
596
+ }
597
+ return response.json();
598
+ }
599
+ /**
600
+ * Poll for pending events
601
+ *
602
+ * @param options - Optional polling configuration
603
+ * @returns List of pending event deliveries
604
+ */
605
+ async poll(options = {}) {
606
+ const params = new URLSearchParams();
607
+ if (options.limit) params.set("limit", String(options.limit));
608
+ if (options.types?.length) params.set("types", options.types.join(","));
609
+ const url = `${this.coreApiUrl}/api/events/poll${params.toString() ? `?${params}` : ""}`;
610
+ const response = await fetch(url, {
611
+ headers: {
612
+ "X-Eldrin-App-Id": this.appId
613
+ }
614
+ });
615
+ if (!response.ok) {
616
+ throw new Error(`Failed to poll events: ${response.status}`);
617
+ }
618
+ return response.json();
619
+ }
620
+ /**
621
+ * Acknowledge successful event processing
622
+ *
623
+ * @param deliveryId - The delivery record ID to acknowledge
624
+ */
625
+ async ack(deliveryId) {
626
+ const response = await fetch(`${this.coreApiUrl}/api/events/ack`, {
627
+ method: "POST",
628
+ headers: {
629
+ "Content-Type": "application/json",
630
+ "X-Eldrin-App-Id": this.appId
631
+ },
632
+ body: JSON.stringify({ deliveryId })
633
+ });
634
+ if (!response.ok) {
635
+ throw new Error(`Failed to acknowledge event: ${response.status}`);
636
+ }
637
+ }
638
+ /**
639
+ * Negative acknowledge - event will be retried later
640
+ *
641
+ * @param deliveryId - The delivery record ID
642
+ * @param error - Optional error message for logging
643
+ */
644
+ async nack(deliveryId, error) {
645
+ const response = await fetch(`${this.coreApiUrl}/api/events/nack`, {
646
+ method: "POST",
647
+ headers: {
648
+ "Content-Type": "application/json",
649
+ "X-Eldrin-App-Id": this.appId
650
+ },
651
+ body: JSON.stringify({ deliveryId, error })
652
+ });
653
+ if (!response.ok) {
654
+ throw new Error(`Failed to nack event: ${response.status}`);
655
+ }
656
+ }
657
+ /**
658
+ * Get all registered event types
659
+ *
660
+ * @returns List of all registered event types across all apps
661
+ */
662
+ async getEventTypes() {
663
+ const response = await fetch(`${this.coreApiUrl}/api/events/types`, {
664
+ headers: {
665
+ "X-Eldrin-App-Id": this.appId
666
+ }
667
+ });
668
+ if (!response.ok) {
669
+ throw new Error(`Failed to get event types: ${response.status}`);
670
+ }
671
+ const data = await response.json();
672
+ return data.types;
673
+ }
674
+ /**
675
+ * Get subscriptions for this app
676
+ *
677
+ * @returns List of active subscriptions
678
+ */
679
+ async getSubscriptions() {
680
+ const response = await fetch(`${this.coreApiUrl}/api/events/subscriptions`, {
681
+ headers: {
682
+ "X-Eldrin-App-Id": this.appId
683
+ }
684
+ });
685
+ if (!response.ok) {
686
+ throw new Error(`Failed to get subscriptions: ${response.status}`);
687
+ }
688
+ const data = await response.json();
689
+ return data.subscriptions;
690
+ }
691
+ };
692
+ function createEventClient(env, appId) {
693
+ const coreApiUrl = env.ELDRIN_CORE_URL || "http://localhost:4000";
694
+ return new EldrinEventClient({ appId, coreApiUrl });
695
+ }
696
+
697
+ // src/auth/index.ts
698
+ var AUTH_HEADERS = {
699
+ USER_ID: "x-eldrin-user-id",
700
+ USER_EMAIL: "x-eldrin-user-email",
701
+ USER_NAME: "x-eldrin-user-name",
702
+ USER_ROLES: "x-eldrin-user-roles",
703
+ USER_PERMISSIONS: "x-eldrin-user-permissions"
704
+ };
705
+ function base64UrlDecode(str) {
706
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
707
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
708
+ const binary = atob(padded);
709
+ const bytes = new Uint8Array(binary.length);
710
+ for (let i = 0; i < binary.length; i++) {
711
+ bytes[i] = binary.charCodeAt(i);
712
+ }
713
+ return bytes;
714
+ }
715
+ async function verifyJWTSignature(token, secret) {
716
+ const parts = token.split(".");
717
+ if (parts.length !== 3) {
718
+ return { valid: false, error: "Invalid JWT format" };
719
+ }
720
+ const [headerB64, payloadB64, signatureB64] = parts;
721
+ try {
722
+ const encoder = new TextEncoder();
723
+ const keyData = encoder.encode(secret);
724
+ const key = await crypto.subtle.importKey(
725
+ "raw",
726
+ keyData,
727
+ { name: "HMAC", hash: "SHA-256" },
728
+ false,
729
+ ["verify"]
730
+ );
731
+ const signedData = encoder.encode(`${headerB64}.${payloadB64}`);
732
+ const signature = base64UrlDecode(signatureB64);
733
+ const isValid = await crypto.subtle.verify(
734
+ "HMAC",
735
+ key,
736
+ signature.buffer,
737
+ signedData
738
+ );
739
+ if (!isValid) {
740
+ return { valid: false, error: "Invalid signature" };
741
+ }
742
+ const payloadJson = new TextDecoder().decode(base64UrlDecode(payloadB64));
743
+ const payload = JSON.parse(payloadJson);
744
+ const now = Date.now();
745
+ if (payload.exp && payload.exp < now) {
746
+ return { valid: false, error: "Token expired" };
747
+ }
748
+ return { valid: true, payload };
749
+ } catch (error) {
750
+ return {
751
+ valid: false,
752
+ error: error instanceof Error ? error.message : "JWT verification failed"
753
+ };
754
+ }
755
+ }
756
+ function extractAppPermissions(payload, appId, developerId) {
757
+ const permissions = [];
758
+ for (const [key, perms] of Object.entries(payload.appPermissions)) {
759
+ if (developerId) {
760
+ if (key === `${developerId}:${appId}`) {
761
+ permissions.push(...perms);
762
+ }
763
+ } else {
764
+ if (key.endsWith(`:${appId}`)) {
765
+ permissions.push(...perms);
766
+ }
767
+ }
768
+ }
769
+ return permissions;
770
+ }
771
+ function jwtPayloadToAuthContext(payload, appId, developerId) {
772
+ return {
773
+ userId: payload.sub,
774
+ email: payload.email,
775
+ name: `${payload.firstName} ${payload.lastName}`.trim(),
776
+ platformRoles: payload.platformRoles,
777
+ permissions: extractAppPermissions(payload, appId, developerId)
778
+ };
779
+ }
780
+ async function verifyJWT(request, options) {
781
+ const authHeader = request.headers.get("Authorization");
782
+ if (!authHeader) {
783
+ return { success: false, error: "Missing Authorization header" };
784
+ }
785
+ if (!authHeader.startsWith("Bearer ")) {
786
+ return { success: false, error: "Invalid Authorization header format" };
787
+ }
788
+ const token = authHeader.slice(7);
789
+ const result = await verifyJWTSignature(token, options.secret);
790
+ if (!result.valid || !result.payload) {
791
+ return { success: false, error: result.error || "JWT verification failed" };
792
+ }
793
+ const auth = jwtPayloadToAuthContext(result.payload, options.appId, options.developerId);
794
+ return { success: true, auth };
795
+ }
796
+ async function getAuthContextFromJWT(request, options) {
797
+ const authHeader = request.headers.get("Authorization");
798
+ if (authHeader?.startsWith("Bearer ")) {
799
+ const result = await verifyJWT(request, options);
800
+ if (result.success) {
801
+ return result.auth;
802
+ }
803
+ return null;
804
+ }
805
+ return getAuthContext(request);
806
+ }
807
+ async function requireJWTAuth(request, options) {
808
+ const auth = await getAuthContextFromJWT(request, options);
809
+ if (!auth) {
810
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
811
+ status: 401,
812
+ headers: { "Content-Type": "application/json" }
813
+ });
814
+ }
815
+ return auth;
816
+ }
817
+ async function requireJWTPermission(request, options, resource, action) {
818
+ const auth = await requireJWTAuth(request, options);
819
+ if (auth instanceof Response) {
820
+ return auth;
821
+ }
822
+ if (isPlatformAdmin(auth)) {
823
+ return auth;
824
+ }
825
+ if (!hasPermission(auth, resource, action)) {
826
+ return new Response(
827
+ JSON.stringify({
828
+ error: "Forbidden",
829
+ message: `Missing permission: ${resource}:${action}`
830
+ }),
831
+ {
832
+ status: 403,
833
+ headers: { "Content-Type": "application/json" }
834
+ }
835
+ );
836
+ }
837
+ return auth;
838
+ }
839
+ function getAuthContext(request) {
840
+ const userId = request.headers.get(AUTH_HEADERS.USER_ID);
841
+ if (!userId) {
842
+ return null;
843
+ }
844
+ const email = request.headers.get(AUTH_HEADERS.USER_EMAIL) || "";
845
+ const name = request.headers.get(AUTH_HEADERS.USER_NAME) || "";
846
+ const rolesHeader = request.headers.get(AUTH_HEADERS.USER_ROLES) || "";
847
+ const permissionsHeader = request.headers.get(AUTH_HEADERS.USER_PERMISSIONS) || "";
848
+ return {
849
+ userId,
850
+ email,
851
+ name,
852
+ platformRoles: rolesHeader ? rolesHeader.split(",").map((r) => r.trim()) : [],
853
+ permissions: permissionsHeader ? permissionsHeader.split(",").map((p) => p.trim()) : []
854
+ };
855
+ }
856
+ function requireAuth(request) {
857
+ const auth = getAuthContext(request);
858
+ if (!auth) {
859
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
860
+ status: 401,
861
+ headers: { "Content-Type": "application/json" }
862
+ });
863
+ }
864
+ return auth;
865
+ }
866
+ function hasPermission(auth, resource, action) {
867
+ const required = `${resource}:${action}`;
868
+ if (auth.permissions.includes(required)) {
869
+ return true;
870
+ }
871
+ if (auth.permissions.includes(`${resource}:*`)) {
872
+ return true;
873
+ }
874
+ if (auth.permissions.includes("*:*")) {
875
+ return true;
876
+ }
877
+ return false;
878
+ }
879
+ function hasPlatformRole(auth, role) {
880
+ return auth.platformRoles.includes(role);
881
+ }
882
+ function isPlatformAdmin(auth) {
883
+ return auth.platformRoles.includes("admin");
884
+ }
885
+ function requirePermission(request, resource, action) {
886
+ const auth = requireAuth(request);
887
+ if (auth instanceof Response) {
888
+ return auth;
889
+ }
890
+ if (isPlatformAdmin(auth)) {
891
+ return auth;
892
+ }
893
+ if (!hasPermission(auth, resource, action)) {
894
+ return new Response(
895
+ JSON.stringify({
896
+ error: "Forbidden",
897
+ message: `Missing permission: ${resource}:${action}`
898
+ }),
899
+ {
900
+ status: 403,
901
+ headers: { "Content-Type": "application/json" }
902
+ }
903
+ );
904
+ }
905
+ return auth;
906
+ }
907
+ function requireAnyPermission(request, permissions) {
908
+ const auth = requireAuth(request);
909
+ if (auth instanceof Response) {
910
+ return auth;
911
+ }
912
+ if (isPlatformAdmin(auth)) {
913
+ return auth;
914
+ }
915
+ const hasAny = permissions.some(
916
+ ([resource, action]) => hasPermission(auth, resource, action)
917
+ );
918
+ if (!hasAny) {
919
+ const permList = permissions.map(([r, a]) => `${r}:${a}`).join(", ");
920
+ return new Response(
921
+ JSON.stringify({
922
+ error: "Forbidden",
923
+ message: `Missing one of permissions: ${permList}`
924
+ }),
925
+ {
926
+ status: 403,
927
+ headers: { "Content-Type": "application/json" }
928
+ }
929
+ );
930
+ }
931
+ return auth;
932
+ }
933
+ function requireAllPermissions(request, permissions) {
934
+ const auth = requireAuth(request);
935
+ if (auth instanceof Response) {
936
+ return auth;
937
+ }
938
+ if (isPlatformAdmin(auth)) {
939
+ return auth;
940
+ }
941
+ const missing = permissions.filter(
942
+ ([resource, action]) => !hasPermission(auth, resource, action)
943
+ );
944
+ if (missing.length > 0) {
945
+ const permList = missing.map(([r, a]) => `${r}:${a}`).join(", ");
946
+ return new Response(
947
+ JSON.stringify({
948
+ error: "Forbidden",
949
+ message: `Missing permissions: ${permList}`
950
+ }),
951
+ {
952
+ status: 403,
953
+ headers: { "Content-Type": "application/json" }
954
+ }
955
+ );
956
+ }
957
+ return auth;
958
+ }
959
+
960
+ exports.AUTH_HEADERS = AUTH_HEADERS;
562
961
  exports.CHECKSUM_PREFIX = CHECKSUM_PREFIX;
563
962
  exports.DatabaseProvider = DatabaseProvider;
963
+ exports.EldrinEventClient = EldrinEventClient;
564
964
  exports.calculateChecksum = calculateChecksum;
565
965
  exports.calculatePrefixedChecksum = calculatePrefixedChecksum;
566
966
  exports.createApp = createApp;
967
+ exports.createEventClient = createEventClient;
567
968
  exports.extractTimestamp = extractTimestamp;
568
969
  exports.generateMigrationManifest = generateMigrationManifest;
970
+ exports.getAuthContext = getAuthContext;
971
+ exports.getAuthContextFromJWT = getAuthContextFromJWT;
569
972
  exports.getMigrationStatus = getMigrationStatus;
570
973
  exports.getRollbackFilename = getRollbackFilename;
974
+ exports.hasPermission = hasPermission;
975
+ exports.hasPlatformRole = hasPlatformRole;
976
+ exports.isPlatformAdmin = isPlatformAdmin;
571
977
  exports.isValidMigrationFilename = isValidMigrationFilename;
572
978
  exports.isValidRollbackFilename = isValidRollbackFilename;
573
979
  exports.parseSQLStatements = parseSQLStatements;
980
+ exports.requireAllPermissions = requireAllPermissions;
981
+ exports.requireAnyPermission = requireAnyPermission;
982
+ exports.requireAuth = requireAuth;
983
+ exports.requireJWTAuth = requireJWTAuth;
984
+ exports.requireJWTPermission = requireJWTPermission;
985
+ exports.requirePermission = requirePermission;
574
986
  exports.rollbackMigrations = rollbackMigrations;
575
987
  exports.runMigrations = runMigrations;
576
988
  exports.useDatabase = useDatabase;
@@ -578,5 +990,6 @@ exports.useDatabaseContext = useDatabaseContext;
578
990
  exports.useMigrationsComplete = useMigrationsComplete;
579
991
  exports.validateMigrationManifest = validateMigrationManifest;
580
992
  exports.verifyChecksum = verifyChecksum;
993
+ exports.verifyJWT = verifyJWT;
581
994
  //# sourceMappingURL=index.cjs.map
582
995
  //# sourceMappingURL=index.cjs.map