cursedbelt 4.6.0 → 5.0.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 (144) hide show
  1. package/README.md +36 -0
  2. package/dist/react/chat/index.d.ts +1 -0
  3. package/dist/react/chat/index.d.ts.map +1 -1
  4. package/dist/react/chat/index.js +1 -0
  5. package/dist/react/chat/index.js.map +1 -1
  6. package/dist/react/chat/usePresence.d.ts +75 -0
  7. package/dist/react/chat/usePresence.d.ts.map +1 -0
  8. package/dist/react/chat/usePresence.js +109 -0
  9. package/dist/react/chat/usePresence.js.map +1 -0
  10. package/dist/react/components/SecretInput.d.ts +7 -2
  11. package/dist/react/components/SecretInput.d.ts.map +1 -1
  12. package/dist/react/components/SecretInput.js +5 -2
  13. package/dist/react/components/SecretInput.js.map +1 -1
  14. package/dist/react/components/TooltipImpl.d.ts.map +1 -1
  15. package/dist/react/components/TooltipImpl.js +52 -3
  16. package/dist/react/components/TooltipImpl.js.map +1 -1
  17. package/dist/react/file-tree/FileTree.d.ts.map +1 -1
  18. package/dist/react/file-tree/FileTree.js +19 -1
  19. package/dist/react/file-tree/FileTree.js.map +1 -1
  20. package/dist/react/index.d.ts +1 -0
  21. package/dist/react/index.d.ts.map +1 -1
  22. package/dist/react/index.js +1 -0
  23. package/dist/react/index.js.map +1 -1
  24. package/dist/react/master-lock/MasterLockDialog.d.ts.map +1 -1
  25. package/dist/react/master-lock/MasterLockDialog.js +2 -1
  26. package/dist/react/master-lock/MasterLockDialog.js.map +1 -1
  27. package/dist/react/master-lock/MasterLockProvider.d.ts +12 -18
  28. package/dist/react/master-lock/MasterLockProvider.d.ts.map +1 -1
  29. package/dist/react/master-lock/MasterLockProvider.js +14 -20
  30. package/dist/react/master-lock/MasterLockProvider.js.map +1 -1
  31. package/dist/react/media/MediaUploadControl.d.ts +3 -7
  32. package/dist/react/media/MediaUploadControl.d.ts.map +1 -1
  33. package/dist/react/media/MediaUploadControl.js +4 -13
  34. package/dist/react/media/MediaUploadControl.js.map +1 -1
  35. package/dist/react/media/fileUploadClient.d.ts +0 -8
  36. package/dist/react/media/fileUploadClient.d.ts.map +1 -1
  37. package/dist/react/media/fileUploadClient.js +0 -8
  38. package/dist/react/media/fileUploadClient.js.map +1 -1
  39. package/dist/react/media/hlsSource.d.ts.map +1 -1
  40. package/dist/react/media/hlsSource.js +4 -2
  41. package/dist/react/media/hlsSource.js.map +1 -1
  42. package/dist/react/media/index.d.ts +0 -4
  43. package/dist/react/media/index.d.ts.map +1 -1
  44. package/dist/react/media/index.js +0 -4
  45. package/dist/react/media/index.js.map +1 -1
  46. package/dist/react/sessionAuth.d.ts +1 -2
  47. package/dist/react/sessionAuth.d.ts.map +1 -1
  48. package/dist/react/sessionAuth.js +2 -3
  49. package/dist/react/sessionAuth.js.map +1 -1
  50. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  51. package/dist/styles-areas/activity-bar.css +1 -1
  52. package/dist/styles-areas/auth.css +1 -1
  53. package/dist/styles-areas/charts.css +1 -1
  54. package/dist/styles-areas/chat.css +1 -1
  55. package/dist/styles-areas/core.css +1 -1
  56. package/dist/styles-areas/data-table.css +1 -1
  57. package/dist/styles-areas/disk-usage.css +1 -1
  58. package/dist/styles-areas/fields.css +1 -1
  59. package/dist/styles-areas/file-tree.css +1 -1
  60. package/dist/styles-areas/master-detail.css +1 -1
  61. package/dist/styles-areas/master-lock.css +108 -2
  62. package/dist/styles-areas/media-gallery.css +1 -1
  63. package/dist/styles-areas/media.css +1 -17
  64. package/dist/styles-areas/nav.css +1 -1
  65. package/dist/styles-areas/notifications.css +1 -1
  66. package/dist/styles-areas/spreadsheet.css +1 -1
  67. package/dist/styles-areas/stats.css +1 -1
  68. package/dist/styles-areas/wizard.css +1 -1
  69. package/dist/styles-areas/workbook-viewer.css +1 -1
  70. package/dist/styles-utilities.css +0 -16
  71. package/dist/testing/_noBun.d.ts +2 -0
  72. package/dist/testing/_noBun.d.ts.map +1 -0
  73. package/dist/testing/_noBun.js +10 -0
  74. package/dist/testing/_noBun.js.map +1 -0
  75. package/package.json +42 -19
  76. package/scripts/checkAreaStyles.spec.ts +71 -0
  77. package/scripts/checkAreaStyles.ts +150 -0
  78. package/scripts/guardrailsEnforce.spec.ts +43 -0
  79. package/scripts/guardrailsEnforce.ts +46 -1
  80. package/scripts/styleAreas.ts +1 -2
  81. package/src/barrelsReachNoOptionalPeer.spec.ts +6 -3
  82. package/src/bundlerOnlyEntries.spec.ts +101 -0
  83. package/src/declaredImports.spec.ts +4 -0
  84. package/src/optionalPeersTable.spec.ts +127 -0
  85. package/src/react/chat/index.ts +8 -0
  86. package/src/react/chat/usePresence.spec.tsx +138 -0
  87. package/src/react/chat/usePresence.ts +185 -0
  88. package/src/react/components/SecretInput.tsx +12 -1
  89. package/src/react/components/TooltipImpl.tsx +54 -5
  90. package/src/react/file-tree/FileTree.tsx +19 -1
  91. package/src/react/file-tree/fileTreeThumbs.spec.tsx +90 -0
  92. package/src/react/index.ts +8 -0
  93. package/src/react/master-lock/MasterLockDialog.tsx +13 -9
  94. package/src/react/master-lock/MasterLockProvider.spec.tsx +17 -0
  95. package/src/react/master-lock/MasterLockProvider.tsx +16 -19
  96. package/src/react/media/MediaUploadControl.tsx +8 -29
  97. package/src/react/media/fileUploadClient.ts +7 -17
  98. package/src/react/media/hlsSource.ts +4 -1
  99. package/src/react/media/index.ts +0 -16
  100. package/src/react/sessionAuth.ts +2 -3
  101. package/src/scriptsResolve.spec.ts +66 -0
  102. package/src/styles-areas/activity-bar.css +1 -1
  103. package/src/styles-areas/auth.css +1 -1
  104. package/src/styles-areas/charts.css +1 -1
  105. package/src/styles-areas/chat.css +1 -1
  106. package/src/styles-areas/core.css +1 -1
  107. package/src/styles-areas/data-table.css +1 -1
  108. package/src/styles-areas/disk-usage.css +1 -1
  109. package/src/styles-areas/fields.css +1 -1
  110. package/src/styles-areas/file-tree.css +1 -1
  111. package/src/styles-areas/master-detail.css +1 -1
  112. package/src/styles-areas/master-lock.css +108 -2
  113. package/src/styles-areas/media-gallery.css +1 -1
  114. package/src/styles-areas/media.css +1 -17
  115. package/src/styles-areas/nav.css +1 -1
  116. package/src/styles-areas/notifications.css +1 -1
  117. package/src/styles-areas/spreadsheet.css +1 -1
  118. package/src/styles-areas/stats.css +1 -1
  119. package/src/styles-areas/wizard.css +1 -1
  120. package/src/styles-areas/workbook-viewer.css +1 -1
  121. package/src/styles-utilities.css +0 -16
  122. package/src/testing/_noBun.ts +12 -0
  123. package/src/verifyGraph.spec.ts +11 -0
  124. package/dist/react/media/PrivateIndexView.d.ts +0 -6
  125. package/dist/react/media/PrivateIndexView.d.ts.map +0 -1
  126. package/dist/react/media/PrivateIndexView.js +0 -44
  127. package/dist/react/media/PrivateIndexView.js.map +0 -1
  128. package/dist/react/media/PrivateModeBadge.d.ts +0 -17
  129. package/dist/react/media/PrivateModeBadge.d.ts.map +0 -1
  130. package/dist/react/media/PrivateModeBadge.js +0 -24
  131. package/dist/react/media/PrivateModeBadge.js.map +0 -1
  132. package/dist/react/media/privateClient.d.ts +0 -34
  133. package/dist/react/media/privateClient.d.ts.map +0 -1
  134. package/dist/react/media/privateClient.js +0 -36
  135. package/dist/react/media/privateClient.js.map +0 -1
  136. package/dist/react/media/usePrivateUnlock.d.ts +0 -25
  137. package/dist/react/media/usePrivateUnlock.d.ts.map +0 -1
  138. package/dist/react/media/usePrivateUnlock.js +0 -68
  139. package/dist/react/media/usePrivateUnlock.js.map +0 -1
  140. package/src/react/media/PrivateIndexView.tsx +0 -113
  141. package/src/react/media/PrivateModeBadge.tsx +0 -50
  142. package/src/react/media/privateClient.spec.ts +0 -113
  143. package/src/react/media/privateClient.ts +0 -82
  144. package/src/react/media/usePrivateUnlock.ts +0 -132
@@ -1 +0,0 @@
1
- {"version":3,"file":"privateClient.d.ts","sourceRoot":"","sources":["../../../src/react/media/privateClient.ts"],"names":[],"mappings":"AAOA,2FAA2F;AAC3F,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,mBAAmB;IAClC;8BAC0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;sDACkD;IAClD,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED,eAAO,MAAM,cAAc,GAAI,KAAK,mBAAmB,KAAG,MACJ,CAAC;AAwBvD,6FAA6F;AAC7F,eAAO,MAAM,yBAAyB,GACpC,KAAK,mBAAmB,EACxB,UAAU,MAAM,KACf,OAAO,CAAC,YAAY,EAAE,CAA+C,CAAC;AAEzE;8CAC8C;AAC9C,eAAO,MAAM,sBAAsB,GACjC,KAAK,mBAAmB,EACxB,OAAO,MAAM,KACZ,OAAO,CAAC,YAAY,EAAE,CAAqD,CAAC;AAE/E;oEACoE;AACpE,eAAO,MAAM,sBAAsB,GACjC,KAAK,mBAAmB,EACxB,OAAO,MAAM,EACb,UAAU,MAAM,EAChB,SAAS,MAAM,KACd,MAAoF,CAAC;AAExF;4EAC4E;AAC5E,eAAO,MAAM,uBAAuB,GAClC,KAAK,mBAAmB,EACxB,OAAO,MAAM,EACb,QAAQ,MAAM,KACb,MAA4D,CAAC"}
@@ -1,36 +0,0 @@
1
- // ── Private-media client helpers (hook-free) ────────────────────────────────────
2
- // The fetch/URL logic for the private-media routes, usable OUTSIDE React state — the unlock
3
- // token is held by one component (an unlock plugin) and handed to others (files/gallery
4
- // plugins) via a cross-plugin port, so consumers of a passed-in token need these standalone
5
- // helpers rather than their own usePrivateUnlock state. usePrivateUnlock delegates here:
6
- // one source of truth for the endpoints' shapes.
7
- export const privateBaseUrl = (cfg) => `${cfg.apiBase ?? ''}${cfg.basePath ?? '/_private'}`;
8
- const guardedHeaders = (cfg) => ({
9
- 'content-type': 'application/json',
10
- ...(cfg.authHeaders?.() ?? {}),
11
- });
12
- const postForItems = async (cfg, path, body) => {
13
- const res = await fetch(`${privateBaseUrl(cfg)}${path}`, {
14
- method: 'POST',
15
- headers: guardedHeaders(cfg),
16
- body: JSON.stringify(body),
17
- // Split-origin hosts (SPA on the apex, API on a subdomain) authenticate these
18
- // guarded calls with the app's httpOnly session cookie.
19
- credentials: 'include',
20
- });
21
- if (!res.ok)
22
- return [];
23
- return (await res.json()).items ?? [];
24
- };
25
- /** Try `password` against every item; returns the ones it decrypts (with their metadata). */
26
- export const revealPrivateWithPassword = (cfg, password) => postForItems(cfg, '/reveal', { password });
27
- /** Reveal the items an UNLOCK TOKEN opens — no password re-entry. The token must belong to the
28
- * session caller (the server enforces it). */
29
- export const revealPrivateWithToken = (cfg, token) => postForItems(cfg, '/reveal-unlocked', { token });
30
- /** The token-embedded HLS master URL for a private video — same shape usePrivateUnlock.hlsUrl
31
- * builds. Needs no auth header (the token IS the authorization). */
32
- export const privateHlsUrlWithToken = (cfg, token, sourceId, videoId) => `${privateBaseUrl(cfg)}/u/${token}/hls/${sourceId}/${videoId}/master.m3u8`;
33
- /** The token-embedded URL for a private NON-video file (photo/doc) stored as an encrypted
34
- * blob — served decrypted by the gated blob route. Needs no auth header. */
35
- export const privateFileUrlWithToken = (cfg, token, fileId) => `${privateBaseUrl(cfg)}/u/${token}/file/${fileId}`;
36
- //# sourceMappingURL=privateClient.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"privateClient.js","sourceRoot":"","sources":["../../../src/react/media/privateClient.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,4FAA4F;AAC5F,wFAAwF;AACxF,4FAA4F;AAC5F,yFAAyF;AACzF,iDAAiD;AAuBjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAwB,EAAU,EAAE,CACjE,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,GAAG,GAAG,CAAC,QAAQ,IAAI,WAAW,EAAE,CAAC;AAEvD,MAAM,cAAc,GAAG,CAAC,GAAwB,EAA0B,EAAE,CAAC,CAAC;IAC5E,cAAc,EAAE,kBAAkB;IAClC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC;CAC/B,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,KAAK,EACxB,GAAwB,EACxB,IAAY,EACZ,IAA6B,EACJ,EAAE;IAC3B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,EAAE;QACvD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,cAAc,CAAC,GAAG,CAAC;QAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAC1B,8EAA8E;QAC9E,wDAAwD;QACxD,WAAW,EAAE,SAAS;KACvB,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,CAAC;IACvB,OAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA+B,CAAC,KAAK,IAAI,EAAE,CAAC;AACvE,CAAC,CAAC;AAEF,6FAA6F;AAC7F,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,GAAwB,EACxB,QAAgB,EACS,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;AAEzE;8CAC8C;AAC9C,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,GAAwB,EACxB,KAAa,EACY,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,kBAAkB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;AAE/E;oEACoE;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,GAAwB,EACxB,KAAa,EACb,QAAgB,EAChB,OAAe,EACP,EAAE,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,KAAK,QAAQ,QAAQ,IAAI,OAAO,cAAc,CAAC;AAExF;4EAC4E;AAC5E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,GAAwB,EACxB,KAAa,EACb,MAAc,EACN,EAAE,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,KAAK,SAAS,MAAM,EAAE,CAAC"}
@@ -1,25 +0,0 @@
1
- import { type PrivateClientConfig, type PrivateIndexRow, type RevealedItem } from './privateClient.js';
2
- export type { PrivateIndexRow, RevealedItem } from './privateClient.js';
3
- export type UsePrivateUnlockOptions = PrivateClientConfig;
4
- export type PrivateUnlock = {
5
- token: string | null;
6
- mode: 'standard' | 'deniable' | null;
7
- busy: boolean;
8
- error: string | null;
9
- /** Unlock: pass a password for a deniable set, or omit for the standard (server-key) set.
10
- * Returns true on success. */
11
- unlock(password?: string): Promise<boolean>;
12
- /** Drop the token (re-lock). */
13
- lock(): void;
14
- /** Fetch the clear index (sizes + created-at) for the cleanup view. */
15
- loadIndex(): Promise<PrivateIndexRow[]>;
16
- /** Try a password against every item; returns the ones it decrypts (with their metadata). */
17
- reveal(password: string): Promise<RevealedItem[]>;
18
- /** Reveal the items the CURRENT unlock token opens — no password re-entry. Throws when
19
- * locked (no token). */
20
- revealUnlocked(): Promise<RevealedItem[]>;
21
- /** Build the token-embedded HLS master URL for a private video (null when locked). */
22
- hlsUrl(sourceId: string, videoId: string): string | null;
23
- };
24
- export declare function usePrivateUnlock(opts?: UsePrivateUnlockOptions): PrivateUnlock;
25
- //# sourceMappingURL=usePrivateUnlock.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"usePrivateUnlock.d.ts","sourceRoot":"","sources":["../../../src/react/media/usePrivateUnlock.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAGpB,KAAK,YAAY,EAGlB,MAAM,oBAAoB,CAAC;AAU5B,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAExE,MAAM,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE1D,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;kCAC8B;IAC9B,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,gCAAgC;IAChC,IAAI,IAAI,IAAI,CAAC;IACb,uEAAuE;IACvE,SAAS,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACxC,6FAA6F;IAC7F,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAClD;4BACwB;IACxB,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC1C,sFAAsF;IACtF,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC1D,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG,aAAa,CAuFlF"}
@@ -1,68 +0,0 @@
1
- import React from 'react';
2
- import { privateBaseUrl, privateHlsUrlWithToken, revealPrivateWithPassword, revealPrivateWithToken, } from './privateClient.js';
3
- export function usePrivateUnlock(opts = {}) {
4
- const { apiBase, basePath, authHeaders } = opts;
5
- const cfg = React.useMemo(() => ({ apiBase, basePath, authHeaders }), [apiBase, basePath, authHeaders]);
6
- const base = privateBaseUrl(cfg);
7
- const [token, setToken] = React.useState(null);
8
- const [mode, setMode] = React.useState(null);
9
- const [busy, setBusy] = React.useState(false);
10
- const [error, setError] = React.useState(null);
11
- const guarded = React.useCallback((extra) => ({
12
- 'content-type': 'application/json',
13
- ...(authHeaders?.() ?? {}),
14
- ...extra,
15
- }), [authHeaders]);
16
- const unlock = React.useCallback(async (password) => {
17
- setBusy(true);
18
- setError(null);
19
- try {
20
- const res = await fetch(`${base}/unlock`, {
21
- method: 'POST',
22
- headers: guarded(),
23
- body: JSON.stringify(password ? { password } : {}),
24
- // Split-origin hosts authenticate via the app's httpOnly session cookie —
25
- // matches postForItems in ./privateClient and createServiceClient.
26
- credentials: 'include',
27
- });
28
- if (!res.ok) {
29
- setError('Unlock failed');
30
- return false;
31
- }
32
- const data = (await res.json());
33
- setToken(data.token);
34
- setMode(data.mode);
35
- return true;
36
- }
37
- catch {
38
- setError('Unlock failed');
39
- return false;
40
- }
41
- finally {
42
- setBusy(false);
43
- }
44
- }, [base, guarded]);
45
- const lock = React.useCallback(() => {
46
- setToken(null);
47
- setMode(null);
48
- }, []);
49
- const loadIndex = React.useCallback(async () => {
50
- const res = await fetch(`${base}/index`, { headers: guarded(), credentials: 'include' });
51
- if (!res.ok)
52
- return [];
53
- return (await res.json()).items ?? [];
54
- }, [base, guarded]);
55
- const reveal = React.useCallback((password) => revealPrivateWithPassword(cfg, password), [cfg]);
56
- const revealUnlocked = React.useCallback(() => {
57
- if (!token)
58
- return Promise.reject(new Error('locked — unlock first'));
59
- return revealPrivateWithToken(cfg, token);
60
- }, [cfg, token]);
61
- const hlsUrl = React.useCallback((sourceId, videoId) => token ? privateHlsUrlWithToken(cfg, token, sourceId, videoId) : null, [cfg, token]);
62
- // Memoise the returned object so it's a STABLE reference across renders (the callbacks are
63
- // already useCallback-stable; only real state changes flip it). Without this, consumers doing
64
- // `useEffect(() => { unlock.loadIndex()… }, [unlock])` would loop — a new object every render
65
- // re-runs the effect → setState → re-render → "Maximum update depth exceeded".
66
- return React.useMemo(() => ({ token, mode, busy, error, unlock, lock, loadIndex, reveal, revealUnlocked, hlsUrl }), [token, mode, busy, error, unlock, lock, loadIndex, reveal, revealUnlocked, hlsUrl]);
67
- }
68
- //# sourceMappingURL=usePrivateUnlock.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"usePrivateUnlock.js","sourceRoot":"","sources":["../../../src/react/media/usePrivateUnlock.ts"],"names":[],"mappings":"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"}
@@ -1,113 +0,0 @@
1
- import { formatBytes as fmtBytes } from 'cwip/format';
2
- import React from 'react';
3
- import { Button } from '../components/Button.js';
4
- import { Input } from '../components/Input.js';
5
- import type { PrivateIndexRow, PrivateUnlock } from './usePrivateUnlock.js';
6
-
7
- // The locked-index cleanup view: every private item's SIZE + created-date (never its title,
8
- // thumbnail, or notes), so a user can prune media whose password they've lost. A password field
9
- // runs "try-a-password" — items it decrypts light up with their revealed title; the rest stay
10
- // opaque. Themed off cursedbelt tokens (with fallbacks so it renders anywhere).
11
-
12
- const fmtDate = (ts: number): string => new Date(ts).toISOString().slice(0, 10);
13
-
14
- export function PrivateIndexView({ unlock }: { unlock: PrivateUnlock }) {
15
- const [rows, setRows] = React.useState<PrivateIndexRow[]>([]);
16
- const [revealed, setRevealed] = React.useState<Map<string, Record<string, unknown>>>(new Map());
17
- const [password, setPassword] = React.useState('');
18
- const [trying, setTrying] = React.useState(false);
19
-
20
- React.useEffect(() => {
21
- unlock.loadIndex().then(setRows);
22
- }, [unlock]);
23
-
24
- async function tryPassword() {
25
- if (!password) return;
26
- setTrying(true);
27
- try {
28
- const items = await unlock.reveal(password);
29
- setRevealed(new Map(items.map((i) => [i.id, i.meta])));
30
- } finally {
31
- setTrying(false);
32
- }
33
- }
34
-
35
- const totalBytes = rows.reduce((s, r) => s + r.sizeBytes, 0);
36
- const matchCount = revealed.size;
37
-
38
- return (
39
- <div className='flex flex-col gap-3 text-sm'>
40
- <div className='flex items-center justify-between gap-3'>
41
- <div className='text-[color:var(--muted-foreground)]'>
42
- {rows.length} locked {rows.length === 1 ? 'file' : 'files'} · {fmtBytes(totalBytes)}
43
- </div>
44
- <div className='flex items-center gap-2'>
45
- <Input
46
- type='password'
47
- value={password}
48
- onChange={(e) => setPassword(e.target.value)}
49
- onEnter={() => tryPassword()}
50
- placeholder='Try a password…'
51
- aria-label='Try a password'
52
- className='h-auto w-auto bg-transparent px-2 py-1'
53
- />
54
- <Button
55
- size='sm'
56
- onClick={tryPassword}
57
- disabled={trying || !password}
58
- loading={trying}
59
- tooltip='Try this password against every locked file'
60
- >
61
- {trying ? 'Trying…' : 'Reveal'}
62
- </Button>
63
- </div>
64
- </div>
65
-
66
- {matchCount > 0 ? (
67
- <div className='text-[color:var(--accent)]'>
68
- This password unlocks {matchCount} {matchCount === 1 ? 'file' : 'files'}.
69
- </div>
70
- ) : null}
71
-
72
- <div className='overflow-hidden rounded-lg border border-[color:var(--border-strong)]'>
73
- {rows.map((r) => {
74
- const meta = revealed.get(r.id);
75
- const title = typeof meta?.title === 'string' ? meta.title : null;
76
- return (
77
- <div
78
- key={r.id}
79
- className={`flex items-center justify-between gap-3 border-b border-[color:var(--border-strong)] px-3 py-2 last:border-b-0 ${
80
- // `--card-overlay` (white/navy per theme.css) isn't the tint this row wants —
81
- // the original rgba(16,185,129,0.08) fallback was an emerald-accent highlight,
82
- // not the neutral card-overlay token, so build the same tint from the real
83
- // accent token via color-mix instead of a literal fallback color.
84
- title ? 'bg-[color:color-mix(in_srgb,var(--accent-solid)_8%,transparent)]' : ''
85
- }`}
86
- >
87
- <div className='min-w-0 flex-1 truncate'>
88
- {title ? (
89
- <span className='font-medium text-[color:var(--accent)]'>{title}</span>
90
- ) : (
91
- <span className='font-mono text-[color:var(--muted-foreground)]'>
92
- 🔒 {r.id.slice(0, 8)}…
93
- </span>
94
- )}
95
- </div>
96
- <div className='shrink-0 tabular-nums text-[color:var(--muted-foreground)]'>
97
- {fmtBytes(r.sizeBytes)}
98
- </div>
99
- <div className='shrink-0 tabular-nums text-[color:var(--muted-foreground)]'>
100
- {fmtDate(r.createdAt)}
101
- </div>
102
- </div>
103
- );
104
- })}
105
- {rows.length === 0 ? (
106
- <div className='px-3 py-6 text-center text-[color:var(--muted-foreground)]'>
107
- No private files.
108
- </div>
109
- ) : null}
110
- </div>
111
- </div>
112
- );
113
- }
@@ -1,50 +0,0 @@
1
- import { cn } from '../lib/cn.js';
2
- import { Tooltip } from '../components/Tooltip.js';
3
-
4
- export interface PrivateModeBadgeProps {
5
- /** How many items are private (shown in the label). */
6
- hiddenCount: number;
7
- /** Whether the feed is currently filtered to private-only. */
8
- active: boolean;
9
- /** Toggle the private-only filter. */
10
- onToggle: () => void;
11
- className?: string;
12
- }
13
-
14
- /**
15
- * The "🔓 N private" toggle pill shown in a media gallery's header once private
16
- * mode is unlocked — click filters the feed to private-only items. One shared
17
- * implementation for every gallery (video/photo/media); it was previously
18
- * copy-pasted per plugin with per-plugin CSS classes.
19
- */
20
- export function PrivateModeBadge({ hiddenCount, active, onToggle, className }: PrivateModeBadgeProps) {
21
- return (
22
- <Tooltip content='Private mode unlocked — click to show only private items'>
23
- <button
24
- // guardrails-ignore no-raw-action-button: compact header pill with pressed
25
- // state — Button's control-height/variant chrome doesn't fit a badge-sized
26
- // toggle; this keeps the exact badge geometry the galleries share.
27
- type='button'
28
- data-cb-private-badge=''
29
- aria-pressed={active}
30
- onClick={onToggle}
31
- className={cn(
32
- 'inline-flex shrink-0 cursor-pointer items-center gap-1 rounded-full border border-border bg-surface px-2.5 py-0.5 text-xs font-medium text-muted-foreground transition-colors',
33
- 'hover:border-accent hover:text-foreground',
34
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
35
- // `text-accent-strong`, not plain `text-accent` — the /15 tint only clears
36
- // 4.43:1 with the base accent hue, under AA (cbux-101). (Previously
37
- // `text-accent-foreground`, a token this library never defines — an
38
- // undefined Tailwind color utility compiles to no rule at all, so the
39
- // pill silently fell back to inherited text color instead of an
40
- // intentional one.)
41
- active && 'border-accent bg-accent/15 text-accent-strong',
42
- className,
43
- )}
44
- >
45
- <span aria-hidden>🔓</span>
46
- {hiddenCount} private
47
- </button>
48
- </Tooltip>
49
- );
50
- }
@@ -1,113 +0,0 @@
1
- import { afterEach, describe, expect, mock, test } from 'bun:test';
2
- import { act, renderHook } from '@testing-library/react';
3
- import {
4
- privateHlsUrlWithToken,
5
- revealPrivateWithPassword,
6
- revealPrivateWithToken,
7
- } from './privateClient.js';
8
- import { usePrivateUnlock } from './usePrivateUnlock.js';
9
-
10
- // Mock fetch — the helpers are pure client plumbing; assert the request they build and the
11
- // response shape they return.
12
- const realFetch = globalThis.fetch;
13
- type FetchCall = { url: string; init?: RequestInit };
14
- const mockFetch = (status: number, body: unknown) => {
15
- const calls: FetchCall[] = [];
16
- globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
17
- calls.push({ url: String(url), init });
18
- return new Response(JSON.stringify(body), {
19
- status,
20
- headers: { 'content-type': 'application/json' },
21
- });
22
- }) as unknown as typeof fetch;
23
- return calls;
24
- };
25
-
26
- afterEach(() => {
27
- globalThis.fetch = realFetch;
28
- });
29
-
30
- describe('private client helpers (hook-free)', () => {
31
- test('revealPrivateWithToken POSTs the token to /reveal-unlocked and returns items', async () => {
32
- const items = [{ id: 'a1', meta: { title: 'Alpha' } }];
33
- const calls = mockFetch(200, { items });
34
-
35
- const out = await revealPrivateWithToken(
36
- { apiBase: 'https://api.example', authHeaders: () => ({ authorization: 'Bearer T' }) },
37
- 'tok123',
38
- );
39
- expect(out).toEqual(items);
40
- expect(calls[0].url).toBe('https://api.example/_private/reveal-unlocked');
41
- expect(calls[0].init?.method).toBe('POST');
42
- expect(JSON.parse(calls[0].init?.body as string)).toEqual({ token: 'tok123' });
43
- const headers = calls[0].init?.headers as Record<string, string>;
44
- expect(headers.authorization).toBe('Bearer T');
45
- });
46
-
47
- test('revealPrivateWithPassword POSTs the password to /reveal', async () => {
48
- const calls = mockFetch(200, { items: [] });
49
- await revealPrivateWithPassword({ basePath: '/_pm' }, 'pw');
50
- expect(calls[0].url).toBe('/_pm/reveal');
51
- expect(JSON.parse(calls[0].init?.body as string)).toEqual({ password: 'pw' });
52
- });
53
-
54
- test('reveal helpers return [] on a non-OK response', async () => {
55
- mockFetch(401, { error: 'unauthorized' });
56
- expect(await revealPrivateWithToken({}, 'bad')).toEqual([]);
57
- expect(await revealPrivateWithPassword({}, 'bad')).toEqual([]);
58
- });
59
-
60
- test('privateHlsUrlWithToken builds the token-embedded master URL', () => {
61
- expect(privateHlsUrlWithToken({}, 'tok', 'gallery', 'v1')).toBe(
62
- '/_private/u/tok/hls/gallery/v1/master.m3u8',
63
- );
64
- expect(
65
- privateHlsUrlWithToken({ apiBase: 'https://api.example', basePath: '/_pm' }, 'tok', 'g', 'v'),
66
- ).toBe('https://api.example/_pm/u/tok/hls/g/v/master.m3u8');
67
- });
68
- });
69
-
70
- describe('usePrivateUnlock — revealUnlocked', () => {
71
- test('rejects while locked (no token)', async () => {
72
- const { result } = renderHook(() => usePrivateUnlock());
73
- expect(result.current.token).toBeNull();
74
- await expect(result.current.revealUnlocked()).rejects.toThrow('locked');
75
- });
76
-
77
- test('uses the stored token after unlock and hits /reveal-unlocked', async () => {
78
- const items = [{ id: 'x', meta: { title: 'X' } }];
79
- const calls: FetchCall[] = [];
80
- globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
81
- calls.push({ url: String(url), init });
82
- const u = String(url);
83
- if (u.endsWith('/unlock')) {
84
- return Response.json({ token: 'tok-777', mode: 'deniable' });
85
- }
86
- return Response.json({ items });
87
- }) as unknown as typeof fetch;
88
-
89
- const { result } = renderHook(() => usePrivateUnlock());
90
- await act(async () => {
91
- expect(await result.current.unlock('pw')).toBe(true);
92
- });
93
- expect(result.current.token).toBe('tok-777');
94
-
95
- const out = await result.current.revealUnlocked();
96
- expect(out).toEqual(items);
97
- const revealCall = calls.find((c) => c.url.endsWith('/reveal-unlocked'));
98
- expect(revealCall).toBeDefined();
99
- expect(JSON.parse(revealCall?.init?.body as string)).toEqual({ token: 'tok-777' });
100
- });
101
-
102
- test('hlsUrl delegates to privateHlsUrlWithToken (same URL shape)', async () => {
103
- globalThis.fetch = mock(async () =>
104
- Response.json({ token: 'tok-9', mode: 'standard' }),
105
- ) as unknown as typeof fetch;
106
- const { result } = renderHook(() => usePrivateUnlock());
107
- expect(result.current.hlsUrl('g', 'v')).toBeNull(); // locked
108
- await act(async () => {
109
- await result.current.unlock();
110
- });
111
- expect(result.current.hlsUrl('g', 'v')).toBe(privateHlsUrlWithToken({}, 'tok-9', 'g', 'v'));
112
- });
113
- });
@@ -1,82 +0,0 @@
1
- // ── Private-media client helpers (hook-free) ────────────────────────────────────
2
- // The fetch/URL logic for the private-media routes, usable OUTSIDE React state — the unlock
3
- // token is held by one component (an unlock plugin) and handed to others (files/gallery
4
- // plugins) via a cross-plugin port, so consumers of a passed-in token need these standalone
5
- // helpers rather than their own usePrivateUnlock state. usePrivateUnlock delegates here:
6
- // one source of truth for the endpoints' shapes.
7
-
8
- /** A clear index row — non-sensitive facts only (matches the server's PrivateIndexRow). */
9
- export type PrivateIndexRow = {
10
- id: string;
11
- sizeBytes: number;
12
- createdAt: number;
13
- status: 'pending' | 'processing' | 'ready' | 'failed';
14
- };
15
-
16
- export type RevealedItem = { id: string; meta: Record<string, unknown> };
17
-
18
- export interface PrivateClientConfig {
19
- /** Origin+prefix for the private routes. '' = same-origin; an absolute API origin for a
20
- * split-origin deploy. */
21
- apiBase?: string;
22
- /** Route mount prefix (must match mountPrivateRoutes basePath). Default '/_private'. */
23
- basePath?: string;
24
- /** Extra headers for the GUARDED calls (unlock/index/reveal) — e.g. the app's Bearer. The
25
- * token-embedded byte/key URLs need no header. */
26
- authHeaders?: () => Record<string, string>;
27
- }
28
-
29
- export const privateBaseUrl = (cfg: PrivateClientConfig): string =>
30
- `${cfg.apiBase ?? ''}${cfg.basePath ?? '/_private'}`;
31
-
32
- const guardedHeaders = (cfg: PrivateClientConfig): Record<string, string> => ({
33
- 'content-type': 'application/json',
34
- ...(cfg.authHeaders?.() ?? {}),
35
- });
36
-
37
- const postForItems = async (
38
- cfg: PrivateClientConfig,
39
- path: string,
40
- body: Record<string, unknown>,
41
- ): Promise<RevealedItem[]> => {
42
- const res = await fetch(`${privateBaseUrl(cfg)}${path}`, {
43
- method: 'POST',
44
- headers: guardedHeaders(cfg),
45
- body: JSON.stringify(body),
46
- // Split-origin hosts (SPA on the apex, API on a subdomain) authenticate these
47
- // guarded calls with the app's httpOnly session cookie.
48
- credentials: 'include',
49
- });
50
- if (!res.ok) return [];
51
- return ((await res.json()) as { items: RevealedItem[] }).items ?? [];
52
- };
53
-
54
- /** Try `password` against every item; returns the ones it decrypts (with their metadata). */
55
- export const revealPrivateWithPassword = (
56
- cfg: PrivateClientConfig,
57
- password: string,
58
- ): Promise<RevealedItem[]> => postForItems(cfg, '/reveal', { password });
59
-
60
- /** Reveal the items an UNLOCK TOKEN opens — no password re-entry. The token must belong to the
61
- * session caller (the server enforces it). */
62
- export const revealPrivateWithToken = (
63
- cfg: PrivateClientConfig,
64
- token: string,
65
- ): Promise<RevealedItem[]> => postForItems(cfg, '/reveal-unlocked', { token });
66
-
67
- /** The token-embedded HLS master URL for a private video — same shape usePrivateUnlock.hlsUrl
68
- * builds. Needs no auth header (the token IS the authorization). */
69
- export const privateHlsUrlWithToken = (
70
- cfg: PrivateClientConfig,
71
- token: string,
72
- sourceId: string,
73
- videoId: string,
74
- ): string => `${privateBaseUrl(cfg)}/u/${token}/hls/${sourceId}/${videoId}/master.m3u8`;
75
-
76
- /** The token-embedded URL for a private NON-video file (photo/doc) stored as an encrypted
77
- * blob — served decrypted by the gated blob route. Needs no auth header. */
78
- export const privateFileUrlWithToken = (
79
- cfg: PrivateClientConfig,
80
- token: string,
81
- fileId: string,
82
- ): string => `${privateBaseUrl(cfg)}/u/${token}/file/${fileId}`;
@@ -1,132 +0,0 @@
1
- import React from 'react';
2
- import {
3
- type PrivateClientConfig,
4
- type PrivateIndexRow,
5
- privateBaseUrl,
6
- privateHlsUrlWithToken,
7
- type RevealedItem,
8
- revealPrivateWithPassword,
9
- revealPrivateWithToken,
10
- } from './privateClient.js';
11
-
12
- // ── usePrivateUnlock ────────────────────────────────────────────────────────────
13
- // The client state machine for encrypted private media. It drives the cursedbelt-server
14
- // private-media routes: unlock (get a token sealing the set key), the clear index (sizes only,
15
- // for cleanup), try-a-password reveal, and building the token-embedded HLS URL a player loads.
16
- // UI-agnostic — a modal / index view / gallery toggle all consume this one hook. The fetch/URL
17
- // shapes live in ./privateClient (hook-free) so a component holding a PASSED-IN token (via a
18
- // cross-plugin port) can call the same endpoints without this hook's state.
19
-
20
- export type { PrivateIndexRow, RevealedItem } from './privateClient.js';
21
-
22
- export type UsePrivateUnlockOptions = PrivateClientConfig;
23
-
24
- export type PrivateUnlock = {
25
- token: string | null;
26
- mode: 'standard' | 'deniable' | null;
27
- busy: boolean;
28
- error: string | null;
29
- /** Unlock: pass a password for a deniable set, or omit for the standard (server-key) set.
30
- * Returns true on success. */
31
- unlock(password?: string): Promise<boolean>;
32
- /** Drop the token (re-lock). */
33
- lock(): void;
34
- /** Fetch the clear index (sizes + created-at) for the cleanup view. */
35
- loadIndex(): Promise<PrivateIndexRow[]>;
36
- /** Try a password against every item; returns the ones it decrypts (with their metadata). */
37
- reveal(password: string): Promise<RevealedItem[]>;
38
- /** Reveal the items the CURRENT unlock token opens — no password re-entry. Throws when
39
- * locked (no token). */
40
- revealUnlocked(): Promise<RevealedItem[]>;
41
- /** Build the token-embedded HLS master URL for a private video (null when locked). */
42
- hlsUrl(sourceId: string, videoId: string): string | null;
43
- };
44
-
45
- export function usePrivateUnlock(opts: UsePrivateUnlockOptions = {}): PrivateUnlock {
46
- const { apiBase, basePath, authHeaders } = opts;
47
- const cfg = React.useMemo<PrivateClientConfig>(
48
- () => ({ apiBase, basePath, authHeaders }),
49
- [apiBase, basePath, authHeaders],
50
- );
51
- const base = privateBaseUrl(cfg);
52
- const [token, setToken] = React.useState<string | null>(null);
53
- const [mode, setMode] = React.useState<'standard' | 'deniable' | null>(null);
54
- const [busy, setBusy] = React.useState(false);
55
- const [error, setError] = React.useState<string | null>(null);
56
-
57
- const guarded = React.useCallback(
58
- (extra?: Record<string, string>): Record<string, string> => ({
59
- 'content-type': 'application/json',
60
- ...(authHeaders?.() ?? {}),
61
- ...extra,
62
- }),
63
- [authHeaders],
64
- );
65
-
66
- const unlock = React.useCallback(
67
- async (password?: string): Promise<boolean> => {
68
- setBusy(true);
69
- setError(null);
70
- try {
71
- const res = await fetch(`${base}/unlock`, {
72
- method: 'POST',
73
- headers: guarded(),
74
- body: JSON.stringify(password ? { password } : {}),
75
- // Split-origin hosts authenticate via the app's httpOnly session cookie —
76
- // matches postForItems in ./privateClient and createServiceClient.
77
- credentials: 'include',
78
- });
79
- if (!res.ok) {
80
- setError('Unlock failed');
81
- return false;
82
- }
83
- const data = (await res.json()) as { token: string; mode: 'standard' | 'deniable' };
84
- setToken(data.token);
85
- setMode(data.mode);
86
- return true;
87
- } catch {
88
- setError('Unlock failed');
89
- return false;
90
- } finally {
91
- setBusy(false);
92
- }
93
- },
94
- [base, guarded],
95
- );
96
-
97
- const lock = React.useCallback(() => {
98
- setToken(null);
99
- setMode(null);
100
- }, []);
101
-
102
- const loadIndex = React.useCallback(async (): Promise<PrivateIndexRow[]> => {
103
- const res = await fetch(`${base}/index`, { headers: guarded(), credentials: 'include' });
104
- if (!res.ok) return [];
105
- return ((await res.json()) as { items: PrivateIndexRow[] }).items ?? [];
106
- }, [base, guarded]);
107
-
108
- const reveal = React.useCallback(
109
- (password: string): Promise<RevealedItem[]> => revealPrivateWithPassword(cfg, password),
110
- [cfg],
111
- );
112
-
113
- const revealUnlocked = React.useCallback((): Promise<RevealedItem[]> => {
114
- if (!token) return Promise.reject(new Error('locked — unlock first'));
115
- return revealPrivateWithToken(cfg, token);
116
- }, [cfg, token]);
117
-
118
- const hlsUrl = React.useCallback(
119
- (sourceId: string, videoId: string): string | null =>
120
- token ? privateHlsUrlWithToken(cfg, token, sourceId, videoId) : null,
121
- [cfg, token],
122
- );
123
-
124
- // Memoise the returned object so it's a STABLE reference across renders (the callbacks are
125
- // already useCallback-stable; only real state changes flip it). Without this, consumers doing
126
- // `useEffect(() => { unlock.loadIndex()… }, [unlock])` would loop — a new object every render
127
- // re-runs the effect → setState → re-render → "Maximum update depth exceeded".
128
- return React.useMemo(
129
- () => ({ token, mode, busy, error, unlock, lock, loadIndex, reveal, revealUnlocked, hlsUrl }),
130
- [token, mode, busy, error, unlock, lock, loadIndex, reveal, revealUnlocked, hlsUrl],
131
- );
132
- }