toride 0.0.1 → 0.2.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/chunk-24PMDTLE.js +2366 -0
- package/dist/chunk-475CNU63.js +37 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +224 -0
- package/dist/client-RqwW0K_-.d.ts +368 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +8 -0
- package/dist/index.d.ts +309 -0
- package/dist/index.js +246 -0
- package/package.json +47 -4
- package/index.js +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var CLIENT_VERSION = "0.0.1";
|
|
3
|
+
var TorideClient = class {
|
|
4
|
+
permissions;
|
|
5
|
+
constructor(snapshot) {
|
|
6
|
+
this.permissions = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const [key, actions] of Object.entries(snapshot)) {
|
|
8
|
+
this.permissions.set(key, new Set(actions));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Synchronous permission check.
|
|
13
|
+
* Returns true if the action is permitted for the resource, false otherwise.
|
|
14
|
+
* Unknown resources return false (default-deny).
|
|
15
|
+
*/
|
|
16
|
+
can(action, resource) {
|
|
17
|
+
const key = `${resource.type}:${resource.id}`;
|
|
18
|
+
const actions = this.permissions.get(key);
|
|
19
|
+
if (!actions) return false;
|
|
20
|
+
return actions.has(action);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Return the list of permitted actions for a resource.
|
|
24
|
+
* Returns empty array for unknown resources.
|
|
25
|
+
*/
|
|
26
|
+
permittedActions(resource) {
|
|
27
|
+
const key = `${resource.type}:${resource.id}`;
|
|
28
|
+
const actions = this.permissions.get(key);
|
|
29
|
+
if (!actions) return [];
|
|
30
|
+
return [...actions];
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
CLIENT_VERSION,
|
|
36
|
+
TorideClient
|
|
37
|
+
};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PolicySchema,
|
|
4
|
+
ValidationError,
|
|
5
|
+
parseInlineTests,
|
|
6
|
+
parseTestFile,
|
|
7
|
+
runTestCases,
|
|
8
|
+
validatePolicyResult,
|
|
9
|
+
validatePolicyStrict
|
|
10
|
+
} from "./chunk-24PMDTLE.js";
|
|
11
|
+
|
|
12
|
+
// src/cli.ts
|
|
13
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
14
|
+
import { resolve, dirname, join } from "path";
|
|
15
|
+
import * as YAML from "yaml";
|
|
16
|
+
import * as v from "valibot";
|
|
17
|
+
function loadPolicyFile(filePath) {
|
|
18
|
+
const absPath = resolve(filePath);
|
|
19
|
+
let content;
|
|
20
|
+
try {
|
|
21
|
+
content = readFileSync(absPath, "utf-8");
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(`Cannot read file: ${absPath}`);
|
|
24
|
+
}
|
|
25
|
+
let raw;
|
|
26
|
+
if (absPath.endsWith(".json")) {
|
|
27
|
+
try {
|
|
28
|
+
raw = JSON.parse(content);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new ValidationError(
|
|
31
|
+
`JSON parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
32
|
+
""
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
try {
|
|
37
|
+
raw = YAML.parse(content, { prettyErrors: true });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new ValidationError(
|
|
40
|
+
`YAML parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
41
|
+
""
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const result = v.safeParse(PolicySchema, raw);
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
const issue = result.issues[0];
|
|
48
|
+
const path = issue?.path?.map((p) => String(p.key)).join(".") ?? "";
|
|
49
|
+
throw new ValidationError(
|
|
50
|
+
`Policy validation failed: ${issue?.message ?? "unknown error"}${path ? ` at ${path}` : ""}`,
|
|
51
|
+
path
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return result.output;
|
|
55
|
+
}
|
|
56
|
+
function expandGlob(pattern) {
|
|
57
|
+
const absPattern = resolve(pattern);
|
|
58
|
+
if (!pattern.includes("*")) {
|
|
59
|
+
return [absPattern];
|
|
60
|
+
}
|
|
61
|
+
const parts = absPattern.split("/");
|
|
62
|
+
const baseParts = [];
|
|
63
|
+
for (const part of parts) {
|
|
64
|
+
if (part.includes("*")) break;
|
|
65
|
+
baseParts.push(part);
|
|
66
|
+
}
|
|
67
|
+
const baseDir = baseParts.join("/") || "/";
|
|
68
|
+
const regexStr = absPattern.replace(/\./g, "\\.").replace(/\*\*/g, "___GLOBSTAR___").replace(/\*/g, "[^/]*").replace(/___GLOBSTAR___/g, ".*");
|
|
69
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
70
|
+
const results = [];
|
|
71
|
+
function walkDir(dir) {
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = readdirSync(dir);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const fullPath = join(dir, entry);
|
|
80
|
+
try {
|
|
81
|
+
const stat = statSync(fullPath);
|
|
82
|
+
if (stat.isDirectory()) {
|
|
83
|
+
walkDir(fullPath);
|
|
84
|
+
} else if (regex.test(fullPath)) {
|
|
85
|
+
results.push(fullPath);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
walkDir(baseDir);
|
|
92
|
+
return results.sort();
|
|
93
|
+
}
|
|
94
|
+
async function runTestFile(filePath) {
|
|
95
|
+
const absPath = resolve(filePath);
|
|
96
|
+
const content = readFileSync(absPath, "utf-8");
|
|
97
|
+
if (absPath.endsWith(".test.yaml") || absPath.endsWith(".test.yml")) {
|
|
98
|
+
const testFile = parseTestFile(content);
|
|
99
|
+
const policyPath = resolve(dirname(absPath), testFile.policyPath);
|
|
100
|
+
const policy = loadPolicyFile(policyPath);
|
|
101
|
+
const results = await runTestCases(policy, testFile.tests);
|
|
102
|
+
return { results, file: absPath };
|
|
103
|
+
} else {
|
|
104
|
+
const policy = loadPolicyFile(absPath);
|
|
105
|
+
const { tests } = parseInlineTests(policy);
|
|
106
|
+
if (tests.length === 0) {
|
|
107
|
+
return { results: [], file: absPath };
|
|
108
|
+
}
|
|
109
|
+
const results = await runTestCases(policy, tests);
|
|
110
|
+
return { results, file: absPath };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function main(args = process.argv.slice(2)) {
|
|
114
|
+
const command = args[0];
|
|
115
|
+
if (command !== "validate" && command !== "test") {
|
|
116
|
+
console.error(`Usage: toride <validate|test> [options] <file(s)>`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
if (command === "test") {
|
|
120
|
+
return handleTestCommand(args.slice(1));
|
|
121
|
+
}
|
|
122
|
+
const isStrict = args.includes("--strict");
|
|
123
|
+
const fileArg = args.filter((a) => a !== "validate" && a !== "--strict")[0];
|
|
124
|
+
if (!fileArg) {
|
|
125
|
+
console.error("Error: No policy file specified");
|
|
126
|
+
console.error("Usage: toride validate [--strict] <policy-file>");
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
let policy;
|
|
130
|
+
try {
|
|
131
|
+
policy = loadPolicyFile(fileArg);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err instanceof ValidationError) {
|
|
134
|
+
console.error(`Error: ${err.message}`);
|
|
135
|
+
} else if (err instanceof Error) {
|
|
136
|
+
console.error(`Error: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
if (isStrict) {
|
|
141
|
+
const result = validatePolicyStrict(policy);
|
|
142
|
+
for (const error of result.errors) {
|
|
143
|
+
console.error(`Error: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
for (const warning of result.warnings) {
|
|
146
|
+
console.warn(`Warning: ${warning.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (result.errors.length > 0) {
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
152
|
+
console.log("Policy is valid.");
|
|
153
|
+
} else {
|
|
154
|
+
console.log("Policy is valid (with warnings).");
|
|
155
|
+
}
|
|
156
|
+
return 0;
|
|
157
|
+
} else {
|
|
158
|
+
const result = validatePolicyResult(policy);
|
|
159
|
+
for (const error of result.errors) {
|
|
160
|
+
console.error(`Error: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
if (result.errors.length > 0) {
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
console.log("Policy is valid.");
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function handleTestCommand(args) {
|
|
170
|
+
const fileArgs = args.filter((a) => !a.startsWith("-"));
|
|
171
|
+
if (fileArgs.length === 0) {
|
|
172
|
+
console.error("Error: No test file(s) specified");
|
|
173
|
+
console.error("Usage: toride test <file-or-glob> [...]");
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const allFiles = [];
|
|
177
|
+
for (const arg of fileArgs) {
|
|
178
|
+
const expanded = expandGlob(arg);
|
|
179
|
+
if (expanded.length === 0) {
|
|
180
|
+
console.error(`Warning: No files matched pattern "${arg}"`);
|
|
181
|
+
}
|
|
182
|
+
allFiles.push(...expanded);
|
|
183
|
+
}
|
|
184
|
+
if (allFiles.length === 0) {
|
|
185
|
+
console.error("Error: No test files found");
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
let totalPassed = 0;
|
|
189
|
+
let totalFailed = 0;
|
|
190
|
+
for (const file of allFiles) {
|
|
191
|
+
try {
|
|
192
|
+
const { results } = await runTestFile(file);
|
|
193
|
+
for (const result of results) {
|
|
194
|
+
if (result.passed) {
|
|
195
|
+
console.log(` \u2713 ${result.name}`);
|
|
196
|
+
totalPassed++;
|
|
197
|
+
} else {
|
|
198
|
+
console.log(` \u2717 ${result.name}`);
|
|
199
|
+
console.log(` Expected: ${result.expected}`);
|
|
200
|
+
console.log(` Got: ${result.actual}`);
|
|
201
|
+
totalFailed++;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error(`Error processing ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
206
|
+
totalFailed++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
console.log("");
|
|
210
|
+
if (totalFailed === 0) {
|
|
211
|
+
console.log(`${totalPassed} tests passed`);
|
|
212
|
+
return 0;
|
|
213
|
+
} else {
|
|
214
|
+
console.log(`${totalPassed} passed, ${totalFailed} failed`);
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
var isDirectExecution = typeof process !== "undefined" && process.argv[1] && (process.argv[1].endsWith("/cli.js") || process.argv[1].endsWith("/cli.mjs"));
|
|
219
|
+
if (isDirectExecution) {
|
|
220
|
+
main().then((code) => process.exit(code));
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
main
|
|
224
|
+
};
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape constraint for Toride's type parameter.
|
|
3
|
+
* Each property is a union or mapped type that codegen fills with literals.
|
|
4
|
+
*/
|
|
5
|
+
interface TorideSchema {
|
|
6
|
+
/** Union of all resource type names (e.g., "Document" | "Organization") */
|
|
7
|
+
resources: string;
|
|
8
|
+
/** Global union of all action/permission names across all resources */
|
|
9
|
+
actions: string;
|
|
10
|
+
/** Union of all actor type names (e.g., "User" | "ServiceAccount") */
|
|
11
|
+
actorTypes: string;
|
|
12
|
+
/** Per-resource permission unions: { Document: "read" | "write"; ... } */
|
|
13
|
+
permissionMap: {
|
|
14
|
+
[R in string]: string;
|
|
15
|
+
};
|
|
16
|
+
/** Per-resource role unions: { Document: "admin" | "editor"; ... } */
|
|
17
|
+
roleMap: {
|
|
18
|
+
[R in string]: string;
|
|
19
|
+
};
|
|
20
|
+
/** Per-resource attribute shapes: { Document: { status: string; ownerId: string }; ... } */
|
|
21
|
+
resourceAttributeMap: {
|
|
22
|
+
[R in string]: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
/** Per-actor attribute shapes: { User: { email: string; is_admin: boolean }; ... } */
|
|
25
|
+
actorAttributeMap: {
|
|
26
|
+
[A in string]: Record<string, unknown>;
|
|
27
|
+
};
|
|
28
|
+
/** Per-resource relation maps: { Document: { org: "Organization" }; ... } */
|
|
29
|
+
relationMap: {
|
|
30
|
+
[R in string]: Record<string, string>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Default schema where everything is string / Record<string, unknown>.
|
|
35
|
+
* Used when Toride is instantiated without a type parameter.
|
|
36
|
+
* Provides full backward compatibility with the current untyped API.
|
|
37
|
+
*/
|
|
38
|
+
interface DefaultSchema extends TorideSchema {
|
|
39
|
+
resources: string;
|
|
40
|
+
actions: string;
|
|
41
|
+
actorTypes: string;
|
|
42
|
+
permissionMap: Record<string, string>;
|
|
43
|
+
roleMap: Record<string, string>;
|
|
44
|
+
resourceAttributeMap: Record<string, Record<string, unknown>>;
|
|
45
|
+
actorAttributeMap: Record<string, Record<string, unknown>>;
|
|
46
|
+
relationMap: Record<string, Record<string, string>>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Represents an entity performing actions.
|
|
50
|
+
* Generic discriminated union over actor types in S.
|
|
51
|
+
* When S = DefaultSchema, collapses to the original untyped shape.
|
|
52
|
+
*/
|
|
53
|
+
type ActorRef<S extends TorideSchema = DefaultSchema> = {
|
|
54
|
+
[A in S["actorTypes"]]: {
|
|
55
|
+
readonly type: A;
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly attributes: S["actorAttributeMap"][A];
|
|
58
|
+
};
|
|
59
|
+
}[S["actorTypes"]];
|
|
60
|
+
/**
|
|
61
|
+
* Represents a protected entity being accessed.
|
|
62
|
+
* Generic over schema S and resource type R.
|
|
63
|
+
* When S = DefaultSchema, collapses to the original untyped shape.
|
|
64
|
+
*/
|
|
65
|
+
type ResourceRef<S extends TorideSchema = DefaultSchema, R extends S["resources"] = S["resources"]> = {
|
|
66
|
+
readonly type: R;
|
|
67
|
+
readonly id: string;
|
|
68
|
+
/** Pre-fetched attributes. Inline values take precedence over resolver results. */
|
|
69
|
+
readonly attributes?: S["resourceAttributeMap"][R];
|
|
70
|
+
};
|
|
71
|
+
/** Optional per-check configuration. */
|
|
72
|
+
interface CheckOptions {
|
|
73
|
+
readonly env?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
/** A single item in a canBatch() call. Action narrowed to global actions union. */
|
|
76
|
+
interface BatchCheckItem<S extends TorideSchema = DefaultSchema> {
|
|
77
|
+
readonly action: S["actions"];
|
|
78
|
+
readonly resource: ResourceRef<S>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Per-type resolver function.
|
|
82
|
+
* Called when the engine needs attributes not available inline.
|
|
83
|
+
* Called at most once per unique resource per evaluation (cached).
|
|
84
|
+
*
|
|
85
|
+
* Registering a resolver is **optional** per resource type. When no resolver is
|
|
86
|
+
* registered, inline {@link ResourceRef.attributes} are used as the sole data
|
|
87
|
+
* source — this is referred to as "default resolver" behavior (analogous to
|
|
88
|
+
* GraphQL's default field resolver, which returns `parent[fieldName]`).
|
|
89
|
+
*
|
|
90
|
+
* A resolver is only needed when attributes must be fetched from an external
|
|
91
|
+
* source (e.g., a database). When both inline attributes and a resolver are
|
|
92
|
+
* present, inline attributes take precedence field-by-field over resolver
|
|
93
|
+
* results.
|
|
94
|
+
*/
|
|
95
|
+
type ResourceResolver<S extends TorideSchema = DefaultSchema, R extends S["resources"] = S["resources"]> = (ref: ResourceRef<S, R>) => Promise<Record<string, unknown>>;
|
|
96
|
+
/**
|
|
97
|
+
* Map of resource type names to their resolver functions.
|
|
98
|
+
*
|
|
99
|
+
* Not all types need resolvers. Types without a registered resolver use
|
|
100
|
+
* **default resolver** behavior (also called "trivial resolution"): the engine
|
|
101
|
+
* reads attribute values directly from the inline {@link ResourceRef.attributes}
|
|
102
|
+
* passed at the call site. Fields not present inline resolve to `undefined`,
|
|
103
|
+
* causing conditions that reference them to fail (default-deny).
|
|
104
|
+
*
|
|
105
|
+
* This mirrors GraphQL's default field resolver pattern, where an unresolved
|
|
106
|
+
* field simply returns `parent[fieldName]` — here, inline attributes play the
|
|
107
|
+
* role of the `parent` object.
|
|
108
|
+
*/
|
|
109
|
+
type Resolvers<S extends TorideSchema = DefaultSchema> = {
|
|
110
|
+
[R in S["resources"]]?: ResourceResolver<S, R>;
|
|
111
|
+
};
|
|
112
|
+
/** Custom evaluator function signature. */
|
|
113
|
+
type EvaluatorFn = (actor: ActorRef, resource: ResourceRef, env: Record<string, unknown>) => Promise<boolean>;
|
|
114
|
+
/** Engine construction options. */
|
|
115
|
+
interface TorideOptions<S extends TorideSchema = DefaultSchema> {
|
|
116
|
+
readonly policy: Policy;
|
|
117
|
+
/**
|
|
118
|
+
* Per-type resolver map.
|
|
119
|
+
*
|
|
120
|
+
* Optional — the engine works without any resolvers when all required data is
|
|
121
|
+
* provided inline via {@link ResourceRef.attributes}. This "default resolver"
|
|
122
|
+
* mode is the simplest way to use toride and requires no async data fetching.
|
|
123
|
+
*
|
|
124
|
+
* When both inline attributes and a resolver are present for the same resource
|
|
125
|
+
* type, **inline attributes take precedence** over resolver results on a
|
|
126
|
+
* field-by-field basis.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* // Inline-only mode — no resolvers needed
|
|
131
|
+
* const toride = new Toride({ policy });
|
|
132
|
+
*
|
|
133
|
+
* const allowed = await toride.can(actor, "read", {
|
|
134
|
+
* type: "Document",
|
|
135
|
+
* id: "doc-1",
|
|
136
|
+
* attributes: { status: "published", ownerId: "user-42" },
|
|
137
|
+
* });
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
readonly resolvers?: Resolvers<S>;
|
|
141
|
+
readonly maxConditionDepth?: number;
|
|
142
|
+
readonly maxDerivedRoleDepth?: number;
|
|
143
|
+
readonly customEvaluators?: Record<string, EvaluatorFn>;
|
|
144
|
+
readonly onDecision?: (event: DecisionEvent) => void;
|
|
145
|
+
readonly onQuery?: (event: QueryEvent) => void;
|
|
146
|
+
}
|
|
147
|
+
/** Attribute type for actor declarations. */
|
|
148
|
+
type AttributeType = "string" | "number" | "boolean";
|
|
149
|
+
/** Actor type declaration with attribute schema. */
|
|
150
|
+
interface ActorDeclaration {
|
|
151
|
+
readonly attributes: Record<string, AttributeType>;
|
|
152
|
+
}
|
|
153
|
+
/** Global role definition derived from actor attributes. */
|
|
154
|
+
interface GlobalRole {
|
|
155
|
+
readonly actor_type: string;
|
|
156
|
+
readonly when: ConditionExpression;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Derived role entry. Exactly one derivation pattern per entry.
|
|
160
|
+
* Patterns:
|
|
161
|
+
* 1. from_global_role
|
|
162
|
+
* 2. from_role + on_relation
|
|
163
|
+
* 3. from_relation
|
|
164
|
+
* 4. actor_type + when (conditional)
|
|
165
|
+
* 5. when only
|
|
166
|
+
*/
|
|
167
|
+
interface DerivedRoleEntry {
|
|
168
|
+
readonly role: string;
|
|
169
|
+
readonly from_global_role?: string;
|
|
170
|
+
readonly from_role?: string;
|
|
171
|
+
readonly on_relation?: string;
|
|
172
|
+
readonly from_relation?: string;
|
|
173
|
+
readonly actor_type?: string;
|
|
174
|
+
readonly when?: ConditionExpression;
|
|
175
|
+
}
|
|
176
|
+
/** Conditional rule (permit or forbid). */
|
|
177
|
+
interface Rule {
|
|
178
|
+
readonly effect: "permit" | "forbid";
|
|
179
|
+
readonly roles?: string[];
|
|
180
|
+
readonly permissions: string[];
|
|
181
|
+
readonly when: ConditionExpression;
|
|
182
|
+
}
|
|
183
|
+
/** Field-level access control definition. */
|
|
184
|
+
interface FieldAccessDef {
|
|
185
|
+
readonly read?: string[];
|
|
186
|
+
readonly update?: string[];
|
|
187
|
+
}
|
|
188
|
+
/** Resource block definition. */
|
|
189
|
+
interface ResourceBlock {
|
|
190
|
+
readonly roles: string[];
|
|
191
|
+
readonly permissions: string[];
|
|
192
|
+
/** Optional typed attribute declarations for this resource type. */
|
|
193
|
+
readonly attributes?: Record<string, AttributeType>;
|
|
194
|
+
/** Relations map field names to target resource type names (simplified). */
|
|
195
|
+
readonly relations?: Record<string, string>;
|
|
196
|
+
readonly grants?: Record<string, string[]>;
|
|
197
|
+
readonly derived_roles?: DerivedRoleEntry[];
|
|
198
|
+
readonly rules?: Rule[];
|
|
199
|
+
readonly field_access?: Record<string, FieldAccessDef>;
|
|
200
|
+
}
|
|
201
|
+
/** Operator-based condition value. */
|
|
202
|
+
type ConditionOperator = {
|
|
203
|
+
readonly eq: unknown;
|
|
204
|
+
} | {
|
|
205
|
+
readonly neq: unknown;
|
|
206
|
+
} | {
|
|
207
|
+
readonly gt: unknown;
|
|
208
|
+
} | {
|
|
209
|
+
readonly gte: unknown;
|
|
210
|
+
} | {
|
|
211
|
+
readonly lt: unknown;
|
|
212
|
+
} | {
|
|
213
|
+
readonly lte: unknown;
|
|
214
|
+
} | {
|
|
215
|
+
readonly in: unknown[] | string;
|
|
216
|
+
} | {
|
|
217
|
+
readonly includes: unknown;
|
|
218
|
+
} | {
|
|
219
|
+
readonly exists: boolean;
|
|
220
|
+
} | {
|
|
221
|
+
readonly startsWith: string;
|
|
222
|
+
} | {
|
|
223
|
+
readonly endsWith: string;
|
|
224
|
+
} | {
|
|
225
|
+
readonly contains: string;
|
|
226
|
+
} | {
|
|
227
|
+
readonly custom: string;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Condition value: either a primitive (equality shorthand),
|
|
231
|
+
* a cross-reference string ($actor.x, $resource.x, $env.x),
|
|
232
|
+
* or an operator object.
|
|
233
|
+
*/
|
|
234
|
+
type ConditionValue = string | number | boolean | ConditionOperator;
|
|
235
|
+
/** Simple conditions: all key-value pairs ANDed together. */
|
|
236
|
+
type SimpleConditions = Record<string, ConditionValue>;
|
|
237
|
+
/**
|
|
238
|
+
* Recursive condition expression.
|
|
239
|
+
* Either simple conditions (Record<string, ConditionValue>),
|
|
240
|
+
* or a logical combinator ({ any: ... } or { all: ... }).
|
|
241
|
+
*/
|
|
242
|
+
type ConditionExpression = SimpleConditions | {
|
|
243
|
+
readonly any: ConditionExpression[];
|
|
244
|
+
} | {
|
|
245
|
+
readonly all: ConditionExpression[];
|
|
246
|
+
};
|
|
247
|
+
/** Test case for declarative YAML tests. */
|
|
248
|
+
interface TestCase {
|
|
249
|
+
readonly name: string;
|
|
250
|
+
readonly actor: ActorRef;
|
|
251
|
+
/** Mock resolver data: keyed by "Type:id", values are attribute objects. */
|
|
252
|
+
readonly resolvers?: Record<string, Record<string, unknown>>;
|
|
253
|
+
readonly action: string;
|
|
254
|
+
readonly resource: ResourceRef;
|
|
255
|
+
readonly expected: "allow" | "deny";
|
|
256
|
+
}
|
|
257
|
+
/** Top-level policy object. */
|
|
258
|
+
interface Policy {
|
|
259
|
+
readonly version: "1";
|
|
260
|
+
readonly actors: Record<string, ActorDeclaration>;
|
|
261
|
+
readonly global_roles?: Record<string, GlobalRole>;
|
|
262
|
+
readonly resources: Record<string, ResourceBlock>;
|
|
263
|
+
readonly tests?: TestCase[];
|
|
264
|
+
}
|
|
265
|
+
/** Trace for a derived role showing derivation path. */
|
|
266
|
+
interface DerivedRoleTrace {
|
|
267
|
+
readonly role: string;
|
|
268
|
+
readonly via: string;
|
|
269
|
+
}
|
|
270
|
+
/** Resolved roles detail with direct and derived breakdown. */
|
|
271
|
+
interface ResolvedRolesDetail {
|
|
272
|
+
readonly direct: string[];
|
|
273
|
+
readonly derived: DerivedRoleTrace[];
|
|
274
|
+
}
|
|
275
|
+
/** A matched rule with evaluation context. */
|
|
276
|
+
interface MatchedRule {
|
|
277
|
+
readonly effect: "permit" | "forbid";
|
|
278
|
+
readonly matched: boolean;
|
|
279
|
+
readonly rule: Rule;
|
|
280
|
+
readonly resolvedValues: Record<string, unknown>;
|
|
281
|
+
}
|
|
282
|
+
/** Full decision trace from explain(). */
|
|
283
|
+
interface ExplainResult<S extends TorideSchema = DefaultSchema, R extends S["resources"] = S["resources"]> {
|
|
284
|
+
readonly allowed: boolean;
|
|
285
|
+
readonly resolvedRoles: ResolvedRolesDetail;
|
|
286
|
+
readonly grantedPermissions: S["permissionMap"][R][];
|
|
287
|
+
readonly matchedRules: MatchedRule[];
|
|
288
|
+
readonly finalDecision: string;
|
|
289
|
+
}
|
|
290
|
+
/** Audit event for authorization checks. */
|
|
291
|
+
interface DecisionEvent {
|
|
292
|
+
readonly actor: ActorRef;
|
|
293
|
+
readonly action: string;
|
|
294
|
+
readonly resource: ResourceRef;
|
|
295
|
+
readonly allowed: boolean;
|
|
296
|
+
readonly resolvedRoles: string[];
|
|
297
|
+
readonly matchedRules: {
|
|
298
|
+
effect: string;
|
|
299
|
+
matched: boolean;
|
|
300
|
+
}[];
|
|
301
|
+
readonly timestamp: Date;
|
|
302
|
+
}
|
|
303
|
+
/** Audit event for constraint queries. */
|
|
304
|
+
interface QueryEvent {
|
|
305
|
+
readonly actor: ActorRef;
|
|
306
|
+
readonly action: string;
|
|
307
|
+
readonly resourceType: string;
|
|
308
|
+
readonly resultType: "unrestricted" | "forbidden" | "constrained";
|
|
309
|
+
readonly timestamp: Date;
|
|
310
|
+
}
|
|
311
|
+
/** Thrown when policy validation fails. */
|
|
312
|
+
declare class ValidationError extends Error {
|
|
313
|
+
readonly path: string;
|
|
314
|
+
constructor(message: string, path: string);
|
|
315
|
+
}
|
|
316
|
+
/** Thrown when a cycle is detected in relation traversal. */
|
|
317
|
+
declare class CycleError extends Error {
|
|
318
|
+
readonly path: string[];
|
|
319
|
+
constructor(message: string, path: string[]);
|
|
320
|
+
}
|
|
321
|
+
/** Thrown when depth limit is exceeded. */
|
|
322
|
+
declare class DepthLimitError extends Error {
|
|
323
|
+
readonly limit: number;
|
|
324
|
+
readonly limitType: "condition" | "derivation";
|
|
325
|
+
constructor(message: string, limit: number, limitType: "condition" | "derivation");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A serializable map of permissions keyed by "Type:id".
|
|
330
|
+
* Values are arrays of permitted action strings.
|
|
331
|
+
* Suitable for JSON transport to client-side TorideClient.
|
|
332
|
+
*/
|
|
333
|
+
type PermissionSnapshot = Record<string, string[]>;
|
|
334
|
+
|
|
335
|
+
declare const CLIENT_VERSION = "0.0.1";
|
|
336
|
+
|
|
337
|
+
/** Minimal resource reference for client-side lookups. Generic over S for type narrowing. */
|
|
338
|
+
interface ClientResourceRef<S extends TorideSchema = DefaultSchema> {
|
|
339
|
+
readonly type: S["resources"];
|
|
340
|
+
readonly id: string;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Client-side permission checker that provides instant synchronous checks
|
|
344
|
+
* against a PermissionSnapshot received from the server.
|
|
345
|
+
*
|
|
346
|
+
* Generic over TorideSchema so that action names and resource types
|
|
347
|
+
* are validated at compile time when a concrete schema is provided.
|
|
348
|
+
*
|
|
349
|
+
* Default-deny: unknown resources or actions return false.
|
|
350
|
+
* The snapshot is defensively copied to prevent external mutation.
|
|
351
|
+
*/
|
|
352
|
+
declare class TorideClient<S extends TorideSchema = DefaultSchema> {
|
|
353
|
+
private readonly permissions;
|
|
354
|
+
constructor(snapshot: PermissionSnapshot);
|
|
355
|
+
/**
|
|
356
|
+
* Synchronous permission check.
|
|
357
|
+
* Returns true if the action is permitted for the resource, false otherwise.
|
|
358
|
+
* Unknown resources return false (default-deny).
|
|
359
|
+
*/
|
|
360
|
+
can(action: S["actions"], resource: ClientResourceRef<S>): boolean;
|
|
361
|
+
/**
|
|
362
|
+
* Return the list of permitted actions for a resource.
|
|
363
|
+
* Returns empty array for unknown resources.
|
|
364
|
+
*/
|
|
365
|
+
permittedActions(resource: ClientResourceRef<S>): S["actions"][];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export { type ActorRef as A, type BatchCheckItem as B, type CheckOptions as C, type DefaultSchema as D, type ExplainResult as E, type FieldAccessDef as F, type GlobalRole as G, type MatchedRule as M, type Policy as P, type QueryEvent as Q, type ResourceRef as R, type SimpleConditions as S, type TorideSchema as T, ValidationError as V, type TorideOptions as a, type PermissionSnapshot as b, type TestCase as c, type Resolvers as d, type ActorDeclaration as e, type AttributeType as f, type ClientResourceRef as g, type ConditionExpression as h, type ConditionOperator as i, type ConditionValue as j, CycleError as k, type DecisionEvent as l, DepthLimitError as m, type DerivedRoleEntry as n, type DerivedRoleTrace as o, type EvaluatorFn as p, type ResolvedRolesDetail as q, type ResourceBlock as r, type ResourceResolver as s, type Rule as t, TorideClient as u, CLIENT_VERSION as v };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { v as CLIENT_VERSION, g as ClientResourceRef, b as PermissionSnapshot, u as TorideClient } from './client-RqwW0K_-.js';
|