@milaboratories/pl-middle-layer 1.64.41 → 1.64.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.cjs +16 -0
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +3 -1
  4. package/dist/middle_layer/frontend_path.cjs +1 -0
  5. package/dist/middle_layer/frontend_path.cjs.map +1 -1
  6. package/dist/middle_layer/frontend_path.js +1 -0
  7. package/dist/middle_layer/frontend_path.js.map +1 -1
  8. package/dist/middle_layer/index.d.ts +2 -1
  9. package/dist/middle_layer/middle_layer.cjs +443 -16
  10. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  11. package/dist/middle_layer/middle_layer.d.ts +153 -12
  12. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  13. package/dist/middle_layer/middle_layer.js +445 -18
  14. package/dist/middle_layer/middle_layer.js.map +1 -1
  15. package/dist/middle_layer/ops.cjs +2 -1
  16. package/dist/middle_layer/ops.cjs.map +1 -1
  17. package/dist/middle_layer/ops.d.ts +5 -1
  18. package/dist/middle_layer/ops.d.ts.map +1 -1
  19. package/dist/middle_layer/ops.js +2 -1
  20. package/dist/middle_layer/ops.js.map +1 -1
  21. package/dist/middle_layer/project.d.ts +4 -4
  22. package/dist/middle_layer/project.d.ts.map +1 -1
  23. package/dist/middle_layer/sharing_list.cjs +203 -0
  24. package/dist/middle_layer/sharing_list.cjs.map +1 -0
  25. package/dist/middle_layer/sharing_list.d.ts +41 -0
  26. package/dist/middle_layer/sharing_list.d.ts.map +1 -0
  27. package/dist/middle_layer/sharing_list.js +198 -0
  28. package/dist/middle_layer/sharing_list.js.map +1 -0
  29. package/dist/model/index.cjs +29 -0
  30. package/dist/model/index.d.ts +2 -1
  31. package/dist/model/index.js +4 -0
  32. package/dist/model/project_model.d.ts +3 -3
  33. package/dist/model/project_model.d.ts.map +1 -1
  34. package/dist/model/sharing_model.cjs +81 -0
  35. package/dist/model/sharing_model.cjs.map +1 -0
  36. package/dist/model/sharing_model.d.ts +112 -0
  37. package/dist/model/sharing_model.d.ts.map +1 -0
  38. package/dist/model/sharing_model.js +68 -0
  39. package/dist/model/sharing_model.js.map +1 -0
  40. package/dist/mutator/block-pack/frontend.cjs +1 -0
  41. package/dist/mutator/block-pack/frontend.cjs.map +1 -1
  42. package/dist/mutator/block-pack/frontend.js +1 -0
  43. package/dist/mutator/block-pack/frontend.js.map +1 -1
  44. package/dist/mutator/sharing.cjs +138 -0
  45. package/dist/mutator/sharing.cjs.map +1 -0
  46. package/dist/mutator/sharing.js +130 -0
  47. package/dist/mutator/sharing.js.map +1 -0
  48. package/package.json +14 -14
  49. package/src/middle_layer/index.ts +1 -0
  50. package/src/middle_layer/middle_layer.ts +644 -22
  51. package/src/middle_layer/ops.ts +7 -0
  52. package/src/middle_layer/sharing_list.ts +343 -0
  53. package/src/model/index.ts +2 -0
  54. package/src/model/sharing_model.ts +150 -0
  55. package/src/mutator/sharing.ts +216 -0
@@ -0,0 +1,216 @@
1
+ import type { PlTransaction, ResourceRef, SignedResourceId } from "@milaboratories/pl-client";
2
+ import { field, isNotNullSignedResourceId, resourceIdToString } from "@milaboratories/pl-client";
3
+ import { randomUUID } from "node:crypto";
4
+ import type { ProjectMeta } from "@milaboratories/pl-model-middle-layer";
5
+ import type { ProjectId } from "@milaboratories/pl-model-common";
6
+ import { ProjectMetaKey } from "../model/project_model";
7
+ import { duplicateProject } from "./project";
8
+ import type {
9
+ EnvelopeData,
10
+ EnvelopeAcceptance,
11
+ EnvelopeMode,
12
+ EnvelopeProject,
13
+ ProjectFieldUuid,
14
+ ShareId,
15
+ SharingDecision,
16
+ } from "../model/sharing_model";
17
+ import {
18
+ SharedEnvelopeResourceType,
19
+ acceptanceField,
20
+ decisionField,
21
+ newShareId,
22
+ } from "../model/sharing_model";
23
+
24
+ /** Field name carrying a project snapshot inside a {@link SharedEnvelopeResourceType}. */
25
+ export const EnvelopeProjectFieldPrefix = "project/";
26
+ export const envelopeProjectField = (uuid: ProjectFieldUuid) =>
27
+ `${EnvelopeProjectFieldPrefix}${uuid}`;
28
+
29
+ /** True for an envelope field that carries a project snapshot. */
30
+ export function isEnvelopeProjectField(name: string): boolean {
31
+ return name.startsWith(EnvelopeProjectFieldPrefix);
32
+ }
33
+
34
+ /** Extracts the project field uuid from a `project/{uuid}` field name. */
35
+ export function envelopeProjectFieldUuid(name: string): ProjectFieldUuid {
36
+ return name.slice(EnvelopeProjectFieldPrefix.length) as ProjectFieldUuid;
37
+ }
38
+
39
+ //
40
+ // Donor side
41
+ //
42
+
43
+ /**
44
+ * One project going into an envelope: `fresh` snapshots a live source (normal path); `carry`
45
+ * re-attaches an existing snapshot (change's "keep", or an "update" whose source is gone).
46
+ */
47
+ export type EnvelopeProjectSource =
48
+ | { kind: "fresh"; projectId: ProjectId; sourceRid: SignedResourceId }
49
+ | {
50
+ kind: "carry";
51
+ projectId: ProjectId;
52
+ label: string;
53
+ snapshotRid: SignedResourceId;
54
+ /** ms epoch the carried snapshot was last taken; preserved so "keep" keeps its timestamp. */
55
+ updatedAt: number;
56
+ };
57
+
58
+ /**
59
+ * Builds one {@link SharedEnvelopeResourceType} on the donor side inside the given write
60
+ * transaction: snapshots each source project by reference, seals the envelope with its
61
+ * immutable {@link EnvelopeData}, and attaches the envelope under `{shareId}` on the donor's
62
+ * outbox. The caller is responsible for issuing the per-recipient grants and committing the
63
+ * transaction — keeping create + grant atomic.
64
+ *
65
+ * @returns the new envelope resource and the generated `EnvelopeData`.
66
+ */
67
+ export async function buildShareEnvelope(
68
+ tx: PlTransaction,
69
+ outboxRid: SignedResourceId,
70
+ sources: EnvelopeProjectSource[],
71
+ params: {
72
+ mode: EnvelopeMode;
73
+ sender: string;
74
+ title: string;
75
+ /** ms epoch; sharedAt + ttl for a targeted share, null for share-with-everybody. */
76
+ expiresAt: number | null;
77
+ /** Existing shareId for a change; a fresh one is minted when omitted. */
78
+ shareId?: ShareId;
79
+ sharedAt?: number;
80
+ },
81
+ ): Promise<{ envelope: ResourceRef; data: EnvelopeData }> {
82
+ const shareId = params.shareId ?? newShareId();
83
+ const sharedAt = params.sharedAt ?? Date.now();
84
+
85
+ // Snapshot (fresh) or re-attach (carry) each project, collecting its metadata for the pack.
86
+ const projects: Record<ProjectFieldUuid, EnvelopeProject> = {};
87
+ const snapshots: { uuid: ProjectFieldUuid; ref: ResourceRef | SignedResourceId }[] = [];
88
+ for (const src of sources) {
89
+ const uuid = randomUUID() as ProjectFieldUuid;
90
+ if (src.kind === "fresh") {
91
+ const meta = await tx.getKValueJson<ProjectMeta>(src.sourceRid, ProjectMetaKey);
92
+ const ref = await duplicateProject(tx, src.sourceRid, { label: meta.label });
93
+ projects[uuid] = { label: meta.label, source: src.projectId, updatedAt: sharedAt }; // (re)snapshotted now
94
+ snapshots.push({ uuid, ref });
95
+ } else {
96
+ // Re-attach the prior snapshot; the new envelope references it before the old one is
97
+ // detached in the same tx, so it stays alive. Label and timestamp carry unchanged.
98
+ projects[uuid] = { label: src.label, source: src.projectId, updatedAt: src.updatedAt };
99
+ snapshots.push({ uuid, ref: src.snapshotRid });
100
+ }
101
+ }
102
+
103
+ const data: EnvelopeData = {
104
+ schemaVersion: 1,
105
+ shareId,
106
+ sharedAt,
107
+ expiresAt: params.expiresAt,
108
+ mode: params.mode,
109
+ sender: params.sender,
110
+ title: params.title,
111
+ projects,
112
+ };
113
+
114
+ // Immutable data set once at creation, never altered.
115
+ const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));
116
+
117
+ // Attach the project snapshots as Input fields, then seal the input set one-way.
118
+ for (const { uuid, ref } of snapshots) {
119
+ tx.createField(field(envelope, envelopeProjectField(uuid)), "Input", ref);
120
+ }
121
+ tx.lockInputs(envelope);
122
+
123
+ // Attach to the outbox under {shareId} in the same transaction so the held-resource rule
124
+ // keeps the ephemeral envelope alive.
125
+ tx.createField(field(outboxRid, shareId), "Dynamic", envelope);
126
+
127
+ return { envelope, data };
128
+ }
129
+
130
+ /**
131
+ * Records a response onto the envelope as a dynamic `acceptance/{login}` field: the acceptor
132
+ * writing their own decision (their writable grant permits it), or the donor transferring an
133
+ * existing record onto a changed envelope. Accepts the envelope by ref or id.
134
+ */
135
+ export function writeEnvelopeAcceptance(
136
+ tx: PlTransaction,
137
+ envelopeRid: ResourceRef | SignedResourceId,
138
+ login: string,
139
+ action: EnvelopeAcceptance["action"],
140
+ timestamp: number,
141
+ ): void {
142
+ const acceptance: EnvelopeAcceptance = { action, timestamp };
143
+ const value = tx.createJsonValue(acceptance);
144
+ tx.createField(field(envelopeRid, acceptanceField(login)), "Dynamic", value);
145
+ }
146
+
147
+ //
148
+ // Acceptor side
149
+ //
150
+
151
+ /**
152
+ * Records the acceptor's decision for a handled share on the acceptor's SharingState as a
153
+ * dynamic `decision/{shareId}` field. Keyed on the logical shareId so discovery dedups on the
154
+ * share, not on the envelope instance.
155
+ */
156
+ export function writeSharingDecision(
157
+ tx: PlTransaction,
158
+ stateRid: SignedResourceId,
159
+ shareId: ShareId,
160
+ decision: SharingDecision,
161
+ ): void {
162
+ const value = tx.createJsonValue(decision);
163
+ tx.createField(field(stateRid, decisionField(shareId)), "Dynamic", value);
164
+ }
165
+
166
+ /**
167
+ * Copies every project snapshot inside an envelope into the acceptor's own project list — a
168
+ * cross-color attach the backend permits. Mirrors {@link duplicateProject}'s `rename` contract,
169
+ * but resolves the source against the envelope tree (not the acceptor's own list), so the
170
+ * wrapper cannot be reused.
171
+ *
172
+ * @returns ids of the projects created in the acceptor's list.
173
+ */
174
+ export async function copyEnvelopeProjectsIntoList(
175
+ tx: PlTransaction,
176
+ envelopeRid: SignedResourceId,
177
+ projectListRid: SignedResourceId,
178
+ rename?: (previousLabel: string, existingLabels: string[]) => string,
179
+ ): Promise<SignedResourceId[]> {
180
+ // Read the acceptor's existing project labels once (own color, no relaxation).
181
+ const projectListData = await tx.getResourceData(projectListRid, true);
182
+ const existingRids = projectListData.fields.map((f) => f.value).filter(isNotNullSignedResourceId);
183
+ const existingLabels = (
184
+ await Promise.all(existingRids.map((rid) => tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey)))
185
+ ).map((m) => m.label);
186
+
187
+ // Enumerate the envelope's project/{uuid} input field values (signed envelope-colored ids).
188
+ const envelopeData = await tx.getResourceData(envelopeRid, true);
189
+ const sourceRids = envelopeData.fields
190
+ .filter((f) => isEnvelopeProjectField(f.name))
191
+ .map((f) => f.value)
192
+ .filter(isNotNullSignedResourceId);
193
+
194
+ const created: SignedResourceId[] = [];
195
+ for (const sourceRid of sourceRids) {
196
+ const sourceMeta = await tx.getKValueJson<ProjectMeta>(sourceRid, ProjectMetaKey);
197
+ const newLabel = rename ? rename(sourceMeta.label, existingLabels) : sourceMeta.label;
198
+ existingLabels.push(newLabel);
199
+
200
+ // Cross-color attach: a new UserProject in the acceptor's color whose fields point at
201
+ // envelope-colored resources. Fails with PermissionDenied: color mismatch on a backend
202
+ // that lacks crossTreeRefs:v1.
203
+ const newPrj = await duplicateProject(tx, sourceRid, { label: newLabel });
204
+ tx.createField(field(projectListRid, randomUUID()), "Dynamic", newPrj);
205
+
206
+ const signedRid = await newPrj.globalId;
207
+ created.push(signedRid);
208
+ }
209
+
210
+ return created;
211
+ }
212
+
213
+ /** String-form ids of the projects created by {@link copyEnvelopeProjectsIntoList}. */
214
+ export function resourceIdsToStrings(ids: SignedResourceId[]): string[] {
215
+ return ids.map((id) => resourceIdToString(id));
216
+ }