• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nopeless / hellgate / 5403036151

pending completion
5403036151

push

github

nopeless
move to esm spec

39 of 46 branches covered (84.78%)

Branch coverage included in aggregate %.

46 of 46 new or added lines in 12 files covered. (100.0%)

841 of 1200 relevant lines covered (70.08%)

5.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.12
/src/hellgate/hellgate.ts
1
/**
8✔
2
 * Hellgate 16
8✔
3
 */
8✔
4
import assert from "node:assert";
8✔
5
import { isPromise } from "util/types";
8✔
6
import type { Merge, MergeParameters } from "../types.js";
8✔
7

8✔
8
type HasPromiseNoDistribution<T> = (
8✔
9
  T extends Promise<unknown> ? true : false
8✔
10
) extends false
8✔
11
  ? false
8✔
12
  : true;
8✔
13
type MaybePromise<T> = T | Promise<T>;
8✔
14
type Fn = (this: never, ...args: never[]) => unknown;
8✔
15
type P0<F extends Fn> = Parameters<F>[0];
8✔
16

×
17
function isObject(v: unknown): v is object {
×
18
  return Object(v) === v;
×
19
}
×
20

×
21
function then(
×
22
  f: () => MaybePromise<boolean | undefined>,
8✔
23
  g: (v: boolean | undefined) => MaybePromise<boolean | undefined>
1✔
24
): () => MaybePromise<boolean | undefined> {
1✔
25
  return () => {
1✔
26
    const v = f();
1✔
27
    if (isPromise(v)) {
1✔
28
      return v.then(g);
1✔
29
    }
×
30
    return g(v);
×
31
  };
×
32
}
×
33

×
34
// type Merge<A, B> = { [K in keyof A | keyof B]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : never };
1✔
35
type InquiryResult = {
×
36
  damned: Record<string, unknown> | null;
×
37
  // Deferred values if there is a function call involved
×
38
  value: () => MaybePromise<boolean | undefined>;
×
39
  processed: boolean;
×
40
  final?: boolean | undefined;
1✔
41
};
×
42

×
43
type PermissionFunctionProperties<V extends boolean | undefined> = {
×
44
  /** Function properties
×
45
   * Override can overturn a parent's decision
×
46
   */
×
47
  override?: boolean;
×
48
  /** Final means that the return value cannot be overridden
×
49
   * It does NOT mean that it can overturn a parent's decision
×
50
   */
×
51
  final?: boolean;
×
52
  /** If the function returns a constant value
×
53
   * This can also be a getter, as long as it returns a consistent value
×
54
   * If it is a getter, then it should return the same value as if the
×
55
   * function was called
×
56
   *
×
57
   * Used to mark final booleans
×
58
   */
×
59
  value?: V;
×
60
};
×
61

×
62
type PermissionFunction<
×
63
  This = never,
×
64
  User extends Record<string, unknown> = never,
×
65
  Action extends string = never,
×
66
  Meta extends unknown[] = never[]
×
67
  // Rarely used
1✔
68
> = ((
×
69
  this: This,
×
70
  user: User | null,
×
71
  action: Action,
×
72
  ...meta: Meta
×
73
) => MaybePromise<boolean | undefined>) &
×
74
  PermissionFunctionProperties<boolean | undefined>;
×
75

×
76
// Mapper function
1✔
77
type _PermissionsAddFunctionProperties<Perms extends Permissions> = [
×
78
  keyof Perms
×
79
] extends [never]
×
80
  ? Perms
×
81
  : {
×
82
      [K in keyof Perms]: Perms[K] extends ((
×
83
        ...args: never[]
×
84
      ) => infer R extends MaybePromise<boolean | undefined>)
×
85
        ? Perms[K] & PermissionFunctionProperties<Awaited<R>>
×
86
        : // Ideally the below this should never happen
×
87
          Perms[K];
×
88
    };
×
89

×
90
type Permission<This = never, User extends Record<string, unknown> = never> =
×
91
  | undefined
×
92
  | boolean
×
93
  | PermissionFunction<This, User>;
×
94

×
95
type PermissionWide<
×
96
  This = never,
1✔
97
  User extends Record<string, unknown> = never
1✔
98
> = undefined | boolean | PermissionFunction<This, User, string, unknown[]>;
8✔
99

×
100
type Permissions<
×
101
  This = never,
×
102
  User extends Record<string, unknown> = never
×
103
> = Record<string, Permission<This, User>>;
×
104

×
105
type PermissionsWide<
×
106
  This = never,
×
107
  User extends Record<string, unknown> = never
×
108
> = Record<string, PermissionWide<This, User>>;
×
109

×
110
// Extract meta and create new function
×
111
type _PermissionMetaArgumentsFunction<F extends PermissionFunction> =
×
112
  F extends (
×
113
    u: never,
×
114
    a: never,
×
115
    ...meta: infer Meta extends unknown[]
×
116
  ) => unknown
×
117
    ? (...args: Meta) => unknown
×
118
    : never;
×
119

×
120
type MetaFunction<P extends Permission> = _PermissionMetaArgumentsFunction<
×
121
  Extract<P, PermissionFunction>
×
122
>;
×
123

×
124
type AggregatePermissions<
×
125
  // User is an invariant; therefore any is needed
×
126
  R extends IRing<any>,
×
127
  A extends string
×
128
> = R[`__TYPE_Real`] extends true
×
129
  ? R[`permissions`] extends infer P
×
130
    ? // First, get permission of current
×
131
      | (A extends keyof P
×
132
            ? P[A] extends infer F
×
133
              ? F extends boolean
×
134
                ? F
×
135
                : F extends (...args: never[]) => infer R
×
136
                ? R
×
137
                : never
×
138
              : never
×
139
            : never)
×
140
        | (R[`parent`] extends IRing<any>
×
141
            ? AggregatePermissions<R[`parent`], A>
×
142
            : never)
×
143
    : never
×
144
  : never;
×
145

×
146
type AggregatePermissionKeys<
×
147
  // User is an invariant; therefore any is needed
×
148
  R extends IRing<any>
×
149
> = R[`__TYPE_Real`] extends true
×
150
  ? R[`permissions`] extends infer P
×
151
    ? // First, get permission of current
×
152
      | keyof P
×
153
        | (R[`parent`] extends IRing<any>
×
154
            ? AggregatePermissionKeys<R[`parent`]>
×
155
            : never)
×
156
    : never
×
157
  : never;
×
158

×
159
export type ArrayOfPermissionFunctions<
×
160
  // User is an invariant; therefore any is needed
×
161
  R extends IRing<any>,
×
162
  A extends string
×
163
> = R[`__TYPE_Real`] extends true
×
164
  ? R[`permissions`] extends infer P
×
165
    ? // First, get permission of current
×
166
      R[`parent`] extends infer Pa extends IRing<any>
×
167
      ? [
×
168
          ...ArrayOfPermissionFunctions<Pa, A>,
×
169
          ...(A extends keyof P
×
170
            ? P[A] extends Fn
×
171
              ? [MetaFunction<Extract<P[A], PermissionFunction>>]
8✔
172
              : []
8✔
173
            : [])
8✔
174
        ]
8✔
175
      : A extends keyof P
8✔
176
      ? P[A] extends Fn
8✔
177
        ? [MetaFunction<Extract<P[A], PermissionFunction>>]
8✔
178
        : []
8✔
179
      : []
8✔
180
    : []
8✔
181
  : [];
×
182

×
183
type MetaParameters<
8✔
184
  // User is an invariant; therefore any is needed
8✔
185
  R extends IRing<any>,
8✔
186
  A extends string
8✔
187
> = MergeParameters<ArrayOfPermissionFunctions<R, A>>;
8✔
188

8✔
189
interface IRing<User extends Record<string, unknown>, UserResolvable = never> {
8✔
190
  parent: IRing<User, UserResolvable> | null;
8✔
191
  __TYPE_Real: boolean;
8✔
192
  __TYPE_User: User;
8✔
193
  __TYPE_UserResolvable: (user: UserResolvable) => unknown;
8✔
194
  __TYPE_Damned: Record<string, unknown>;
8✔
195
  permissions: Permissions;
8✔
196
  inquire(
8✔
197
    user: UserResolvable | User | null,
8✔
198
    // action should always be at least a string
8✔
199
    // overloaded later
8✔
200
    action: string,
8✔
201
    // I am not entirely sure how to type this
8✔
202
    ...meta: any
8✔
203
  ): InquiryResult;
8✔
204
  damn(damned: Record<string, unknown> | null): Record<string, unknown>;
8✔
205
  summon(damned: User): Record<string, unknown>;
8✔
206
  getUser(user: UserResolvable): MaybePromise<User | null>;
8✔
207
  exists(action: string): boolean;
8✔
208
  can(
8✔
209
    user: User | null,
8✔
210
    action: string,
8✔
211
    ...meta: any
8✔
212
  ): MaybePromise<boolean | undefined>;
8✔
213
}
8✔
214

8✔
215
type HellgateOptions<
8✔
216
  User extends Record<string, unknown>,
8✔
217
  UserResolvable,
8✔
218
  Sin extends Record<string, unknown>
8✔
219
> = {
8✔
220
  getUser(user: UserResolvable | User): MaybePromise<User | null>;
8✔
221
  getSin?(user: User): Sin;
8✔
222
  damn?(user: User, sin: Record<string, unknown>): Merge<User, Sin>;
8✔
223
  final?: boolean;
8✔
224
};
8✔
225

8✔
226
type RingOptions<
8✔
227
  User extends Record<string, unknown> = never,
8✔
228
  Sin extends Record<string, unknown> = Record<string, unknown>
8✔
229
> = {
8✔
230
  getSin?(damned: User): Sin;
8✔
231
  final?: boolean;
8✔
232
  override?: boolean;
8✔
233
};
8✔
234

8✔
235
class Hellgate<
8✔
236
  OptionsLiteral extends HellgateOptions<User, any, Sin>,
8✔
237
  User extends Record<string, unknown>,
8✔
238
  UserResolvable,
8✔
239
  Sin extends Record<string, unknown> = {},
8✔
240
  Perms extends Permissions<
8✔
241
    IRing<User, UserResolvable>,
8✔
242
    Merge<User, Sin>
8✔
243
  > = Permissions<IRing<User, UserResolvable>, Merge<User, Sin>>
8✔
244
> implements IRing<User, UserResolvable>
8✔
245
{
8✔
246
  __TYPE_Real!: true;
8✔
247
  __TYPE_User!: User;
8✔
248
  __TYPE_Damned!: Merge<User, Sin>;
8✔
249
  __TYPE_UserResolvable!: (user: UserResolvable) => unknown;
8✔
250

8✔
251
  get parent() {
8✔
252
    return null;
8✔
253
  }
8✔
254

8✔
255
  constructor(
8✔
256
    public options: OptionsLiteral & HellgateOptions<User, UserResolvable, Sin>,
8✔
257
    public permissions: _PermissionsAddFunctionProperties<Perms> = {} as _PermissionsAddFunctionProperties<Perms>
8✔
258
  ) {
8✔
259
    //
8✔
260
  }
8✔
261

8✔
262
  public exists<K extends string>(
8✔
263
    action: K
8✔
264
  ): action is K & keyof this[`permissions`] {
8✔
265
    return Object.hasOwn(this.permissions, action);
8✔
266
  }
8✔
267

8✔
268
  public can<K extends AggregatePermissionKeys<this>>(
8✔
269
    user: User | null,
8✔
270
    action: K,
8✔
271
    ...meta: MetaParameters<this, K>
8✔
272
  ): AggregatePermissions<this, K>;
8✔
273
  public can<K extends AggregatePermissionKeys<this>>(
8✔
274
    user: P0<OptionsLiteral[`getUser`]> | User | null,
8✔
275
    action: K,
8✔
276
    ...meta: MetaParameters<this, K>
8✔
277
  ): HasPromiseNoDistribution<
8✔
278
    ReturnType<OptionsLiteral[`getUser`]>
8✔
279
  > extends true
8✔
280
    ? MaybePromise<Awaited<AggregatePermissions<this, K>>>
8✔
281
    : AggregatePermissions<this, K>;
8✔
282
  public can(
8✔
283
    user: P0<OptionsLiteral[`getUser`]>,
8✔
284
    action: string,
8✔
285
    ...meta: any
8✔
286
  ): MaybePromise<boolean | undefined> {
8✔
287
    const u = this.options.getUser(user);
8✔
288
    if (isPromise(u)) {
8✔
289
      return u.then((u) => this.inquire(u, action, ...meta).value());
8✔
290
    }
8✔
291
    return this.inquire(u, action, ...meta).value();
8✔
292
  }
8✔
293

8✔
294
  public inquire(
8✔
295
    cleanUser: User | null,
8✔
296
    action: string,
8✔
297
    ...meta: any
8✔
298
  ): InquiryResult {
8✔
299
    const user = cleanUser === null ? null : this.damn(cleanUser);
8✔
300

8✔
301
    if (!Object.hasOwn(this.permissions, action)) {
8✔
302
      // Permission does not exist
8✔
303
      return {
8✔
304
        damned: user,
8✔
305
        value: () => undefined,
8✔
306
        processed: false,
8✔
307
      };
8✔
308
    }
8✔
309

8✔
310
    const permission = this.permissions[action];
8✔
311
    let final: boolean | undefined;
8✔
312
    let value: () => MaybePromise<boolean | undefined>;
8✔
313
    if (typeof permission === `function`) {
8✔
314
      final = permission.final ?? this.options.final;
8✔
315
      if (Object.hasOwn(permission, `value`)) {
8✔
316
        value = () => permission.value;
8✔
317
      } else {
8✔
318
        value = () =>
8✔
319
          permission.bind(this)(user, action as never, ...(meta as never[]));
8✔
320
      }
8✔
321
    } else {
8✔
322
      value = () => permission;
8✔
323
    }
8✔
324

8✔
325
    return {
8✔
326
      damned: user,
8✔
327
      value,
8✔
328
      processed: true,
8✔
329
      final,
8✔
330
    };
8✔
331
  }
8✔
332
  public damn(user: User): Merge<User, Sin> {
8✔
333
    const sin = this.options.getSin?.(user) ?? ({} as Sin);
8✔
334

8✔
335
    const u: typeof user = { ...user };
8✔
336
    Reflect.setPrototypeOf(u, Object.getPrototypeOf(user));
8✔
337

8✔
338
    Object.assign(u, sin);
8✔
339

8✔
340
    return u as Record<string, unknown> as Merge<User, Sin>;
8✔
341
  }
8✔
342

8✔
343
  public get getUser(): OptionsLiteral[`getUser`] {
8✔
344
    return (u: User) => {
8✔
345
      const r = this.options.getUser(u);
8✔
346

8✔
347
      if (isPromise(r)) {
8✔
348
        return r.then((v) => {
8✔
349
          // Assert that it is object
8✔
350
          assert(
8✔
351
            v === null || isObject(v),
8✔
352
            `Hellgate.getUser must return a MaybePromise<User | null>. Return resolved to ${v}`
8✔
353
          );
8✔
354

8✔
355
          return v;
8✔
356
        });
8✔
357
      }
8✔
358

8✔
359
      assert(
8✔
360
        r === null || isObject(r),
8✔
361
        `Hellgate.getUser must return a MaybePromise<User | null>. Returned ${r}`
8✔
362
      );
8✔
363
      return r;
8✔
364
    };
8✔
365
  }
8✔
366

8✔
367
  /**
8✔
368
   * Summons a user to the current location
8✔
369
   */
8✔
370
  public summon(user: User) {
8✔
371
    return this.damn(user);
8✔
372
  }
8✔
373
}
8✔
374

8✔
375
class Ring<
8✔
376
  // Invariant infer
8✔
377
  Parent extends IRing<any>,
8✔
378
  Sin extends Record<string, unknown> = {},
8✔
379
  Perms extends Permissions<
8✔
380
    IRing<Parent[`__TYPE_User`], P0<Parent[`__TYPE_UserResolvable`]>>,
8✔
381
    Merge<Parent[`__TYPE_Damned`], Sin>
8✔
382
  > = PermissionsWide<
8✔
383
    IRing<Parent[`__TYPE_User`], P0<Parent[`__TYPE_UserResolvable`]>>,
8✔
384
    Merge<Parent[`__TYPE_Damned`], Sin>
8✔
385
  >
8✔
386
> implements IRing<Parent[`__TYPE_User`], P0<Parent[`__TYPE_UserResolvable`]>>
8✔
387
{
8✔
388
  __TYPE_Real!: true;
8✔
389
  __TYPE_User!: Parent[`__TYPE_User`];
8✔
390
  __TYPE_Damned!: Merge<Parent[`__TYPE_Damned`], Sin>;
8✔
391
  __TYPE_UserResolvable!: Parent[`__TYPE_UserResolvable`];
8✔
392

8✔
393
  constructor(
8✔
394
    public parent: Parent,
8✔
395
    public options: RingOptions<Parent[`__TYPE_Damned`], Sin> = {},
8✔
396
    // Desirable type inference behavior for permissions
8✔
397
    public permissions: _PermissionsAddFunctionProperties<Perms> = {} as _PermissionsAddFunctionProperties<Perms>
8✔
398
  ) {
8✔
399
    //
8✔
400
  }
8✔
401

8✔
402
  public exists<K extends string>(
8✔
403
    action: K
8✔
404
  ): action is K & AggregatePermissionKeys<this> {
8✔
405
    return (
8✔
406
      Object.hasOwn(this.permissions, action) || this.parent.exists(action)
8✔
407
    );
8✔
408
  }
8✔
409

8✔
410
  public get getUser() {
8✔
411
    return this.parent.getUser;
8✔
412
  }
8✔
413

8✔
414
  public damn(
8✔
415
    damned: Record<string, unknown>
8✔
416
  ): Merge<Parent[`__TYPE_Damned`], Sin> {
8✔
417
    const sin = this.options.getSin?.(damned) ?? ({} as Sin);
8✔
418

8✔
419
    Object.assign(damned, sin);
8✔
420

8✔
421
    return damned as Record<string, unknown> as Merge<
8✔
422
      Parent[`__TYPE_Damned`],
8✔
423
      Sin
8✔
424
    >;
8✔
425
  }
8✔
426

8✔
427
  public can<K extends AggregatePermissionKeys<this>>(
8✔
428
    user: Parent[`__TYPE_User`] | null,
8✔
429
    action: K,
8✔
430
    ...meta: MetaParameters<this, K>
8✔
431
  ): AggregatePermissions<this, K>;
8✔
432
  public can<K extends AggregatePermissionKeys<this>>(
8✔
433
    user: P0<Parent[`getUser`]> | Parent[`getUser`] | null,
8✔
434
    action: K,
8✔
435
    ...meta: MetaParameters<this, K>
8✔
436
  ): HasPromiseNoDistribution<ReturnType<Parent[`getUser`]>> extends true
8✔
437
    ? MaybePromise<Awaited<AggregatePermissions<this, K>>>
8✔
438
    : AggregatePermissions<this, K>;
8✔
439
  public can(
8✔
440
    user: P0<Parent[`getUser`]>,
8✔
441
    action: string,
8✔
442
    ...meta: any
8✔
443
  ): MaybePromise<boolean | undefined> {
8✔
444
    const u = this.parent.getUser(user) as MaybePromise<
8✔
445
      Parent[`__TYPE_User`] | null
8✔
446
    >;
8✔
447
    if (isPromise(u)) {
8✔
448
      return u.then((u) => this.inquire(u, action, ...meta).value());
8✔
449
    }
8✔
450
    return this.inquire(u, action, ...meta).value();
8✔
451
  }
8✔
452

8✔
453
  public inquire(
8✔
454
    cleanUser: Parent[`__TYPE_User`] | null,
8✔
455
    action: string,
8✔
456
    ...meta: any
8✔
457
  ): InquiryResult {
8✔
458
    const res = this.parent.inquire(cleanUser, action, ...meta);
8✔
459

8✔
460
    // Respect parent's final decision
8✔
461
    if (res.final) {
8✔
462
      return res;
8✔
463
    }
8✔
464

8✔
465
    const user = res.damned === null ? null : this.damn(res.damned);
8✔
466

8✔
467
    if (!Object.hasOwn(this.permissions, action)) {
8✔
468
      return {
8✔
469
        damned: user,
8✔
470
        value: res.value,
8✔
471
        processed: res.processed,
8✔
472
      };
8✔
473
    }
8✔
474

8✔
475
    const permission = this.permissions[action];
8✔
476
    let value: () => MaybePromise<boolean | undefined>;
8✔
477
    let override: boolean | undefined = false;
8✔
478
    let final: boolean | undefined;
8✔
479

8✔
480
    if (typeof permission === `function`) {
8✔
481
      override = permission.override ?? this.options.override;
8✔
482
      final = permission.final ?? this.options.final;
8✔
483
      if (Object.hasOwn(permission, `value`)) {
8✔
484
        value = () => permission.value;
8✔
485
      } else {
8✔
486
        value = () =>
8✔
487
          permission.bind(this)(user, action as never, ...(meta as never[]));
8✔
488
      }
8✔
489
    } else {
8✔
490
      value = () => permission;
8✔
491
    }
8✔
492

8✔
493
    return {
8✔
494
      damned: user,
8✔
495
      value: then(value, (v) => {
8✔
496
        if (override) {
8✔
497
          return v;
8✔
498
        }
8✔
499

8✔
500
        if (v === false) {
8✔
501
          return false;
8✔
502
        } else if (v === true) {
8✔
503
          return res.value() ?? true;
8✔
504
        }
8✔
505
        return res.value();
8✔
506
      }),
8✔
507
      final,
8✔
508
      processed: true,
8✔
509
    };
8✔
510
  }
8✔
511

8✔
512
  /**
8✔
513
   * Summons a user to the current location
8✔
514
   */
8✔
515
  public summon(user: Parent[`__TYPE_User`]) {
8✔
516
    return this.damn(this.parent.summon(user));
8✔
517
  }
8✔
518
}
8✔
519

8✔
520
export { Hellgate, Ring };
8✔
521
export type {
8✔
522
  IRing,
8✔
523
  Permission,
8✔
524
  PermissionFunction,
8✔
525
  PermissionFunctionProperties,
8✔
526
  MetaFunction,
8✔
527
};
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc