@beyondwork/docx-react-component 1.0.40 → 1.0.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.
- package/package.json +13 -1
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +347 -4
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +568 -1
- package/src/index.ts +118 -1
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +271 -0
- package/src/io/ooxml/workflow-payload.ts +146 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +280 -93
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +122 -12
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +230 -34
- package/src/runtime/layout/public-facet.ts +185 -13
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +587 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +11 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +58 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +15 -13
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +32 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +293 -34
package/src/index.ts
CHANGED
|
@@ -8,7 +8,114 @@ export {
|
|
|
8
8
|
EDITOR_SESSION_STATE_VERSION,
|
|
9
9
|
} from "./api/session-state.ts";
|
|
10
10
|
// R2 — issue metadata id for scope-card-overlay P1.
|
|
11
|
-
|
|
11
|
+
// K1-light — review-action metadata id for scope-card-overlay P2.
|
|
12
|
+
export { ISSUE_METADATA_ID, REVIEW_ACTION_METADATA_ID } from "./api/public-types.ts";
|
|
13
|
+
// P17 — metadata persistence error class.
|
|
14
|
+
export { MetadataResolverMissingError } from "./api/public-types.ts";
|
|
15
|
+
|
|
16
|
+
// Collab substrate (P1 – P8f + P14). See docs/plans/collab-master-plan.md
|
|
17
|
+
// for the shipped-slice table. Surfaces are stable for host integration;
|
|
18
|
+
// the chrome preset + markdown renderer land in P9 / P10.
|
|
19
|
+
export { createCollabSession } from "./runtime/collab-session.ts";
|
|
20
|
+
export { createCollabSessionBridge } from "./runtime/collab-session-bridge.ts";
|
|
21
|
+
export { createCollabSessionFacet } from "./runtime/collab-session-facet.ts";
|
|
22
|
+
export { createTamperGate } from "./runtime/tamper-gate.ts";
|
|
23
|
+
export { resignPayload } from "./runtime/resign-payload.ts";
|
|
24
|
+
export {
|
|
25
|
+
setLocalIdentity,
|
|
26
|
+
clearLocalIdentity,
|
|
27
|
+
getPresenceSnapshot,
|
|
28
|
+
getCollabPosture,
|
|
29
|
+
} from "./runtime/awareness-identity.ts";
|
|
30
|
+
export { runtimeSendToExternal } from "./runtime/external-send-runtime.ts";
|
|
31
|
+
export { sendToExternal } from "./io/export/external-send.ts";
|
|
32
|
+
export { maybeRestoreFromExternalCustody } from "./io/import/external-reimport.ts";
|
|
33
|
+
export {
|
|
34
|
+
signWorkflowPayloadXml,
|
|
35
|
+
verifyWorkflowPayloadXml,
|
|
36
|
+
createHmacSigner,
|
|
37
|
+
createHmacVerifier,
|
|
38
|
+
} from "./io/ooxml/payload-signature.ts";
|
|
39
|
+
export type {
|
|
40
|
+
// Collab types
|
|
41
|
+
CollabSession,
|
|
42
|
+
CollabSessionOptions,
|
|
43
|
+
CollabSessionEventOrIntegrity,
|
|
44
|
+
AttachPayloadArgs,
|
|
45
|
+
SendToExternalCallArgs,
|
|
46
|
+
} from "./runtime/collab-session.ts";
|
|
47
|
+
export type {
|
|
48
|
+
CollabSessionBridge,
|
|
49
|
+
CollabSessionEvent,
|
|
50
|
+
CreateCollabSessionBridgeOptions,
|
|
51
|
+
} from "./runtime/collab-session-bridge.ts";
|
|
52
|
+
export type {
|
|
53
|
+
CollabSessionFacet,
|
|
54
|
+
DispatchContext,
|
|
55
|
+
DispatchResult,
|
|
56
|
+
} from "./runtime/collab-session-facet.ts";
|
|
57
|
+
export type {
|
|
58
|
+
TamperGate,
|
|
59
|
+
TamperGateEvent,
|
|
60
|
+
MetadataIntegrity,
|
|
61
|
+
TamperGateArgs,
|
|
62
|
+
AttachArgs as TamperGateAttachArgs,
|
|
63
|
+
GuardResult as TamperGateGuardResult,
|
|
64
|
+
} from "./runtime/tamper-gate.ts";
|
|
65
|
+
export type {
|
|
66
|
+
RuntimeSendToExternalArgs,
|
|
67
|
+
RuntimeSendToExternalResult,
|
|
68
|
+
} from "./runtime/external-send-runtime.ts";
|
|
69
|
+
export type {
|
|
70
|
+
CommentNegotiationAction,
|
|
71
|
+
CommentNegotiationEntry,
|
|
72
|
+
CommentNegotiationSnapshot,
|
|
73
|
+
CommentNegotiationState,
|
|
74
|
+
CommentNegotiationActionType,
|
|
75
|
+
NegotiationHistoryRow,
|
|
76
|
+
NegotiationVote,
|
|
77
|
+
NegotiationCounterProposal,
|
|
78
|
+
NegotiationBlockReason,
|
|
79
|
+
NegotiationRole,
|
|
80
|
+
CollabBlockReason,
|
|
81
|
+
} from "./api/comment-negotiation-types.ts";
|
|
82
|
+
export type {
|
|
83
|
+
CommentPresentation,
|
|
84
|
+
CommentPresentationAction,
|
|
85
|
+
CommentPresentationSnapshot,
|
|
86
|
+
CommentPresentationReply,
|
|
87
|
+
CommentBody,
|
|
88
|
+
CommentAttachment,
|
|
89
|
+
CommentAudience,
|
|
90
|
+
CommentLabel,
|
|
91
|
+
CommentMention,
|
|
92
|
+
CommentReaction,
|
|
93
|
+
} from "./api/comment-presentation-types.ts";
|
|
94
|
+
export type {
|
|
95
|
+
Participant,
|
|
96
|
+
ParticipantRoster,
|
|
97
|
+
ParticipantRole,
|
|
98
|
+
AuthorKind,
|
|
99
|
+
} from "./api/participants-types.ts";
|
|
100
|
+
export type {
|
|
101
|
+
AwarenessIdentity,
|
|
102
|
+
AwarenessPeer,
|
|
103
|
+
CollabPosture,
|
|
104
|
+
PresenceSnapshot,
|
|
105
|
+
TransportStatus,
|
|
106
|
+
} from "./api/awareness-identity-types.ts";
|
|
107
|
+
export type {
|
|
108
|
+
ExternalCustody,
|
|
109
|
+
ExternalCustodyResolver,
|
|
110
|
+
ExternalCustodyArchivePayload,
|
|
111
|
+
ExternalCustodyRestoredContent,
|
|
112
|
+
} from "./api/external-custody-types.ts";
|
|
113
|
+
export type {
|
|
114
|
+
PayloadSignature,
|
|
115
|
+
PayloadSigner,
|
|
116
|
+
PayloadVerifier,
|
|
117
|
+
SignatureAlgorithm,
|
|
118
|
+
} from "./io/ooxml/payload-signature.ts";
|
|
12
119
|
export type {
|
|
13
120
|
LoadRequest,
|
|
14
121
|
LoadSourcePolicy,
|
|
@@ -113,6 +220,11 @@ export type {
|
|
|
113
220
|
IssueMetadataValue,
|
|
114
221
|
ScopeIssueAction,
|
|
115
222
|
ScopeCardModel,
|
|
223
|
+
// R3 — suggestion groups (scope-card-overlay P2)
|
|
224
|
+
SuggestionGroup,
|
|
225
|
+
// K1-light — review-action audit trail (scope-card-overlay P2)
|
|
226
|
+
ReviewActionKind,
|
|
227
|
+
ReviewActionMetadataValue,
|
|
116
228
|
WorkflowBlockedCommandReason,
|
|
117
229
|
WorkflowScopeSnapshot,
|
|
118
230
|
InteractionGuardSnapshot,
|
|
@@ -150,4 +262,9 @@ export type {
|
|
|
150
262
|
RuntimeContextAnalyticsUnavailableField,
|
|
151
263
|
RuntimeContextAnalyticsProvenance,
|
|
152
264
|
RuntimeContextAnalyticsSnapshot,
|
|
265
|
+
// P17 — metadata persistence types (schema 1.1)
|
|
266
|
+
MetadataPersistenceMode,
|
|
267
|
+
ScopeMetadataPersistence,
|
|
268
|
+
ScopeMetadataResolver,
|
|
269
|
+
ScopeMetadataStorageRef,
|
|
153
270
|
} from "./api/public-types.ts";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape a string for safe use inside a double-quoted XML attribute value.
|
|
3
|
+
*
|
|
4
|
+
* Handles the minimum set XML 1.0 §2.3 requires to survive an attribute
|
|
5
|
+
* parser: `&` (otherwise any `&foo;` sequence becomes an entity reference),
|
|
6
|
+
* `<` (not permitted in any attribute value), and `"` (the delimiter
|
|
7
|
+
* itself). Escaping `>` is not strictly required but matches the pattern
|
|
8
|
+
* already shipped by every serializer in this directory — keep doing it
|
|
9
|
+
* so the output remains byte-identical to the previous per-file copies.
|
|
10
|
+
*
|
|
11
|
+
* Rationale: before this helper existed, eight files under `src/io/export/`
|
|
12
|
+
* carried private copies of the same 3-or-4-line function
|
|
13
|
+
* (`serialize-{comments,footnotes,headers-footers,numbering,main-document,
|
|
14
|
+
* runtime-revisions,tables}.ts` + `table-properties-xml.ts`). Two of them
|
|
15
|
+
* had a subtle variant (`escapeXml(value).replace('"', '"')`) that
|
|
16
|
+
* composed with a local `escapeXml` copy; the end state was equivalent for
|
|
17
|
+
* every observed input, and this single implementation keeps that
|
|
18
|
+
* behaviour while removing seven copies.
|
|
19
|
+
*/
|
|
20
|
+
export function escapeXmlAttribute(value: string): string {
|
|
21
|
+
return value
|
|
22
|
+
.replace(/&/gu, "&")
|
|
23
|
+
.replace(/</gu, "<")
|
|
24
|
+
.replace(/>/gu, ">")
|
|
25
|
+
.replace(/"/gu, """);
|
|
26
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommentNegotiationSnapshot,
|
|
3
|
+
} from "../../api/comment-negotiation-types.ts";
|
|
4
|
+
import type {
|
|
5
|
+
CommentPresentationSnapshot,
|
|
6
|
+
} from "../../api/comment-presentation-types.ts";
|
|
7
|
+
import type {
|
|
8
|
+
ExternalCustody,
|
|
9
|
+
ExternalCustodyResolver,
|
|
10
|
+
} from "../../api/external-custody-types.ts";
|
|
11
|
+
import type { ParticipantRoster } from "../../api/participants-types.ts";
|
|
12
|
+
|
|
13
|
+
export interface SendToExternalArgs {
|
|
14
|
+
/** Snapshot taken from the runtime before the call. */
|
|
15
|
+
presentation: CommentPresentationSnapshot;
|
|
16
|
+
negotiation: CommentNegotiationSnapshot;
|
|
17
|
+
participants: ParticipantRoster;
|
|
18
|
+
|
|
19
|
+
/** Current runtime role — author-only action. */
|
|
20
|
+
role: "author" | "reviewer" | "observer";
|
|
21
|
+
/** Blocks send if the runtime flagged the payload as tampered. */
|
|
22
|
+
metadataIntegrity: "unsigned" | "verified" | "tampered";
|
|
23
|
+
|
|
24
|
+
/** Origin document identity, used to populate the custody receipt. */
|
|
25
|
+
originDocumentId: string;
|
|
26
|
+
originPayloadId: string;
|
|
27
|
+
/** sha256:{hex} of canonicalized word/document.xml at send time. */
|
|
28
|
+
originContentHash: string;
|
|
29
|
+
|
|
30
|
+
/** Host-provided archive+restore contract. */
|
|
31
|
+
resolver: ExternalCustodyResolver;
|
|
32
|
+
recipient: string;
|
|
33
|
+
sentBy: string;
|
|
34
|
+
archiveRef: string;
|
|
35
|
+
|
|
36
|
+
/** Optional uuid + clock overrides for deterministic tests. */
|
|
37
|
+
custodyId?: string;
|
|
38
|
+
now?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SendToExternalResult {
|
|
42
|
+
custody: ExternalCustody;
|
|
43
|
+
kept: {
|
|
44
|
+
presentation: CommentPresentationSnapshot;
|
|
45
|
+
negotiation: CommentNegotiationSnapshot;
|
|
46
|
+
participants: ParticipantRoster;
|
|
47
|
+
};
|
|
48
|
+
stripped: {
|
|
49
|
+
commentIds: string[];
|
|
50
|
+
participantIds: string[];
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SendToExternalBlock =
|
|
55
|
+
| { ok: false; reason: "collab_role_restricted" }
|
|
56
|
+
| { ok: false; reason: "metadata_tampered" }
|
|
57
|
+
| { ok: true; result: SendToExternalResult };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Pure pipeline: classify comments by audience, split the snapshots
|
|
61
|
+
* into kept vs. archived, hand archived content to the resolver, and
|
|
62
|
+
* return the kept snapshots + custody receipt for the caller to
|
|
63
|
+
* serialize into the supplier-bound docx.
|
|
64
|
+
*
|
|
65
|
+
* The caller is responsible for: serializing the kept snapshots back
|
|
66
|
+
* into `bw:extensions`, rewriting `word/comments.xml` (and the three
|
|
67
|
+
* companion parts + `word/document.xml` anchor runs) to drop the
|
|
68
|
+
* stripped `commentIds`, appending the custody receipt, and re-signing
|
|
69
|
+
* via the central `resignPayload()` hook.
|
|
70
|
+
*/
|
|
71
|
+
export async function sendToExternal(
|
|
72
|
+
args: SendToExternalArgs,
|
|
73
|
+
): Promise<SendToExternalBlock> {
|
|
74
|
+
if (args.role !== "author") {
|
|
75
|
+
return { ok: false, reason: "collab_role_restricted" };
|
|
76
|
+
}
|
|
77
|
+
if (args.metadataIntegrity === "tampered") {
|
|
78
|
+
return { ok: false, reason: "metadata_tampered" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const strippedPresentation = args.presentation.entries.filter(
|
|
82
|
+
(e) => e.audience === "internal",
|
|
83
|
+
);
|
|
84
|
+
const keptPresentation = args.presentation.entries.filter(
|
|
85
|
+
(e) => e.audience !== "internal",
|
|
86
|
+
);
|
|
87
|
+
const strippedCommentIds = strippedPresentation.map((e) => e.commentId);
|
|
88
|
+
const strippedCommentIdSet = new Set(strippedCommentIds);
|
|
89
|
+
|
|
90
|
+
const strippedNegotiation = args.negotiation.entries.filter((e) =>
|
|
91
|
+
strippedCommentIdSet.has(e.commentId),
|
|
92
|
+
);
|
|
93
|
+
const keptNegotiation = args.negotiation.entries.filter(
|
|
94
|
+
(e) => !strippedCommentIdSet.has(e.commentId),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const referencedUserIds = collectReferencedUserIds(
|
|
98
|
+
keptPresentation,
|
|
99
|
+
keptNegotiation,
|
|
100
|
+
);
|
|
101
|
+
const strippedParticipants = args.participants.entries.filter(
|
|
102
|
+
(p) => !referencedUserIds.has(p.userId),
|
|
103
|
+
);
|
|
104
|
+
const keptParticipants = args.participants.entries.filter((p) =>
|
|
105
|
+
referencedUserIds.has(p.userId),
|
|
106
|
+
);
|
|
107
|
+
const strippedParticipantIds = strippedParticipants.map((p) => p.userId);
|
|
108
|
+
|
|
109
|
+
const custody: ExternalCustody = {
|
|
110
|
+
schemaVersion: 1,
|
|
111
|
+
custodyId: args.custodyId ?? newCustodyId(),
|
|
112
|
+
originDocumentId: args.originDocumentId,
|
|
113
|
+
originPayloadId: args.originPayloadId,
|
|
114
|
+
originContentHash: args.originContentHash,
|
|
115
|
+
sentAt: args.now ?? new Date().toISOString(),
|
|
116
|
+
sentBy: args.sentBy,
|
|
117
|
+
recipient: args.recipient,
|
|
118
|
+
archiveRef: args.archiveRef,
|
|
119
|
+
strippedCommentIds,
|
|
120
|
+
strippedParticipantIds,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await args.resolver.archive({
|
|
124
|
+
custodyId: custody.custodyId,
|
|
125
|
+
strippedPresentation,
|
|
126
|
+
strippedNegotiation,
|
|
127
|
+
strippedParticipants,
|
|
128
|
+
originContentHash: args.originContentHash,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
result: {
|
|
134
|
+
custody,
|
|
135
|
+
kept: {
|
|
136
|
+
presentation: { schemaVersion: 1, entries: keptPresentation },
|
|
137
|
+
negotiation: { schemaVersion: 1, entries: keptNegotiation },
|
|
138
|
+
participants: { schemaVersion: 1, entries: keptParticipants },
|
|
139
|
+
},
|
|
140
|
+
stripped: {
|
|
141
|
+
commentIds: strippedCommentIds,
|
|
142
|
+
participantIds: strippedParticipantIds,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function collectReferencedUserIds(
|
|
149
|
+
presentation: CommentPresentationSnapshot["entries"],
|
|
150
|
+
negotiation: CommentNegotiationSnapshot["entries"],
|
|
151
|
+
): Set<string> {
|
|
152
|
+
const ids = new Set<string>();
|
|
153
|
+
for (const entry of presentation) {
|
|
154
|
+
for (const m of entry.mentions) ids.add(m.userId);
|
|
155
|
+
for (const r of entry.reactions) ids.add(r.authorId);
|
|
156
|
+
}
|
|
157
|
+
for (const entry of negotiation) {
|
|
158
|
+
for (const v of entry.votes) ids.add(v.authorId);
|
|
159
|
+
for (const a of entry.requiredApprovers) ids.add(a);
|
|
160
|
+
for (const p of entry.counterProposals) ids.add(p.authorId);
|
|
161
|
+
for (const h of entry.history) ids.add(h.actorId);
|
|
162
|
+
if (entry.lockedBy) ids.add(entry.lockedBy);
|
|
163
|
+
}
|
|
164
|
+
return ids;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function newCustodyId(): string {
|
|
168
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
169
|
+
return globalThis.crypto.randomUUID();
|
|
170
|
+
}
|
|
171
|
+
// Cryptographic fallback. custodyId is used as a lookup key against the
|
|
172
|
+
// host archive that holds stripped internal comments — a predictable id
|
|
173
|
+
// would let an attacker enumerate or guess other sessions' archives.
|
|
174
|
+
// WebCrypto's getRandomValues is in Node 18+ and every modern browser;
|
|
175
|
+
// throw if unavailable rather than fall back to Math.random().
|
|
176
|
+
if (typeof globalThis.crypto?.getRandomValues !== "function") {
|
|
177
|
+
throw new Error(
|
|
178
|
+
"newCustodyId: WebCrypto (crypto.getRandomValues) is required — refusing " +
|
|
179
|
+
"to fall back to Math.random() for a security-sensitive identifier.",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const bytes = new Uint8Array(16);
|
|
183
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
184
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
|
185
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
186
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
187
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
188
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommentEntry, CommentThread } from "../../review/store/comment-store.ts";
|
|
2
2
|
import type { RevisionParagraphBoundary } from "../ooxml/revision-boundaries.ts";
|
|
3
3
|
import type { ImportedCommentDefinition } from "../ooxml/parse-comments.ts";
|
|
4
|
+
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
4
5
|
|
|
5
6
|
interface XmlElementNode {
|
|
6
7
|
type: "element";
|
|
@@ -210,7 +211,7 @@ export function serializeMergedCommentsXml(
|
|
|
210
211
|
.sort((left, right) => left.localeCompare(right))
|
|
211
212
|
.map(
|
|
212
213
|
(authorId) =>
|
|
213
|
-
`<w15:person w15:author="${
|
|
214
|
+
`<w15:person w15:author="${escapeXmlAttribute(authorId)}" />`,
|
|
214
215
|
),
|
|
215
216
|
`</w15:people>`,
|
|
216
217
|
].join("\n")
|
|
@@ -289,17 +290,17 @@ export function serializeCommentAnchorsIntoDocumentXml(
|
|
|
289
290
|
pushInsertion(
|
|
290
291
|
insertions,
|
|
291
292
|
startIndex,
|
|
292
|
-
`<w:commentRangeStart w:id="${
|
|
293
|
+
`<w:commentRangeStart w:id="${escapeXmlAttribute(exportCommentId)}"/>`,
|
|
293
294
|
);
|
|
294
295
|
pushInsertion(
|
|
295
296
|
insertions,
|
|
296
297
|
endIndex,
|
|
297
|
-
`<w:commentRangeEnd w:id="${
|
|
298
|
+
`<w:commentRangeEnd w:id="${escapeXmlAttribute(exportCommentId)}"/>`,
|
|
298
299
|
);
|
|
299
300
|
pushInsertion(
|
|
300
301
|
insertions,
|
|
301
302
|
endIndex,
|
|
302
|
-
`<w:r><w:commentReference w:id="${
|
|
303
|
+
`<w:r><w:commentReference w:id="${escapeXmlAttribute(exportCommentId)}"/></w:r>`,
|
|
303
304
|
);
|
|
304
305
|
serializedCommentIds.push(thread.commentId);
|
|
305
306
|
}
|
|
@@ -462,12 +463,12 @@ function serializeThreadEntries(entries: readonly SerializableCommentEntry[]): s
|
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
function serializeCommentEntry(entry: SerializableCommentEntry): string {
|
|
465
|
-
const author =
|
|
466
|
-
const createdAt =
|
|
466
|
+
const author = escapeXmlAttribute(entry.entry.authorId);
|
|
467
|
+
const createdAt = escapeXmlAttribute(entry.entry.createdAt);
|
|
467
468
|
const initials = entry.entry.metadata?.initials;
|
|
468
469
|
const paragraphXml = serializeCommentParagraphs(entry.entry.body, entry.paraId);
|
|
469
470
|
|
|
470
|
-
return `<w:comment w:id="${
|
|
471
|
+
return `<w:comment w:id="${escapeXmlAttribute(entry.exportCommentId)}"${initials ? ` w:initials="${escapeXmlAttribute(initials)}"` : ""} w:author="${author}" w:date="${createdAt}">${paragraphXml}</w:comment>`;
|
|
471
472
|
}
|
|
472
473
|
|
|
473
474
|
function serializeCommentParagraphs(body: string, paraId: string): string {
|
|
@@ -477,7 +478,7 @@ function serializeCommentParagraphs(body: string, paraId: string): string {
|
|
|
477
478
|
.map((paragraph, index) => {
|
|
478
479
|
const attributes =
|
|
479
480
|
index === 0
|
|
480
|
-
? ` w14:paraId="${
|
|
481
|
+
? ` w14:paraId="${escapeXmlAttribute(paraId)}" w14:textId="${escapeXmlAttribute(textId)}"`
|
|
481
482
|
: "";
|
|
482
483
|
return `<w:p${attributes}><w:r>${serializeText(paragraph)}</w:r></w:p>`;
|
|
483
484
|
})
|
|
@@ -500,13 +501,13 @@ function serializePreservedCommentExtension(
|
|
|
500
501
|
definition: ImportedCommentDefinition,
|
|
501
502
|
): string {
|
|
502
503
|
const doneValue = definition.isDone ? "true" : "false";
|
|
503
|
-
return `<w15:commentEx w15:paraId="${
|
|
504
|
+
return `<w15:commentEx w15:paraId="${escapeXmlAttribute(definition.paraId!)}"${definition.parentParaId ? ` w15:paraIdParent="${escapeXmlAttribute(definition.parentParaId)}"` : ""} w15:done="${doneValue}" />`;
|
|
504
505
|
}
|
|
505
506
|
|
|
506
507
|
function serializeCommentExtension(
|
|
507
508
|
entry: SerializableCommentEntry,
|
|
508
509
|
): string | undefined {
|
|
509
|
-
return `<w15:commentEx w15:paraId="${
|
|
510
|
+
return `<w15:commentEx w15:paraId="${escapeXmlAttribute(entry.paraId)}"${entry.isRoot ? "" : ` w15:paraIdParent="${escapeXmlAttribute(findRootParaId(entry.thread, entry.paraId))}"`} w15:done="${entry.isRoot && entry.thread.status === "resolved" ? "true" : "false"}" />`;
|
|
510
511
|
}
|
|
511
512
|
|
|
512
513
|
function serializePreservedCommentIds(
|
|
@@ -515,7 +516,7 @@ function serializePreservedCommentIds(
|
|
|
515
516
|
return definitions
|
|
516
517
|
.map((definition) =>
|
|
517
518
|
definition.paraId && definition.durableId
|
|
518
|
-
? `<w16cid:commentId w16cid:paraId="${
|
|
519
|
+
? `<w16cid:commentId w16cid:paraId="${escapeXmlAttribute(definition.paraId)}" w16cid:durableId="${escapeXmlAttribute(definition.durableId)}" />`
|
|
519
520
|
: undefined,
|
|
520
521
|
)
|
|
521
522
|
.filter((xml): xml is string => Boolean(xml));
|
|
@@ -528,7 +529,7 @@ function serializeCommentDurableId(
|
|
|
528
529
|
return undefined;
|
|
529
530
|
}
|
|
530
531
|
|
|
531
|
-
return `<w16cid:commentId w16cid:paraId="${
|
|
532
|
+
return `<w16cid:commentId w16cid:paraId="${escapeXmlAttribute(entry.paraId)}" w16cid:durableId="${escapeXmlAttribute(entry.durableId)}" />`;
|
|
532
533
|
}
|
|
533
534
|
|
|
534
535
|
function getRootEntry(thread: CommentThread): CommentEntry | undefined {
|
|
@@ -794,10 +795,6 @@ function escapeXml(value: string): string {
|
|
|
794
795
|
.replace(/>/g, ">");
|
|
795
796
|
}
|
|
796
797
|
|
|
797
|
-
function escapeAttribute(value: string): string {
|
|
798
|
-
return escapeXml(value).replace(/"/g, """);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
798
|
function openingTagLength(xml: string, start: number): number {
|
|
802
799
|
const end = xml.indexOf(">", start);
|
|
803
800
|
if (end < 0) {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
serializeTableRowPropertiesXml,
|
|
18
18
|
} from "./table-properties-xml.ts";
|
|
19
19
|
import { twip } from "./twip.ts";
|
|
20
|
+
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
20
21
|
|
|
21
22
|
export const WORD_FOOTNOTES_CONTENT_TYPE =
|
|
22
23
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml";
|
|
@@ -104,7 +105,7 @@ function serializeNoteDefinition(
|
|
|
104
105
|
.join("");
|
|
105
106
|
|
|
106
107
|
const body = blocks || `<w:p><w:r><w:t></w:t></w:r></w:p>`;
|
|
107
|
-
const noteXml = `<${tag} w:id="${
|
|
108
|
+
const noteXml = `<${tag} w:id="${escapeXmlAttribute(definition.noteId)}">${body}</${tag}>`;
|
|
108
109
|
if (revisions.length === 0) {
|
|
109
110
|
return noteXml;
|
|
110
111
|
}
|
|
@@ -188,17 +189,17 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
188
189
|
const parts: string[] = [];
|
|
189
190
|
|
|
190
191
|
if (paragraph.styleId) {
|
|
191
|
-
parts.push(`<w:pStyle w:val="${
|
|
192
|
+
parts.push(`<w:pStyle w:val="${escapeXmlAttribute(paragraph.styleId)}"/>`);
|
|
192
193
|
}
|
|
193
194
|
if (paragraph.alignment) {
|
|
194
|
-
parts.push(`<w:jc w:val="${
|
|
195
|
+
parts.push(`<w:jc w:val="${escapeXmlAttribute(paragraph.alignment)}"/>`);
|
|
195
196
|
}
|
|
196
197
|
if (paragraph.spacing) {
|
|
197
198
|
const attrs: string[] = [];
|
|
198
199
|
if (paragraph.spacing.before !== undefined) attrs.push(`w:before="${twip(paragraph.spacing.before)}"`);
|
|
199
200
|
if (paragraph.spacing.after !== undefined) attrs.push(`w:after="${twip(paragraph.spacing.after)}"`);
|
|
200
201
|
if (paragraph.spacing.line !== undefined) attrs.push(`w:line="${twip(paragraph.spacing.line)}"`);
|
|
201
|
-
if (paragraph.spacing.lineRule) attrs.push(`w:lineRule="${
|
|
202
|
+
if (paragraph.spacing.lineRule) attrs.push(`w:lineRule="${escapeXmlAttribute(paragraph.spacing.lineRule)}"`);
|
|
202
203
|
if (attrs.length > 0) {
|
|
203
204
|
parts.push(`<w:spacing ${attrs.join(" ")}/>`);
|
|
204
205
|
}
|
|
@@ -233,8 +234,8 @@ function serializeInlineNode(node: InlineNode): string {
|
|
|
233
234
|
case "footnote_ref": {
|
|
234
235
|
const refElement =
|
|
235
236
|
node.noteKind === "footnote"
|
|
236
|
-
? `<w:footnoteReference w:id="${
|
|
237
|
-
: `<w:endnoteReference w:id="${
|
|
237
|
+
? `<w:footnoteReference w:id="${escapeXmlAttribute(node.noteId)}"/>`
|
|
238
|
+
: `<w:endnoteReference w:id="${escapeXmlAttribute(node.noteId)}"/>`;
|
|
238
239
|
const styleVal =
|
|
239
240
|
node.noteKind === "footnote"
|
|
240
241
|
? "FootnoteReference"
|
|
@@ -242,9 +243,9 @@ function serializeInlineNode(node: InlineNode): string {
|
|
|
242
243
|
return `<w:r><w:rPr><w:rStyle w:val="${styleVal}"/></w:rPr>${refElement}</w:r>`;
|
|
243
244
|
}
|
|
244
245
|
case "bookmark_start":
|
|
245
|
-
return `<w:bookmarkStart w:id="${
|
|
246
|
+
return `<w:bookmarkStart w:id="${escapeXmlAttribute(node.bookmarkId)}" w:name="${escapeXmlAttribute(node.name)}"/>`;
|
|
246
247
|
case "bookmark_end":
|
|
247
|
-
return `<w:bookmarkEnd w:id="${
|
|
248
|
+
return `<w:bookmarkEnd w:id="${escapeXmlAttribute(node.bookmarkId)}"/>`;
|
|
248
249
|
case "field":
|
|
249
250
|
if (node.children && node.children.length > 0) {
|
|
250
251
|
const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
|
|
@@ -257,10 +258,10 @@ function serializeInlineNode(node: InlineNode): string {
|
|
|
257
258
|
`<w:r><w:fldChar w:fldCharType="end"/></w:r>`
|
|
258
259
|
);
|
|
259
260
|
}
|
|
260
|
-
return `<w:fldSimple w:instr="${
|
|
261
|
+
return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
|
|
261
262
|
}
|
|
262
263
|
if (node.fieldType === "simple") {
|
|
263
|
-
return `<w:fldSimple w:instr="${
|
|
264
|
+
return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}"/>`;
|
|
264
265
|
}
|
|
265
266
|
return (
|
|
266
267
|
`<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
|
|
@@ -282,10 +283,10 @@ function serializeInlineNode(node: InlineNode): string {
|
|
|
282
283
|
`<w:r><w:fldChar w:fldCharType="end"/></w:r>`
|
|
283
284
|
);
|
|
284
285
|
}
|
|
285
|
-
return `<w:fldSimple w:instr="${
|
|
286
|
+
return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
|
|
286
287
|
}
|
|
287
288
|
if (node.fieldType === "simple") {
|
|
288
|
-
return `<w:fldSimple w:instr="${
|
|
289
|
+
return `<w:fldSimple w:instr="${escapeXmlAttribute(node.instruction)}"/>`;
|
|
289
290
|
}
|
|
290
291
|
return (
|
|
291
292
|
`<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
|
|
@@ -327,14 +328,14 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
|
|
|
327
328
|
break;
|
|
328
329
|
case "fontFamily":
|
|
329
330
|
parts.push(
|
|
330
|
-
`<w:rFonts w:ascii="${
|
|
331
|
+
`<w:rFonts w:ascii="${escapeXmlAttribute(mark.val)}" w:hAnsi="${escapeXmlAttribute(mark.val)}"/>`,
|
|
331
332
|
);
|
|
332
333
|
break;
|
|
333
334
|
case "fontSize":
|
|
334
335
|
parts.push(`<w:sz w:val="${twip(mark.val)}"/>`);
|
|
335
336
|
break;
|
|
336
337
|
case "textColor":
|
|
337
|
-
parts.push(`<w:color w:val="${
|
|
338
|
+
parts.push(`<w:color w:val="${escapeXmlAttribute(mark.color)}"/>`);
|
|
338
339
|
break;
|
|
339
340
|
case "smallCaps":
|
|
340
341
|
parts.push("<w:smallCaps/>");
|
|
@@ -371,23 +372,15 @@ function escapeXml(text: string): string {
|
|
|
371
372
|
.replace(/>/g, ">");
|
|
372
373
|
}
|
|
373
374
|
|
|
374
|
-
function escapeAttribute(value: string): string {
|
|
375
|
-
return value
|
|
376
|
-
.replace(/&/g, "&")
|
|
377
|
-
.replace(/</g, "<")
|
|
378
|
-
.replace(/>/g, ">")
|
|
379
|
-
.replace(/"/g, """);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
375
|
function serializeHyperlinkNode(node: Extract<InlineNode, { type: "hyperlink" }>): string {
|
|
383
376
|
const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
|
|
384
377
|
if (node.href.startsWith("#")) {
|
|
385
|
-
return `<w:hyperlink w:anchor="${
|
|
378
|
+
return `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">${childrenXml}</w:hyperlink>`;
|
|
386
379
|
}
|
|
387
380
|
if (!/^rId[A-Za-z0-9._-]+$/u.test(node.href)) {
|
|
388
381
|
throw new Error("Cannot safely serialize URL-backed note hyperlinks without relationship context.");
|
|
389
382
|
}
|
|
390
|
-
return `<w:hyperlink r:id="${
|
|
383
|
+
return `<w:hyperlink r:id="${escapeXmlAttribute(node.href)}">${childrenXml}</w:hyperlink>`;
|
|
391
384
|
}
|
|
392
385
|
|
|
393
386
|
function buildTableCellPropertiesXml(cell: TableCellNode): string {
|