field-guard 0.1.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/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/combineGuards.d.ts +14 -0
- package/dist/combineGuards.d.ts.map +1 -0
- package/dist/combineGuards.js +13 -0
- package/dist/combineGuards.js.map +1 -0
- package/dist/defineGuard.d.ts +32 -0
- package/dist/defineGuard.d.ts.map +1 -0
- package/dist/defineGuard.js +57 -0
- package/dist/defineGuard.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/mergeFieldVerdicts.d.ts +4 -0
- package/dist/mergeFieldVerdicts.d.ts.map +1 -0
- package/dist/mergeFieldVerdicts.js +12 -0
- package/dist/mergeFieldVerdicts.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Masaaki Ota
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# field-guard
|
|
2
|
+
|
|
3
|
+
A lightweight, fully type-safe, field-level access control library for TypeScript.
|
|
4
|
+
|
|
5
|
+
Define **who** can see **which fields** of your data — with zero runtime dependencies.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔒 **Field-level access control** — Grant or deny access per field, per access level
|
|
10
|
+
- 🏗️ **Builder pattern API** — Chainable `.withDerive()` and `.withCheck()` for composable guards
|
|
11
|
+
- 🔀 **Merge strategies** — Combine multiple verdicts via `union` or `intersection`
|
|
12
|
+
- 🧩 **Composable** — Combine multiple guards into a single object with `combineGuards`
|
|
13
|
+
- 🦺 **Fully type-safe** — All fields, levels, and results are inferred from your definitions
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npm install field-guard
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
import { defineGuard, combineGuards, mergeFieldVerdicts } from "field-guard";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Core Concepts
|
|
26
|
+
|
|
27
|
+
| Concept | Description |
|
|
28
|
+
| ------------- | --------------------------------------------------------------------------- |
|
|
29
|
+
| **Field** | A string literal representing a property name (e.g. `"id"`, `"email"`) |
|
|
30
|
+
| **Level** | An access level label (e.g. `"owner"`, `"public"`, `"admin"`) |
|
|
31
|
+
| **Policy** | A mapping from levels to field permissions (`true`, `false`, or per-field) |
|
|
32
|
+
| **Verdict** | The resolved result: a list of allowed fields with helper methods |
|
|
33
|
+
| **Context** | Arbitrary user/session data passed into guards at evaluation time |
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### 1. Define a Guard
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { defineGuard } from "field-guard";
|
|
41
|
+
|
|
42
|
+
type Ctx = { userId: string; role: "admin" | "user" };
|
|
43
|
+
type User = { id: string; email: string; name: string };
|
|
44
|
+
|
|
45
|
+
const userGuard = defineGuard<"owner" | "other", Ctx>()({
|
|
46
|
+
fields: ["id", "email", "name"],
|
|
47
|
+
policy: {
|
|
48
|
+
owner: true, // all fields allowed
|
|
49
|
+
other: { id: true, name: true }, // whitelist mode — only id and name
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### Policy Modes
|
|
55
|
+
|
|
56
|
+
- **`true`** — Allow all fields for this level
|
|
57
|
+
- **`false`** — Deny all fields for this level
|
|
58
|
+
- **Whitelist** `{ id: true, name: true }` — Only explicitly listed fields are allowed
|
|
59
|
+
- **Blacklist** `{ secretField: false }` — All fields allowed *except* those set to `false`
|
|
60
|
+
|
|
61
|
+
> The mode is auto-detected: if any value is `true`, it's whitelist mode; if all values are `false`, it's blacklist mode.
|
|
62
|
+
|
|
63
|
+
### 2. Add a Target Check
|
|
64
|
+
|
|
65
|
+
Use `.withCheck<T>()` to resolve the access level based on the context and a target object:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const userGuard = defineGuard<"owner" | "other", Ctx>()({
|
|
69
|
+
fields: ["id", "email", "name"],
|
|
70
|
+
policy: {
|
|
71
|
+
owner: true,
|
|
72
|
+
other: { id: true, name: true },
|
|
73
|
+
},
|
|
74
|
+
}).withCheck<User>()(({ ctx, target, verdictMap }) => {
|
|
75
|
+
const level = ctx.userId === target.id ? "owner" : "other";
|
|
76
|
+
return verdictMap[level];
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Evaluate the Guard
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const guard = userGuard.for({ userId: "1", role: "user" });
|
|
84
|
+
|
|
85
|
+
const verdict = guard.check({ id: "1", email: "me@example.com", name: "Me" });
|
|
86
|
+
verdict.allowedFields; // ["id", "email", "name"]
|
|
87
|
+
|
|
88
|
+
const verdict2 = guard.check({ id: "2", email: "other@example.com", name: "Other" });
|
|
89
|
+
verdict2.allowedFields; // ["id", "name"]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 4. Use Verdict Helpers
|
|
93
|
+
|
|
94
|
+
Each `FieldVerdict` comes with two convenience methods:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
verdict.coversAll(["id", "name"]); // true — all requested fields are allowed
|
|
98
|
+
verdict.coversSome(["email"]); // true — at least one requested field is allowed
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 5. Derive Extra Properties
|
|
102
|
+
|
|
103
|
+
Use `.withDerive()` to compute additional properties from the context:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const guard = defineGuard<"public", Ctx>()({
|
|
107
|
+
fields: ["id", "email"],
|
|
108
|
+
policy: { public: true },
|
|
109
|
+
}).withDerive(({ ctx }) => ({
|
|
110
|
+
isAdmin: ctx.role === "admin",
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
const g = guard.for({ userId: "1", role: "admin" });
|
|
114
|
+
g.isAdmin; // true
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 6. Combine Multiple Guards
|
|
118
|
+
|
|
119
|
+
Use `combineGuards` to bundle guards for different resources and bind them all at once:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { combineGuards } from "field-guard";
|
|
123
|
+
|
|
124
|
+
const guards = combineGuards<Ctx>()({
|
|
125
|
+
users: userGuard,
|
|
126
|
+
posts: postGuard,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const g = guards.for({ userId: "1", role: "user" });
|
|
130
|
+
|
|
131
|
+
g.users.check({ id: "1", email: "a@b.com", name: "A" });
|
|
132
|
+
g.posts.check({ id: "p1", content: "hello", authorId: "1" });
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 7. Merge Verdicts
|
|
136
|
+
|
|
137
|
+
Merge multiple verdicts with `union` (any-of) or `intersection` (all-of) strategy:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { mergeFieldVerdicts } from "field-guard";
|
|
141
|
+
|
|
142
|
+
// Union: field is allowed if ANY verdict allows it
|
|
143
|
+
mergeFieldVerdicts("union", [verdictA, verdictB], fields);
|
|
144
|
+
|
|
145
|
+
// Intersection: field is allowed only if ALL verdicts allow it
|
|
146
|
+
mergeFieldVerdicts("intersection", [verdictA, verdictB], fields);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This is also available as `mergeVerdicts` on every guard instance:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const guard = defineGuard<"owner" | "admin", Ctx>()({ /* ... */ });
|
|
153
|
+
|
|
154
|
+
const verdict = guard.mergeVerdicts("union", { owner: true, admin: false });
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## API Reference
|
|
158
|
+
|
|
159
|
+
### `defineGuard<Levels, Context>()`
|
|
160
|
+
|
|
161
|
+
Returns a factory function that accepts `{ fields, policy }` and returns a guard chain.
|
|
162
|
+
|
|
163
|
+
### Guard Chain Methods
|
|
164
|
+
|
|
165
|
+
| Method | Description |
|
|
166
|
+
| ------------------------- | ------------------------------------------------------------------ |
|
|
167
|
+
| `.withDerive(fn)` | Add derived properties computed from context |
|
|
168
|
+
| `.withCheck<Target>()(fn)` | Add a `check(target)` method that resolves a verdict per target |
|
|
169
|
+
| `.for(ctx)` | Bind context and return the resolved guard object |
|
|
170
|
+
|
|
171
|
+
### Guard Base Properties
|
|
172
|
+
|
|
173
|
+
| Property | Description |
|
|
174
|
+
| --------------- | -------------------------------------------------------- |
|
|
175
|
+
| `fields` | The full list of field names |
|
|
176
|
+
| `verdictMap` | Pre-computed `FieldVerdictMap` for each level |
|
|
177
|
+
| `mergeVerdicts` | Helper to merge verdicts by level flags |
|
|
178
|
+
|
|
179
|
+
### `combineGuards<Context>()(guards)`
|
|
180
|
+
|
|
181
|
+
Combines multiple guards into a single object with a shared `.for(ctx)` method.
|
|
182
|
+
|
|
183
|
+
### `mergeFieldVerdicts(mode, verdicts, fields)`
|
|
184
|
+
|
|
185
|
+
Merges an array of `FieldVerdict` objects using `"union"` or `"intersection"` strategy.
|
|
186
|
+
|
|
187
|
+
### `FieldVerdict<F>`
|
|
188
|
+
|
|
189
|
+
| Property | Type | Description |
|
|
190
|
+
| ---------------- | ----------------------- | ---------------------------------------- |
|
|
191
|
+
| `allowedFields` | `F[]` | List of allowed field names |
|
|
192
|
+
| `coversAll(fs)` | `(fields: F[]) => boolean` | `true` if all given fields are allowed |
|
|
193
|
+
| `coversSome(fs)` | `(fields: F[]) => boolean` | `true` if any given field is allowed |
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type GuardWithFor<A> = {
|
|
2
|
+
for: (actor: A) => Record<string, unknown>;
|
|
3
|
+
};
|
|
4
|
+
type Params<A> = Record<string, GuardWithFor<A>>;
|
|
5
|
+
type BoundGuards<A, P extends Params<A>> = {
|
|
6
|
+
[K in keyof P]: P[K] extends {
|
|
7
|
+
for: (actor: A) => infer R;
|
|
8
|
+
} ? R : never;
|
|
9
|
+
};
|
|
10
|
+
export declare function combineGuards<A>(): <P extends Params<A>>(params: P) => {
|
|
11
|
+
for: (actor: A) => BoundGuards<A, P>;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=combineGuards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combineGuards.d.ts","sourceRoot":"","sources":["../src/combineGuards.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,CAAC,CAAC,IAAI;IAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AACtE,KAAK,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;AACjD,KAAK,WAAW,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,IAAI;KACxC,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;QAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK;CACxE,CAAC;AAEF,wBAAgB,aAAa,CAAC,CAAC,MACrB,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC;iBAErB,CAAC,KAAG,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;EAUvC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function combineGuards() {
|
|
2
|
+
return (params) => {
|
|
3
|
+
return {
|
|
4
|
+
for: (actor) => {
|
|
5
|
+
return Object.fromEntries(Object.entries(params).map(([key, guard]) => [
|
|
6
|
+
key,
|
|
7
|
+
guard.for(actor),
|
|
8
|
+
]));
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=combineGuards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combineGuards.js","sourceRoot":"","sources":["../src/combineGuards.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAsB,MAAS,EAAE,EAAE;QACxC,OAAO;YACL,GAAG,EAAE,CAAC,KAAQ,EAAqB,EAAE;gBACnC,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;oBAC3C,GAAG;oBACH,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;iBACjB,CAAC,CACkB,CAAC;YACzB,CAAC;SACF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type MergeFieldVerdictsMode } from "./mergeFieldVerdicts.js";
|
|
2
|
+
import { type FieldPolicy, type FieldVerdict, type FieldVerdictMap } from "./types.js";
|
|
3
|
+
type BaseParams<C, L extends string, F extends string> = {
|
|
4
|
+
ctx: C;
|
|
5
|
+
fields: F[];
|
|
6
|
+
verdictMap: FieldVerdictMap<L, F>;
|
|
7
|
+
mergeVerdicts: (mode: MergeFieldVerdictsMode, flags: Partial<Record<L, boolean>>) => FieldVerdict<F>;
|
|
8
|
+
};
|
|
9
|
+
export type DeriveParams<C, L extends string, F extends string> = BaseParams<C, L, F>;
|
|
10
|
+
export type ResolveParams<C, T, L extends string, F extends string> = BaseParams<C, L, F> & {
|
|
11
|
+
target: T;
|
|
12
|
+
};
|
|
13
|
+
export type GuardBase<L extends string, F extends string> = {
|
|
14
|
+
fields: F[];
|
|
15
|
+
verdictMap: FieldVerdictMap<L, F>;
|
|
16
|
+
mergeVerdicts: (mode: MergeFieldVerdictsMode, flags: Partial<Record<L, boolean>>) => FieldVerdict<F>;
|
|
17
|
+
};
|
|
18
|
+
export type GuardChain<C, L extends string, F extends string, R extends Record<string, unknown>, HasDerive extends boolean = false, HasResolve extends boolean = false> = GuardBase<L, F> & {
|
|
19
|
+
for(ctx: C): R;
|
|
20
|
+
} & (HasDerive extends false ? {
|
|
21
|
+
withDerive<D extends Record<string, unknown>>(fn: (p: DeriveParams<C, L, F>) => D): GuardChain<C, L, F, R & D, true, HasResolve>;
|
|
22
|
+
} : {}) & (HasResolve extends false ? {
|
|
23
|
+
withCheck<T>(): <M>(fn: (p: ResolveParams<C, T, L, F>) => M) => GuardChain<C, L, F, R & {
|
|
24
|
+
check: (target: T) => M;
|
|
25
|
+
}, HasDerive, true>;
|
|
26
|
+
} : {});
|
|
27
|
+
export declare function defineGuard<L extends string, C>(): <F extends string>(params: {
|
|
28
|
+
fields: F[];
|
|
29
|
+
policy: FieldPolicy<L, F>;
|
|
30
|
+
}) => GuardChain<C, L, F, Record<string, never>, false, false>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=defineGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defineGuard.d.ts","sourceRoot":"","sources":["../src/defineGuard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mergeFieldVerdicts } from "./mergeFieldVerdicts.js";
|
|
2
|
+
import { createVerdict } from "./types.js";
|
|
3
|
+
function buildVerdictMap(policy, fields) {
|
|
4
|
+
return Object.fromEntries(Object.entries(policy).map(([_level, _mask]) => {
|
|
5
|
+
const level = _level;
|
|
6
|
+
const mask = _mask;
|
|
7
|
+
if (typeof mask === "boolean") {
|
|
8
|
+
return [level, createVerdict(mask ? fields : [])];
|
|
9
|
+
}
|
|
10
|
+
const isWhiteListMode = Object.values(mask).length === 0 || Object.values(mask).some((v) => v === true);
|
|
11
|
+
const allowedFields = isWhiteListMode
|
|
12
|
+
? fields.filter((f) => mask[f] === true)
|
|
13
|
+
: fields.filter((f) => mask[f] !== false);
|
|
14
|
+
return [level, createVerdict(allowedFields)];
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function createChain(fields, verdictMap, mergeVerdicts, deriveFn, resolveFn) {
|
|
18
|
+
return {
|
|
19
|
+
fields,
|
|
20
|
+
verdictMap,
|
|
21
|
+
mergeVerdicts,
|
|
22
|
+
withDerive(fn) {
|
|
23
|
+
const prevDeriveFn = deriveFn;
|
|
24
|
+
const nextDeriveFn = (p) => ({
|
|
25
|
+
...(prevDeriveFn ? prevDeriveFn(p) : {}),
|
|
26
|
+
...fn(p),
|
|
27
|
+
});
|
|
28
|
+
return createChain(fields, verdictMap, mergeVerdicts, nextDeriveFn, resolveFn);
|
|
29
|
+
},
|
|
30
|
+
withCheck() {
|
|
31
|
+
return (fn) => {
|
|
32
|
+
return createChain(fields, verdictMap, mergeVerdicts, deriveFn, fn);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
for(ctx) {
|
|
36
|
+
const baseParams = { ctx, fields, verdictMap, mergeVerdicts };
|
|
37
|
+
const derived = deriveFn ? deriveFn(baseParams) : {};
|
|
38
|
+
const result = resolveFn
|
|
39
|
+
? { ...derived, check: (target) => resolveFn({ ...baseParams, target }) }
|
|
40
|
+
: { ...derived };
|
|
41
|
+
return result;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function defineGuard() {
|
|
46
|
+
return (params) => {
|
|
47
|
+
const { fields, policy } = params;
|
|
48
|
+
const verdictMap = buildVerdictMap(policy, fields);
|
|
49
|
+
const _mergeVerdicts = (mode, flags) => {
|
|
50
|
+
const levels = Object.keys(flags).filter((l) => flags[l]);
|
|
51
|
+
const verdicts = levels.map((l) => verdictMap[l]);
|
|
52
|
+
return mergeFieldVerdicts(mode, verdicts, fields);
|
|
53
|
+
};
|
|
54
|
+
return createChain(fields, verdictMap, _mergeVerdicts, undefined, undefined);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=defineGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defineGuard.js","sourceRoot":"","sources":["../src/defineGuard.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { FieldVerdict } from "./types.js";
|
|
2
|
+
export type MergeFieldVerdictsMode = "union" | "intersection";
|
|
3
|
+
export declare function mergeFieldVerdicts<F extends string>(mode: MergeFieldVerdictsMode, verdicts: FieldVerdict<F>[], fields: F[]): FieldVerdict<F>;
|
|
4
|
+
//# sourceMappingURL=mergeFieldVerdicts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mergeFieldVerdicts.d.ts","sourceRoot":"","sources":["../src/mergeFieldVerdicts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,YAAY,EAAE,MAAM,YAAY,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,cAAc,CAAC;AAE9D,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EACjD,IAAI,EAAE,sBAAsB,EAC5B,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,EAC3B,MAAM,EAAE,CAAC,EAAE,GACV,YAAY,CAAC,CAAC,CAAC,CAYjB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createVerdict } from "./types.js";
|
|
2
|
+
export function mergeFieldVerdicts(mode, verdicts, fields) {
|
|
3
|
+
if (verdicts.length === 0) {
|
|
4
|
+
return createVerdict([]);
|
|
5
|
+
}
|
|
6
|
+
const allowedSets = verdicts.map((v) => new Set(v.allowedFields));
|
|
7
|
+
const allowedFields = mode === "union"
|
|
8
|
+
? fields.filter((f) => allowedSets.some((set) => set.has(f)))
|
|
9
|
+
: fields.filter((f) => allowedSets.every((set) => set.has(f)));
|
|
10
|
+
return createVerdict(allowedFields);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=mergeFieldVerdicts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mergeFieldVerdicts.js","sourceRoot":"","sources":["../src/mergeFieldVerdicts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAgB,MAAM,YAAY,CAAC;AAIzD,MAAM,UAAU,kBAAkB,CAChC,IAA4B,EAC5B,QAA2B,EAC3B,MAAW;IAEX,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,aAAa,CAAI,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;IAElE,MAAM,aAAa,GAAG,IAAI,KAAK,OAAO;QACpC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjE,OAAO,aAAa,CAAC,aAAa,CAAC,CAAC;AACtC,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type FieldRule<F extends string> = Record<F, boolean>;
|
|
2
|
+
export type FieldPolicy<L extends string, F extends string> = Record<L, boolean | Partial<FieldRule<F>>>;
|
|
3
|
+
export type FieldVerdict<F extends string> = {
|
|
4
|
+
allowedFields: F[];
|
|
5
|
+
coversAll: (fields: F[]) => boolean;
|
|
6
|
+
coversSome: (fields: F[]) => boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function createVerdict<F extends string>(allowedFields: F[]): FieldVerdict<F>;
|
|
9
|
+
export type FieldVerdictMap<L extends string, F extends string> = Record<L, FieldVerdict<F>>;
|
|
10
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,MAAM,IAAI,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AAG7D,MAAM,MAAM,WAAW,CACrB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,MAAM,IACd,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAM/C,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,IAAI;IAC3C,aAAa,EAAE,CAAC,EAAE,CAAC;IACnB,SAAS,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC;IACpC,UAAU,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC;CACtC,CAAC;AAEF,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,aAAa,EAAE,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,CAOnF;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,IAAI,MAAM,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const fieldRule = { f1: true, f2: false };
|
|
2
|
+
const fieldPolicy = {
|
|
3
|
+
public: { f1: true },
|
|
4
|
+
private: { f1: true, f2: false },
|
|
5
|
+
};
|
|
6
|
+
export function createVerdict(allowedFields) {
|
|
7
|
+
const set = new Set(allowedFields);
|
|
8
|
+
return {
|
|
9
|
+
allowedFields,
|
|
10
|
+
coversAll: (fields) => fields.every((f) => set.has(f)),
|
|
11
|
+
coversSome: (fields) => fields.some((f) => set.has(f)),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,MAAM,SAAS,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAA2B,CAAC;AAMnE,MAAM,WAAW,GAAG;IAClB,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;IACpB,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE;CACD,CAAC;AAQlC,MAAM,UAAU,aAAa,CAAmB,aAAkB;IAChE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAI,aAAa,CAAC,CAAC;IACtC,OAAO;QACL,aAAa;QACb,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACtD,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KACvD,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "field-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight, fully type-safe, field-level access control library for TypeScript",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"test": "bun run vitest run",
|
|
22
|
+
"prepublishOnly": "bun run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"field",
|
|
26
|
+
"guard",
|
|
27
|
+
"access-control",
|
|
28
|
+
"permission",
|
|
29
|
+
"authorization",
|
|
30
|
+
"field-level",
|
|
31
|
+
"typescript",
|
|
32
|
+
"type-safe"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/mohhh-ok/field-guard.git"
|
|
38
|
+
},
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.8.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
}
|
|
44
|
+
}
|