@milaboratories/pl-middle-layer 1.64.40 → 1.64.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.cjs +16 -0
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +3 -1
  4. package/dist/middle_layer/frontend_path.cjs +1 -0
  5. package/dist/middle_layer/frontend_path.cjs.map +1 -1
  6. package/dist/middle_layer/frontend_path.js +1 -0
  7. package/dist/middle_layer/frontend_path.js.map +1 -1
  8. package/dist/middle_layer/index.d.ts +2 -1
  9. package/dist/middle_layer/middle_layer.cjs +443 -16
  10. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  11. package/dist/middle_layer/middle_layer.d.ts +153 -12
  12. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  13. package/dist/middle_layer/middle_layer.js +445 -18
  14. package/dist/middle_layer/middle_layer.js.map +1 -1
  15. package/dist/middle_layer/ops.cjs +2 -1
  16. package/dist/middle_layer/ops.cjs.map +1 -1
  17. package/dist/middle_layer/ops.d.ts +5 -1
  18. package/dist/middle_layer/ops.d.ts.map +1 -1
  19. package/dist/middle_layer/ops.js +2 -1
  20. package/dist/middle_layer/ops.js.map +1 -1
  21. package/dist/middle_layer/project.d.ts +4 -4
  22. package/dist/middle_layer/project.d.ts.map +1 -1
  23. package/dist/middle_layer/sharing_list.cjs +203 -0
  24. package/dist/middle_layer/sharing_list.cjs.map +1 -0
  25. package/dist/middle_layer/sharing_list.d.ts +41 -0
  26. package/dist/middle_layer/sharing_list.d.ts.map +1 -0
  27. package/dist/middle_layer/sharing_list.js +198 -0
  28. package/dist/middle_layer/sharing_list.js.map +1 -0
  29. package/dist/model/index.cjs +29 -0
  30. package/dist/model/index.d.ts +2 -1
  31. package/dist/model/index.js +4 -0
  32. package/dist/model/project_model.d.ts +3 -3
  33. package/dist/model/project_model.d.ts.map +1 -1
  34. package/dist/model/sharing_model.cjs +81 -0
  35. package/dist/model/sharing_model.cjs.map +1 -0
  36. package/dist/model/sharing_model.d.ts +112 -0
  37. package/dist/model/sharing_model.d.ts.map +1 -0
  38. package/dist/model/sharing_model.js +68 -0
  39. package/dist/model/sharing_model.js.map +1 -0
  40. package/dist/mutator/block-pack/frontend.cjs +1 -0
  41. package/dist/mutator/block-pack/frontend.cjs.map +1 -1
  42. package/dist/mutator/block-pack/frontend.js +1 -0
  43. package/dist/mutator/block-pack/frontend.js.map +1 -1
  44. package/dist/mutator/sharing.cjs +138 -0
  45. package/dist/mutator/sharing.cjs.map +1 -0
  46. package/dist/mutator/sharing.js +130 -0
  47. package/dist/mutator/sharing.js.map +1 -0
  48. package/package.json +13 -13
  49. package/src/middle_layer/index.ts +1 -0
  50. package/src/middle_layer/middle_layer.ts +644 -22
  51. package/src/middle_layer/ops.ts +7 -0
  52. package/src/middle_layer/sharing_list.ts +343 -0
  53. package/src/model/index.ts +2 -0
  54. package/src/model/sharing_model.ts +150 -0
  55. package/src/mutator/sharing.ts +216 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing_list.cjs","names":["SharingOutboxResourceType","SharedEnvelopeResourceType","treeFilter","Computable","AcceptanceFieldPrefix","SynchronizedTreeState","asShareId","SharingStateResourceType"],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceIdToString,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId } from \"@milaboratories/pl-model-common\";\nimport type {\n EnvelopeAcceptance,\n EnvelopeData,\n EnvelopeMode,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n AcceptanceFieldPrefix,\n asShareId,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // stable logical identity of the share, preserved across changes\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** One entry per project in the pack, so the change UI can offer a per-project decision.\n * `projectId` is the donor's source project id; `updatedAt` is when this project's\n * snapshot was last (re)taken. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n /** Per recipient who has responded: their decision and when, from acceptance/{login}. */\n responses: Record<string, { action: \"accepted\" | \"rejected\"; timestamp: number }>;\n}\n\n/** Per-project view for the donor's change UI, from {@link EnvelopeData.projects}. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(data.projects).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/** Acceptor-facing view of one pending share. */\nexport interface PendingShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 renders only \"copy\" entries\n grantedAt: number;\n}\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then\n * enriches each share's full recipient list via `ListGrants` on the envelope (`[\"*\"]` for an\n * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is\n * exactly who reads this view.\n *\n * API-level only in M1 (no UI).\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n\n const responses: OutgoingShare[\"responses\"] = {};\n for (const f of envelope.listDynamicFields()) {\n if (!f.startsWith(AcceptanceFieldPrefix)) continue;\n const login = f.slice(AcceptanceFieldPrefix.length);\n const acc = envelope.traverse(f);\n const accData = acc?.getDataAsJson<EnvelopeAcceptance>();\n if (accData === undefined) continue;\n responses[login] = { action: accData.action, timestamp: accData.timestamp };\n }\n\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n projects: envelopeProjects(data),\n responses,\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\nconst DecisionFieldPrefix = \"decision/\";\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst PendingSharesFieldFilter: Filter = treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name);\n\n/**\n * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.\n *\n * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —\n * the dedup is applied by the caller, which holds the SharingState tree.\n */\nexport async function createPendingSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: PendingSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/** A live envelope discovered in the acceptor's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. The accept/reject flow keys these by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Builds a Computable yielding the acceptor's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no\n * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx\n * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed\n * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live\n * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n });\n}\n\n/**\n * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered\n * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's\n * SharingState). Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope as a pending share. There is no\n * scenario where a user accepts their own share, so they are dropped from the pending view.\n */\nexport function createPendingSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<PendingShare[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n\n // Set of shareIds already handled (accepted or rejected) — dedup discovery on the logical share.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const handled = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (f.startsWith(DecisionFieldPrefix))\n handled.add(asShareId(f.slice(DecisionFieldPrefix.length)));\n\n const result: PendingShare[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n if (handled.has(data.shareId)) continue; // already accepted or rejected\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n grantedAt: data.sharedAt,\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Creates the acceptor's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n/** Maps an envelope resource id (signed) to its string form, for failure reporting. */\nexport function envelopeIdString(rid: SignedResourceId): string {\n return resourceIdToString(rid);\n}\n"],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { EnvelopeMode, ShareId } from "../model/sharing_model.js";
2
+ import { PlClient, SignedResourceId } from "@milaboratories/pl-client";
3
+ import { SynchronizedTreeState } from "@milaboratories/pl-tree";
4
+ import { Computable } from "@milaboratories/computable";
5
+ import { ProjectId } from "@milaboratories/pl-model-common";
6
+
7
+ //#region src/middle_layer/sharing_list.d.ts
8
+ /** Donor-facing view of one outgoing share. */
9
+ interface OutgoingShare {
10
+ shareId: ShareId;
11
+ sharedAt: number;
12
+ expiresAt?: number;
13
+ mode: EnvelopeMode;
14
+ title: string;
15
+ /** One entry per project in the pack, so the change UI can offer a per-project decision.
16
+ * `projectId` is the donor's source project id; `updatedAt` is when this project's
17
+ * snapshot was last (re)taken. */
18
+ projects: {
19
+ projectId: ProjectId;
20
+ label: string;
21
+ updatedAt: number;
22
+ }[];
23
+ /** Full recipient logins, from `ListGrants` on the envelope; `["*"]` for everyone-shares. */
24
+ recipients: string[];
25
+ /** Per recipient who has responded: their decision and when, from acceptance/{login}. */
26
+ responses: Record<string, {
27
+ action: "accepted" | "rejected";
28
+ timestamp: number;
29
+ }>;
30
+ }
31
+ /** Acceptor-facing view of one pending share. */
32
+ interface PendingShare {
33
+ shareId: ShareId;
34
+ sender: string;
35
+ title: string;
36
+ mode: EnvelopeMode;
37
+ grantedAt: number;
38
+ }
39
+ //#endregion
40
+ export { OutgoingShare, PendingShare };
41
+ //# sourceMappingURL=sharing_list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing_list.d.ts","names":[],"sources":["../../src/middle_layer/sharing_list.ts"],"mappings":";;;;;;;;UA4BiB,aAAA;EACf,OAAA,EAAS,OAAA;EACT,QAAA;EACA,SAAA;EACA,IAAA,EAAM,YAAA;EACN,KAAA;EAIuB;;;EAAvB,QAAA;IAAY,SAAA,EAAW,SAAA;IAAW,KAAA;IAAe,SAAA;EAAA;EALjD;EAOA,UAAA;EANA;EAQA,SAAA,EAAW,MAAA;IAAiB,MAAA;IAAiC,SAAA;EAAA;AAAA;;UAa9C,YAAA;EACf,OAAA,EAAS,OAAA;EACT,MAAA;EACA,KAAA;EACA,IAAA,EAAM,YAAY;EAClB,SAAA;AAAA"}
@@ -0,0 +1,198 @@
1
+ import { AcceptanceFieldPrefix, SharedEnvelopeResourceType, SharingOutboxResourceType, SharingStateResourceType, asShareId } from "../model/sharing_model.js";
2
+ import { isEveryoneUserLogin, resourceType, resourceTypesEqual, treeFilter } from "@milaboratories/pl-client";
3
+ import { SynchronizedTreeState } from "@milaboratories/pl-tree";
4
+ import { Computable } from "@milaboratories/computable";
5
+ //#region src/middle_layer/sharing_list.ts
6
+ /** Per-project view for the donor's change UI, from {@link EnvelopeData.projects}. */
7
+ function envelopeProjects(data) {
8
+ return Object.values(data.projects).map((p) => ({
9
+ projectId: p.source,
10
+ label: p.label,
11
+ updatedAt: p.updatedAt
12
+ }));
13
+ }
14
+ const SharingOutboxPruningFunction = (resource) => {
15
+ if (!resourceTypesEqual(resource.type, SharingOutboxResourceType) && !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];
16
+ return resource.fields;
17
+ };
18
+ const SharingOutboxFieldFilter = treeFilter.or(treeFilter.resourceTypeEq(SharingOutboxResourceType.name), treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name));
19
+ /**
20
+ * Reactive view of the donor's own outbox. Reads each live envelope's immutable
21
+ * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then
22
+ * enriches each share's full recipient list via `ListGrants` on the envelope (`["*"]` for an
23
+ * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is
24
+ * exactly who reads this view.
25
+ *
26
+ * API-level only in M1 (no UI).
27
+ */
28
+ function createOutgoingSharesComputable(pl, tree) {
29
+ return Computable.make((ctx) => {
30
+ const node = ctx.accessor(tree.entry()).node();
31
+ if (node === void 0) return void 0;
32
+ const drafts = [];
33
+ for (const fieldName of node.listDynamicFields()) {
34
+ const envelope = node.traverse(fieldName);
35
+ if (envelope === void 0) continue;
36
+ if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;
37
+ const data = envelope.getDataAsJson();
38
+ if (data === void 0) continue;
39
+ const responses = {};
40
+ for (const f of envelope.listDynamicFields()) {
41
+ if (!f.startsWith("acceptance/")) continue;
42
+ const login = f.slice(AcceptanceFieldPrefix.length);
43
+ const accData = envelope.traverse(f)?.getDataAsJson();
44
+ if (accData === void 0) continue;
45
+ responses[login] = {
46
+ action: accData.action,
47
+ timestamp: accData.timestamp
48
+ };
49
+ }
50
+ drafts.push({
51
+ shareId: data.shareId,
52
+ sharedAt: data.sharedAt,
53
+ ...data.expiresAt !== null ? { expiresAt: data.expiresAt } : {},
54
+ mode: data.mode,
55
+ title: data.title,
56
+ projects: envelopeProjects(data),
57
+ responses,
58
+ envelopeRid: envelope.id
59
+ });
60
+ }
61
+ drafts.sort((a, b) => b.sharedAt - a.sharedAt);
62
+ return drafts;
63
+ }, { postprocessValue: async (drafts) => {
64
+ if (drafts === void 0) return void 0;
65
+ const self = pl.userResources.authUser;
66
+ const toRecipients = (grants) => grants.some((g) => isEveryoneUserLogin(g.user)) ? ["*"] : grants.map((g) => g.user).filter((u) => u !== self);
67
+ const txGrants = pl.hasCapability("txListGrants:v1");
68
+ const settled = await Promise.allSettled(drafts.map((d) => txGrants ? pl.withReadTx("ListShareGrant", (tx) => tx.listGrants(d.envelopeRid)) : pl.userResources.listGrants(d.envelopeRid)));
69
+ return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i) => {
70
+ const r = settled[i];
71
+ return {
72
+ ...share,
73
+ recipients: r.status === "fulfilled" ? toRecipients(r.value) : []
74
+ };
75
+ });
76
+ } });
77
+ }
78
+ /**
79
+ * Creates the donor's outbox synchronized tree (single explicit root) plus the
80
+ * {@link OutgoingShare} computable over it.
81
+ */
82
+ async function createOutgoingShares(pl, outboxRid, env) {
83
+ const tree = await SynchronizedTreeState.init(pl, outboxRid, {
84
+ ...env.ops.defaultTreeOptions,
85
+ pruning: SharingOutboxPruningFunction,
86
+ fieldFilter: SharingOutboxFieldFilter
87
+ }, env.logger);
88
+ return {
89
+ computable: createOutgoingSharesComputable(pl, tree),
90
+ tree
91
+ };
92
+ }
93
+ const DecisionFieldPrefix = "decision/";
94
+ const SharedEnvelopePruningFunction = (resource) => {
95
+ if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];
96
+ return resource.fields;
97
+ };
98
+ const PendingSharesFieldFilter = treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name);
99
+ /**
100
+ * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over
101
+ * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.
102
+ *
103
+ * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —
104
+ * the dedup is applied by the caller, which holds the SharingState tree.
105
+ */
106
+ async function createPendingSharesTree(pl, env) {
107
+ return await SynchronizedTreeState.init(pl, {
108
+ kind: "shared",
109
+ resourceType: resourceType(SharedEnvelopeResourceType.name, SharedEnvelopeResourceType.version)
110
+ }, {
111
+ ...env.ops.defaultTreeOptions,
112
+ pruning: SharedEnvelopePruningFunction,
113
+ fieldFilter: PendingSharesFieldFilter
114
+ }, env.logger);
115
+ }
116
+ /**
117
+ * Builds a Computable yielding the acceptor's currently-live envelopes, read from the
118
+ * shared-resource discovery tree the ML already maintains — the single discovery mechanism.
119
+ * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no
120
+ * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx
121
+ * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.
122
+ *
123
+ * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed
124
+ * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live
125
+ * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.
126
+ */
127
+ function createLiveEnvelopesComputable(sharedTree) {
128
+ return Computable.make((ctx) => {
129
+ const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();
130
+ const result = [];
131
+ for (const envelope of roots) {
132
+ if (envelope === void 0) continue;
133
+ if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;
134
+ const data = envelope.getDataAsJson();
135
+ if (data === void 0) continue;
136
+ result.push({
137
+ rid: envelope.id,
138
+ data
139
+ });
140
+ }
141
+ return result;
142
+ });
143
+ }
144
+ /**
145
+ * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered
146
+ * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's
147
+ * SharingState). Both trees feed one Computable so it recomputes when either changes.
148
+ *
149
+ * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants
150
+ * the everyone-user, so the donor discovers their own envelope as a pending share. There is no
151
+ * scenario where a user accepts their own share, so they are dropped from the pending view.
152
+ */
153
+ function createPendingSharesComputable(sharedTree, sharingStateTree, currentUserLogin) {
154
+ return Computable.make((ctx) => {
155
+ const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();
156
+ const stateNode = ctx.accessor(sharingStateTree.entry()).node();
157
+ const handled = /* @__PURE__ */ new Set();
158
+ if (stateNode !== void 0) {
159
+ for (const f of stateNode.listDynamicFields()) if (f.startsWith(DecisionFieldPrefix)) handled.add(asShareId(f.slice(9)));
160
+ }
161
+ const result = [];
162
+ for (const envelope of roots) {
163
+ if (envelope === void 0) continue;
164
+ if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;
165
+ const data = envelope.getDataAsJson();
166
+ if (data === void 0) continue;
167
+ if (handled.has(data.shareId)) continue;
168
+ if (currentUserLogin !== null && data.sender === currentUserLogin) continue;
169
+ if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;
170
+ result.push({
171
+ shareId: data.shareId,
172
+ sender: data.sender,
173
+ title: data.title,
174
+ mode: data.mode,
175
+ grantedAt: data.sharedAt
176
+ });
177
+ }
178
+ result.sort((a, b) => b.grantedAt - a.grantedAt);
179
+ return result;
180
+ });
181
+ }
182
+ const SharingStatePruningFunction = (resource) => {
183
+ if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];
184
+ return resource.fields;
185
+ };
186
+ const SharingStateFieldFilter = treeFilter.resourceTypeEq(SharingStateResourceType.name);
187
+ /** Creates the acceptor's SharingState synchronized tree (single explicit root). */
188
+ async function createSharingStateTree(pl, stateRid, env) {
189
+ return await SynchronizedTreeState.init(pl, stateRid, {
190
+ ...env.ops.defaultTreeOptions,
191
+ pruning: SharingStatePruningFunction,
192
+ fieldFilter: SharingStateFieldFilter
193
+ }, env.logger);
194
+ }
195
+ //#endregion
196
+ export { createLiveEnvelopesComputable, createOutgoingShares, createOutgoingSharesComputable, createPendingSharesComputable, createPendingSharesTree, createSharingStateTree };
197
+
198
+ //# sourceMappingURL=sharing_list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing_list.js","names":[],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceIdToString,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId } from \"@milaboratories/pl-model-common\";\nimport type {\n EnvelopeAcceptance,\n EnvelopeData,\n EnvelopeMode,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n AcceptanceFieldPrefix,\n asShareId,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // stable logical identity of the share, preserved across changes\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** One entry per project in the pack, so the change UI can offer a per-project decision.\n * `projectId` is the donor's source project id; `updatedAt` is when this project's\n * snapshot was last (re)taken. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n /** Per recipient who has responded: their decision and when, from acceptance/{login}. */\n responses: Record<string, { action: \"accepted\" | \"rejected\"; timestamp: number }>;\n}\n\n/** Per-project view for the donor's change UI, from {@link EnvelopeData.projects}. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(data.projects).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/** Acceptor-facing view of one pending share. */\nexport interface PendingShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 renders only \"copy\" entries\n grantedAt: number;\n}\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then\n * enriches each share's full recipient list via `ListGrants` on the envelope (`[\"*\"]` for an\n * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is\n * exactly who reads this view.\n *\n * API-level only in M1 (no UI).\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n\n const responses: OutgoingShare[\"responses\"] = {};\n for (const f of envelope.listDynamicFields()) {\n if (!f.startsWith(AcceptanceFieldPrefix)) continue;\n const login = f.slice(AcceptanceFieldPrefix.length);\n const acc = envelope.traverse(f);\n const accData = acc?.getDataAsJson<EnvelopeAcceptance>();\n if (accData === undefined) continue;\n responses[login] = { action: accData.action, timestamp: accData.timestamp };\n }\n\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n projects: envelopeProjects(data),\n responses,\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\nconst DecisionFieldPrefix = \"decision/\";\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst PendingSharesFieldFilter: Filter = treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name);\n\n/**\n * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.\n *\n * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —\n * the dedup is applied by the caller, which holds the SharingState tree.\n */\nexport async function createPendingSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: PendingSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/** A live envelope discovered in the acceptor's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. The accept/reject flow keys these by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Builds a Computable yielding the acceptor's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no\n * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx\n * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed\n * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live\n * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n });\n}\n\n/**\n * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered\n * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's\n * SharingState). Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope as a pending share. There is no\n * scenario where a user accepts their own share, so they are dropped from the pending view.\n */\nexport function createPendingSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<PendingShare[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n\n // Set of shareIds already handled (accepted or rejected) — dedup discovery on the logical share.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const handled = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (f.startsWith(DecisionFieldPrefix))\n handled.add(asShareId(f.slice(DecisionFieldPrefix.length)));\n\n const result: PendingShare[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n if (handled.has(data.shareId)) continue; // already accepted or rejected\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n grantedAt: data.sharedAt,\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Creates the acceptor's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n/** Maps an envelope resource id (signed) to its string form, for failure reporting. */\nexport function envelopeIdString(rid: SignedResourceId): string {\n return resourceIdToString(rid);\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ const require_project_model = require("./project_model.cjs");
2
+ const require_block_pack_spec = require("./block_pack_spec.cjs");
3
+ const require_sharing_model = require("./sharing_model.cjs");
4
+ exports.AcceptanceFieldPrefix = require_sharing_model.AcceptanceFieldPrefix;
5
+ exports.BlockArgsAuthorKeyPrefix = require_project_model.BlockArgsAuthorKeyPrefix;
6
+ exports.FrontendFromFolderResourceType = require_block_pack_spec.FrontendFromFolderResourceType;
7
+ exports.FrontendFromLocalTgzResourceType = require_block_pack_spec.FrontendFromLocalTgzResourceType;
8
+ exports.FrontendFromUrlResourceType = require_block_pack_spec.FrontendFromUrlResourceType;
9
+ exports.ProjectCreatedTimestamp = require_project_model.ProjectCreatedTimestamp;
10
+ exports.ProjectLastModifiedTimestamp = require_project_model.ProjectLastModifiedTimestamp;
11
+ exports.ProjectMetaKey = require_project_model.ProjectMetaKey;
12
+ exports.ProjectResourceType = require_project_model.ProjectResourceType;
13
+ exports.ProjectStructureAuthorKey = require_project_model.ProjectStructureAuthorKey;
14
+ exports.ProjectStructureKey = require_project_model.ProjectStructureKey;
15
+ exports.SchemaVersionCurrent = require_project_model.SchemaVersionCurrent;
16
+ exports.SchemaVersionKey = require_project_model.SchemaVersionKey;
17
+ exports.SharedEnvelopeResourceType = require_sharing_model.SharedEnvelopeResourceType;
18
+ exports.SharingOutboxField = require_sharing_model.SharingOutboxField;
19
+ exports.SharingOutboxResourceType = require_sharing_model.SharingOutboxResourceType;
20
+ exports.SharingStateField = require_sharing_model.SharingStateField;
21
+ exports.SharingStateResourceType = require_sharing_model.SharingStateResourceType;
22
+ exports.acceptanceField = require_sharing_model.acceptanceField;
23
+ exports.acceptanceFieldLogin = require_sharing_model.acceptanceFieldLogin;
24
+ exports.asShareId = require_sharing_model.asShareId;
25
+ exports.canGrantToEveryone = require_sharing_model.canGrantToEveryone;
26
+ exports.decisionField = require_sharing_model.decisionField;
27
+ exports.decodeEnvelopeData = require_sharing_model.decodeEnvelopeData;
28
+ exports.isAcceptanceField = require_sharing_model.isAcceptanceField;
29
+ exports.newShareId = require_sharing_model.newShareId;
@@ -1,3 +1,4 @@
1
1
  import { BlockArgsAuthorKeyPrefix, ProjectCreatedTimestamp, ProjectField, ProjectId, ProjectLastModifiedTimestamp, ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey } from "./project_model.js";
2
+ import { AcceptanceFieldPrefix, EnvelopeAcceptance, EnvelopeData, EnvelopeMode, EnvelopeProject, ProjectChangeAction, ProjectFieldUuid, ShareId, ShareProjectsOptions, SharedEnvelopeResourceType, SharingDecision, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, decisionField, decodeEnvelopeData, isAcceptanceField, newShareId } from "./sharing_model.js";
2
3
  import { BlockPackExplicit, BlockPackSpecAny, BlockPackSpecPrepared, FrontendFromFolder, FrontendFromFolderData, FrontendFromFolderResourceType, FrontendFromLocalTgz, FrontendFromLocalTgzData, FrontendFromLocalTgzResourceType, FrontendFromUrl, FrontendFromUrlData, FrontendFromUrlResourceType, FrontendSpec } from "./block_pack_spec.js";
3
- export { BlockArgsAuthorKeyPrefix, BlockPackExplicit, BlockPackSpecAny, BlockPackSpecPrepared, FrontendFromFolder, FrontendFromFolderData, FrontendFromFolderResourceType, FrontendFromLocalTgz, FrontendFromLocalTgzData, FrontendFromLocalTgzResourceType, FrontendFromUrl, FrontendFromUrlData, FrontendFromUrlResourceType, FrontendSpec, ProjectCreatedTimestamp, type ProjectField, ProjectLastModifiedTimestamp, type ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey };
4
+ export { AcceptanceFieldPrefix, BlockArgsAuthorKeyPrefix, BlockPackExplicit, BlockPackSpecAny, BlockPackSpecPrepared, EnvelopeAcceptance, EnvelopeData, EnvelopeMode, EnvelopeProject, FrontendFromFolder, FrontendFromFolderData, FrontendFromFolderResourceType, FrontendFromLocalTgz, FrontendFromLocalTgzData, FrontendFromLocalTgzResourceType, FrontendFromUrl, FrontendFromUrlData, FrontendFromUrlResourceType, FrontendSpec, ProjectChangeAction, ProjectCreatedTimestamp, type ProjectField, ProjectFieldUuid, ProjectLastModifiedTimestamp, type ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey, ShareId, ShareProjectsOptions, SharedEnvelopeResourceType, SharingDecision, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, decisionField, decodeEnvelopeData, isAcceptanceField, newShareId };
@@ -0,0 +1,4 @@
1
+ import { BlockArgsAuthorKeyPrefix, ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey } from "./project_model.js";
2
+ import { FrontendFromFolderResourceType, FrontendFromLocalTgzResourceType, FrontendFromUrlResourceType } from "./block_pack_spec.js";
3
+ import { AcceptanceFieldPrefix, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, decisionField, decodeEnvelopeData, isAcceptanceField, newShareId } from "./sharing_model.js";
4
+ export { AcceptanceFieldPrefix, BlockArgsAuthorKeyPrefix, FrontendFromFolderResourceType, FrontendFromLocalTgzResourceType, FrontendFromUrlResourceType, ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, decisionField, decodeEnvelopeData, isAcceptanceField, newShareId };
@@ -1,11 +1,11 @@
1
1
  import { ProjectListEntry, ProjectMeta } from "@milaboratories/pl-model-middle-layer";
2
2
  import { ResourceType } from "@milaboratories/pl-client";
3
- import { ProjectId } from "@milaboratories/pl-model-common";
3
+ import { ProjectId as ProjectId$1 } from "@milaboratories/pl-model-common";
4
4
 
5
5
  //#region src/model/project_model.d.ts
6
6
  interface ProjectListEntry$1 extends Omit<ProjectListEntry, "id"> {
7
7
  /** Unique project identifier in middle layer. Use to operate with given project. */
8
- id: ProjectId;
8
+ id: ProjectId$1;
9
9
  }
10
10
  declare const ProjectResourceType: ResourceType;
11
11
  declare const SchemaVersionKey = "SchemaVersion";
@@ -21,5 +21,5 @@ interface ProjectField {
21
21
  fieldName: "blockPack" | "blockSettings" | "blockStorage" | "prodArgs" | "currentArgs" | "currentPrerunArgs" | "prodChainCtx" | "prodCtx" | "prodUiCtx" | "prodOutput" | "prodCtxPrevious" | "prodUiCtxPrevious" | "prodOutputPrevious" | "stagingCtx" | "stagingUiCtx" | "stagingOutput" | "stagingCtxPrevious" | "stagingUiCtxPrevious" | "stagingOutputPrevious";
22
22
  }
23
23
  //#endregion
24
- export { BlockArgsAuthorKeyPrefix, ProjectCreatedTimestamp, ProjectField, type ProjectId, ProjectLastModifiedTimestamp, ProjectListEntry$1 as ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey };
24
+ export { BlockArgsAuthorKeyPrefix, ProjectCreatedTimestamp, ProjectField, type ProjectId$1 as ProjectId, ProjectLastModifiedTimestamp, ProjectListEntry$1 as ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey };
25
25
  //# sourceMappingURL=project_model.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project_model.d.ts","names":[],"sources":["../../src/model/project_model.ts"],"mappings":";;;;;UAUiB,kBAAA,SAAyB,IAAA,CAAK,gBAAA;EAA9B;EAEf,EAAA,EAAI,SAAA;AAAA;AAAA,cA+DO,mBAAA,EAAqB,YAAoD;AAAA,cAEzE,gBAAA;AAAA,cAGA,oBAAA;AAAA,cAEA,uBAAA;AAAA,cACA,4BAAA;AAAA,cACA,cAAA;AAAA,cACA,mBAAA;AAAA,cAGA,wBAAA;AAAA,cAOA,yBAAA;AAAA,UAQI,YAAA;EACf,OAAA;EACA,SAAS;AAAA"}
1
+ {"version":3,"file":"project_model.d.ts","names":[],"sources":["../../src/model/project_model.ts"],"mappings":";;;;;UAUiB,kBAAA,SAAyB,IAAA,CAAK,gBAAA;EAA9B;EAEf,EAAA,EAAI,WAAA;AAAA;AAAA,cA+DO,mBAAA,EAAqB,YAAoD;AAAA,cAEzE,gBAAA;AAAA,cAGA,oBAAA;AAAA,cAEA,uBAAA;AAAA,cACA,4BAAA;AAAA,cACA,cAAA;AAAA,cACA,mBAAA;AAAA,cAGA,wBAAA;AAAA,cAOA,yBAAA;AAAA,UAQI,YAAA;EACf,OAAA;EACA,SAAS;AAAA"}
@@ -0,0 +1,81 @@
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
+ /** Dynamic field on SharingState, one per handled share, keyed by shareId. */
44
+ const decisionField = (shareId) => `decision/${shareId}`;
45
+ /** Dynamic field on SharedEnvelope, one per recipient who accepted or rejected, keyed
46
+ * by recipient login. Written by the acceptor in read-write shares only (Copy & Share,
47
+ * Live collaboration) — the acceptor's writable envelope grant is what permits the
48
+ * write; read-only shares omit it. The donor reads these from its own outbox to see
49
+ * who responded and when. Informational, not authoritative (a writable grant holder
50
+ * could write under another login — same trust assumption as the sender field).
51
+ * Copied forward when a share is changed. */
52
+ const AcceptanceFieldPrefix = "acceptance/";
53
+ const acceptanceField = (login) => `${AcceptanceFieldPrefix}${login}`;
54
+ const isAcceptanceField = (name) => name.startsWith(AcceptanceFieldPrefix);
55
+ const acceptanceFieldLogin = (name) => name.slice(11);
56
+ /**
57
+ * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`
58
+ * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource
59
+ * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node
60
+ * path uses `node.getDataAsJson<EnvelopeData>()`, which decodes the same JSON.
61
+ */
62
+ function decodeEnvelopeData(data) {
63
+ return JSON.parse(Buffer.from(data).toString("utf-8"));
64
+ }
65
+ //#endregion
66
+ exports.AcceptanceFieldPrefix = AcceptanceFieldPrefix;
67
+ exports.SharedEnvelopeResourceType = SharedEnvelopeResourceType;
68
+ exports.SharingOutboxField = SharingOutboxField;
69
+ exports.SharingOutboxResourceType = SharingOutboxResourceType;
70
+ exports.SharingStateField = SharingStateField;
71
+ exports.SharingStateResourceType = SharingStateResourceType;
72
+ exports.acceptanceField = acceptanceField;
73
+ exports.acceptanceFieldLogin = acceptanceFieldLogin;
74
+ exports.asShareId = asShareId;
75
+ exports.canGrantToEveryone = canGrantToEveryone;
76
+ exports.decisionField = decisionField;
77
+ exports.decodeEnvelopeData = decodeEnvelopeData;
78
+ exports.isAcceptanceField = isAcceptanceField;
79
+ exports.newShareId = newShareId;
80
+
81
+ //# 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/** 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;;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,112 @@
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
+ /** One project's snapshot inside an envelope, keyed by {@link ProjectFieldUuid} in {@link EnvelopeData.projects}. */
40
+ interface EnvelopeProject {
41
+ label: string;
42
+ source: ProjectId;
43
+ updatedAt: number;
44
+ }
45
+ /** Immutable `data` on a SharedEnvelope, set at createEphemeral, never mutated. */
46
+ interface EnvelopeData {
47
+ schemaVersion: 1;
48
+ shareId: ShareId;
49
+ sharedAt: number;
50
+ expiresAt: number | null;
51
+ mode: EnvelopeMode;
52
+ sender: string;
53
+ title: string;
54
+ projects: Record<ProjectFieldUuid, EnvelopeProject>;
55
+ }
56
+ /** Dynamic field on SharingState, one per handled share, keyed by shareId. */
57
+ declare const decisionField: (shareId: ShareId) => string;
58
+ interface SharingDecision {
59
+ decision: "accepted" | "rejected";
60
+ timestamp: number;
61
+ envelopeSharedAt: number;
62
+ acceptedProjects: string[];
63
+ }
64
+ /** Dynamic field on SharedEnvelope, one per recipient who accepted or rejected, keyed
65
+ * by recipient login. Written by the acceptor in read-write shares only (Copy & Share,
66
+ * Live collaboration) — the acceptor's writable envelope grant is what permits the
67
+ * write; read-only shares omit it. The donor reads these from its own outbox to see
68
+ * who responded and when. Informational, not authoritative (a writable grant holder
69
+ * could write under another login — same trust assumption as the sender field).
70
+ * Copied forward when a share is changed. */
71
+ declare const AcceptanceFieldPrefix = "acceptance/";
72
+ declare const acceptanceField: (login: string) => string;
73
+ declare const isAcceptanceField: (name: string) => boolean;
74
+ declare const acceptanceFieldLogin: (name: string) => string;
75
+ interface EnvelopeAcceptance {
76
+ action: "accepted" | "rejected";
77
+ timestamp: number;
78
+ }
79
+ /**
80
+ * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`
81
+ * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource
82
+ * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node
83
+ * path uses `node.getDataAsJson<EnvelopeData>()`, which decodes the same JSON.
84
+ */
85
+ declare function decodeEnvelopeData(data: Uint8Array): EnvelopeData;
86
+ /**
87
+ * Options for {@link MiddleLayer.shareProjects}.
88
+ *
89
+ * Recipients XOR everyone — two clean variants, not one struct with mutually exclusive
90
+ * optional fields. The everyone variant issues a single make-public grant (the envelope's
91
+ * `expiresAt` is set to `null`, so it never expires); the recipients variant grants each
92
+ * named recipient and the envelope expires after the default TTL.
93
+ */
94
+ type ShareProjectsOptions = {
95
+ recipients: string[];
96
+ title: string;
97
+ mode: EnvelopeMode;
98
+ } | {
99
+ everyone: true;
100
+ /**
101
+ * When true and an everyone-share of the same project already exists, refresh it under its
102
+ * stable shareId (recipients who already accepted or rejected are not re-prompted) instead of
103
+ * minting a new share. No-op when no prior everyone-share of the project exists. Callers that
104
+ * don't care pass `false`.
105
+ */
106
+ replace: boolean;
107
+ title: string;
108
+ mode: EnvelopeMode;
109
+ };
110
+ //#endregion
111
+ export { AcceptanceFieldPrefix, EnvelopeAcceptance, EnvelopeData, EnvelopeMode, EnvelopeProject, ProjectChangeAction, ProjectFieldUuid, ShareId, ShareProjectsOptions, SharedEnvelopeResourceType, SharingDecision, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, decisionField, decodeEnvelopeData, isAcceptanceField, newShareId };
112
+ //# 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;;UAY5B,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;AAvCN;AAAA,cA2ClB,aAAA,GAAiB,OAAgB,EAAP,OAAO;AAAA,UAE7B,eAAA;EACf,QAAA;EACA,SAAA;EACA,gBAAA;EACA,gBAAA;AAAA;;;AArCkD;AAYpD;;;;cAmCa,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;EAvD8C;AAAA;AAIpD;;;;EA0DM,OAAA;EACA,KAAA;EACA,IAAA,EAAM,YAAY;AAAA"}