@objectstack/plugin-sharing 9.9.1 → 9.11.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/index.mjs CHANGED
@@ -1412,12 +1412,12 @@ var SysSharingRule = ObjectSchema2.create({
1412
1412
  group: "Target"
1413
1413
  }),
1414
1414
  recipient_type: Field2.select(
1415
- ["user", "team", "department", "role", "queue"],
1415
+ ["user", "team", "department", "role", "role_and_subordinates", "queue"],
1416
1416
  {
1417
1417
  label: "Recipient Type",
1418
1418
  required: true,
1419
1419
  defaultValue: "department",
1420
- description: "Kind of principal that receives access \u2014 expanded to user grants at evaluation time. `department` walks the parent_department_id tree; `team` is flat (better-auth).",
1420
+ description: "Kind of principal that receives access \u2014 expanded to user grants at evaluation time. `department` walks the parent_department_id tree; `team` is flat (better-auth); `role` is the role's direct members; `role_and_subordinates` walks the sys_role.parent hierarchy to also include every subordinate role (ADR-0056 D6).",
1421
1421
  group: "Recipient"
1422
1422
  }
1423
1423
  ),
@@ -1675,7 +1675,7 @@ var OWNER_FIELD = "owner_id";
1675
1675
  function effectiveSharingModel(schema) {
1676
1676
  const m = schema?.sharingModel ?? schema?.security?.sharingModel;
1677
1677
  if (m === "private") return "private";
1678
- if (m === "read") return "read";
1678
+ if (m === "read" || m === "public_read") return "read";
1679
1679
  return "public";
1680
1680
  }
1681
1681
  function hasOwnerField(schema) {
@@ -1942,8 +1942,77 @@ async function expandPrincipal(input, ctx) {
1942
1942
  return [`${t}:${v}`];
1943
1943
  }
1944
1944
 
1945
- // src/department-graph.ts
1945
+ // src/role-graph.ts
1946
1946
  var SYSTEM_CTX3 = { isSystem: true, roles: [], permissions: [] };
1947
+ var RoleGraphService = class {
1948
+ constructor(opts) {
1949
+ var _a, _b;
1950
+ this.engine = opts.engine;
1951
+ this.organizationId = opts.organizationId ?? null;
1952
+ this.cache = opts.cache ?? {};
1953
+ (_a = this.cache).descendants ?? (_a.descendants = /* @__PURE__ */ new Map());
1954
+ (_b = this.cache).expand ?? (_b.expand = /* @__PURE__ */ new Map());
1955
+ this.teamGraph = opts.teamGraph ?? new TeamGraphService({ engine: this.engine, organizationId: this.organizationId });
1956
+ }
1957
+ /** Direct child roles of `roleName` (`sys_role.parent === roleName`). */
1958
+ async childRoles(roleName) {
1959
+ const filter = { parent: roleName };
1960
+ if (this.organizationId) filter.organization_id = this.organizationId;
1961
+ let rows = [];
1962
+ try {
1963
+ rows = await this.engine.find("sys_role", {
1964
+ filter,
1965
+ fields: ["name"],
1966
+ limit: 5e3,
1967
+ context: SYSTEM_CTX3
1968
+ });
1969
+ } catch {
1970
+ rows = [];
1971
+ }
1972
+ return Array.from(new Set((rows ?? []).map((r) => String(r.name ?? "")).filter(Boolean)));
1973
+ }
1974
+ /** `roleName` plus every role beneath it in the hierarchy (BFS, cycle-safe). */
1975
+ async descendantRoles(roleName) {
1976
+ if (!roleName) return [];
1977
+ const cached = this.cache.descendants.get(roleName);
1978
+ if (cached) return cached;
1979
+ const out = [];
1980
+ const seen = /* @__PURE__ */ new Set();
1981
+ const queue = [roleName];
1982
+ while (queue.length) {
1983
+ const r = queue.shift();
1984
+ if (seen.has(r)) continue;
1985
+ seen.add(r);
1986
+ out.push(r);
1987
+ for (const child of await this.childRoles(r)) {
1988
+ if (!seen.has(child)) queue.push(child);
1989
+ }
1990
+ }
1991
+ this.cache.descendants.set(roleName, out);
1992
+ return out;
1993
+ }
1994
+ /** Users holding `roleName` OR any subordinate role (the `role_and_subordinates` set). */
1995
+ async expandRoleAndSubordinates(roleName, organizationId) {
1996
+ if (!roleName) return [];
1997
+ const org = organizationId ?? this.organizationId ?? "*";
1998
+ const key = `${org}::${roleName}`;
1999
+ const cached = this.cache.expand.get(key);
2000
+ if (cached) return cached;
2001
+ const roles = await this.descendantRoles(roleName);
2002
+ const users = /* @__PURE__ */ new Set();
2003
+ for (const role of roles) {
2004
+ for (const uid2 of await this.teamGraph.expandRoleUsers(role, organizationId ?? this.organizationId ?? void 0)) {
2005
+ users.add(uid2);
2006
+ }
2007
+ }
2008
+ const result = Array.from(users);
2009
+ this.cache.expand.set(key, result);
2010
+ return result;
2011
+ }
2012
+ };
2013
+
2014
+ // src/department-graph.ts
2015
+ var SYSTEM_CTX4 = { isSystem: true, roles: [], permissions: [] };
1947
2016
  var DepartmentGraphService = class {
1948
2017
  constructor(opts) {
1949
2018
  var _a, _b, _c;
@@ -1965,7 +2034,7 @@ var DepartmentGraphService = class {
1965
2034
  filter: this.orgScope({ id: departmentId }),
1966
2035
  fields: ["id", "active"],
1967
2036
  limit: 1,
1968
- context: SYSTEM_CTX3
2037
+ context: SYSTEM_CTX4
1969
2038
  });
1970
2039
  const seedRow = Array.isArray(seedRows) ? seedRows[0] : null;
1971
2040
  if (!seedRow) seedActive = false;
@@ -1987,7 +2056,7 @@ var DepartmentGraphService = class {
1987
2056
  filter: this.orgScope({ parent_department_id: parent, active: { $ne: false } }),
1988
2057
  fields: ["id"],
1989
2058
  limit: 1e3,
1990
- context: SYSTEM_CTX3
2059
+ context: SYSTEM_CTX4
1991
2060
  });
1992
2061
  } catch {
1993
2062
  children = [];
@@ -2016,7 +2085,7 @@ var DepartmentGraphService = class {
2016
2085
  filter: { department_id: { $in: depts } },
2017
2086
  fields: ["user_id"],
2018
2087
  limit: 1e4,
2019
- context: SYSTEM_CTX3
2088
+ context: SYSTEM_CTX4
2020
2089
  });
2021
2090
  } catch {
2022
2091
  rows = [];
@@ -2036,7 +2105,7 @@ var DepartmentGraphService = class {
2036
2105
  filter: { id: departmentId },
2037
2106
  fields: ["id", "manager_user_id"],
2038
2107
  limit: 1,
2039
- context: SYSTEM_CTX3
2108
+ context: SYSTEM_CTX4
2040
2109
  });
2041
2110
  row = Array.isArray(rows) ? rows[0] : null;
2042
2111
  } catch {
@@ -2054,7 +2123,7 @@ var DepartmentGraphService = class {
2054
2123
  filter: { id: userId },
2055
2124
  fields: ["id", "manager_id"],
2056
2125
  limit: 1,
2057
- context: SYSTEM_CTX3
2126
+ context: SYSTEM_CTX4
2058
2127
  });
2059
2128
  const row = Array.isArray(rows) ? rows[0] : null;
2060
2129
  return row?.manager_id ? String(row.manager_id) : null;
@@ -2069,7 +2138,7 @@ var DepartmentGraphService = class {
2069
2138
  };
2070
2139
 
2071
2140
  // src/sharing-rule-service.ts
2072
- var SYSTEM_CTX4 = { isSystem: true, roles: [], permissions: [] };
2141
+ var SYSTEM_CTX5 = { isSystem: true, roles: [], permissions: [] };
2073
2142
  function uid(prefix) {
2074
2143
  const g = globalThis;
2075
2144
  if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
@@ -2125,7 +2194,7 @@ var SharingRuleService = class {
2125
2194
  const existing = await this.engine.find("sys_sharing_rule", {
2126
2195
  filter: orgId ? { name: input.name, organization_id: orgId } : { name: input.name },
2127
2196
  limit: 1,
2128
- context: SYSTEM_CTX4
2197
+ context: SYSTEM_CTX5
2129
2198
  });
2130
2199
  if (Array.isArray(existing) && existing[0]) {
2131
2200
  const row = existing[0];
@@ -2141,7 +2210,7 @@ var SharingRuleService = class {
2141
2210
  active,
2142
2211
  updated_at: now
2143
2212
  };
2144
- await this.engine.update("sys_sharing_rule", patch, { context: SYSTEM_CTX4 });
2213
+ await this.engine.update("sys_sharing_rule", patch, { context: SYSTEM_CTX5 });
2145
2214
  return rowFromRule({ ...row, ...patch });
2146
2215
  }
2147
2216
  const newRow = {
@@ -2159,7 +2228,7 @@ var SharingRuleService = class {
2159
2228
  created_at: now,
2160
2229
  updated_at: now
2161
2230
  };
2162
- await this.engine.insert("sys_sharing_rule", newRow, { context: SYSTEM_CTX4 });
2231
+ await this.engine.insert("sys_sharing_rule", newRow, { context: SYSTEM_CTX5 });
2163
2232
  return rowFromRule(newRow);
2164
2233
  }
2165
2234
  async listRules(filter, context) {
@@ -2172,7 +2241,7 @@ var SharingRuleService = class {
2172
2241
  filter: where,
2173
2242
  orderBy: [{ field: "name", order: "asc" }],
2174
2243
  limit: 1e3,
2175
- context: SYSTEM_CTX4
2244
+ context: SYSTEM_CTX5
2176
2245
  });
2177
2246
  return Array.isArray(rows) ? rows.map(rowFromRule) : [];
2178
2247
  }
@@ -2182,13 +2251,13 @@ var SharingRuleService = class {
2182
2251
  const byId = await this.engine.find("sys_sharing_rule", {
2183
2252
  filter: { id: idOrName },
2184
2253
  limit: 1,
2185
- context: SYSTEM_CTX4
2254
+ context: SYSTEM_CTX5
2186
2255
  });
2187
2256
  if (Array.isArray(byId) && byId[0]) return rowFromRule(byId[0]);
2188
2257
  const byName = await this.engine.find("sys_sharing_rule", {
2189
2258
  filter: orgId ? { name: idOrName, organization_id: orgId } : { name: idOrName },
2190
2259
  limit: 1,
2191
- context: SYSTEM_CTX4
2260
+ context: SYSTEM_CTX5
2192
2261
  });
2193
2262
  if (Array.isArray(byName) && byName[0]) return rowFromRule(byName[0]);
2194
2263
  return null;
@@ -2198,11 +2267,11 @@ var SharingRuleService = class {
2198
2267
  if (!row) return;
2199
2268
  await this.engine.delete("sys_record_share", {
2200
2269
  where: { source: "rule", source_id: row.id },
2201
- context: SYSTEM_CTX4
2270
+ context: SYSTEM_CTX5
2202
2271
  });
2203
2272
  await this.engine.delete("sys_sharing_rule", {
2204
2273
  where: { id: row.id },
2205
- context: SYSTEM_CTX4
2274
+ context: SYSTEM_CTX5
2206
2275
  });
2207
2276
  }
2208
2277
  async evaluateRule(idOrName, context) {
@@ -2235,7 +2304,7 @@ var SharingRuleService = class {
2235
2304
  filter,
2236
2305
  fields: ["id"],
2237
2306
  limit: 5e3,
2238
- context: SYSTEM_CTX4
2307
+ context: SYSTEM_CTX5
2239
2308
  });
2240
2309
  return Array.isArray(rows) ? rows.map((r) => String(r.id)).filter(Boolean) : [];
2241
2310
  } catch (err) {
@@ -2250,7 +2319,7 @@ var SharingRuleService = class {
2250
2319
  filter,
2251
2320
  fields: ["id"],
2252
2321
  limit: 1,
2253
- context: SYSTEM_CTX4
2322
+ context: SYSTEM_CTX5
2254
2323
  });
2255
2324
  return Array.isArray(rows) && rows.length > 0;
2256
2325
  } catch {
@@ -2273,6 +2342,14 @@ var SharingRuleService = class {
2273
2342
  return dept.expandUsers(rule.recipient_id);
2274
2343
  }
2275
2344
  if (rule.recipient_type === "role") return team.expandRoleUsers(rule.recipient_id, rule.organization_id ?? void 0);
2345
+ if (rule.recipient_type === "role_and_subordinates") {
2346
+ const roleGraph = new RoleGraphService({
2347
+ engine: this.engine,
2348
+ organizationId: rule.organization_id ?? null,
2349
+ teamGraph: team
2350
+ });
2351
+ return roleGraph.expandRoleAndSubordinates(rule.recipient_id, rule.organization_id ?? void 0);
2352
+ }
2276
2353
  return [];
2277
2354
  }
2278
2355
  async reconcile(rule, matchedIds, users) {
@@ -2280,7 +2357,7 @@ var SharingRuleService = class {
2280
2357
  filter: { source: "rule", source_id: rule.id },
2281
2358
  fields: ["id", "record_id", "recipient_id", "access_level"],
2282
2359
  limit: 1e5,
2283
- context: SYSTEM_CTX4
2360
+ context: SYSTEM_CTX5
2284
2361
  });
2285
2362
  const desired = /* @__PURE__ */ new Map();
2286
2363
  for (const rid of matchedIds) {
@@ -2306,7 +2383,7 @@ var SharingRuleService = class {
2306
2383
  sourceId: rule.id,
2307
2384
  reason: `rule:${rule.name}`
2308
2385
  },
2309
- SYSTEM_CTX4
2386
+ SYSTEM_CTX5
2310
2387
  );
2311
2388
  updated += 1;
2312
2389
  }
@@ -2323,13 +2400,13 @@ var SharingRuleService = class {
2323
2400
  sourceId: rule.id,
2324
2401
  reason: `rule:${rule.name}`
2325
2402
  },
2326
- SYSTEM_CTX4
2403
+ SYSTEM_CTX5
2327
2404
  );
2328
2405
  created += 1;
2329
2406
  }
2330
2407
  }
2331
2408
  for (const [, stale] of existingMap.entries()) {
2332
- await this.sharing.revoke(stale.id, SYSTEM_CTX4);
2409
+ await this.sharing.revoke(stale.id, SYSTEM_CTX5);
2333
2410
  revoked += 1;
2334
2411
  }
2335
2412
  return {
@@ -2346,7 +2423,7 @@ var SharingRuleService = class {
2346
2423
  filter: { source: "rule", source_id: rule.id, record_id: recordId },
2347
2424
  fields: ["id", "record_id", "recipient_id", "access_level"],
2348
2425
  limit: 1e3,
2349
- context: SYSTEM_CTX4
2426
+ context: SYSTEM_CTX5
2350
2427
  });
2351
2428
  const existingMap = /* @__PURE__ */ new Map();
2352
2429
  for (const row of existing ?? []) existingMap.set(String(row.recipient_id), row);
@@ -2369,7 +2446,7 @@ var SharingRuleService = class {
2369
2446
  sourceId: rule.id,
2370
2447
  reason: `rule:${rule.name}`
2371
2448
  },
2372
- SYSTEM_CTX4
2449
+ SYSTEM_CTX5
2373
2450
  );
2374
2451
  updated += 1;
2375
2452
  }
@@ -2386,14 +2463,14 @@ var SharingRuleService = class {
2386
2463
  sourceId: rule.id,
2387
2464
  reason: `rule:${rule.name}`
2388
2465
  },
2389
- SYSTEM_CTX4
2466
+ SYSTEM_CTX5
2390
2467
  );
2391
2468
  created += 1;
2392
2469
  }
2393
2470
  }
2394
2471
  }
2395
2472
  for (const [, stale] of existingMap.entries()) {
2396
- await this.sharing.revoke(stale.id, SYSTEM_CTX4);
2473
+ await this.sharing.revoke(stale.id, SYSTEM_CTX5);
2397
2474
  revoked += 1;
2398
2475
  }
2399
2476
  return {
@@ -2410,11 +2487,11 @@ var SharingRuleService = class {
2410
2487
  filter: { source: "rule", source_id: ruleId },
2411
2488
  fields: ["id"],
2412
2489
  limit: 1e5,
2413
- context: SYSTEM_CTX4
2490
+ context: SYSTEM_CTX5
2414
2491
  });
2415
2492
  let revoked = 0;
2416
2493
  for (const row of existing ?? []) {
2417
- await this.sharing.revoke(row.id, SYSTEM_CTX4);
2494
+ await this.sharing.revoke(row.id, SYSTEM_CTX5);
2418
2495
  revoked += 1;
2419
2496
  }
2420
2497
  return revoked;
@@ -2422,7 +2499,7 @@ var SharingRuleService = class {
2422
2499
  };
2423
2500
 
2424
2501
  // src/share-link-service.ts
2425
- var SYSTEM_CTX5 = { isSystem: true, roles: [], permissions: [] };
2502
+ var SYSTEM_CTX6 = { isSystem: true, roles: [], permissions: [] };
2426
2503
  var TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
2427
2504
  var TOKEN_LENGTH = 24;
2428
2505
  var DEFAULT_MAX_EXPIRY_DAYS = 365;
@@ -2562,7 +2639,7 @@ var ShareLinkService = class {
2562
2639
  where: { id: input.recordId },
2563
2640
  fields: ["id"],
2564
2641
  limit: 1,
2565
- context: SYSTEM_CTX5
2642
+ context: SYSTEM_CTX6
2566
2643
  });
2567
2644
  if (!Array.isArray(exists) || exists.length === 0) {
2568
2645
  throw makeError(404, "RECORD_NOT_FOUND", `${input.object}/${input.recordId} does not exist`);
@@ -2588,7 +2665,7 @@ var ShareLinkService = class {
2588
2665
  last_used_at: null,
2589
2666
  use_count: 0
2590
2667
  };
2591
- await this.engine.insert("sys_share_link", row, { context: SYSTEM_CTX5 });
2668
+ await this.engine.insert("sys_share_link", row, { context: SYSTEM_CTX6 });
2592
2669
  return row;
2593
2670
  }
2594
2671
  async revokeLink(idOrToken, _context) {
@@ -2598,7 +2675,7 @@ var ShareLinkService = class {
2598
2675
  where: filter,
2599
2676
  fields: ["id", "revoked_at"],
2600
2677
  limit: 1,
2601
- context: SYSTEM_CTX5
2678
+ context: SYSTEM_CTX6
2602
2679
  });
2603
2680
  const row = Array.isArray(rows) ? rows[0] : void 0;
2604
2681
  if (!row) return;
@@ -2606,7 +2683,7 @@ var ShareLinkService = class {
2606
2683
  await this.engine.update(
2607
2684
  "sys_share_link",
2608
2685
  { id: row.id, revoked_at: (/* @__PURE__ */ new Date()).toISOString() },
2609
- { context: SYSTEM_CTX5 }
2686
+ { context: SYSTEM_CTX6 }
2610
2687
  );
2611
2688
  }
2612
2689
  async listLinks(filter, context) {
@@ -2619,7 +2696,7 @@ var ShareLinkService = class {
2619
2696
  where,
2620
2697
  limit: 200,
2621
2698
  sort: [{ field: "created_at", order: "desc" }],
2622
- context: context.isSystem ? SYSTEM_CTX5 : context
2699
+ context: context.isSystem ? SYSTEM_CTX6 : context
2623
2700
  });
2624
2701
  return Array.isArray(rows) ? rows : [];
2625
2702
  }
@@ -2628,7 +2705,7 @@ var ShareLinkService = class {
2628
2705
  const rows = await this.engine.find("sys_share_link", {
2629
2706
  where: { token },
2630
2707
  limit: 1,
2631
- context: SYSTEM_CTX5
2708
+ context: SYSTEM_CTX6
2632
2709
  });
2633
2710
  const row = Array.isArray(rows) ? rows[0] : void 0;
2634
2711
  if (!row) return null;
@@ -2658,7 +2735,7 @@ var ShareLinkService = class {
2658
2735
  last_used_at: (/* @__PURE__ */ new Date()).toISOString(),
2659
2736
  use_count: (row.use_count ?? 0) + 1
2660
2737
  },
2661
- { context: SYSTEM_CTX5 }
2738
+ { context: SYSTEM_CTX6 }
2662
2739
  );
2663
2740
  } catch {
2664
2741
  }
@@ -2667,7 +2744,7 @@ var ShareLinkService = class {
2667
2744
  };
2668
2745
 
2669
2746
  // src/share-link-routes.ts
2670
- var SYSTEM_CTX6 = { isSystem: true, roles: [], permissions: [] };
2747
+ var SYSTEM_CTX7 = { isSystem: true, roles: [], permissions: [] };
2671
2748
  var defaultContext = (req) => {
2672
2749
  const header = (name) => {
2673
2750
  const v = req.headers?.[name];
@@ -2768,7 +2845,7 @@ function registerShareLinkRoutes(http, service, engine, opts = {}) {
2768
2845
  const probe = await engine.find("sys_share_link", {
2769
2846
  where: { token: req.params.token },
2770
2847
  limit: 1,
2771
- context: SYSTEM_CTX6
2848
+ context: SYSTEM_CTX7
2772
2849
  });
2773
2850
  const row = Array.isArray(probe) && probe[0] ? probe[0] : null;
2774
2851
  if (row && !row.revoked_at && (!row.expires_at || Date.parse(row.expires_at) > Date.now())) {
@@ -2792,7 +2869,7 @@ function registerShareLinkRoutes(http, service, engine, opts = {}) {
2792
2869
  const rows = await engine.find(resolved.link.object_name, {
2793
2870
  where: { id: resolved.link.record_id },
2794
2871
  limit: 1,
2795
- context: SYSTEM_CTX6
2872
+ context: SYSTEM_CTX7
2796
2873
  });
2797
2874
  const record = Array.isArray(rows) && rows[0] ? rows[0] : null;
2798
2875
  if (!record) {
@@ -2829,12 +2906,12 @@ function registerShareLinkRoutes(http, service, engine, opts = {}) {
2829
2906
  sendError(res, 400, "UNSUPPORTED", "This share link does not expose messages");
2830
2907
  return;
2831
2908
  }
2832
- const SYSTEM_CTX8 = { isSystem: true, roles: [], permissions: [] };
2909
+ const SYSTEM_CTX9 = { isSystem: true, roles: [], permissions: [] };
2833
2910
  const rows = await engine.find("ai_messages", {
2834
2911
  where: { conversation_id: resolved.link.record_id },
2835
2912
  sort: [{ field: "created_at", order: "asc" }],
2836
2913
  limit: 500,
2837
- context: SYSTEM_CTX8
2914
+ context: SYSTEM_CTX9
2838
2915
  });
2839
2916
  res.status(200).json({ data: rows ?? [] });
2840
2917
  } catch (err) {
@@ -2849,7 +2926,7 @@ function registerShareLinkRoutes(http, service, engine, opts = {}) {
2849
2926
  }
2850
2927
 
2851
2928
  // src/rule-hooks.ts
2852
- var SYSTEM_CTX7 = { isSystem: true, roles: [], permissions: [] };
2929
+ var SYSTEM_CTX8 = { isSystem: true, roles: [], permissions: [] };
2853
2930
  var SHARING_RULE_HOOK_PACKAGE = "plugin-sharing:rules";
2854
2931
  function bindRuleHooks(engine, service, rules, logger) {
2855
2932
  const objects = /* @__PURE__ */ new Set();
@@ -2864,7 +2941,7 @@ function bindRuleHooks(engine, service, rules, logger) {
2864
2941
  const data = ctx?.result ?? ctx?.input?.data ?? {};
2865
2942
  const id = String(data?.id ?? ctx?.input?.id ?? "");
2866
2943
  if (!id) return;
2867
- await service.evaluateAllForRecord(objectName, id, SYSTEM_CTX7);
2944
+ await service.evaluateAllForRecord(objectName, id, SYSTEM_CTX8);
2868
2945
  } catch (err) {
2869
2946
  logger?.warn?.("[sharing-rule] hook evaluation failed", { object: objectName, error: err?.message });
2870
2947
  }