@daltonr/authwrite-loader-db 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/dist/index.d.ts +21 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
- package/src/index.ts +191 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AuthContext, PolicyLoader, Resource, Subject } from '@daltonr/authwrite-core';
|
|
2
|
+
export interface RuleFn<S extends Subject = Subject, R extends Resource = Resource> {
|
|
3
|
+
match: (ctx: AuthContext<S, R>) => boolean;
|
|
4
|
+
condition?: (ctx: AuthContext<S, R>) => boolean;
|
|
5
|
+
}
|
|
6
|
+
export type RuleRegistry<S extends Subject = Subject, R extends Resource = Resource> = Record<string, RuleFn<S, R>>;
|
|
7
|
+
export interface DbLoaderConfig<S extends Subject = Subject, R extends Resource = Resource> {
|
|
8
|
+
/**
|
|
9
|
+
* Called on each load/poll to fetch raw policy data from the database.
|
|
10
|
+
* Must return an object matching the serializable policy schema.
|
|
11
|
+
*/
|
|
12
|
+
query: () => Promise<unknown>;
|
|
13
|
+
/** Maps rule IDs (from the query result) to their match/condition implementations. */
|
|
14
|
+
rules: RuleRegistry<S, R>;
|
|
15
|
+
/**
|
|
16
|
+
* How often to poll for policy changes, in milliseconds.
|
|
17
|
+
* @default 30000
|
|
18
|
+
*/
|
|
19
|
+
pollInterval?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function createDbLoader<S extends Subject = Subject, R extends Resource = Resource>(config: DbLoaderConfig<S, R>): PolicyLoader<S, R>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
2
|
+
function validate(raw) {
|
|
3
|
+
if (!raw || typeof raw !== 'object') {
|
|
4
|
+
throw new Error('Policy data must be an object');
|
|
5
|
+
}
|
|
6
|
+
const p = raw;
|
|
7
|
+
if (typeof p['id'] !== 'string' || !p['id']) {
|
|
8
|
+
throw new Error('Policy data must have a string "id" field');
|
|
9
|
+
}
|
|
10
|
+
if (p['defaultEffect'] !== 'allow' && p['defaultEffect'] !== 'deny') {
|
|
11
|
+
throw new Error(`"defaultEffect" must be "allow" or "deny", got: ${JSON.stringify(p['defaultEffect'])}`);
|
|
12
|
+
}
|
|
13
|
+
if (!Array.isArray(p['rules'])) {
|
|
14
|
+
throw new Error('Policy data must have a "rules" array');
|
|
15
|
+
}
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
function mergeRules(serializableRules, registry) {
|
|
19
|
+
return serializableRules.map(sr => {
|
|
20
|
+
const fn = registry[sr.id];
|
|
21
|
+
if (!fn) {
|
|
22
|
+
throw new Error(`Rule "${sr.id}" has no implementation in the registry. ` +
|
|
23
|
+
`Add an entry for "${sr.id}" to the rules registry.`);
|
|
24
|
+
}
|
|
25
|
+
const rule = {
|
|
26
|
+
id: sr.id,
|
|
27
|
+
match: fn.match,
|
|
28
|
+
allow: sr.allow,
|
|
29
|
+
deny: sr.deny,
|
|
30
|
+
};
|
|
31
|
+
if (sr.description !== undefined)
|
|
32
|
+
rule.description = sr.description;
|
|
33
|
+
if (sr.priority !== undefined)
|
|
34
|
+
rule.priority = sr.priority;
|
|
35
|
+
if (fn.condition !== undefined)
|
|
36
|
+
rule.condition = fn.condition;
|
|
37
|
+
return rule;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function mergeFieldRules(serializableFieldRules, registry) {
|
|
41
|
+
return serializableFieldRules.map(sfr => {
|
|
42
|
+
const fn = registry[sfr.id];
|
|
43
|
+
if (!fn) {
|
|
44
|
+
throw new Error(`FieldRule "${sfr.id}" has no implementation in the registry. ` +
|
|
45
|
+
`Add an entry for "${sfr.id}" to the rules registry.`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
id: sfr.id,
|
|
49
|
+
match: fn.match,
|
|
50
|
+
expose: sfr.expose,
|
|
51
|
+
redact: sfr.redact,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function buildPolicy(raw, registry) {
|
|
56
|
+
const serializable = validate(raw);
|
|
57
|
+
const policy = {
|
|
58
|
+
id: serializable.id,
|
|
59
|
+
defaultEffect: serializable.defaultEffect,
|
|
60
|
+
rules: mergeRules(serializable.rules, registry),
|
|
61
|
+
};
|
|
62
|
+
if (serializable.version !== undefined)
|
|
63
|
+
policy.version = serializable.version;
|
|
64
|
+
if (serializable.description !== undefined)
|
|
65
|
+
policy.description = serializable.description;
|
|
66
|
+
if (serializable.fieldRules && serializable.fieldRules.length > 0) {
|
|
67
|
+
policy.fieldRules = mergeFieldRules(serializable.fieldRules, registry);
|
|
68
|
+
}
|
|
69
|
+
return policy;
|
|
70
|
+
}
|
|
71
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
72
|
+
export function createDbLoader(config) {
|
|
73
|
+
const pollInterval = config.pollInterval ?? 30_000;
|
|
74
|
+
async function load() {
|
|
75
|
+
const raw = await config.query();
|
|
76
|
+
return buildPolicy(raw, config.rules);
|
|
77
|
+
}
|
|
78
|
+
function watch(cb) {
|
|
79
|
+
setInterval(async () => {
|
|
80
|
+
try {
|
|
81
|
+
const policy = await load();
|
|
82
|
+
cb(policy);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Swallow errors during polling — transient DB failures should not
|
|
86
|
+
// crash the watch loop. The next interval will retry automatically.
|
|
87
|
+
}
|
|
88
|
+
}, pollInterval);
|
|
89
|
+
}
|
|
90
|
+
return { load, watch };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@daltonr/authwrite-loader-db",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Hot-reloadable database policy loader for AuthEngine.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/richardadalton/authwrite.git",
|
|
10
|
+
"directory": "packages/loader-db"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["authorization", "authz", "loader", "policy", "hot-reload"],
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
26
|
+
"prepublishOnly": "test -d dist && echo 'dist already built, skipping' || (npm run clean && npm run build)"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@daltonr/authwrite-core": "*"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Action,
|
|
3
|
+
AuthContext,
|
|
4
|
+
FieldRule,
|
|
5
|
+
PolicyDefinition,
|
|
6
|
+
PolicyLoader,
|
|
7
|
+
PolicyRule,
|
|
8
|
+
Resource,
|
|
9
|
+
Subject,
|
|
10
|
+
} from '@daltonr/authwrite-core'
|
|
11
|
+
|
|
12
|
+
// ─── Serializable schema ──────────────────────────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// Rules returned by the query contain everything EXCEPT functions.
|
|
15
|
+
// The match/condition functions are provided through the RuleRegistry.
|
|
16
|
+
|
|
17
|
+
interface SerializableRule {
|
|
18
|
+
id: string
|
|
19
|
+
description?: string
|
|
20
|
+
priority?: number
|
|
21
|
+
allow?: Action[]
|
|
22
|
+
deny?: Action[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SerializableFieldRule {
|
|
26
|
+
id: string
|
|
27
|
+
expose: string[]
|
|
28
|
+
redact: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SerializablePolicy {
|
|
32
|
+
id: string
|
|
33
|
+
version?: string
|
|
34
|
+
description?: string
|
|
35
|
+
defaultEffect: 'allow' | 'deny'
|
|
36
|
+
rules: SerializableRule[]
|
|
37
|
+
fieldRules?: SerializableFieldRule[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Rule registry ────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface RuleFn<
|
|
43
|
+
S extends Subject = Subject,
|
|
44
|
+
R extends Resource = Resource,
|
|
45
|
+
> {
|
|
46
|
+
match: (ctx: AuthContext<S, R>) => boolean
|
|
47
|
+
condition?: (ctx: AuthContext<S, R>) => boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type RuleRegistry<
|
|
51
|
+
S extends Subject = Subject,
|
|
52
|
+
R extends Resource = Resource,
|
|
53
|
+
> = Record<string, RuleFn<S, R>>
|
|
54
|
+
|
|
55
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface DbLoaderConfig<
|
|
58
|
+
S extends Subject = Subject,
|
|
59
|
+
R extends Resource = Resource,
|
|
60
|
+
> {
|
|
61
|
+
/**
|
|
62
|
+
* Called on each load/poll to fetch raw policy data from the database.
|
|
63
|
+
* Must return an object matching the serializable policy schema.
|
|
64
|
+
*/
|
|
65
|
+
query: () => Promise<unknown>
|
|
66
|
+
/** Maps rule IDs (from the query result) to their match/condition implementations. */
|
|
67
|
+
rules: RuleRegistry<S, R>
|
|
68
|
+
/**
|
|
69
|
+
* How often to poll for policy changes, in milliseconds.
|
|
70
|
+
* @default 30000
|
|
71
|
+
*/
|
|
72
|
+
pollInterval?: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function validate(raw: unknown): SerializablePolicy {
|
|
78
|
+
if (!raw || typeof raw !== 'object') {
|
|
79
|
+
throw new Error('Policy data must be an object')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const p = raw as Record<string, unknown>
|
|
83
|
+
|
|
84
|
+
if (typeof p['id'] !== 'string' || !p['id']) {
|
|
85
|
+
throw new Error('Policy data must have a string "id" field')
|
|
86
|
+
}
|
|
87
|
+
if (p['defaultEffect'] !== 'allow' && p['defaultEffect'] !== 'deny') {
|
|
88
|
+
throw new Error(`"defaultEffect" must be "allow" or "deny", got: ${JSON.stringify(p['defaultEffect'])}`)
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(p['rules'])) {
|
|
91
|
+
throw new Error('Policy data must have a "rules" array')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return p as unknown as SerializablePolicy
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mergeRules<S extends Subject, R extends Resource>(
|
|
98
|
+
serializableRules: SerializableRule[],
|
|
99
|
+
registry: RuleRegistry<S, R>,
|
|
100
|
+
): PolicyRule<S, R>[] {
|
|
101
|
+
return serializableRules.map(sr => {
|
|
102
|
+
const fn = registry[sr.id]
|
|
103
|
+
if (!fn) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Rule "${sr.id}" has no implementation in the registry. ` +
|
|
106
|
+
`Add an entry for "${sr.id}" to the rules registry.`
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
const rule: PolicyRule<S, R> = {
|
|
110
|
+
id: sr.id,
|
|
111
|
+
match: fn.match,
|
|
112
|
+
allow: sr.allow,
|
|
113
|
+
deny: sr.deny,
|
|
114
|
+
}
|
|
115
|
+
if (sr.description !== undefined) rule.description = sr.description
|
|
116
|
+
if (sr.priority !== undefined) rule.priority = sr.priority
|
|
117
|
+
if (fn.condition !== undefined) rule.condition = fn.condition
|
|
118
|
+
return rule
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mergeFieldRules<S extends Subject, R extends Resource>(
|
|
123
|
+
serializableFieldRules: SerializableFieldRule[],
|
|
124
|
+
registry: RuleRegistry<S, R>,
|
|
125
|
+
): FieldRule<S, R>[] {
|
|
126
|
+
return serializableFieldRules.map(sfr => {
|
|
127
|
+
const fn = registry[sfr.id]
|
|
128
|
+
if (!fn) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`FieldRule "${sfr.id}" has no implementation in the registry. ` +
|
|
131
|
+
`Add an entry for "${sfr.id}" to the rules registry.`
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
id: sfr.id,
|
|
136
|
+
match: fn.match,
|
|
137
|
+
expose: sfr.expose,
|
|
138
|
+
redact: sfr.redact,
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildPolicy<S extends Subject, R extends Resource>(
|
|
144
|
+
raw: unknown,
|
|
145
|
+
registry: RuleRegistry<S, R>,
|
|
146
|
+
): PolicyDefinition<S, R> {
|
|
147
|
+
const serializable = validate(raw)
|
|
148
|
+
|
|
149
|
+
const policy: PolicyDefinition<S, R> = {
|
|
150
|
+
id: serializable.id,
|
|
151
|
+
defaultEffect: serializable.defaultEffect,
|
|
152
|
+
rules: mergeRules(serializable.rules, registry),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (serializable.version !== undefined) policy.version = serializable.version
|
|
156
|
+
if (serializable.description !== undefined) policy.description = serializable.description
|
|
157
|
+
|
|
158
|
+
if (serializable.fieldRules && serializable.fieldRules.length > 0) {
|
|
159
|
+
policy.fieldRules = mergeFieldRules(serializable.fieldRules, registry)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return policy
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
export function createDbLoader<
|
|
168
|
+
S extends Subject = Subject,
|
|
169
|
+
R extends Resource = Resource,
|
|
170
|
+
>(config: DbLoaderConfig<S, R>): PolicyLoader<S, R> {
|
|
171
|
+
const pollInterval = config.pollInterval ?? 30_000
|
|
172
|
+
|
|
173
|
+
async function load(): Promise<PolicyDefinition<S, R>> {
|
|
174
|
+
const raw = await config.query()
|
|
175
|
+
return buildPolicy(raw, config.rules)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function watch(cb: (policy: PolicyDefinition<S, R>) => void): void {
|
|
179
|
+
setInterval(async () => {
|
|
180
|
+
try {
|
|
181
|
+
const policy = await load()
|
|
182
|
+
cb(policy)
|
|
183
|
+
} catch {
|
|
184
|
+
// Swallow errors during polling — transient DB failures should not
|
|
185
|
+
// crash the watch loop. The next interval will retry automatically.
|
|
186
|
+
}
|
|
187
|
+
}, pollInterval)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { load, watch }
|
|
191
|
+
}
|