@geonosis/policy 1.0.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/LICENSE +202 -0
- package/README.md +107 -0
- package/dist/chunk-JICJ4XEX.js +17 -0
- package/dist/drizzle/index.d.ts +43 -0
- package/dist/drizzle/index.js +54 -0
- package/dist/index.d.ts +375 -0
- package/dist/index.js +779 -0
- package/dist/medusa/index.d.ts +81 -0
- package/dist/medusa/index.js +59 -0
- package/dist/permissions/index.d.ts +126 -0
- package/dist/permissions/index.js +103 -0
- package/dist/types-DzC5zTZ8.d.ts +202 -0
- package/package.json +53 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { PermissionAction } from '../permissions/index.js';
|
|
2
|
+
import { A as Awaitable, P as PolicyQuery, d as PolicyRule, p as PolicyJournalEntry, a as PolicyRuleRow, b as PolicyAdapter } from '../types-DzC5zTZ8.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Medusa store, behind the ports the kernel reads.
|
|
6
|
+
*
|
|
7
|
+
* Nothing here imports `@medusajs/*`. The container is a thing the runtime hands the caller, so it
|
|
8
|
+
* arrives as an argument — which is also what makes every check in the conformance suite runnable
|
|
9
|
+
* without a booted server, and what let the 219 lines of resolution logic this package extracted
|
|
10
|
+
* stop dragging a module, a service and three ORM models behind them.
|
|
11
|
+
*/
|
|
12
|
+
/** The bit of the container this needs — structural, so a plain object stands in for it. */
|
|
13
|
+
type MedusaContainerLike = {
|
|
14
|
+
resolve: (key: string) => unknown;
|
|
15
|
+
};
|
|
16
|
+
/** The bit of the module service this needs: the generated list, and the generated create. */
|
|
17
|
+
type MedusaPolicyServiceLike = {
|
|
18
|
+
createPolicyJournalEntries?: (data: Record<string, unknown>) => Awaitable<unknown>;
|
|
19
|
+
listPolicyRules?: (filter: Record<string, unknown>) => Awaitable<readonly PolicyRuleRow[]>;
|
|
20
|
+
};
|
|
21
|
+
/** The bit of a runtime-config service this needs. Its `get` answers with the fallback it is given. */
|
|
22
|
+
type ConfigStoreLike = {
|
|
23
|
+
get: <T>(namespace: string, key: string, fallback: T) => Awaitable<T>;
|
|
24
|
+
};
|
|
25
|
+
type ConfigStoreBinding = (store: ConfigStoreLike, query: PolicyQuery) => Awaitable<unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* One row of the runtime config, as the value of one kind.
|
|
28
|
+
*
|
|
29
|
+
* The fallback handed to `get` is `undefined` on purpose: the service answers with whatever it is
|
|
30
|
+
* given when no row exists, so the only way to tell an unset row from a row holding that same value
|
|
31
|
+
* is to hand it something no row can legitimately hold. Junk in the row is not filtered here — the
|
|
32
|
+
* KIND's schema refuses it, once, for every layer that could produce one.
|
|
33
|
+
*/
|
|
34
|
+
declare const configRow: (namespace: string, key: string) => ConfigStoreBinding;
|
|
35
|
+
type MedusaPolicyAdapterOptions = {
|
|
36
|
+
/** Which config rows stand for which kind. A kind with no binding has no admin-set value. */
|
|
37
|
+
bindings?: Record<string, ConfigStoreBinding>;
|
|
38
|
+
configStoreKey?: string;
|
|
39
|
+
container: MedusaContainerLike;
|
|
40
|
+
/** What that store calls its own events, e.g. `rules_engine.`. */
|
|
41
|
+
eventPrefix?: string;
|
|
42
|
+
/** Rules from somewhere other than the table — a learned playbook's, appended LAST. */
|
|
43
|
+
extraRules?: () => Awaitable<readonly PolicyRule[]>;
|
|
44
|
+
moduleKey: string;
|
|
45
|
+
/** The row this store keeps. Overridden when its columns are not these. */
|
|
46
|
+
toRow?: (entry: PolicyJournalEntry, eventPrefix: string) => Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
declare const createMedusaPolicyAdapter: (options: MedusaPolicyAdapterOptions) => PolicyAdapter;
|
|
49
|
+
/** One role as that module holds it: an id, and the policies granted to it. */
|
|
50
|
+
type MedusaRbacRole = {
|
|
51
|
+
id: string;
|
|
52
|
+
policies?: readonly PermissionAction[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The native RBAC module's roles, as grants the permission lattice can read.
|
|
56
|
+
*
|
|
57
|
+
* This is the whole bridge. Upstream's `hasPermission` fetches these itself through the container
|
|
58
|
+
* and a cache, which is why nothing that uses it can be answered without a booted server; the fetch
|
|
59
|
+
* is the caller's here, and the decision is a value.
|
|
60
|
+
*/
|
|
61
|
+
declare const medusaGrants: (roles: readonly MedusaRbacRole[]) => Record<string, readonly PermissionAction[]>;
|
|
62
|
+
/**
|
|
63
|
+
* The signature a middleware advertises, deliberately NOT the framework's `MiddlewareFunction`.
|
|
64
|
+
*
|
|
65
|
+
* That type carries `#private` class fields, so two copies of `@medusajs/framework` in one store
|
|
66
|
+
* make the same shape nominally incompatible and every `defineMiddlewares` call site goes red.
|
|
67
|
+
* `unknown` params are contravariant-safe: this assigns to every version's, and the runtime value
|
|
68
|
+
* is unchanged.
|
|
69
|
+
*/
|
|
70
|
+
type PortableMiddleware = (request: unknown, response: unknown, next: (error?: unknown) => void) => unknown;
|
|
71
|
+
type Wrapper = (handler: PortableMiddleware, policies: PermissionAction[]) => PortableMiddleware;
|
|
72
|
+
/**
|
|
73
|
+
* The framework's own permission check as a one-liner a route array can hold.
|
|
74
|
+
*
|
|
75
|
+
* The wrapper is passed in rather than imported, for the same reason the container is: a package
|
|
76
|
+
* that imported it would be unusable anywhere else, and would pin one of the two framework copies.
|
|
77
|
+
* The wrapped handler does nothing but continue — the wrapper itself is the gate.
|
|
78
|
+
*/
|
|
79
|
+
declare const requirePermission: (wrap: Wrapper, ...policies: PermissionAction[]) => PortableMiddleware;
|
|
80
|
+
|
|
81
|
+
export { type ConfigStoreBinding, type ConfigStoreLike, type MedusaContainerLike, type MedusaPolicyAdapterOptions, type MedusaPolicyServiceLike, type MedusaRbacRole, type PortableMiddleware, configRow, createMedusaPolicyAdapter, medusaGrants, requirePermission };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toPolicyRules
|
|
3
|
+
} from "../chunk-JICJ4XEX.js";
|
|
4
|
+
|
|
5
|
+
// src/medusa/index.ts
|
|
6
|
+
var configRow = (namespace, key) => (store) => store.get(namespace, key, void 0);
|
|
7
|
+
var defaultRow = (entry, eventPrefix) => ({
|
|
8
|
+
actor_kind: "system",
|
|
9
|
+
...entry.rejected ? { error: `rejected ${JSON.stringify(entry.rejected.value)}: ${entry.rejected.reason}` } : {},
|
|
10
|
+
event_name: `${eventPrefix}${entry.event}`,
|
|
11
|
+
fell_back_to: { constraintKind: entry.kind, value: entry.fellBackTo },
|
|
12
|
+
...entry.op ? { op: entry.op } : {},
|
|
13
|
+
...entry.subject ? { subject: entry.subject } : {}
|
|
14
|
+
});
|
|
15
|
+
var attempt = async (what) => {
|
|
16
|
+
try {
|
|
17
|
+
return await what();
|
|
18
|
+
} catch {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var createMedusaPolicyAdapter = (options) => {
|
|
23
|
+
const bindings = options.bindings ?? {};
|
|
24
|
+
const eventPrefix = options.eventPrefix ?? "";
|
|
25
|
+
const toRow = options.toRow ?? defaultRow;
|
|
26
|
+
const service = () => options.container.resolve(options.moduleKey);
|
|
27
|
+
const store = () => options.container.resolve(options.configStoreKey ?? "configStore");
|
|
28
|
+
return {
|
|
29
|
+
config: {
|
|
30
|
+
get: async (kind, query) => {
|
|
31
|
+
const binding = bindings[kind];
|
|
32
|
+
return binding ? attempt(() => binding(store(), query)) : void 0;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
journal: {
|
|
36
|
+
write: async (entry) => {
|
|
37
|
+
await attempt(() => service().createPolicyJournalEntries?.(toRow(entry, eventPrefix)));
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
name: `medusa:${options.moduleKey}`,
|
|
41
|
+
rule: {
|
|
42
|
+
rules: async () => [
|
|
43
|
+
...toPolicyRules(await service().listPolicyRules?.({ status: "active" }) ?? []),
|
|
44
|
+
// Appended, never prepended: the lattice decides authority, and a list order that could
|
|
45
|
+
// stand in for it would be a second, silent one.
|
|
46
|
+
...await options.extraRules?.() ?? []
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
var medusaGrants = (roles) => Object.fromEntries(roles.map((role) => [role.id, role.policies ?? []]));
|
|
52
|
+
var passThrough = (_request, _response, next) => next();
|
|
53
|
+
var requirePermission = (wrap, ...policies) => wrap(passThrough, policies);
|
|
54
|
+
export {
|
|
55
|
+
configRow,
|
|
56
|
+
createMedusaPolicyAdapter,
|
|
57
|
+
medusaGrants,
|
|
58
|
+
requirePermission
|
|
59
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A permission lattice — Medusa 2.19.0's `definePolicies` / `hasPermission` / `resolvePermissions`
|
|
3
|
+
* with the container lifted out.
|
|
4
|
+
*
|
|
5
|
+
* dielime did not reinvent permissions and was right not to: it adopted the native module and put a
|
|
6
|
+
* typed registry in front of it. What that registry could not be given was a way to ANSWER the
|
|
7
|
+
* question without a booted container — so the same 22 named policies are unit-testable in node and
|
|
8
|
+
* the check they feed is not. Everything here is pure: the grants are data the caller fetched, and
|
|
9
|
+
* the answer is a value.
|
|
10
|
+
*
|
|
11
|
+
* Every semantic is pinned from the shipped implementation. No test file ships in either tarball.
|
|
12
|
+
*/
|
|
13
|
+
/** The single character used as the wildcard in BOTH the resource and the operation slot. */
|
|
14
|
+
declare const WILDCARD = "*";
|
|
15
|
+
type PolicyDefinition = {
|
|
16
|
+
description?: string;
|
|
17
|
+
name: string;
|
|
18
|
+
operation: string;
|
|
19
|
+
resource: string;
|
|
20
|
+
};
|
|
21
|
+
/** A resource with one or more operations on it — what a route asks for. */
|
|
22
|
+
type PermissionAction = {
|
|
23
|
+
operation: readonly string[] | string;
|
|
24
|
+
resource: string;
|
|
25
|
+
};
|
|
26
|
+
/** A single (resource, operation) pair with the key a store would hold it under. */
|
|
27
|
+
type ExpandedPolicy = {
|
|
28
|
+
key: string;
|
|
29
|
+
operation: string;
|
|
30
|
+
resource: string;
|
|
31
|
+
};
|
|
32
|
+
/** What each role has been granted. The caller fetched it; this decides with it. */
|
|
33
|
+
type PolicyGrants = ReadonlyMap<string, readonly PermissionAction[]> | Readonly<Record<string, readonly PermissionAction[]>>;
|
|
34
|
+
declare const permissionKey: (resource: string, operation: string) => string;
|
|
35
|
+
/**
|
|
36
|
+
* `ReadBrands` → `read_brands`, and the wildcard through untouched.
|
|
37
|
+
*
|
|
38
|
+
* Exported because a repo has to be able to check its own vocabulary against it: an operation
|
|
39
|
+
* spelled `certificate:create` normalises to `certificate_create`, so a repo that authors its rows
|
|
40
|
+
* directly (as dielime does) and ALSO declares them through here would end up with two spellings of
|
|
41
|
+
* one permission and no error anywhere.
|
|
42
|
+
*/
|
|
43
|
+
declare const normalizePolicyKey: (key: string) => string;
|
|
44
|
+
type DefinePoliciesOptions = {
|
|
45
|
+
normalize?: (key: string) => string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Declare policies. Answers with them; registers nothing globally.
|
|
49
|
+
*
|
|
50
|
+
* Upstream writes into three `globalThis` registries and REWRITES the caller's objects in place, so
|
|
51
|
+
* the array you passed comes back normalised under you and a second file declaring the same name
|
|
52
|
+
* silently replaces the first. Neither is load-bearing for the decision, and both are surprising, so
|
|
53
|
+
* neither is reproduced — `createPolicyCatalog` is the registry, and it refuses a duplicate.
|
|
54
|
+
*/
|
|
55
|
+
declare const definePolicies: (policies: PolicyDefinition | readonly PolicyDefinition[], options?: DefinePoliciesOptions) => {
|
|
56
|
+
policies: PolicyDefinition[];
|
|
57
|
+
};
|
|
58
|
+
type PolicyCatalog = {
|
|
59
|
+
declare: (policies: PolicyDefinition | readonly PolicyDefinition[]) => PolicyDefinition[];
|
|
60
|
+
get: (name: string) => PolicyDefinition | undefined;
|
|
61
|
+
list: () => PolicyDefinition[];
|
|
62
|
+
operations: () => string[];
|
|
63
|
+
resources: () => string[];
|
|
64
|
+
};
|
|
65
|
+
/** The registry upstream keeps globally, made local and made to refuse a name already taken. */
|
|
66
|
+
declare const createPolicyCatalog: (options?: DefinePoliciesOptions) => PolicyCatalog;
|
|
67
|
+
/**
|
|
68
|
+
* Expand actions into the unique one-operation rows a store keeps, deduped, key derived.
|
|
69
|
+
*
|
|
70
|
+
* The key is `<resource>:<operation>` on the RAW operation, which is how a wildcard grant becomes
|
|
71
|
+
* the single `*:*` row that authorises every route — granting the union of a repo's own named
|
|
72
|
+
* policies instead covers its custom resources only, and 403s every native one.
|
|
73
|
+
*/
|
|
74
|
+
declare const expandPolicies: (actions: readonly PermissionAction[]) => ExpandedPolicy[];
|
|
75
|
+
type HasPermissionInput = {
|
|
76
|
+
actions: PermissionAction | readonly PermissionAction[];
|
|
77
|
+
/**
|
|
78
|
+
* Whether the mechanism is on at all. `false` allows everything, which is what the `rbac` feature
|
|
79
|
+
* flag does upstream — and it is off by default there.
|
|
80
|
+
*/
|
|
81
|
+
enabled?: boolean;
|
|
82
|
+
grants: PolicyGrants;
|
|
83
|
+
/**
|
|
84
|
+
* What an actor with NO roles gets. Upstream's `hasPermission` allows (`!roleIds?.length` returns
|
|
85
|
+
* true) while its own route wrapper denies, so the same request gets opposite answers depending
|
|
86
|
+
* on which door it came through. `allow` is upstream's answer here.
|
|
87
|
+
*/
|
|
88
|
+
onEmptyRoles?: 'allow' | 'deny';
|
|
89
|
+
roles: readonly string[] | string;
|
|
90
|
+
};
|
|
91
|
+
/** Do these roles grant every one of these actions? */
|
|
92
|
+
declare const hasPermission: (input: HasPermissionInput) => boolean;
|
|
93
|
+
type ResolvePermissionsInput = {
|
|
94
|
+
enabled?: boolean;
|
|
95
|
+
grants: PolicyGrants;
|
|
96
|
+
roles: readonly string[] | string;
|
|
97
|
+
/** The (resource, operation) pairs to evaluate against, with wildcards expanded into them. */
|
|
98
|
+
universe: readonly {
|
|
99
|
+
operation: string;
|
|
100
|
+
resource: string;
|
|
101
|
+
}[];
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The subset of the universe these roles are granted, keyed `resource:operation`.
|
|
105
|
+
*
|
|
106
|
+
* The inverse of `hasPermission`, for a client that would otherwise have to re-implement wildcard
|
|
107
|
+
* semantics to render a menu. Note the asymmetry, which is upstream's: with the mechanism off this
|
|
108
|
+
* answers with EVERYTHING, and for an actor with no roles it answers with NOTHING — where
|
|
109
|
+
* `hasPermission` answers true for that same actor.
|
|
110
|
+
*/
|
|
111
|
+
declare const resolvePermissions: (input: ResolvePermissionsInput) => Set<string>;
|
|
112
|
+
type PermissionCheck = {
|
|
113
|
+
allowed: boolean;
|
|
114
|
+
required: string[];
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* The decision a route guard needs: the answer, plus WHICH permissions were required.
|
|
118
|
+
*
|
|
119
|
+
* A refusal that names them teaches the person blocked by it something; one that says "Forbidden"
|
|
120
|
+
* sends them to whoever owns the roles table. Denies a role-less actor, as the route wrapper does —
|
|
121
|
+
* but unlike the wrapper it still honours the flag, which the wrapper reads only after it has
|
|
122
|
+
* already thrown.
|
|
123
|
+
*/
|
|
124
|
+
declare const checkPermissions: (input: Omit<HasPermissionInput, "onEmptyRoles">) => PermissionCheck;
|
|
125
|
+
|
|
126
|
+
export { type DefinePoliciesOptions, type ExpandedPolicy, type HasPermissionInput, type PermissionAction, type PermissionCheck, type PolicyCatalog, type PolicyDefinition, type PolicyGrants, type ResolvePermissionsInput, WILDCARD, checkPermissions, createPolicyCatalog, definePolicies, expandPolicies, hasPermission, normalizePolicyKey, permissionKey, resolvePermissions };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/permissions/index.ts
|
|
2
|
+
var WILDCARD = "*";
|
|
3
|
+
var permissionKey = (resource, operation) => `${resource}:${operation}`;
|
|
4
|
+
var normalizePolicyKey = (key) => key === WILDCARD ? WILDCARD : key.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
5
|
+
var definePolicies = (policies, options = {}) => {
|
|
6
|
+
const declared = Array.isArray(policies) ? policies : [policies];
|
|
7
|
+
const normalize = options.normalize ?? normalizePolicyKey;
|
|
8
|
+
return {
|
|
9
|
+
policies: declared.map((policy) => {
|
|
10
|
+
for (const field of ["name", "operation", "resource"]) {
|
|
11
|
+
if (!policy[field]) {
|
|
12
|
+
throw new TypeError(
|
|
13
|
+
`definePolicies: a policy needs a \`${field}\`. Received ${JSON.stringify(policy)} \u2014 a policy missing one of the three cannot be granted, looked up, or named in a refusal.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
...policy,
|
|
19
|
+
operation: normalize(policy.operation),
|
|
20
|
+
resource: normalize(policy.resource)
|
|
21
|
+
};
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
var createPolicyCatalog = (options = {}) => {
|
|
26
|
+
const byName = /* @__PURE__ */ new Map();
|
|
27
|
+
return {
|
|
28
|
+
declare: (policies) => {
|
|
29
|
+
const declared = definePolicies(policies, options).policies;
|
|
30
|
+
for (const policy of declared) {
|
|
31
|
+
if (byName.has(policy.name)) {
|
|
32
|
+
throw new TypeError(
|
|
33
|
+
`policy catalogue: "${policy.name}" is already declared as ${JSON.stringify(
|
|
34
|
+
byName.get(policy.name)
|
|
35
|
+
)}. Upstream keeps these in a global object with no duplicate check, so the one that wins is decided by load order.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
byName.set(policy.name, policy);
|
|
39
|
+
}
|
|
40
|
+
return declared;
|
|
41
|
+
},
|
|
42
|
+
get: (name) => byName.get(name),
|
|
43
|
+
list: () => [...byName.values()],
|
|
44
|
+
operations: () => [...new Set([...byName.values()].map((policy) => policy.operation))],
|
|
45
|
+
resources: () => [...new Set([...byName.values()].map((policy) => policy.resource))]
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
var expandPolicies = (actions) => {
|
|
49
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const action of actions) {
|
|
51
|
+
const operations = Array.isArray(action.operation) ? action.operation : [action.operation];
|
|
52
|
+
for (const operation of operations) {
|
|
53
|
+
const key = permissionKey(action.resource, operation);
|
|
54
|
+
byKey.set(key, { key, operation, resource: action.resource });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...byKey.values()];
|
|
58
|
+
};
|
|
59
|
+
var grantsFor = (grants, role) => grants instanceof Map ? grants.get(role) ?? [] : grants[role] ?? [];
|
|
60
|
+
var asArray = (value) => Array.isArray(value) ? value : [value];
|
|
61
|
+
var policyAllows = (grants, roles, resource, operation) => roles.some((role) => {
|
|
62
|
+
const allowed = new Set(
|
|
63
|
+
expandPolicies(grantsFor(grants, role)).filter((policy) => policy.resource === resource || policy.resource === WILDCARD).map((policy) => policy.operation)
|
|
64
|
+
);
|
|
65
|
+
return allowed.has(operation) || allowed.has(WILDCARD);
|
|
66
|
+
});
|
|
67
|
+
var hasPermission = (input) => {
|
|
68
|
+
const roles = asArray(input.roles);
|
|
69
|
+
const actions = asArray(input.actions);
|
|
70
|
+
if (input.enabled === false) return true;
|
|
71
|
+
if (actions.length === 0) return true;
|
|
72
|
+
if (roles.length === 0) return (input.onEmptyRoles ?? "allow") === "allow";
|
|
73
|
+
return actions.every(
|
|
74
|
+
(action) => asArray(action.operation).every(
|
|
75
|
+
(operation) => policyAllows(input.grants, roles, action.resource, operation)
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
var resolvePermissions = (input) => {
|
|
80
|
+
const roles = asArray(input.roles);
|
|
81
|
+
if (input.enabled === false) {
|
|
82
|
+
return new Set(input.universe.map((entry) => permissionKey(entry.resource, entry.operation)));
|
|
83
|
+
}
|
|
84
|
+
if (roles.length === 0) return /* @__PURE__ */ new Set();
|
|
85
|
+
return new Set(
|
|
86
|
+
input.universe.filter((entry) => policyAllows(input.grants, roles, entry.resource, entry.operation)).map((entry) => permissionKey(entry.resource, entry.operation))
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
var checkPermissions = (input) => ({
|
|
90
|
+
allowed: hasPermission({ ...input, onEmptyRoles: "deny" }),
|
|
91
|
+
required: expandPolicies(asArray(input.actions)).map((policy) => policy.key)
|
|
92
|
+
});
|
|
93
|
+
export {
|
|
94
|
+
WILDCARD,
|
|
95
|
+
checkPermissions,
|
|
96
|
+
createPolicyCatalog,
|
|
97
|
+
definePolicies,
|
|
98
|
+
expandPolicies,
|
|
99
|
+
hasPermission,
|
|
100
|
+
normalizePolicyKey,
|
|
101
|
+
permissionKey,
|
|
102
|
+
resolvePermissions
|
|
103
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** Something a port may answer with, or answer with later. */
|
|
2
|
+
type Awaitable<T> = Promise<T> | T;
|
|
3
|
+
/**
|
|
4
|
+
* A schema, recognised by what it does rather than by what it is an instance of.
|
|
5
|
+
*
|
|
6
|
+
* dielime imports `z` from `@medusajs/framework/zod` under a repo rule forbidding bare `zod`, which
|
|
7
|
+
* resolves to the copy `@medusajs/deps` ships, while its own devDependency and during.day's catalog
|
|
8
|
+
* are two further copies of the same major. `value instanceof z.ZodType` compares against a class
|
|
9
|
+
* object, so a schema from another copy is not an instance of ours: such a guard passes every test
|
|
10
|
+
* written against the kit's own zod and refuses every schema a consumer hands it.
|
|
11
|
+
*/
|
|
12
|
+
type SchemaLike = {
|
|
13
|
+
safeParse: (value: unknown) => {
|
|
14
|
+
success: boolean;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** What a rule is aimed at, and what a decision is being made about. */
|
|
18
|
+
type PolicyScope = Record<string, unknown>;
|
|
19
|
+
type PolicyQuery = PolicyScope;
|
|
20
|
+
/** One authored constraint: a value, plus who said it, who it reaches and why it exists. */
|
|
21
|
+
type PolicyRule = {
|
|
22
|
+
constraint: {
|
|
23
|
+
kind: string;
|
|
24
|
+
value: unknown;
|
|
25
|
+
};
|
|
26
|
+
/** Stable id — quoted in every refusal and every trace, so it has to read as prose. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** May the overriding tier beat this rule? Defaults to the policy's `overridableByDefault`. */
|
|
29
|
+
overridable?: boolean;
|
|
30
|
+
/** Why this rule exists, in the words of whoever set it. Printed beside every refusal. */
|
|
31
|
+
reason: string;
|
|
32
|
+
scope?: PolicyScope;
|
|
33
|
+
tier: string;
|
|
34
|
+
};
|
|
35
|
+
/** One authority, in a lattice declared strongest first. */
|
|
36
|
+
type PolicyTierSpec = {
|
|
37
|
+
/** Fills silence only. A promoted learning enters here and can never outrank an authority. */
|
|
38
|
+
advisory?: boolean;
|
|
39
|
+
name: string;
|
|
40
|
+
/** May the overriding tier beat a rule at this tier, when the rule itself allows it? */
|
|
41
|
+
overridable?: boolean;
|
|
42
|
+
/** This tier IS the audited escape hatch. At most one per lattice. */
|
|
43
|
+
overrides?: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** One thing policy may constrain, and what is true of its values. */
|
|
46
|
+
type PolicyKindSpec = {
|
|
47
|
+
/**
|
|
48
|
+
* Read the value this one POINTS at. Called only when `isReference` says so; answering
|
|
49
|
+
* `undefined` means the thing it points at is unset, which is a gap and not a number — the read
|
|
50
|
+
* falls through to the config layer and then to the caller's constant, and journals.
|
|
51
|
+
*/
|
|
52
|
+
dereference?: (value: unknown, context: {
|
|
53
|
+
config?: PolicyConfigReader;
|
|
54
|
+
query: PolicyQuery;
|
|
55
|
+
}) => Awaitable<unknown>;
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Whether this value names something else rather than holding the answer. */
|
|
58
|
+
isReference?: (value: unknown) => boolean;
|
|
59
|
+
kind: string;
|
|
60
|
+
/**
|
|
61
|
+
* Do two values of this kind say the same thing? Defaults to JSON equality.
|
|
62
|
+
*
|
|
63
|
+
* A kind whose value is a SET says so here: `['flat','on-edge']` and `['on-edge','flat']` permit
|
|
64
|
+
* the same poses, and reporting them as a conflict to "resolve explicitly" is the fastest way to
|
|
65
|
+
* teach people to ignore conflict reports.
|
|
66
|
+
*/
|
|
67
|
+
sameValue?: (a: unknown, b: unknown) => boolean;
|
|
68
|
+
schema?: SchemaLike;
|
|
69
|
+
};
|
|
70
|
+
/** What policy decided, and the whole trace of why. */
|
|
71
|
+
type PolicyDecision<V = unknown> = {
|
|
72
|
+
/** Same authority, same specificity, DIFFERENT value. Reported, never guessed. */
|
|
73
|
+
conflicts: PolicyRule[];
|
|
74
|
+
explain: string;
|
|
75
|
+
kind: string;
|
|
76
|
+
/** Set when the overriding tier beat an authority that allowed it — the audit trail. */
|
|
77
|
+
overrode?: PolicyRule;
|
|
78
|
+
/** Every other rule that reached this subject, strongest first. */
|
|
79
|
+
suppressed: PolicyRule[];
|
|
80
|
+
value: V;
|
|
81
|
+
winner: PolicyRule;
|
|
82
|
+
};
|
|
83
|
+
/** A rule this build cannot honour, and the reason nobody has to guess at. */
|
|
84
|
+
type UnusableRule = {
|
|
85
|
+
id: string;
|
|
86
|
+
reason: string;
|
|
87
|
+
};
|
|
88
|
+
/** Where the authored rules come from. */
|
|
89
|
+
type PolicyRuleReader = {
|
|
90
|
+
rules: (kind: string, query: PolicyQuery) => Awaitable<readonly PolicyRule[]>;
|
|
91
|
+
};
|
|
92
|
+
/** The layer an admin can edit. `undefined` means no value is set. */
|
|
93
|
+
type PolicyConfigReader = {
|
|
94
|
+
get: (kind: string, query: PolicyQuery) => Awaitable<unknown>;
|
|
95
|
+
};
|
|
96
|
+
/** Where a fallback is recorded. Best-effort by contract: it may never change a decision. */
|
|
97
|
+
type PolicyJournal = {
|
|
98
|
+
write: (entry: PolicyJournalEntry) => Awaitable<void>;
|
|
99
|
+
};
|
|
100
|
+
declare const POLICY_FALLBACK = "policy.fallback";
|
|
101
|
+
declare const POLICY_INVALID_VALUE = "policy.invalid_value";
|
|
102
|
+
type PolicyJournalEntry = {
|
|
103
|
+
event: typeof POLICY_FALLBACK | typeof POLICY_INVALID_VALUE;
|
|
104
|
+
/** The value that decided instead. */
|
|
105
|
+
fellBackTo: unknown;
|
|
106
|
+
kind: string;
|
|
107
|
+
/** A named operation, so repeats are countable per operation rather than per request. */
|
|
108
|
+
op?: string;
|
|
109
|
+
rejected?: {
|
|
110
|
+
reason: string;
|
|
111
|
+
value: unknown;
|
|
112
|
+
};
|
|
113
|
+
subject?: string;
|
|
114
|
+
};
|
|
115
|
+
/** The data half of a policy — everything the lattice needs and nothing it does not. */
|
|
116
|
+
type PolicyData = {
|
|
117
|
+
kinds: readonly PolicyKindSpec[];
|
|
118
|
+
overridableByDefault: boolean;
|
|
119
|
+
rules: readonly PolicyRule[];
|
|
120
|
+
/** Vaguest first: the weight of a key is derived from where it sits. */
|
|
121
|
+
scopeKeys: readonly string[];
|
|
122
|
+
/** Strongest first. */
|
|
123
|
+
tiers: readonly PolicyTierSpec[];
|
|
124
|
+
};
|
|
125
|
+
type Policy = PolicyData & {
|
|
126
|
+
/** The fallbacks and unusable values this process has already recorded, keyed per boot. */
|
|
127
|
+
journalledOnce: Set<string>;
|
|
128
|
+
kindOf: (kind: string) => PolicyKindSpec | undefined;
|
|
129
|
+
rank: (tier: string) => number | undefined;
|
|
130
|
+
/** Test seam: the once-per-boot cache would otherwise leak between cases. */
|
|
131
|
+
resetJournal: () => void;
|
|
132
|
+
resolve: <V = unknown>(kind: string, query?: PolicyQuery, rules?: readonly PolicyRule[]) => PolicyDecision<V> | undefined;
|
|
133
|
+
specificity: (scope?: PolicyScope) => number;
|
|
134
|
+
tierOf: (tier: string) => PolicyTierSpec | undefined;
|
|
135
|
+
/** Kinds this build reads that no rule sets — the other half of the inert problem. */
|
|
136
|
+
unsetKinds: (rules?: readonly PolicyRule[]) => string[];
|
|
137
|
+
/** Rules scoped on a key this build does not understand — the policy is newer than the code. */
|
|
138
|
+
unsupportedScopes: (rules?: readonly PolicyRule[]) => {
|
|
139
|
+
id: string;
|
|
140
|
+
keys: string[];
|
|
141
|
+
}[];
|
|
142
|
+
unusableRules: (rules?: readonly PolicyRule[]) => UnusableRule[];
|
|
143
|
+
usableRules: (rules?: readonly PolicyRule[]) => PolicyRule[];
|
|
144
|
+
};
|
|
145
|
+
/** One recorded thing that happened, as the learning loop reads it. */
|
|
146
|
+
type JournalObservationEntry = {
|
|
147
|
+
actor: {
|
|
148
|
+
id?: string;
|
|
149
|
+
kind: 'agent' | 'system' | 'user';
|
|
150
|
+
};
|
|
151
|
+
/** A refusal message, when the action was refused. */
|
|
152
|
+
error?: string;
|
|
153
|
+
/** Set by a reader that found no rule and used its own default. */
|
|
154
|
+
fellBackTo?: {
|
|
155
|
+
kind: string;
|
|
156
|
+
value: unknown;
|
|
157
|
+
};
|
|
158
|
+
id: string;
|
|
159
|
+
/** The event or workflow name. */
|
|
160
|
+
name: string;
|
|
161
|
+
/** A named operation this entry belongs to. */
|
|
162
|
+
op?: string;
|
|
163
|
+
/** What it happened to — an order, a company, a document. */
|
|
164
|
+
subject?: string;
|
|
165
|
+
};
|
|
166
|
+
/** What a read answered with, and which layer answered. */
|
|
167
|
+
type PolicyReading<V> = {
|
|
168
|
+
/** Absent when no rule spoke and a lower layer decided. */
|
|
169
|
+
decision?: PolicyDecision<V>;
|
|
170
|
+
source: 'config' | 'fallback' | 'rule';
|
|
171
|
+
/** True when the caller's own compiled-in constant decided. */
|
|
172
|
+
usedFallback: boolean;
|
|
173
|
+
value: V;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* One stored rule, in the shape both consumers' stores keep it in.
|
|
177
|
+
*
|
|
178
|
+
* Snake_case because that is what a table column is called in both, and `rule_key` rather than `id`
|
|
179
|
+
* because the surrogate id is the store's and the KEY is the rule's: it is what a refusal quotes,
|
|
180
|
+
* and what lets evidence accumulate against one rule across environments.
|
|
181
|
+
*/
|
|
182
|
+
type PolicyRuleRow = {
|
|
183
|
+
constraint_kind: string;
|
|
184
|
+
constraint_value: unknown;
|
|
185
|
+
overridable?: boolean | null;
|
|
186
|
+
reason: string;
|
|
187
|
+
rule_key: string;
|
|
188
|
+
scope?: Record<string, unknown> | null;
|
|
189
|
+
/** `active` | `retired`. A retired rule stays readable; it just stops deciding. */
|
|
190
|
+
status?: null | string;
|
|
191
|
+
tier: string;
|
|
192
|
+
};
|
|
193
|
+
/** What a store offers the kernel. Every port is optional except the rules themselves. */
|
|
194
|
+
type PolicyAdapter = {
|
|
195
|
+
config?: PolicyConfigReader;
|
|
196
|
+
journal?: PolicyJournal;
|
|
197
|
+
/** For a diagnostic — a report naming "the adapter" helps nobody. */
|
|
198
|
+
name: string;
|
|
199
|
+
rule: PolicyRuleReader;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export { type Awaitable as A, type JournalObservationEntry as J, type PolicyQuery as P, type SchemaLike as S, type UnusableRule as U, type PolicyRuleRow as a, type PolicyAdapter as b, type PolicyKindSpec as c, type PolicyRule as d, type PolicyTierSpec as e, type Policy as f, type PolicyData as g, type PolicyDecision as h, type PolicyScope as i, type PolicyJournal as j, type PolicyConfigReader as k, type PolicyRuleReader as l, type PolicyReading as m, POLICY_FALLBACK as n, POLICY_INVALID_VALUE as o, type PolicyJournalEntry as p };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geonosis/policy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A threshold is a value plus the authority that set it, the subject it reaches and the reason it exists. The precedence lattice, the rule → config → constant read with its fallback journalled once per boot, the learning loop, the generated policy prose, and a permission lattice with the runtime lifted out.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"policy",
|
|
7
|
+
"rules-engine",
|
|
8
|
+
"rbac",
|
|
9
|
+
"permissions",
|
|
10
|
+
"precedence",
|
|
11
|
+
"medusa",
|
|
12
|
+
"drizzle"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/microcompanies/geonosis/tree/main/packages/policy",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/microcompanies/geonosis.git",
|
|
18
|
+
"directory": "packages/policy"
|
|
19
|
+
},
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.js",
|
|
25
|
+
"./permissions": "./dist/permissions/index.js",
|
|
26
|
+
"./medusa": "./dist/medusa/index.js",
|
|
27
|
+
"./drizzle": "./dist/drizzle/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"zod": "^4.0.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"zod": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"zod": "^4.0.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"typecheck": "tsc --noEmit"
|
|
52
|
+
}
|
|
53
|
+
}
|