@objectstack/plugin-sharing 4.0.1
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/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE +202 -0
- package/dist/index.d.mts +290 -0
- package/dist/index.d.ts +290 -0
- package/dist/index.js +980 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +942 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
- package/src/department-graph.ts +178 -0
- package/src/index.ts +46 -0
- package/src/rule-hooks.ts +64 -0
- package/src/sharing-plugin.ts +211 -0
- package/src/sharing-rule-service.ts +438 -0
- package/src/sharing-rule.test.ts +348 -0
- package/src/sharing-service.test.ts +355 -0
- package/src/sharing-service.ts +283 -0
- package/src/team-graph.ts +158 -0
- package/tsconfig.json +10 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
export { SysRecordShare, SysSharingRule } from '@objectstack/platform-objects/security';
|
|
2
|
+
export { SysDepartment, SysDepartmentMember } from '@objectstack/platform-objects/identity';
|
|
3
|
+
import { ISharingService, SharingExecutionContext, GrantShareInput, RecordShare, ISharingRuleService, DefineSharingRuleInput, SharingRuleRow, SharingRuleEvaluationResult, ITeamGraphService, IDepartmentGraphService } from '@objectstack/spec/contracts';
|
|
4
|
+
export { DefineSharingRuleInput, GrantShareInput, IDepartmentGraphService, ISharingRuleService, ISharingService, ITeamGraphService, RecordShare, ShareAccessLevel, ShareRecipientType, ShareSource, SharingExecutionContext, SharingRuleEvaluationResult, SharingRuleRecipientType, SharingRuleRow } from '@objectstack/spec/contracts';
|
|
5
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
6
|
+
import { EngineMiddleware } from '@objectstack/objectql';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shape of the data engine the service actually needs. Kept narrow so
|
|
10
|
+
* unit tests can pass an in-memory fake without depending on the full
|
|
11
|
+
* ObjectQL engine class.
|
|
12
|
+
*/
|
|
13
|
+
interface SharingEngine {
|
|
14
|
+
find(object: string, options?: any): Promise<any[]>;
|
|
15
|
+
findOne?(object: string, options?: any): Promise<any>;
|
|
16
|
+
insert(object: string, data: any, options?: any): Promise<any>;
|
|
17
|
+
update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;
|
|
18
|
+
delete(object: string, options?: any): Promise<any>;
|
|
19
|
+
getSchema?(object: string): any | undefined;
|
|
20
|
+
}
|
|
21
|
+
interface SharingServiceOptions {
|
|
22
|
+
engine: SharingEngine;
|
|
23
|
+
/** Object names that bypass sharing — typically platform internals. */
|
|
24
|
+
bypassObjects?: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Default `ISharingService` implementation.
|
|
28
|
+
*
|
|
29
|
+
* Stores every grant in `sys_record_share`. The plugin layer registers
|
|
30
|
+
* an engine middleware that calls `buildReadFilter` / `canEdit` so that
|
|
31
|
+
* neither this class nor its callers need to know about middleware
|
|
32
|
+
* plumbing.
|
|
33
|
+
*/
|
|
34
|
+
declare class SharingService implements ISharingService {
|
|
35
|
+
private readonly engine;
|
|
36
|
+
private readonly bypassObjects;
|
|
37
|
+
constructor(options: SharingServiceOptions);
|
|
38
|
+
/**
|
|
39
|
+
* Build a `FilterCondition` restricting `find` to records the caller
|
|
40
|
+
* may see. Returns `null` when no filter should be applied.
|
|
41
|
+
*/
|
|
42
|
+
buildReadFilter(object: string, context: SharingExecutionContext): Promise<unknown | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Return `true` if the caller may edit `(object, recordId)`. Always
|
|
45
|
+
* `true` for system context, public objects, and objects without an
|
|
46
|
+
* owner field.
|
|
47
|
+
*/
|
|
48
|
+
canEdit(object: string, recordId: string, context: SharingExecutionContext): Promise<boolean>;
|
|
49
|
+
/**
|
|
50
|
+
* Upsert a share row. Returning the existing row when an identical
|
|
51
|
+
* grant already exists keeps the REST endpoint idempotent.
|
|
52
|
+
*/
|
|
53
|
+
grant(input: GrantShareInput, context: SharingExecutionContext): Promise<RecordShare>;
|
|
54
|
+
/** Delete a share row by id. No-op when not found. */
|
|
55
|
+
revoke(shareId: string, _context: SharingExecutionContext): Promise<void>;
|
|
56
|
+
/** List share rows for `(object, recordId)`. */
|
|
57
|
+
listShares(object: string, recordId: string, _context: SharingExecutionContext): Promise<RecordShare[]>;
|
|
58
|
+
private shouldBypass;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface SharingRuleServiceOptions {
|
|
62
|
+
engine: SharingEngine;
|
|
63
|
+
sharing: SharingService;
|
|
64
|
+
logger?: {
|
|
65
|
+
info?: Function;
|
|
66
|
+
warn?: Function;
|
|
67
|
+
error?: Function;
|
|
68
|
+
debug?: Function;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Default {@link ISharingRuleService} implementation.
|
|
73
|
+
*
|
|
74
|
+
* Stores rule definitions in `sys_sharing_rule` and materialises grants
|
|
75
|
+
* as `sys_record_share` rows with `source='rule'` and `source_id={ruleId}`
|
|
76
|
+
* so reconcile can diff old grants vs fresh evaluation results without
|
|
77
|
+
* touching manual / team-derived shares.
|
|
78
|
+
*/
|
|
79
|
+
declare class SharingRuleService implements ISharingRuleService {
|
|
80
|
+
private readonly engine;
|
|
81
|
+
private readonly sharing;
|
|
82
|
+
private readonly logger?;
|
|
83
|
+
constructor(opts: SharingRuleServiceOptions);
|
|
84
|
+
defineRule(input: DefineSharingRuleInput, context: SharingExecutionContext): Promise<SharingRuleRow>;
|
|
85
|
+
listRules(filter: {
|
|
86
|
+
object?: string;
|
|
87
|
+
activeOnly?: boolean;
|
|
88
|
+
}, context: SharingExecutionContext): Promise<SharingRuleRow[]>;
|
|
89
|
+
getRule(idOrName: string, context: SharingExecutionContext): Promise<SharingRuleRow | null>;
|
|
90
|
+
deleteRule(idOrName: string, context: SharingExecutionContext): Promise<void>;
|
|
91
|
+
evaluateRule(idOrName: string, context: SharingExecutionContext): Promise<SharingRuleEvaluationResult>;
|
|
92
|
+
evaluateAllForRecord(object: string, recordId: string, context: SharingExecutionContext): Promise<SharingRuleEvaluationResult[]>;
|
|
93
|
+
private findMatchingRecords;
|
|
94
|
+
private recordMatches;
|
|
95
|
+
private expandRecipient;
|
|
96
|
+
private reconcile;
|
|
97
|
+
private reconcileForRecord;
|
|
98
|
+
private purgeRuleGrants;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type Cache = {
|
|
102
|
+
expandUsers?: Map<string, string[]>;
|
|
103
|
+
expandRole?: Map<string, string[]>;
|
|
104
|
+
manager?: Map<string, string | null>;
|
|
105
|
+
};
|
|
106
|
+
interface TeamGraphOptions {
|
|
107
|
+
engine: SharingEngine;
|
|
108
|
+
/** Optional tenant scope; null means cross-tenant lookups. */
|
|
109
|
+
organizationId?: string | null;
|
|
110
|
+
/** Optional shared cache across one evaluator pass. */
|
|
111
|
+
cache?: Cache;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Default {@link ITeamGraphService} implementation backed by
|
|
115
|
+
* `sys_team` + `sys_team_member` (better-auth's flat collaboration
|
|
116
|
+
* grouping) plus `sys_member.role` for tenant role expansion.
|
|
117
|
+
*
|
|
118
|
+
* **This service does NOT walk a hierarchy.** Teams here are flat —
|
|
119
|
+
* the enterprise org chart lives in `sys_department` and is served by
|
|
120
|
+
* {@link DepartmentGraphService}.
|
|
121
|
+
*
|
|
122
|
+
* All queries elevate to {@link SYSTEM_CTX} since the graph is platform
|
|
123
|
+
* metadata; callers (sharing rule evaluator, approval engine) own their
|
|
124
|
+
* own enforcement.
|
|
125
|
+
*/
|
|
126
|
+
declare class TeamGraphService implements ITeamGraphService {
|
|
127
|
+
private readonly engine;
|
|
128
|
+
private readonly organizationId;
|
|
129
|
+
private readonly cache;
|
|
130
|
+
constructor(opts: TeamGraphOptions);
|
|
131
|
+
expandUsers(teamId: string): Promise<string[]>;
|
|
132
|
+
expandRoleUsers(roleName: string, organizationId?: string): Promise<string[]>;
|
|
133
|
+
managerOf(userId: string, _organizationId?: string): Promise<string | null>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Convenience helper used by the sharing-rule evaluator + approval
|
|
137
|
+
* engine: expand an approver / recipient descriptor of the form
|
|
138
|
+
* `{type, value}` into a flat list of user IDs by routing to the
|
|
139
|
+
* appropriate graph service.
|
|
140
|
+
*
|
|
141
|
+
* `team` → flat team members (this service).
|
|
142
|
+
* `department` → recursive department members (delegated; requires a
|
|
143
|
+
* {@link IDepartmentGraphService} instance passed in `opts.dept`).
|
|
144
|
+
* `role` → tenant role members.
|
|
145
|
+
* `manager` → submitter's manager via `record[value] ?? record.owner_id`.
|
|
146
|
+
* `field` → literal user id stored in `record[value]`.
|
|
147
|
+
* `user` → literal value.
|
|
148
|
+
* Anything else echoes `type:value` for back-compat with legacy
|
|
149
|
+
* substring-match approver flows.
|
|
150
|
+
*/
|
|
151
|
+
declare function expandPrincipal(input: {
|
|
152
|
+
type: string;
|
|
153
|
+
value: string;
|
|
154
|
+
record?: any;
|
|
155
|
+
}, ctx: {
|
|
156
|
+
team: TeamGraphService;
|
|
157
|
+
dept?: {
|
|
158
|
+
expandUsers(id: string): Promise<string[]>;
|
|
159
|
+
};
|
|
160
|
+
organizationId?: string | null;
|
|
161
|
+
}): Promise<string[]>;
|
|
162
|
+
|
|
163
|
+
type DeptCache = {
|
|
164
|
+
descendants?: Map<string, string[]>;
|
|
165
|
+
expandUsers?: Map<string, string[]>;
|
|
166
|
+
head?: Map<string, string | null>;
|
|
167
|
+
};
|
|
168
|
+
interface DepartmentGraphOptions {
|
|
169
|
+
engine: SharingEngine;
|
|
170
|
+
/** Optional tenant scope; null means cross-tenant lookups. */
|
|
171
|
+
organizationId?: string | null;
|
|
172
|
+
/** Optional shared cache across one evaluator pass. */
|
|
173
|
+
cache?: DeptCache;
|
|
174
|
+
/**
|
|
175
|
+
* Optional team-graph instance to share role / manager lookups with —
|
|
176
|
+
* department graph proxies `managerOf` through so callers only need one
|
|
177
|
+
* service.
|
|
178
|
+
*/
|
|
179
|
+
teamGraph?: TeamGraphService;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Default {@link IDepartmentGraphService} implementation.
|
|
183
|
+
*
|
|
184
|
+
* Walks `sys_department.parent_department_id` for hierarchy and
|
|
185
|
+
* `sys_department_member` for member expansion. Treats the optional
|
|
186
|
+
* `active` flag as a hard filter (inactive departments contribute no
|
|
187
|
+
* members and stop BFS descent into their subtrees).
|
|
188
|
+
*
|
|
189
|
+
* Reuses {@link TeamGraphService.managerOf} for user-level manager
|
|
190
|
+
* lookup so callers can use this single service in approval / sharing
|
|
191
|
+
* pipelines.
|
|
192
|
+
*/
|
|
193
|
+
declare class DepartmentGraphService implements IDepartmentGraphService {
|
|
194
|
+
private readonly engine;
|
|
195
|
+
private readonly organizationId;
|
|
196
|
+
private readonly cache;
|
|
197
|
+
private readonly teamGraph?;
|
|
198
|
+
constructor(opts: DepartmentGraphOptions);
|
|
199
|
+
descendants(departmentId: string): Promise<string[]>;
|
|
200
|
+
expandUsers(departmentId: string): Promise<string[]>;
|
|
201
|
+
headOf(departmentId: string): Promise<string | null>;
|
|
202
|
+
managerOf(userId: string, organizationId?: string): Promise<string | null>;
|
|
203
|
+
private orgScope;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
declare const SHARING_RULE_HOOK_PACKAGE = "plugin-sharing:rules";
|
|
207
|
+
interface MinimalEngine {
|
|
208
|
+
registerHook(event: string, handler: (ctx: any) => any | Promise<any>, options?: {
|
|
209
|
+
object?: string | string[];
|
|
210
|
+
priority?: number;
|
|
211
|
+
packageId?: string;
|
|
212
|
+
}): void;
|
|
213
|
+
unregisterHooksByPackage(packageId: string): number;
|
|
214
|
+
}
|
|
215
|
+
interface MinimalLogger {
|
|
216
|
+
info?: (msg: any, ...rest: any[]) => void;
|
|
217
|
+
warn?: (msg: any, ...rest: any[]) => void;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Bind afterInsert/afterUpdate hooks for every distinct object_name in
|
|
221
|
+
* `rules`. Each hook calls `service.evaluateAllForRecord(object, id, …)`
|
|
222
|
+
* with SYSTEM_CTX so the evaluator can write `sys_record_share` rows
|
|
223
|
+
* without being blocked by its own enforcement.
|
|
224
|
+
*
|
|
225
|
+
* Caller is responsible for invoking {@link unbindAllRuleHooks} before
|
|
226
|
+
* re-binding when the rule set changes.
|
|
227
|
+
*/
|
|
228
|
+
declare function bindRuleHooks(engine: MinimalEngine, service: SharingRuleService, rules: SharingRuleRow[], logger?: MinimalLogger): void;
|
|
229
|
+
declare function unbindAllRuleHooks(engine: MinimalEngine): number;
|
|
230
|
+
|
|
231
|
+
interface SharingPluginOptions {
|
|
232
|
+
/** Extra object names that bypass sharing entirely. */
|
|
233
|
+
bypassObjects?: string[];
|
|
234
|
+
/**
|
|
235
|
+
* Disable enforcement (read filter + canEdit) while still registering
|
|
236
|
+
* the schema + service. Useful in development to flip enforcement on
|
|
237
|
+
* via env var without rebuilding.
|
|
238
|
+
*/
|
|
239
|
+
enforce?: boolean;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* SharingServicePlugin — registers `sys_record_share`, the `sharing`
|
|
243
|
+
* service, and the engine middleware that enforces
|
|
244
|
+
* `object.sharingModel`.
|
|
245
|
+
*
|
|
246
|
+
* Enforcement is opt-in per object:
|
|
247
|
+
*
|
|
248
|
+
* - `sharingModel: 'private'` → reads filtered to `(owner_id == me) OR
|
|
249
|
+
* (record explicitly shared with me)`. Writes require ownership or
|
|
250
|
+
* an `edit`/`full` share.
|
|
251
|
+
* - `sharingModel: 'read'` → reads unrestricted; writes gated as
|
|
252
|
+
* above (typical "everyone can see, only owner can edit").
|
|
253
|
+
* - any other value (or no value) → no enforcement. This keeps
|
|
254
|
+
* existing CRM behaviour identical until admins explicitly enable
|
|
255
|
+
* sharing on a per-object basis.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* import { SharingServicePlugin } from '@objectstack/plugin-sharing';
|
|
260
|
+
*
|
|
261
|
+
* kernel.use(new SharingServicePlugin());
|
|
262
|
+
*
|
|
263
|
+
* // Mark an object private — middleware enforces from this point on.
|
|
264
|
+
* defineObject({
|
|
265
|
+
* name: 'account',
|
|
266
|
+
* sharingModel: 'private',
|
|
267
|
+
* fields: { owner_id: Field.lookup('sys_user'), ... },
|
|
268
|
+
* });
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
declare class SharingServicePlugin implements Plugin {
|
|
272
|
+
name: string;
|
|
273
|
+
version: string;
|
|
274
|
+
type: string;
|
|
275
|
+
dependencies: string[];
|
|
276
|
+
private readonly options;
|
|
277
|
+
private service?;
|
|
278
|
+
private ruleService?;
|
|
279
|
+
constructor(options?: SharingPluginOptions);
|
|
280
|
+
init(ctx: PluginContext): Promise<void>;
|
|
281
|
+
start(ctx: PluginContext): Promise<void>;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Build the engine middleware that injects read filters and gates
|
|
285
|
+
* write operations. Exported so it can be unit-tested without booting
|
|
286
|
+
* a kernel.
|
|
287
|
+
*/
|
|
288
|
+
declare function buildSharingMiddleware(service: SharingService): EngineMiddleware;
|
|
289
|
+
|
|
290
|
+
export { type DepartmentGraphOptions, DepartmentGraphService, SHARING_RULE_HOOK_PACKAGE, type SharingEngine, type SharingPluginOptions, SharingRuleService, type SharingRuleServiceOptions, SharingService, type SharingServiceOptions, SharingServicePlugin, type TeamGraphOptions, TeamGraphService, bindRuleHooks, buildSharingMiddleware, expandPrincipal, unbindAllRuleHooks };
|