@pthm/melange 0.4.0 → 0.6.4
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 +294 -18
- package/dist/adapters/index.d.ts +10 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/pg.d.ts +42 -0
- package/dist/adapters/pg.d.ts.map +1 -0
- package/dist/adapters/pg.js +38 -0
- package/dist/adapters/pg.js.map +1 -0
- package/dist/adapters/postgres.d.ts +37 -0
- package/dist/adapters/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres.js +35 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/cache.d.ts +90 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +93 -0
- package/dist/cache.js.map +1 -0
- package/dist/cache.test.d.ts +5 -0
- package/dist/cache.test.d.ts.map +1 -0
- package/dist/cache.test.js +127 -0
- package/dist/cache.test.js.map +1 -0
- package/dist/checker.d.ts +169 -7
- package/dist/checker.d.ts.map +1 -1
- package/dist/checker.js +214 -9
- package/dist/checker.js.map +1 -1
- package/dist/database.d.ts +45 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +8 -0
- package/dist/database.js.map +1 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +58 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validator.d.ts +28 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +60 -0
- package/dist/validator.js.map +1 -0
- package/dist/validator.test.d.ts +5 -0
- package/dist/validator.test.d.ts.map +1 -0
- package/dist/validator.test.js +88 -0
- package/dist/validator.test.js.map +1 -0
- package/package.json +24 -8
package/README.md
CHANGED
|
@@ -2,43 +2,319 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript client for Melange PostgreSQL authorization.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
> Use the Go runtime (`github.com/pthm/melange/melange`) for production workloads.
|
|
5
|
+
Melange is an OpenFGA-compatible authorization library that runs entirely in PostgreSQL. This TypeScript client provides type-safe access to the authorization system.
|
|
7
6
|
|
|
8
7
|
## Installation
|
|
9
8
|
|
|
10
9
|
```bash
|
|
11
|
-
npm install @pthm/melange
|
|
10
|
+
npm install @pthm/melange pg
|
|
11
|
+
# or
|
|
12
|
+
yarn add @pthm/melange pg
|
|
13
|
+
# or
|
|
14
|
+
pnpm add @pthm/melange pg
|
|
12
15
|
```
|
|
13
16
|
|
|
14
|
-
##
|
|
17
|
+
## Quick Start
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
```typescript
|
|
20
|
+
import { Checker } from '@pthm/melange';
|
|
21
|
+
import { Pool } from 'pg';
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
// Create a PostgreSQL connection pool
|
|
24
|
+
const pool = new Pool({
|
|
25
|
+
connectionString: process.env.DATABASE_URL,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Create a checker instance
|
|
29
|
+
const checker = new Checker(pool);
|
|
30
|
+
|
|
31
|
+
// Perform a permission check
|
|
32
|
+
const decision = await checker.check(
|
|
33
|
+
{ type: 'user', id: '123' },
|
|
34
|
+
'can_read',
|
|
35
|
+
{ type: 'repository', id: '456' }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (decision.allowed) {
|
|
39
|
+
console.log('Access granted!');
|
|
40
|
+
} else {
|
|
41
|
+
console.log('Access denied.');
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
### Permission Checks
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// Check if a user can read a repository
|
|
51
|
+
const decision = await checker.check(
|
|
52
|
+
{ type: 'user', id: '123' },
|
|
53
|
+
'can_read',
|
|
54
|
+
{ type: 'repository', id: '456' }
|
|
55
|
+
);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### List Operations
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// List all repositories a user can read
|
|
62
|
+
const result = await checker.listObjects(
|
|
63
|
+
{ type: 'user', id: '123' },
|
|
64
|
+
'can_read',
|
|
65
|
+
'repository',
|
|
66
|
+
{ limit: 100 }
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
for (const repoId of result.items) {
|
|
70
|
+
console.log(`Repository: ${repoId}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// List all users who can read a repository
|
|
74
|
+
const users = await checker.listSubjects(
|
|
75
|
+
'user',
|
|
76
|
+
'can_read',
|
|
77
|
+
{ type: 'repository', id: '456' },
|
|
78
|
+
{ limit: 100 }
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Caching
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { Checker, MemoryCache } from '@pthm/melange';
|
|
86
|
+
|
|
87
|
+
// Create a checker with caching
|
|
88
|
+
const cache = new MemoryCache(60000); // 60 second TTL
|
|
89
|
+
const checker = new Checker(pool, { cache });
|
|
90
|
+
|
|
91
|
+
// First check hits the database
|
|
92
|
+
await checker.check(user, 'can_read', repo);
|
|
93
|
+
|
|
94
|
+
// Second check within 60s uses the cache
|
|
95
|
+
await checker.check(user, 'can_read', repo); // cached
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Decision Overrides for Testing
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Checker, DecisionAllow, DecisionDeny } from '@pthm/melange';
|
|
102
|
+
|
|
103
|
+
// Test authorized paths
|
|
104
|
+
const allowChecker = new Checker(pool, { decision: DecisionAllow });
|
|
105
|
+
await allowChecker.check(user, 'can_read', repo); // always returns { allowed: true }
|
|
106
|
+
|
|
107
|
+
// Test unauthorized paths
|
|
108
|
+
const denyChecker = new Checker(pool, { decision: DecisionDeny });
|
|
109
|
+
await denyChecker.check(user, 'can_read', repo); // always returns { allowed: false }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Contextual Tuples
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Check with temporary permissions
|
|
116
|
+
const decision = await checker.checkWithContextualTuples(
|
|
117
|
+
{ type: 'user', id: '123' },
|
|
118
|
+
'can_read',
|
|
119
|
+
{ type: 'document', id: '789' },
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
subject: { type: 'user', id: '123' },
|
|
123
|
+
relation: 'temp_access',
|
|
124
|
+
object: { type: 'document', id: '789' }
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Database Adapters
|
|
131
|
+
|
|
132
|
+
The runtime works with any PostgreSQL client that implements the `Queryable` interface:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
interface Queryable {
|
|
136
|
+
query<T>(text: string, params?: any[]): Promise<{ rows: T[] }>;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### node-postgres (pg)
|
|
141
|
+
|
|
142
|
+
node-postgres Pool and Client already implement `Queryable` and can be used directly:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { Checker } from '@pthm/melange';
|
|
146
|
+
import { Pool } from 'pg';
|
|
147
|
+
|
|
148
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
149
|
+
const checker = new Checker(pool); // Works directly
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### postgres.js
|
|
153
|
+
|
|
154
|
+
Use the `postgresAdapter` to wrap a postgres.js instance:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import postgres from 'postgres';
|
|
158
|
+
import { Checker, postgresAdapter } from '@pthm/melange';
|
|
159
|
+
|
|
160
|
+
const sql = postgres(process.env.DATABASE_URL);
|
|
161
|
+
const checker = new Checker(postgresAdapter(sql));
|
|
162
|
+
```
|
|
24
163
|
|
|
25
164
|
## Generated Client Code
|
|
26
165
|
|
|
27
|
-
|
|
166
|
+
Generate type-safe constants and factory functions from your schema:
|
|
28
167
|
|
|
29
168
|
```bash
|
|
30
169
|
melange generate client --runtime typescript --schema schema.fga --output ./src/authz/
|
|
31
170
|
```
|
|
32
171
|
|
|
33
|
-
This generates:
|
|
34
|
-
- Type constants (`TYPE_USER`, `TYPE_REPOSITORY`)
|
|
35
|
-
- Relation constants (`REL_CAN_READ`, `REL_OWNER`)
|
|
36
|
-
- Factory functions (`user()`, `repository()`)
|
|
172
|
+
This generates three files:
|
|
37
173
|
|
|
38
|
-
|
|
174
|
+
### types.ts
|
|
39
175
|
|
|
40
|
-
|
|
176
|
+
```typescript
|
|
177
|
+
export const ObjectTypes = {
|
|
178
|
+
User: "user",
|
|
179
|
+
Repository: "repository",
|
|
180
|
+
} as const;
|
|
181
|
+
|
|
182
|
+
export type ObjectType = (typeof ObjectTypes)[keyof typeof ObjectTypes];
|
|
183
|
+
|
|
184
|
+
export const Relations = {
|
|
185
|
+
CanRead: "can_read",
|
|
186
|
+
Owner: "owner",
|
|
187
|
+
} as const;
|
|
188
|
+
|
|
189
|
+
export type Relation = (typeof Relations)[keyof typeof Relations];
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### schema.ts
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import type { MelangeObject } from '@pthm/melange';
|
|
196
|
+
import { ObjectTypes } from './types.js';
|
|
197
|
+
|
|
198
|
+
export function user(id: string): MelangeObject {
|
|
199
|
+
return { type: ObjectTypes.User, id };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function repository(id: string): MelangeObject {
|
|
203
|
+
return { type: ObjectTypes.Repository, id };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function anyUser(): MelangeObject {
|
|
207
|
+
return { type: ObjectTypes.User, id: '*' };
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Usage
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { Checker } from '@pthm/melange';
|
|
215
|
+
import { user, repository, Relations } from './authz/index.js';
|
|
216
|
+
|
|
217
|
+
const decision = await checker.check(
|
|
218
|
+
user('123'),
|
|
219
|
+
Relations.CanRead,
|
|
220
|
+
repository('456')
|
|
221
|
+
);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## API Reference
|
|
225
|
+
|
|
226
|
+
### Checker
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
class Checker {
|
|
230
|
+
constructor(db: Queryable, options?: CheckerOptions);
|
|
231
|
+
|
|
232
|
+
check(
|
|
233
|
+
subject: MelangeObject,
|
|
234
|
+
relation: Relation,
|
|
235
|
+
object: MelangeObject,
|
|
236
|
+
contextualTuples?: ContextualTuple[]
|
|
237
|
+
): Promise<Decision>;
|
|
238
|
+
|
|
239
|
+
listObjects(
|
|
240
|
+
subject: MelangeObject,
|
|
241
|
+
relation: Relation,
|
|
242
|
+
objectType: ObjectType,
|
|
243
|
+
options?: PageOptions
|
|
244
|
+
): Promise<ListResult<string>>;
|
|
245
|
+
|
|
246
|
+
listSubjects(
|
|
247
|
+
subjectType: ObjectType,
|
|
248
|
+
relation: Relation,
|
|
249
|
+
object: MelangeObject,
|
|
250
|
+
options?: PageOptions
|
|
251
|
+
): Promise<ListResult<string>>;
|
|
252
|
+
|
|
253
|
+
checkWithContextualTuples(
|
|
254
|
+
subject: MelangeObject,
|
|
255
|
+
relation: Relation,
|
|
256
|
+
object: MelangeObject,
|
|
257
|
+
contextualTuples: ContextualTuple[]
|
|
258
|
+
): Promise<Decision>;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### CheckerOptions
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
interface CheckerOptions {
|
|
266
|
+
cache?: Cache; // Default: NoopCache
|
|
267
|
+
decision?: Decision; // For testing only
|
|
268
|
+
validateRequest?: boolean; // Default: true
|
|
269
|
+
validateUserset?: boolean; // Default: true
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Cache
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
interface Cache {
|
|
277
|
+
get(key: string): Promise<Decision | undefined>;
|
|
278
|
+
set(key: string, value: Decision): Promise<void>;
|
|
279
|
+
clear(): Promise<void>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class NoopCache implements Cache { } // No caching
|
|
283
|
+
class MemoryCache implements Cache { // In-memory with TTL
|
|
284
|
+
constructor(ttlMs?: number);
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Error Handling
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { MelangeError, ValidationError, NotFoundError } from '@pthm/melange';
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
await checker.check(user, 'can_read', repo);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (err instanceof ValidationError) {
|
|
297
|
+
console.error('Invalid input:', err.message);
|
|
298
|
+
} else if (err instanceof NotFoundError) {
|
|
299
|
+
console.error('Resource not found:', err.message);
|
|
300
|
+
} else if (err instanceof MelangeError) {
|
|
301
|
+
console.error('Melange error:', err.message);
|
|
302
|
+
} else {
|
|
303
|
+
console.error('Unknown error:', err);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Requirements
|
|
309
|
+
|
|
310
|
+
- Node.js 18 or higher
|
|
311
|
+
- PostgreSQL 14 or higher
|
|
312
|
+
- Melange schema and functions installed in your database
|
|
41
313
|
|
|
42
314
|
## License
|
|
43
315
|
|
|
44
316
|
MIT
|
|
317
|
+
|
|
318
|
+
## Contributing
|
|
319
|
+
|
|
320
|
+
See the [main repository](https://github.com/pthm/melange) for contribution guidelines.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database adapters for Melange.
|
|
3
|
+
*
|
|
4
|
+
* This module provides adapters for popular PostgreSQL clients.
|
|
5
|
+
*/
|
|
6
|
+
export { pgAdapter } from './pg.js';
|
|
7
|
+
export type { PgQueryable } from './pg.js';
|
|
8
|
+
export { postgresAdapter } from './postgres.js';
|
|
9
|
+
export type { PostgresSql } from './postgres.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-postgres (pg) adapter for Melange.
|
|
3
|
+
*
|
|
4
|
+
* This module provides an adapter to use node-postgres Pool or Client
|
|
5
|
+
* with the Melange Checker.
|
|
6
|
+
*/
|
|
7
|
+
import type { Queryable } from '../database.js';
|
|
8
|
+
/**
|
|
9
|
+
* PgQueryable represents a node-postgres client that can execute queries.
|
|
10
|
+
*
|
|
11
|
+
* This interface matches the query signature of both Pool and Client from 'pg'.
|
|
12
|
+
*/
|
|
13
|
+
export interface PgQueryable {
|
|
14
|
+
query<T = any>(text: string, params?: any[]): Promise<{
|
|
15
|
+
rows: T[];
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* pgAdapter wraps a node-postgres Pool or Client for use with Checker.
|
|
20
|
+
*
|
|
21
|
+
* Note: In most cases, you don't need this adapter. The pg Pool and Client
|
|
22
|
+
* already implement the Queryable interface and can be used directly.
|
|
23
|
+
*
|
|
24
|
+
* This adapter is provided for explicit type conversion if needed.
|
|
25
|
+
*
|
|
26
|
+
* @param client - A pg Pool or Client
|
|
27
|
+
* @returns A Queryable instance
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { Pool } from 'pg';
|
|
32
|
+
* import { Checker, pgAdapter } from '@pthm/melange';
|
|
33
|
+
*
|
|
34
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
35
|
+
* const checker = new Checker(pgAdapter(pool));
|
|
36
|
+
*
|
|
37
|
+
* // Or use the pool directly (preferred):
|
|
38
|
+
* const checker = new Checker(pool);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function pgAdapter(client: PgQueryable): Queryable;
|
|
42
|
+
//# sourceMappingURL=pg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg.d.ts","sourceRoot":"","sources":["../../src/adapters/pg.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,gBAAgB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;CACtE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAOxD"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-postgres (pg) adapter for Melange.
|
|
3
|
+
*
|
|
4
|
+
* This module provides an adapter to use node-postgres Pool or Client
|
|
5
|
+
* with the Melange Checker.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* pgAdapter wraps a node-postgres Pool or Client for use with Checker.
|
|
9
|
+
*
|
|
10
|
+
* Note: In most cases, you don't need this adapter. The pg Pool and Client
|
|
11
|
+
* already implement the Queryable interface and can be used directly.
|
|
12
|
+
*
|
|
13
|
+
* This adapter is provided for explicit type conversion if needed.
|
|
14
|
+
*
|
|
15
|
+
* @param client - A pg Pool or Client
|
|
16
|
+
* @returns A Queryable instance
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { Pool } from 'pg';
|
|
21
|
+
* import { Checker, pgAdapter } from '@pthm/melange';
|
|
22
|
+
*
|
|
23
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
24
|
+
* const checker = new Checker(pgAdapter(pool));
|
|
25
|
+
*
|
|
26
|
+
* // Or use the pool directly (preferred):
|
|
27
|
+
* const checker = new Checker(pool);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function pgAdapter(client) {
|
|
31
|
+
return {
|
|
32
|
+
async query(text, params) {
|
|
33
|
+
const result = await client.query(text, params);
|
|
34
|
+
return { rows: result.rows };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=pg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg.js","sourceRoot":"","sources":["../../src/adapters/pg.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,SAAS,CAAC,MAAmB;IAC3C,OAAO;QACL,KAAK,CAAC,KAAK,CAAU,IAAY,EAAE,MAAc;YAC/C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;YACnD,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;QAC/B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postgres.js adapter for Melange.
|
|
3
|
+
*
|
|
4
|
+
* This module provides an adapter to use postgres.js with the Melange Checker.
|
|
5
|
+
*/
|
|
6
|
+
import type { Queryable } from '../database.js';
|
|
7
|
+
/**
|
|
8
|
+
* PostgresSql represents a postgres.js Sql instance.
|
|
9
|
+
*
|
|
10
|
+
* This is a minimal interface matching postgres.js's unsafe method.
|
|
11
|
+
*/
|
|
12
|
+
export interface PostgresSql {
|
|
13
|
+
unsafe<T = any>(text: string, params?: any[]): Promise<T[]>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* postgresAdapter wraps a postgres.js Sql instance for use with Checker.
|
|
17
|
+
*
|
|
18
|
+
* @param sql - A postgres.js Sql instance
|
|
19
|
+
* @returns A Queryable instance
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import postgres from 'postgres';
|
|
24
|
+
* import { Checker, postgresAdapter } from '@pthm/melange';
|
|
25
|
+
*
|
|
26
|
+
* const sql = postgres(process.env.DATABASE_URL);
|
|
27
|
+
* const checker = new Checker(postgresAdapter(sql));
|
|
28
|
+
*
|
|
29
|
+
* const decision = await checker.check(
|
|
30
|
+
* { type: 'user', id: '123' },
|
|
31
|
+
* 'can_read',
|
|
32
|
+
* { type: 'repository', id: '456' }
|
|
33
|
+
* );
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function postgresAdapter(sql: PostgresSql): Queryable;
|
|
37
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/adapters/postgres.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,gBAAgB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,GAAG,SAAS,CAO3D"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postgres.js adapter for Melange.
|
|
3
|
+
*
|
|
4
|
+
* This module provides an adapter to use postgres.js with the Melange Checker.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* postgresAdapter wraps a postgres.js Sql instance for use with Checker.
|
|
8
|
+
*
|
|
9
|
+
* @param sql - A postgres.js Sql instance
|
|
10
|
+
* @returns A Queryable instance
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import postgres from 'postgres';
|
|
15
|
+
* import { Checker, postgresAdapter } from '@pthm/melange';
|
|
16
|
+
*
|
|
17
|
+
* const sql = postgres(process.env.DATABASE_URL);
|
|
18
|
+
* const checker = new Checker(postgresAdapter(sql));
|
|
19
|
+
*
|
|
20
|
+
* const decision = await checker.check(
|
|
21
|
+
* { type: 'user', id: '123' },
|
|
22
|
+
* 'can_read',
|
|
23
|
+
* { type: 'repository', id: '456' }
|
|
24
|
+
* );
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function postgresAdapter(sql) {
|
|
28
|
+
return {
|
|
29
|
+
async query(text, params = []) {
|
|
30
|
+
const rows = await sql.unsafe(text, params);
|
|
31
|
+
return { rows };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/adapters/postgres.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,GAAgB;IAC9C,OAAO;QACL,KAAK,CAAC,KAAK,CAAU,IAAY,EAAE,SAAgB,EAAE;YACnD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,MAAM,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;YAC/C,OAAO,EAAE,IAAI,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caching for Melange authorization checks.
|
|
3
|
+
*
|
|
4
|
+
* This module provides cache interfaces and implementations for storing
|
|
5
|
+
* permission check results to reduce database load.
|
|
6
|
+
*/
|
|
7
|
+
import type { Decision } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Cache stores permission check results.
|
|
10
|
+
*
|
|
11
|
+
* Implementations should be safe for concurrent access if the Checker
|
|
12
|
+
* is shared across requests. For request-scoped caching, create a new
|
|
13
|
+
* Checker per request with a request-scoped cache.
|
|
14
|
+
*/
|
|
15
|
+
export interface Cache {
|
|
16
|
+
/**
|
|
17
|
+
* Get a cached decision.
|
|
18
|
+
*
|
|
19
|
+
* @param key - Cache key
|
|
20
|
+
* @returns Cached decision, or undefined if not found or expired
|
|
21
|
+
*/
|
|
22
|
+
get(key: string): Promise<Decision | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* Store a decision in the cache.
|
|
25
|
+
*
|
|
26
|
+
* @param key - Cache key
|
|
27
|
+
* @param value - Decision to cache
|
|
28
|
+
*/
|
|
29
|
+
set(key: string, value: Decision): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Clear all cached entries.
|
|
32
|
+
*/
|
|
33
|
+
clear(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* NoopCache is a no-op cache that never stores anything.
|
|
37
|
+
*
|
|
38
|
+
* This is the default cache implementation, suitable for applications
|
|
39
|
+
* that don't want caching overhead or have other caching strategies.
|
|
40
|
+
*/
|
|
41
|
+
export declare class NoopCache implements Cache {
|
|
42
|
+
get(_key: string): Promise<Decision | undefined>;
|
|
43
|
+
set(_key: string, _value: Decision): Promise<void>;
|
|
44
|
+
clear(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* MemoryCache is a simple in-memory cache with TTL support.
|
|
48
|
+
*
|
|
49
|
+
* This cache stores decisions in a Map with time-based expiration.
|
|
50
|
+
* It's suitable for single-instance applications or request-scoped caching.
|
|
51
|
+
*
|
|
52
|
+
* For multi-instance deployments, consider using a distributed cache
|
|
53
|
+
* like Redis with a custom Cache implementation.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { Checker, MemoryCache } from '@pthm/melange';
|
|
58
|
+
* import { Pool } from 'pg';
|
|
59
|
+
*
|
|
60
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
61
|
+
* const cache = new MemoryCache(60000); // 60 second TTL
|
|
62
|
+
* const checker = new Checker(pool, { cache });
|
|
63
|
+
*
|
|
64
|
+
* // First check hits database
|
|
65
|
+
* await checker.check(user, 'can_read', repo);
|
|
66
|
+
*
|
|
67
|
+
* // Second check within 60s uses cache
|
|
68
|
+
* await checker.check(user, 'can_read', repo); // cached
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare class MemoryCache implements Cache {
|
|
72
|
+
private readonly cache;
|
|
73
|
+
private readonly ttlMs;
|
|
74
|
+
/**
|
|
75
|
+
* Create a new MemoryCache.
|
|
76
|
+
*
|
|
77
|
+
* @param ttlMs - Time to live in milliseconds (default: 60000 = 1 minute)
|
|
78
|
+
*/
|
|
79
|
+
constructor(ttlMs?: number);
|
|
80
|
+
get(key: string): Promise<Decision | undefined>;
|
|
81
|
+
set(key: string, value: Decision): Promise<void>;
|
|
82
|
+
clear(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Get the number of entries in the cache.
|
|
85
|
+
*
|
|
86
|
+
* Note: This includes expired entries that haven't been accessed yet.
|
|
87
|
+
*/
|
|
88
|
+
get size(): number;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,WAAW,KAAK;IACpB;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjD;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;GAKG;AACH,qBAAa,SAAU,YAAW,KAAK;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAIhD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,WAAY,YAAW,KAAK;IACvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B;;;;OAIG;gBACS,KAAK,GAAE,MAAc;IAO3B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAe/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAOhD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;;;OAIG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|