@milaboratories/pl-middle-layer 1.64.40 → 1.64.42

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.
Files changed (55) hide show
  1. package/dist/index.cjs +16 -0
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +3 -1
  4. package/dist/middle_layer/frontend_path.cjs +1 -0
  5. package/dist/middle_layer/frontend_path.cjs.map +1 -1
  6. package/dist/middle_layer/frontend_path.js +1 -0
  7. package/dist/middle_layer/frontend_path.js.map +1 -1
  8. package/dist/middle_layer/index.d.ts +2 -1
  9. package/dist/middle_layer/middle_layer.cjs +443 -16
  10. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  11. package/dist/middle_layer/middle_layer.d.ts +153 -12
  12. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  13. package/dist/middle_layer/middle_layer.js +445 -18
  14. package/dist/middle_layer/middle_layer.js.map +1 -1
  15. package/dist/middle_layer/ops.cjs +2 -1
  16. package/dist/middle_layer/ops.cjs.map +1 -1
  17. package/dist/middle_layer/ops.d.ts +5 -1
  18. package/dist/middle_layer/ops.d.ts.map +1 -1
  19. package/dist/middle_layer/ops.js +2 -1
  20. package/dist/middle_layer/ops.js.map +1 -1
  21. package/dist/middle_layer/project.d.ts +4 -4
  22. package/dist/middle_layer/project.d.ts.map +1 -1
  23. package/dist/middle_layer/sharing_list.cjs +203 -0
  24. package/dist/middle_layer/sharing_list.cjs.map +1 -0
  25. package/dist/middle_layer/sharing_list.d.ts +41 -0
  26. package/dist/middle_layer/sharing_list.d.ts.map +1 -0
  27. package/dist/middle_layer/sharing_list.js +198 -0
  28. package/dist/middle_layer/sharing_list.js.map +1 -0
  29. package/dist/model/index.cjs +29 -0
  30. package/dist/model/index.d.ts +2 -1
  31. package/dist/model/index.js +4 -0
  32. package/dist/model/project_model.d.ts +3 -3
  33. package/dist/model/project_model.d.ts.map +1 -1
  34. package/dist/model/sharing_model.cjs +81 -0
  35. package/dist/model/sharing_model.cjs.map +1 -0
  36. package/dist/model/sharing_model.d.ts +112 -0
  37. package/dist/model/sharing_model.d.ts.map +1 -0
  38. package/dist/model/sharing_model.js +68 -0
  39. package/dist/model/sharing_model.js.map +1 -0
  40. package/dist/mutator/block-pack/frontend.cjs +1 -0
  41. package/dist/mutator/block-pack/frontend.cjs.map +1 -1
  42. package/dist/mutator/block-pack/frontend.js +1 -0
  43. package/dist/mutator/block-pack/frontend.js.map +1 -1
  44. package/dist/mutator/sharing.cjs +138 -0
  45. package/dist/mutator/sharing.cjs.map +1 -0
  46. package/dist/mutator/sharing.js +130 -0
  47. package/dist/mutator/sharing.js.map +1 -0
  48. package/package.json +13 -13
  49. package/src/middle_layer/index.ts +1 -0
  50. package/src/middle_layer/middle_layer.ts +644 -22
  51. package/src/middle_layer/ops.ts +7 -0
  52. package/src/middle_layer/sharing_list.ts +343 -0
  53. package/src/model/index.ts +2 -0
  54. package/src/model/sharing_model.ts +150 -0
  55. package/src/mutator/sharing.ts +216 -0
@@ -2,9 +2,12 @@ const require_registry_v2_provider = require("../block_registry/registry-v2-prov
2
2
  require("../block_registry/index.cjs");
3
3
  const require_project_model = require("../model/project_model.cjs");
4
4
  const require_project_list = require("./project_list.cjs");
5
+ const require_sharing_model = require("../model/sharing_model.cjs");
5
6
  const require_block_pack = require("../mutator/block-pack/block_pack.cjs");
6
7
  const require_index$1 = require("../debug/index.cjs");
7
8
  const require_project = require("../mutator/project.cjs");
9
+ const require_sharing = require("../mutator/sharing.cjs");
10
+ const require_sharing_list = require("./sharing_list.cjs");
8
11
  const require_project$1 = require("./project.cjs");
9
12
  const require_ops = require("./ops.cjs");
10
13
  const require_watcher = require("../block_registry/watcher.cjs");
@@ -38,22 +41,38 @@ var MiddleLayer = class MiddleLayer {
38
41
  driverKit;
39
42
  signer;
40
43
  projectListResourceId;
44
+ sharingOutboxResourceId;
45
+ sharingStateResourceId;
41
46
  openedProjectsList;
42
47
  projectListTree;
48
+ sharingOutboxTree;
49
+ sharingStateTree;
50
+ pendingSharesTree;
43
51
  blockRegistryProvider;
44
- pl;
45
- /** Contains a reactive list of projects along with their meta information. */
46
52
  projectList;
47
- constructor(env, driverKit, signer, projectListResourceId, openedProjectsList, projectListTree, blockRegistryProvider, projectList) {
53
+ outgoingShares;
54
+ pendingShares;
55
+ liveEnvelopes;
56
+ pl;
57
+ constructor(env, driverKit, signer, projectListResourceId, sharingOutboxResourceId, sharingStateResourceId, openedProjectsList, projectListTree, sharingOutboxTree, sharingStateTree, pendingSharesTree, blockRegistryProvider, projectList, outgoingShares, pendingShares, liveEnvelopes) {
48
58
  this.env = env;
49
59
  this.driverKit = driverKit;
50
60
  this.signer = signer;
51
61
  this.projectListResourceId = projectListResourceId;
62
+ this.sharingOutboxResourceId = sharingOutboxResourceId;
63
+ this.sharingStateResourceId = sharingStateResourceId;
52
64
  this.openedProjectsList = openedProjectsList;
53
65
  this.projectListTree = projectListTree;
66
+ this.sharingOutboxTree = sharingOutboxTree;
67
+ this.sharingStateTree = sharingStateTree;
68
+ this.pendingSharesTree = pendingSharesTree;
54
69
  this.blockRegistryProvider = blockRegistryProvider;
55
70
  this.projectList = projectList;
71
+ this.outgoingShares = outgoingShares;
72
+ this.pendingShares = pendingShares;
73
+ this.liveEnvelopes = liveEnvelopes;
56
74
  this.pl = this.env.pl;
75
+ this.startEnvelopeCleanup();
57
76
  }
58
77
  /**
59
78
  * Get the OS where backend is running.
@@ -71,6 +90,38 @@ var MiddleLayer = class MiddleLayer {
71
90
  get serverCapabilities() {
72
91
  return this.pl.serverInfo.capabilities ?? [];
73
92
  }
93
+ /**
94
+ * Login of the authenticated user, for the "Signed in as" UI. `null` when the
95
+ * backend has no auth (local/dev mode) — the UI hides the element.
96
+ */
97
+ get currentUserLogin() {
98
+ return this.pl.userResources.authUser;
99
+ }
100
+ /**
101
+ * Whether the connected backend supports project sharing. Synthetic — computed
102
+ * in the middle layer from the backend capabilities the share flow needs (the
103
+ * cross-color field-reference relaxation the accept flow rests on). It can absorb
104
+ * additional required capabilities later without a UI change.
105
+ */
106
+ get sharingSupported() {
107
+ return this.serverCapabilities.includes("crossTreeRefs:v1");
108
+ }
109
+ /**
110
+ * Role of the authenticated user, from the `GetSessionInfo` RPC surfaced through the
111
+ * pl-client. `null` in no-auth mode (when {@link currentUserLogin} is null).
112
+ */
113
+ get currentUserRole() {
114
+ return this.pl.currentUserRole;
115
+ }
116
+ /**
117
+ * Whether the UI offers share-with-everybody — no role policy lives in the UI:
118
+ * serverCapabilities.has("publicGrants:v1") && canGrantToEveryone(currentUserRole)
119
+ * Not a security boundary: a crafted call still hits the backend's role +
120
+ * permission-ceiling gate.
121
+ */
122
+ get canShareWithEveryone() {
123
+ return this.serverCapabilities.includes("publicGrants:v1") && require_sharing_model.canGrantToEveryone(this.currentUserRole);
124
+ }
74
125
  /** Adds a runtime capability to the middle layer. */
75
126
  addRuntimeCapability(requirement, value = true) {
76
127
  this.env.runtimeCapabilities.addSupportedRequirement(requirement, value);
@@ -170,6 +221,360 @@ var MiddleLayer = class MiddleLayer {
170
221
  this.projectIdCache.set(newProjectId, signedRid);
171
222
  return newProjectId;
172
223
  }
224
+ /**
225
+ * Shares the given projects (Copy & Share). Snapshots the projects, creates one envelope, and
226
+ * grants it — all in one atomic write transaction, so a failed grant rolls the whole thing back
227
+ * and the outbox is left as it was.
228
+ *
229
+ * Two variants (see {@link ShareProjectsOptions}):
230
+ * - `{ recipients }` — one writable grant per named recipient; the envelope expires after the
231
+ * default TTL (`sharedAt + envelopeTtlMs`).
232
+ * - `{ everyone: true }` — one make-public grant (backend rewrites the target to the
233
+ * everyone-user); the envelope's `expiresAt` is `null`, so it never expires.
234
+ *
235
+ * v1 always passes `mode: "copy"`.
236
+ */
237
+ async shareProjects(projectIds, options) {
238
+ if (projectIds.length === 0) throw new Error("shareProjects: no projects given");
239
+ if ("everyone" in options && options.replace) {
240
+ const priorEveryone = (await this.findSupersedableEnvelopes(projectIds)).find((p) => p.everyone);
241
+ if (priorEveryone !== void 0) {
242
+ await this.changeShare(priorEveryone.shareId, { title: options.title });
243
+ return;
244
+ }
245
+ }
246
+ await this.createNewShare(projectIds, options);
247
+ }
248
+ /**
249
+ * Mints a fresh share: snapshots the projects into one new envelope (a fresh shareId),
250
+ * supersedes prior shares of the same project, and grants it — all in one atomic write
251
+ * transaction, so a failed grant rolls the whole thing back and the outbox is left as it was.
252
+ * The everyone-refresh path is the {@link changeShare} branch of {@link shareProjects}; this is
253
+ * the mint-a-new-envelope branch.
254
+ */
255
+ async createNewShare(projectIds, options) {
256
+ const everyone = "everyone" in options;
257
+ const sources = await Promise.all(projectIds.map(async (id) => ({
258
+ kind: "fresh",
259
+ projectId: id,
260
+ sourceRid: await this.resolveProjectId(id)
261
+ })));
262
+ const sender = this.currentUserLogin ?? "";
263
+ const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;
264
+ const priors = await this.findSupersedableEnvelopes(projectIds);
265
+ await this.pl.withWriteTx("MLShareProjects", async (tx) => {
266
+ if (everyone) {
267
+ for (const prior of priors) if (prior.everyone) tx.removeField((0, _milaboratories_pl_client.field)(this.sharingOutboxResourceId, prior.fieldName));
268
+ } else {
269
+ const newRecipients = new Set(options.recipients);
270
+ for (const prior of priors) {
271
+ if (prior.everyone) continue;
272
+ const toRemove = prior.recipients.filter((u) => newRecipients.has(u));
273
+ if (toRemove.length === 0) continue;
274
+ if (prior.recipients.filter((u) => !newRecipients.has(u)).length === 0) tx.removeField((0, _milaboratories_pl_client.field)(this.sharingOutboxResourceId, prior.fieldName));
275
+ else for (const u of toRemove) tx.revokeAccess(prior.rid, u);
276
+ }
277
+ }
278
+ const { envelope } = await require_sharing.buildShareEnvelope(tx, this.sharingOutboxResourceId, sources, {
279
+ mode: options.mode,
280
+ sender,
281
+ title: options.title,
282
+ expiresAt
283
+ });
284
+ const envelopeGid = await envelope.globalId;
285
+ if (everyone) tx.grantAccess(envelopeGid, "", { writable: true }, _milaboratories_pl_client.GrantType.ANY_AUTHORISED);
286
+ else for (const recipient of options.recipients) tx.grantAccess(envelopeGid, recipient, { writable: true });
287
+ await tx.commit();
288
+ });
289
+ await this.sharingOutboxTree.refreshState();
290
+ }
291
+ /**
292
+ * Changes a share in place (same {@link ShareId}), in one write transaction: re-snapshots live
293
+ * source projects and carries deleted ones' snapshots forward; applies edited recipients/title;
294
+ * transfers already-decided recipients' accept/reject records (they keep their copy and aren't
295
+ * re-prompted); re-grants; drops the old envelope.
296
+ *
297
+ * `opts.recipients` is the full targeted set (decided users are always kept). `opts.title`
298
+ * replaces the title — omit keeps the current one. `opts.everyone` upgrades targeted ->
299
+ * everyone; the reverse is impossible and ignored.
300
+ *
301
+ * `opts.projectActions` is a per-source-project decision, keyed by projectId: `update`
302
+ * re-snapshots the live source (falls back to carry if the source is gone), `keep` carries the
303
+ * existing snapshot (and its timestamp), `remove` drops the project from the pack. A project not
304
+ * in the map defaults to `keep`. Omit the whole map for the legacy auto behavior (live sources
305
+ * updated, gone ones kept) — the everyone-refresh path relies on that.
306
+ */
307
+ async changeShare(shareId, opts = {}) {
308
+ await this.pl.withWriteTx("MLChangeShare", async (tx) => {
309
+ const old = await this.resolveOutboxEnvelope(tx, shareId);
310
+ if (old === void 0) throw new Error(`changeShare: no live share with id ${shareId} in the outbox.`);
311
+ const self = this.currentUserLogin ?? "";
312
+ const grants = await tx.listGrants(old.rid);
313
+ const everyone = grants.some((g) => (0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user)) || opts.everyone === true;
314
+ const oldRd = await tx.getResourceData(old.rid, true);
315
+ const snapshotByUuid = /* @__PURE__ */ new Map();
316
+ const acceptances = [];
317
+ for (const f of oldRd.fields) {
318
+ if ((0, _milaboratories_pl_client.isNullSignedResourceId)(f.value)) continue;
319
+ if (require_sharing.isEnvelopeProjectField(f.name)) snapshotByUuid.set(require_sharing.envelopeProjectFieldUuid(f.name), f.value);
320
+ else if (require_sharing_model.isAcceptanceField(f.name)) {
321
+ const raw = (await tx.getResourceData(f.value, false)).data;
322
+ if (raw === void 0) continue;
323
+ acceptances.push({
324
+ login: require_sharing_model.acceptanceFieldLogin(f.name),
325
+ acc: (0, _milaboratories_ts_helpers.cachedDeserialize)(raw)
326
+ });
327
+ }
328
+ }
329
+ const decidedLogins = acceptances.map((a) => a.login);
330
+ const priorRecipients = grants.filter((g) => !(0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user) && g.user !== self).map((g) => g.user);
331
+ const recipients = everyone ? [] : Array.from(/* @__PURE__ */ new Set([...opts.recipients ?? priorRecipients, ...decidedLogins]));
332
+ const liveProjects = /* @__PURE__ */ new Map();
333
+ const projList = await tx.getResourceData(this.projectListResourceId, true);
334
+ for (const f of projList.fields) {
335
+ if ((0, _milaboratories_pl_client.isNullSignedResourceId)(f.value)) continue;
336
+ liveProjects.set((0, _milaboratories_pl_client.resourceIdToString)(f.value), f.value);
337
+ }
338
+ const actions = opts.projectActions;
339
+ const sources = [];
340
+ for (const uuid of Object.keys(old.data.projects)) {
341
+ const { label, source, updatedAt } = old.data.projects[uuid];
342
+ const liveRid = liveProjects.get(source);
343
+ const action = actions ? actions[source] ?? "keep" : liveRid !== void 0 ? "update" : "keep";
344
+ if (action === "remove") continue;
345
+ if (action === "update" && liveRid !== void 0) sources.push({
346
+ kind: "fresh",
347
+ projectId: source,
348
+ sourceRid: liveRid
349
+ });
350
+ else {
351
+ const snapshotRid = snapshotByUuid.get(uuid);
352
+ if (snapshotRid !== void 0) sources.push({
353
+ kind: "carry",
354
+ projectId: source,
355
+ label,
356
+ snapshotRid,
357
+ updatedAt
358
+ });
359
+ }
360
+ }
361
+ const title = opts.title === void 0 ? old.data.title : opts.title.trim();
362
+ const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;
363
+ tx.removeField((0, _milaboratories_pl_client.field)(this.sharingOutboxResourceId, old.fieldName));
364
+ const { envelope } = await require_sharing.buildShareEnvelope(tx, this.sharingOutboxResourceId, sources, {
365
+ mode: old.data.mode,
366
+ sender: self,
367
+ title,
368
+ expiresAt,
369
+ shareId
370
+ });
371
+ for (const { login, acc } of acceptances) {
372
+ if (!everyone && !recipients.includes(login)) continue;
373
+ require_sharing.writeEnvelopeAcceptance(tx, envelope, login, acc.action, acc.timestamp);
374
+ }
375
+ const gid = await envelope.globalId;
376
+ if (everyone) tx.grantAccess(gid, "", { writable: true }, _milaboratories_pl_client.GrantType.ANY_AUTHORISED);
377
+ else for (const r of recipients) tx.grantAccess(gid, r, { writable: true });
378
+ await tx.commit();
379
+ });
380
+ await this.sharingOutboxTree.refreshState();
381
+ }
382
+ /**
383
+ * Finds the donor's own outgoing envelopes built from any of the given source projects —
384
+ * the supersede candidates for a fresh share of the same project(s). Reads each envelope's
385
+ * recipient set via `ListGrants` so the caller can pull individual recipients or detect an
386
+ * everyone-share.
387
+ */
388
+ async findSupersedableEnvelopes(projectIds) {
389
+ const wanted = new Set(projectIds);
390
+ const matched = await this.pl.withReadTx("MLFindSupersede", async (tx) => {
391
+ const outbox = await tx.getResourceData(this.sharingOutboxResourceId, true);
392
+ const out = [];
393
+ for (const f of outbox.fields) {
394
+ if ((0, _milaboratories_pl_client.isNullSignedResourceId)(f.value)) continue;
395
+ const rd = await tx.getResourceData(f.value, false);
396
+ if (rd.data === void 0) continue;
397
+ const data = require_sharing_model.decodeEnvelopeData(rd.data);
398
+ if (Object.values(data.projects).some((p) => wanted.has(p.source))) out.push({
399
+ fieldName: f.name,
400
+ rid: f.value,
401
+ shareId: data.shareId
402
+ });
403
+ }
404
+ return out;
405
+ });
406
+ return await Promise.all(matched.map(async ({ fieldName, rid, shareId }) => {
407
+ const grants = await this.pl.userResources.listGrants(rid);
408
+ return {
409
+ fieldName,
410
+ rid,
411
+ shareId,
412
+ everyone: grants.some((g) => (0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user)),
413
+ recipients: grants.filter((g) => !(0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user)).map((g) => g.user)
414
+ };
415
+ }));
416
+ }
417
+ /**
418
+ * Revokes and deletes an outgoing share for all recipients: detaches and deletes the envelope, and
419
+ * its grants are revoked along with it. Already-accepted copies are unaffected (ref-counting keeps
420
+ * the adopted resources alive). Idempotent — revoking a share that is already gone is a no-op.
421
+ */
422
+ async revokeShare(shareId) {
423
+ await this.pl.withWriteTx("MLRevokeShare", async (tx) => {
424
+ const target = await this.resolveOutboxEnvelope(tx, shareId);
425
+ if (target === void 0) return;
426
+ tx.removeField((0, _milaboratories_pl_client.field)(this.sharingOutboxResourceId, target.fieldName));
427
+ await tx.commit();
428
+ });
429
+ await this.sharingOutboxTree.refreshState();
430
+ }
431
+ /**
432
+ * Resolves a live envelope from the donor's own outbox by its logical `shareId`, returning the
433
+ * outbox field name (for detach), the signed envelope id, and its decoded {@link EnvelopeData}.
434
+ * The outbox is keyed by `{shareId}` directly, but a replaced/legacy share may have drifted, so
435
+ * we match on the decoded `shareId` rather than the field name alone.
436
+ */
437
+ async resolveOutboxEnvelope(tx, shareId) {
438
+ const outboxData = await tx.getResourceData(this.sharingOutboxResourceId, true);
439
+ for (const f of outboxData.fields) {
440
+ if ((0, _milaboratories_pl_client.isNullSignedResourceId)(f.value)) continue;
441
+ const rd = await tx.getResourceData(f.value, false);
442
+ if (rd.data === void 0) continue;
443
+ const data = require_sharing_model.decodeEnvelopeData(rd.data);
444
+ if (data.shareId === shareId) return {
445
+ fieldName: f.name,
446
+ rid: f.value,
447
+ data
448
+ };
449
+ }
450
+ }
451
+ /**
452
+ * Resolves currently-shared envelopes (granted to this user) to their resource ids, keyed by
453
+ * the envelope's logical `shareId`.
454
+ *
455
+ * Reads the {@link liveEnvelopes} Computable — the same shared-resource discovery tree that
456
+ * feeds {@link pendingShares}. This is the single discovery mechanism: there is no separate
457
+ * `ListUserResources` re-stream on every accept/reject. `refreshState()` is awaited first so a
458
+ * just-granted envelope is observed (the tree's discovery poll may otherwise lag a freshly
459
+ * landed grant). The tree is gRPC-only, so this is empty on a REST-connected client.
460
+ */
461
+ async resolveLiveEnvelopes() {
462
+ await this.pendingSharesTree.refreshState();
463
+ const live = await this.liveEnvelopes.getValue() ?? [];
464
+ const map = /* @__PURE__ */ new Map();
465
+ for (const e of live) map.set(e.data.shareId, e);
466
+ return map;
467
+ }
468
+ /**
469
+ * Accepts one or more pending shares: duplicates each share's projects into this user's
470
+ * project list, records the decision per share, and (read-write share) writes the donor-visible
471
+ * acceptance onto the envelope. Per-share failures (e.g. an expiry race) are collected, not
472
+ * short-circuited — the rest still get accepted. Accept-all = pass every current pending shareId.
473
+ *
474
+ * `rename` resolves label collisions (same callback contract as {@link duplicateProject}), but
475
+ * the source lives in the envelope tree, so accept calls the low-level mutator directly.
476
+ */
477
+ async acceptShare(shareIds, rename) {
478
+ const live = await this.resolveLiveEnvelopes();
479
+ const login = this.currentUserLogin;
480
+ const accepted = [];
481
+ const failed = [];
482
+ for (const shareId of shareIds) {
483
+ const envelope = live.get(shareId);
484
+ if (envelope === void 0) {
485
+ failed.push({
486
+ shareId,
487
+ error: "Share is no longer available."
488
+ });
489
+ continue;
490
+ }
491
+ try {
492
+ const now = Date.now();
493
+ const createdRids = await this.pl.withWriteTx("MLAcceptShare", async (tx) => {
494
+ const created = await require_sharing.copyEnvelopeProjectsIntoList(tx, envelope.rid, this.projectListResourceId, rename);
495
+ require_sharing.writeSharingDecision(tx, this.sharingStateResourceId, shareId, {
496
+ decision: "accepted",
497
+ timestamp: now,
498
+ envelopeSharedAt: envelope.data.sharedAt,
499
+ acceptedProjects: require_sharing.resourceIdsToStrings(created)
500
+ });
501
+ if (login !== null && envelope.data.mode !== "read-only") require_sharing.writeEnvelopeAcceptance(tx, envelope.rid, login, "accepted", now);
502
+ await tx.commit();
503
+ return created;
504
+ });
505
+ for (const rid of createdRids) {
506
+ const projectId = (0, _milaboratories_pl_client.resourceIdToString)(rid);
507
+ this.projectIdCache.set(projectId, rid);
508
+ accepted.push(projectId);
509
+ }
510
+ } catch (e) {
511
+ failed.push({
512
+ shareId,
513
+ error: e instanceof Error ? e.message : String(e)
514
+ });
515
+ }
516
+ }
517
+ await Promise.all([this.projectListTree.refreshState(), this.sharingStateTree.refreshState()]);
518
+ return {
519
+ accepted,
520
+ failed
521
+ };
522
+ }
523
+ /** Records rejection of a pending share; it never surfaces again. */
524
+ async rejectShare(shareId) {
525
+ const envelope = (await this.resolveLiveEnvelopes()).get(shareId);
526
+ const login = this.currentUserLogin;
527
+ const now = Date.now();
528
+ await this.pl.withWriteTx("MLRejectShare", async (tx) => {
529
+ require_sharing.writeSharingDecision(tx, this.sharingStateResourceId, shareId, {
530
+ decision: "rejected",
531
+ timestamp: now,
532
+ envelopeSharedAt: envelope?.data.sharedAt ?? now,
533
+ acceptedProjects: []
534
+ });
535
+ if (envelope !== void 0 && login !== null && envelope.data.mode !== "read-only") require_sharing.writeEnvelopeAcceptance(tx, envelope.rid, login, "rejected", now);
536
+ await tx.commit();
537
+ });
538
+ await this.sharingStateTree.refreshState();
539
+ }
540
+ static EnvelopeCleanupIntervalMs = 6 * 3600 * 1e3;
541
+ envelopeCleanupTimer;
542
+ /** On ML start and every 6h, delete envelopes whose immutable `expiresAt` has passed. */
543
+ startEnvelopeCleanup() {
544
+ this.runEnvelopeCleanup();
545
+ this.envelopeCleanupTimer = setInterval(() => {
546
+ this.runEnvelopeCleanup();
547
+ }, MiddleLayer.EnvelopeCleanupIntervalMs);
548
+ this.envelopeCleanupTimer.unref?.();
549
+ }
550
+ /** Scans the donor's outbox and deletes expired envelopes (backend auto-revokes their grants).
551
+ * Envelopes with `expiresAt: null` (share-with-everybody) are skipped. */
552
+ async runEnvelopeCleanup() {
553
+ try {
554
+ const now = Date.now();
555
+ const expired = await this.pl.withReadTx("MLEnvelopeCleanupScan", async (tx) => {
556
+ const data = await tx.getResourceData(this.sharingOutboxResourceId, true);
557
+ const toDelete = [];
558
+ for (const f of data.fields) {
559
+ if ((0, _milaboratories_pl_client.isNullSignedResourceId)(f.value)) continue;
560
+ const rd = await tx.getResourceData(f.value, false);
561
+ if (rd.data === void 0) continue;
562
+ const envData = require_sharing_model.decodeEnvelopeData(rd.data);
563
+ if (envData.expiresAt === null) continue;
564
+ if (envData.expiresAt <= now) toDelete.push({ fieldName: f.name });
565
+ }
566
+ return toDelete;
567
+ });
568
+ if (expired.length === 0) return;
569
+ await this.pl.withWriteTx("MLEnvelopeCleanup", async (tx) => {
570
+ for (const { fieldName } of expired) tx.removeField((0, _milaboratories_pl_client.field)(this.sharingOutboxResourceId, fieldName));
571
+ await tx.commit();
572
+ });
573
+ await this.sharingOutboxTree.refreshState();
574
+ } catch (e) {
575
+ this.env.logger.warn(`envelope cleanup failed: ${e instanceof Error ? e.message : String(e)}`);
576
+ }
577
+ }
173
578
  openedProjects = /* @__PURE__ */ new Map();
174
579
  /** Opens a project, and starts corresponding project maintenance loop. */
175
580
  async openProject(id) {
@@ -202,8 +607,14 @@ var MiddleLayer = class MiddleLayer {
202
607
  * them.
203
608
  */
204
609
  async close() {
610
+ if (this.envelopeCleanupTimer !== void 0) clearInterval(this.envelopeCleanupTimer);
205
611
  await Promise.all([...this.openedProjects.values()].map((prj) => prj.destroy()));
206
- await this.projectListTree.terminate();
612
+ await Promise.all([
613
+ this.projectListTree.terminate(),
614
+ this.sharingOutboxTree.terminate(),
615
+ this.sharingStateTree.terminate(),
616
+ this.pendingSharesTree.terminate()
617
+ ]);
207
618
  await this.env.dispose();
208
619
  await this.pl.close();
209
620
  }
@@ -230,17 +641,28 @@ var MiddleLayer = class MiddleLayer {
230
641
  ops.defaultTreeOptions.logStat = require_index$1.getDebugFlags().logTreeStats;
231
642
  ops.debugOps.dumpInitialTreeState = require_index$1.getDebugFlags().dumpInitialTreeState;
232
643
  if (ops.defaultTreeOptions.traversalMode === void 0 && require_index$1.getDebugFlags().treeTraversalMode !== void 0) ops.defaultTreeOptions.traversalMode = require_index$1.getDebugFlags().treeTraversalMode;
233
- const projects = await pl.withWriteTx("MLInitialization", async (tx) => {
234
- const projectsField = (0, _milaboratories_pl_client.field)(tx.clientRoot, require_project_list.ProjectsField);
235
- tx.createField(projectsField, "Dynamic");
236
- const projectsFieldData = await tx.getField(projectsField);
237
- if ((0, _milaboratories_pl_client.isNullSignedResourceId)(projectsFieldData.value)) {
238
- const projects = tx.createEphemeral(require_project_list.ProjectsResourceType);
239
- tx.lock(projects);
240
- tx.setField(projectsField, projects);
241
- await tx.commit();
242
- return await projects.globalId;
243
- } else return projectsFieldData.value;
644
+ const { projects, sharingOutbox, sharingState } = await pl.withWriteTx("MLInitialization", async (tx) => {
645
+ const lazyInit = async (fieldName, type) => {
646
+ const f = (0, _milaboratories_pl_client.field)(tx.clientRoot, fieldName);
647
+ tx.createField(f, "Dynamic");
648
+ const fData = await tx.getField(f);
649
+ if ((0, _milaboratories_pl_client.isNullSignedResourceId)(fData.value)) {
650
+ const ref = tx.createEphemeral(type);
651
+ tx.lock(ref);
652
+ tx.setField(f, ref);
653
+ return { ref };
654
+ }
655
+ return { existing: fData.value };
656
+ };
657
+ const projectsR = await lazyInit(require_project_list.ProjectsField, require_project_list.ProjectsResourceType);
658
+ const outboxR = await lazyInit(require_sharing_model.SharingOutboxField, require_sharing_model.SharingOutboxResourceType);
659
+ const stateR = await lazyInit(require_sharing_model.SharingStateField, require_sharing_model.SharingStateResourceType);
660
+ await tx.commit();
661
+ return {
662
+ projects: projectsR.existing ?? await projectsR.ref.globalId,
663
+ sharingState: stateR.existing ?? await stateR.ref.globalId,
664
+ sharingOutbox: outboxR.existing ?? await outboxR.ref.globalId
665
+ };
244
666
  });
245
667
  const logger = ops.logger;
246
668
  const driverKit = await require_driver_kit.initDriverKit(pl, workdir, ops.frontendDownloadPath, ops);
@@ -283,7 +705,12 @@ var MiddleLayer = class MiddleLayer {
283
705
  };
284
706
  const openedProjects = new _milaboratories_computable.WatchableValue([]);
285
707
  const projectListTC = await require_project_list.createProjectList(pl, projects, openedProjects, env);
286
- return new MiddleLayer(env, driverKit, driverKit.signer, projects, openedProjects, projectListTC.tree, v2RegistryProvider, projectListTC.computable);
708
+ const outgoingTC = await require_sharing_list.createOutgoingShares(pl, sharingOutbox, env);
709
+ const sharingStateTree = await require_sharing_list.createSharingStateTree(pl, sharingState, env);
710
+ const pendingSharesTree = await require_sharing_list.createPendingSharesTree(pl, env);
711
+ const pendingShares = require_sharing_list.createPendingSharesComputable(pendingSharesTree, sharingStateTree, pl.userResources.authUser);
712
+ const liveEnvelopes = require_sharing_list.createLiveEnvelopesComputable(pendingSharesTree);
713
+ return new MiddleLayer(env, driverKit, driverKit.signer, projects, sharingOutbox, sharingState, openedProjects, projectListTC.tree, outgoingTC.tree, sharingStateTree, pendingSharesTree, v2RegistryProvider, projectListTC.computable, outgoingTC.computable, pendingShares, liveEnvelopes);
287
714
  }
288
715
  };
289
716
  //#endregion