jurist 0.0.0

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/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # jurist
2
+
3
+ ```sh
4
+ npm i jurist
5
+ ```
6
+
7
+ Jurist is a TypeScript-first authorization library supporting role-based access control.
8
+
9
+ ## usage
10
+
11
+ ### `Laws`
12
+
13
+ With Jurist, permissions are expressed in a tree structure.
14
+
15
+ ```typescript
16
+ // my-authorization.ts
17
+ import { Laws, optional, required } from "jurist";
18
+
19
+ const authorization = new Laws({
20
+ roles: ["suspended", "free", "premium"],
21
+ permissions: required({
22
+ create: optional({
23
+ document: required({
24
+ "<=5": optional({ "<=5000": null }),
25
+ shareDocuments: null,
26
+ }),
27
+ }),
28
+ }),
29
+ rolePermissions: {
30
+ suspended: new Set([]),
31
+ free: new Set([
32
+ /* a granted permission grants all preceding permissions, e.g.,
33
+ "create"
34
+ "create_documents" */
35
+ "create_documents_<=5",
36
+ ]),
37
+ premium: new Set([
38
+ "create_documents_<=5_<=5000",
39
+ "create_documents_shareDocuments",
40
+ ]),
41
+ },
42
+ });
43
+ ```
44
+
45
+ The idea here is that some permissions must logically follow from other permissions. In other words, the `create_documents_<=5` permission is a prerequisite for the `create_documents_<=5_<=5000` permission, because if you are allowed to create 5,000 documents, you are logically allowed to create 5 documents in the first place.
46
+
47
+ This makes authorization checks easier, safer, and more explicit.
48
+
49
+ ```typescript
50
+ // routes.ts
51
+ import { Roles } from "jurist";
52
+
53
+ import { authorization } from "./my-authorization";
54
+ import { app } from "./my-hono-like-app";
55
+
56
+ type Role = Roles<typeof authorization>; // "free" | "premium" | "suspended"
57
+
58
+ type User = {
59
+ id: string;
60
+ role: Role;
61
+ };
62
+
63
+ app.post("/api/v1/resources", async (c) => {
64
+ const { user /* { id: "123-456", role: "free" } */ } = c.get("user");
65
+ /* ✅ true */ authorization.check(user.role, "create");
66
+ /* ✅ true */ authorization.check(user.role, "create_documents_<=5");
67
+ /* ❌ false */ authorization.check(user.role, "create_documents_<=5_<=5000");
68
+ });
69
+ ```
70
+
71
+ Here, we can see that a free user has permission to create a resource, and has permission to own up to 5 documents, but they cannot own up to 5,000 documents like a premium user.
@@ -0,0 +1,43 @@
1
+ import { Tree, Join, TreePathName } from 'treetrunks';
2
+ export * from 'treetrunks';
3
+
4
+ type Roles<L extends Laws<any, any, any, any>> = L extends Laws<infer Role, any, any, any> ? Role : never;
5
+ type Permissions<L extends Laws<any, any, any, any>> = L extends Laws<any, any, infer Permission, any> ? Permission : never;
6
+ type PermissionData<L extends Laws<any, any, any, any>, D> = Entries<Permissions<L>, D>;
7
+ declare class Laws<Role extends string, PermissionTree extends Tree, Permission extends Join<TreePathName<PermissionTree>, `_`>, RolePermissions extends {
8
+ [r in Role]: ReadonlySet<Permission>;
9
+ }> {
10
+ readonly roles: Role[];
11
+ readonly permissionTree: PermissionTree;
12
+ readonly rolePermissions: RolePermissions;
13
+ protected readonly decompressedRolePermissions: {
14
+ [r in Role]: ReadonlySet<string>;
15
+ };
16
+ constructor(options: {
17
+ roles: Role[];
18
+ permissions: PermissionTree;
19
+ rolePermissions: RolePermissions;
20
+ });
21
+ check(role: Role, permission: Permission): boolean;
22
+ }
23
+ type EscalatorStyle = `firstFound` | `lastFound` | `untilMiss`;
24
+ declare class Escalator<S extends EscalatorStyle, L extends Laws<any, any, any, any>, P extends PermissionData<L, any>, D extends P extends PermissionData<L, infer d> ? d : never, F> {
25
+ readonly style: S;
26
+ readonly laws: L;
27
+ readonly permissionData: P;
28
+ readonly fallback: F;
29
+ constructor(options: Readonly<{
30
+ style: S;
31
+ laws: L;
32
+ permissionData: P;
33
+ fallback: F;
34
+ }>);
35
+ get(role: Roles<L>): D | F;
36
+ }
37
+ type Count<N extends number, A extends any[] = []> = [
38
+ ...A,
39
+ any
40
+ ][`length`] extends N ? A[`length`] : A[`length`] | Count<N, [...A, any]>;
41
+ type Entries<K extends PropertyKey = keyof any, V = any> = [K, V][];
42
+
43
+ export { type Count, Escalator, type EscalatorStyle, Laws, type PermissionData, type Permissions, type Roles };
package/dist/jurist.js ADDED
@@ -0,0 +1,78 @@
1
+ export * from 'treetrunks';
2
+
3
+ // src/jurist.ts
4
+ var Laws = class {
5
+ roles;
6
+ permissionTree;
7
+ rolePermissions;
8
+ decompressedRolePermissions;
9
+ constructor(options) {
10
+ const { roles, permissions, rolePermissions } = options;
11
+ this.roles = roles;
12
+ this.permissionTree = permissions;
13
+ this.rolePermissions = rolePermissions;
14
+ this.decompressedRolePermissions = fromEntries(
15
+ toEntries(rolePermissions).map(
16
+ ([role, permissionsOfRole]) => [
17
+ role,
18
+ decompressRolePermissions(permissionsOfRole)
19
+ ]
20
+ )
21
+ );
22
+ }
23
+ check(role, permission) {
24
+ return this.decompressedRolePermissions[role].has(permission);
25
+ }
26
+ };
27
+ function decompressRolePermissions(permissionSet) {
28
+ const decompressed = new Set(permissionSet);
29
+ for (const permission of permissionSet) {
30
+ const preconditions = permission.split(`_`);
31
+ for (let i = 0; i < preconditions.length - 1; i++) {
32
+ const subPermission = preconditions.slice(0, i + 1).join(`_`);
33
+ decompressed.add(subPermission);
34
+ }
35
+ }
36
+ return decompressed;
37
+ }
38
+ var Escalator = class {
39
+ style;
40
+ laws;
41
+ permissionData;
42
+ fallback;
43
+ constructor(options) {
44
+ const { style, laws, permissionData, fallback } = options;
45
+ this.style = style;
46
+ this.laws = laws;
47
+ this.permissionData = permissionData;
48
+ this.fallback = fallback;
49
+ }
50
+ get(role) {
51
+ let result = this.fallback;
52
+ for (const [permission, data] of this.permissionData) {
53
+ const hasPermission = this.laws.check(role, permission);
54
+ switch (this.style) {
55
+ case `firstFound`:
56
+ if (hasPermission) return data;
57
+ break;
58
+ case `lastFound`:
59
+ if (hasPermission) result = data;
60
+ break;
61
+ case `untilMiss`:
62
+ if (hasPermission) result = data;
63
+ else return result;
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+ };
69
+ function fromEntries(entries) {
70
+ return Object.fromEntries(entries);
71
+ }
72
+ function toEntries(obj) {
73
+ return Object.entries(obj);
74
+ }
75
+
76
+ export { Escalator, Laws };
77
+ //# sourceMappingURL=jurist.js.map
78
+ //# sourceMappingURL=jurist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/jurist.ts"],"names":[],"mappings":";;;AA2BO,IAAM,OAAN,MAKL;AAAA,EACe,KAAA;AAAA,EACA,cAAA;AAAA,EACA,eAAA;AAAA,EACG,2BAAA;AAAA,EAIZ,YAAY,OAIhB,EAAA;AACF,IAAA,MAAM,EAAE,KAAA,EAAO,WAAa,EAAA,eAAA,EAAoB,GAAA,OAAA;AAChD,IAAA,IAAA,CAAK,KAAQ,GAAA,KAAA;AACb,IAAA,IAAA,CAAK,cAAiB,GAAA,WAAA;AACtB,IAAA,IAAA,CAAK,eAAkB,GAAA,eAAA;AACvB,IAAA,IAAA,CAAK,2BAA8B,GAAA,WAAA;AAAA,MAClC,SAAA,CAA2B,eAAe,CAAE,CAAA,GAAA;AAAA,QAC3C,CAAC,CAAC,IAAM,EAAA,iBAAiB,CAAM,KAAA;AAAA,UAC9B,IAAA;AAAA,UACA,0BAA0B,iBAAiB;AAAA;AAC5C;AACD,KACD;AAAA;AACD,EAEO,KAAA,CAAM,MAAY,UAAiC,EAAA;AACzD,IAAA,OAAO,IAAK,CAAA,2BAAA,CAA4B,IAAI,CAAA,CAAE,IAAI,UAAU,CAAA;AAAA;AAE9D;AAEA,SAAS,0BACR,aACsB,EAAA;AACtB,EAAM,MAAA,YAAA,GAAe,IAAI,GAAA,CAAY,aAAa,CAAA;AAClD,EAAA,KAAA,MAAW,cAAc,aAAe,EAAA;AACvC,IAAM,MAAA,aAAA,GAAgB,UAAW,CAAA,KAAA,CAAM,CAAG,CAAA,CAAA,CAAA;AAC1C,IAAA,KAAA,IAAS,IAAI,CAAG,EAAA,CAAA,GAAI,aAAc,CAAA,MAAA,GAAS,GAAG,CAAK,EAAA,EAAA;AAClD,MAAM,MAAA,aAAA,GAAgB,cAAc,KAAM,CAAA,CAAA,EAAG,IAAI,CAAC,CAAA,CAAE,KAAK,CAAG,CAAA,CAAA,CAAA;AAC5D,MAAA,YAAA,CAAa,IAAI,aAAa,CAAA;AAAA;AAC/B;AAED,EAAO,OAAA,YAAA;AACR;AAGO,IAAM,YAAN,MAML;AAAA,EACe,KAAA;AAAA,EACA,IAAA;AAAA,EACA,cAAA;AAAA,EACA,QAAA;AAAA,EACT,YACN,OAMC,EAAA;AACD,IAAA,MAAM,EAAE,KAAA,EAAO,IAAM,EAAA,cAAA,EAAgB,UAAa,GAAA,OAAA;AAClD,IAAA,IAAA,CAAK,KAAQ,GAAA,KAAA;AACb,IAAA,IAAA,CAAK,IAAO,GAAA,IAAA;AACZ,IAAA,IAAA,CAAK,cAAiB,GAAA,cAAA;AACtB,IAAA,IAAA,CAAK,QAAW,GAAA,QAAA;AAAA;AACjB,EAEO,IAAI,IAAuB,EAAA;AACjC,IAAA,IAAI,SAAgB,IAAK,CAAA,QAAA;AACzB,IAAA,KAAA,MAAW,CAAC,UAAA,EAAY,IAAI,CAAA,IAAK,KAAK,cAAgB,EAAA;AACrD,MAAA,MAAM,aAAgB,GAAA,IAAA,CAAK,IAAK,CAAA,KAAA,CAAM,MAAM,UAAU,CAAA;AACtD,MAAA,QAAQ,KAAK,KAAO;AAAA,QACnB,KAAK,CAAA,UAAA,CAAA;AACJ,UAAA,IAAI,eAAsB,OAAA,IAAA;AAC1B,UAAA;AAAA,QACD,KAAK,CAAA,SAAA,CAAA;AACJ,UAAA,IAAI,eAAwB,MAAA,GAAA,IAAA;AAC5B,UAAA;AAAA,QACD,KAAK,CAAA,SAAA,CAAA;AACJ,UAAA,IAAI,eAAwB,MAAA,GAAA,IAAA;AAAA,eAChB,OAAA,MAAA;AAAA;AACd;AAED,IAAO,OAAA,MAAA;AAAA;AAET;AAyBA,SAAS,YAA+B,OAA4B,EAAA;AACnE,EAAO,OAAA,MAAA,CAAO,YAAY,OAAO,CAAA;AAClC;AAEA,SAAS,UAA4B,GAAsC,EAAA;AAC1E,EAAO,OAAA,MAAA,CAAO,QAAQ,GAAG,CAAA;AAC1B","file":"jurist.js","sourcesContent":["import type { Join, Tree, TreePathName } from \"treetrunks\"\n\nexport * from \"treetrunks\"\n\nexport type Roles<L extends Laws<any, any, any, any>> = L extends Laws<\n\tinfer Role,\n\tany,\n\tany,\n\tany\n>\n\t? Role\n\t: never\n\nexport type Permissions<L extends Laws<any, any, any, any>> = L extends Laws<\n\tany,\n\tany,\n\tinfer Permission,\n\tany\n>\n\t? Permission\n\t: never\n\nexport type PermissionData<L extends Laws<any, any, any, any>, D> = Entries<\n\tPermissions<L>,\n\tD\n>\n\nexport class Laws<\n\tRole extends string,\n\tPermissionTree extends Tree,\n\tPermission extends Join<TreePathName<PermissionTree>, `_`>,\n\tRolePermissions extends { [r in Role]: ReadonlySet<Permission> },\n> {\n\tpublic readonly roles: Role[]\n\tpublic readonly permissionTree: PermissionTree\n\tpublic readonly rolePermissions: RolePermissions\n\tprotected readonly decompressedRolePermissions: {\n\t\t[r in Role]: ReadonlySet<string>\n\t}\n\n\tpublic constructor(options: {\n\t\troles: Role[]\n\t\tpermissions: PermissionTree\n\t\trolePermissions: RolePermissions\n\t}) {\n\t\tconst { roles, permissions, rolePermissions } = options\n\t\tthis.roles = roles\n\t\tthis.permissionTree = permissions\n\t\tthis.rolePermissions = rolePermissions\n\t\tthis.decompressedRolePermissions = fromEntries(\n\t\t\ttoEntries<RolePermissions>(rolePermissions).map(\n\t\t\t\t([role, permissionsOfRole]) => [\n\t\t\t\t\trole,\n\t\t\t\t\tdecompressRolePermissions(permissionsOfRole),\n\t\t\t\t],\n\t\t\t),\n\t\t)\n\t}\n\n\tpublic check(role: Role, permission: Permission): boolean {\n\t\treturn this.decompressedRolePermissions[role].has(permission)\n\t}\n}\n\nfunction decompressRolePermissions(\n\tpermissionSet: ReadonlySet<string>,\n): ReadonlySet<string> {\n\tconst decompressed = new Set<string>(permissionSet)\n\tfor (const permission of permissionSet) {\n\t\tconst preconditions = permission.split(`_`)\n\t\tfor (let i = 0; i < preconditions.length - 1; i++) {\n\t\t\tconst subPermission = preconditions.slice(0, i + 1).join(`_`)\n\t\t\tdecompressed.add(subPermission)\n\t\t}\n\t}\n\treturn decompressed\n}\n\nexport type EscalatorStyle = `firstFound` | `lastFound` | `untilMiss`\nexport class Escalator<\n\tS extends EscalatorStyle,\n\tL extends Laws<any, any, any, any>,\n\tP extends PermissionData<L, any>,\n\tD extends P extends PermissionData<L, infer d> ? d : never,\n\tF,\n> {\n\tpublic readonly style: S\n\tpublic readonly laws: L\n\tpublic readonly permissionData: P\n\tpublic readonly fallback: F\n\tpublic constructor(\n\t\toptions: Readonly<{\n\t\t\tstyle: S\n\t\t\tlaws: L\n\t\t\tpermissionData: P\n\t\t\tfallback: F\n\t\t}>,\n\t) {\n\t\tconst { style, laws, permissionData, fallback } = options\n\t\tthis.style = style\n\t\tthis.laws = laws\n\t\tthis.permissionData = permissionData\n\t\tthis.fallback = fallback\n\t}\n\n\tpublic get(role: Roles<L>): D | F {\n\t\tlet result: D | F = this.fallback\n\t\tfor (const [permission, data] of this.permissionData) {\n\t\t\tconst hasPermission = this.laws.check(role, permission)\n\t\t\tswitch (this.style) {\n\t\t\t\tcase `firstFound`:\n\t\t\t\t\tif (hasPermission) return data\n\t\t\t\t\tbreak\n\t\t\t\tcase `lastFound`:\n\t\t\t\t\tif (hasPermission) result = data\n\t\t\t\t\tbreak\n\t\t\t\tcase `untilMiss`:\n\t\t\t\t\tif (hasPermission) result = data\n\t\t\t\t\telse return result\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n}\n\ntype Flat<R extends { [K in PropertyKey]: any }> = {\n\t[K in keyof R]: R[K]\n}\n\nexport type Count<N extends number, A extends any[] = []> = [\n\t...A,\n\tany,\n][`length`] extends N\n\t? A[`length`]\n\t: A[`length`] | Count<N, [...A, any]>\n\ntype Entries<K extends PropertyKey = keyof any, V = any> = [K, V][]\n\ntype KeyOfEntries<E extends Entries> = E extends [infer K, any][] ? K : never\n\ntype ValueOfEntry<E extends Entries, K extends KeyOfEntries<E>> = {\n\t[P in Count<E[`length`]>]: E[P] extends [K, infer V] ? V : never\n}[Count<E[`length`]>]\n\ntype FromEntries<E extends Entries> = Flat<{\n\t[K in KeyOfEntries<E>]: ValueOfEntry<E, K>\n}>\n\nfunction fromEntries<E extends Entries>(entries: E): FromEntries<E> {\n\treturn Object.fromEntries(entries) as FromEntries<E>\n}\n\nfunction toEntries<T extends object>(obj: T): Entries<keyof T, T[keyof T]> {\n\treturn Object.entries(obj) as Entries<keyof T, T[keyof T]>\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "jurist",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "author": {
6
+ "name": "Jeremy Banka",
7
+ "email": "hello@jeremybanka.com"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/jeremybanka/wayforge.git",
15
+ "directory": "packages/jurist"
16
+ },
17
+ "type": "module",
18
+ "files": ["dist", "src"],
19
+ "main": "dist/jurist.js",
20
+ "scripts": {
21
+ "build": "tsup-node",
22
+ "lint:biome": "biome check -- .",
23
+ "lint:eslint": "eslint -- .",
24
+ "lint:types": "tsc --noEmit",
25
+ "watch:types": "tsc --watch --noEmit",
26
+ "lint": "concurrently \"bun:lint:*\"",
27
+ "test": "vitest",
28
+ "test:once": "vitest run",
29
+ "postversion": "biome format --write package.json"
30
+ },
31
+ "dependencies": {
32
+ "treetrunks": "workspace:*"
33
+ },
34
+ "devDependencies": {
35
+ "concurrently": "9.1.2",
36
+ "eslint": "9.21.0",
37
+ "rimraf": "6.0.1",
38
+ "tsup": "8.4.0",
39
+ "vitest": "3.0.7"
40
+ }
41
+ }
package/src/jurist.ts ADDED
@@ -0,0 +1,155 @@
1
+ import type { Join, Tree, TreePathName } from "treetrunks"
2
+
3
+ export * from "treetrunks"
4
+
5
+ export type Roles<L extends Laws<any, any, any, any>> = L extends Laws<
6
+ infer Role,
7
+ any,
8
+ any,
9
+ any
10
+ >
11
+ ? Role
12
+ : never
13
+
14
+ export type Permissions<L extends Laws<any, any, any, any>> = L extends Laws<
15
+ any,
16
+ any,
17
+ infer Permission,
18
+ any
19
+ >
20
+ ? Permission
21
+ : never
22
+
23
+ export type PermissionData<L extends Laws<any, any, any, any>, D> = Entries<
24
+ Permissions<L>,
25
+ D
26
+ >
27
+
28
+ export class Laws<
29
+ Role extends string,
30
+ PermissionTree extends Tree,
31
+ Permission extends Join<TreePathName<PermissionTree>, `_`>,
32
+ RolePermissions extends { [r in Role]: ReadonlySet<Permission> },
33
+ > {
34
+ public readonly roles: Role[]
35
+ public readonly permissionTree: PermissionTree
36
+ public readonly rolePermissions: RolePermissions
37
+ protected readonly decompressedRolePermissions: {
38
+ [r in Role]: ReadonlySet<string>
39
+ }
40
+
41
+ public constructor(options: {
42
+ roles: Role[]
43
+ permissions: PermissionTree
44
+ rolePermissions: RolePermissions
45
+ }) {
46
+ const { roles, permissions, rolePermissions } = options
47
+ this.roles = roles
48
+ this.permissionTree = permissions
49
+ this.rolePermissions = rolePermissions
50
+ this.decompressedRolePermissions = fromEntries(
51
+ toEntries<RolePermissions>(rolePermissions).map(
52
+ ([role, permissionsOfRole]) => [
53
+ role,
54
+ decompressRolePermissions(permissionsOfRole),
55
+ ],
56
+ ),
57
+ )
58
+ }
59
+
60
+ public check(role: Role, permission: Permission): boolean {
61
+ return this.decompressedRolePermissions[role].has(permission)
62
+ }
63
+ }
64
+
65
+ function decompressRolePermissions(
66
+ permissionSet: ReadonlySet<string>,
67
+ ): ReadonlySet<string> {
68
+ const decompressed = new Set<string>(permissionSet)
69
+ for (const permission of permissionSet) {
70
+ const preconditions = permission.split(`_`)
71
+ for (let i = 0; i < preconditions.length - 1; i++) {
72
+ const subPermission = preconditions.slice(0, i + 1).join(`_`)
73
+ decompressed.add(subPermission)
74
+ }
75
+ }
76
+ return decompressed
77
+ }
78
+
79
+ export type EscalatorStyle = `firstFound` | `lastFound` | `untilMiss`
80
+ export class Escalator<
81
+ S extends EscalatorStyle,
82
+ L extends Laws<any, any, any, any>,
83
+ P extends PermissionData<L, any>,
84
+ D extends P extends PermissionData<L, infer d> ? d : never,
85
+ F,
86
+ > {
87
+ public readonly style: S
88
+ public readonly laws: L
89
+ public readonly permissionData: P
90
+ public readonly fallback: F
91
+ public constructor(
92
+ options: Readonly<{
93
+ style: S
94
+ laws: L
95
+ permissionData: P
96
+ fallback: F
97
+ }>,
98
+ ) {
99
+ const { style, laws, permissionData, fallback } = options
100
+ this.style = style
101
+ this.laws = laws
102
+ this.permissionData = permissionData
103
+ this.fallback = fallback
104
+ }
105
+
106
+ public get(role: Roles<L>): D | F {
107
+ let result: D | F = this.fallback
108
+ for (const [permission, data] of this.permissionData) {
109
+ const hasPermission = this.laws.check(role, permission)
110
+ switch (this.style) {
111
+ case `firstFound`:
112
+ if (hasPermission) return data
113
+ break
114
+ case `lastFound`:
115
+ if (hasPermission) result = data
116
+ break
117
+ case `untilMiss`:
118
+ if (hasPermission) result = data
119
+ else return result
120
+ }
121
+ }
122
+ return result
123
+ }
124
+ }
125
+
126
+ type Flat<R extends { [K in PropertyKey]: any }> = {
127
+ [K in keyof R]: R[K]
128
+ }
129
+
130
+ export type Count<N extends number, A extends any[] = []> = [
131
+ ...A,
132
+ any,
133
+ ][`length`] extends N
134
+ ? A[`length`]
135
+ : A[`length`] | Count<N, [...A, any]>
136
+
137
+ type Entries<K extends PropertyKey = keyof any, V = any> = [K, V][]
138
+
139
+ type KeyOfEntries<E extends Entries> = E extends [infer K, any][] ? K : never
140
+
141
+ type ValueOfEntry<E extends Entries, K extends KeyOfEntries<E>> = {
142
+ [P in Count<E[`length`]>]: E[P] extends [K, infer V] ? V : never
143
+ }[Count<E[`length`]>]
144
+
145
+ type FromEntries<E extends Entries> = Flat<{
146
+ [K in KeyOfEntries<E>]: ValueOfEntry<E, K>
147
+ }>
148
+
149
+ function fromEntries<E extends Entries>(entries: E): FromEntries<E> {
150
+ return Object.fromEntries(entries) as FromEntries<E>
151
+ }
152
+
153
+ function toEntries<T extends object>(obj: T): Entries<keyof T, T[keyof T]> {
154
+ return Object.entries(obj) as Entries<keyof T, T[keyof T]>
155
+ }