@brftech/filex-core 0.42.1 → 0.43.0

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 (233) hide show
  1. package/README.md +14 -2
  2. package/dist/ArchiveViewer-BqU761Wk.js +149 -0
  3. package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
  4. package/dist/CsvViewer-ByHVK5ga.js +172 -0
  5. package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
  6. package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
  9. package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
  10. package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
  11. package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
  12. package/dist/MermaidViewer-CEDTD_pp.js +136 -0
  13. package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
  14. package/dist/PsdViewer-CZcxUpZO.js +121 -0
  15. package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
  16. package/dist/TiffViewer-BwI62CbQ.js +141 -0
  17. package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
  18. package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
  19. package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
  20. package/dist/filex-core.js +475 -219
  21. package/dist/filex-core.umd.cjs +165 -108
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/fonts/caveat-latin-ext.woff2 +0 -0
  24. package/dist/fonts/caveat-latin.woff2 +0 -0
  25. package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
  26. package/dist/fonts/dancing-script-latin.woff2 +0 -0
  27. package/dist/fonts/homemade-apple-latin.woff2 +0 -0
  28. package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
  29. package/dist/fonts/source-serif-4-latin.woff2 +0 -0
  30. package/dist/index-DuWnGuv5.js +35901 -0
  31. package/dist/index-DuWnGuv5.js.map +1 -0
  32. package/dist/index.d.ts +5241 -553
  33. package/dist/pdf-CaPYsOUo.js +21302 -0
  34. package/dist/pdf-CaPYsOUo.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/package.json +1 -1
  37. package/src/FileExplorer.vue +1207 -343
  38. package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
  39. package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
  40. package/src/assets/fonts/README.md +58 -0
  41. package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
  42. package/src/assets/fonts/caveat-latin.woff2 +0 -0
  43. package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
  44. package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
  45. package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
  46. package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
  47. package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
  48. package/src/components/AdvancedSearch.vue +49 -0
  49. package/src/components/Breadcrumb.vue +5 -5
  50. package/src/components/ChoiceButtons.vue +152 -0
  51. package/src/components/CommandPalette.vue +5 -5
  52. package/src/components/ConnectionGuideView.vue +1 -1
  53. package/src/components/ConnectionNotice.vue +59 -0
  54. package/src/components/ConnectionsPanel.vue +30 -568
  55. package/src/components/ContextMenu.vue +31 -5
  56. package/src/components/DataTable.vue +1331 -0
  57. package/src/components/FilePane.vue +3 -0
  58. package/src/components/FilterBar.vue +12 -5
  59. package/src/components/GalleryView.vue +50 -2
  60. package/src/components/GridView.vue +51 -3
  61. package/src/components/HomeView.vue +18 -6
  62. package/src/components/InspectorPanel.vue +134 -17
  63. package/src/components/ListView.vue +366 -900
  64. package/src/components/LogoMark.vue +57 -0
  65. package/src/components/NFSExportsPanel.vue +76 -28
  66. package/src/components/OnboardingTour.vue +19 -7
  67. package/src/components/OperationsCenter.vue +77 -1
  68. package/src/components/PendingOpsTray.vue +59 -5
  69. package/src/components/PresenceBar.vue +7 -7
  70. package/src/components/ProductVersion.vue +31 -0
  71. package/src/components/QuickLook.vue +10 -14
  72. package/src/components/RecentlyOpened.vue +47 -17
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -56
  76. package/src/components/SSHKeysPanel.vue +79 -26
  77. package/src/components/SideNav.vue +260 -96
  78. package/src/components/StarButton.vue +1 -0
  79. package/src/components/StorageFields.vue +74 -6
  80. package/src/components/StorageTags.vue +62 -0
  81. package/src/components/TabBar.vue +1 -1
  82. package/src/components/TagKindIcon.vue +56 -0
  83. package/src/components/TagPicker.vue +226 -40
  84. package/src/components/ThemePalette.vue +4 -4
  85. package/src/components/TimeZonePicker.vue +29 -10
  86. package/src/components/TokensPanel.vue +82 -27
  87. package/src/components/Toolbar.vue +28 -6
  88. package/src/components/plugin/PluginConfirmModal.vue +54 -0
  89. package/src/components/plugin/PluginInspectorSection.vue +149 -0
  90. package/src/components/plugin/PluginPageView.vue +339 -0
  91. package/src/components/plugin/PluginViewModal.vue +159 -0
  92. package/src/components/plugin/SurfaceConversation.vue +76 -0
  93. package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
  94. package/src/components/plugin/SurfaceRenderer.vue +299 -0
  95. package/src/components/plugin/SurfaceSections.vue +91 -0
  96. package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
  97. package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
  98. package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
  99. package/src/components/plugin/nodes/SurfaceList.vue +127 -0
  100. package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
  101. package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
  102. package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
  103. package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
  104. package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
  105. package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
  106. package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
  107. package/src/components/public/PublicLanguagePicker.vue +58 -0
  108. package/src/components/public/PublicLinkPage.vue +286 -0
  109. package/src/components/public/PublicLinkPreview.vue +85 -0
  110. package/src/components/public/PublicPinGate.vue +108 -0
  111. package/src/components/public/PublicRequestBody.vue +204 -0
  112. package/src/components/public/PublicShareBody.vue +188 -0
  113. package/src/components/public/PublicShell.vue +332 -0
  114. package/src/composables/useConnections.ts +8 -2
  115. package/src/composables/useFileApi.ts +183 -44
  116. package/src/composables/useLocale.ts +277 -41
  117. package/src/composables/useNFSExports.ts +8 -10
  118. package/src/composables/useOperations.ts +44 -3
  119. package/src/composables/usePendingOps.ts +59 -8
  120. package/src/composables/usePluginActions.ts +121 -0
  121. package/src/composables/usePluginSurface.ts +223 -0
  122. package/src/composables/usePublicBranding.ts +172 -0
  123. package/src/composables/usePublicLink.ts +573 -0
  124. package/src/composables/usePublicPage.ts +61 -0
  125. package/src/composables/useS3Keys.ts +17 -12
  126. package/src/composables/useSSHKeys.ts +8 -10
  127. package/src/composables/useTokens.ts +8 -12
  128. package/src/composables/useUploadChunked.ts +18 -8
  129. package/src/composables/useViewerFetch.ts +6 -3
  130. package/src/index.ts +404 -8
  131. package/src/lib/actionIcons.ts +46 -1
  132. package/src/lib/advSearch.ts +14 -7
  133. package/src/lib/appLock.ts +130 -0
  134. package/src/lib/connection.ts +110 -0
  135. package/src/lib/destinationTree.ts +19 -3
  136. package/src/lib/direction.ts +353 -0
  137. package/src/lib/dragGhost.ts +6 -0
  138. package/src/lib/dropLimits.ts +69 -0
  139. package/src/lib/errorWords.ts +394 -0
  140. package/src/lib/externalReach.ts +5 -1
  141. package/src/lib/fileFilters.ts +7 -2
  142. package/src/lib/fileIcons.ts +4 -0
  143. package/src/lib/internalPaths.ts +77 -0
  144. package/src/lib/listInput.ts +64 -0
  145. package/src/lib/listing.ts +58 -23
  146. package/src/lib/modalStack.ts +26 -0
  147. package/src/lib/nodeRow.ts +121 -0
  148. package/src/lib/pdfFieldRules.ts +126 -0
  149. package/src/lib/pdfFields.ts +589 -0
  150. package/src/lib/pdfFieldsGeom.ts +203 -0
  151. package/src/lib/pdfjsLoader.ts +51 -0
  152. package/src/lib/personName.ts +47 -0
  153. package/src/lib/pluginApplies.ts +123 -0
  154. package/src/lib/pluginLabel.ts +72 -0
  155. package/src/lib/pluginMenu.ts +253 -0
  156. package/src/lib/pluginPage.ts +58 -0
  157. package/src/lib/plural.ts +181 -0
  158. package/src/lib/popupLayer.ts +103 -0
  159. package/src/lib/prefs.ts +563 -0
  160. package/src/lib/productVersion.ts +35 -0
  161. package/src/lib/publicLayout.ts +39 -0
  162. package/src/lib/serviceGate.ts +104 -0
  163. package/src/lib/shareTtl.ts +25 -11
  164. package/src/lib/signFonts.ts +85 -0
  165. package/src/lib/signaturePad.ts +111 -0
  166. package/src/lib/sortOrder.ts +100 -38
  167. package/src/lib/star.ts +7 -1
  168. package/src/lib/surfaceCell.ts +41 -0
  169. package/src/lib/surfaceConditions.ts +156 -0
  170. package/src/lib/surfaceOpen.ts +106 -0
  171. package/src/lib/surfaceSteps.ts +68 -0
  172. package/src/lib/surfaceValues.ts +191 -0
  173. package/src/lib/symlink.ts +150 -0
  174. package/src/lib/tableColumns.ts +493 -0
  175. package/src/lib/tableEnv.ts +34 -0
  176. package/src/lib/tablePrefs.ts +84 -0
  177. package/src/lib/tags.ts +121 -12
  178. package/src/lib/themes.ts +286 -18
  179. package/src/lib/tour.ts +152 -0
  180. package/src/lib/trashEmpty.ts +111 -0
  181. package/src/lib/trashTimeLeft.ts +23 -0
  182. package/src/lib/uiLocales.ts +460 -0
  183. package/src/lib/viewPrefs.ts +696 -660
  184. package/src/lib/viewPrefsHttp.ts +78 -0
  185. package/src/locales/en.ts +522 -119
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +539 -155
  188. package/src/modals/ConvertModal.vue +62 -11
  189. package/src/modals/DestinationPickerModal.vue +59 -16
  190. package/src/modals/Modal.vue +113 -23
  191. package/src/modals/NewDocumentModal.vue +16 -6
  192. package/src/modals/NewFolderModal.vue +8 -0
  193. package/src/modals/PermissionsModal.vue +272 -163
  194. package/src/modals/PreviewModal.vue +119 -20
  195. package/src/modals/RenameModal.vue +37 -1
  196. package/src/styles/base.css +2895 -241
  197. package/src/styles/sign-fonts.css +118 -0
  198. package/src/styles/variables.css +60 -0
  199. package/src/types/Connections.ts +17 -0
  200. package/src/types/ExplorerConfig.ts +117 -1
  201. package/src/types/FileNode.ts +58 -0
  202. package/src/types/Plugins.ts +494 -0
  203. package/src/types/Public.ts +212 -0
  204. package/src/viewers/ArchiveViewer.vue +93 -48
  205. package/src/viewers/CsvViewer.vue +99 -53
  206. package/src/viewers/DrawioViewer.vue +35 -9
  207. package/src/viewers/EpubViewer.vue +7 -8
  208. package/src/viewers/IpynbViewer.vue +3 -1
  209. package/src/viewers/MermaidViewer.vue +4 -5
  210. package/src/viewers/PdfViewer.vue +8 -17
  211. package/src/viewers/PsdViewer.vue +2 -4
  212. package/src/viewers/TiffViewer.vue +4 -5
  213. package/src/viewers/Viewer3D.vue +1 -1
  214. package/LICENSE +0 -21
  215. package/dist/ArchiveViewer-CLNevfPE.js +0 -124
  216. package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
  217. package/dist/CsvViewer-K8XA3z7N.js +0 -141
  218. package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
  219. package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
  220. package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
  221. package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
  222. package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
  223. package/dist/MermaidViewer-DqucH_io.js +0 -137
  224. package/dist/MermaidViewer-DqucH_io.js.map +0 -1
  225. package/dist/PsdViewer-CwiVENmq.js +0 -122
  226. package/dist/PsdViewer-CwiVENmq.js.map +0 -1
  227. package/dist/TiffViewer-CpewSVmQ.js +0 -142
  228. package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
  229. package/dist/Viewer3D-CIrOn63I.js.map +0 -1
  230. package/dist/index-Dx0IbBcI.js +0 -26831
  231. package/dist/index-Dx0IbBcI.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
@@ -0,0 +1,573 @@
1
+ /**
2
+ * usePublicLink — a visitor with a link and no account, whatever the link is.
3
+ *
4
+ * ⚠⚠ v3 §1: "one public surface, not three". A share (`/s/<token>`), a file
5
+ * request (`/d/<token>`) and an app plugin's page (a share carrying a
6
+ * `page_id`) used to be two rendering stacks and three layouts, so the PIN
7
+ * box of a signature request looked nothing like the PIN box of a download
8
+ * and neither carried the instance's own colours. They are the same walk:
9
+ *
10
+ * info → (PIN when required) → the body → the states it can end in
11
+ *
12
+ * Only the BODY differs, and only the caller decides that. Everything here —
13
+ * the fetch, the 401/404/410 reading, the PIN attempt and its lock, the
14
+ * plugin conversation when the link carries one — is shared, because every
15
+ * one of those is a place the three used to disagree.
16
+ *
17
+ * ⚠ Nothing is stored on this side: no session, no token copy, no PIN. The
18
+ * unlock cookie is the server's and HttpOnly.
19
+ *
20
+ * ⚠ The endpoints are `/api/public/s/{token}` and `/api/public/d/{token}`;
21
+ * the app-page walk (`/api/p/{token}`) is the same shape at a different
22
+ * root, which is why the root is an argument. That is what lets `/p/*` be
23
+ * retired without a second implementation living on for the transition.
24
+ */
25
+ import { ref, type Ref } from 'vue';
26
+ import type { PluginRunResult, PluginSurface, PluginViewEventBody } from '../types/Plugins';
27
+ import type { PublicFailure, PublicRequestInfo, PublicShareInfo } from '../types/Public';
28
+ import { usePluginSurface, type PluginSurfaceStore } from './usePluginSurface';
29
+ import { foreignText } from '../lib/direction';
30
+ import { checkDropFiles, type DropRefusal } from '../lib/dropLimits';
31
+
32
+ export type PublicStatus =
33
+ | 'loading'
34
+ | 'pin'
35
+ /** The body may be drawn (a file, a folder, an upload box, a surface). */
36
+ | 'ready'
37
+ /** A surface answered `202` — the job is queued and the visitor is done. */
38
+ | 'accepted'
39
+ | 'done'
40
+ | 'not_found'
41
+ | 'gone'
42
+ | 'error';
43
+
44
+ export type PinFailure = '' | 'wrong' | 'locked';
45
+
46
+ /** A refused answer: the HTTP status and the server's `error` code. */
47
+ export class PublicLinkError extends Error {
48
+ constructor(
49
+ public status: number,
50
+ public code: string,
51
+ message?: string,
52
+ ) {
53
+ super(message || code || String(status));
54
+ this.name = 'PublicLinkError';
55
+ }
56
+ }
57
+
58
+ /** Builds the addresses of one link's walk. */
59
+ export type PublicRoot = (tail: string) => string;
60
+
61
+ function root(base: string | undefined, prefix: string, token: string): PublicRoot {
62
+ const origin = (base ?? '').replace(/\/+$/, '');
63
+ return (tail = '') => `${origin}${prefix}${encodeURIComponent(token)}${tail}`;
64
+ }
65
+
66
+ /** `/api/public/s/{token}` — a share (download, folder, or an app's page). */
67
+ export function shareRoot(base: string | undefined, token: string): PublicRoot {
68
+ return root(base, '/api/public/s/', token);
69
+ }
70
+
71
+ /** `/api/public/d/{token}` — a file request. */
72
+ export function requestRoot(base: string | undefined, token: string): PublicRoot {
73
+ return root(base, '/api/public/d/', token);
74
+ }
75
+
76
+ /**
77
+ * `/api/p/{token}` — an app plugin's page.
78
+ *
79
+ * ⚠ Retired by v3 (§1: "`/p/*` — retired, 301 to the share"). Kept because
80
+ * links already in people's mailboxes point at it and a 301 only helps a
81
+ * browser that follows one; nothing new should be built on it.
82
+ */
83
+ export function pageRoot(base: string | undefined, token: string): PublicRoot {
84
+ return root(base, '/api/p/', token);
85
+ }
86
+
87
+ export interface PublicLinkOptions {
88
+ /** API origin; empty = same origin. */
89
+ base?: string;
90
+ /** The visitor's language, sent as `Accept-Language` so a plugin answers in it. */
91
+ locale: () => string;
92
+ /** The words for an answer that carries nothing to show. */
93
+ errorText: () => string;
94
+ fetchImpl?: typeof fetch;
95
+ }
96
+
97
+ export function publicLinkClient<TInfo>(url: PublicRoot, opts: PublicLinkOptions) {
98
+ const doFetch = (...args: Parameters<typeof fetch>) => (opts.fetchImpl ?? fetch)(...args);
99
+
100
+ async function call<T>(tail: string, init: { method?: string; body?: unknown } = {}): Promise<{ status: number; body: T }> {
101
+ const headers: Record<string, string> = { Accept: 'application/json', 'Accept-Language': opts.locale() };
102
+ if (init.body !== undefined) headers['Content-Type'] = 'application/json';
103
+ const res = await doFetch(url(tail), {
104
+ method: init.method ?? 'GET',
105
+ headers,
106
+ credentials: 'same-origin',
107
+ cache: 'no-store',
108
+ body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
109
+ });
110
+ let body: unknown = null;
111
+ try {
112
+ body = await res.json();
113
+ } catch {
114
+ body = null;
115
+ }
116
+ if (!res.ok) {
117
+ const b = (body ?? {}) as { error?: string; message?: string };
118
+ throw new PublicLinkError(res.status, b.error ?? '', b.message);
119
+ }
120
+ return { status: res.status, body: body as T };
121
+ }
122
+
123
+ return {
124
+ /** What the link is and what the visitor may see of it. */
125
+ info: (query = '') => call<TInfo>(query).then((r) => r.body),
126
+ pin: (pin: string) => call<TInfo>('/pin', { method: 'POST', body: { pin } }).then((r) => r.body),
127
+ /**
128
+ * The app's opening screen.
129
+ *
130
+ * ⚠ There is no `/view` route: an open is `POST /event
131
+ * {"event":"open"}`, because the opening surface and every later one are
132
+ * the same call to the app. One endpoint, one code path in the plugin.
133
+ */
134
+ open: () =>
135
+ call<{ surface?: PluginSurface }>('/event', { method: 'POST', body: { event: 'open', state: {}, data: {} } }).then(
136
+ (r) => r.body,
137
+ ),
138
+ /** `{surface}`, or `{op}` when the server queued a job (202). */
139
+ event: async (body: Omit<PluginViewEventBody, 'path' | 'storage_id'>): Promise<PluginRunResult> => {
140
+ const r = await call<{ surface?: PluginSurface; accepted?: boolean; job_id?: string }>('/event', {
141
+ method: 'POST',
142
+ body,
143
+ });
144
+ if (r.status === 202 || r.body?.accepted) {
145
+ return { op: { accepted: true, job_id: r.body?.job_id }, job_id: r.body?.job_id };
146
+ }
147
+ return { surface: r.body.surface as PluginSurface };
148
+ },
149
+ /** Where the browser loads an exposed copy from (`pub:N`). */
150
+ fileUrl: (ref: string) => url(`/file/${encodeURIComponent(ref)}`),
151
+ /**
152
+ * The same copy, asked for as a DOWNLOAD — the Download button's link.
153
+ * ⚠ Kept apart from `fileUrl` because only this one counts as a download
154
+ * (handlers/public_api.go serveExposed): the page's own viewer loads the
155
+ * document to SHOW it, and the owner ruled on 2026-09-21 that looking at
156
+ * a signing link is not downloading it.
157
+ */
158
+ fileDownloadUrl: (ref: string) => url(`/file/${encodeURIComponent(ref)}?download=1`),
159
+ url,
160
+ };
161
+ }
162
+
163
+ export type PublicLinkClient<T> = ReturnType<typeof publicLinkClient<T>>;
164
+
165
+ /**
166
+ * The walk itself.
167
+ *
168
+ * `TInfo` is the shape the body needs (`PublicShareInfo`,
169
+ * `PublicRequestInfo`); this composable only reads the four facts every
170
+ * public link has — does it want a PIN, is it unlocked, has it a surface,
171
+ * and is it still alive.
172
+ */
173
+ /**
174
+ * What `usePublicLink` hands back.
175
+ *
176
+ * ⚠⚠ Written out rather than inferred (`ReturnType<typeof …>`). The returned
177
+ * object carries the surface conversation, whose own type names Vue's
178
+ * internal ref helpers, and an inferred declaration cannot name them from
179
+ * outside this package: `vue-tsc` failed the package build with TS2742 — "the
180
+ * inferred type cannot be named without a reference to @vue/shared" — and the
181
+ * published `.d.ts` was built anyway, with a type consumers could not use.
182
+ * A written type is also the contract: adding a field here is a decision.
183
+ */
184
+ export interface PublicLinkStore<
185
+ TInfo extends { needs_pin?: boolean; unlocked?: boolean; expired?: boolean; revoked?: boolean; locked?: boolean },
186
+ > {
187
+ status: Ref<PublicStatus>;
188
+ info: Ref<TInfo | null>;
189
+ reason: Ref<PublicFailure>;
190
+ pinFailure: Ref<PinFailure>;
191
+ lockMessage: Ref<string>;
192
+ pinBusy: Ref<boolean>;
193
+ failure: Ref<string>;
194
+ toast: Ref<string>;
195
+ jobId: Ref<string>;
196
+ /** The plugin surface on this link, when it carries one. */
197
+ conv: PluginSurfaceStore;
198
+ /** (Re-)read the link. `query` is appended verbatim (`?path=…`). */
199
+ load: (query?: string) => Promise<void>;
200
+ submitPin: (pin: string) => Promise<void>;
201
+ /** Where the browser loads an exposed copy from (`pub:N`). */
202
+ fileUrl: (ref: string) => string;
203
+ /** The same copy as a download — the only fetch that counts as one. */
204
+ fileDownloadUrl: (ref: string) => string;
205
+ resolveFile: (ref: string) => { url: string; name?: string; mime?: string };
206
+ url: PublicRoot;
207
+ }
208
+
209
+ export function usePublicLink<
210
+ TInfo extends { needs_pin?: boolean; unlocked?: boolean; expired?: boolean; revoked?: boolean; locked?: boolean },
211
+ >(
212
+ url: PublicRoot,
213
+ opts: PublicLinkOptions & {
214
+ /** True when this link's body is an app plugin's surface. */
215
+ hasSurface?: (info: TInfo) => boolean;
216
+ },
217
+ ): PublicLinkStore<TInfo> {
218
+ const client = publicLinkClient<TInfo>(url, opts);
219
+
220
+ const status = ref<PublicStatus>('loading');
221
+ // ⚠ Annotated, not inferred: `ref<TInfo|null>` of a GENERIC unwraps to a
222
+ // type that cannot be written down outside this package (TS2742), and
223
+ // the store's declared shape has to match what callers actually get.
224
+ const info = ref(null) as Ref<TInfo | null>;
225
+ const pinFailure = ref<PinFailure>('');
226
+ /** The server's own words for a lock (`429 locked`), when it sends any. */
227
+ const lockMessage = ref('');
228
+ const jobId = ref('');
229
+ const toast = ref('');
230
+ /** A failure outside the conversation (network, 5xx). */
231
+ const failure = ref('');
232
+ /** Why there is nothing to show, for a caller that wants to say more. */
233
+ const reason = ref<PublicFailure>('');
234
+
235
+ const conv = usePluginSurface(
236
+ {
237
+ api: { pluginViewEvent: (_plugin, _view, body) => client.event(body) },
238
+ plugin: '',
239
+ view: '',
240
+ locale: opts.locale,
241
+ errorText: opts.errorText,
242
+ },
243
+ {
244
+ onOp: (op) => {
245
+ jobId.value = String((op as { job_id?: string }).job_id ?? '');
246
+ status.value = 'accepted';
247
+ },
248
+ onDone: () => {
249
+ if (status.value !== 'accepted') status.value = 'done';
250
+ },
251
+ onToast: (m) => {
252
+ toast.value = m;
253
+ },
254
+ },
255
+ );
256
+
257
+ /**
258
+ * ⚠ 404 and 410 both land on "this link is not available", and the two
259
+ * states are kept apart only for the caller's own logging. A visitor is
260
+ * never told WHICH — "expired" confirms that the link existed, which is
261
+ * one bit more than somebody guessing tokens should get.
262
+ */
263
+ function fail(e: unknown): void {
264
+ const err = e as PublicLinkError;
265
+ if (err?.status === 404) {
266
+ status.value = 'not_found';
267
+ reason.value = 'not_found';
268
+ } else if (err?.status === 410) {
269
+ status.value = 'gone';
270
+ reason.value = 'gone';
271
+ } else {
272
+ status.value = 'error';
273
+ reason.value = 'error';
274
+ // ⚠ An app's own error text never met the catalogue — isolate it.
275
+ failure.value = foreignText(opts.locale(), String((e as Error)?.message ?? e)) || opts.errorText();
276
+ }
277
+ }
278
+
279
+ async function openSurface(): Promise<void> {
280
+ try {
281
+ const res = await client.open();
282
+ const s = res?.surface;
283
+ if (!s) {
284
+ status.value = 'error';
285
+ failure.value = opts.errorText();
286
+ return;
287
+ }
288
+ conv.setSurface(s);
289
+ status.value = s.done ? 'done' : 'ready';
290
+ } catch (e) {
291
+ if ((e as PublicLinkError)?.status === 401) {
292
+ status.value = 'pin';
293
+ return;
294
+ }
295
+ fail(e);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Is this link dead?
301
+ *
302
+ * ⚠⚠ A link that ran out answers 200 with the ordinary shape and
303
+ * `expired` / `revoked` set — NOT a 410. That is deliberate on the server
304
+ * side (a visitor who was given a link is entitled to be told it is over),
305
+ * and it means a client that only checks the status code renders an empty
306
+ * download page instead of an explanation. Both flags mean the same thing
307
+ * to the person reading the page, which is why they collapse here and are
308
+ * kept apart in the payload.
309
+ */
310
+ function dead(i: TInfo | null): boolean {
311
+ return i?.expired === true || i?.revoked === true;
312
+ }
313
+
314
+ /** The first call: the facts, then the PIN form or the body. */
315
+ async function load(query = ''): Promise<void> {
316
+ status.value = 'loading';
317
+ failure.value = '';
318
+ reason.value = '';
319
+ try {
320
+ info.value = await client.info(query);
321
+ } catch (e) {
322
+ fail(e);
323
+ return;
324
+ }
325
+ if (dead(info.value as TInfo)) {
326
+ status.value = 'gone';
327
+ reason.value = 'gone';
328
+ return;
329
+ }
330
+ if (info.value?.needs_pin && !info.value?.unlocked) {
331
+ status.value = 'pin';
332
+ // A gate shut by too many wrong answers says so before the first try,
333
+ // rather than letting somebody spend an attempt finding out.
334
+ pinFailure.value = info.value?.locked === true ? 'locked' : '';
335
+ return;
336
+ }
337
+ if (opts.hasSurface?.(info.value as TInfo)) {
338
+ await openSurface();
339
+ return;
340
+ }
341
+ status.value = 'ready';
342
+ }
343
+
344
+ const pinBusy = ref(false);
345
+
346
+ /** `401 pin_wrong` → `wrong`, `429 locked` → `locked`; on success straight to the body. */
347
+ async function submitPin(pin: string): Promise<void> {
348
+ if (pinBusy.value) return;
349
+ pinFailure.value = '';
350
+ lockMessage.value = '';
351
+ pinBusy.value = true;
352
+ try {
353
+ // ⚠ Trimmed. A PIN is usually pasted out of a message, and a trailing
354
+ // space is not a wrong code — refusing it spends one of the attempts
355
+ // before the lockout on something the person cannot even see.
356
+ info.value = await client.pin(pin.trim());
357
+ if (dead(info.value as TInfo)) {
358
+ status.value = 'gone';
359
+ reason.value = 'gone';
360
+ } else if (opts.hasSurface?.(info.value as TInfo)) {
361
+ await openSurface();
362
+ } else {
363
+ status.value = 'ready';
364
+ }
365
+ } catch (e) {
366
+ const err = e as PublicLinkError;
367
+ if (err?.status === 429) {
368
+ pinFailure.value = 'locked';
369
+ // ⚠ The server's own rate-limit sentence — isolate it.
370
+ lockMessage.value = foreignText(opts.locale(), err.message ?? '');
371
+ } else if (err?.status === 401 || err?.status === 403) {
372
+ pinFailure.value = 'wrong';
373
+ } else {
374
+ fail(e);
375
+ }
376
+ } finally {
377
+ pinBusy.value = false;
378
+ }
379
+ }
380
+
381
+ return {
382
+ status,
383
+ info,
384
+ reason,
385
+ pinFailure,
386
+ lockMessage,
387
+ pinBusy,
388
+ failure,
389
+ toast,
390
+ jobId,
391
+ conv,
392
+ load,
393
+ submitPin,
394
+ fileUrl: client.fileUrl,
395
+ fileDownloadUrl: client.fileDownloadUrl,
396
+ /**
397
+ * A `FileRefResolver` for the surface renderer: the URL, plus the name
398
+ * and type the link's own `files[]` already told us.
399
+ *
400
+ * ⚠ The renderer needs the mime to decide how to draw the thing (a
401
+ * picture, an embedded PDF, a link). Handing it a bare URL makes every
402
+ * exposed copy render as "something to download", which is what a
403
+ * `preview` node is specifically not for.
404
+ */
405
+ resolveFile: (ref: string) => {
406
+ const files = (info.value as { app?: { files?: Array<{ ref: string; name?: string; mime?: string }> } } | null)
407
+ ?.app?.files;
408
+ const f = files?.find((x) => x.ref === ref);
409
+ return { url: client.fileUrl(ref), name: f?.name, mime: f?.mime };
410
+ },
411
+ url: client.url,
412
+ };
413
+ }
414
+
415
+
416
+
417
+ /**
418
+ * Where the BYTES of a share live.
419
+ *
420
+ * ⚠ Not under `/api/`. `/s/<token>` is the address that has always served
421
+ * the file, and it still does — the SPA shell is what a JavaScript browser
422
+ * gets when it NAVIGATES there (`handlers/public_shell.go`), while
423
+ * `?download=1` and `?zip=1` are among the escapes that mean "I am asking for
424
+ * the thing, not for a page". Building a second download route under `/api/`
425
+ * would be a second thing to keep working.
426
+ */
427
+ export function shareDownloadUrl(base: string | undefined, token: string, opts: { zip?: boolean; path?: string } = {}): string {
428
+ const origin = (base ?? '').replace(/\/+$/, '');
429
+ const t = encodeURIComponent(token);
430
+ if (opts.path) return `${origin}/s/${t}/f/${opts.path.split('/').map(encodeURIComponent).join('/')}?download=1`;
431
+ return `${origin}/s/${t}?${opts.zip ? 'zip=1' : 'download=1'}`;
432
+ }
433
+
434
+ /** The no-JS page, for whatever the shell cannot draw yet (a folder walk). */
435
+ export function shareNoJsUrl(base: string | undefined, token: string): string {
436
+ return `${(base ?? '').replace(/\/+$/, '')}/s/${encodeURIComponent(token)}?nojs=1`;
437
+ }
438
+
439
+ /** A share: a file, a folder, or an app plugin's page. */
440
+ export function usePublicShare(token: string, opts: PublicLinkOptions) {
441
+ const link = usePublicLink<PublicShareInfo>(shareRoot(opts.base, token), {
442
+ ...opts,
443
+ hasSurface: (i) => i?.kind === 'app',
444
+ });
445
+ return {
446
+ ...link,
447
+ downloadUrl: (o: { zip?: boolean; path?: string } = {}) => shareDownloadUrl(opts.base, token, o),
448
+ noJsUrl: () => shareNoJsUrl(opts.base, token),
449
+ };
450
+ }
451
+
452
+ /** One file on its way up, as the drop box shows it. */
453
+ export interface PublicUpload {
454
+ name: string;
455
+ size: number;
456
+ /** 0..100, or -1 while the browser has not said anything yet. */
457
+ percent: number;
458
+ /**
459
+ * `refused` — never sent: the link does not take it (its type, its size, one
460
+ * file too many). Said before the transfer, not after it.
461
+ */
462
+ state: 'sending' | 'done' | 'failed' | 'refused';
463
+ error?: string;
464
+ }
465
+
466
+ /** What the person on the drop page adds to the files. */
467
+ export interface PublicUploadOptions {
468
+ /** Their name, when the link asks for it (`limits.ask_name`). */
469
+ name?: string;
470
+ /**
471
+ * The words for a file this page will not send. Handed in by the page
472
+ * because the page has the catalogue; the rule that decides lives in
473
+ * `lib/dropLimits`.
474
+ */
475
+ refusedText?: (reason: DropRefusal) => string;
476
+ }
477
+
478
+ /**
479
+ * A file request: the drop box an outsider uploads into.
480
+ *
481
+ * The upload is XHR and not `fetch` for one reason: `fetch` has no upload
482
+ * progress. A stranger sending a 300 MB file to somebody else's server with
483
+ * no indication that anything is happening will reload the page, and the
484
+ * transfer starts over.
485
+ *
486
+ * ⚠⚠ ONE request per drop, carrying every file of it. The server makes ONE
487
+ * submission folder per request (`<date>_<name>`, drop.go) and counts
488
+ * `max_files` per request; sending the files one by one made three folders out
489
+ * of one drop of three files, and let a person past the per-submission cap by
490
+ * simply sending them separately. The no-JavaScript page always sent them
491
+ * together.
492
+ */
493
+ export function usePublicRequest(token: string, opts: PublicLinkOptions) {
494
+ const link = usePublicLink<PublicRequestInfo>(requestRoot(opts.base, token), opts);
495
+ const uploads = ref<PublicUpload[]>([]);
496
+
497
+ function patchRange(from: number, to: number, p: Partial<PublicUpload>): void {
498
+ uploads.value = uploads.value.map((u, n) => (n >= from && n < to ? { ...u, ...p } : u));
499
+ }
500
+
501
+ function send(files: File[], from: number, name: string): Promise<void> {
502
+ const to = from + files.length;
503
+ return new Promise<void>((resolve) => {
504
+ let xhr: XMLHttpRequest;
505
+ try {
506
+ xhr = new XMLHttpRequest();
507
+ } catch {
508
+ patchRange(from, to, { state: 'failed', error: opts.errorText() });
509
+ resolve();
510
+ return;
511
+ }
512
+ xhr.open('POST', link.url('/upload'));
513
+ xhr.withCredentials = true;
514
+ xhr.setRequestHeader('Accept', 'application/json');
515
+ xhr.setRequestHeader('Accept-Language', opts.locale());
516
+ xhr.upload.onprogress = (e) => {
517
+ if (e.lengthComputable && e.total > 0) patchRange(from, to, { percent: Math.round((e.loaded / e.total) * 100) });
518
+ };
519
+ xhr.onload = () => {
520
+ if (xhr.status >= 200 && xhr.status < 300) {
521
+ patchRange(from, to, { state: 'done', percent: 100 });
522
+ } else {
523
+ // ⚠ The server says why, in the visitor's language (`message`,
524
+ // drop.go `refuse`). An older server sends only the code; the
525
+ // page's own "could not be sent" is then the honest fallback.
526
+ let msg = '';
527
+ try {
528
+ msg = String((JSON.parse(xhr.responseText) as { message?: string })?.message ?? '');
529
+ } catch {
530
+ msg = '';
531
+ }
532
+ patchRange(from, to, { state: 'failed', error: msg || opts.errorText() });
533
+ }
534
+ resolve();
535
+ };
536
+ xhr.onerror = () => {
537
+ patchRange(from, to, { state: 'failed', error: opts.errorText() });
538
+ resolve();
539
+ };
540
+ const form = new FormData();
541
+ for (const f of files) form.append('file', f, f.name);
542
+ if (name) form.append('uploader_name', name);
543
+ xhr.send(form);
544
+ });
545
+ }
546
+
547
+ /** Everything the person dropped: what the link takes goes up in one request, the rest is said. */
548
+ async function upload(files: File[] | FileList, o: PublicUploadOptions = {}): Promise<void> {
549
+ const info = link.info.value;
550
+ const { accepted, refused } = checkDropFiles(Array.from(files), info?.limits, info?.uploads_left ?? null);
551
+ const said = o.refusedText ?? (() => opts.errorText());
552
+ uploads.value = [
553
+ ...uploads.value,
554
+ ...refused.map(({ file, reason }) => ({
555
+ name: file.name,
556
+ size: file.size,
557
+ percent: -1,
558
+ state: 'refused' as const,
559
+ error: said(reason),
560
+ })),
561
+ ];
562
+ if (!accepted.length) return;
563
+ const from = uploads.value.length;
564
+ uploads.value = [
565
+ ...uploads.value,
566
+ ...accepted.map((f) => ({ name: f.name, size: f.size, percent: -1, state: 'sending' as const })),
567
+ ];
568
+ await send(accepted, from, (o.name ?? '').trim());
569
+ await link.load();
570
+ }
571
+
572
+ return { ...link, uploads, upload };
573
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * usePublicPage — the `/p/<token>` walk, kept as a NAME.
3
+ *
4
+ * ⚠⚠ v3 §1 retired `/p/*`: an app plugin's public page is a real share now
5
+ * (`/s/<token>` carrying a `page_id`), so the administrator revokes it in
6
+ * **Shares** like every other link and there is one PIN implementation, one
7
+ * expiry policy and one visit counter instead of two.
8
+ *
9
+ * ⚠ So this file holds no implementation any more, and that is the point.
10
+ * The walk it used to own — info → PIN → view → events — turned out to be
11
+ * the SAME walk a share and a file request make, and `usePublicLink` is that
12
+ * one walk at whichever root the link lives under. Keeping a second copy
13
+ * here "for the transition" is exactly how two public pages came to disagree
14
+ * about their own PIN box in the first place.
15
+ *
16
+ * The exports survive because a host may be importing them from the package:
17
+ * they are the old names pointed at the shared machinery, and nothing new
18
+ * should reach for them.
19
+ */
20
+ import type { PublicPageInfo } from '../types/Plugins';
21
+ import {
22
+ PublicLinkError,
23
+ pageRoot,
24
+ publicLinkClient,
25
+ usePublicLink,
26
+ type PublicLinkOptions,
27
+ type PublicStatus,
28
+ } from './usePublicLink';
29
+
30
+ export type { PinFailure } from './usePublicLink';
31
+
32
+ /** @deprecated The old name of {@link PublicLinkError}. */
33
+ export { PublicLinkError as PublicPageError };
34
+
35
+ /**
36
+ * @deprecated `/p/` is retired (v3 §1). The status names moved with the
37
+ * walk: what used to be `'surface'` is `'ready'`, because a share's body is a
38
+ * document or a folder at least as often as it is a plugin's screen.
39
+ */
40
+ export type PublicPageStatus = PublicStatus;
41
+
42
+ export type PublicPageClientOptions = PublicLinkOptions;
43
+
44
+ /** `/api/p/{token}` and its children. */
45
+ export function publicPageUrl(base: string | undefined, token: string, tail = ''): string {
46
+ return pageRoot(base, token)(tail);
47
+ }
48
+
49
+ /** @deprecated Use `publicLinkClient(shareRoot(base, token), …)`. */
50
+ export function publicPageClient(token: string, opts: PublicPageClientOptions) {
51
+ return publicLinkClient<PublicPageInfo>(pageRoot(opts.base, token), opts);
52
+ }
53
+
54
+ export type PublicPageClient = ReturnType<typeof publicPageClient>;
55
+
56
+ /** @deprecated Use `usePublicShare` — an app's public page IS a share (v3 §1.1). */
57
+ export function usePublicPage(token: string, opts: PublicPageClientOptions & { errorText: () => string }) {
58
+ return usePublicLink<PublicPageInfo>(pageRoot(opts.base, token), { ...opts, hasSurface: () => true });
59
+ }
60
+
61
+ export type PublicPageStore = ReturnType<typeof usePublicPage>;