@milaboratories/pl-middle-layer 1.64.41 → 1.65.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 (63) hide show
  1. package/dist/index.cjs +17 -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 +482 -16
  10. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  11. package/dist/middle_layer/middle_layer.d.ts +172 -12
  12. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  13. package/dist/middle_layer/middle_layer.js +485 -19
  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/project_list.cjs +19 -0
  24. package/dist/middle_layer/project_list.cjs.map +1 -1
  25. package/dist/middle_layer/project_list.d.ts +1 -1
  26. package/dist/middle_layer/project_list.d.ts.map +1 -1
  27. package/dist/middle_layer/project_list.js +20 -2
  28. package/dist/middle_layer/project_list.js.map +1 -1
  29. package/dist/middle_layer/sharing_list.cjs +203 -0
  30. package/dist/middle_layer/sharing_list.cjs.map +1 -0
  31. package/dist/middle_layer/sharing_list.d.ts +41 -0
  32. package/dist/middle_layer/sharing_list.d.ts.map +1 -0
  33. package/dist/middle_layer/sharing_list.js +198 -0
  34. package/dist/middle_layer/sharing_list.js.map +1 -0
  35. package/dist/model/index.cjs +30 -0
  36. package/dist/model/index.d.ts +2 -1
  37. package/dist/model/index.js +4 -0
  38. package/dist/model/project_model.d.ts +3 -3
  39. package/dist/model/project_model.d.ts.map +1 -1
  40. package/dist/model/sharing_model.cjs +98 -0
  41. package/dist/model/sharing_model.cjs.map +1 -0
  42. package/dist/model/sharing_model.d.ts +122 -0
  43. package/dist/model/sharing_model.d.ts.map +1 -0
  44. package/dist/model/sharing_model.js +84 -0
  45. package/dist/model/sharing_model.js.map +1 -0
  46. package/dist/mutator/block-pack/frontend.cjs +1 -0
  47. package/dist/mutator/block-pack/frontend.cjs.map +1 -1
  48. package/dist/mutator/block-pack/frontend.js +1 -0
  49. package/dist/mutator/block-pack/frontend.js.map +1 -1
  50. package/dist/mutator/sharing.cjs +138 -0
  51. package/dist/mutator/sharing.cjs.map +1 -0
  52. package/dist/mutator/sharing.js +130 -0
  53. package/dist/mutator/sharing.js.map +1 -0
  54. package/package.json +15 -15
  55. package/src/middle_layer/index.ts +1 -0
  56. package/src/middle_layer/middle_layer.ts +715 -23
  57. package/src/middle_layer/ops.ts +7 -0
  58. package/src/middle_layer/project_list.ts +33 -2
  59. package/src/middle_layer/sharing_list.ts +343 -0
  60. package/src/model/index.ts +2 -0
  61. package/src/model/sharing_model.test.ts +22 -0
  62. package/src/model/sharing_model.ts +169 -0
  63. package/src/mutator/sharing.ts +216 -0
@@ -0,0 +1,98 @@
1
+ let _milaboratories_pl_client = require("@milaboratories/pl-client");
2
+ let node_crypto = require("node:crypto");
3
+ //#region src/model/sharing_model.ts
4
+ /** Mints a fresh {@link ShareId}. The single place a share's logical identity is created. */
5
+ function newShareId() {
6
+ return (0, node_crypto.randomUUID)();
7
+ }
8
+ /** Brands a string already known to be a share id (e.g. parsed from a `decision/{shareId}`
9
+ * field name) as a {@link ShareId}, without minting a new one. */
10
+ function asShareId(id) {
11
+ return id;
12
+ }
13
+ /** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */
14
+ const SharingOutboxField = "sharingOutbox";
15
+ /** Field on the acceptor's clientRoot holding the {@link SharingStateResourceType} resource. */
16
+ const SharingStateField = "sharingState";
17
+ const SharingOutboxResourceType = {
18
+ name: "SharingOutbox",
19
+ version: "1"
20
+ };
21
+ const SharedEnvelopeResourceType = {
22
+ name: "SharedEnvelope",
23
+ version: "1"
24
+ };
25
+ const SharingStateResourceType = {
26
+ name: "SharingState",
27
+ version: "1"
28
+ };
29
+ /**
30
+ * Whether a role may make a resource public (grant to everyone): true for controller,
31
+ * admin, and user; false for workflow and unspecified. The middle layer carries no policy
32
+ * of its own here — a crafted call still hits the backend's role + permission-ceiling gate.
33
+ * `null` (no-auth mode) returns false.
34
+ */
35
+ function canGrantToEveryone(role) {
36
+ switch (role) {
37
+ case _milaboratories_pl_client.Role.CONTROLLER:
38
+ case _milaboratories_pl_client.Role.ADMIN:
39
+ case _milaboratories_pl_client.Role.USER: return true;
40
+ default: return false;
41
+ }
42
+ }
43
+ /**
44
+ * Whether a role may impersonate another user: open/create another user's root and list
45
+ * the resources that user can access. Mirrors the backend's authorization rule
46
+ * `util/misecurity/role.go` `CanImpersonate` — true for controller and admin only. This is
47
+ * the admin gate for the "open another user's root" feature and is intentionally stricter
48
+ * than {@link canGrantToEveryone}, which also returns true for a regular user (a normal user
49
+ * may share their own projects, but must never be offered impersonation). `null` (no-auth
50
+ * mode) returns false.
51
+ */
52
+ function canImpersonate(role) {
53
+ switch (role) {
54
+ case _milaboratories_pl_client.Role.CONTROLLER:
55
+ case _milaboratories_pl_client.Role.ADMIN: return true;
56
+ default: return false;
57
+ }
58
+ }
59
+ /** Dynamic field on SharingState, one per handled share, keyed by shareId. */
60
+ const decisionField = (shareId) => `decision/${shareId}`;
61
+ /** Dynamic field on SharedEnvelope, one per recipient who accepted or rejected, keyed
62
+ * by recipient login. Written by the acceptor in read-write shares only (Copy & Share,
63
+ * Live collaboration) — the acceptor's writable envelope grant is what permits the
64
+ * write; read-only shares omit it. The donor reads these from its own outbox to see
65
+ * who responded and when. Informational, not authoritative (a writable grant holder
66
+ * could write under another login — same trust assumption as the sender field).
67
+ * Copied forward when a share is changed. */
68
+ const AcceptanceFieldPrefix = "acceptance/";
69
+ const acceptanceField = (login) => `${AcceptanceFieldPrefix}${login}`;
70
+ const isAcceptanceField = (name) => name.startsWith(AcceptanceFieldPrefix);
71
+ const acceptanceFieldLogin = (name) => name.slice(11);
72
+ /**
73
+ * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`
74
+ * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource
75
+ * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node
76
+ * path uses `node.getDataAsJson<EnvelopeData>()`, which decodes the same JSON.
77
+ */
78
+ function decodeEnvelopeData(data) {
79
+ return JSON.parse(Buffer.from(data).toString("utf-8"));
80
+ }
81
+ //#endregion
82
+ exports.AcceptanceFieldPrefix = AcceptanceFieldPrefix;
83
+ exports.SharedEnvelopeResourceType = SharedEnvelopeResourceType;
84
+ exports.SharingOutboxField = SharingOutboxField;
85
+ exports.SharingOutboxResourceType = SharingOutboxResourceType;
86
+ exports.SharingStateField = SharingStateField;
87
+ exports.SharingStateResourceType = SharingStateResourceType;
88
+ exports.acceptanceField = acceptanceField;
89
+ exports.acceptanceFieldLogin = acceptanceFieldLogin;
90
+ exports.asShareId = asShareId;
91
+ exports.canGrantToEveryone = canGrantToEveryone;
92
+ exports.canImpersonate = canImpersonate;
93
+ exports.decisionField = decisionField;
94
+ exports.decodeEnvelopeData = decodeEnvelopeData;
95
+ exports.isAcceptanceField = isAcceptanceField;
96
+ exports.newShareId = newShareId;
97
+
98
+ //# sourceMappingURL=sharing_model.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing_model.cjs","names":["RoleEnum"],"sources":["../../src/model/sharing_model.ts"],"sourcesContent":["import type { ResourceType, Role } from \"@milaboratories/pl-client\";\nimport { Role as RoleEnum } from \"@milaboratories/pl-client\";\nimport type { Branded, ProjectId } from \"@milaboratories/pl-model-common\";\nimport { randomUUID } from \"node:crypto\";\n\n/**\n * Logical identity of a share, stable across replaces. A donor-generated UUID string,\n * branded so it cannot be silently confused with a project id, a login, or a raw field\n * name. Minted once with {@link newShareId}; every other site receives it (from decoded\n * {@link EnvelopeData} or by parsing a `decision/{shareId}` field name) and threads it\n * through unchanged.\n */\nexport type ShareId = Branded<string, \"ShareId\">;\n\n/** Mints a fresh {@link ShareId}. The single place a share's logical identity is created. */\nexport function newShareId(): ShareId {\n return randomUUID() as ShareId;\n}\n\n/** Brands a string already known to be a share id (e.g. parsed from a `decision/{shareId}`\n * field name) as a {@link ShareId}, without minting a new one. */\nexport function asShareId(id: string): ShareId {\n return id as ShareId;\n}\n\n//\n// Pl Model — Project Sharing\n//\n// All sharing structures are defined and managed by the middle layer; the\n// backend knows nothing about envelopes.\n//\n\n/** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */\nexport const SharingOutboxField = \"sharingOutbox\";\n/** Field on the acceptor's clientRoot holding the {@link SharingStateResourceType} resource. */\nexport const SharingStateField = \"sharingState\";\n\nexport const SharingOutboxResourceType: ResourceType = { name: \"SharingOutbox\", version: \"1\" };\nexport const SharedEnvelopeResourceType: ResourceType = { name: \"SharedEnvelope\", version: \"1\" };\nexport const SharingStateResourceType: ResourceType = { name: \"SharingState\", version: \"1\" };\n\nexport type EnvelopeMode = \"copy\" | \"read-only\" | \"collaboration\";\n\n/** Per-project decision on change, matching the UI labels: re-snapshot the live source (\"update\"),\n * carry the existing snapshot (\"keep\"), or drop the project from the pack (\"remove\"). */\nexport type ProjectChangeAction = \"keep\" | \"update\" | \"remove\";\n\n/** Key of the per-project envelope maps: a uuid minted per snapshot to name the `project/{uuid}`\n * field. Distinct from {@link ProjectId} — re-snapshotting one source yields a new uuid each time. */\nexport type ProjectFieldUuid = Branded<string, \"ProjectFieldUuid\">;\n\n/**\n * Whether a role may make a resource public (grant to everyone): true for controller,\n * admin, and user; false for workflow and unspecified. The middle layer carries no policy\n * of its own here — a crafted call still hits the backend's role + permission-ceiling gate.\n * `null` (no-auth mode) returns false.\n */\nexport function canGrantToEveryone(role: Role | null): boolean {\n switch (role) {\n case RoleEnum.CONTROLLER:\n case RoleEnum.ADMIN:\n case RoleEnum.USER:\n return true;\n default:\n return false;\n }\n}\n\n/**\n * Whether a role may impersonate another user: open/create another user's root and list\n * the resources that user can access. Mirrors the backend's authorization rule\n * `util/misecurity/role.go` `CanImpersonate` — true for controller and admin only. This is\n * the admin gate for the \"open another user's root\" feature and is intentionally stricter\n * than {@link canGrantToEveryone}, which also returns true for a regular user (a normal user\n * may share their own projects, but must never be offered impersonation). `null` (no-auth\n * mode) returns false.\n */\nexport function canImpersonate(role: Role | null): boolean {\n switch (role) {\n case RoleEnum.CONTROLLER:\n case RoleEnum.ADMIN:\n return true;\n default:\n return false;\n }\n}\n\n/** One project's snapshot inside an envelope, keyed by {@link ProjectFieldUuid} in {@link EnvelopeData.projects}. */\nexport interface EnvelopeProject {\n label: string; // carried so the pending-share UI renders without traversing into the project\n source: ProjectId; // donor's source projectId; supersedes a prior share and matches the snapshot to its live source on change\n updatedAt: number; // ms epoch of the last (re)snapshot\n}\n\n/** Immutable `data` on a SharedEnvelope, set at createEphemeral, never mutated. */\nexport interface EnvelopeData {\n schemaVersion: 1;\n shareId: ShareId; // donor-generated UUID; logical share identity, stable across changes\n sharedAt: number; // ms epoch; this instance's creation time — distinguishes instances of one shareId\n expiresAt: number | null; // ms epoch; sharedAt + ttl (default 14 days) for a targeted share; null for share-with-everybody (never expires)\n mode: EnvelopeMode; // what the acceptor's app should do with the contents\n sender: string; // donor login (informational; backend granted_by is authoritative)\n title: string; // display name shown to recipients; defaults to the first project's name\n projects: Record<ProjectFieldUuid, EnvelopeProject>; // contained projects, keyed by project field uuid\n}\n\n/** Dynamic field on SharingState, one per handled share, keyed by shareId. */\nexport const decisionField = (shareId: ShareId) => `decision/${shareId}`;\n\nexport interface SharingDecision {\n decision: \"accepted\" | \"rejected\";\n timestamp: number; // ms epoch — when the acceptor acted\n envelopeSharedAt: number; // the acted-on envelope instance's sharedAt — pins which instance was handled (paired with the shareId key; the resource id is never stored)\n acceptedProjects: string[]; // ids of the projects created in the acceptor's list ([] for a rejected share)\n}\n\n/** Dynamic field on SharedEnvelope, one per recipient who accepted or rejected, keyed\n * by recipient login. Written by the acceptor in read-write shares only (Copy & Share,\n * Live collaboration) — the acceptor's writable envelope grant is what permits the\n * write; read-only shares omit it. The donor reads these from its own outbox to see\n * who responded and when. Informational, not authoritative (a writable grant holder\n * could write under another login — same trust assumption as the sender field).\n * Copied forward when a share is changed. */\nexport const AcceptanceFieldPrefix = \"acceptance/\";\nexport const acceptanceField = (login: string) => `${AcceptanceFieldPrefix}${login}`;\nexport const isAcceptanceField = (name: string) => name.startsWith(AcceptanceFieldPrefix);\nexport const acceptanceFieldLogin = (name: string) => name.slice(AcceptanceFieldPrefix.length);\n\nexport interface EnvelopeAcceptance {\n action: \"accepted\" | \"rejected\";\n timestamp: number; // ms since epoch\n}\n\n/**\n * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`\n * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource\n * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node\n * path uses `node.getDataAsJson<EnvelopeData>()`, which decodes the same JSON.\n */\nexport function decodeEnvelopeData(data: Uint8Array): EnvelopeData {\n return JSON.parse(Buffer.from(data).toString(\"utf-8\")) as EnvelopeData;\n}\n\n/**\n * Options for {@link MiddleLayer.shareProjects}.\n *\n * Recipients XOR everyone — two clean variants, not one struct with mutually exclusive\n * optional fields. The everyone variant issues a single make-public grant (the envelope's\n * `expiresAt` is set to `null`, so it never expires); the recipients variant grants each\n * named recipient and the envelope expires after the default TTL.\n */\nexport type ShareProjectsOptions =\n | {\n recipients: string[]; // recipient logins\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 UI always sends \"copy\"\n }\n | {\n everyone: true; // share with all users on the server\n /**\n * When true and an everyone-share of the same project already exists, refresh it under its\n * stable shareId (recipients who already accepted or rejected are not re-prompted) instead of\n * minting a new share. No-op when no prior everyone-share of the project exists. Callers that\n * don't care pass `false`.\n */\n replace: boolean;\n title: string;\n mode: EnvelopeMode;\n };\n"],"mappings":";;;;AAeA,SAAgB,aAAsB;CACpC,QAAA,GAAA,YAAA,WAAA,CAAkB;AACpB;;;AAIA,SAAgB,UAAU,IAAqB;CAC7C,OAAO;AACT;;AAUA,MAAa,qBAAqB;;AAElC,MAAa,oBAAoB;AAEjC,MAAa,4BAA0C;CAAE,MAAM;CAAiB,SAAS;AAAI;AAC7F,MAAa,6BAA2C;CAAE,MAAM;CAAkB,SAAS;AAAI;AAC/F,MAAa,2BAAyC;CAAE,MAAM;CAAgB,SAAS;AAAI;;;;;;;AAkB3F,SAAgB,mBAAmB,MAA4B;CAC7D,QAAQ,MAAR;EACE,KAAKA,0BAAAA,KAAS;EACd,KAAKA,0BAAAA,KAAS;EACd,KAAKA,0BAAAA,KAAS,MACZ,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;;;;;;;AAWA,SAAgB,eAAe,MAA4B;CACzD,QAAQ,MAAR;EACE,KAAKA,0BAAAA,KAAS;EACd,KAAKA,0BAAAA,KAAS,OACZ,OAAO;EACT,SACE,OAAO;CACX;AACF;;AAsBA,MAAa,iBAAiB,YAAqB,YAAY;;;;;;;;AAgB/D,MAAa,wBAAwB;AACrC,MAAa,mBAAmB,UAAkB,GAAG,wBAAwB;AAC7E,MAAa,qBAAqB,SAAiB,KAAK,WAAW,qBAAqB;AACxF,MAAa,wBAAwB,SAAiB,KAAK,MAAM,EAA4B;;;;;;;AAa7F,SAAgB,mBAAmB,MAAgC;CACjE,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC;AACvD"}
@@ -0,0 +1,122 @@
1
+ import { ResourceType, Role } from "@milaboratories/pl-client";
2
+ import { Branded, ProjectId } from "@milaboratories/pl-model-common";
3
+
4
+ //#region src/model/sharing_model.d.ts
5
+ /**
6
+ * Logical identity of a share, stable across replaces. A donor-generated UUID string,
7
+ * branded so it cannot be silently confused with a project id, a login, or a raw field
8
+ * name. Minted once with {@link newShareId}; every other site receives it (from decoded
9
+ * {@link EnvelopeData} or by parsing a `decision/{shareId}` field name) and threads it
10
+ * through unchanged.
11
+ */
12
+ type ShareId = Branded<string, "ShareId">;
13
+ /** Mints a fresh {@link ShareId}. The single place a share's logical identity is created. */
14
+ declare function newShareId(): ShareId;
15
+ /** Brands a string already known to be a share id (e.g. parsed from a `decision/{shareId}`
16
+ * field name) as a {@link ShareId}, without minting a new one. */
17
+ declare function asShareId(id: string): ShareId;
18
+ /** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */
19
+ declare const SharingOutboxField = "sharingOutbox";
20
+ /** Field on the acceptor's clientRoot holding the {@link SharingStateResourceType} resource. */
21
+ declare const SharingStateField = "sharingState";
22
+ declare const SharingOutboxResourceType: ResourceType;
23
+ declare const SharedEnvelopeResourceType: ResourceType;
24
+ declare const SharingStateResourceType: ResourceType;
25
+ type EnvelopeMode = "copy" | "read-only" | "collaboration";
26
+ /** Per-project decision on change, matching the UI labels: re-snapshot the live source ("update"),
27
+ * carry the existing snapshot ("keep"), or drop the project from the pack ("remove"). */
28
+ type ProjectChangeAction = "keep" | "update" | "remove";
29
+ /** Key of the per-project envelope maps: a uuid minted per snapshot to name the `project/{uuid}`
30
+ * field. Distinct from {@link ProjectId} — re-snapshotting one source yields a new uuid each time. */
31
+ type ProjectFieldUuid = Branded<string, "ProjectFieldUuid">;
32
+ /**
33
+ * Whether a role may make a resource public (grant to everyone): true for controller,
34
+ * admin, and user; false for workflow and unspecified. The middle layer carries no policy
35
+ * of its own here — a crafted call still hits the backend's role + permission-ceiling gate.
36
+ * `null` (no-auth mode) returns false.
37
+ */
38
+ declare function canGrantToEveryone(role: Role | null): boolean;
39
+ /**
40
+ * Whether a role may impersonate another user: open/create another user's root and list
41
+ * the resources that user can access. Mirrors the backend's authorization rule
42
+ * `util/misecurity/role.go` `CanImpersonate` — true for controller and admin only. This is
43
+ * the admin gate for the "open another user's root" feature and is intentionally stricter
44
+ * than {@link canGrantToEveryone}, which also returns true for a regular user (a normal user
45
+ * may share their own projects, but must never be offered impersonation). `null` (no-auth
46
+ * mode) returns false.
47
+ */
48
+ declare function canImpersonate(role: Role | null): boolean;
49
+ /** One project's snapshot inside an envelope, keyed by {@link ProjectFieldUuid} in {@link EnvelopeData.projects}. */
50
+ interface EnvelopeProject {
51
+ label: string;
52
+ source: ProjectId;
53
+ updatedAt: number;
54
+ }
55
+ /** Immutable `data` on a SharedEnvelope, set at createEphemeral, never mutated. */
56
+ interface EnvelopeData {
57
+ schemaVersion: 1;
58
+ shareId: ShareId;
59
+ sharedAt: number;
60
+ expiresAt: number | null;
61
+ mode: EnvelopeMode;
62
+ sender: string;
63
+ title: string;
64
+ projects: Record<ProjectFieldUuid, EnvelopeProject>;
65
+ }
66
+ /** Dynamic field on SharingState, one per handled share, keyed by shareId. */
67
+ declare const decisionField: (shareId: ShareId) => string;
68
+ interface SharingDecision {
69
+ decision: "accepted" | "rejected";
70
+ timestamp: number;
71
+ envelopeSharedAt: number;
72
+ acceptedProjects: string[];
73
+ }
74
+ /** Dynamic field on SharedEnvelope, one per recipient who accepted or rejected, keyed
75
+ * by recipient login. Written by the acceptor in read-write shares only (Copy & Share,
76
+ * Live collaboration) — the acceptor's writable envelope grant is what permits the
77
+ * write; read-only shares omit it. The donor reads these from its own outbox to see
78
+ * who responded and when. Informational, not authoritative (a writable grant holder
79
+ * could write under another login — same trust assumption as the sender field).
80
+ * Copied forward when a share is changed. */
81
+ declare const AcceptanceFieldPrefix = "acceptance/";
82
+ declare const acceptanceField: (login: string) => string;
83
+ declare const isAcceptanceField: (name: string) => boolean;
84
+ declare const acceptanceFieldLogin: (name: string) => string;
85
+ interface EnvelopeAcceptance {
86
+ action: "accepted" | "rejected";
87
+ timestamp: number;
88
+ }
89
+ /**
90
+ * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`
91
+ * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource
92
+ * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node
93
+ * path uses `node.getDataAsJson<EnvelopeData>()`, which decodes the same JSON.
94
+ */
95
+ declare function decodeEnvelopeData(data: Uint8Array): EnvelopeData;
96
+ /**
97
+ * Options for {@link MiddleLayer.shareProjects}.
98
+ *
99
+ * Recipients XOR everyone — two clean variants, not one struct with mutually exclusive
100
+ * optional fields. The everyone variant issues a single make-public grant (the envelope's
101
+ * `expiresAt` is set to `null`, so it never expires); the recipients variant grants each
102
+ * named recipient and the envelope expires after the default TTL.
103
+ */
104
+ type ShareProjectsOptions = {
105
+ recipients: string[];
106
+ title: string;
107
+ mode: EnvelopeMode;
108
+ } | {
109
+ everyone: true;
110
+ /**
111
+ * When true and an everyone-share of the same project already exists, refresh it under its
112
+ * stable shareId (recipients who already accepted or rejected are not re-prompted) instead of
113
+ * minting a new share. No-op when no prior everyone-share of the project exists. Callers that
114
+ * don't care pass `false`.
115
+ */
116
+ replace: boolean;
117
+ title: string;
118
+ mode: EnvelopeMode;
119
+ };
120
+ //#endregion
121
+ export { AcceptanceFieldPrefix, EnvelopeAcceptance, EnvelopeData, EnvelopeMode, EnvelopeProject, ProjectChangeAction, ProjectFieldUuid, ShareId, ShareProjectsOptions, SharedEnvelopeResourceType, SharingDecision, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, canImpersonate, decisionField, decodeEnvelopeData, isAcceptanceField, newShareId };
122
+ //# sourceMappingURL=sharing_model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing_model.d.ts","names":[],"sources":["../../src/model/sharing_model.ts"],"mappings":";;;;;;AAYA;;;;AAA6B;KAAjB,OAAA,GAAU,OAAO;;iBAGb,UAAA,IAAc,OAAO;;AAAA;iBAMrB,SAAA,CAAU,EAAA,WAAa,OAAO;;cAYjC,kBAAA;;cAEA,iBAAA;AAAA,cAEA,yBAAA,EAA2B,YAAsD;AAAA,cACjF,0BAAA,EAA4B,YAAuD;AAAA,cACnF,wBAAA,EAA0B,YAAqD;AAAA,KAEhF,YAAA;AARmB;AAE/B;AAF+B,KAYnB,mBAAA;;;KAIA,gBAAA,GAAmB,OAAO;AAZtC;;;;AAA8F;AAC9F;AADA,iBAoBgB,kBAAA,CAAmB,IAAiB,EAAX,IAAI;;;AAnBmD;AAChG;;;;AAA4F;AAE5F;iBAoCgB,cAAA,CAAe,IAAiB,EAAX,IAAI;;UAWxB,eAAA;EACf,KAAA;EACA,MAAA,EAAQ,SAAS;EACjB,SAAA;AAAA;;UAIe,YAAA;EACf,aAAA;EACA,OAAA,EAAS,OAAA;EACT,QAAA;EACA,SAAA;EACA,IAAA,EAAM,YAAA;EACN,MAAA;EACA,KAAA;EACA,QAAA,EAAU,MAAA,CAAO,gBAAA,EAAkB,eAAA;AAAA;AA9Ce;AAAA,cAkDvC,aAAA,GAAiB,OAAgB,EAAP,OAAO;AAAA,UAE7B,eAAA;EACf,QAAA;EACA,SAAA;EACA,gBAAA;EACA,gBAAA;AAAA;;;;;;;;cAUW,qBAAA;AAAA,cACA,eAAA,GAAmB,KAAa;AAAA,cAChC,iBAAA,GAAqB,IAAY;AAAA,cACjC,oBAAA,GAAwB,IAAY;AAAA,UAEhC,kBAAA;EACf,MAAA;EACA,SAAS;AAAA;;;;;;;iBASK,kBAAA,CAAmB,IAAA,EAAM,UAAA,GAAa,YAAY;;;;;;;;;KAYtD,oBAAA;EAEN,UAAA;EACA,KAAA;EACA,IAAA,EAAM,YAAA;AAAA;EAGN,QAAA;EAnDiC;AAAO;AAE9C;;;;EAwDM,OAAA;EACA,KAAA;EACA,IAAA,EAAM,YAAY;AAAA"}
@@ -0,0 +1,84 @@
1
+ import { Role } from "@milaboratories/pl-client";
2
+ import { randomUUID } from "node:crypto";
3
+ //#region src/model/sharing_model.ts
4
+ /** Mints a fresh {@link ShareId}. The single place a share's logical identity is created. */
5
+ function newShareId() {
6
+ return randomUUID();
7
+ }
8
+ /** Brands a string already known to be a share id (e.g. parsed from a `decision/{shareId}`
9
+ * field name) as a {@link ShareId}, without minting a new one. */
10
+ function asShareId(id) {
11
+ return id;
12
+ }
13
+ /** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */
14
+ const SharingOutboxField = "sharingOutbox";
15
+ /** Field on the acceptor's clientRoot holding the {@link SharingStateResourceType} resource. */
16
+ const SharingStateField = "sharingState";
17
+ const SharingOutboxResourceType = {
18
+ name: "SharingOutbox",
19
+ version: "1"
20
+ };
21
+ const SharedEnvelopeResourceType = {
22
+ name: "SharedEnvelope",
23
+ version: "1"
24
+ };
25
+ const SharingStateResourceType = {
26
+ name: "SharingState",
27
+ version: "1"
28
+ };
29
+ /**
30
+ * Whether a role may make a resource public (grant to everyone): true for controller,
31
+ * admin, and user; false for workflow and unspecified. The middle layer carries no policy
32
+ * of its own here — a crafted call still hits the backend's role + permission-ceiling gate.
33
+ * `null` (no-auth mode) returns false.
34
+ */
35
+ function canGrantToEveryone(role) {
36
+ switch (role) {
37
+ case Role.CONTROLLER:
38
+ case Role.ADMIN:
39
+ case Role.USER: return true;
40
+ default: return false;
41
+ }
42
+ }
43
+ /**
44
+ * Whether a role may impersonate another user: open/create another user's root and list
45
+ * the resources that user can access. Mirrors the backend's authorization rule
46
+ * `util/misecurity/role.go` `CanImpersonate` — true for controller and admin only. This is
47
+ * the admin gate for the "open another user's root" feature and is intentionally stricter
48
+ * than {@link canGrantToEveryone}, which also returns true for a regular user (a normal user
49
+ * may share their own projects, but must never be offered impersonation). `null` (no-auth
50
+ * mode) returns false.
51
+ */
52
+ function canImpersonate(role) {
53
+ switch (role) {
54
+ case Role.CONTROLLER:
55
+ case Role.ADMIN: return true;
56
+ default: return false;
57
+ }
58
+ }
59
+ /** Dynamic field on SharingState, one per handled share, keyed by shareId. */
60
+ const decisionField = (shareId) => `decision/${shareId}`;
61
+ /** Dynamic field on SharedEnvelope, one per recipient who accepted or rejected, keyed
62
+ * by recipient login. Written by the acceptor in read-write shares only (Copy & Share,
63
+ * Live collaboration) — the acceptor's writable envelope grant is what permits the
64
+ * write; read-only shares omit it. The donor reads these from its own outbox to see
65
+ * who responded and when. Informational, not authoritative (a writable grant holder
66
+ * could write under another login — same trust assumption as the sender field).
67
+ * Copied forward when a share is changed. */
68
+ const AcceptanceFieldPrefix = "acceptance/";
69
+ const acceptanceField = (login) => `${AcceptanceFieldPrefix}${login}`;
70
+ const isAcceptanceField = (name) => name.startsWith(AcceptanceFieldPrefix);
71
+ const acceptanceFieldLogin = (name) => name.slice(11);
72
+ /**
73
+ * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`
74
+ * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource
75
+ * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node
76
+ * path uses `node.getDataAsJson<EnvelopeData>()`, which decodes the same JSON.
77
+ */
78
+ function decodeEnvelopeData(data) {
79
+ return JSON.parse(Buffer.from(data).toString("utf-8"));
80
+ }
81
+ //#endregion
82
+ export { AcceptanceFieldPrefix, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, canImpersonate, decisionField, decodeEnvelopeData, isAcceptanceField, newShareId };
83
+
84
+ //# sourceMappingURL=sharing_model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing_model.js","names":["RoleEnum"],"sources":["../../src/model/sharing_model.ts"],"sourcesContent":["import type { ResourceType, Role } from \"@milaboratories/pl-client\";\nimport { Role as RoleEnum } from \"@milaboratories/pl-client\";\nimport type { Branded, ProjectId } from \"@milaboratories/pl-model-common\";\nimport { randomUUID } from \"node:crypto\";\n\n/**\n * Logical identity of a share, stable across replaces. A donor-generated UUID string,\n * branded so it cannot be silently confused with a project id, a login, or a raw field\n * name. Minted once with {@link newShareId}; every other site receives it (from decoded\n * {@link EnvelopeData} or by parsing a `decision/{shareId}` field name) and threads it\n * through unchanged.\n */\nexport type ShareId = Branded<string, \"ShareId\">;\n\n/** Mints a fresh {@link ShareId}. The single place a share's logical identity is created. */\nexport function newShareId(): ShareId {\n return randomUUID() as ShareId;\n}\n\n/** Brands a string already known to be a share id (e.g. parsed from a `decision/{shareId}`\n * field name) as a {@link ShareId}, without minting a new one. */\nexport function asShareId(id: string): ShareId {\n return id as ShareId;\n}\n\n//\n// Pl Model — Project Sharing\n//\n// All sharing structures are defined and managed by the middle layer; the\n// backend knows nothing about envelopes.\n//\n\n/** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */\nexport const SharingOutboxField = \"sharingOutbox\";\n/** Field on the acceptor's clientRoot holding the {@link SharingStateResourceType} resource. */\nexport const SharingStateField = \"sharingState\";\n\nexport const SharingOutboxResourceType: ResourceType = { name: \"SharingOutbox\", version: \"1\" };\nexport const SharedEnvelopeResourceType: ResourceType = { name: \"SharedEnvelope\", version: \"1\" };\nexport const SharingStateResourceType: ResourceType = { name: \"SharingState\", version: \"1\" };\n\nexport type EnvelopeMode = \"copy\" | \"read-only\" | \"collaboration\";\n\n/** Per-project decision on change, matching the UI labels: re-snapshot the live source (\"update\"),\n * carry the existing snapshot (\"keep\"), or drop the project from the pack (\"remove\"). */\nexport type ProjectChangeAction = \"keep\" | \"update\" | \"remove\";\n\n/** Key of the per-project envelope maps: a uuid minted per snapshot to name the `project/{uuid}`\n * field. Distinct from {@link ProjectId} — re-snapshotting one source yields a new uuid each time. */\nexport type ProjectFieldUuid = Branded<string, \"ProjectFieldUuid\">;\n\n/**\n * Whether a role may make a resource public (grant to everyone): true for controller,\n * admin, and user; false for workflow and unspecified. The middle layer carries no policy\n * of its own here — a crafted call still hits the backend's role + permission-ceiling gate.\n * `null` (no-auth mode) returns false.\n */\nexport function canGrantToEveryone(role: Role | null): boolean {\n switch (role) {\n case RoleEnum.CONTROLLER:\n case RoleEnum.ADMIN:\n case RoleEnum.USER:\n return true;\n default:\n return false;\n }\n}\n\n/**\n * Whether a role may impersonate another user: open/create another user's root and list\n * the resources that user can access. Mirrors the backend's authorization rule\n * `util/misecurity/role.go` `CanImpersonate` — true for controller and admin only. This is\n * the admin gate for the \"open another user's root\" feature and is intentionally stricter\n * than {@link canGrantToEveryone}, which also returns true for a regular user (a normal user\n * may share their own projects, but must never be offered impersonation). `null` (no-auth\n * mode) returns false.\n */\nexport function canImpersonate(role: Role | null): boolean {\n switch (role) {\n case RoleEnum.CONTROLLER:\n case RoleEnum.ADMIN:\n return true;\n default:\n return false;\n }\n}\n\n/** One project's snapshot inside an envelope, keyed by {@link ProjectFieldUuid} in {@link EnvelopeData.projects}. */\nexport interface EnvelopeProject {\n label: string; // carried so the pending-share UI renders without traversing into the project\n source: ProjectId; // donor's source projectId; supersedes a prior share and matches the snapshot to its live source on change\n updatedAt: number; // ms epoch of the last (re)snapshot\n}\n\n/** Immutable `data` on a SharedEnvelope, set at createEphemeral, never mutated. */\nexport interface EnvelopeData {\n schemaVersion: 1;\n shareId: ShareId; // donor-generated UUID; logical share identity, stable across changes\n sharedAt: number; // ms epoch; this instance's creation time — distinguishes instances of one shareId\n expiresAt: number | null; // ms epoch; sharedAt + ttl (default 14 days) for a targeted share; null for share-with-everybody (never expires)\n mode: EnvelopeMode; // what the acceptor's app should do with the contents\n sender: string; // donor login (informational; backend granted_by is authoritative)\n title: string; // display name shown to recipients; defaults to the first project's name\n projects: Record<ProjectFieldUuid, EnvelopeProject>; // contained projects, keyed by project field uuid\n}\n\n/** Dynamic field on SharingState, one per handled share, keyed by shareId. */\nexport const decisionField = (shareId: ShareId) => `decision/${shareId}`;\n\nexport interface SharingDecision {\n decision: \"accepted\" | \"rejected\";\n timestamp: number; // ms epoch — when the acceptor acted\n envelopeSharedAt: number; // the acted-on envelope instance's sharedAt — pins which instance was handled (paired with the shareId key; the resource id is never stored)\n acceptedProjects: string[]; // ids of the projects created in the acceptor's list ([] for a rejected share)\n}\n\n/** Dynamic field on SharedEnvelope, one per recipient who accepted or rejected, keyed\n * by recipient login. Written by the acceptor in read-write shares only (Copy & Share,\n * Live collaboration) — the acceptor's writable envelope grant is what permits the\n * write; read-only shares omit it. The donor reads these from its own outbox to see\n * who responded and when. Informational, not authoritative (a writable grant holder\n * could write under another login — same trust assumption as the sender field).\n * Copied forward when a share is changed. */\nexport const AcceptanceFieldPrefix = \"acceptance/\";\nexport const acceptanceField = (login: string) => `${AcceptanceFieldPrefix}${login}`;\nexport const isAcceptanceField = (name: string) => name.startsWith(AcceptanceFieldPrefix);\nexport const acceptanceFieldLogin = (name: string) => name.slice(AcceptanceFieldPrefix.length);\n\nexport interface EnvelopeAcceptance {\n action: \"accepted\" | \"rejected\";\n timestamp: number; // ms since epoch\n}\n\n/**\n * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`\n * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource\n * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node\n * path uses `node.getDataAsJson<EnvelopeData>()`, which decodes the same JSON.\n */\nexport function decodeEnvelopeData(data: Uint8Array): EnvelopeData {\n return JSON.parse(Buffer.from(data).toString(\"utf-8\")) as EnvelopeData;\n}\n\n/**\n * Options for {@link MiddleLayer.shareProjects}.\n *\n * Recipients XOR everyone — two clean variants, not one struct with mutually exclusive\n * optional fields. The everyone variant issues a single make-public grant (the envelope's\n * `expiresAt` is set to `null`, so it never expires); the recipients variant grants each\n * named recipient and the envelope expires after the default TTL.\n */\nexport type ShareProjectsOptions =\n | {\n recipients: string[]; // recipient logins\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 UI always sends \"copy\"\n }\n | {\n everyone: true; // share with all users on the server\n /**\n * When true and an everyone-share of the same project already exists, refresh it under its\n * stable shareId (recipients who already accepted or rejected are not re-prompted) instead of\n * minting a new share. No-op when no prior everyone-share of the project exists. Callers that\n * don't care pass `false`.\n */\n replace: boolean;\n title: string;\n mode: EnvelopeMode;\n };\n"],"mappings":";;;;AAeA,SAAgB,aAAsB;CACpC,OAAO,WAAW;AACpB;;;AAIA,SAAgB,UAAU,IAAqB;CAC7C,OAAO;AACT;;AAUA,MAAa,qBAAqB;;AAElC,MAAa,oBAAoB;AAEjC,MAAa,4BAA0C;CAAE,MAAM;CAAiB,SAAS;AAAI;AAC7F,MAAa,6BAA2C;CAAE,MAAM;CAAkB,SAAS;AAAI;AAC/F,MAAa,2BAAyC;CAAE,MAAM;CAAgB,SAAS;AAAI;;;;;;;AAkB3F,SAAgB,mBAAmB,MAA4B;CAC7D,QAAQ,MAAR;EACE,KAAKA,KAAS;EACd,KAAKA,KAAS;EACd,KAAKA,KAAS,MACZ,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;;;;;;;AAWA,SAAgB,eAAe,MAA4B;CACzD,QAAQ,MAAR;EACE,KAAKA,KAAS;EACd,KAAKA,KAAS,OACZ,OAAO;EACT,SACE,OAAO;CACX;AACF;;AAsBA,MAAa,iBAAiB,YAAqB,YAAY;;;;;;;;AAgB/D,MAAa,wBAAwB;AACrC,MAAa,mBAAmB,UAAkB,GAAG,wBAAwB;AAC7E,MAAa,qBAAqB,SAAiB,KAAK,WAAW,qBAAqB;AACxF,MAAa,wBAAwB,SAAiB,KAAK,MAAM,EAA4B;;;;;;;AAa7F,SAAgB,mBAAmB,MAAgC;CACjE,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC;AACvD"}
@@ -1,4 +1,5 @@
1
1
  const require_block_pack_spec = require("../../model/block_pack_spec.cjs");
2
+ require("../../model/index.cjs");
2
3
  let _milaboratories_ts_helpers = require("@milaboratories/ts-helpers");
3
4
  //#region src/mutator/block-pack/frontend.ts
4
5
  function createFrontend(tx, spec) {
@@ -1 +1 @@
1
- {"version":3,"file":"frontend.cjs","names":["FrontendFromUrlResourceType","FrontendFromFolderResourceType","FrontendFromLocalTgzResourceType"],"sources":["../../../src/mutator/block-pack/frontend.ts"],"sourcesContent":["import type { AnyResourceRef, PlTransaction } from \"@milaboratories/pl-client\";\nimport type {\n FrontendFromFolderData,\n FrontendFromLocalTgzData,\n FrontendFromUrlData,\n FrontendSpec,\n} from \"../../model\";\nimport {\n FrontendFromFolderResourceType,\n FrontendFromLocalTgzResourceType,\n FrontendFromUrlResourceType,\n} from \"../../model\";\nimport { assertNever } from \"@milaboratories/ts-helpers\";\n\nexport function createFrontend(tx: PlTransaction, spec: FrontendSpec): AnyResourceRef {\n switch (spec.type) {\n case \"url\":\n return tx.createValue(\n FrontendFromUrlResourceType,\n JSON.stringify({ url: spec.url } as FrontendFromUrlData),\n );\n case \"local\":\n return tx.createValue(\n FrontendFromFolderResourceType,\n JSON.stringify({\n path: spec.path,\n signature: spec.signature,\n } as FrontendFromFolderData),\n );\n case \"local-tgz\":\n return tx.createValue(\n FrontendFromLocalTgzResourceType,\n JSON.stringify({\n path: spec.path,\n mtime: spec.mtime,\n signature: spec.signature,\n } as FrontendFromLocalTgzData),\n );\n default:\n return assertNever(spec);\n }\n}\n"],"mappings":";;;AAcA,SAAgB,eAAe,IAAmB,MAAoC;CACpF,QAAQ,KAAK,MAAb;EACE,KAAK,OACH,OAAO,GAAG,YACRA,wBAAAA,6BACA,KAAK,UAAU,EAAE,KAAK,KAAK,IAAI,CAAwB,CACzD;EACF,KAAK,SACH,OAAO,GAAG,YACRC,wBAAAA,gCACA,KAAK,UAAU;GACb,MAAM,KAAK;GACX,WAAW,KAAK;EAClB,CAA2B,CAC7B;EACF,KAAK,aACH,OAAO,GAAG,YACRC,wBAAAA,kCACA,KAAK,UAAU;GACb,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,WAAW,KAAK;EAClB,CAA6B,CAC/B;EACF,SACE,QAAA,GAAA,2BAAA,YAAA,CAAmB,IAAI;CAC3B;AACF"}
1
+ {"version":3,"file":"frontend.cjs","names":["FrontendFromUrlResourceType","FrontendFromFolderResourceType","FrontendFromLocalTgzResourceType"],"sources":["../../../src/mutator/block-pack/frontend.ts"],"sourcesContent":["import type { AnyResourceRef, PlTransaction } from \"@milaboratories/pl-client\";\nimport type {\n FrontendFromFolderData,\n FrontendFromLocalTgzData,\n FrontendFromUrlData,\n FrontendSpec,\n} from \"../../model\";\nimport {\n FrontendFromFolderResourceType,\n FrontendFromLocalTgzResourceType,\n FrontendFromUrlResourceType,\n} from \"../../model\";\nimport { assertNever } from \"@milaboratories/ts-helpers\";\n\nexport function createFrontend(tx: PlTransaction, spec: FrontendSpec): AnyResourceRef {\n switch (spec.type) {\n case \"url\":\n return tx.createValue(\n FrontendFromUrlResourceType,\n JSON.stringify({ url: spec.url } as FrontendFromUrlData),\n );\n case \"local\":\n return tx.createValue(\n FrontendFromFolderResourceType,\n JSON.stringify({\n path: spec.path,\n signature: spec.signature,\n } as FrontendFromFolderData),\n );\n case \"local-tgz\":\n return tx.createValue(\n FrontendFromLocalTgzResourceType,\n JSON.stringify({\n path: spec.path,\n mtime: spec.mtime,\n signature: spec.signature,\n } as FrontendFromLocalTgzData),\n );\n default:\n return assertNever(spec);\n }\n}\n"],"mappings":";;;;AAcA,SAAgB,eAAe,IAAmB,MAAoC;CACpF,QAAQ,KAAK,MAAb;EACE,KAAK,OACH,OAAO,GAAG,YACRA,wBAAAA,6BACA,KAAK,UAAU,EAAE,KAAK,KAAK,IAAI,CAAwB,CACzD;EACF,KAAK,SACH,OAAO,GAAG,YACRC,wBAAAA,gCACA,KAAK,UAAU;GACb,MAAM,KAAK;GACX,WAAW,KAAK;EAClB,CAA2B,CAC7B;EACF,KAAK,aACH,OAAO,GAAG,YACRC,wBAAAA,kCACA,KAAK,UAAU;GACb,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,WAAW,KAAK;EAClB,CAA6B,CAC/B;EACF,SACE,QAAA,GAAA,2BAAA,YAAA,CAAmB,IAAI;CAC3B;AACF"}
@@ -1,4 +1,5 @@
1
1
  import { FrontendFromFolderResourceType, FrontendFromLocalTgzResourceType, FrontendFromUrlResourceType } from "../../model/block_pack_spec.js";
2
+ import "../../model/index.js";
2
3
  import { assertNever } from "@milaboratories/ts-helpers";
3
4
  //#region src/mutator/block-pack/frontend.ts
4
5
  function createFrontend(tx, spec) {
@@ -1 +1 @@
1
- {"version":3,"file":"frontend.js","names":[],"sources":["../../../src/mutator/block-pack/frontend.ts"],"sourcesContent":["import type { AnyResourceRef, PlTransaction } from \"@milaboratories/pl-client\";\nimport type {\n FrontendFromFolderData,\n FrontendFromLocalTgzData,\n FrontendFromUrlData,\n FrontendSpec,\n} from \"../../model\";\nimport {\n FrontendFromFolderResourceType,\n FrontendFromLocalTgzResourceType,\n FrontendFromUrlResourceType,\n} from \"../../model\";\nimport { assertNever } from \"@milaboratories/ts-helpers\";\n\nexport function createFrontend(tx: PlTransaction, spec: FrontendSpec): AnyResourceRef {\n switch (spec.type) {\n case \"url\":\n return tx.createValue(\n FrontendFromUrlResourceType,\n JSON.stringify({ url: spec.url } as FrontendFromUrlData),\n );\n case \"local\":\n return tx.createValue(\n FrontendFromFolderResourceType,\n JSON.stringify({\n path: spec.path,\n signature: spec.signature,\n } as FrontendFromFolderData),\n );\n case \"local-tgz\":\n return tx.createValue(\n FrontendFromLocalTgzResourceType,\n JSON.stringify({\n path: spec.path,\n mtime: spec.mtime,\n signature: spec.signature,\n } as FrontendFromLocalTgzData),\n );\n default:\n return assertNever(spec);\n }\n}\n"],"mappings":";;;AAcA,SAAgB,eAAe,IAAmB,MAAoC;CACpF,QAAQ,KAAK,MAAb;EACE,KAAK,OACH,OAAO,GAAG,YACR,6BACA,KAAK,UAAU,EAAE,KAAK,KAAK,IAAI,CAAwB,CACzD;EACF,KAAK,SACH,OAAO,GAAG,YACR,gCACA,KAAK,UAAU;GACb,MAAM,KAAK;GACX,WAAW,KAAK;EAClB,CAA2B,CAC7B;EACF,KAAK,aACH,OAAO,GAAG,YACR,kCACA,KAAK,UAAU;GACb,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,WAAW,KAAK;EAClB,CAA6B,CAC/B;EACF,SACE,OAAO,YAAY,IAAI;CAC3B;AACF"}
1
+ {"version":3,"file":"frontend.js","names":[],"sources":["../../../src/mutator/block-pack/frontend.ts"],"sourcesContent":["import type { AnyResourceRef, PlTransaction } from \"@milaboratories/pl-client\";\nimport type {\n FrontendFromFolderData,\n FrontendFromLocalTgzData,\n FrontendFromUrlData,\n FrontendSpec,\n} from \"../../model\";\nimport {\n FrontendFromFolderResourceType,\n FrontendFromLocalTgzResourceType,\n FrontendFromUrlResourceType,\n} from \"../../model\";\nimport { assertNever } from \"@milaboratories/ts-helpers\";\n\nexport function createFrontend(tx: PlTransaction, spec: FrontendSpec): AnyResourceRef {\n switch (spec.type) {\n case \"url\":\n return tx.createValue(\n FrontendFromUrlResourceType,\n JSON.stringify({ url: spec.url } as FrontendFromUrlData),\n );\n case \"local\":\n return tx.createValue(\n FrontendFromFolderResourceType,\n JSON.stringify({\n path: spec.path,\n signature: spec.signature,\n } as FrontendFromFolderData),\n );\n case \"local-tgz\":\n return tx.createValue(\n FrontendFromLocalTgzResourceType,\n JSON.stringify({\n path: spec.path,\n mtime: spec.mtime,\n signature: spec.signature,\n } as FrontendFromLocalTgzData),\n );\n default:\n return assertNever(spec);\n }\n}\n"],"mappings":";;;;AAcA,SAAgB,eAAe,IAAmB,MAAoC;CACpF,QAAQ,KAAK,MAAb;EACE,KAAK,OACH,OAAO,GAAG,YACR,6BACA,KAAK,UAAU,EAAE,KAAK,KAAK,IAAI,CAAwB,CACzD;EACF,KAAK,SACH,OAAO,GAAG,YACR,gCACA,KAAK,UAAU;GACb,MAAM,KAAK;GACX,WAAW,KAAK;EAClB,CAA2B,CAC7B;EACF,KAAK,aACH,OAAO,GAAG,YACR,kCACA,KAAK,UAAU;GACb,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,WAAW,KAAK;EAClB,CAA6B,CAC/B;EACF,SACE,OAAO,YAAY,IAAI;CAC3B;AACF"}
@@ -0,0 +1,138 @@
1
+ const require_project_model = require("../model/project_model.cjs");
2
+ const require_sharing_model = require("../model/sharing_model.cjs");
3
+ const require_project = require("./project.cjs");
4
+ let _milaboratories_pl_client = require("@milaboratories/pl-client");
5
+ let node_crypto = require("node:crypto");
6
+ //#region src/mutator/sharing.ts
7
+ /** Field name carrying a project snapshot inside a {@link SharedEnvelopeResourceType}. */
8
+ const EnvelopeProjectFieldPrefix = "project/";
9
+ const envelopeProjectField = (uuid) => `${EnvelopeProjectFieldPrefix}${uuid}`;
10
+ /** True for an envelope field that carries a project snapshot. */
11
+ function isEnvelopeProjectField(name) {
12
+ return name.startsWith(EnvelopeProjectFieldPrefix);
13
+ }
14
+ /** Extracts the project field uuid from a `project/{uuid}` field name. */
15
+ function envelopeProjectFieldUuid(name) {
16
+ return name.slice(8);
17
+ }
18
+ /**
19
+ * Builds one {@link SharedEnvelopeResourceType} on the donor side inside the given write
20
+ * transaction: snapshots each source project by reference, seals the envelope with its
21
+ * immutable {@link EnvelopeData}, and attaches the envelope under `{shareId}` on the donor's
22
+ * outbox. The caller is responsible for issuing the per-recipient grants and committing the
23
+ * transaction — keeping create + grant atomic.
24
+ *
25
+ * @returns the new envelope resource and the generated `EnvelopeData`.
26
+ */
27
+ async function buildShareEnvelope(tx, outboxRid, sources, params) {
28
+ const shareId = params.shareId ?? require_sharing_model.newShareId();
29
+ const sharedAt = params.sharedAt ?? Date.now();
30
+ const projects = {};
31
+ const snapshots = [];
32
+ for (const src of sources) {
33
+ const uuid = (0, node_crypto.randomUUID)();
34
+ if (src.kind === "fresh") {
35
+ const meta = await tx.getKValueJson(src.sourceRid, require_project_model.ProjectMetaKey);
36
+ const ref = await require_project.duplicateProject(tx, src.sourceRid, { label: meta.label });
37
+ projects[uuid] = {
38
+ label: meta.label,
39
+ source: src.projectId,
40
+ updatedAt: sharedAt
41
+ };
42
+ snapshots.push({
43
+ uuid,
44
+ ref
45
+ });
46
+ } else {
47
+ projects[uuid] = {
48
+ label: src.label,
49
+ source: src.projectId,
50
+ updatedAt: src.updatedAt
51
+ };
52
+ snapshots.push({
53
+ uuid,
54
+ ref: src.snapshotRid
55
+ });
56
+ }
57
+ }
58
+ const data = {
59
+ schemaVersion: 1,
60
+ shareId,
61
+ sharedAt,
62
+ expiresAt: params.expiresAt,
63
+ mode: params.mode,
64
+ sender: params.sender,
65
+ title: params.title,
66
+ projects
67
+ };
68
+ const envelope = tx.createEphemeral(require_sharing_model.SharedEnvelopeResourceType, JSON.stringify(data));
69
+ for (const { uuid, ref } of snapshots) tx.createField((0, _milaboratories_pl_client.field)(envelope, envelopeProjectField(uuid)), "Input", ref);
70
+ tx.lockInputs(envelope);
71
+ tx.createField((0, _milaboratories_pl_client.field)(outboxRid, shareId), "Dynamic", envelope);
72
+ return {
73
+ envelope,
74
+ data
75
+ };
76
+ }
77
+ /**
78
+ * Records a response onto the envelope as a dynamic `acceptance/{login}` field: the acceptor
79
+ * writing their own decision (their writable grant permits it), or the donor transferring an
80
+ * existing record onto a changed envelope. Accepts the envelope by ref or id.
81
+ */
82
+ function writeEnvelopeAcceptance(tx, envelopeRid, login, action, timestamp) {
83
+ const acceptance = {
84
+ action,
85
+ timestamp
86
+ };
87
+ const value = tx.createJsonValue(acceptance);
88
+ tx.createField((0, _milaboratories_pl_client.field)(envelopeRid, require_sharing_model.acceptanceField(login)), "Dynamic", value);
89
+ }
90
+ /**
91
+ * Records the acceptor's decision for a handled share on the acceptor's SharingState as a
92
+ * dynamic `decision/{shareId}` field. Keyed on the logical shareId so discovery dedups on the
93
+ * share, not on the envelope instance.
94
+ */
95
+ function writeSharingDecision(tx, stateRid, shareId, decision) {
96
+ const value = tx.createJsonValue(decision);
97
+ tx.createField((0, _milaboratories_pl_client.field)(stateRid, require_sharing_model.decisionField(shareId)), "Dynamic", value);
98
+ }
99
+ /**
100
+ * Copies every project snapshot inside an envelope into the acceptor's own project list — a
101
+ * cross-color attach the backend permits. Mirrors {@link duplicateProject}'s `rename` contract,
102
+ * but resolves the source against the envelope tree (not the acceptor's own list), so the
103
+ * wrapper cannot be reused.
104
+ *
105
+ * @returns ids of the projects created in the acceptor's list.
106
+ */
107
+ async function copyEnvelopeProjectsIntoList(tx, envelopeRid, projectListRid, rename) {
108
+ const existingRids = (await tx.getResourceData(projectListRid, true)).fields.map((f) => f.value).filter(_milaboratories_pl_client.isNotNullSignedResourceId);
109
+ const existingLabels = (await Promise.all(existingRids.map((rid) => tx.getKValueJson(rid, require_project_model.ProjectMetaKey)))).map((m) => m.label);
110
+ const sourceRids = (await tx.getResourceData(envelopeRid, true)).fields.filter((f) => isEnvelopeProjectField(f.name)).map((f) => f.value).filter(_milaboratories_pl_client.isNotNullSignedResourceId);
111
+ const created = [];
112
+ for (const sourceRid of sourceRids) {
113
+ const sourceMeta = await tx.getKValueJson(sourceRid, require_project_model.ProjectMetaKey);
114
+ const newLabel = rename ? rename(sourceMeta.label, existingLabels) : sourceMeta.label;
115
+ existingLabels.push(newLabel);
116
+ const newPrj = await require_project.duplicateProject(tx, sourceRid, { label: newLabel });
117
+ tx.createField((0, _milaboratories_pl_client.field)(projectListRid, (0, node_crypto.randomUUID)()), "Dynamic", newPrj);
118
+ const signedRid = await newPrj.globalId;
119
+ created.push(signedRid);
120
+ }
121
+ return created;
122
+ }
123
+ /** String-form ids of the projects created by {@link copyEnvelopeProjectsIntoList}. */
124
+ function resourceIdsToStrings(ids) {
125
+ return ids.map((id) => (0, _milaboratories_pl_client.resourceIdToString)(id));
126
+ }
127
+ //#endregion
128
+ exports.EnvelopeProjectFieldPrefix = EnvelopeProjectFieldPrefix;
129
+ exports.buildShareEnvelope = buildShareEnvelope;
130
+ exports.copyEnvelopeProjectsIntoList = copyEnvelopeProjectsIntoList;
131
+ exports.envelopeProjectField = envelopeProjectField;
132
+ exports.envelopeProjectFieldUuid = envelopeProjectFieldUuid;
133
+ exports.isEnvelopeProjectField = isEnvelopeProjectField;
134
+ exports.resourceIdsToStrings = resourceIdsToStrings;
135
+ exports.writeEnvelopeAcceptance = writeEnvelopeAcceptance;
136
+ exports.writeSharingDecision = writeSharingDecision;
137
+
138
+ //# sourceMappingURL=sharing.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing.cjs","names":["newShareId","ProjectMetaKey","duplicateProject","SharedEnvelopeResourceType","acceptanceField","decisionField","isNotNullSignedResourceId"],"sources":["../../src/mutator/sharing.ts"],"sourcesContent":["import type { PlTransaction, ResourceRef, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { field, isNotNullSignedResourceId, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { ProjectMeta } from \"@milaboratories/pl-model-middle-layer\";\nimport type { ProjectId } from \"@milaboratories/pl-model-common\";\nimport { ProjectMetaKey } from \"../model/project_model\";\nimport { duplicateProject } from \"./project\";\nimport type {\n EnvelopeData,\n EnvelopeAcceptance,\n EnvelopeMode,\n EnvelopeProject,\n ProjectFieldUuid,\n ShareId,\n SharingDecision,\n} from \"../model/sharing_model\";\nimport {\n SharedEnvelopeResourceType,\n acceptanceField,\n decisionField,\n newShareId,\n} from \"../model/sharing_model\";\n\n/** Field name carrying a project snapshot inside a {@link SharedEnvelopeResourceType}. */\nexport const EnvelopeProjectFieldPrefix = \"project/\";\nexport const envelopeProjectField = (uuid: ProjectFieldUuid) =>\n `${EnvelopeProjectFieldPrefix}${uuid}`;\n\n/** True for an envelope field that carries a project snapshot. */\nexport function isEnvelopeProjectField(name: string): boolean {\n return name.startsWith(EnvelopeProjectFieldPrefix);\n}\n\n/** Extracts the project field uuid from a `project/{uuid}` field name. */\nexport function envelopeProjectFieldUuid(name: string): ProjectFieldUuid {\n return name.slice(EnvelopeProjectFieldPrefix.length) as ProjectFieldUuid;\n}\n\n//\n// Donor side\n//\n\n/**\n * One project going into an envelope: `fresh` snapshots a live source (normal path); `carry`\n * re-attaches an existing snapshot (change's \"keep\", or an \"update\" whose source is gone).\n */\nexport type EnvelopeProjectSource =\n | { kind: \"fresh\"; projectId: ProjectId; sourceRid: SignedResourceId }\n | {\n kind: \"carry\";\n projectId: ProjectId;\n label: string;\n snapshotRid: SignedResourceId;\n /** ms epoch the carried snapshot was last taken; preserved so \"keep\" keeps its timestamp. */\n updatedAt: number;\n };\n\n/**\n * Builds one {@link SharedEnvelopeResourceType} on the donor side inside the given write\n * transaction: snapshots each source project by reference, seals the envelope with its\n * immutable {@link EnvelopeData}, and attaches the envelope under `{shareId}` on the donor's\n * outbox. The caller is responsible for issuing the per-recipient grants and committing the\n * transaction — keeping create + grant atomic.\n *\n * @returns the new envelope resource and the generated `EnvelopeData`.\n */\nexport async function buildShareEnvelope(\n tx: PlTransaction,\n outboxRid: SignedResourceId,\n sources: EnvelopeProjectSource[],\n params: {\n mode: EnvelopeMode;\n sender: string;\n title: string;\n /** ms epoch; sharedAt + ttl for a targeted share, null for share-with-everybody. */\n expiresAt: number | null;\n /** Existing shareId for a change; a fresh one is minted when omitted. */\n shareId?: ShareId;\n sharedAt?: number;\n },\n): Promise<{ envelope: ResourceRef; data: EnvelopeData }> {\n const shareId = params.shareId ?? newShareId();\n const sharedAt = params.sharedAt ?? Date.now();\n\n // Snapshot (fresh) or re-attach (carry) each project, collecting its metadata for the pack.\n const projects: Record<ProjectFieldUuid, EnvelopeProject> = {};\n const snapshots: { uuid: ProjectFieldUuid; ref: ResourceRef | SignedResourceId }[] = [];\n for (const src of sources) {\n const uuid = randomUUID() as ProjectFieldUuid;\n if (src.kind === \"fresh\") {\n const meta = await tx.getKValueJson<ProjectMeta>(src.sourceRid, ProjectMetaKey);\n const ref = await duplicateProject(tx, src.sourceRid, { label: meta.label });\n projects[uuid] = { label: meta.label, source: src.projectId, updatedAt: sharedAt }; // (re)snapshotted now\n snapshots.push({ uuid, ref });\n } else {\n // Re-attach the prior snapshot; the new envelope references it before the old one is\n // detached in the same tx, so it stays alive. Label and timestamp carry unchanged.\n projects[uuid] = { label: src.label, source: src.projectId, updatedAt: src.updatedAt };\n snapshots.push({ uuid, ref: src.snapshotRid });\n }\n }\n\n const data: EnvelopeData = {\n schemaVersion: 1,\n shareId,\n sharedAt,\n expiresAt: params.expiresAt,\n mode: params.mode,\n sender: params.sender,\n title: params.title,\n projects,\n };\n\n // Immutable data set once at creation, never altered.\n const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));\n\n // Attach the project snapshots as Input fields, then seal the input set one-way.\n for (const { uuid, ref } of snapshots) {\n tx.createField(field(envelope, envelopeProjectField(uuid)), \"Input\", ref);\n }\n tx.lockInputs(envelope);\n\n // Attach to the outbox under {shareId} in the same transaction so the held-resource rule\n // keeps the ephemeral envelope alive.\n tx.createField(field(outboxRid, shareId), \"Dynamic\", envelope);\n\n return { envelope, data };\n}\n\n/**\n * Records a response onto the envelope as a dynamic `acceptance/{login}` field: the acceptor\n * writing their own decision (their writable grant permits it), or the donor transferring an\n * existing record onto a changed envelope. Accepts the envelope by ref or id.\n */\nexport function writeEnvelopeAcceptance(\n tx: PlTransaction,\n envelopeRid: ResourceRef | SignedResourceId,\n login: string,\n action: EnvelopeAcceptance[\"action\"],\n timestamp: number,\n): void {\n const acceptance: EnvelopeAcceptance = { action, timestamp };\n const value = tx.createJsonValue(acceptance);\n tx.createField(field(envelopeRid, acceptanceField(login)), \"Dynamic\", value);\n}\n\n//\n// Acceptor side\n//\n\n/**\n * Records the acceptor's decision for a handled share on the acceptor's SharingState as a\n * dynamic `decision/{shareId}` field. Keyed on the logical shareId so discovery dedups on the\n * share, not on the envelope instance.\n */\nexport function writeSharingDecision(\n tx: PlTransaction,\n stateRid: SignedResourceId,\n shareId: ShareId,\n decision: SharingDecision,\n): void {\n const value = tx.createJsonValue(decision);\n tx.createField(field(stateRid, decisionField(shareId)), \"Dynamic\", value);\n}\n\n/**\n * Copies every project snapshot inside an envelope into the acceptor's own project list — a\n * cross-color attach the backend permits. Mirrors {@link duplicateProject}'s `rename` contract,\n * but resolves the source against the envelope tree (not the acceptor's own list), so the\n * wrapper cannot be reused.\n *\n * @returns ids of the projects created in the acceptor's list.\n */\nexport async function copyEnvelopeProjectsIntoList(\n tx: PlTransaction,\n envelopeRid: SignedResourceId,\n projectListRid: SignedResourceId,\n rename?: (previousLabel: string, existingLabels: string[]) => string,\n): Promise<SignedResourceId[]> {\n // Read the acceptor's existing project labels once (own color, no relaxation).\n const projectListData = await tx.getResourceData(projectListRid, true);\n const existingRids = projectListData.fields.map((f) => f.value).filter(isNotNullSignedResourceId);\n const existingLabels = (\n await Promise.all(existingRids.map((rid) => tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey)))\n ).map((m) => m.label);\n\n // Enumerate the envelope's project/{uuid} input field values (signed envelope-colored ids).\n const envelopeData = await tx.getResourceData(envelopeRid, true);\n const sourceRids = envelopeData.fields\n .filter((f) => isEnvelopeProjectField(f.name))\n .map((f) => f.value)\n .filter(isNotNullSignedResourceId);\n\n const created: SignedResourceId[] = [];\n for (const sourceRid of sourceRids) {\n const sourceMeta = await tx.getKValueJson<ProjectMeta>(sourceRid, ProjectMetaKey);\n const newLabel = rename ? rename(sourceMeta.label, existingLabels) : sourceMeta.label;\n existingLabels.push(newLabel);\n\n // Cross-color attach: a new UserProject in the acceptor's color whose fields point at\n // envelope-colored resources. Fails with PermissionDenied: color mismatch on a backend\n // that lacks crossTreeRefs:v1.\n const newPrj = await duplicateProject(tx, sourceRid, { label: newLabel });\n tx.createField(field(projectListRid, randomUUID()), \"Dynamic\", newPrj);\n\n const signedRid = await newPrj.globalId;\n created.push(signedRid);\n }\n\n return created;\n}\n\n/** String-form ids of the projects created by {@link copyEnvelopeProjectsIntoList}. */\nexport function resourceIdsToStrings(ids: SignedResourceId[]): string[] {\n return ids.map((id) => resourceIdToString(id));\n}\n"],"mappings":";;;;;;;AAwBA,MAAa,6BAA6B;AAC1C,MAAa,wBAAwB,SACnC,GAAG,6BAA6B;;AAGlC,SAAgB,uBAAuB,MAAuB;CAC5D,OAAO,KAAK,WAAW,0BAA0B;AACnD;;AAGA,SAAgB,yBAAyB,MAAgC;CACvE,OAAO,KAAK,MAAM,CAAiC;AACrD;;;;;;;;;;AA8BA,eAAsB,mBACpB,IACA,WACA,SACA,QAUwD;CACxD,MAAM,UAAU,OAAO,WAAWA,sBAAAA,WAAW;CAC7C,MAAM,WAAW,OAAO,YAAY,KAAK,IAAI;CAG7C,MAAM,WAAsD,CAAC;CAC7D,MAAM,YAA+E,CAAC;CACtF,KAAK,MAAM,OAAO,SAAS;EACzB,MAAM,QAAA,GAAA,YAAA,WAAA,CAAkB;EACxB,IAAI,IAAI,SAAS,SAAS;GACxB,MAAM,OAAO,MAAM,GAAG,cAA2B,IAAI,WAAWC,sBAAAA,cAAc;GAC9E,MAAM,MAAM,MAAMC,gBAAAA,iBAAiB,IAAI,IAAI,WAAW,EAAE,OAAO,KAAK,MAAM,CAAC;GAC3E,SAAS,QAAQ;IAAE,OAAO,KAAK;IAAO,QAAQ,IAAI;IAAW,WAAW;GAAS;GACjF,UAAU,KAAK;IAAE;IAAM;GAAI,CAAC;EAC9B,OAAO;GAGL,SAAS,QAAQ;IAAE,OAAO,IAAI;IAAO,QAAQ,IAAI;IAAW,WAAW,IAAI;GAAU;GACrF,UAAU,KAAK;IAAE;IAAM,KAAK,IAAI;GAAY,CAAC;EAC/C;CACF;CAEA,MAAM,OAAqB;EACzB,eAAe;EACf;EACA;EACA,WAAW,OAAO;EAClB,MAAM,OAAO;EACb,QAAQ,OAAO;EACf,OAAO,OAAO;EACd;CACF;CAGA,MAAM,WAAW,GAAG,gBAAgBC,sBAAAA,4BAA4B,KAAK,UAAU,IAAI,CAAC;CAGpF,KAAK,MAAM,EAAE,MAAM,SAAS,WAC1B,GAAG,aAAA,GAAA,0BAAA,MAAA,CAAkB,UAAU,qBAAqB,IAAI,CAAC,GAAG,SAAS,GAAG;CAE1E,GAAG,WAAW,QAAQ;CAItB,GAAG,aAAA,GAAA,0BAAA,MAAA,CAAkB,WAAW,OAAO,GAAG,WAAW,QAAQ;CAE7D,OAAO;EAAE;EAAU;CAAK;AAC1B;;;;;;AAOA,SAAgB,wBACd,IACA,aACA,OACA,QACA,WACM;CACN,MAAM,aAAiC;EAAE;EAAQ;CAAU;CAC3D,MAAM,QAAQ,GAAG,gBAAgB,UAAU;CAC3C,GAAG,aAAA,GAAA,0BAAA,MAAA,CAAkB,aAAaC,sBAAAA,gBAAgB,KAAK,CAAC,GAAG,WAAW,KAAK;AAC7E;;;;;;AAWA,SAAgB,qBACd,IACA,UACA,SACA,UACM;CACN,MAAM,QAAQ,GAAG,gBAAgB,QAAQ;CACzC,GAAG,aAAA,GAAA,0BAAA,MAAA,CAAkB,UAAUC,sBAAAA,cAAc,OAAO,CAAC,GAAG,WAAW,KAAK;AAC1E;;;;;;;;;AAUA,eAAsB,6BACpB,IACA,aACA,gBACA,QAC6B;CAG7B,MAAM,gBAAe,MADS,GAAG,gBAAgB,gBAAgB,IAAI,EAAA,CAChC,OAAO,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,OAAOC,0BAAAA,yBAAyB;CAChG,MAAM,kBACJ,MAAM,QAAQ,IAAI,aAAa,KAAK,QAAQ,GAAG,cAA2B,KAAKL,sBAAAA,cAAc,CAAC,CAAC,EAAA,CAC/F,KAAK,MAAM,EAAE,KAAK;CAIpB,MAAM,cAAa,MADQ,GAAG,gBAAgB,aAAa,IAAI,EAAA,CAC/B,OAC7B,QAAQ,MAAM,uBAAuB,EAAE,IAAI,CAAC,CAAC,CAC7C,KAAK,MAAM,EAAE,KAAK,CAAC,CACnB,OAAOK,0BAAAA,yBAAyB;CAEnC,MAAM,UAA8B,CAAC;CACrC,KAAK,MAAM,aAAa,YAAY;EAClC,MAAM,aAAa,MAAM,GAAG,cAA2B,WAAWL,sBAAAA,cAAc;EAChF,MAAM,WAAW,SAAS,OAAO,WAAW,OAAO,cAAc,IAAI,WAAW;EAChF,eAAe,KAAK,QAAQ;EAK5B,MAAM,SAAS,MAAMC,gBAAAA,iBAAiB,IAAI,WAAW,EAAE,OAAO,SAAS,CAAC;EACxE,GAAG,aAAA,GAAA,0BAAA,MAAA,CAAkB,iBAAA,GAAA,YAAA,WAAA,CAA2B,CAAC,GAAG,WAAW,MAAM;EAErE,MAAM,YAAY,MAAM,OAAO;EAC/B,QAAQ,KAAK,SAAS;CACxB;CAEA,OAAO;AACT;;AAGA,SAAgB,qBAAqB,KAAmC;CACtE,OAAO,IAAI,KAAK,QAAA,GAAA,0BAAA,mBAAA,CAA0B,EAAE,CAAC;AAC/C"}