@printwithsynergy/lens-pdf 0.3.0-beta.81

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 (213) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +344 -0
  3. package/dist/browser/codexOverlay.d.ts +109 -0
  4. package/dist/browser/codexOverlay.d.ts.map +1 -0
  5. package/dist/browser/codexOverlay.js +256 -0
  6. package/dist/browser/codexOverlay.js.map +1 -0
  7. package/dist/browser/constants.d.ts +13 -0
  8. package/dist/browser/constants.d.ts.map +1 -0
  9. package/dist/browser/constants.js +13 -0
  10. package/dist/browser/constants.js.map +1 -0
  11. package/dist/browser/index.d.ts +211 -0
  12. package/dist/browser/index.d.ts.map +1 -0
  13. package/dist/browser/index.js +1190 -0
  14. package/dist/browser/index.js.map +1 -0
  15. package/dist/browser/pantone-gold.d.ts +59 -0
  16. package/dist/browser/pantone-gold.d.ts.map +1 -0
  17. package/dist/browser/pantone-gold.js +237 -0
  18. package/dist/browser/pantone-gold.js.map +1 -0
  19. package/dist/components/AnnotationCanvas.d.ts +27 -0
  20. package/dist/components/AnnotationCanvas.d.ts.map +1 -0
  21. package/dist/components/AnnotationCanvas.js +401 -0
  22. package/dist/components/AnnotationCanvas.js.map +1 -0
  23. package/dist/components/AnnotationNotesPanel.d.ts +15 -0
  24. package/dist/components/AnnotationNotesPanel.d.ts.map +1 -0
  25. package/dist/components/AnnotationNotesPanel.js +235 -0
  26. package/dist/components/AnnotationNotesPanel.js.map +1 -0
  27. package/dist/components/AnnotationThread.d.ts +18 -0
  28. package/dist/components/AnnotationThread.d.ts.map +1 -0
  29. package/dist/components/AnnotationThread.js +163 -0
  30. package/dist/components/AnnotationThread.js.map +1 -0
  31. package/dist/components/AnnotationToolbar.d.ts +39 -0
  32. package/dist/components/AnnotationToolbar.d.ts.map +1 -0
  33. package/dist/components/AnnotationToolbar.js +258 -0
  34. package/dist/components/AnnotationToolbar.js.map +1 -0
  35. package/dist/components/BoxOverlay.d.ts +20 -0
  36. package/dist/components/BoxOverlay.d.ts.map +1 -0
  37. package/dist/components/BoxOverlay.js +107 -0
  38. package/dist/components/BoxOverlay.js.map +1 -0
  39. package/dist/components/ColorPickerTool.d.ts +11 -0
  40. package/dist/components/ColorPickerTool.d.ts.map +1 -0
  41. package/dist/components/ColorPickerTool.js +220 -0
  42. package/dist/components/ColorPickerTool.js.map +1 -0
  43. package/dist/components/DensitometerTool.d.ts +25 -0
  44. package/dist/components/DensitometerTool.d.ts.map +1 -0
  45. package/dist/components/DensitometerTool.js +246 -0
  46. package/dist/components/DensitometerTool.js.map +1 -0
  47. package/dist/components/DielineInfoPanel.d.ts +27 -0
  48. package/dist/components/DielineInfoPanel.d.ts.map +1 -0
  49. package/dist/components/DielineInfoPanel.js +23 -0
  50. package/dist/components/DielineInfoPanel.js.map +1 -0
  51. package/dist/components/DielineOverlay.d.ts +10 -0
  52. package/dist/components/DielineOverlay.d.ts.map +1 -0
  53. package/dist/components/DielineOverlay.js +57 -0
  54. package/dist/components/DielineOverlay.js.map +1 -0
  55. package/dist/components/FindingsSidebar.d.ts +50 -0
  56. package/dist/components/FindingsSidebar.d.ts.map +1 -0
  57. package/dist/components/FindingsSidebar.js +78 -0
  58. package/dist/components/FindingsSidebar.js.map +1 -0
  59. package/dist/components/LayerCanvas.d.ts +30 -0
  60. package/dist/components/LayerCanvas.d.ts.map +1 -0
  61. package/dist/components/LayerCanvas.js +84 -0
  62. package/dist/components/LayerCanvas.js.map +1 -0
  63. package/dist/components/LayerPanel.d.ts +9 -0
  64. package/dist/components/LayerPanel.d.ts.map +1 -0
  65. package/dist/components/LayerPanel.js +144 -0
  66. package/dist/components/LayerPanel.js.map +1 -0
  67. package/dist/components/LensPDF.d.ts +61 -0
  68. package/dist/components/LensPDF.d.ts.map +1 -0
  69. package/dist/components/LensPDF.js +49 -0
  70. package/dist/components/LensPDF.js.map +1 -0
  71. package/dist/components/LensPDFDemo.d.ts +160 -0
  72. package/dist/components/LensPDFDemo.d.ts.map +1 -0
  73. package/dist/components/LensPDFDemo.js +1060 -0
  74. package/dist/components/LensPDFDemo.js.map +1 -0
  75. package/dist/components/LensPDFDemo.styles.d.ts +38 -0
  76. package/dist/components/LensPDFDemo.styles.d.ts.map +1 -0
  77. package/dist/components/LensPDFDemo.styles.js +282 -0
  78. package/dist/components/LensPDFDemo.styles.js.map +1 -0
  79. package/dist/components/LensPDFViewer.d.ts +79 -0
  80. package/dist/components/LensPDFViewer.d.ts.map +1 -0
  81. package/dist/components/LensPDFViewer.js +254 -0
  82. package/dist/components/LensPDFViewer.js.map +1 -0
  83. package/dist/components/MeasureTool.d.ts +16 -0
  84. package/dist/components/MeasureTool.d.ts.map +1 -0
  85. package/dist/components/MeasureTool.js +137 -0
  86. package/dist/components/MeasureTool.js.map +1 -0
  87. package/dist/components/MobileBottomSheet.d.ts +12 -0
  88. package/dist/components/MobileBottomSheet.d.ts.map +1 -0
  89. package/dist/components/MobileBottomSheet.js +113 -0
  90. package/dist/components/MobileBottomSheet.js.map +1 -0
  91. package/dist/components/MobileDrawer.d.ts +31 -0
  92. package/dist/components/MobileDrawer.d.ts.map +1 -0
  93. package/dist/components/MobileDrawer.js +67 -0
  94. package/dist/components/MobileDrawer.js.map +1 -0
  95. package/dist/components/PageCanvas.d.ts +33 -0
  96. package/dist/components/PageCanvas.d.ts.map +1 -0
  97. package/dist/components/PageCanvas.js +385 -0
  98. package/dist/components/PageCanvas.js.map +1 -0
  99. package/dist/components/PageNavigator.d.ts +18 -0
  100. package/dist/components/PageNavigator.d.ts.map +1 -0
  101. package/dist/components/PageNavigator.js +44 -0
  102. package/dist/components/PageNavigator.js.map +1 -0
  103. package/dist/components/SeparationCanvas.d.ts +12 -0
  104. package/dist/components/SeparationCanvas.d.ts.map +1 -0
  105. package/dist/components/SeparationCanvas.js +174 -0
  106. package/dist/components/SeparationCanvas.js.map +1 -0
  107. package/dist/components/TACHeatmapOverlay.d.ts +17 -0
  108. package/dist/components/TACHeatmapOverlay.d.ts.map +1 -0
  109. package/dist/components/TACHeatmapOverlay.js +119 -0
  110. package/dist/components/TACHeatmapOverlay.js.map +1 -0
  111. package/dist/components/ZoomControls.d.ts +11 -0
  112. package/dist/components/ZoomControls.d.ts.map +1 -0
  113. package/dist/components/ZoomControls.js +26 -0
  114. package/dist/components/ZoomControls.js.map +1 -0
  115. package/dist/components/defaultShellPlugins.d.ts +3 -0
  116. package/dist/components/defaultShellPlugins.d.ts.map +1 -0
  117. package/dist/components/defaultShellPlugins.js +273 -0
  118. package/dist/components/defaultShellPlugins.js.map +1 -0
  119. package/dist/components/index.d.ts +32 -0
  120. package/dist/components/index.d.ts.map +1 -0
  121. package/dist/components/index.js +32 -0
  122. package/dist/components/index.js.map +1 -0
  123. package/dist/components/presets.d.ts +8 -0
  124. package/dist/components/presets.d.ts.map +1 -0
  125. package/dist/components/presets.js +14 -0
  126. package/dist/components/presets.js.map +1 -0
  127. package/dist/components/shellPlugins.d.ts +105 -0
  128. package/dist/components/shellPlugins.d.ts.map +1 -0
  129. package/dist/components/shellPlugins.js +52 -0
  130. package/dist/components/shellPlugins.js.map +1 -0
  131. package/dist/components/useIsMobile.d.ts +16 -0
  132. package/dist/components/useIsMobile.d.ts.map +1 -0
  133. package/dist/components/useIsMobile.js +30 -0
  134. package/dist/components/useIsMobile.js.map +1 -0
  135. package/dist/fallback-pdfjs/index.d.ts +60 -0
  136. package/dist/fallback-pdfjs/index.d.ts.map +1 -0
  137. package/dist/fallback-pdfjs/index.js +163 -0
  138. package/dist/fallback-pdfjs/index.js.map +1 -0
  139. package/dist/host/LensPDFProvider.d.ts +36 -0
  140. package/dist/host/LensPDFProvider.d.ts.map +1 -0
  141. package/dist/host/LensPDFProvider.js +12 -0
  142. package/dist/host/LensPDFProvider.js.map +1 -0
  143. package/dist/host/index.d.ts +167 -0
  144. package/dist/host/index.d.ts.map +1 -0
  145. package/dist/host/index.js +173 -0
  146. package/dist/host/index.js.map +1 -0
  147. package/dist/host/pdfFallback.d.ts +50 -0
  148. package/dist/host/pdfFallback.d.ts.map +1 -0
  149. package/dist/host/pdfFallback.js +171 -0
  150. package/dist/host/pdfFallback.js.map +1 -0
  151. package/dist/host/pdfValidation.d.ts +45 -0
  152. package/dist/host/pdfValidation.d.ts.map +1 -0
  153. package/dist/host/pdfValidation.js +78 -0
  154. package/dist/host/pdfValidation.js.map +1 -0
  155. package/dist/host/shareLink.d.ts +80 -0
  156. package/dist/host/shareLink.d.ts.map +1 -0
  157. package/dist/host/shareLink.js +114 -0
  158. package/dist/host/shareLink.js.map +1 -0
  159. package/dist/host/useLensPDF.d.ts +73 -0
  160. package/dist/host/useLensPDF.d.ts.map +1 -0
  161. package/dist/host/useLensPDF.js +213 -0
  162. package/dist/host/useLensPDF.js.map +1 -0
  163. package/dist/index.d.ts +68 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +62 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/plugin/context.d.ts +70 -0
  168. package/dist/plugin/context.d.ts.map +1 -0
  169. package/dist/plugin/context.js +16 -0
  170. package/dist/plugin/context.js.map +1 -0
  171. package/dist/plugin/findings-location.d.ts +53 -0
  172. package/dist/plugin/findings-location.d.ts.map +1 -0
  173. package/dist/plugin/findings-location.js +72 -0
  174. package/dist/plugin/findings-location.js.map +1 -0
  175. package/dist/plugin/index.d.ts +19 -0
  176. package/dist/plugin/index.d.ts.map +1 -0
  177. package/dist/plugin/index.js +16 -0
  178. package/dist/plugin/index.js.map +1 -0
  179. package/dist/plugin/registry.d.ts +61 -0
  180. package/dist/plugin/registry.d.ts.map +1 -0
  181. package/dist/plugin/registry.js +102 -0
  182. package/dist/plugin/registry.js.map +1 -0
  183. package/dist/plugin/services.d.ts +380 -0
  184. package/dist/plugin/services.d.ts.map +1 -0
  185. package/dist/plugin/services.js +104 -0
  186. package/dist/plugin/services.js.map +1 -0
  187. package/dist/plugin/types.d.ts +198 -0
  188. package/dist/plugin/types.d.ts.map +1 -0
  189. package/dist/plugin/types.js +24 -0
  190. package/dist/plugin/types.js.map +1 -0
  191. package/dist/types/index.d.ts +191 -0
  192. package/dist/types/index.d.ts.map +1 -0
  193. package/dist/types/index.js +95 -0
  194. package/dist/types/index.js.map +1 -0
  195. package/dist/units/index.d.ts +64 -0
  196. package/dist/units/index.d.ts.map +1 -0
  197. package/dist/units/index.js +98 -0
  198. package/dist/units/index.js.map +1 -0
  199. package/docs/architecture.md +90 -0
  200. package/docs/components.md +569 -0
  201. package/docs/contributing.md +78 -0
  202. package/docs/fallback.md +174 -0
  203. package/docs/lens-pdf-viewer.md +128 -0
  204. package/docs/licensing.md +78 -0
  205. package/docs/measurement-units.md +87 -0
  206. package/docs/plugins.md +256 -0
  207. package/docs/security.md +69 -0
  208. package/docs/server.md +212 -0
  209. package/docs/services.md +210 -0
  210. package/docs/share-links.md +111 -0
  211. package/docs/theming.md +164 -0
  212. package/docs/validation.md +83 -0
  213. package/package.json +139 -0
@@ -0,0 +1,256 @@
1
+ ---
2
+ title: "Plugin model"
3
+ description: "Slot identifiers, plugin shapes, and registration semantics. Includes the replaces mechanism for shadowing first-party plugins with third-party drop-in alternatives."
4
+ group: "Reference"
5
+ order: 6
6
+ ---
7
+
8
+ # Plugin model
9
+
10
+ LensPDF mounts plugins into nine slots:
11
+
12
+ - `overlay.canvas` — drawn on top of the page tile.
13
+ - `panel.right`, `panel.left`, `panel.bottom` — side / bottom panels.
14
+ - `toolbar.top`, `toolbar.left`, `toolbar.bottom` — toolbar pills.
15
+ - `annotation.source` — non-visual; supplies annotation data via
16
+ `AnnotationSourceProvider`.
17
+ - `dialog.modal` — modal dialog launched from another plugin.
18
+
19
+ ## The manifest
20
+
21
+ Every plugin shares a manifest:
22
+
23
+ ```ts
24
+ interface ViewerPluginManifest {
25
+ id: string; // "vendor.area.feature"
26
+ version: string; // semver — bump on protocol-affecting changes
27
+ slot: ViewerSlot;
28
+ replaces?: string; // shadow another plugin's id in slot lookups
29
+ }
30
+ ```
31
+
32
+ Visual plugins (overlay / panel / toolbar / dialog) implement
33
+ `mount(ctx: ViewerContext): ReactNode`. `AnnotationSourceProvider`
34
+ instead provides `subscribe(ctx, onChange)` returning an unsubscribe
35
+ callback.
36
+
37
+ `ViewerContext` carries the live viewer state and the same
38
+ `ViewerServices` your host wired up:
39
+
40
+ ```ts
41
+ interface ViewerContext {
42
+ readonly page: number; // 1-indexed current page
43
+ readonly zoom: number; // multiplier; 1.0 = 100%
44
+ readonly pan: { x: number; y: number }; // CSS px
45
+ readonly viewport: { width: number; height: number }; // CSS px
46
+ readonly selectionBbox: readonly [number, number, number, number] | null;
47
+ readonly document: { pageCount: number; pageDimensions: ReadonlyArray<{ width: number; height: number }> };
48
+ readonly services: ViewerServices;
49
+ }
50
+ ```
51
+
52
+ ## Plugin shapes
53
+
54
+ ### `OverlayPlugin`
55
+
56
+ ```ts
57
+ interface OverlayPlugin extends ViewerPluginManifest {
58
+ slot: "overlay.canvas";
59
+ mount(ctx: ViewerContext): ReactNode;
60
+ }
61
+ ```
62
+
63
+ Use for overlays that draw on top of the page canvas (rulers, finding
64
+ boxes, brand-spec violations, etc.).
65
+
66
+ ### `PanelPlugin`
67
+
68
+ ```ts
69
+ interface PanelPlugin extends ViewerPluginManifest {
70
+ slot: "panel.right" | "panel.left" | "panel.bottom";
71
+ title: string; // tab / header label
72
+ order?: number; // lower renders first
73
+ mount(ctx: ViewerContext): ReactNode;
74
+ }
75
+ ```
76
+
77
+ ### `ToolbarPlugin`
78
+
79
+ ```ts
80
+ interface ToolbarPlugin extends ViewerPluginManifest {
81
+ slot: "toolbar.top" | "toolbar.left" | "toolbar.bottom";
82
+ order?: number;
83
+ mount(ctx: ViewerContext): ReactNode;
84
+ }
85
+ ```
86
+
87
+ ### `AnnotationSourceProvider`
88
+
89
+ Non-visual; supplies annotation data to the viewer. The viewer subscribes
90
+ on mount and the provider invokes the callback with the current list and
91
+ on every change.
92
+
93
+ ```ts
94
+ interface AnnotationSourceProvider extends ViewerPluginManifest {
95
+ slot: "annotation.source";
96
+ subscribe(
97
+ ctx: ViewerContext,
98
+ onChange: (annotations: ReadonlyArray<unknown>) => void,
99
+ ): () => void; // returns an unsubscribe
100
+ }
101
+ ```
102
+
103
+ ### `DialogPlugin`
104
+
105
+ ```ts
106
+ interface DialogPlugin extends ViewerPluginManifest {
107
+ slot: "dialog.modal";
108
+ mount(ctx: ViewerContext): ReactNode;
109
+ }
110
+ ```
111
+
112
+ ## Registering a plugin
113
+
114
+ ```tsx
115
+ import { register, type OverlayPlugin } from "@printwithsynergy/lens-pdf/plugin";
116
+
117
+ const ruler: OverlayPlugin = {
118
+ id: "demo.overlay.ruler",
119
+ version: "0.1.0",
120
+ slot: "overlay.canvas",
121
+ mount(ctx) {
122
+ return <RulerOverlay zoom={ctx.zoom} viewport={ctx.viewport} />;
123
+ },
124
+ };
125
+
126
+ register(ruler);
127
+ ```
128
+
129
+ `register` throws if an id is already registered or if a `replaces` claim
130
+ collides — both are programmer errors.
131
+
132
+ `unregister(id)` removes a plugin and frees any `replaces` claim it held.
133
+ `listAll()` returns every registered plugin (including the shadowed ones)
134
+ for inspection / debugging.
135
+
136
+ `_resetRegistryForTesting()` is exported for tests only — production code
137
+ never calls it.
138
+
139
+ ## Reading plugins back at render-time
140
+
141
+ The host mounts each slot by calling `getPluginsForSlot(slot)`:
142
+
143
+ ```tsx
144
+ import { Fragment } from "react";
145
+ import {
146
+ getPluginsForSlot,
147
+ type ViewerContext,
148
+ } from "@printwithsynergy/lens-pdf/plugin";
149
+
150
+ function OverlaySlot({ ctx }: { ctx: ViewerContext }) {
151
+ const plugins = getPluginsForSlot("overlay.canvas");
152
+ return (
153
+ <>
154
+ {plugins.map((p) => (
155
+ <Fragment key={p.id}>{p.mount(ctx)}</Fragment>
156
+ ))}
157
+ </>
158
+ );
159
+ }
160
+ ```
161
+
162
+ `getPluginsForSlot` returns plugins:
163
+
164
+ - Sorted by `order` ascending (lowest first); insertion order breaks ties.
165
+ - With anything shadowed by a `replaces` claim filtered out.
166
+
167
+ ## Replacing a first-party plugin
168
+
169
+ When a plugin pack ships a drop-in alternative, set `replaces` on the
170
+ override:
171
+
172
+ ```ts
173
+ register({
174
+ id: "thirdparty.panel.findings",
175
+ version: "0.1.0",
176
+ slot: "panel.right",
177
+ replaces: "vendor.panel.findings", // shadow the original
178
+ title: "Findings",
179
+ mount: (ctx) => <ThirdPartyFindings ctx={ctx} />,
180
+ });
181
+ ```
182
+
183
+ Constraints:
184
+
185
+ - The replacement must declare the same `slot` as the target. Cross-slot
186
+ overrides are not supported (panels can't replace overlays, etc.).
187
+ - At most one plugin can claim a given `replaces` target — a second
188
+ registration that targets the same id throws.
189
+ - The target id does not need to be registered yet. The override
190
+ registers cleanly even before the target loads, and starts shadowing
191
+ as soon as the target appears.
192
+
193
+ ## Viewer shell plugins (`LensPDF` / `LensPDFDemo`)
194
+
195
+ The drop-in components also expose a focused shell-plugin API for
196
+ sidebar/menu/tool customization without touching the global plugin
197
+ registry.
198
+
199
+ Import from `@printwithsynergy/lens-pdf/components`:
200
+
201
+ ```ts
202
+ type LensPDFShellSlot = "panel.left" | "overlay.toolbar";
203
+
204
+ interface LensPDFShellPlugin {
205
+ id: string;
206
+ slot: LensPDFShellSlot;
207
+ order?: number;
208
+ replaces?: string;
209
+ isAvailable?: (ctx: LensPDFShellPluginContext) => boolean;
210
+ render: (ctx: LensPDFShellPluginContext) => ReactNode;
211
+ }
212
+ ```
213
+
214
+ Pass plugins directly:
215
+
216
+ ```tsx
217
+ <LensPDF
218
+ pdfUrl="/proofs/abc.pdf"
219
+ plugins={[
220
+ {
221
+ id: "acme.left.custom",
222
+ slot: "panel.left",
223
+ order: 15,
224
+ render: (ctx) => <div>Page {ctx.currentPage}</div>,
225
+ },
226
+ ]}
227
+ />
228
+ ```
229
+
230
+ `replaces` uses the same shadow semantics as the global registry:
231
+ set `replaces: "<builtin-id>"` to override a first-party shell plugin.
232
+
233
+ ## `OverlayItem`
234
+
235
+ Plugins and host adapters translate their domain types — findings,
236
+ annotations, brand-spec violations — into `OverlayItem`s before handing
237
+ them to a core component. The shape is deliberately minimal:
238
+
239
+ ```ts
240
+ interface OverlayItem {
241
+ readonly id: string;
242
+ readonly page: number; // 1-indexed
243
+ readonly bbox?: readonly [number, number, number, number]; // PDF points
244
+ readonly tier?: "error" | "warning" | "advisory" | "info" | "neutral";
245
+ readonly color?: string; // CSS hex, optional override
246
+ readonly label?: string;
247
+ readonly description?: string;
248
+ readonly code?: string; // short identifier code
249
+ readonly data?: Record<string, unknown>; // round-trip payload
250
+ }
251
+ ```
252
+
253
+ `PageCanvas` and `PageNavigator` consume `OverlayItem[]` directly. The
254
+ default tier→colour map is `error` red, `warning` amber, `advisory` blue,
255
+ `info` / `neutral` slate (see `SEVERITY_COLORS` in `/types`); set `color`
256
+ on an item to override per-item.
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: "Security policy"
3
+ description: "How to report a vulnerability in LensPDF, what's in scope vs. out of scope, supported versions, and how disclosure is coordinated."
4
+ group: "Project"
5
+ order: 10
6
+ ---
7
+
8
+ # Security policy
9
+
10
+ LensPDF is the renderer, not the access layer. This page covers what
11
+ counts as a LensPDF vulnerability, how to report one, and what we
12
+ promise back.
13
+
14
+ ## Reporting a vulnerability
15
+
16
+ If you believe you've found a security issue in LensPDF, please **do
17
+ not** open a public GitHub issue. Instead, email
18
+ **security@printwithsynergy.com** with:
19
+
20
+ - A clear description of the issue and its impact.
21
+ - Steps to reproduce (a minimal repro repo or code snippet helps).
22
+ - The version / commit you tested against.
23
+ - Any suggested mitigation, if you have one.
24
+
25
+ We aim to acknowledge reports within **3 business days** and to ship a
26
+ fix or workaround within **30 days** for confirmed issues, depending on
27
+ severity.
28
+
29
+ You're welcome to request a CVE assignment; we'll coordinate disclosure
30
+ timing with you.
31
+
32
+ ## Scope
33
+
34
+ LensPDF is a pure renderer. It does not authenticate, sign, or
35
+ rate-limit any URL it consumes — those concerns are the host's. The
36
+ following are **not** in scope as LensPDF vulnerabilities and should
37
+ be reported to the relevant host instead:
38
+
39
+ - A signed URL that didn't expire when the host expected it to.
40
+ - Unauthorised access to a PDF the host served from an unguarded
41
+ endpoint.
42
+ - Cross-tenant data leaks at the host's API layer.
43
+
44
+ In scope:
45
+
46
+ - Issues in the viewer's rendering or sampling that could leak data the
47
+ user's session shouldn't see (e.g., a tool returning a value derived
48
+ from a PDF object the host meant to hide).
49
+ - XSS / injection / prototype-pollution in any code shipped from this
50
+ repo.
51
+ - Issues in the pdf.js fallback adapter or in any dependency we
52
+ bundle / re-export.
53
+ - DoS-grade resource exhaustion in the renderer or sampling tools.
54
+
55
+ For dependency vulnerabilities, please report to the upstream project
56
+ first; we'll bump our version after they ship a fix.
57
+
58
+ ## Supported versions
59
+
60
+ Until the package reaches `1.0.0`, only the latest minor version line
61
+ receives security fixes. Once `1.0.0` ships, the latest two minor
62
+ version lines are supported.
63
+
64
+ ## Disclosure
65
+
66
+ We follow [coordinated
67
+ disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure):
68
+ fixes ship before details are made public, and reporters are credited
69
+ in the release notes unless they request otherwise.
package/docs/server.md ADDED
@@ -0,0 +1,212 @@
1
+ ---
2
+ title: "Reference server"
3
+ description: "Optional Node + Ghostscript backend that supplies real ink separations, densitometer readings, TAC heatmap, and color sampling. Deploy if you need preflight-grade tools the in-browser fallback can't provide."
4
+ group: "Reference"
5
+ order: 9
6
+ ---
7
+
8
+ # Reference server
9
+
10
+ The viewer's [`SeparationCanvas`](./components.md#separationcanvas),
11
+ [`DensitometerTool`](./components.md#densitometertool), and
12
+ [`TACHeatmapOverlay`](./components.md#tacheatmapoverlay) all require
13
+ real ink-channel rasters. The pdf.js fallback can't produce those —
14
+ pdf.js renders to RGB, and there's no in-browser path to reconstruct
15
+ CMYK or spot inks from the result. For preflight-grade output you need
16
+ a server-side renderer.
17
+
18
+ The repo ships a small reference implementation under
19
+ [`server/`](https://github.com/Printwithsynergy/lens-pdf/tree/main/server)
20
+ that you can deploy as-is or read as a contract guide and replace with
21
+ your own. It's a Node + Express service that shells out to Ghostscript
22
+ (`tiffsep` device) for separation rendering. Auth, rate limiting, and
23
+ multi-tenant isolation are deliberately out of scope; run it behind
24
+ your gateway.
25
+
26
+ ## Quick start
27
+
28
+ ```sh
29
+ git clone https://github.com/Printwithsynergy/lens-pdf
30
+ cd lens-pdf/server
31
+ docker build -t lens-pdf-server .
32
+ docker run -p 3000:3000 -v lens-jobs:/var/lib/lens-pdf/jobs lens-pdf-server
33
+ ```
34
+
35
+ `server/README.md` has the full local-development workflow and the
36
+ list of environment variables.
37
+
38
+ ## When you need it
39
+
40
+ | Component | Reference server | pdf.js fallback | Empty |
41
+ | --- | --- | --- | --- |
42
+ | `PageCanvas` | ✅ | ✅ | hidden |
43
+ | `PageNavigator` | ✅ | ✅ | hidden |
44
+ | `LayerPanel` | wire your own `layers` service | ✅ | hidden |
45
+ | `MeasureTool` | ✅ (page dims via PDF) | ✅ | hidden |
46
+ | `ColorPickerTool` | ✅ (true RGB sample) | ✅ (RGB only) | hidden |
47
+ | `SeparationCanvas` | **✅ only here** | hidden | hidden |
48
+ | `DensitometerTool` | **✅ only here** | hidden | hidden |
49
+ | `TACHeatmapOverlay` | **✅ only here** | hidden | hidden |
50
+ | `AnnotationCanvas` | wire your own `annotations` service | hidden | hidden |
51
+ | Reports | wire your own `reports` service | hidden | hidden |
52
+
53
+ Mix and match — the host can use the reference server for separations
54
+ and the pdf.js fallback for everything else, or wire its own
55
+ implementations for any subset.
56
+
57
+ ## Wiring example
58
+
59
+ Pre-register the PDF on the server (do this server-side at upload
60
+ time, not from the browser, so you don't have to expose the source
61
+ URL to the user):
62
+
63
+ ```ts
64
+ await fetch(`${apiBase}/jobs/${jobId}/source`, {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
67
+ body: JSON.stringify({ url: signedPdfUrl }),
68
+ });
69
+ ```
70
+
71
+ Then point the viewer's services at the same base URL:
72
+
73
+ ```ts
74
+ import type { ViewerServices } from "@printwithsynergy/lens-pdf/plugin";
75
+
76
+ const services: ViewerServices = {
77
+ pageImages: {
78
+ getPageImageUrl: ({ pageNum, dpi }) =>
79
+ `${apiBase}/jobs/${jobId}/page/${pageNum}.png?dpi=${dpi}`,
80
+ },
81
+ separations: {
82
+ getChannelImageUrl: ({ pageNum, channelName, dpi }) =>
83
+ `${apiBase}/jobs/${jobId}/channel/${encodeURIComponent(channelName)}.png?page=${pageNum}&dpi=${dpi}`,
84
+ },
85
+ tacHeatmap: {
86
+ getHeatmapImageUrl: ({ pageNum, dpi, tacLimit }) =>
87
+ `${apiBase}/jobs/${jobId}/tac.png?page=${pageNum}&dpi=${dpi}&limit=${tacLimit}`,
88
+ listRuns: async () => [], // not implemented in the reference server yet
89
+ },
90
+ colorSample: {
91
+ sampleAt: async ({ pageNum, pdfX, pdfY, dpi = 150 }) => {
92
+ const r = await fetch(
93
+ `${apiBase}/jobs/${jobId}/color?page=${pageNum}` +
94
+ `&x=${pdfX}&y=${pdfY}&dpi=${dpi}` +
95
+ `&pageWidthPts=${pageWidthPts}&pageHeightPts=${pageHeightPts}`,
96
+ );
97
+ return r.ok ? await r.json() : null;
98
+ },
99
+ },
100
+ densitometer: {
101
+ sampleAt: async ({ pageNum, pdfX, pdfY, dpi = 150, tacLimit }) => {
102
+ const r = await fetch(`${apiBase}/jobs/${jobId}/density`, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({
106
+ page: pageNum,
107
+ x: pdfX,
108
+ y: pdfY,
109
+ pageWidthPts,
110
+ pageHeightPts,
111
+ dpi,
112
+ tacLimit,
113
+ }),
114
+ });
115
+ if (!r.ok) {
116
+ if (r.status === 422) throw new Error("No separations available for this page.");
117
+ throw new Error(`Sampling failed (${r.status})`);
118
+ }
119
+ return await r.json();
120
+ },
121
+ },
122
+ // …leave layers / annotations / reports unwired or supply your own.
123
+ } as ViewerServices;
124
+ ```
125
+
126
+ The viewer doesn't care that any of these came from the same backend —
127
+ each `ViewerServices` field is independent and can point anywhere.
128
+
129
+ ## HTTP contract
130
+
131
+ The reference server is one shape; you can implement any of the
132
+ endpoints differently as long as the responses match. The contract:
133
+
134
+ | Method | Path | Returns |
135
+ | --- | --- | --- |
136
+ | `POST` | `/jobs/{jobId}/source` | Accept the PDF (raw bytes via `application/pdf`, or `application/json` `{ url }` to fetch). |
137
+ | `GET` | `/jobs/{jobId}/page/{n}.png?dpi=N` | Composite RGB PNG. |
138
+ | `GET` | `/jobs/{jobId}/channels?page=N` | `{ "channels": ["Cyan", "Magenta", ...] }`. |
139
+ | `GET` | `/jobs/{jobId}/channel/{name}.png?page=N&dpi=N` | Grayscale PNG, white = no ink. |
140
+ | `GET` | `/jobs/{jobId}/tac.png?page=N&dpi=N&limit=L` | RGBA PNG, transparent under the limit. |
141
+ | `GET` | `/jobs/{jobId}/color?page=N&x=X&y=Y&pageWidthPts=W&pageHeightPts=H&dpi=N` | `ColorSample` JSON. |
142
+ | `POST` | `/jobs/{jobId}/density` | `DensitometerSample` JSON. Body: `{ page, x, y, pageWidthPts, pageHeightPts, dpi, tacLimit }`. |
143
+ | `DELETE` | `/jobs/{jobId}` | Drop server-side state for the job. |
144
+
145
+ `ColorSample` and `DensitometerSample` shapes are defined in
146
+ `@printwithsynergy/lens-pdf/types` — match those exactly.
147
+
148
+ ## Security caveats
149
+
150
+ The viewer is a pure renderer; the reference server is a thin
151
+ Ghostscript wrapper. Authz, rate limiting, multi-tenant isolation, and
152
+ SSRF prevention are **your responsibility**. Specifically:
153
+
154
+ - The optional `LENS_BEARER_TOKEN` is a coarse single-secret check
155
+ meant for private-network deploys. For anything user-facing, run the
156
+ service behind your real gateway.
157
+ - The `{ url: "..." }` upload mode fetches whatever URL you give it.
158
+ Block internal hostnames at your egress layer or skip the URL flow
159
+ and upload PDFs directly.
160
+ - Treat every uploaded PDF as hostile. Run the container with
161
+ `--read-only`, drop capabilities, set ulimits.
162
+ - Ghostscript with `-dSAFER` is the default but historical sandbox
163
+ bypasses exist; isolate the process accordingly.
164
+ - The 60 s render timeout protects against the most obvious DoS
165
+ attempts; pair with per-tenant concurrency caps.
166
+
167
+ See [`server/README.md`](https://github.com/Printwithsynergy/lens-pdf/tree/main/server#security)
168
+ for the full list.
169
+
170
+ ## Cloudflare / CDN deployment
171
+
172
+ Every per-job GET response is marked **immutable** with a 1-year TTL
173
+ and tagged with `Cache-Tag: job-{jobId}`:
174
+
175
+ ```
176
+ Cache-Control: public, max-age=31536000, immutable, s-maxage=31536000
177
+ Cache-Tag: job-{jobId}
178
+ ```
179
+
180
+ So putting Cloudflare in front of the server gives you free edge
181
+ caching with no extra config — the default Cache Rules will respect
182
+ the headers and store responses at the edge for a year.
183
+
184
+ Two things to watch:
185
+
186
+ 1. **Don't set `LENS_BEARER_TOKEN`** if you want CDN caching. An
187
+ `Authorization` header makes Cloudflare bypass the edge cache.
188
+ Move auth to the gateway tier (Cloudflare Access, signed URLs,
189
+ mTLS at the origin) instead.
190
+ 2. **`DELETE /jobs/{jobId}` should be paired with a Cloudflare
191
+ purge-by-tag call** from your control plane (tag: `job-{jobId}`).
192
+ On Cloudflare plans without tag purges, rely on the immutable URL
193
+ pattern — a new `jobId` produces new URLs that haven't been cached
194
+ yet.
195
+
196
+ The reference server's `server/README.md` has the full Cloudflare
197
+ deployment writeup.
198
+
199
+ ## Limitations of this reference
200
+
201
+ - Per-text-run TAC metadata (the hover-tooltip layer of
202
+ `TACHeatmapOverlay`) is not implemented — heatmap renders fine, the
203
+ per-run list is empty.
204
+ - ICC output-intent overrides are not exposed as env vars yet.
205
+ - The in-process cache is in-memory; multi-pod deployments need to
206
+ swap `cache.ts` for a shared backend (or rely entirely on the
207
+ Cloudflare edge tier).
208
+ - Layers (OCGs), annotations, reports — wire those to your own
209
+ services.
210
+
211
+ If you need any of these, the source is small enough to fork. Pull
212
+ requests welcome.