@daltonr/authwrite-loader-yaml 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.
@@ -0,0 +1,13 @@
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 FileLoaderConfig<S extends Subject = Subject, R extends Resource = Resource> {
8
+ /** Absolute or relative path to a .yaml, .yml, or .json policy file. */
9
+ path: string;
10
+ /** Maps rule IDs (from the file) to their match/condition implementations. */
11
+ rules: RuleRegistry<S, R>;
12
+ }
13
+ export declare function createFileLoader<S extends Subject = Subject, R extends Resource = Resource>(config: FileLoaderConfig<S, R>): PolicyLoader<S, R>;
package/dist/index.js ADDED
@@ -0,0 +1,100 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { watch } from 'node:fs';
3
+ import { parse as parseYaml } from 'yaml';
4
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
5
+ function validate(raw) {
6
+ if (!raw || typeof raw !== 'object') {
7
+ throw new Error('Policy file must be a YAML/JSON object');
8
+ }
9
+ const p = raw;
10
+ if (typeof p['id'] !== 'string' || !p['id']) {
11
+ throw new Error('Policy file must have a string "id" field');
12
+ }
13
+ if (p['defaultEffect'] !== 'allow' && p['defaultEffect'] !== 'deny') {
14
+ throw new Error(`"defaultEffect" must be "allow" or "deny", got: ${JSON.stringify(p['defaultEffect'])}`);
15
+ }
16
+ if (!Array.isArray(p['rules'])) {
17
+ throw new Error('Policy file must have a "rules" array');
18
+ }
19
+ return p;
20
+ }
21
+ function mergeRules(serializableRules, registry, context) {
22
+ return serializableRules.map(sr => {
23
+ const fn = registry[sr.id];
24
+ if (!fn) {
25
+ throw new Error(`Rule "${sr.id}" (from ${context}) has no implementation in the registry. ` +
26
+ `Add an entry for "${sr.id}" to the rules registry.`);
27
+ }
28
+ const rule = {
29
+ id: sr.id,
30
+ match: fn.match,
31
+ allow: sr.allow,
32
+ deny: sr.deny,
33
+ };
34
+ if (sr.description !== undefined)
35
+ rule.description = sr.description;
36
+ if (sr.priority !== undefined)
37
+ rule.priority = sr.priority;
38
+ if (fn.condition !== undefined)
39
+ rule.condition = fn.condition;
40
+ return rule;
41
+ });
42
+ }
43
+ function mergeFieldRules(serializableFieldRules, registry) {
44
+ return serializableFieldRules.map(sfr => {
45
+ const fn = registry[sfr.id];
46
+ if (!fn) {
47
+ throw new Error(`FieldRule "${sfr.id}" has no implementation in the registry. ` +
48
+ `Add an entry for "${sfr.id}" to the rules registry.`);
49
+ }
50
+ return {
51
+ id: sfr.id,
52
+ match: fn.match,
53
+ expose: sfr.expose,
54
+ redact: sfr.redact,
55
+ };
56
+ });
57
+ }
58
+ function buildPolicy(content, registry) {
59
+ const raw = parseYaml(content);
60
+ const serializable = validate(raw);
61
+ const policy = {
62
+ id: serializable.id,
63
+ defaultEffect: serializable.defaultEffect,
64
+ rules: mergeRules(serializable.rules, registry, 'rules'),
65
+ };
66
+ if (serializable.version !== undefined)
67
+ policy.version = serializable.version;
68
+ if (serializable.description !== undefined)
69
+ policy.description = serializable.description;
70
+ if (serializable.fieldRules && serializable.fieldRules.length > 0) {
71
+ policy.fieldRules = mergeFieldRules(serializable.fieldRules, registry);
72
+ }
73
+ return policy;
74
+ }
75
+ // ─── Factory ──────────────────────────────────────────────────────────────────
76
+ export function createFileLoader(config) {
77
+ async function load() {
78
+ const content = await readFile(config.path, 'utf-8');
79
+ return buildPolicy(content, config.rules);
80
+ }
81
+ function watchFile(cb) {
82
+ // Debounce: many editors write files in multiple events for a single save.
83
+ let debounceTimer;
84
+ watch(config.path, () => {
85
+ clearTimeout(debounceTimer);
86
+ debounceTimer = setTimeout(async () => {
87
+ try {
88
+ const policy = await load();
89
+ cb(policy);
90
+ }
91
+ catch {
92
+ // Swallow parse errors during watch — the file may be mid-write.
93
+ // The next fs event (when the write completes) will retry.
94
+ }
95
+ }, 50);
96
+ });
97
+ }
98
+ return { load, watch: watchFile };
99
+ }
100
+ //# 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,40 @@
1
+ {
2
+ "name": "@daltonr/authwrite-loader-yaml",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "YAML/JSON file-based policy loader for AuthEngine.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/richardadalton/authwrite.git",
10
+ "directory": "packages/loader-yaml"
11
+ },
12
+ "keywords": ["authorization", "authz", "loader", "policy", "yaml", "gitops"],
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
+ "peerDependencies": {
32
+ "yaml": ">=2.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "yaml": "^2.0.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,196 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { watch } from 'node:fs'
3
+ import { parse as parseYaml } from 'yaml'
4
+ import type {
5
+ Action,
6
+ AuthContext,
7
+ FieldRule,
8
+ PolicyDefinition,
9
+ PolicyLoader,
10
+ PolicyRule,
11
+ Resource,
12
+ Subject,
13
+ } from '@daltonr/authwrite-core'
14
+
15
+ // ─── Serializable file schema ─────────────────────────────────────────────────
16
+ //
17
+ // Rules and fieldRules in the file contain everything EXCEPT functions.
18
+ // The match/condition functions are provided through the RuleRegistry.
19
+
20
+ interface SerializableRule {
21
+ id: string
22
+ description?: string
23
+ priority?: number
24
+ allow?: Action[]
25
+ deny?: Action[]
26
+ }
27
+
28
+ interface SerializableFieldRule {
29
+ id: string
30
+ expose: string[]
31
+ redact: string[]
32
+ }
33
+
34
+ interface SerializablePolicy {
35
+ id: string
36
+ version?: string
37
+ description?: string
38
+ defaultEffect: 'allow' | 'deny'
39
+ rules: SerializableRule[]
40
+ fieldRules?: SerializableFieldRule[]
41
+ }
42
+
43
+ // ─── Rule registry ────────────────────────────────────────────────────────────
44
+ //
45
+ // Maps rule IDs to their runtime functions. Entries may be used by both
46
+ // rules and fieldRules — the match/condition shape is the same for both.
47
+
48
+ export interface RuleFn<
49
+ S extends Subject = Subject,
50
+ R extends Resource = Resource,
51
+ > {
52
+ match: (ctx: AuthContext<S, R>) => boolean
53
+ condition?: (ctx: AuthContext<S, R>) => boolean
54
+ }
55
+
56
+ export type RuleRegistry<
57
+ S extends Subject = Subject,
58
+ R extends Resource = Resource,
59
+ > = Record<string, RuleFn<S, R>>
60
+
61
+ // ─── Config ───────────────────────────────────────────────────────────────────
62
+
63
+ export interface FileLoaderConfig<
64
+ S extends Subject = Subject,
65
+ R extends Resource = Resource,
66
+ > {
67
+ /** Absolute or relative path to a .yaml, .yml, or .json policy file. */
68
+ path: string
69
+ /** Maps rule IDs (from the file) to their match/condition implementations. */
70
+ rules: RuleRegistry<S, R>
71
+ }
72
+
73
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
74
+
75
+ function validate(raw: unknown): SerializablePolicy {
76
+ if (!raw || typeof raw !== 'object') {
77
+ throw new Error('Policy file must be a YAML/JSON object')
78
+ }
79
+
80
+ const p = raw as Record<string, unknown>
81
+
82
+ if (typeof p['id'] !== 'string' || !p['id']) {
83
+ throw new Error('Policy file must have a string "id" field')
84
+ }
85
+ if (p['defaultEffect'] !== 'allow' && p['defaultEffect'] !== 'deny') {
86
+ throw new Error(`"defaultEffect" must be "allow" or "deny", got: ${JSON.stringify(p['defaultEffect'])}`)
87
+ }
88
+ if (!Array.isArray(p['rules'])) {
89
+ throw new Error('Policy file must have a "rules" array')
90
+ }
91
+
92
+ return p as unknown as SerializablePolicy
93
+ }
94
+
95
+ function mergeRules<S extends Subject, R extends Resource>(
96
+ serializableRules: SerializableRule[],
97
+ registry: RuleRegistry<S, R>,
98
+ context: 'rules' | 'fieldRules',
99
+ ): PolicyRule<S, R>[] {
100
+ return serializableRules.map(sr => {
101
+ const fn = registry[sr.id]
102
+ if (!fn) {
103
+ throw new Error(
104
+ `Rule "${sr.id}" (from ${context}) has no implementation in the registry. ` +
105
+ `Add an entry for "${sr.id}" to the rules registry.`
106
+ )
107
+ }
108
+ const rule: PolicyRule<S, R> = {
109
+ id: sr.id,
110
+ match: fn.match,
111
+ allow: sr.allow,
112
+ deny: sr.deny,
113
+ }
114
+ if (sr.description !== undefined) rule.description = sr.description
115
+ if (sr.priority !== undefined) rule.priority = sr.priority
116
+ if (fn.condition !== undefined) rule.condition = fn.condition
117
+ return rule
118
+ })
119
+ }
120
+
121
+ function mergeFieldRules<S extends Subject, R extends Resource>(
122
+ serializableFieldRules: SerializableFieldRule[],
123
+ registry: RuleRegistry<S, R>,
124
+ ): FieldRule<S, R>[] {
125
+ return serializableFieldRules.map(sfr => {
126
+ const fn = registry[sfr.id]
127
+ if (!fn) {
128
+ throw new Error(
129
+ `FieldRule "${sfr.id}" has no implementation in the registry. ` +
130
+ `Add an entry for "${sfr.id}" to the rules registry.`
131
+ )
132
+ }
133
+ return {
134
+ id: sfr.id,
135
+ match: fn.match,
136
+ expose: sfr.expose,
137
+ redact: sfr.redact,
138
+ }
139
+ })
140
+ }
141
+
142
+ function buildPolicy<S extends Subject, R extends Resource>(
143
+ content: string,
144
+ registry: RuleRegistry<S, R>,
145
+ ): PolicyDefinition<S, R> {
146
+ const raw = parseYaml(content)
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, 'rules'),
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 createFileLoader<
168
+ S extends Subject = Subject,
169
+ R extends Resource = Resource,
170
+ >(config: FileLoaderConfig<S, R>): PolicyLoader<S, R> {
171
+
172
+ async function load(): Promise<PolicyDefinition<S, R>> {
173
+ const content = await readFile(config.path, 'utf-8')
174
+ return buildPolicy(content, config.rules)
175
+ }
176
+
177
+ function watchFile(cb: (policy: PolicyDefinition<S, R>) => void): void {
178
+ // Debounce: many editors write files in multiple events for a single save.
179
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined
180
+
181
+ watch(config.path, () => {
182
+ clearTimeout(debounceTimer)
183
+ debounceTimer = setTimeout(async () => {
184
+ try {
185
+ const policy = await load()
186
+ cb(policy)
187
+ } catch {
188
+ // Swallow parse errors during watch — the file may be mid-write.
189
+ // The next fs event (when the write completes) will retry.
190
+ }
191
+ }, 50)
192
+ })
193
+ }
194
+
195
+ return { load, watch: watchFile }
196
+ }