Skip to content

Commit 29e4ccd

Browse files
authored
feat!: migrate to Bun, publish ESM-only exports, add framework integrations (#30)
- Tooling: switch scripts/CI to Bun - Packaging: ESM-only build, modern exports map + wildcard subpath exports for integrations (permask/<integration>) - Integrations: express, fastify, h3, nitro, nestjs, hono, koa, itty-router, elysia (thin adapters) - Core: add PermaskError, base64 helpers, portable packBitmasks/unpackBitmasks, add formatBitmask BREAKING CHANGE: package is ESM-only (CJS/UMD entry points removed) BREAKING CHANGE: unpackBitmasks now throws PermaskError on invalid input BREAKING CHANGE: createPermask().create now throws PermaskError for unknown groups
1 parent 985d5c8 commit 29e4ccd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4506
-6633
lines changed

.github/workflows/release.yml

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,35 @@ jobs:
2525
- uses: actions/checkout@v4
2626
with:
2727
fetch-depth: 0
28-
- name: Install pnpm
29-
uses: pnpm/action-setup@v4
28+
- name: Setup Bun
29+
uses: oven-sh/setup-bun@v2
3030
with:
31-
version: 10
31+
bun-version: "1.3.1"
3232
- name: Use Node.js
3333
uses: actions/setup-node@v4
3434
with:
3535
node-version: 22
36-
cache: 'pnpm'
36+
37+
- name: Use cache
38+
uses: actions/cache@v4
39+
with:
40+
path: |
41+
~/.bun/install/cache
42+
node_modules
43+
*/*/node_modules
44+
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
45+
restore-keys: |
46+
bun-${{ runner.os }}-
47+
3748
- name: Install dependencies
38-
run: pnpm install
49+
run: bun install --frozen-lockfile
3950
- name: Build
40-
run: pnpm build
51+
run: bun run build
4152
- name: Release
4253
env:
4354
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4455
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
45-
run: pnpm dlx semantic-release
56+
run: npx semantic-release
4657

4758
playground:
4859
name: Playground
@@ -52,26 +63,29 @@ jobs:
5263

5364
steps:
5465
- uses: actions/checkout@v4
55-
- name: Install pnpm
56-
uses: pnpm/action-setup@v4
66+
- name: Setup Bun
67+
uses: oven-sh/setup-bun@v2
5768
with:
58-
version: 10
59-
- name: Use Node.js
60-
uses: actions/setup-node@v4
69+
bun-version: "1.3.1"
70+
71+
- name: Use cache
72+
uses: actions/cache@v4
6173
with:
62-
node-version: 22
63-
cache: 'pnpm'
74+
path: |
75+
~/.bun/install/cache
76+
node_modules
77+
*/*/node_modules
78+
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
79+
restore-keys: |
80+
bun-${{ runner.os }}-
81+
6482
- name: Install dependencies
65-
run: |
66-
cd playground/vue
67-
pnpm install
83+
run: bun install --frozen-lockfile
6884
- name: Build playground
69-
run: |
70-
cd playground/vue
71-
pnpm build
85+
working-directory: playground/vue
86+
run: bun run build
7287
- name: Deploy to GitHub Pages
7388
uses: peaceiris/actions-gh-pages@v4
7489
with:
7590
github_token: ${{ secrets.GITHUB_TOKEN }}
7691
publish_dir: ./playground/vue/dist
77-

.github/workflows/test.yml

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: Test
2+
23
on:
4+
push:
5+
branches:
6+
- main
37
pull_request:
48
branches:
59
- main
@@ -8,9 +12,6 @@ jobs:
812
test:
913
name: Test
1014
runs-on: ubuntu-latest
11-
strategy:
12-
matrix:
13-
node-version: [ 22 ]
1415

1516
permissions:
1617
# Required to checkout the code
@@ -20,19 +21,27 @@ jobs:
2021

2122
steps:
2223
- uses: actions/checkout@v4
23-
- name: Install pnpm
24-
uses: pnpm/action-setup@v4
24+
- name: Setup Bun
25+
uses: oven-sh/setup-bun@v2
2526
with:
26-
version: 10
27-
- name: Use Node.js ${{ matrix.node-version }}
28-
uses: actions/setup-node@v4
27+
bun-version: "1.3.1"
28+
29+
- name: Use cache
30+
uses: actions/cache@v4
2931
with:
30-
node-version: ${{ matrix.node-version }}
31-
cache: 'pnpm'
32+
path: |
33+
~/.bun/install/cache
34+
node_modules
35+
*/*/node_modules
36+
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
37+
restore-keys: |
38+
bun-${{ runner.os }}-
39+
3240
- name: Install dependencies
33-
run: pnpm install
41+
run: bun install --frozen-lockfile
42+
3443
- name: 'Test'
35-
run: pnpm test:coverage
44+
run: bun run test:coverage
3645
- name: 'Report Coverage'
3746
if: always() # Also generate the report if tests are failing
3847
uses: davelosert/vitest-coverage-report-action@v2

AGENTS.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Repository Guidelines
2+
3+
## Project Structure
4+
5+
- `src/`: main TypeScript source (utilities, constants, integrations).
6+
- `src/**/*.test.ts`: Vitest unit tests colocated with code.
7+
- `playground/vue/`: Vue playground for interactive/manual validation.
8+
- `dist/`: build output (generated; don’t edit by hand).
9+
- `.github/workflows/`: CI and release pipelines (Bun-based).
10+
11+
## Build, Test, and Development Commands
12+
13+
This repo uses **Bun** as the package manager and runtime.
14+
Use the version pinned in `package.json#packageManager` (CI matches it).
15+
16+
```bash
17+
bun install # install deps (uses bun.lock)
18+
bun run build # build library bundles + types into dist/
19+
bun run test # run vitest (watch/default mode)
20+
bun run test:coverage # run tests + V8 coverage summary
21+
bun run lint # biome check for formatting + lint
22+
bun run lint:fix # apply biome fixes where safe
23+
```
24+
25+
Playground (from repo root):
26+
27+
```bash
28+
bun --cwd=playground/vue run dev
29+
bun --cwd=playground/vue run build
30+
```
31+
32+
## Coding Style & Naming Conventions
33+
34+
- Language: TypeScript (ESM, `strict`).
35+
- Formatting/linting: **Biome** (`bunx biome check .`).
36+
- Indentation: 2 spaces; line width: 120.
37+
- File naming: `kebab-case.ts` for modules; tests as `*.test.ts`.
38+
39+
## Testing Guidelines
40+
41+
- Framework: **Vitest**.
42+
- Keep tests close to the code they validate (`src/**`).
43+
- Add/extend tests for new behavior and bug fixes; prefer deterministic tests.
44+
- Run locally before pushing: `bun run test:coverage`.
45+
46+
## Commit & Pull Request Guidelines
47+
48+
- This repo’s public changelog is generated by **semantic-release**, so commit messages must follow **Conventional Commits** (enforced by commitlint).
49+
- Prefer small, focused commits (one logical change per commit), because release notes are derived from them.
50+
- Use scopes to keep the changelog readable: `core`, `integrations`, `docs`, `playground`, `ci`, `build`.
51+
- Examples: `feat(core): add formatBitmask utility`, `feat(integrations): add elysia adapter`, `fix(pack): throw on invalid base64`, `chore(ci): switch workflows to bun`.
52+
- Breaking changes: use `feat!:` / `fix!:` or add a `BREAKING CHANGE:` footer.
53+
- PRs should include: a short description, rationale, and any relevant usage notes.
54+
- Requirements: `bun run lint`, `bun run test:coverage`, and `bun run build` must pass; avoid committing generated `dist/` output.

README.md

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ For example in UNIX file systems, bitmasks are used to manage file permissions (
1717
- Fast: Bitwise operations (&, |) are faster than comparing strings.
1818
- Compact: Combine multiple permissions in a single integer (e.g., 0b1111 for read, create, update, delete).
1919
Just 4 bits for access control. For groups, you can use any number of bits,
20-
- Flexible: Easy to check, add, or remove permissions.
20+
- Flexible: Easy to check, add or remove permissions.
2121

2222
Example of using bitmasks:
2323

@@ -51,6 +51,8 @@ Group(1) Permissions(read, create, update)
5151
Install `permask`:
5252

5353
```bash
54+
# bun
55+
bun add permask
5456
# npm
5557
npm install permask
5658
# pnpm
@@ -93,20 +95,18 @@ const permask = createPermask(PermissionGroup);
9395

9496
- #### create a bitmask from an object:
9597
```ts
96-
const bitmask2 = permask.create({
98+
console.log(permask.create({
9799
group: "LIKE",
98100
read: true,
99101
create: false,
100102
update: true,
101103
delete: false
102-
});
103-
console.log(bitmask2); // 53 (0b110101)
104+
})); // 53 (0b110101)
104105
```
105106

106107
- #### parse a bitmask to an object:
107108
```ts
108-
const parsed = permask.parse(31); // 0b11111
109-
console.log(parsed);
109+
console.log(permask.parse(31)); // 0b11111
110110
// {
111111
// group: 1,
112112
// groupName: "POST",
@@ -120,36 +120,33 @@ console.log(parsed);
120120
- #### check if a bitmask has a specific group:
121121

122122
```ts
123-
const hasGroup = permask.hasGroup(23, "LIKE");
124-
console.log(hasGroup); // true
123+
console.log(permask.hasGroup(23, "LIKE")); // true
125124
// You can also use numeric group IDs
126125
const hasGroupById = permask.hasGroup(23, PermissionGroup.LIKE);
127126
```
128127

129128
- #### check if a bitmask has a specific permission:
130129
```ts
131-
const canRead = permask.canRead(17);
132-
const canCreate = permask.canCreate(17);
133-
const canDelete = permask.canDelete(17);
134-
const canUpdate = permask.canUpdate(17);
135-
console.log(canRead, canCreate, canDelete, canUpdate); // true, false, false, false
130+
console.log(
131+
permask.canRead(17),
132+
permask.canCreate(17),
133+
permask.canDelete(17),
134+
permask.canUpdate(17)
135+
); // true, false, false, false
136136
```
137137

138138
- #### check if a bitmask has access to a specific group and permission:
139139
```ts
140140
import { PermissionAccess } from "permask";
141141

142142
// Check if bitmask has read access to LIKE group using string access
143-
const hasReadAccess = permask.hasAccess(53, "LIKE", "read");
144-
console.log(hasReadAccess); // true
143+
console.log(permask.hasAccess(53, "LIKE", "read")); // true
145144

146145
// Check if bitmask has create access to LIKE group using string access
147-
const hasCreateAccess = permask.hasAccess(53, "LIKE", "create");
148-
console.log(hasCreateAccess); // false
146+
console.log(permask.hasAccess(53, "LIKE", "create")); // false
149147

150148
// You can also use numeric group IDs with string access
151-
const hasAccessById = permask.hasAccess(53, PermissionGroup.LIKE, "update");
152-
console.log(hasAccessById); // true
149+
console.log(permask.hasAccess(53, PermissionGroup.LIKE, "update")); // true
153150

154151
// You can use numeric access values (PermissionAccess) instead of strings
155152
const hasUpdateAccessNumeric = permask.hasAccess(53, PermissionGroup.LIKE, PermissionAccess.UPDATE);
@@ -158,10 +155,8 @@ console.log(hasUpdateAccessNumeric); // true
158155

159156
- #### get group name from bitmask:
160157
```ts
161-
const groupName = permask.getGroupName(23);
162-
console.log(groupName); // "LIKE"
163-
const groupName2 = permask.getGroupName(29);
164-
console.log(groupName2); // undefined
158+
console.log(permask.getGroupName(23)); // "LIKE"
159+
console.log(permask.getGroupName(29)); // undefined
165160
```
166161

167162

@@ -173,25 +168,35 @@ You can use `permask` just with bitmask utility functions.
173168

174169
### Use bitmask utilities:
175170

171+
#### `access` name vs `access` mask
172+
173+
- **Access name**: `"read" | "create" | "update" | "delete"` (used in `createPermask().hasAccess(...)`).
174+
- **Access mask**: a number made from `PermissionAccess` flags (used in low-level utils), e.g. `PermissionAccess.READ | PermissionAccess.UPDATE`.
175+
176176
**Functions:**
177177
- `createBitmask({ group: number, read: boolean, create: boolean, delete: boolean, update: boolean }): number` - creates a bitmask from an options.
178178
- `parseBitmask(bitmask: number): { group: number, read: boolean, create: boolean, delete: boolean, update: boolean }` - parses a bitmask and returns an object.
179179
- `getPermissionGroup(bitmask: number): number` - returns a group number from a bitmask.
180-
- `getPermissionAccess(bitmask: number): number` - returns an access number from a bitmask.
180+
- `getPermissionAccess(bitmask: number): number` - returns an access mask (4 bits) from a bitmask.
181181
- `hasPermissionGroup(bitmask: number, group: number): boolean` - checks if a bitmask has a specific group.
182-
- `hasPermissionAccess(bitmask: number, access: number): boolean` - checks if a bitmask has a specific access.
183-
- `hasRequiredPermission(bitmasks: number[], group: number, access: number): boolean` - checks if any bitmask in the array has the required permission for the specified group and access.
182+
- `hasPermissionAccess(bitmask: number, accessMask: number): boolean` - checks if a bitmask has **any** access flag from `accessMask`.
183+
- `hasAllPermissionAccess(bitmask: number, accessMask: number): boolean` - checks if a bitmask has **all** access flags from `accessMask`.
184+
- `hasRequiredPermission(bitmasks: number[], group: number, accessMask: number): boolean` - checks if any bitmask has the required group and **any** access flag from `accessMask`.
184185

185186
useful functions:
186187
- `canRead(bitmask: number): boolean`
187188
- `canCreate(bitmask: number): boolean`
188189
- `canDelete(bitmask: number): boolean`
189190
- `canUpdate(bitmask: number): boolean`
190191
- `setPermissionGroup(bitmask: number, group: number): number` - sets a group in a bitmask (will overwrite the previous group).
191-
- `setPermissionAccess(bitmask: number, access: number): number` - sets access in a bitmask (will overwrite the previous access).
192-
- `getPermissionBitmask(group: number, access: number): number` - creates a bitmask from a group and access.
193-
- `packBitbasks(bitmasks: number[], urlSafe?: boolean): string` - packs bitmasks to base64 string. (more compact than JSON.stringify)
194-
- `unpackBitmasks(base64: string, urlSafe?: boolean): number[]` - unpacks bitmasks from a base64 string.
192+
- `setPermissionAccess(bitmask: number, accessMask: number): number` - sets access mask (will overwrite the previous access).
193+
- `removePermissionAccess(bitmask: number, accessMask: number): number` - removes access flags from a bitmask.
194+
- `togglePermissionAccess(bitmask: number, accessMask: number): number` - toggles access flags in a bitmask.
195+
- `clearPermissionAccess(bitmask: number): number` - clears all access flags in a bitmask.
196+
- `getPermissionBitmask(group: number, accessMask: number): number` - creates a bitmask from a group and an access mask.
197+
- `packBitmasks(bitmasks: number[], urlSafe?: boolean): string` - packs bitmasks to base64 string (more compact than JSON.stringify).
198+
- `unpackBitmasks(packed: string, urlSafe?: boolean): number[]` - unpacks bitmasks from a packed string (throws `PermaskError` on invalid input).
199+
- `formatBitmask(bitmask: number, groups?: Record<string, number>): string` - formats a bitmask into a readable string for logging/debugging.
195200

196201
**Constants:**
197202
- `PermissionAccess` - an enum-like object with access types.
@@ -212,8 +217,19 @@ You can use `permask` just with bitmask utility functions.
212217
} as const;
213218
```
214219

220+
## Integrations
221+
222+
Framework integrations are published as subpath exports (ESM-only). See [src/integrations/README.md](src/integrations/README.md).
215223

216-
## [Integration with frameworks](https://github.com/dschewchenko/permask/blob/main/integrations/README.md)
224+
- Express: `import { permaskExpress } from "permask/express";`
225+
- Fastify: `import { permaskFastify } from "permask/fastify";`
226+
- H3: `import { permaskH3 } from "permask/h3";`
227+
- Nitro: `import { permaskNitro } from "permask/nitro";`
228+
- NestJS: `import { permaskNestjs } from "permask/nestjs";`
229+
- Hono: `import { permaskHono } from "permask/hono";`
230+
- Koa: `import { permaskKoa } from "permask/koa";`
231+
- itty-router: `import { permaskIttyRouter } from "permask/itty-router";`
232+
- Elysia: `import { permaskElysia } from "permask/elysia";`
217233

218234
## How I'm using it?
219235

@@ -236,15 +252,16 @@ If you have any questions or suggestions, feel free to open an issue or pull req
236252
- [x] Create a library
237253
- [x] Add tests
238254
- [x] Add documentation
239-
- [ ] Add easy-to-use integration with frameworks
240-
- [x] Express
241-
- [ ] Fastify
242-
- [ ] H3
243-
- [ ] Nitro
244-
- [ ] NestJS
245-
- [ ] Hono
246-
- [ ] Koa
247-
- [ ] itty-router
255+
- [x] Add easy-to-use integration with frameworks (subpath exports, ESM-only)
256+
- [x] Express (`permask/express`) — Express middleware
257+
- [x] Fastify (`permask/fastify`) — `preHandler` hook (works well with `@fastify/auth`)
258+
- [x] H3 (`permask/h3`) — `requirePermask(event, ...)` + middleware
259+
- [x] Nitro (`permask/nitro`) — H3-based helpers + handler wrapper
260+
- [x] NestJS (`permask/nestjs`) — Decorator + Guard
261+
- [x] Hono (`permask/hono`) — middleware (context-based)
262+
- [x] Koa (`permask/koa`) — async middleware (typically via `ctx.state`)
263+
- [x] itty-router (`permask/itty-router`) — handler / `before` middleware returning `Response`
264+
- [x] Elysia (`permask/elysia`) — `beforeHandle` helper (usable inside `guard`)
248265

249266

250267
## License

0 commit comments

Comments
 (0)