@baeta/auth 0.0.0 → 2.0.0-next.15
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/CHANGELOG.md +377 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/index.d.ts +204 -0
- package/dist/index.js +426 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -2
- package/coverage/lcov.info +0 -1084
- package/coverage/tmp/coverage-80733-1779319154081-1.json +0 -1
- package/coverage/tmp/coverage-80733-1779319154102-0.json +0 -1
- package/coverage/tmp/coverage-80734-1779319150108-0.json +0 -1
- package/coverage/tmp/coverage-80760-1779319154037-1.json +0 -1
- package/coverage/tmp/coverage-80760-1779319154052-0.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153341-13.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153366-12.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153366-15.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153377-2.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153386-19.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153386-5.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153392-8.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153407-7.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153429-21.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153447-9.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153544-20.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153563-4.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153765-16.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153787-6.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153805-17.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153816-14.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153828-11.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153850-3.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153897-18.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153922-10.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153954-0.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153963-1.json +0 -1
- package/coverage/tmp/coverage-80769-1779319153972-0.json +0 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AppPlugin, FieldUsePlugin, SubscriptionUsePlugin, TypeUsePlugin } from "@baeta/core/sdk";
|
|
2
|
+
import { ResolverParams } from "@baeta/core";
|
|
3
|
+
|
|
4
|
+
//#region lib/error.d.ts
|
|
5
|
+
/** Custom error resolver function for authorization failures. */
|
|
6
|
+
type ScopeErrorResolver = (err: unknown) => unknown;
|
|
7
|
+
/**
|
|
8
|
+
* Default error resolver for authorization failures.
|
|
9
|
+
* If multiple authorization errors are encountered they are combined into `AggregateGraphQLError` with proper HTTP status codes.
|
|
10
|
+
*/
|
|
11
|
+
declare function aggregateErrorResolver(err: AggregateError): any;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region lib/grant.d.ts
|
|
14
|
+
/**
|
|
15
|
+
* Represents the result of a grant operation.
|
|
16
|
+
* Can be either a single grant or an array of grants defined in AuthExtension.GrantsMap.
|
|
17
|
+
*/
|
|
18
|
+
type GetGrantResult<Grant extends string, Result> = Grant | Grant[] | GrantConfig<Grant, Result> | GrantConfig<Grant, Result>[];
|
|
19
|
+
/**
|
|
20
|
+
* Attaches a grant to a specific object derived from the resolver result,
|
|
21
|
+
* instead of the result itself. For array results, `target` is invoked per
|
|
22
|
+
* entry. `target` must return a non-primitive value.
|
|
23
|
+
*/
|
|
24
|
+
type GrantConfig<Grant extends string, Result> = {
|
|
25
|
+
grant: Grant | Grant[];
|
|
26
|
+
target: (result: GrantTarget<Result>) => unknown;
|
|
27
|
+
};
|
|
28
|
+
type GrantTarget<Result> = Result extends Array<infer U> ? U : Result;
|
|
29
|
+
/**
|
|
30
|
+
* Function that determines grants based on resolver parameters and result.
|
|
31
|
+
* Used for dynamic permission granting based on resolved data.
|
|
32
|
+
*/
|
|
33
|
+
type GetGrantFn<Grants extends string, Result, Source, Context, Args, Info> = (params: ResolverParams<Source, Context, Args, Info>, result: Result) => GetGrantResult<Grants, Result> | PromiseLike<GetGrantResult<Grants, Result>>;
|
|
34
|
+
/**
|
|
35
|
+
* Union type for grant specifications.
|
|
36
|
+
* Can be either a static grant result or a function that determines grants dynamically.
|
|
37
|
+
*/
|
|
38
|
+
type GetGrant<Grants extends string, Result, Source, Context, Args, Info> = GetGrantFn<Grants, Result, Source, Context, Args, Info> | GetGrantResult<Grants, Result>;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region lib/scope-rules.d.ts
|
|
41
|
+
type LogicRule = 'and' | 'or' | 'chain' | 'race';
|
|
42
|
+
type ScopesShape = Record<string, unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* Defines the structure of authorization scope rules.
|
|
45
|
+
* Combines individual scope rules with logical operators and granted permissions.
|
|
46
|
+
*/
|
|
47
|
+
type ScopeRules<Scopes extends ScopesShape, Grants extends string> = ScopeRule<Scopes, Grants> | ScopeLogicRule<Scopes, Grants>;
|
|
48
|
+
/**
|
|
49
|
+
* Utility type that enforces boolean scopes must be true.
|
|
50
|
+
* For non-boolean scopes, preserves the original type.
|
|
51
|
+
*/
|
|
52
|
+
type ScopeRule<Scopes extends ScopesShape, Grants extends string> = { [K in keyof Scopes]: {
|
|
53
|
+
type: 'scope';
|
|
54
|
+
key: K;
|
|
55
|
+
value: Scopes[K] extends boolean ? true : Scopes[K];
|
|
56
|
+
} }[keyof Scopes] | {
|
|
57
|
+
type: 'grant';
|
|
58
|
+
grant: Grants;
|
|
59
|
+
};
|
|
60
|
+
type ScopeLogicRule<Scopes extends ScopesShape, Grants extends string> = {
|
|
61
|
+
type: 'rule';
|
|
62
|
+
rule: LogicRule;
|
|
63
|
+
scopes: ScopeRules<Scopes, Grants>[];
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region lib/scope-cache-keys.d.ts
|
|
67
|
+
/**
|
|
68
|
+
* Builds a cache key for a single scope. The returned value must be stable —
|
|
69
|
+
* equal inputs must produce a value that compares equal as a `Map` key (a
|
|
70
|
+
* string, or a stable object reference).
|
|
71
|
+
*/
|
|
72
|
+
type ScopeCacheKeyFn<Param> = (param: Param) => unknown;
|
|
73
|
+
/**
|
|
74
|
+
* Provide an entry when the scope's argument can't be safely
|
|
75
|
+
* auto-serialized in a stable manner or when a more compact key
|
|
76
|
+
* is preferable.
|
|
77
|
+
*/
|
|
78
|
+
type ScopeCacheKeyMap<Scopes extends ScopesShape> = { [K in keyof Scopes as Scopes[K] extends boolean ? never : K]?: ScopeCacheKeyFn<Scopes[K]> };
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region lib/scope-defaults.d.ts
|
|
81
|
+
/** Configuration for default authorization scopes that apply to all operations of a specific type. */
|
|
82
|
+
type DefaultScopes<Scopes extends ScopesShape, Grants extends string> = {
|
|
83
|
+
/** Default scopes applied to all Query operations */Query?: ScopeRules<Scopes, Grants>; /** Default scopes applied to all Mutation operations */
|
|
84
|
+
Mutation?: ScopeRules<Scopes, Grants>; /** Default scopes for Subscription operations */
|
|
85
|
+
Subscription?: ScopeRules<Scopes, Grants>;
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region lib/scope-resolver.d.ts
|
|
89
|
+
/**
|
|
90
|
+
* Function that creates scope loaders for authorization checks.
|
|
91
|
+
* Returns a map of scope loaders that can be synchronous or asynchronous.
|
|
92
|
+
*
|
|
93
|
+
* @param ctx - The application context
|
|
94
|
+
* @returns A map of scope loaders or a promise resolving to scope loaders
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* const getScopeLoader: GetScopeLoader<Context> = (ctx) => ({
|
|
99
|
+
* isLoggedIn: async () => {
|
|
100
|
+
* if (!ctx.userId) throw new UnauthenticatedError();
|
|
101
|
+
* return true;
|
|
102
|
+
* },
|
|
103
|
+
* hasAccess: (role) => ctx.user?.role === role
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
type GetScopeLoader<Scopes extends ScopesShape, Ctx> = (ctx: Ctx) => ScopeLoaderMap<Scopes> | Promise<ScopeLoaderMap<Scopes>>;
|
|
108
|
+
/**
|
|
109
|
+
* Represents a scope loader that can be either a boolean value or a function.
|
|
110
|
+
* Function loaders receive the scope value and return a boolean result.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* // Boolean loader
|
|
115
|
+
* const publicLoader: ScopeLoader<boolean> = true;
|
|
116
|
+
*
|
|
117
|
+
* // Function loader
|
|
118
|
+
* const roleLoader: ScopeLoader<string> = (role) => userRole === role;
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
type ScopeLoader<T> = T extends boolean ? boolean | (() => boolean | Promise<boolean>) : (param: T) => boolean | Promise<boolean>;
|
|
122
|
+
/**
|
|
123
|
+
* Maps scope names to their respective loaders.
|
|
124
|
+
* Each loader handles authorization checks for its scope.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const loaders: ScopeLoaderMap = {
|
|
129
|
+
* isPublic: true,
|
|
130
|
+
* isLoggedIn: () => Boolean(ctx.userId),
|
|
131
|
+
* hasAccess: (role) => ctx.user?.roles.includes(role)
|
|
132
|
+
* };
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
type ScopeLoaderMap<Scopes extends ScopesShape> = { [K in keyof Scopes]: Scopes[K] extends boolean ? boolean | (() => boolean | Promise<boolean>) : (param: Scopes[K]) => boolean | Promise<boolean> };
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region lib/auth-middlewares.d.ts
|
|
138
|
+
/**
|
|
139
|
+
* Options for authorization middlewares
|
|
140
|
+
*/
|
|
141
|
+
interface AuthMiddlewareOptions<Grants extends string, Result, Source, Context, Args, Info> {
|
|
142
|
+
/** Permissions to grant after successful authorization */
|
|
143
|
+
grants?: GetGrant<Grants, Result, Source, Context, Args, Info>;
|
|
144
|
+
/** Whether to skip default scopes for this operation */
|
|
145
|
+
skipDefaults?: boolean;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Function to get scope rules for pre-resolution authorization
|
|
149
|
+
*/
|
|
150
|
+
type GetScopeRules<Scopes extends ScopesShape, Grants extends string, Source, Context, Args, Info> = (params: ResolverParams<Source, Context, Args, Info>) => boolean | ScopeRules<Scopes, Grants> | PromiseLike<boolean | ScopeRules<Scopes, Grants>>;
|
|
151
|
+
/**
|
|
152
|
+
* Function to get scope rules for post-resolution authorization
|
|
153
|
+
*/
|
|
154
|
+
type GetPostScopeRules<Scopes extends ScopesShape, Grants extends string, Result, Source, Context, Args, Info> = (params: ResolverParams<Source, Context, Args, Info>, result: Result) => boolean | ScopeRules<Scopes, Grants> | PromiseLike<boolean | ScopeRules<Scopes, Grants>>;
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region lib/define-rules.d.ts
|
|
157
|
+
type RuleAccessor<Scopes extends ScopesShape, Grants extends string> = {
|
|
158
|
+
and(scope: ScopeRules<Scopes, Grants>, ...scopes: ScopeRules<Scopes, Grants>[]): ScopeRules<Scopes, Grants>;
|
|
159
|
+
or(scope: ScopeRules<Scopes, Grants>, ...scopes: ScopeRules<Scopes, Grants>[]): ScopeRules<Scopes, Grants>;
|
|
160
|
+
chain(scope: ScopeRules<Scopes, Grants>, ...scopes: ScopeRules<Scopes, Grants>[]): ScopeRules<Scopes, Grants>;
|
|
161
|
+
race(scope: ScopeRules<Scopes, Grants>, ...scopes: ScopeRules<Scopes, Grants>[]): ScopeRules<Scopes, Grants>;
|
|
162
|
+
};
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region lib/define-scopes.d.ts
|
|
165
|
+
type ScopeAccessor<Scopes extends ScopesShape, Grants extends string> = { [K in keyof Scopes]: Scopes[K] extends boolean ? ScopeRules<Scopes, Grants> : (param: Scopes[K]) => ScopeRules<Scopes, Grants> } & {
|
|
166
|
+
$granted: <G extends Grants>(grant: G) => ScopeRule<Scopes, G>;
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region lib/create-auth.d.ts
|
|
170
|
+
type AuthPlugin<Result, Source, Context, Args, Info> = FieldUsePlugin<Result, Source, Context, Args, Info> & TypeUsePlugin<Source, Context, Info> & SubscriptionUsePlugin<Result, Source, Context, Args, Info, 'resolve'> & SubscriptionUsePlugin<Result, Source, Context, Args, Info, 'subscribe'>;
|
|
171
|
+
/** Configuration options for Auth */
|
|
172
|
+
interface AuthOptions<Scopes extends ScopesShape, Grants extends string> {
|
|
173
|
+
/** Default authorization scopes for queries, mutations or subscriptions */
|
|
174
|
+
defaultScopes?: (opt: {
|
|
175
|
+
scope: ScopeAccessor<Scopes, Grants>;
|
|
176
|
+
rule: RuleAccessor<Scopes, Grants>;
|
|
177
|
+
}) => DefaultScopes<Scopes, Grants>;
|
|
178
|
+
/** Custom error resolver for authorization failures */
|
|
179
|
+
errorResolver?: ScopeErrorResolver;
|
|
180
|
+
/**
|
|
181
|
+
* Per-scope cache key overrides. Recommended for scopes whose argument
|
|
182
|
+
* isn't safely auto-serializable: serializable args (primitives, plain
|
|
183
|
+
* objects, arrays of those) are stringified automatically, and anything
|
|
184
|
+
* else falls back to reference identity — which may miss cache hits when
|
|
185
|
+
* callers construct equivalent-but-distinct values.
|
|
186
|
+
*/
|
|
187
|
+
cacheKeyMap?: ScopeCacheKeyMap<Scopes>;
|
|
188
|
+
}
|
|
189
|
+
declare function createAuth<Context, Scopes extends ScopesShape, Grants extends string>(loadScopes: GetScopeLoader<Scopes, Context>, globalOptions?: AuthOptions<Scopes, Grants>): {
|
|
190
|
+
auth: <Result, Source, Context_1, Args, Info>(scopes: ScopeRules<Scopes, Grants> | GetScopeRules<Scopes, Grants, Source, Context_1, Args, Info>, options?: AuthMiddlewareOptions<Grants, Result, Source, Context_1, Args, Info>) => AuthPlugin<Result, Source, Context_1, Args, Info>;
|
|
191
|
+
authAfter: <Result, Source, Context_1, Args, Info>(getScopes: GetPostScopeRules<Scopes, Grants, Result, Source, Context_1, Args, Info>, options?: AuthMiddlewareOptions<Grants, Result, Source, Context_1, Args, Info>) => AuthPlugin<Result, Source, Context_1, Args, Info>;
|
|
192
|
+
authAppPlugin: AppPlugin;
|
|
193
|
+
rule: RuleAccessor<Scopes, Grants>;
|
|
194
|
+
scope: ScopeAccessor<Scopes, Grants>;
|
|
195
|
+
};
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region lib/serialize.d.ts
|
|
198
|
+
type SerializableScope = string | number | boolean | null | SerializableScope[] | {
|
|
199
|
+
[key: string]: SerializableScope;
|
|
200
|
+
};
|
|
201
|
+
declare function createScopeCacheKey(params: SerializableScope): string;
|
|
202
|
+
//#endregion
|
|
203
|
+
export { type AuthMiddlewareOptions, type AuthOptions, type DefaultScopes, type GetGrant, type GetGrantFn, type GetGrantResult, type GetPostScopeRules, type GetScopeLoader, type GetScopeRules, type GrantConfig, type LogicRule, type ScopeCacheKeyFn, type ScopeCacheKeyMap, type ScopeErrorResolver, type ScopeLoader, type ScopeLoaderMap, type ScopeRule, type ScopeRules, type ScopesShape, type SerializableScope, aggregateErrorResolver, createAuth, createScopeCacheKey };
|
|
204
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { AggregateGraphQLError, ForbiddenError, InternalServerError } from "@baeta/errors";
|
|
2
|
+
import { log } from "@baeta/util-log";
|
|
3
|
+
import { GraphQLError } from "graphql";
|
|
4
|
+
import { createAppPluginId, makePluginSymbol, nameFunction } from "@baeta/core/sdk";
|
|
5
|
+
import { createContextStore } from "@baeta/core";
|
|
6
|
+
import stringify from "safe-stable-stringify";
|
|
7
|
+
//#region lib/error.ts
|
|
8
|
+
function resolveError(err, resolve) {
|
|
9
|
+
const resolvedError = resolve(err);
|
|
10
|
+
if (resolvedError instanceof Error) throw resolvedError;
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
function defaultErrorResolver(err) {
|
|
14
|
+
if (err instanceof AggregateError) return aggregateErrorResolver(err);
|
|
15
|
+
if (!isGraphqlError(err)) log.warn(`Non GraphQLError encountered by auth`, err);
|
|
16
|
+
return err;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Default error resolver for authorization failures.
|
|
20
|
+
* If multiple authorization errors are encountered they are combined into `AggregateGraphQLError` with proper HTTP status codes.
|
|
21
|
+
*/
|
|
22
|
+
function aggregateErrorResolver(err) {
|
|
23
|
+
if (err.errors.length === 1) {
|
|
24
|
+
if (!isGraphqlError(err.errors[0])) log.warn(`Non GraphQLError encountered by auth`, err);
|
|
25
|
+
return err.errors[0];
|
|
26
|
+
}
|
|
27
|
+
let http = {};
|
|
28
|
+
const errors = [];
|
|
29
|
+
for (const error of err.errors) {
|
|
30
|
+
if (!isGraphqlError(error)) {
|
|
31
|
+
errors.push(new InternalServerError(error));
|
|
32
|
+
log.warn(`Non GraphQLError encountered by auth`, err);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
errors.push(error);
|
|
36
|
+
if (error.extensions.http && http?.status !== 401) http = error.extensions.http;
|
|
37
|
+
}
|
|
38
|
+
return new AggregateGraphQLError(errors, void 0, { extensions: { http } });
|
|
39
|
+
}
|
|
40
|
+
function isGraphqlError(err) {
|
|
41
|
+
return err instanceof GraphQLError;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region utils/resolver.ts
|
|
45
|
+
function isOperationType(type) {
|
|
46
|
+
return [
|
|
47
|
+
"Query",
|
|
48
|
+
"Mutation",
|
|
49
|
+
"Subscription"
|
|
50
|
+
].includes(type);
|
|
51
|
+
}
|
|
52
|
+
const [getAuthStore, setAuthStore] = createContextStore(Symbol("@baeta/auth"));
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region lib/grant.ts
|
|
55
|
+
async function saveGrants(params, result, grants) {
|
|
56
|
+
if (result == null) return;
|
|
57
|
+
const [store, resolvedGrants] = await Promise.all([getAuthStore(params.ctx), resolveGrants(params, result, grants)]);
|
|
58
|
+
const entries = Array.isArray(result) ? result : [result];
|
|
59
|
+
resolvedGrants.forEach(({ grant, target }) => {
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (entry == null) continue;
|
|
62
|
+
store.grantCache.addGrants(target(entry), grant);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function defaultTarget(entry) {
|
|
67
|
+
return entry;
|
|
68
|
+
}
|
|
69
|
+
function normalizeGrant(grants) {
|
|
70
|
+
if (typeof grants === "string") return {
|
|
71
|
+
grant: [grants],
|
|
72
|
+
target: defaultTarget
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
grant: Array.isArray(grants.grant) ? grants.grant : [grants.grant],
|
|
76
|
+
target: grants.target
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function normalizeGrants(grants) {
|
|
80
|
+
if (Array.isArray(grants)) return grants.map((el) => normalizeGrant(el));
|
|
81
|
+
return [normalizeGrant(grants)];
|
|
82
|
+
}
|
|
83
|
+
async function resolveGrants(params, result, grants) {
|
|
84
|
+
if (typeof grants !== "function") return normalizeGrants(grants);
|
|
85
|
+
return normalizeGrants(await grants(params, result));
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region lib/scope-defaults.ts
|
|
89
|
+
function selectDefaultScopes(skipDefaults, type, defaultScopes) {
|
|
90
|
+
if (!defaultScopes) return;
|
|
91
|
+
if (!isOperationType(type)) return;
|
|
92
|
+
if (skipDefaults === true) return;
|
|
93
|
+
if (type === "Query") return defaultScopes.Query;
|
|
94
|
+
if (type === "Mutation") return defaultScopes.Mutation;
|
|
95
|
+
if (type === "Subscription") return defaultScopes.Subscription;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region lib/scope-rules.ts
|
|
99
|
+
async function verifyGrant(params, grant) {
|
|
100
|
+
if (grant == null) throw new Error("Grant key '$granted' must be defined in the scope rules!");
|
|
101
|
+
if ((await getAuthStore(params.ctx)).grantCache.getGrants(params.source)?.has(grant) !== true) throw new ForbiddenError();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
async function verifyScope(params, scope) {
|
|
105
|
+
if (scope == null) throw new Error("Scope rules cannot be undefined!");
|
|
106
|
+
if (scope.type === "rule") return await verifyScopeRule(params, scope);
|
|
107
|
+
if (scope.type === "grant") return await verifyGrant(params, scope.grant);
|
|
108
|
+
if (scope.type === "scope") {
|
|
109
|
+
const resolve = (await getAuthStore(params.ctx)).scopes.get(scope.key);
|
|
110
|
+
if (resolve == null) throw new Error(`No scope resolver found for key '${scope.key}'!`);
|
|
111
|
+
if (await resolve(scope.value) !== true) throw new Error("Scope resolver should throw for non true results!");
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
throw new Error("Invalid scope rule! Must be a scope, grant, or nested rule.");
|
|
115
|
+
}
|
|
116
|
+
async function verifyScopeRule(params, scope) {
|
|
117
|
+
if (scope.scopes.length === 0) throw new Error("Scope rule cannot be empty!");
|
|
118
|
+
if (scope.rule === "chain") return await verifyChainScopes(params, scope.scopes);
|
|
119
|
+
if (scope.rule === "race") return await verifyRaceScopes(params, scope.scopes);
|
|
120
|
+
if (scope.rule === "or") return await verifyOrScopes(params, scope.scopes);
|
|
121
|
+
if (scope.rule === "and") return await verifyAndScopes(params, scope.scopes);
|
|
122
|
+
scope.rule;
|
|
123
|
+
throw new Error("Invalid logic rule! Must be one of 'chain', 'race', 'or', or 'and'.");
|
|
124
|
+
}
|
|
125
|
+
async function verifyChainScopes(params, scopes) {
|
|
126
|
+
for (const scope of scopes) await verifyScope(params, scope);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
async function verifyRaceScopes(params, scopes) {
|
|
130
|
+
for (const scope of scopes) if (await verifyScope(params, scope).catch((err) => err) === true) return true;
|
|
131
|
+
throw new ForbiddenError();
|
|
132
|
+
}
|
|
133
|
+
async function verifyOrScopes(params, scopes) {
|
|
134
|
+
const promises = scopes.map((scope) => verifyScope(params, scope));
|
|
135
|
+
return await Promise.any(promises);
|
|
136
|
+
}
|
|
137
|
+
async function verifyAndScopes(params, scopes) {
|
|
138
|
+
const promises = scopes.map((scope) => verifyScope(params, scope));
|
|
139
|
+
await Promise.all(promises);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region lib/grant-cache.ts
|
|
144
|
+
function createGrantCache() {
|
|
145
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
146
|
+
return {
|
|
147
|
+
getGrants: (result) => {
|
|
148
|
+
if (!isValidTarget(result)) return;
|
|
149
|
+
return cache.get(result);
|
|
150
|
+
},
|
|
151
|
+
addGrants: (result, values) => {
|
|
152
|
+
if (!isValidTarget(result)) {
|
|
153
|
+
log.warn(`Attempted to add grants for a non-object result. Grants will not be saved.`, (/* @__PURE__ */ new Error()).stack);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const existing = cache.get(result) ?? /* @__PURE__ */ new Set();
|
|
157
|
+
values.forEach((grant) => existing.add(grant));
|
|
158
|
+
cache.set(result, existing);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function isValidTarget(target) {
|
|
163
|
+
return typeof target === "object" && target !== null;
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region lib/serialize.ts
|
|
167
|
+
function createScopeCacheKey(params) {
|
|
168
|
+
return stringify(params);
|
|
169
|
+
}
|
|
170
|
+
function canSafelySerialize(value, path = /* @__PURE__ */ new WeakSet()) {
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
if (path.has(value)) return false;
|
|
173
|
+
path.add(value);
|
|
174
|
+
const ok = value.every((v) => canSafelySerialize(v, path));
|
|
175
|
+
path.delete(value);
|
|
176
|
+
return ok;
|
|
177
|
+
}
|
|
178
|
+
if (value && typeof value === "object") {
|
|
179
|
+
const proto = Object.getPrototypeOf(value);
|
|
180
|
+
if (proto !== null && proto !== Object.prototype) return false;
|
|
181
|
+
if (path.has(value)) return false;
|
|
182
|
+
path.add(value);
|
|
183
|
+
const ok = Object.values(value).every((v) => canSafelySerialize(v, path));
|
|
184
|
+
path.delete(value);
|
|
185
|
+
return ok;
|
|
186
|
+
}
|
|
187
|
+
return isSerializablePrimitive(value);
|
|
188
|
+
}
|
|
189
|
+
function isSerializablePrimitive(value) {
|
|
190
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
191
|
+
return value === null || typeof value === "boolean" || typeof value === "string";
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region lib/scope-cache.ts
|
|
195
|
+
const noParameterKey = Symbol("no-parameter");
|
|
196
|
+
function createScopeCache(cacheKeyMap) {
|
|
197
|
+
const scopeCache = /* @__PURE__ */ new Map();
|
|
198
|
+
const keyFns = cacheKeyMap;
|
|
199
|
+
const makeKey = (scope, params) => {
|
|
200
|
+
if (params === void 0) return noParameterKey;
|
|
201
|
+
const customKeyFn = keyFns[scope];
|
|
202
|
+
if (customKeyFn) return customKeyFn(params);
|
|
203
|
+
if (canSafelySerialize(params)) return createScopeCacheKey(params);
|
|
204
|
+
return params;
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
getScopeValue: (scope, params) => {
|
|
208
|
+
return scopeCache.get(scope)?.get(makeKey(scope, params));
|
|
209
|
+
},
|
|
210
|
+
setScopeValue: (scope, params, value) => {
|
|
211
|
+
let scopeParamsMap = scopeCache.get(scope);
|
|
212
|
+
if (scopeParamsMap == null) {
|
|
213
|
+
scopeParamsMap = /* @__PURE__ */ new Map();
|
|
214
|
+
scopeCache.set(scope, scopeParamsMap);
|
|
215
|
+
}
|
|
216
|
+
scopeParamsMap.set(makeKey(scope, params), value);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region lib/scope-resolver.ts
|
|
222
|
+
function resolveBoolean(param) {
|
|
223
|
+
if (param !== true) throw new ForbiddenError();
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
function createScopeResolver(ctx, name, value) {
|
|
227
|
+
if (!(typeof value === "function")) return () => resolveBoolean(value);
|
|
228
|
+
return async (params) => {
|
|
229
|
+
const store = await getAuthStore(ctx);
|
|
230
|
+
const cached = await store.scopeCache.getScopeValue(name, params);
|
|
231
|
+
if (cached != null) return resolveBoolean(cached);
|
|
232
|
+
const resultPromise = value(params);
|
|
233
|
+
store.scopeCache.setScopeValue(name, params, resultPromise);
|
|
234
|
+
return resolveBoolean(await resultPromise);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function createScopeResolverMap(ctx, scopeLoaderMap) {
|
|
238
|
+
const map = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const [key, value] of Object.entries(scopeLoaderMap)) map.set(key, createScopeResolver(ctx, key, value));
|
|
240
|
+
return map;
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region lib/store-loader.ts
|
|
244
|
+
function loadAuthStore(ctx, getScopeLoader, cacheKeyMap) {
|
|
245
|
+
setAuthStore(ctx, async () => {
|
|
246
|
+
return {
|
|
247
|
+
scopes: createScopeResolverMap(ctx, await getScopeLoader(ctx)),
|
|
248
|
+
scopeCache: createScopeCache(cacheKeyMap),
|
|
249
|
+
grantCache: createGrantCache()
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region lib/auth-middlewares.ts
|
|
255
|
+
function createMiddleware(type, loadScopes, cacheKeyMap, scopes, globalScopes, options, onError) {
|
|
256
|
+
const getScopes = typeof scopes === "function" ? scopes : () => scopes;
|
|
257
|
+
const defaultScopes = selectDefaultScopes(options?.skipDefaults, type, globalScopes);
|
|
258
|
+
return async (next, params) => {
|
|
259
|
+
loadAuthStore(params.ctx, loadScopes, cacheKeyMap);
|
|
260
|
+
await verifyMiddlewareScopes(params, defaultScopes, await getScopes(params), onError ?? defaultErrorResolver);
|
|
261
|
+
const result = await next();
|
|
262
|
+
if (options?.grants) await saveGrants(params, result, options.grants);
|
|
263
|
+
return result;
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function createPostMiddleware(type, loadScopes, cacheKeyMap, getScopes, globalScopes, options, onError) {
|
|
267
|
+
const defaultScopes = selectDefaultScopes(options?.skipDefaults, type, globalScopes);
|
|
268
|
+
return async (next, params) => {
|
|
269
|
+
loadAuthStore(params.ctx, loadScopes, cacheKeyMap);
|
|
270
|
+
const result = await next();
|
|
271
|
+
await verifyMiddlewareScopes(params, defaultScopes, await getScopes(params, result), onError ?? defaultErrorResolver);
|
|
272
|
+
if (options?.grants) await saveGrants(params, result, options.grants);
|
|
273
|
+
return result;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function createFallbackMiddleware(type, loadScopes, cacheKeyMap, globalScopes, onError) {
|
|
277
|
+
const rules = selectDefaultScopes(false, type, globalScopes);
|
|
278
|
+
if (rules == null) return;
|
|
279
|
+
return createMiddleware(type, loadScopes, cacheKeyMap, rules, {}, { skipDefaults: true }, onError);
|
|
280
|
+
}
|
|
281
|
+
async function verifyMiddlewareScopes(params, defaultScopes, requiredScopes, errorResolver) {
|
|
282
|
+
if (requiredScopes === false) throw new ForbiddenError();
|
|
283
|
+
const promises = [];
|
|
284
|
+
if (defaultScopes) promises.push(verifyScope(params, defaultScopes));
|
|
285
|
+
if (requiredScopes !== true) promises.push(verifyScope(params, requiredScopes));
|
|
286
|
+
if (promises.length === 0) return;
|
|
287
|
+
return await Promise.all(promises).catch((err) => resolveError(err, errorResolver));
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region lib/define-rules.ts
|
|
291
|
+
function defineRules() {
|
|
292
|
+
return {
|
|
293
|
+
and(...scopes) {
|
|
294
|
+
return {
|
|
295
|
+
type: "rule",
|
|
296
|
+
rule: "and",
|
|
297
|
+
scopes
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
or(...scopes) {
|
|
301
|
+
return {
|
|
302
|
+
type: "rule",
|
|
303
|
+
rule: "or",
|
|
304
|
+
scopes
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
chain(...scopes) {
|
|
308
|
+
return {
|
|
309
|
+
type: "rule",
|
|
310
|
+
rule: "chain",
|
|
311
|
+
scopes
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
race(...scopes) {
|
|
315
|
+
return {
|
|
316
|
+
type: "rule",
|
|
317
|
+
rule: "race",
|
|
318
|
+
scopes
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region lib/define-scopes.ts
|
|
325
|
+
function defineScopes() {
|
|
326
|
+
const cache = /* @__PURE__ */ new Map();
|
|
327
|
+
const resolveScope = (prop) => {
|
|
328
|
+
if (prop === "$granted") return (grant) => makeGrant(grant);
|
|
329
|
+
const leaf = makeProp(prop, true);
|
|
330
|
+
const fn = (param) => makeProp(prop, param);
|
|
331
|
+
return Object.assign(fn, leaf);
|
|
332
|
+
};
|
|
333
|
+
return new Proxy({}, { get(_target, prop) {
|
|
334
|
+
const cached = cache.get(prop);
|
|
335
|
+
if (cached) return cached;
|
|
336
|
+
const result = resolveScope(prop);
|
|
337
|
+
cache.set(prop, result);
|
|
338
|
+
return result;
|
|
339
|
+
} });
|
|
340
|
+
}
|
|
341
|
+
function makeGrant(grant) {
|
|
342
|
+
return {
|
|
343
|
+
type: "grant",
|
|
344
|
+
grant
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function makeProp(prop, value) {
|
|
348
|
+
return {
|
|
349
|
+
type: "scope",
|
|
350
|
+
key: prop,
|
|
351
|
+
value
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region lib/create-auth.ts
|
|
356
|
+
function createAuth(loadScopes, globalOptions = {}) {
|
|
357
|
+
const id = createAppPluginId("Baeta Auth");
|
|
358
|
+
const stateKey = Symbol("auth");
|
|
359
|
+
const scope = defineScopes();
|
|
360
|
+
const rule = defineRules();
|
|
361
|
+
const loadScopesFn = loadScopes;
|
|
362
|
+
const defaultScopes = globalOptions.defaultScopes?.({
|
|
363
|
+
scope,
|
|
364
|
+
rule
|
|
365
|
+
});
|
|
366
|
+
const cacheKeyMap = globalOptions.cacheKeyMap ?? {};
|
|
367
|
+
const makeAuthBuilder = (buildMiddleware) => {
|
|
368
|
+
return { [makePluginSymbol]: ({ type, field, subscriptionFieldKind }) => {
|
|
369
|
+
const middleware = buildMiddleware(type);
|
|
370
|
+
nameFunction(middleware, buildMiddlewareName(type, field, subscriptionFieldKind));
|
|
371
|
+
return {
|
|
372
|
+
id,
|
|
373
|
+
middleware,
|
|
374
|
+
meta: new Map([[stateKey, { hasAuth: true }]])
|
|
375
|
+
};
|
|
376
|
+
} };
|
|
377
|
+
};
|
|
378
|
+
const auth = (scopes, options) => makeAuthBuilder((type) => createMiddleware(type, loadScopesFn, cacheKeyMap, scopes, defaultScopes, options, globalOptions.errorResolver));
|
|
379
|
+
const authAfter = (getScopes, options) => makeAuthBuilder((type) => {
|
|
380
|
+
if (type === "Mutation") throw new Error("\"authAfter\" cannot be used on Mutations! authAfter is executed after the resolver thus cannot protect mutations. Use \"auth\" instead for mutations.");
|
|
381
|
+
return createPostMiddleware(type, loadScopesFn, cacheKeyMap, getScopes, defaultScopes, options, globalOptions.errorResolver);
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
auth,
|
|
385
|
+
authAfter,
|
|
386
|
+
authAppPlugin: {
|
|
387
|
+
id,
|
|
388
|
+
name: "Baeta Auth",
|
|
389
|
+
mutate: (compilers) => {
|
|
390
|
+
if (defaultScopes == null) return;
|
|
391
|
+
for (const typeCompiler of iterateTypes(compilers)) {
|
|
392
|
+
if (!isOperationType(typeCompiler.type)) continue;
|
|
393
|
+
if (defaultScopes[typeCompiler.type] == null) continue;
|
|
394
|
+
if (hasAuth(typeCompiler.useMetadata(stateKey).get())) continue;
|
|
395
|
+
for (const fieldCompiler of typeCompiler.fields) {
|
|
396
|
+
if (hasAuth(readFieldAuthState(fieldCompiler, stateKey))) continue;
|
|
397
|
+
const middleware = createFallbackMiddleware(typeCompiler.type, loadScopesFn, cacheKeyMap, defaultScopes, globalOptions.errorResolver);
|
|
398
|
+
if (!middleware) continue;
|
|
399
|
+
if (fieldCompiler.kind === "Field") fieldCompiler.addTopLevelMiddleware(middleware);
|
|
400
|
+
else fieldCompiler.addTopLevelSubscribeMiddleware(middleware);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
rule,
|
|
406
|
+
scope
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function buildMiddlewareName(type, field, subscriptionFieldKind) {
|
|
410
|
+
if (field && subscriptionFieldKind) return `${type}.${field}.${subscriptionFieldKind}.$use.auth`;
|
|
411
|
+
if (field) return `${type}.${field}.$use.auth`;
|
|
412
|
+
return `${type}.$use.auth`;
|
|
413
|
+
}
|
|
414
|
+
function hasAuth(state) {
|
|
415
|
+
return state?.hasAuth === true;
|
|
416
|
+
}
|
|
417
|
+
function readFieldAuthState(field, key) {
|
|
418
|
+
return field.kind === "Field" ? field.useMetadata(key).get() : field.useSubscribeMetadata(key).get();
|
|
419
|
+
}
|
|
420
|
+
function* iterateTypes(compilers) {
|
|
421
|
+
for (const compiler of compilers) for (const typeCompiler of compiler.types) yield typeCompiler;
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
export { aggregateErrorResolver, createAuth, createScopeCacheKey };
|
|
425
|
+
|
|
426
|
+
//# sourceMappingURL=index.js.map
|