@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.
@@ -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 };