@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.d.cts CHANGED
@@ -56,20 +56,94 @@ interface MigrationOptions {
56
56
  /** Callback for logging */
57
57
  onLog?: (message: string, level: 'info' | 'warn' | 'error') => void;
58
58
  }
59
+ /**
60
+ * Permission declaration in app manifest
61
+ */
62
+ interface ManifestPermission {
63
+ /** Resource name (e.g., "invoices", "customers") */
64
+ resource: string;
65
+ /** Allowed actions on this resource */
66
+ actions: string[];
67
+ }
68
+ /**
69
+ * Group/role declaration in app manifest
70
+ */
71
+ interface ManifestGroup {
72
+ /** Group identifier (e.g., "admin", "viewer") */
73
+ id: string;
74
+ /** Display name for the group */
75
+ name: string;
76
+ /** Optional description */
77
+ description?: string;
78
+ /** Permissions granted to this group (e.g., ["invoices:read", "invoices:*"]) */
79
+ permissions: string[];
80
+ }
59
81
  /**
60
82
  * App configuration from eldrin-app.manifest.json
61
83
  */
62
84
  interface AppManifest {
85
+ /** App identifier (unique within developer namespace) */
63
86
  name: string;
87
+ /** App version (semver) */
64
88
  version: string;
89
+ /** Developer ID (namespace owner) */
90
+ developerId?: string;
91
+ /** Display name shown in UI */
65
92
  displayName?: string;
93
+ /** Short description */
66
94
  description?: string;
67
- permissions?: string[];
95
+ /** Icon URL or path */
96
+ icon?: string;
97
+ /** Webhook hooks configuration */
68
98
  hooks?: Record<string, string>;
99
+ /** Event system configuration */
69
100
  events?: {
101
+ /** Event types this app emits */
70
102
  emits?: string[];
103
+ /** Event types this app subscribes to */
71
104
  subscribes?: string[];
72
105
  };
106
+ /**
107
+ * Permission declarations for this app
108
+ *
109
+ * Defines the resources and actions that can be assigned to users.
110
+ * Full permission format: developer_id:app_id:resource:action
111
+ *
112
+ * @example
113
+ * ```json
114
+ * {
115
+ * "permissions": [
116
+ * { "resource": "invoices", "actions": ["read", "write", "delete"] },
117
+ * { "resource": "customers", "actions": ["read", "write"] }
118
+ * ]
119
+ * }
120
+ * ```
121
+ */
122
+ permissions?: ManifestPermission[];
123
+ /**
124
+ * Group/role definitions for this app
125
+ *
126
+ * Groups bundle permissions together for easier assignment.
127
+ *
128
+ * @example
129
+ * ```json
130
+ * {
131
+ * "groups": [
132
+ * {
133
+ * "id": "admin",
134
+ * "name": "App Admin",
135
+ * "permissions": ["invoices:*", "customers:*"]
136
+ * },
137
+ * {
138
+ * "id": "viewer",
139
+ * "name": "Viewer",
140
+ * "permissions": ["invoices:read", "customers:read"]
141
+ * }
142
+ * ]
143
+ * }
144
+ * ```
145
+ */
146
+ groups?: ManifestGroup[];
73
147
  }
74
148
  /**
75
149
  * Options for createApp factory
@@ -482,4 +556,513 @@ declare function validateMigrationManifest(manifest: MigrationManifest, files: M
482
556
  errors: string[];
483
557
  }>;
484
558
 
485
- export { type AppLifecycle, type AppManifest, CHECKSUM_PREFIX, type CreateAppOptions, type DatabaseContext, DatabaseProvider, type DatabaseProviderProps, type EldrinContext, type GenerateMigrationManifestOptions, type GenerateMigrationManifestResult, type LifecycleProps, type MigrationFile, type MigrationFiles, type MigrationManifest, type MigrationManifestEntry, type MigrationOptions, type MigrationRecord, type MigrationResult, type RollbackResult, calculateChecksum, calculatePrefixedChecksum, createApp, extractTimestamp, generateMigrationManifest, getMigrationStatus, getRollbackFilename, isValidMigrationFilename, isValidRollbackFilename, parseSQLStatements, rollbackMigrations, runMigrations, useDatabase, useDatabaseContext, useMigrationsComplete, validateMigrationManifest, verifyChecksum };
559
+ /**
560
+ * Event System Types
561
+ * Types for server-side inter-app event communication
562
+ */
563
+ /**
564
+ * Base event structure
565
+ */
566
+ interface EldrinEvent<T = unknown> {
567
+ /** Unique event ID (UUID) */
568
+ id: string;
569
+ /** Event type identifier (e.g., "invoice.created") */
570
+ type: string;
571
+ /** App ID that emitted the event */
572
+ source: string;
573
+ /** Event payload data */
574
+ payload: T;
575
+ /** Payload schema version */
576
+ version: number;
577
+ /** Unix timestamp (ms) when event was created */
578
+ timestamp: number;
579
+ /** Optional idempotency key for deduplication */
580
+ idempotencyKey?: string;
581
+ }
582
+ /**
583
+ * Options for emitting events
584
+ */
585
+ interface EmitOptions {
586
+ /** Optional key to prevent duplicate events */
587
+ idempotencyKey?: string;
588
+ /** Payload schema version (default: 1) */
589
+ version?: number;
590
+ }
591
+ /**
592
+ * Result from emitting an event
593
+ */
594
+ interface EmitResult {
595
+ /** ID of the created event */
596
+ eventId: string;
597
+ /** Event type */
598
+ type: string;
599
+ /** True if event was deduplicated (already existed) */
600
+ deduplicated?: boolean;
601
+ }
602
+ /**
603
+ * Event delivery record
604
+ */
605
+ interface EventDelivery<T = unknown> {
606
+ /** Delivery record ID */
607
+ id: number;
608
+ /** The event being delivered */
609
+ event: EldrinEvent<T>;
610
+ /** Delivery status */
611
+ status: 'pending' | 'delivered' | 'failed';
612
+ /** Number of delivery attempts */
613
+ attempts: number;
614
+ /** Timestamp of last attempt */
615
+ lastAttemptAt?: number;
616
+ }
617
+ /**
618
+ * Result from polling events
619
+ */
620
+ interface PollResult<T = unknown> {
621
+ /** List of pending event deliveries */
622
+ events: EventDelivery<T>[];
623
+ /** True if there are more events to fetch */
624
+ hasMore: boolean;
625
+ }
626
+ /**
627
+ * Options for polling events
628
+ */
629
+ interface PollOptions {
630
+ /** Maximum number of events to fetch (default: 10, max: 100) */
631
+ limit?: number;
632
+ /** Only fetch events of these types */
633
+ types?: string[];
634
+ }
635
+ /**
636
+ * Configuration for the event client
637
+ */
638
+ interface EventClientConfig {
639
+ /** App ID for this client */
640
+ appId: string;
641
+ /** Base URL of the Eldrin Core worker */
642
+ coreApiUrl: string;
643
+ }
644
+ /**
645
+ * Event handler function type
646
+ */
647
+ type EventHandler<T = unknown> = (event: EldrinEvent<T>) => Promise<void> | void;
648
+ /**
649
+ * Subscription info returned from the API
650
+ */
651
+ interface SubscriptionInfo {
652
+ /** Subscription ID */
653
+ id: number;
654
+ /** Event pattern (e.g., "invoice.*") */
655
+ pattern: string;
656
+ /** Delivery mode */
657
+ deliveryMode: 'poll' | 'push';
658
+ /** Whether subscription is enabled */
659
+ enabled: boolean;
660
+ }
661
+ /**
662
+ * Registered event type info
663
+ */
664
+ interface RegisteredEventType {
665
+ /** Event type identifier */
666
+ type: string;
667
+ /** App that can emit this event */
668
+ sourceApp: string;
669
+ /** Human-readable description */
670
+ description?: string;
671
+ }
672
+
673
+ /**
674
+ * Eldrin Event Client
675
+ * SDK for app workers to emit and consume events
676
+ */
677
+
678
+ /**
679
+ * Client for interacting with the Eldrin event bus
680
+ *
681
+ * @example
682
+ * ```typescript
683
+ * const events = new EldrinEventClient({
684
+ * appId: 'invoicing',
685
+ * coreApiUrl: 'https://your-eldrin-core.workers.dev'
686
+ * });
687
+ *
688
+ * // Emit an event
689
+ * await events.emit('invoice.created', { invoiceId: 'inv_123', total: 1500 });
690
+ *
691
+ * // Poll for events
692
+ * const pending = await events.poll();
693
+ * for (const delivery of pending.events) {
694
+ * await processEvent(delivery.event);
695
+ * await events.ack(delivery.id);
696
+ * }
697
+ * ```
698
+ */
699
+ declare class EldrinEventClient {
700
+ private readonly appId;
701
+ private readonly coreApiUrl;
702
+ constructor(config: EventClientConfig);
703
+ /**
704
+ * Emit an event to the event bus
705
+ *
706
+ * @param type - Event type (must be declared in app's manifest emits)
707
+ * @param payload - Event payload data
708
+ * @param options - Optional emit configuration
709
+ * @returns Result containing the event ID
710
+ * @throws Error if app is not authorized to emit this event type
711
+ */
712
+ emit<T>(type: string, payload: T, options?: EmitOptions): Promise<EmitResult>;
713
+ /**
714
+ * Poll for pending events
715
+ *
716
+ * @param options - Optional polling configuration
717
+ * @returns List of pending event deliveries
718
+ */
719
+ poll<T = unknown>(options?: PollOptions): Promise<PollResult<T>>;
720
+ /**
721
+ * Acknowledge successful event processing
722
+ *
723
+ * @param deliveryId - The delivery record ID to acknowledge
724
+ */
725
+ ack(deliveryId: number): Promise<void>;
726
+ /**
727
+ * Negative acknowledge - event will be retried later
728
+ *
729
+ * @param deliveryId - The delivery record ID
730
+ * @param error - Optional error message for logging
731
+ */
732
+ nack(deliveryId: number, error?: string): Promise<void>;
733
+ /**
734
+ * Get all registered event types
735
+ *
736
+ * @returns List of all registered event types across all apps
737
+ */
738
+ getEventTypes(): Promise<RegisteredEventType[]>;
739
+ /**
740
+ * Get subscriptions for this app
741
+ *
742
+ * @returns List of active subscriptions
743
+ */
744
+ getSubscriptions(): Promise<SubscriptionInfo[]>;
745
+ }
746
+ /**
747
+ * Environment bindings that may include Eldrin Core URL
748
+ */
749
+ interface EldrinEnv {
750
+ ELDRIN_CORE_URL?: string;
751
+ [key: string]: unknown;
752
+ }
753
+ /**
754
+ * Create an event client for use in app workers
755
+ *
756
+ * @param env - Worker environment bindings (must have optional ELDRIN_CORE_URL)
757
+ * @param appId - The app's identifier
758
+ * @returns Configured event client
759
+ *
760
+ * @example
761
+ * ```typescript
762
+ * export default {
763
+ * async fetch(request: Request, env: Env) {
764
+ * const events = createEventClient(env, 'invoicing');
765
+ * await events.emit('invoice.created', { invoiceId: 'inv_123' });
766
+ * }
767
+ * };
768
+ * ```
769
+ */
770
+ declare function createEventClient(env: EldrinEnv, appId: string): EldrinEventClient;
771
+
772
+ /**
773
+ * Auth utilities for Eldrin apps
774
+ *
775
+ * Supports two authentication methods:
776
+ *
777
+ * 1. JWT Token (Authorization header):
778
+ * - Direct API calls with JWT token in Authorization: Bearer <token>
779
+ * - JWT is verified using shared secret (JWT_SECRET env var)
780
+ * - Permissions extracted from JWT payload
781
+ *
782
+ * 2. Proxy Headers (X-Eldrin-User-* headers):
783
+ * - Requests proxied through eldrin-core shell
784
+ * - Shell verifies JWT and injects user context headers
785
+ * - Legacy support for proxied requests
786
+ *
787
+ * JWT takes precedence if Authorization header is present.
788
+ */
789
+ /**
790
+ * Auth context extracted from JWT or shell-injected headers
791
+ */
792
+ interface AppAuthContext {
793
+ /** User ID */
794
+ userId: string;
795
+ /** User email */
796
+ email: string;
797
+ /** User display name */
798
+ name: string;
799
+ /** Platform roles (admin, editor, viewer) */
800
+ platformRoles: string[];
801
+ /** App-specific permissions (e.g., ["invoices:read", "invoices:write"]) */
802
+ permissions: string[];
803
+ }
804
+ /**
805
+ * JWT payload structure (matches eldrin-core JWT format)
806
+ */
807
+ interface JWTPayload {
808
+ /** User ID */
809
+ sub: string;
810
+ /** Email */
811
+ email: string;
812
+ /** First name */
813
+ firstName: string;
814
+ /** Last name */
815
+ lastName: string;
816
+ /** Platform role names */
817
+ platformRoles: string[];
818
+ /** Platform permission names */
819
+ platformPermissions: string[];
820
+ /**
821
+ * App permissions in compact format
822
+ * Key: "developer_id:app_id", Value: ["resource:action", ...]
823
+ */
824
+ appPermissions: Record<string, string[]>;
825
+ /** Issued at (timestamp) */
826
+ iat: number;
827
+ /** Expiration (timestamp) */
828
+ exp: number;
829
+ }
830
+ /**
831
+ * Options for JWT verification
832
+ */
833
+ interface JWTVerifyOptions {
834
+ /** JWT secret key (from env.JWT_SECRET) */
835
+ secret: string;
836
+ /** App ID for extracting app-specific permissions */
837
+ appId: string;
838
+ /** Developer ID (optional, uses wildcard matching if not provided) */
839
+ developerId?: string;
840
+ }
841
+ /**
842
+ * Header names used by the shell for auth context injection
843
+ */
844
+ declare const AUTH_HEADERS: {
845
+ readonly USER_ID: "x-eldrin-user-id";
846
+ readonly USER_EMAIL: "x-eldrin-user-email";
847
+ readonly USER_NAME: "x-eldrin-user-name";
848
+ readonly USER_ROLES: "x-eldrin-user-roles";
849
+ readonly USER_PERMISSIONS: "x-eldrin-user-permissions";
850
+ };
851
+ /**
852
+ * Verify JWT token and extract auth context
853
+ *
854
+ * @example
855
+ * ```typescript
856
+ * export default {
857
+ * async fetch(request: Request, env: Env) {
858
+ * const result = await verifyJWT(request, {
859
+ * secret: env.JWT_SECRET,
860
+ * appId: 'react-todo',
861
+ * });
862
+ *
863
+ * if (!result.success) {
864
+ * return new Response(JSON.stringify({ error: result.error }), {
865
+ * status: 401,
866
+ * headers: { 'Content-Type': 'application/json' },
867
+ * });
868
+ * }
869
+ *
870
+ * // result.auth contains the AppAuthContext
871
+ * console.log('User:', result.auth.email);
872
+ * }
873
+ * }
874
+ * ```
875
+ */
876
+ declare function verifyJWT(request: Request, options: JWTVerifyOptions): Promise<{
877
+ success: true;
878
+ auth: AppAuthContext;
879
+ } | {
880
+ success: false;
881
+ error: string;
882
+ }>;
883
+ /**
884
+ * Get auth context from JWT token or fall back to proxy headers
885
+ *
886
+ * This is the recommended way to get auth context as it supports both:
887
+ * - Direct API calls with JWT token
888
+ * - Proxied requests with shell-injected headers
889
+ *
890
+ * @example
891
+ * ```typescript
892
+ * export default {
893
+ * async fetch(request: Request, env: Env) {
894
+ * const auth = await getAuthContextFromJWT(request, {
895
+ * secret: env.JWT_SECRET,
896
+ * appId: 'react-todo',
897
+ * });
898
+ *
899
+ * if (!auth) {
900
+ * return new Response('Unauthorized', { status: 401 });
901
+ * }
902
+ *
903
+ * console.log('User:', auth.email);
904
+ * }
905
+ * }
906
+ * ```
907
+ */
908
+ declare function getAuthContextFromJWT(request: Request, options: JWTVerifyOptions): Promise<AppAuthContext | null>;
909
+ /**
910
+ * Require JWT authentication - returns auth context or error response
911
+ *
912
+ * @example
913
+ * ```typescript
914
+ * export default {
915
+ * async fetch(request: Request, env: Env) {
916
+ * const auth = await requireJWTAuth(request, {
917
+ * secret: env.JWT_SECRET,
918
+ * appId: 'react-todo',
919
+ * });
920
+ *
921
+ * if (auth instanceof Response) {
922
+ * return auth; // 401 Unauthorized
923
+ * }
924
+ *
925
+ * console.log('User:', auth.email);
926
+ * }
927
+ * }
928
+ * ```
929
+ */
930
+ declare function requireJWTAuth(request: Request, options: JWTVerifyOptions): Promise<AppAuthContext | Response>;
931
+ /**
932
+ * Require JWT auth with specific permission - returns auth context or error response
933
+ *
934
+ * @example
935
+ * ```typescript
936
+ * export default {
937
+ * async fetch(request: Request, env: Env) {
938
+ * const auth = await requireJWTPermission(request, {
939
+ * secret: env.JWT_SECRET,
940
+ * appId: 'react-todo',
941
+ * }, 'categories', 'create');
942
+ *
943
+ * if (auth instanceof Response) {
944
+ * return auth; // 401 or 403
945
+ * }
946
+ *
947
+ * // User has categories:create permission
948
+ * }
949
+ * }
950
+ * ```
951
+ */
952
+ declare function requireJWTPermission(request: Request, options: JWTVerifyOptions, resource: string, action: string): Promise<AppAuthContext | Response>;
953
+ /**
954
+ * Extract auth context from request headers (proxy mode)
955
+ *
956
+ * Returns null if the user is not authenticated (no user ID header).
957
+ *
958
+ * @example
959
+ * ```typescript
960
+ * export default {
961
+ * async fetch(request: Request) {
962
+ * const auth = getAuthContext(request);
963
+ * if (!auth) {
964
+ * return new Response('Unauthorized', { status: 401 });
965
+ * }
966
+ * console.log('User:', auth.email);
967
+ * }
968
+ * }
969
+ * ```
970
+ */
971
+ declare function getAuthContext(request: Request): AppAuthContext | null;
972
+ /**
973
+ * Require authentication - returns auth context or error response
974
+ *
975
+ * @example
976
+ * ```typescript
977
+ * export default {
978
+ * async fetch(request: Request) {
979
+ * const auth = requireAuth(request);
980
+ * if (auth instanceof Response) {
981
+ * return auth; // 401 Unauthorized
982
+ * }
983
+ * // auth is AppAuthContext
984
+ * console.log('User:', auth.email);
985
+ * }
986
+ * }
987
+ * ```
988
+ */
989
+ declare function requireAuth(request: Request): AppAuthContext | Response;
990
+ /**
991
+ * Check if auth context has a specific permission
992
+ *
993
+ * Supports wildcard matching:
994
+ * - "invoices:*" matches any action on invoices
995
+ * - "*:*" matches all permissions
996
+ *
997
+ * @example
998
+ * ```typescript
999
+ * const auth = getAuthContext(request);
1000
+ * if (auth && hasPermission(auth, 'invoices', 'write')) {
1001
+ * // User can write invoices
1002
+ * }
1003
+ * ```
1004
+ */
1005
+ declare function hasPermission(auth: AppAuthContext, resource: string, action: string): boolean;
1006
+ /**
1007
+ * Check if auth context has a platform role
1008
+ *
1009
+ * @example
1010
+ * ```typescript
1011
+ * const auth = getAuthContext(request);
1012
+ * if (auth && hasPlatformRole(auth, 'admin')) {
1013
+ * // User is platform admin
1014
+ * }
1015
+ * ```
1016
+ */
1017
+ declare function hasPlatformRole(auth: AppAuthContext, role: string): boolean;
1018
+ /**
1019
+ * Check if auth context is a platform admin
1020
+ *
1021
+ * Platform admins have full access to all apps and resources.
1022
+ */
1023
+ declare function isPlatformAdmin(auth: AppAuthContext): boolean;
1024
+ /**
1025
+ * Require a specific permission - returns auth context or error response
1026
+ *
1027
+ * Also grants access if user is a platform admin.
1028
+ *
1029
+ * @example
1030
+ * ```typescript
1031
+ * export default {
1032
+ * async fetch(request: Request) {
1033
+ * const auth = requirePermission(request, 'invoices', 'write');
1034
+ * if (auth instanceof Response) {
1035
+ * return auth; // 401 or 403
1036
+ * }
1037
+ * // User has invoices:write permission
1038
+ * }
1039
+ * }
1040
+ * ```
1041
+ */
1042
+ declare function requirePermission(request: Request, resource: string, action: string): AppAuthContext | Response;
1043
+ /**
1044
+ * Require any of the specified permissions - returns auth context or error response
1045
+ *
1046
+ * @example
1047
+ * ```typescript
1048
+ * const auth = requireAnyPermission(request, [
1049
+ * ['invoices', 'read'],
1050
+ * ['invoices', 'write'],
1051
+ * ]);
1052
+ * ```
1053
+ */
1054
+ declare function requireAnyPermission(request: Request, permissions: Array<[resource: string, action: string]>): AppAuthContext | Response;
1055
+ /**
1056
+ * Require all of the specified permissions - returns auth context or error response
1057
+ *
1058
+ * @example
1059
+ * ```typescript
1060
+ * const auth = requireAllPermissions(request, [
1061
+ * ['invoices', 'read'],
1062
+ * ['customers', 'read'],
1063
+ * ]);
1064
+ * ```
1065
+ */
1066
+ declare function requireAllPermissions(request: Request, permissions: Array<[resource: string, action: string]>): AppAuthContext | Response;
1067
+
1068
+ export { AUTH_HEADERS, type AppAuthContext, type AppLifecycle, type AppManifest, CHECKSUM_PREFIX, type CreateAppOptions, type DatabaseContext, DatabaseProvider, type DatabaseProviderProps, type EldrinContext, type EldrinEnv, type EldrinEvent, EldrinEventClient, type EmitOptions, type EmitResult, type EventClientConfig, type EventDelivery, type EventHandler, type GenerateMigrationManifestOptions, type GenerateMigrationManifestResult, type JWTPayload, type JWTVerifyOptions, type LifecycleProps, type ManifestGroup, type ManifestPermission, type MigrationFile, type MigrationFiles, type MigrationManifest, type MigrationManifestEntry, type MigrationOptions, type MigrationRecord, type MigrationResult, type PollOptions, type PollResult, type RegisteredEventType, type RollbackResult, type SubscriptionInfo, calculateChecksum, calculatePrefixedChecksum, createApp, createEventClient, extractTimestamp, generateMigrationManifest, getAuthContext, getAuthContextFromJWT, getMigrationStatus, getRollbackFilename, hasPermission, hasPlatformRole, isPlatformAdmin, isValidMigrationFilename, isValidRollbackFilename, parseSQLStatements, requireAllPermissions, requireAnyPermission, requireAuth, requireJWTAuth, requireJWTPermission, requirePermission, rollbackMigrations, runMigrations, useDatabase, useDatabaseContext, useMigrationsComplete, validateMigrationManifest, verifyChecksum, verifyJWT };