@milaboratories/pl-middle-layer 1.68.3 → 1.69.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.
Files changed (68) hide show
  1. package/dist/index.cjs +10 -0
  2. package/dist/index.d.ts +6 -2
  3. package/dist/index.js +5 -2
  4. package/dist/middle_layer/build_stamp.cjs +1 -1
  5. package/dist/middle_layer/build_stamp.js +1 -1
  6. package/dist/middle_layer/index.cjs +2 -0
  7. package/dist/middle_layer/index.d.ts +2 -1
  8. package/dist/middle_layer/index.js +2 -1
  9. package/dist/middle_layer/middle_layer.cjs +333 -26
  10. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  11. package/dist/middle_layer/middle_layer.d.ts +125 -7
  12. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  13. package/dist/middle_layer/middle_layer.js +335 -28
  14. package/dist/middle_layer/middle_layer.js.map +1 -1
  15. package/dist/middle_layer/project_list.d.ts +1 -1
  16. package/dist/middle_layer/sharing_list.cjs +13 -5
  17. package/dist/middle_layer/sharing_list.cjs.map +1 -1
  18. package/dist/middle_layer/sharing_list.d.ts +16 -2
  19. package/dist/middle_layer/sharing_list.d.ts.map +1 -1
  20. package/dist/middle_layer/sharing_list.js +14 -6
  21. package/dist/middle_layer/sharing_list.js.map +1 -1
  22. package/dist/middle_layer/template_list.cjs +72 -0
  23. package/dist/middle_layer/template_list.cjs.map +1 -0
  24. package/dist/middle_layer/template_list.d.ts +77 -0
  25. package/dist/middle_layer/template_list.d.ts.map +1 -0
  26. package/dist/middle_layer/template_list.js +64 -0
  27. package/dist/middle_layer/template_list.js.map +1 -0
  28. package/dist/model/index.cjs +8 -0
  29. package/dist/model/index.d.ts +5 -2
  30. package/dist/model/index.js +4 -2
  31. package/dist/model/sharing_model.cjs +59 -2
  32. package/dist/model/sharing_model.cjs.map +1 -1
  33. package/dist/model/sharing_model.d.ts +70 -7
  34. package/dist/model/sharing_model.d.ts.map +1 -1
  35. package/dist/model/sharing_model.js +57 -3
  36. package/dist/model/sharing_model.js.map +1 -1
  37. package/dist/model/template_serializer.d.ts +36 -0
  38. package/dist/model/template_serializer.d.ts.map +1 -1
  39. package/dist/model/template_share.cjs +42 -0
  40. package/dist/model/template_share.cjs.map +1 -0
  41. package/dist/model/template_share.d.ts +27 -0
  42. package/dist/model/template_share.d.ts.map +1 -0
  43. package/dist/model/template_share.js +42 -0
  44. package/dist/model/template_share.js.map +1 -0
  45. package/dist/mutator/project.cjs +2 -2
  46. package/dist/mutator/project.js +2 -2
  47. package/dist/mutator/sharing.cjs +40 -2
  48. package/dist/mutator/sharing.cjs.map +1 -1
  49. package/dist/mutator/sharing.js +40 -3
  50. package/dist/mutator/sharing.js.map +1 -1
  51. package/dist/mutator/template.cjs +54 -0
  52. package/dist/mutator/template.cjs.map +1 -0
  53. package/dist/mutator/template.js +52 -0
  54. package/dist/mutator/template.js.map +1 -0
  55. package/package.json +8 -8
  56. package/src/middle_layer/index.ts +9 -0
  57. package/src/middle_layer/middle_layer.ts +449 -36
  58. package/src/middle_layer/sharing_list.ts +37 -7
  59. package/src/middle_layer/template_list.ts +177 -0
  60. package/src/middle_layer/templates.test.ts +301 -0
  61. package/src/model/index.ts +14 -0
  62. package/src/model/sharing_model.test.ts +115 -1
  63. package/src/model/sharing_model.ts +134 -9
  64. package/src/model/template_share.test.ts +78 -0
  65. package/src/model/template_share.ts +52 -0
  66. package/src/mutator/sharing.ts +55 -3
  67. package/src/mutator/template.ts +75 -0
  68. package/src/test/with_ml.ts +38 -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_template_list = require("./template_list.cjs");
6
+ const require_template = require("../mutator/template.cjs");
5
7
  const require_sharing_model = require("../model/sharing_model.cjs");
6
8
  const require_template_resolve = require("../model/template_resolve.cjs");
7
9
  const require_template_apply = require("../model/template_apply.cjs");
10
+ const require_template_share = require("../model/template_share.cjs");
8
11
  const require_block_pack = require("../mutator/block-pack/block_pack.cjs");
9
12
  const require_index$1 = require("../debug/index.cjs");
10
13
  const require_project = require("../mutator/project.cjs");
@@ -50,33 +53,39 @@ var MiddleLayer = class MiddleLayer {
50
53
  driverKit;
51
54
  signer;
52
55
  projectListResourceId;
56
+ templateListResourceId;
53
57
  sharingOutboxResourceId;
54
58
  sharingStateResourceId;
55
59
  openedProjectsList;
56
60
  projectListTree;
61
+ templateListTree;
57
62
  sharingOutboxTree;
58
63
  sharingStateTree;
59
64
  pendingSharesTree;
60
65
  blockRegistryProvider;
61
66
  projectList;
67
+ templateList;
62
68
  outgoingShares;
63
69
  pendingShares;
64
70
  liveEnvelopes;
65
71
  pl;
66
- constructor(env, driverKit, signer, projectListResourceId, sharingOutboxResourceId, sharingStateResourceId, openedProjectsList, projectListTree, sharingOutboxTree, sharingStateTree, pendingSharesTree, blockRegistryProvider, projectList, outgoingShares, pendingShares, liveEnvelopes) {
72
+ constructor(env, driverKit, signer, projectListResourceId, templateListResourceId, sharingOutboxResourceId, sharingStateResourceId, openedProjectsList, projectListTree, templateListTree, sharingOutboxTree, sharingStateTree, pendingSharesTree, blockRegistryProvider, projectList, templateList, outgoingShares, pendingShares, liveEnvelopes) {
67
73
  this.env = env;
68
74
  this.driverKit = driverKit;
69
75
  this.signer = signer;
70
76
  this.projectListResourceId = projectListResourceId;
77
+ this.templateListResourceId = templateListResourceId;
71
78
  this.sharingOutboxResourceId = sharingOutboxResourceId;
72
79
  this.sharingStateResourceId = sharingStateResourceId;
73
80
  this.openedProjectsList = openedProjectsList;
74
81
  this.projectListTree = projectListTree;
82
+ this.templateListTree = templateListTree;
75
83
  this.sharingOutboxTree = sharingOutboxTree;
76
84
  this.sharingStateTree = sharingStateTree;
77
85
  this.pendingSharesTree = pendingSharesTree;
78
86
  this.blockRegistryProvider = blockRegistryProvider;
79
87
  this.projectList = projectList;
88
+ this.templateList = templateList;
80
89
  this.outgoingShares = outgoingShares;
81
90
  this.pendingShares = pendingShares;
82
91
  this.liveEnvelopes = liveEnvelopes;
@@ -263,13 +272,29 @@ var MiddleLayer = class MiddleLayer {
263
272
  * the whole document
264
273
  */
265
274
  async applyTemplateToProject(id, document, provider, options = {}) {
266
- const resolution = await require_template_resolve.resolveTemplateEntries(document, provider, { allowUnstable: options.allowUnstable ?? false });
267
- if (resolution.problems.length > 0) return {
275
+ const preparation = await this.prepareTemplateEntries(document, provider, { allowUnstable: options.allowUnstable ?? false });
276
+ if (preparation.problems.length > 0) return {
268
277
  added: [],
269
- problems: resolution.problems
278
+ problems: preparation.problems
270
279
  };
271
- const byEntryId = new Map(document.blocks.map((entry) => [entry.id, entry]));
280
+ return await this.applyPreparedEntries(id, document, preparation.prepared, options.author);
281
+ }
282
+ /**
283
+ * Stages 1 and 2 of an apply — resolve every entry to a block pack, then prepare every
284
+ * block — for a document that may not have a project yet.
285
+ *
286
+ * Neither stage creates anything, which is what lets a caller run them before it decides to
287
+ * create a project at all: {@link createProjectFromTemplate} does exactly that, so an
288
+ * unapplicable template leaves no empty project behind.
289
+ */
290
+ async prepareTemplateEntries(document, provider, options) {
272
291
  const prepared = /* @__PURE__ */ new Map();
292
+ const resolution = await require_template_resolve.resolveTemplateEntries(document, provider, options);
293
+ if (resolution.problems.length > 0) return {
294
+ prepared,
295
+ problems: [...resolution.problems]
296
+ };
297
+ const byEntryId = new Map(document.blocks.map((entry) => [entry.id, entry]));
273
298
  const problems = [];
274
299
  for (const entry of resolution.resolved) try {
275
300
  const documentEntry = byEntryId.get(entry.entryId);
@@ -305,14 +330,22 @@ var MiddleLayer = class MiddleLayer {
305
330
  error: `This entry's block could not be installed: ${(0, _platforma_sdk_model.ensureError)(e).message}`
306
331
  });
307
332
  }
308
- if (problems.length > 0) return {
309
- added: [],
333
+ return {
334
+ prepared,
310
335
  problems
311
336
  };
337
+ }
338
+ /**
339
+ * Stage 3 of an apply — create the blocks in one transaction, all or nothing.
340
+ *
341
+ * An entry it cannot create throws, the transaction is never committed, and the project keeps
342
+ * none of the blocks the apply had placed.
343
+ */
344
+ async applyPreparedEntries(id, document, prepared, author) {
312
345
  const rid = await this.resolveProjectId(id);
313
346
  let added = [];
314
347
  try {
315
- await require_project.withProjectAuthored(this.env.projectHelper, this.pl, rid, options.author, (mut) => {
348
+ await require_project.withProjectAuthored(this.env.projectHelper, this.pl, rid, author, (mut) => {
316
349
  added = require_template_construct.applyTemplateEntries({
317
350
  document,
318
351
  placer: mut,
@@ -338,6 +371,131 @@ var MiddleLayer = class MiddleLayer {
338
371
  problems: []
339
372
  };
340
373
  }
374
+ templateIdCache = new lru_cache.LRUCache({ max: 1024 });
375
+ /**
376
+ * Saves a project as a template: a snapshot of its blocks and their params, no data.
377
+ *
378
+ * The document the export produced is what gets stored; the YAML it also rendered is a
379
+ * file format, and a stored template is rendered to it only on download.
380
+ *
381
+ * A block that cannot be expressed as a template entry stores nothing at all, and every
382
+ * such block is reported — fixing an unexportable project takes one pass, not one per block.
383
+ *
384
+ * @param projectId project to snapshot
385
+ * @param label label for the template; defaults to the project's own label
386
+ */
387
+ async saveProjectAsTemplate(projectId, label) {
388
+ const outcome = await this.exportProjectAsTemplate(projectId);
389
+ if (!outcome.ok) return {
390
+ ok: false,
391
+ problems: outcome.problems
392
+ };
393
+ const rid = await this.resolveProjectId(projectId);
394
+ let tpl;
395
+ await this.pl.withWriteTx("MLSaveProjectAsTemplate", async (tx) => {
396
+ const meta = await tx.getKValueJson(rid, require_project_model.ProjectMetaKey);
397
+ tpl = require_template.createTemplate(tx, this.templateListResourceId, label ?? meta.label, {
398
+ schemaVersion: 1,
399
+ document: outcome.document,
400
+ sourceProjectLabel: meta.label
401
+ });
402
+ await tx.commit();
403
+ });
404
+ await this.templateListTree.refreshState();
405
+ const signedRid = await tpl.globalId;
406
+ const templateId = (0, _milaboratories_pl_client.resourceIdToString)(signedRid);
407
+ this.templateIdCache.set(templateId, signedRid);
408
+ return {
409
+ ok: true,
410
+ templateId
411
+ };
412
+ }
413
+ /** Changes a template's label. The stored document is immutable and stays untouched —
414
+ * improving a template means saving a new one. */
415
+ async renameTemplate(id, label) {
416
+ const rid = await this.resolveTemplateId(id);
417
+ await this.pl.withWriteTx("MLRenameTemplate", async (tx) => {
418
+ require_template.renameTemplate(tx, rid, label);
419
+ await tx.commit();
420
+ });
421
+ await this.templateListTree.refreshState();
422
+ }
423
+ /** Permanently deletes a template from the template list. */
424
+ async deleteTemplate(id) {
425
+ await this.pl.withWriteTx("MLRemoveTemplate", async (tx) => {
426
+ await require_template.deleteTemplate(tx, this.templateListResourceId, id);
427
+ await tx.commit();
428
+ });
429
+ this.templateIdCache.delete(id);
430
+ await this.templateListTree.refreshState();
431
+ }
432
+ /** Reads a stored template: its document plus what was true when it was taken. */
433
+ async getTemplateData(id) {
434
+ const rid = await this.resolveTemplateId(id);
435
+ return await this.pl.withReadTx("MLGetTemplate", async (tx) => {
436
+ const rd = await tx.getResourceData(rid, false);
437
+ if (rd.data === void 0) throw new Error(`Template ${id} carries no document.`);
438
+ return require_template_list.decodeStoredTemplateData(rd.data);
439
+ });
440
+ }
441
+ /**
442
+ * Where each entry of a stored template would get its block from, and which entries have
443
+ * nowhere to get one — resolution creates nothing, so this is the preview a UI shows before
444
+ * offering Apply. {@link createProjectFromTemplate} runs the same stage itself.
445
+ */
446
+ async resolveTemplate(id, provider, options = {}) {
447
+ return await require_template_resolve.resolveTemplateEntries((await this.getTemplateData(id)).document, provider, { allowUnstable: options.allowUnstable ?? false });
448
+ }
449
+ /**
450
+ * Creates one project holding every block the stored template lists, in the template's order.
451
+ *
452
+ * Resolution and preparation run before the project exists, so a template with an entry
453
+ * nothing can supply a block for leaves no empty project in the list. The one write that
454
+ * follows is all or nothing, and an entry it rejects takes the project with it.
455
+ *
456
+ * @param id template to apply
457
+ * @param label label for the new project
458
+ * @param provider where each entry's block comes from
459
+ * @param options `allowUnstable` widens resolution to pre-release implementations
460
+ */
461
+ async createProjectFromTemplate(id, label, provider, options = {}) {
462
+ const stored = await this.getTemplateData(id);
463
+ const preparation = await this.prepareTemplateEntries(stored.document, provider, { allowUnstable: options.allowUnstable ?? false });
464
+ if (preparation.problems.length > 0) return {
465
+ ok: false,
466
+ problems: preparation.problems
467
+ };
468
+ const projectId = await this.createProject({ label });
469
+ const report = await this.applyPreparedEntries(projectId, stored.document, preparation.prepared, options.author);
470
+ if (report.problems.length > 0) {
471
+ await this.deleteProject(projectId);
472
+ return {
473
+ ok: false,
474
+ problems: report.problems
475
+ };
476
+ }
477
+ return {
478
+ ok: true,
479
+ projectId,
480
+ added: report.added
481
+ };
482
+ }
483
+ /** Resolves a TemplateId to a signed SignedResourceId.
484
+ * Uses LRU cache with TX-scan fallback. */
485
+ async resolveTemplateId(templateId) {
486
+ const cached = this.templateIdCache.get(templateId);
487
+ if (cached !== void 0) return cached;
488
+ const rid = await this.pl.withReadTx("ResolveTemplateId", async (tx) => {
489
+ const data = await tx.getResourceData(this.templateListResourceId, true);
490
+ for (const f of data.fields) {
491
+ if ((0, _milaboratories_pl_client.isNullSignedResourceId)(f.value)) continue;
492
+ if ((0, _milaboratories_pl_client.resourceIdToString)(f.value) === templateId) return f.value;
493
+ }
494
+ throw new Error(`Template ${templateId} not found in template list.`);
495
+ });
496
+ this.templateIdCache.set(templateId, rid);
497
+ return rid;
498
+ }
341
499
  /** Permanently deletes project from the project list, this will result in
342
500
  * destruction of all attached objects, like files, analysis results etc. */
343
501
  async deleteProject(id) {
@@ -467,14 +625,98 @@ var MiddleLayer = class MiddleLayer {
467
625
  title: options.title,
468
626
  expiresAt
469
627
  });
470
- const envelopeGid = await envelope.globalId;
471
- if (everyone) tx.grantAccess(envelopeGid, "", { writable: true }, _milaboratories_pl_client.GrantType.ANY_AUTHORISED);
472
- else for (const recipient of options.recipients) tx.grantAccess(envelopeGid, recipient, { writable: true });
628
+ await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, { writable: true });
473
629
  await tx.commit();
474
630
  });
475
631
  await this.sharingOutboxTree.refreshState();
476
632
  }
477
633
  /**
634
+ * Grants one freshly built envelope inside the transaction that created it: a single make-public
635
+ * grant for an everyone-share (empty/ignored target, ANY_AUTHORISED — the backend rewrites the
636
+ * target to the everyone-user, gated by role + permission ceiling), or one grant per named
637
+ * recipient.
638
+ *
639
+ * `writable` is not a preference. A project pack needs a writable grant because accepting copies
640
+ * the snapshots out of the envelope, and the cross-color attach rule permits that only to a
641
+ * writable grant holder. A template share copies nothing — the document sits in the envelope's
642
+ * own immutable data — so it is granted read-only, and must be: a writable everyone-grant would
643
+ * hand every user on the server write access to the envelope.
644
+ */
645
+ async grantShareEnvelope(tx, envelope, everyone, recipients, permissions) {
646
+ const gid = await envelope.globalId;
647
+ if (everyone) tx.grantAccess(gid, "", permissions, _milaboratories_pl_client.GrantType.ANY_AUTHORISED);
648
+ else for (const r of recipients) tx.grantAccess(gid, r, permissions);
649
+ }
650
+ /**
651
+ * Shares one stored template. The envelope carries the document itself, so there is no project
652
+ * snapshot and no resource for the recipient to copy out — which is why the grant is read-only.
653
+ * The cost of that is the donor's receipt: nobody can write an acceptance onto a read-only
654
+ * envelope, so a template share never reports who accepted it.
655
+ *
656
+ * A template holding a block installed from a folder on this machine is refused rather than sent,
657
+ * with every offending entry named. {@link checkTemplateShareable} answers the same question
658
+ * without attempting the share, so a UI can state it on the template itself.
659
+ *
660
+ * @param id template to share
661
+ * @param options recipients XOR everyone, plus the title recipients see
662
+ */
663
+ async shareTemplate(id, options) {
664
+ const loaded = await this.loadShareableTemplate(id);
665
+ if (!loaded.ok) return {
666
+ ok: false,
667
+ problems: loaded.problems
668
+ };
669
+ const everyone = "everyone" in options;
670
+ const sender = this.currentUserLogin ?? "";
671
+ const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;
672
+ let shareId;
673
+ await this.pl.withWriteTx("MLShareTemplate", async (tx) => {
674
+ const { envelope, data } = require_sharing.buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, loaded.template, {
675
+ sender,
676
+ title: options.title,
677
+ expiresAt
678
+ });
679
+ shareId = data.shareId;
680
+ await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, { writable: false });
681
+ await tx.commit();
682
+ });
683
+ await this.sharingOutboxTree.refreshState();
684
+ return {
685
+ ok: true,
686
+ shareId
687
+ };
688
+ }
689
+ /**
690
+ * Every entry of a stored template that stands in the way of sharing it, empty for a template
691
+ * that can be shared. Reads the template and nothing else, so a UI can state the refusal on the
692
+ * template itself instead of only when the user tries to share it.
693
+ */
694
+ async checkTemplateShareable(id) {
695
+ return require_template_share.unshareableTemplateEntries((await this.getTemplateData(id)).document);
696
+ }
697
+ /** The document and the label of a template that may be shared, or every entry that stops it.
698
+ * The label is what the recipient's own list will show, so it travels with the document. */
699
+ async loadShareableTemplate(id) {
700
+ const rid = await this.resolveTemplateId(id);
701
+ const template = await this.pl.withReadTx("MLReadTemplateForShare", async (tx) => {
702
+ const rd = await tx.getResourceData(rid, false);
703
+ if (rd.data === void 0) throw new Error(`Template ${id} carries no document.`);
704
+ return {
705
+ document: require_template_list.decodeStoredTemplateData(rd.data).document,
706
+ label: await tx.getKValueJson(rid, require_template_list.TemplateLabelKey)
707
+ };
708
+ });
709
+ const problems = require_template_share.unshareableTemplateEntries(template.document);
710
+ if (problems.length > 0) return {
711
+ ok: false,
712
+ problems
713
+ };
714
+ return {
715
+ ok: true,
716
+ template
717
+ };
718
+ }
719
+ /**
478
720
  * Changes a share in place (same {@link ShareId}), in one write transaction: re-snapshots live
479
721
  * source projects and carries deleted ones' snapshots forward; applies edited recipients/title;
480
722
  * transfers already-decided recipients' accept/reject records (they keep their copy and aren't
@@ -489,14 +731,37 @@ var MiddleLayer = class MiddleLayer {
489
731
  * existing snapshot (and its timestamp), `remove` drops the project from the pack. A project not
490
732
  * in the map defaults to `keep`. Omit the whole map for the legacy auto behavior (live sources
491
733
  * updated, gone ones kept) — the everyone-refresh path relies on that.
734
+ *
735
+ * `opts.templateId` is required for, and only used by, a share that carries a template: a stored
736
+ * template is immutable, so an improved one is a different template and the share cannot re-read
737
+ * the one it started from — the caller names the new target. Every other option means the same
738
+ * thing for both kinds of share. Sharing the named template must be permitted (see
739
+ * {@link checkTemplateShareable}) or this throws.
492
740
  */
493
741
  async changeShare(shareId, opts = {}) {
742
+ const target = opts.templateId === void 0 ? void 0 : await this.loadShareableTemplate(opts.templateId);
743
+ if (target !== void 0 && !target.ok) throw new Error(`changeShare: template ${opts.templateId} cannot be shared: ${describeShareProblems(target.problems)}`);
494
744
  await this.pl.withWriteTx("MLChangeShare", async (tx) => {
495
745
  const old = await this.resolveOutboxEnvelope(tx, shareId);
496
746
  if (old === void 0) throw new Error(`changeShare: no live share with id ${shareId} in the outbox.`);
497
747
  const self = this.currentUserLogin ?? "";
498
748
  const grants = await tx.listGrants(old.rid);
499
749
  const everyone = grants.some((g) => (0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user)) || opts.everyone === true;
750
+ const priorRecipients = grants.filter((g) => !(0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user) && g.user !== self).map((g) => g.user);
751
+ if (old.data.payload.kind === "template") {
752
+ if (target === void 0) throw new Error(`changeShare: share ${shareId} carries a template, so it needs an explicit target template — a stored template never changes, so an improved one is a different template.`);
753
+ const recipients = everyone ? [] : opts.recipients ?? priorRecipients;
754
+ tx.removeField((0, _milaboratories_pl_client.field)(this.sharingOutboxResourceId, old.fieldName));
755
+ const { envelope } = require_sharing.buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, target.template, {
756
+ sender: self,
757
+ title: opts.title === void 0 ? old.data.title : opts.title.trim(),
758
+ expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs,
759
+ shareId
760
+ });
761
+ await this.grantShareEnvelope(tx, envelope, everyone, recipients, { writable: false });
762
+ await tx.commit();
763
+ return;
764
+ }
500
765
  const oldRd = await tx.getResourceData(old.rid, true);
501
766
  const snapshotByUuid = /* @__PURE__ */ new Map();
502
767
  const acceptances = [];
@@ -513,7 +778,6 @@ var MiddleLayer = class MiddleLayer {
513
778
  }
514
779
  }
515
780
  const decidedLogins = acceptances.map((a) => a.login);
516
- const priorRecipients = grants.filter((g) => !(0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user) && g.user !== self).map((g) => g.user);
517
781
  const recipients = everyone ? [] : Array.from(/* @__PURE__ */ new Set([...opts.recipients ?? priorRecipients, ...decidedLogins]));
518
782
  const liveProjects = /* @__PURE__ */ new Map();
519
783
  const projList = await tx.getResourceData(this.projectListResourceId, true);
@@ -523,8 +787,9 @@ var MiddleLayer = class MiddleLayer {
523
787
  }
524
788
  const actions = opts.projectActions;
525
789
  const sources = [];
526
- for (const uuid of Object.keys(old.data.projects)) {
527
- const { label, source, updatedAt } = old.data.projects[uuid];
790
+ const oldProjects = require_sharing_model.envelopeProjectMap(old.data);
791
+ for (const uuid of Object.keys(oldProjects)) {
792
+ const { label, source, updatedAt } = oldProjects[uuid];
528
793
  const liveRid = liveProjects.get(source);
529
794
  const action = actions ? actions[source] ?? "keep" : liveRid !== void 0 ? "update" : "keep";
530
795
  if (action === "remove") continue;
@@ -558,9 +823,7 @@ var MiddleLayer = class MiddleLayer {
558
823
  if (!everyone && !recipients.includes(login)) continue;
559
824
  require_sharing.writeEnvelopeAcceptance(tx, envelope, login, acc.action, acc.timestamp);
560
825
  }
561
- const gid = await envelope.globalId;
562
- if (everyone) tx.grantAccess(gid, "", { writable: true }, _milaboratories_pl_client.GrantType.ANY_AUTHORISED);
563
- else for (const r of recipients) tx.grantAccess(gid, r, { writable: true });
826
+ await this.grantShareEnvelope(tx, envelope, everyone, recipients, { writable: true });
564
827
  await tx.commit();
565
828
  });
566
829
  await this.sharingOutboxTree.refreshState();
@@ -581,7 +844,8 @@ var MiddleLayer = class MiddleLayer {
581
844
  const rd = await tx.getResourceData(f.value, false);
582
845
  if (rd.data === void 0) continue;
583
846
  const data = require_sharing_model.decodeEnvelopeData(rd.data);
584
- if (Object.values(data.projects).some((p) => wanted.has(p.source))) out.push({
847
+ if (data === void 0) continue;
848
+ if (Object.values(require_sharing_model.envelopeProjectMap(data)).some((p) => wanted.has(p.source))) out.push({
585
849
  fieldName: f.name,
586
850
  rid: f.value,
587
851
  shareId: data.shareId
@@ -627,6 +891,7 @@ var MiddleLayer = class MiddleLayer {
627
891
  const rd = await tx.getResourceData(f.value, false);
628
892
  if (rd.data === void 0) continue;
629
893
  const data = require_sharing_model.decodeEnvelopeData(rd.data);
894
+ if (data === void 0) continue;
630
895
  if (data.shareId === shareId) return {
631
896
  fieldName: f.name,
632
897
  rid: f.value,
@@ -652,18 +917,23 @@ var MiddleLayer = class MiddleLayer {
652
917
  return map;
653
918
  }
654
919
  /**
655
- * Accepts one or more pending shares: duplicates each share's projects into this user's
656
- * project list, records the decision per share, and (read-write share) writes the donor-visible
657
- * acceptance onto the envelope. Per-share failures (e.g. an expiry race) are collected, not
658
- * short-circuited the rest still get accepted. Accept-all = pass every current pending shareId.
920
+ * Accepts one or more pending shares. What accepting does depends on what the share carries: a
921
+ * pack of projects is duplicated into this user's project list, while a template is added to this
922
+ * user's own template list and builds nothing the recipient decides later whether to apply it.
923
+ * Either way the decision is recorded per share, and a read-write share also gets the
924
+ * donor-visible acceptance written onto its envelope. Per-share failures (e.g. an expiry race)
925
+ * are collected, not short-circuited — the rest still get accepted. Accept-all = pass every
926
+ * current pending shareId.
659
927
  *
660
928
  * `rename` resolves label collisions (same callback contract as {@link duplicateProject}), but
661
- * the source lives in the envelope tree, so accept calls the low-level mutator directly.
929
+ * the source lives in the envelope tree, so accept calls the low-level mutator directly. It does
930
+ * not apply to a template share, whose label is not required to be unique.
662
931
  */
663
932
  async acceptShare(shareIds, rename) {
664
933
  const live = await this.resolveLiveEnvelopes();
665
934
  const login = this.currentUserLogin;
666
935
  const accepted = [];
936
+ const acceptedTemplates = [];
667
937
  const failed = [];
668
938
  for (const shareId of shareIds) {
669
939
  const envelope = live.get(shareId);
@@ -676,6 +946,28 @@ var MiddleLayer = class MiddleLayer {
676
946
  }
677
947
  try {
678
948
  const now = Date.now();
949
+ const payload = envelope.data.payload;
950
+ if (payload.kind === "template") {
951
+ const rid = await this.pl.withWriteTx("MLAcceptTemplateShare", async (tx) => {
952
+ const tpl = require_template.createTemplate(tx, this.templateListResourceId, payload.label, {
953
+ schemaVersion: 1,
954
+ document: payload.document,
955
+ sender: payload.from
956
+ });
957
+ require_sharing.writeSharingDecision(tx, this.sharingStateResourceId, shareId, {
958
+ decision: "accepted",
959
+ timestamp: now,
960
+ envelopeSharedAt: envelope.data.sharedAt,
961
+ acceptedProjects: []
962
+ });
963
+ await tx.commit();
964
+ return await tpl.globalId;
965
+ });
966
+ const templateId = (0, _milaboratories_pl_client.resourceIdToString)(rid);
967
+ this.templateIdCache.set(templateId, rid);
968
+ acceptedTemplates.push(templateId);
969
+ continue;
970
+ }
679
971
  const createdRids = await this.pl.withWriteTx("MLAcceptShare", async (tx) => {
680
972
  const created = await require_sharing.copyEnvelopeProjectsIntoList(tx, envelope.rid, this.projectListResourceId, rename);
681
973
  require_sharing.writeSharingDecision(tx, this.sharingStateResourceId, shareId, {
@@ -700,9 +992,14 @@ var MiddleLayer = class MiddleLayer {
700
992
  });
701
993
  }
702
994
  }
703
- await Promise.all([this.projectListTree.refreshState(), this.sharingStateTree.refreshState()]);
995
+ await Promise.all([
996
+ this.projectListTree.refreshState(),
997
+ this.templateListTree.refreshState(),
998
+ this.sharingStateTree.refreshState()
999
+ ]);
704
1000
  return {
705
1001
  accepted,
1002
+ acceptedTemplates,
706
1003
  failed
707
1004
  };
708
1005
  }
@@ -746,6 +1043,7 @@ var MiddleLayer = class MiddleLayer {
746
1043
  const rd = await tx.getResourceData(f.value, false);
747
1044
  if (rd.data === void 0) continue;
748
1045
  const envData = require_sharing_model.decodeEnvelopeData(rd.data);
1046
+ if (envData === void 0) continue;
749
1047
  if (envData.expiresAt === null) continue;
750
1048
  if (envData.expiresAt <= now) toDelete.push({ fieldName: f.name });
751
1049
  }
@@ -830,6 +1128,7 @@ var MiddleLayer = class MiddleLayer {
830
1128
  await Promise.all([...this.openedProjects.values()].map((prj) => prj.destroy()));
831
1129
  await Promise.all([
832
1130
  this.projectListTree.terminate(),
1131
+ this.templateListTree.terminate(),
833
1132
  this.sharingOutboxTree.terminate(),
834
1133
  this.sharingStateTree.terminate(),
835
1134
  this.pendingSharesTree.terminate()
@@ -861,7 +1160,7 @@ var MiddleLayer = class MiddleLayer {
861
1160
  ops.defaultTreeOptions.logStat = require_index$1.getDebugFlags().logTreeStats;
862
1161
  ops.debugOps.dumpInitialTreeState = require_index$1.getDebugFlags().dumpInitialTreeState;
863
1162
  if (ops.defaultTreeOptions.traversalMode === void 0 && require_index$1.getDebugFlags().treeTraversalMode !== void 0) ops.defaultTreeOptions.traversalMode = require_index$1.getDebugFlags().treeTraversalMode;
864
- const { projects, sharingOutbox, sharingState } = await pl.withWriteTx("MLInitialization", async (tx) => {
1163
+ const { projects, templates, sharingOutbox, sharingState } = await pl.withWriteTx("MLInitialization", async (tx) => {
865
1164
  const lazyInit = async (fieldName, type) => {
866
1165
  const f = (0, _milaboratories_pl_client.field)(tx.clientRoot, fieldName);
867
1166
  tx.createField(f, "Dynamic");
@@ -875,11 +1174,13 @@ var MiddleLayer = class MiddleLayer {
875
1174
  return { existing: fData.value };
876
1175
  };
877
1176
  const projectsR = await lazyInit(require_project_list.ProjectsField, require_project_list.ProjectsResourceType);
1177
+ const templatesR = await lazyInit(require_template_list.TemplatesField, require_template_list.TemplatesResourceType);
878
1178
  const outboxR = await lazyInit(require_sharing_model.SharingOutboxField, require_sharing_model.SharingOutboxResourceType);
879
1179
  const stateR = await lazyInit(require_sharing_model.SharingStateField, require_sharing_model.SharingStateResourceType);
880
1180
  await tx.commit();
881
1181
  return {
882
1182
  projects: projectsR.existing ?? await projectsR.ref.globalId,
1183
+ templates: templatesR.existing ?? await templatesR.ref.globalId,
883
1184
  sharingState: stateR.existing ?? await stateR.ref.globalId,
884
1185
  sharingOutbox: outboxR.existing ?? await outboxR.ref.globalId
885
1186
  };
@@ -934,14 +1235,20 @@ var MiddleLayer = class MiddleLayer {
934
1235
  };
935
1236
  const openedProjects = new _milaboratories_computable.WatchableValue([]);
936
1237
  const projectListTC = await require_project_list.createProjectList(pl, projects, openedProjects, env);
1238
+ const templateListTC = await require_template_list.createTemplateList(pl, templates, env);
937
1239
  const outgoingTC = await require_sharing_list.createOutgoingShares(pl, sharingOutbox, env);
938
1240
  const sharingStateTree = await require_sharing_list.createSharingStateTree(pl, sharingState, env);
939
1241
  const pendingSharesTree = await require_sharing_list.createPendingSharesTree(pl, env);
940
1242
  const pendingShares = require_sharing_list.createPendingSharesComputable(pendingSharesTree, sharingStateTree, pl.userResources.authUser);
941
1243
  const liveEnvelopes = require_sharing_list.createLiveEnvelopesComputable(pendingSharesTree);
942
- return new MiddleLayer(env, driverKit, driverKit.signer, projects, sharingOutbox, sharingState, openedProjects, projectListTC.tree, outgoingTC.tree, sharingStateTree, pendingSharesTree, v2RegistryProvider, projectListTC.computable, outgoingTC.computable, pendingShares, liveEnvelopes);
1244
+ return new MiddleLayer(env, driverKit, driverKit.signer, projects, templates, sharingOutbox, sharingState, openedProjects, projectListTC.tree, templateListTC.tree, outgoingTC.tree, sharingStateTree, pendingSharesTree, v2RegistryProvider, projectListTC.computable, templateListTC.computable, outgoingTC.computable, pendingShares, liveEnvelopes);
943
1245
  }
944
1246
  };
1247
+ /** Refusal reasons as one line, each naming the entry it belongs to, so a throw that escapes to a
1248
+ * log still says which block stands in the way. */
1249
+ function describeShareProblems(problems) {
1250
+ return problems.map((p) => `${p.entryId}: ${p.error}`).join("; ");
1251
+ }
945
1252
  //#endregion
946
1253
  exports.MiddleLayer = MiddleLayer;
947
1254