@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 +413 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +585 -2
- package/dist/index.d.ts +585 -2
- package/dist/index.js +399 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|