@striae-org/striae 3.0.4

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 (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. package/wrangler.toml.example +8 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "KEYS_WORKER_NAME",
3
+ "account_id": "ACCOUNT_ID",
4
+ "main": "src/keys.ts",
5
+ "compatibility_date": "2026-03-09",
6
+ "compatibility_flags": [
7
+ "nodejs_compat"
8
+ ],
9
+
10
+ "observability": {
11
+ "enabled": true
12
+ },
13
+
14
+ "routes": [
15
+ {
16
+ "pattern": "KEYS_WORKER_DOMAIN",
17
+ "custom_domain": true
18
+ }
19
+ ],
20
+
21
+ "placement": { "mode": "smart" }
22
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "pdf-worker",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "deploy": "wrangler deploy",
7
+ "dev": "wrangler dev",
8
+ "start": "wrangler dev",
9
+ "test": "vitest"
10
+ },
11
+ "devDependencies": {
12
+ "@cloudflare/puppeteer": "^1.0.4",
13
+ "@cloudflare/vitest-pool-workers": "^0.12.9",
14
+ "vitest": "~3.2.0",
15
+ "wrangler": "^4.69.0"
16
+ }
17
+ }
@@ -0,0 +1,534 @@
1
+ import type { PDFGenerationData, ReportRenderer } from './report-types';
2
+
3
+ export const renderReport: ReportRenderer = (data: PDFGenerationData): string => {
4
+ const { imageUrl, caseNumber, annotationData, activeAnnotations, currentDate, notesUpdatedFormatted, userCompany } = data;
5
+ const annotationsSet = new Set(activeAnnotations);
6
+
7
+ // Programmatically determine if a color is dark and needs a light background
8
+ const needsLightBackground = (color: string | undefined): boolean => {
9
+ if (!color) return false;
10
+
11
+ // Handle named colors
12
+ const namedColors: Record<string, string> = {
13
+ 'black': '#000000',
14
+ 'white': '#ffffff',
15
+ 'red': '#ff0000',
16
+ 'green': '#008000',
17
+ 'blue': '#0000ff',
18
+ 'yellow': '#ffff00',
19
+ 'cyan': '#00ffff',
20
+ 'magenta': '#ff00ff',
21
+ 'silver': '#c0c0c0',
22
+ 'gray': '#808080',
23
+ 'maroon': '#800000',
24
+ 'olive': '#808000',
25
+ 'lime': '#00ff00',
26
+ 'aqua': '#00ffff',
27
+ 'teal': '#008080',
28
+ 'navy': '#000080',
29
+ 'fuchsia': '#ff00ff',
30
+ 'purple': '#800080'
31
+ };
32
+
33
+ let hexColor = color.toLowerCase().trim();
34
+
35
+ // Convert named color to hex
36
+ if (namedColors[hexColor]) {
37
+ hexColor = namedColors[hexColor];
38
+ }
39
+
40
+ // Remove # if present
41
+ hexColor = hexColor.replace('#', '');
42
+
43
+ // Handle 3-digit hex codes
44
+ if (hexColor.length === 3) {
45
+ hexColor = hexColor.split('').map(char => char + char).join('');
46
+ }
47
+
48
+ // Validate hex color
49
+ if (!/^[0-9a-f]{6}$/i.test(hexColor)) {
50
+ return false; // Invalid color, don't apply background
51
+ }
52
+
53
+ // Convert to RGB
54
+ const r = parseInt(hexColor.substr(0, 2), 16);
55
+ const g = parseInt(hexColor.substr(2, 2), 16);
56
+ const b = parseInt(hexColor.substr(4, 2), 16);
57
+
58
+ // Calculate relative luminance using WCAG formula
59
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
60
+
61
+ // Colors with luminance < 0.5 are considered dark
62
+ return luminance < 0.5;
63
+ };
64
+
65
+ // Use passed currentDate or generate fallback
66
+ const displayDate = currentDate || (() => {
67
+ const now = new Date();
68
+ return `${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getDate().toString().padStart(2, '0')}/${now.getFullYear()}`;
69
+ })();
70
+
71
+ return `
72
+ <!DOCTYPE html>
73
+ <html lang="en">
74
+ <head>
75
+ <meta charset="utf-8" />
76
+ <style>
77
+ html, body {
78
+ width: 100%;
79
+ height: 100%;
80
+ margin: 0;
81
+ font-family: Arial, sans-serif;
82
+ background-color: white;
83
+ display: flex;
84
+ flex-direction: column;
85
+ min-height: 100vh;
86
+ }
87
+ .header {
88
+ display: flex;
89
+ align-items: center;
90
+ margin-bottom: 15px;
91
+ border-bottom: 2px solid #333;
92
+ padding-bottom: 8px;
93
+ position: relative;
94
+ }
95
+ .header-content {
96
+ flex: 1;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: space-between;
100
+ }
101
+ .date {
102
+ font-size: 16px;
103
+ font-weight: bold;
104
+ }
105
+ .case-number {
106
+ font-size: 16px;
107
+ font-weight: bold;
108
+ color: #333;
109
+ text-align: right;
110
+ }
111
+ .image-container {
112
+ width: 100%;
113
+ margin: 10px 0;
114
+ position: relative;
115
+ }
116
+ .image-wrapper {
117
+ display: flex;
118
+ justify-content: center;
119
+ align-items: center;
120
+ width: 100%;
121
+ position: relative;
122
+ }
123
+ .image-container img {
124
+ width: 100%;
125
+ max-height: 65vh;
126
+ height: auto;
127
+ display: block;
128
+ box-sizing: border-box;
129
+ object-fit: contain;
130
+ }
131
+ .image-with-border {
132
+ max-width: calc(100% - 10px);
133
+ max-height: calc(100% - 10px);
134
+ margin: 0 auto;
135
+ }
136
+ .annotations-overlay {
137
+ position: absolute;
138
+ top: 0;
139
+ left: 0;
140
+ width: 100%;
141
+ height: 100%;
142
+ pointer-events: none;
143
+ z-index: 10;
144
+ }
145
+ .left-annotation,
146
+ .right-annotation {
147
+ position: absolute;
148
+ padding: 12px 16px;
149
+ background: rgba(0, 0, 0, 0.7);
150
+ border-radius: 6px;
151
+ backdrop-filter: blur(4px);
152
+ border: 2px solid rgba(255, 255, 255, 0.2);
153
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
154
+ }
155
+ .left-annotation {
156
+ top: 2%;
157
+ left: 4%;
158
+ }
159
+ .right-annotation {
160
+ top: 2%;
161
+ right: 4%;
162
+ }
163
+ .case-text {
164
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
165
+ font-size: 18px;
166
+ font-weight: 700;
167
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
168
+ white-space: nowrap;
169
+ letter-spacing: 0.5px;
170
+ }
171
+ .below-image-annotations {
172
+ display: flex;
173
+ justify-content: space-between;
174
+ align-items: flex-start;
175
+ width: 100%;
176
+ margin-top: 12px;
177
+ min-height: 50px;
178
+ }
179
+ .support-level-annotation {
180
+ flex: 1;
181
+ display: flex;
182
+ justify-content: flex-start;
183
+ }
184
+ .class-annotation {
185
+ flex: 1;
186
+ display: flex;
187
+ justify-content: center;
188
+ }
189
+ .subclass-annotation {
190
+ flex: 1;
191
+ display: flex;
192
+ justify-content: flex-end;
193
+ }
194
+ .support-level-text {
195
+ padding: 10px 20px;
196
+ background: rgba(240, 240, 240, 0.95);
197
+ border-radius: 6px;
198
+ backdrop-filter: blur(6px);
199
+ border: 2px solid rgba(200, 200, 200, 0.6);
200
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
201
+ font-family: 'Inter', Arial, sans-serif;
202
+ font-size: 14px;
203
+ font-weight: 700;
204
+ text-align: center;
205
+ letter-spacing: 0.5px;
206
+ text-shadow: none;
207
+ white-space: nowrap;
208
+ }
209
+ .class-text-annotation {
210
+ padding: 10px 20px;
211
+ background: rgba(0, 0, 0, 0.8);
212
+ color: #ffffff;
213
+ border-radius: 6px;
214
+ backdrop-filter: blur(6px);
215
+ border: 2px solid rgba(255, 255, 255, 0.2);
216
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
217
+ font-family: 'Inter', Arial, sans-serif;
218
+ font-size: 14px;
219
+ font-weight: 600;
220
+ text-align: center;
221
+ letter-spacing: 0.5px;
222
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
223
+ white-space: nowrap;
224
+ }
225
+ .subclass-text {
226
+ padding: 12px 24px;
227
+ background: rgba(220, 53, 69, 0.9);
228
+ color: #ffffff;
229
+ border-radius: 8px;
230
+ backdrop-filter: blur(6px);
231
+ border: 2px solid rgba(255, 255, 255, 0.3);
232
+ box-shadow: 0 4px 16px rgba(220, 53, 69, 0.4);
233
+ font-family: 'Inter', Arial, sans-serif;
234
+ font-size: 14px;
235
+ font-weight: 700;
236
+ text-align: center;
237
+ letter-spacing: 0.5px;
238
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
239
+ white-space: nowrap;
240
+ }
241
+ .confirmation-section {
242
+ margin-top: 20px;
243
+ display: flex;
244
+ justify-content: space-between;
245
+ align-items: flex-start;
246
+ }
247
+ .confirmation-box {
248
+ background: #ffffff;
249
+ border: 2px solid #333;
250
+ border-radius: 6px;
251
+ padding: 15px;
252
+ width: 280px;
253
+ font-family: 'Inter', Arial, sans-serif;
254
+ }
255
+ .confirmation-label {
256
+ font-size: 14px;
257
+ font-weight: 600;
258
+ color: #333;
259
+ margin-bottom: 8px;
260
+ }
261
+ .confirmation-line {
262
+ border-bottom: 1px solid #333;
263
+ height: 18px;
264
+ margin-bottom: 15px;
265
+ width: 100%;
266
+ }
267
+ .confirmation-date-label {
268
+ font-size: 14px;
269
+ font-weight: 600;
270
+ color: #333;
271
+ margin-bottom: 8px;
272
+ margin-top: 10px;
273
+ }
274
+ .confirmation-data {
275
+ background: #f8f9fa;
276
+ border: 2px solid #28a745;
277
+ border-radius: 6px;
278
+ padding: 15px;
279
+ width: 280px;
280
+ font-family: 'Inter', Arial, sans-serif;
281
+ }
282
+ .confirmation-title {
283
+ font-size: 14px;
284
+ font-weight: 700;
285
+ color: #28a745;
286
+ margin-bottom: 12px;
287
+ text-align: center;
288
+ border-bottom: 1px solid #28a745;
289
+ padding-bottom: 6px;
290
+ }
291
+ .confirmation-field {
292
+ margin-bottom: 8px;
293
+ font-size: 13px;
294
+ line-height: 1.4;
295
+ }
296
+ .confirmation-name {
297
+ font-weight: 700;
298
+ color: #333;
299
+ font-size: 14px;
300
+ }
301
+ .confirmation-badge {
302
+ color: #666;
303
+ font-weight: 600;
304
+ }
305
+ .confirmation-company {
306
+ color: #333;
307
+ font-weight: 500;
308
+ font-style: italic;
309
+ }
310
+ .confirmation-timestamp {
311
+ color: #555;
312
+ font-size: 12px;
313
+ font-weight: 500;
314
+ }
315
+ .confirmation-id {
316
+ color: #28a745;
317
+ font-weight: 700;
318
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
319
+ font-size: 12px;
320
+ letter-spacing: 1px;
321
+ }
322
+ .additional-notes-section {
323
+ max-width: 400px;
324
+ font-family: 'Inter', Arial, sans-serif;
325
+ font-size: 14px;
326
+ line-height: 1.6;
327
+ color: #333;
328
+ white-space: pre-wrap;
329
+ word-wrap: break-word;
330
+ text-indent: 0 !important;
331
+ padding: 0;
332
+ margin: 0;
333
+ margin-left: 20px;
334
+ flex-shrink: 0;
335
+ text-align: left;
336
+ display: block;
337
+ }
338
+ .footer {
339
+ margin-top: auto;
340
+ padding-top: 15px;
341
+ border-top: 1px solid #ccc;
342
+ display: flex;
343
+ justify-content: space-between;
344
+ align-items: center;
345
+ font-family: 'Inter', Arial, sans-serif;
346
+ font-size: 11px;
347
+ color: #666;
348
+ }
349
+ .main-content {
350
+ flex: 1;
351
+ display: flex;
352
+ flex-direction: column;
353
+ }
354
+ .content-wrapper {
355
+ flex-grow: 0;
356
+ flex-shrink: 0;
357
+ }
358
+ .footer-left {
359
+ font-weight: 500;
360
+ flex: 1;
361
+ text-align: left;
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 6px;
365
+ }
366
+ .footer-brand-icon {
367
+ width: 14px;
368
+ height: 14px;
369
+ object-fit: contain;
370
+ }
371
+ .footer-center {
372
+ font-weight: 600;
373
+ flex: 1;
374
+ text-align: center;
375
+ color: #333;
376
+ }
377
+ .footer-right {
378
+ font-style: italic;
379
+ flex: 1;
380
+ text-align: right;
381
+ }
382
+ .index-section {
383
+ text-align: center;
384
+ margin: 15px 0 8px 0;
385
+ font-family: 'Inter', Arial, sans-serif;
386
+ font-size: 14px;
387
+ font-weight: 600;
388
+ color: #333;
389
+ }
390
+ .box-annotation {
391
+ position: absolute;
392
+ box-sizing: border-box;
393
+ pointer-events: none;
394
+ background: transparent;
395
+ border-width: 2px;
396
+ border-style: solid;
397
+ opacity: 0.8;
398
+ }
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <div class="main-content">
403
+ <div class="content-wrapper">
404
+ <div class="header">
405
+ <div class="header-content">
406
+ <div class="date">${displayDate}</div>
407
+ ${caseNumber ? `<div class="case-number">${caseNumber}</div>` : '<div class="case-number"></div>'}
408
+ </div>
409
+ </div>
410
+
411
+ ${imageUrl && imageUrl !== '/clear.jpg' ? `
412
+ ${annotationData && annotationsSet?.has('index') && annotationData.indexType === 'number' && annotationData.indexNumber ? `
413
+ <div class="index-section">
414
+ Index: ${annotationData.indexNumber}
415
+ </div>
416
+ ` : ''}
417
+
418
+ <div class="image-container">
419
+ <div class="image-wrapper">
420
+ <img src="${imageUrl}" alt="Comparison Image" ${annotationData && annotationsSet?.has('index') && annotationData.indexType === 'color' && annotationData.indexColor ? `class="image-with-border" style="border: 5px solid ${annotationData.indexColor};"` : ''} />
421
+
422
+ ${annotationData && annotationsSet?.has('number') ? `
423
+ <div class="annotations-overlay">
424
+ <div class="left-annotation" style="${needsLightBackground(annotationData.caseFontColor || '#FFDE21') ? 'background: rgba(255, 255, 255, 0.9); border: 2px solid rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : ''}">
425
+ <div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
426
+ ${annotationData.leftCase}${annotationData.leftItem ? ` ${annotationData.leftItem}` : ''}
427
+ </div>
428
+ </div>
429
+ <div class="right-annotation" style="${needsLightBackground(annotationData.caseFontColor || '#FFDE21') ? 'background: rgba(255, 255, 255, 0.9); border: 2px solid rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : ''}">
430
+ <div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
431
+ ${annotationData.rightCase}${annotationData.rightItem ? ` ${annotationData.rightItem}` : ''}
432
+ </div>
433
+ </div>
434
+ </div>
435
+ ` : ''}
436
+
437
+ ${annotationData && annotationsSet?.has('box') && annotationData.boxAnnotations ? `
438
+ <div class="annotations-overlay">
439
+ ${annotationData.boxAnnotations.map(box => `
440
+ <div class="box-annotation" style="
441
+ left: ${box.x}%;
442
+ top: ${box.y}%;
443
+ width: ${box.width}%;
444
+ height: ${box.height}%;
445
+ border-color: ${box.color};
446
+ "></div>
447
+ `).join('')}
448
+ </div>
449
+ ` : ''}
450
+ </div>
451
+ </div>
452
+
453
+ <div class="below-image-annotations">
454
+ ${annotationData && annotationsSet?.has('id') ? `
455
+ <div class="support-level-annotation">
456
+ <div class="support-level-text" style="color: ${annotationData.supportLevel === 'ID' ? '#28a745' : annotationData.supportLevel === 'Exclusion' ? '#dc3545' : '#ffc107'}; background: ${annotationData.supportLevel === 'Inconclusive' ? 'rgba(120, 120, 120, 0.95)' : 'rgba(240, 240, 240, 0.95)'};">
457
+ ${annotationData.supportLevel === 'ID' ? 'Identification' : annotationData.supportLevel}
458
+ </div>
459
+ </div>
460
+ ` : '<div class="support-level-annotation"></div>'}
461
+
462
+ ${annotationData && annotationsSet?.has('class') ? `
463
+ <div class="class-annotation">
464
+ <div class="class-text-annotation">
465
+ ${annotationData.customClass || annotationData.classType}${annotationData.classNote ? ` (${annotationData.classNote})` : ''}
466
+ </div>
467
+ </div>
468
+ ` : '<div class="class-annotation"></div>'}
469
+
470
+ ${annotationData && annotationsSet?.has('class') && annotationData.hasSubclass ? `
471
+ <div class="subclass-annotation">
472
+ <div class="subclass-text">
473
+ POTENTIAL SUBCLASS
474
+ </div>
475
+ </div>
476
+ ` : '<div class="subclass-annotation"></div>'}
477
+ </div>
478
+ </div>
479
+ ` : ''}
480
+
481
+ ${annotationData && ((annotationData.includeConfirmation === true) || annotationData.additionalNotes) ? `
482
+ <div class="confirmation-section">
483
+ ${annotationData && (annotationData.includeConfirmation === true) ? `
484
+ ${annotationData.confirmationData ? `
485
+ <div class="confirmation-data">
486
+ <div class="confirmation-title">IDENTIFICATION CONFIRMED</div>
487
+ <div class="confirmation-field">
488
+ <div class="confirmation-name">${annotationData.confirmationData.fullName}, ${annotationData.confirmationData.badgeId}</div>
489
+ </div>
490
+ <div class="confirmation-field">
491
+ <div class="confirmation-company">${annotationData.confirmationData.confirmedByCompany || 'N/A'}</div>
492
+ </div>
493
+ <div class="confirmation-field">
494
+ <div class="confirmation-timestamp">${annotationData.confirmationData.timestamp}</div>
495
+ </div>
496
+ <div class="confirmation-field">
497
+ <div class="confirmation-id">ID: ${annotationData.confirmationData.confirmationId}</div>
498
+ </div>
499
+ </div>
500
+ ` : `
501
+ <div class="confirmation-box">
502
+ <div class="confirmation-label">Confirmation by:</div>
503
+ <div class="confirmation-line"></div>
504
+ <div class="confirmation-date-label">Date:</div>
505
+ <div class="confirmation-line"></div>
506
+ </div>
507
+ `}
508
+ ` : '<div></div>'}
509
+
510
+ ${annotationData && annotationsSet?.has('notes') && annotationData.additionalNotes && annotationData.additionalNotes.trim() ? `
511
+ <div class="additional-notes-section">${annotationData.additionalNotes.trim()}</div>
512
+ ` : '<div></div>'}
513
+ </div>
514
+ ` : ''}
515
+
516
+ </div>
517
+ </div>
518
+
519
+ <div class="footer">
520
+ <div class="footer-left">
521
+ <span>Notes formatted by Striae</span>
522
+ <img class="footer-brand-icon" src="https://app.striae.org/icon-256.png" alt="Striae icon" />
523
+ </div>
524
+ <div class="footer-center">
525
+ ${userCompany ? userCompany : ''}
526
+ </div>
527
+ <div class="footer-right">
528
+ ${notesUpdatedFormatted ? `Notes updated ${notesUpdatedFormatted}` : ''}
529
+ </div>
530
+ </div>
531
+ </body>
532
+ </html>
533
+ `;
534
+ };
@@ -0,0 +1,119 @@
1
+ import puppeteer from "@cloudflare/puppeteer";
2
+ import type { PDFGenerationData, PDFGenerationRequest, ReportModule } from './report-types';
3
+
4
+ interface Env {
5
+ BROWSER: Fetcher;
6
+ }
7
+
8
+ const DEFAULT_REPORT_FORMAT = 'striae';
9
+
10
+ const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
11
+ // Default Striae report format module
12
+ striae: () => import('./format-striae'),
13
+ };
14
+
15
+ const corsHeaders: Record<string, string> = {
16
+ 'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
17
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
18
+ 'Access-Control-Allow-Headers': 'Content-Type',
19
+ };
20
+
21
+ function normalizeReportFormat(format: unknown): string {
22
+ if (typeof format !== 'string') {
23
+ return DEFAULT_REPORT_FORMAT;
24
+ }
25
+
26
+ const normalized = format.trim().toLowerCase();
27
+ return normalized || DEFAULT_REPORT_FORMAT;
28
+ }
29
+
30
+ function resolveReportRequest(payload: unknown): { reportFormat: string; data: PDFGenerationData } {
31
+ if (!payload || typeof payload !== 'object') {
32
+ throw new Error('Request body must be a JSON object');
33
+ }
34
+
35
+ const record = payload as Record<string, unknown>;
36
+ const reportFormat = normalizeReportFormat(record.reportFormat);
37
+
38
+ if (record.data && typeof record.data === 'object') {
39
+ return {
40
+ reportFormat,
41
+ data: record.data as PDFGenerationData,
42
+ };
43
+ }
44
+
45
+ // Backward compatibility: accept legacy top-level payload shape.
46
+ const legacyData: Record<string, unknown> = { ...record };
47
+ delete legacyData.reportFormat;
48
+ delete legacyData.data;
49
+
50
+ return {
51
+ reportFormat,
52
+ data: legacyData as PDFGenerationData,
53
+ };
54
+ }
55
+
56
+ async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<string> {
57
+ const loader = reportModuleLoaders[reportFormat];
58
+
59
+ if (!loader) {
60
+ const supportedFormats = Object.keys(reportModuleLoaders).sort().join(', ');
61
+ throw new Error(`Unsupported report format "${reportFormat}". Supported formats: ${supportedFormats}`);
62
+ }
63
+
64
+ const reportModule = await loader();
65
+ return reportModule.renderReport(data);
66
+ }
67
+
68
+ export default {
69
+ async fetch(request: Request, env: Env): Promise<Response> {
70
+ if (request.method === 'OPTIONS') {
71
+ return new Response(null, { headers: corsHeaders });
72
+ }
73
+
74
+ if (request.method === 'POST') {
75
+ let browser: Awaited<ReturnType<typeof puppeteer.launch>> | undefined;
76
+
77
+ try {
78
+ const payload = await request.json() as PDFGenerationData | PDFGenerationRequest;
79
+ const { reportFormat, data } = resolveReportRequest(payload);
80
+
81
+ browser = await puppeteer.launch(env.BROWSER);
82
+ const page = await browser.newPage();
83
+
84
+ // Render report from module selected by report format name.
85
+ const document = await renderReport(reportFormat, data);
86
+ await page.setContent(document);
87
+
88
+ const pdfBuffer = await page.pdf({
89
+ printBackground: true,
90
+ format: 'letter',
91
+ margin: { top: '0.5in', bottom: '0.5in', left: '0.5in', right: '0.5in' },
92
+ });
93
+
94
+ return new Response(new Uint8Array(pdfBuffer), {
95
+ headers: {
96
+ ...corsHeaders,
97
+ 'content-type': 'application/pdf',
98
+ },
99
+ });
100
+ } catch (error) {
101
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
102
+
103
+ return new Response(JSON.stringify({ error: errorMessage }), {
104
+ status: 500,
105
+ headers: { ...corsHeaders, 'content-type': 'application/json' },
106
+ });
107
+ } finally {
108
+ if (browser) {
109
+ await browser.close();
110
+ }
111
+ }
112
+ }
113
+
114
+ return new Response(JSON.stringify({ error: 'Method not allowed' }), {
115
+ status: 405,
116
+ headers: { ...corsHeaders, 'content-type': 'application/json' },
117
+ });
118
+ },
119
+ };