@uploadista/server 0.0.18-beta.7 → 0.0.18-beta.9
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 +2 -2
- package/dist/index.d.cts +578 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +578 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/core/http-handlers/dlq-http-handlers.ts +42 -0
- package/src/core/http-handlers/flow-http-handlers.ts +61 -6
- package/src/core/http-handlers/health-http-handlers.ts +16 -0
- package/src/core/http-handlers/upload-http-handlers.ts +56 -3
- package/src/core/server.ts +10 -2
- package/src/core/types.ts +35 -0
- package/src/index.ts +2 -0
- package/src/permissions/errors.ts +105 -0
- package/src/permissions/index.ts +9 -0
- package/src/permissions/matcher.ts +139 -0
- package/src/permissions/types.ts +151 -0
- package/src/service.ts +101 -3
- package/src/usage-hooks/index.ts +8 -0
- package/src/usage-hooks/service.ts +162 -0
- package/src/usage-hooks/types.ts +221 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization Error Types
|
|
3
|
+
*
|
|
4
|
+
* Error classes for permission and authorization failures.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AdapterError } from "../error-types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Authorization error - indicates the user lacks required permissions.
|
|
11
|
+
* Returns HTTP 403 Forbidden status.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* if (!hasPermission(permissions, "engine:metrics")) {
|
|
16
|
+
* throw new AuthorizationError("engine:metrics");
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class AuthorizationError extends AdapterError {
|
|
21
|
+
/**
|
|
22
|
+
* The permission that was required but not granted.
|
|
23
|
+
*/
|
|
24
|
+
public readonly requiredPermission: string;
|
|
25
|
+
|
|
26
|
+
constructor(requiredPermission: string, message?: string) {
|
|
27
|
+
super(
|
|
28
|
+
message ?? `Permission denied: ${requiredPermission} required`,
|
|
29
|
+
403,
|
|
30
|
+
"PERMISSION_DENIED",
|
|
31
|
+
);
|
|
32
|
+
this.name = "AuthorizationError";
|
|
33
|
+
this.requiredPermission = requiredPermission;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Authentication required error - indicates no authentication context.
|
|
39
|
+
* Returns HTTP 401 Unauthorized status.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* if (!authContext) {
|
|
44
|
+
* throw new AuthenticationRequiredError();
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export class AuthenticationRequiredError extends AdapterError {
|
|
49
|
+
constructor(message = "Authentication required") {
|
|
50
|
+
super(message, 401, "AUTHENTICATION_REQUIRED");
|
|
51
|
+
this.name = "AuthenticationRequiredError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Organization mismatch error - indicates accessing a resource from another organization.
|
|
57
|
+
* Returns HTTP 403 Forbidden status.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* if (resource.organizationId !== clientId) {
|
|
62
|
+
* throw new OrganizationMismatchError();
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export class OrganizationMismatchError extends AdapterError {
|
|
67
|
+
constructor(message = "Access denied: resource belongs to another organization") {
|
|
68
|
+
super(message, 403, "ORGANIZATION_MISMATCH");
|
|
69
|
+
this.name = "OrganizationMismatchError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Quota exceeded error - indicates usage quota has been exceeded.
|
|
75
|
+
* Returns HTTP 402 Payment Required status.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* if (usage > quota) {
|
|
80
|
+
* throw new QuotaExceededError("Storage quota exceeded");
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export class QuotaExceededError extends AdapterError {
|
|
85
|
+
constructor(message = "Quota exceeded", code = "QUOTA_EXCEEDED") {
|
|
86
|
+
super(message, 402, code);
|
|
87
|
+
this.name = "QuotaExceededError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a standardized error response body for AuthorizationError.
|
|
93
|
+
* Includes the required permission in the response.
|
|
94
|
+
*
|
|
95
|
+
* @param error - The AuthorizationError to format
|
|
96
|
+
* @returns Standardized error response body
|
|
97
|
+
*/
|
|
98
|
+
export const createAuthorizationErrorResponseBody = (
|
|
99
|
+
error: AuthorizationError,
|
|
100
|
+
) => ({
|
|
101
|
+
error: error.message,
|
|
102
|
+
code: error.errorCode,
|
|
103
|
+
requiredPermission: error.requiredPermission,
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Matching Logic
|
|
3
|
+
*
|
|
4
|
+
* Implements permission matching with support for:
|
|
5
|
+
* - Exact match: `engine:health` matches `engine:health`
|
|
6
|
+
* - Wildcard match: `engine:*` matches `engine:health`, `engine:metrics`, etc.
|
|
7
|
+
* - Hierarchical match: `engine:dlq` implies `engine:dlq:read` and `engine:dlq:write`
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { PERMISSION_HIERARCHY } from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a granted permission matches a required permission.
|
|
14
|
+
*
|
|
15
|
+
* @param granted - The permission that has been granted to the user
|
|
16
|
+
* @param required - The permission that is required for the operation
|
|
17
|
+
* @returns true if the granted permission satisfies the required permission
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* matchesPermission("engine:*", "engine:health") // true (wildcard)
|
|
22
|
+
* matchesPermission("engine:health", "engine:health") // true (exact)
|
|
23
|
+
* matchesPermission("engine:dlq", "engine:dlq:read") // true (hierarchical)
|
|
24
|
+
* matchesPermission("flow:execute", "engine:health") // false
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const matchesPermission = (
|
|
28
|
+
granted: string,
|
|
29
|
+
required: string,
|
|
30
|
+
): boolean => {
|
|
31
|
+
// Exact match
|
|
32
|
+
if (granted === required) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Wildcard match: `engine:*` matches `engine:health`
|
|
37
|
+
if (granted.endsWith(":*")) {
|
|
38
|
+
const prefix = granted.slice(0, -1); // Remove the `*`, keep the `:`
|
|
39
|
+
if (required.startsWith(prefix)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Hierarchical match: `engine:dlq` implies `engine:dlq:read`
|
|
45
|
+
const impliedPermissions = PERMISSION_HIERARCHY[granted];
|
|
46
|
+
if (impliedPermissions?.includes(required)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Checks if any of the granted permissions satisfy the required permission.
|
|
55
|
+
*
|
|
56
|
+
* @param grantedPermissions - Array of permissions granted to the user
|
|
57
|
+
* @param required - The permission that is required for the operation
|
|
58
|
+
* @returns true if any granted permission satisfies the required permission
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* hasPermission(["flow:*", "upload:create"], "flow:execute") // true
|
|
63
|
+
* hasPermission(["upload:create"], "flow:execute") // false
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export const hasPermission = (
|
|
67
|
+
grantedPermissions: readonly string[],
|
|
68
|
+
required: string,
|
|
69
|
+
): boolean => {
|
|
70
|
+
return grantedPermissions.some((granted) =>
|
|
71
|
+
matchesPermission(granted, required),
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks if any of the granted permissions satisfy any of the required permissions.
|
|
77
|
+
*
|
|
78
|
+
* @param grantedPermissions - Array of permissions granted to the user
|
|
79
|
+
* @param requiredPermissions - Array of permissions, any of which would be sufficient
|
|
80
|
+
* @returns true if any granted permission satisfies any required permission
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* hasAnyPermission(["upload:create"], ["flow:execute", "upload:create"]) // true
|
|
85
|
+
* hasAnyPermission(["upload:read"], ["flow:execute", "upload:create"]) // false
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export const hasAnyPermission = (
|
|
89
|
+
grantedPermissions: readonly string[],
|
|
90
|
+
requiredPermissions: readonly string[],
|
|
91
|
+
): boolean => {
|
|
92
|
+
return requiredPermissions.some((required) =>
|
|
93
|
+
hasPermission(grantedPermissions, required),
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Checks if all of the required permissions are satisfied.
|
|
99
|
+
*
|
|
100
|
+
* @param grantedPermissions - Array of permissions granted to the user
|
|
101
|
+
* @param requiredPermissions - Array of permissions, all of which must be satisfied
|
|
102
|
+
* @returns true if all required permissions are satisfied
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* hasAllPermissions(["flow:*", "upload:*"], ["flow:execute", "upload:create"]) // true
|
|
107
|
+
* hasAllPermissions(["flow:execute"], ["flow:execute", "upload:create"]) // false
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export const hasAllPermissions = (
|
|
111
|
+
grantedPermissions: readonly string[],
|
|
112
|
+
requiredPermissions: readonly string[],
|
|
113
|
+
): boolean => {
|
|
114
|
+
return requiredPermissions.every((required) =>
|
|
115
|
+
hasPermission(grantedPermissions, required),
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Expands a permission to include all implied permissions.
|
|
121
|
+
* Useful for display or audit purposes.
|
|
122
|
+
*
|
|
123
|
+
* @param permission - The permission to expand
|
|
124
|
+
* @returns Array of the permission and all implied permissions
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* expandPermission("engine:dlq") // ["engine:dlq", "engine:dlq:read", "engine:dlq:write"]
|
|
129
|
+
* expandPermission("engine:health") // ["engine:health"]
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export const expandPermission = (permission: string): string[] => {
|
|
133
|
+
const result = [permission];
|
|
134
|
+
const implied = PERMISSION_HIERARCHY[permission];
|
|
135
|
+
if (implied) {
|
|
136
|
+
result.push(...implied);
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Types and Constants
|
|
3
|
+
*
|
|
4
|
+
* Defines the permission model for fine-grained access control in the uploadista engine.
|
|
5
|
+
* Permissions follow a hierarchical format: `resource:action` with support for wildcards.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Engine Permissions - Admin operations
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Engine permissions for administrative operations.
|
|
14
|
+
* These control access to health, readiness, metrics, and DLQ endpoints.
|
|
15
|
+
*/
|
|
16
|
+
export const ENGINE_PERMISSIONS = {
|
|
17
|
+
/** Full admin access to all engine operations */
|
|
18
|
+
ALL: "engine:*",
|
|
19
|
+
/** Access health endpoint */
|
|
20
|
+
HEALTH: "engine:health",
|
|
21
|
+
/** Access readiness endpoint */
|
|
22
|
+
READINESS: "engine:readiness",
|
|
23
|
+
/** Access metrics endpoint */
|
|
24
|
+
METRICS: "engine:metrics",
|
|
25
|
+
/** Full DLQ access (implies read and write) */
|
|
26
|
+
DLQ: "engine:dlq",
|
|
27
|
+
/** Read DLQ entries */
|
|
28
|
+
DLQ_READ: "engine:dlq:read",
|
|
29
|
+
/** Retry/delete DLQ entries */
|
|
30
|
+
DLQ_WRITE: "engine:dlq:write",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Flow Permissions - Flow execution operations
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Flow permissions for flow execution operations.
|
|
39
|
+
*/
|
|
40
|
+
export const FLOW_PERMISSIONS = {
|
|
41
|
+
/** Full access to all flow operations */
|
|
42
|
+
ALL: "flow:*",
|
|
43
|
+
/** Execute flows */
|
|
44
|
+
EXECUTE: "flow:execute",
|
|
45
|
+
/** Cancel running flows */
|
|
46
|
+
CANCEL: "flow:cancel",
|
|
47
|
+
/** Check flow status */
|
|
48
|
+
STATUS: "flow:status",
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Upload Permissions - File upload operations
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Upload permissions for file upload operations.
|
|
57
|
+
*/
|
|
58
|
+
export const UPLOAD_PERMISSIONS = {
|
|
59
|
+
/** Full access to all upload operations */
|
|
60
|
+
ALL: "upload:*",
|
|
61
|
+
/** Create uploads */
|
|
62
|
+
CREATE: "upload:create",
|
|
63
|
+
/** Read upload status */
|
|
64
|
+
READ: "upload:read",
|
|
65
|
+
/** Cancel uploads */
|
|
66
|
+
CANCEL: "upload:cancel",
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Combined Permissions Object
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* All available permissions organized by category.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* import { PERMISSIONS } from "@uploadista/server";
|
|
79
|
+
*
|
|
80
|
+
* const adminPermissions = [PERMISSIONS.ENGINE.ALL];
|
|
81
|
+
* const userPermissions = [PERMISSIONS.FLOW.ALL, PERMISSIONS.UPLOAD.ALL];
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export const PERMISSIONS = {
|
|
85
|
+
ENGINE: ENGINE_PERMISSIONS,
|
|
86
|
+
FLOW: FLOW_PERMISSIONS,
|
|
87
|
+
UPLOAD: UPLOAD_PERMISSIONS,
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Permission Type Definitions
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/** All engine permission strings */
|
|
95
|
+
export type EnginePermission =
|
|
96
|
+
(typeof ENGINE_PERMISSIONS)[keyof typeof ENGINE_PERMISSIONS];
|
|
97
|
+
|
|
98
|
+
/** All flow permission strings */
|
|
99
|
+
export type FlowPermission =
|
|
100
|
+
(typeof FLOW_PERMISSIONS)[keyof typeof FLOW_PERMISSIONS];
|
|
101
|
+
|
|
102
|
+
/** All upload permission strings */
|
|
103
|
+
export type UploadPermission =
|
|
104
|
+
(typeof UPLOAD_PERMISSIONS)[keyof typeof UPLOAD_PERMISSIONS];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Union type of all valid permission strings.
|
|
108
|
+
* Includes standard permissions and allows custom permissions via string.
|
|
109
|
+
*/
|
|
110
|
+
export type Permission =
|
|
111
|
+
| EnginePermission
|
|
112
|
+
| FlowPermission
|
|
113
|
+
| UploadPermission
|
|
114
|
+
| (string & {}); // Allow custom permissions while maintaining autocomplete
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Predefined permission sets for common use cases.
|
|
118
|
+
*/
|
|
119
|
+
export const PERMISSION_SETS = {
|
|
120
|
+
/** Full admin access - all engine, flow, and upload permissions */
|
|
121
|
+
ADMIN: [ENGINE_PERMISSIONS.ALL] as const,
|
|
122
|
+
|
|
123
|
+
/** Organization owner - all flow and upload permissions */
|
|
124
|
+
ORGANIZATION_OWNER: [
|
|
125
|
+
FLOW_PERMISSIONS.ALL,
|
|
126
|
+
UPLOAD_PERMISSIONS.ALL,
|
|
127
|
+
] as const,
|
|
128
|
+
|
|
129
|
+
/** Organization member - same as owner for now */
|
|
130
|
+
ORGANIZATION_MEMBER: [
|
|
131
|
+
FLOW_PERMISSIONS.ALL,
|
|
132
|
+
UPLOAD_PERMISSIONS.ALL,
|
|
133
|
+
] as const,
|
|
134
|
+
|
|
135
|
+
/** API key - limited to execute flows and create uploads */
|
|
136
|
+
API_KEY: [
|
|
137
|
+
FLOW_PERMISSIONS.EXECUTE,
|
|
138
|
+
UPLOAD_PERMISSIONS.CREATE,
|
|
139
|
+
] as const,
|
|
140
|
+
} as const;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Hierarchical permission relationships.
|
|
144
|
+
* When a parent permission is granted, all child permissions are implied.
|
|
145
|
+
*/
|
|
146
|
+
export const PERMISSION_HIERARCHY: Record<string, readonly string[]> = {
|
|
147
|
+
[ENGINE_PERMISSIONS.DLQ]: [
|
|
148
|
+
ENGINE_PERMISSIONS.DLQ_READ,
|
|
149
|
+
ENGINE_PERMISSIONS.DLQ_WRITE,
|
|
150
|
+
],
|
|
151
|
+
} as const;
|
package/src/service.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
2
|
import type { AuthContext } from "./types";
|
|
3
|
+
import {
|
|
4
|
+
hasPermission as matchHasPermission,
|
|
5
|
+
hasAnyPermission as matchHasAnyPermission,
|
|
6
|
+
} from "./permissions/matcher";
|
|
7
|
+
import { AuthorizationError, AuthenticationRequiredError } from "./permissions/errors";
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Authentication Context Service
|
|
@@ -39,10 +44,68 @@ export class AuthContextService extends Context.Tag("AuthContextService")<
|
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
46
|
* Check if the current client has a specific permission.
|
|
47
|
+
* Supports exact match, wildcard match, and hierarchical match.
|
|
42
48
|
* Returns false if no authentication context or permission not found.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* // Exact match
|
|
53
|
+
* yield* authService.hasPermission("engine:health")
|
|
54
|
+
*
|
|
55
|
+
* // Wildcard: user with "engine:*" will match "engine:health"
|
|
56
|
+
* yield* authService.hasPermission("engine:health")
|
|
57
|
+
*
|
|
58
|
+
* // Hierarchical: user with "engine:dlq" will match "engine:dlq:read"
|
|
59
|
+
* yield* authService.hasPermission("engine:dlq:read")
|
|
60
|
+
* ```
|
|
43
61
|
*/
|
|
44
62
|
readonly hasPermission: (permission: string) => Effect.Effect<boolean>;
|
|
45
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Check if the current client has any of the specified permissions.
|
|
66
|
+
* Returns true if at least one permission is granted.
|
|
67
|
+
*/
|
|
68
|
+
readonly hasAnyPermission: (
|
|
69
|
+
permissions: readonly string[],
|
|
70
|
+
) => Effect.Effect<boolean>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Require a specific permission, failing with AuthorizationError if not granted.
|
|
74
|
+
* Use this when you want to fail fast on missing permissions.
|
|
75
|
+
*
|
|
76
|
+
* @throws AuthorizationError if permission is not granted
|
|
77
|
+
* @throws AuthenticationRequiredError if no auth context
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const protectedHandler = Effect.gen(function* () {
|
|
82
|
+
* const authService = yield* AuthContextService;
|
|
83
|
+
* yield* authService.requirePermission("engine:metrics");
|
|
84
|
+
* // Only reaches here if permission is granted
|
|
85
|
+
* return yield* getMetrics();
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
readonly requirePermission: (
|
|
90
|
+
permission: string,
|
|
91
|
+
) => Effect.Effect<void, AuthorizationError | AuthenticationRequiredError>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Require authentication, failing with AuthenticationRequiredError if not authenticated.
|
|
95
|
+
*
|
|
96
|
+
* @throws AuthenticationRequiredError if no auth context
|
|
97
|
+
*/
|
|
98
|
+
readonly requireAuthentication: () => Effect.Effect<
|
|
99
|
+
AuthContext,
|
|
100
|
+
AuthenticationRequiredError
|
|
101
|
+
>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get all permissions granted to the current client.
|
|
105
|
+
* Returns empty array if no authentication context or no permissions.
|
|
106
|
+
*/
|
|
107
|
+
readonly getPermissions: () => Effect.Effect<readonly string[]>;
|
|
108
|
+
|
|
46
109
|
/**
|
|
47
110
|
* Get the full authentication context if available.
|
|
48
111
|
* Returns null if no authentication context is available.
|
|
@@ -60,14 +123,49 @@ export class AuthContextService extends Context.Tag("AuthContextService")<
|
|
|
60
123
|
*/
|
|
61
124
|
export const AuthContextServiceLive = (
|
|
62
125
|
authContext: AuthContext | null,
|
|
63
|
-
): Layer.Layer<AuthContextService> =>
|
|
64
|
-
|
|
126
|
+
): Layer.Layer<AuthContextService> => {
|
|
127
|
+
const permissions = authContext?.permissions ?? [];
|
|
128
|
+
|
|
129
|
+
return Layer.succeed(AuthContextService, {
|
|
65
130
|
getClientId: () => Effect.succeed(authContext?.clientId ?? null),
|
|
131
|
+
|
|
66
132
|
getMetadata: () => Effect.succeed(authContext?.metadata ?? {}),
|
|
133
|
+
|
|
67
134
|
hasPermission: (permission: string) =>
|
|
68
|
-
Effect.succeed(
|
|
135
|
+
Effect.succeed(matchHasPermission(permissions, permission)),
|
|
136
|
+
|
|
137
|
+
hasAnyPermission: (requiredPermissions: readonly string[]) =>
|
|
138
|
+
Effect.succeed(matchHasAnyPermission(permissions, requiredPermissions)),
|
|
139
|
+
|
|
140
|
+
requirePermission: (permission: string) =>
|
|
141
|
+
Effect.gen(function* () {
|
|
142
|
+
if (!authContext) {
|
|
143
|
+
yield* Effect.logDebug(
|
|
144
|
+
`[Auth] Permission check failed: authentication required for '${permission}'`,
|
|
145
|
+
);
|
|
146
|
+
return yield* Effect.fail(new AuthenticationRequiredError());
|
|
147
|
+
}
|
|
148
|
+
if (!matchHasPermission(permissions, permission)) {
|
|
149
|
+
yield* Effect.logDebug(
|
|
150
|
+
`[Auth] Permission denied: '${permission}' for client '${authContext.clientId}'`,
|
|
151
|
+
);
|
|
152
|
+
return yield* Effect.fail(new AuthorizationError(permission));
|
|
153
|
+
}
|
|
154
|
+
yield* Effect.logDebug(
|
|
155
|
+
`[Auth] Permission granted: '${permission}' for client '${authContext.clientId}'`,
|
|
156
|
+
);
|
|
157
|
+
}),
|
|
158
|
+
|
|
159
|
+
requireAuthentication: () =>
|
|
160
|
+
authContext
|
|
161
|
+
? Effect.succeed(authContext)
|
|
162
|
+
: Effect.fail(new AuthenticationRequiredError()),
|
|
163
|
+
|
|
164
|
+
getPermissions: () => Effect.succeed(permissions),
|
|
165
|
+
|
|
69
166
|
getAuthContext: () => Effect.succeed(authContext),
|
|
70
167
|
});
|
|
168
|
+
};
|
|
71
169
|
|
|
72
170
|
/**
|
|
73
171
|
* No-auth implementation of AuthContextService.
|