cursedbelt 4.7.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.
- package/README.md +36 -0
- package/dist/react/chat/index.d.ts +1 -0
- package/dist/react/chat/index.d.ts.map +1 -1
- package/dist/react/chat/index.js +1 -0
- package/dist/react/chat/index.js.map +1 -1
- package/dist/react/chat/usePresence.d.ts +75 -0
- package/dist/react/chat/usePresence.d.ts.map +1 -0
- package/dist/react/chat/usePresence.js +109 -0
- package/dist/react/chat/usePresence.js.map +1 -0
- package/dist/react/components/SecretInput.d.ts +7 -2
- package/dist/react/components/SecretInput.d.ts.map +1 -1
- package/dist/react/components/SecretInput.js +5 -2
- package/dist/react/components/SecretInput.js.map +1 -1
- package/dist/react/components/TooltipImpl.d.ts.map +1 -1
- package/dist/react/components/TooltipImpl.js +52 -3
- package/dist/react/components/TooltipImpl.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/master-lock/MasterLockDialog.d.ts.map +1 -1
- package/dist/react/master-lock/MasterLockDialog.js +2 -1
- package/dist/react/master-lock/MasterLockDialog.js.map +1 -1
- package/dist/react/master-lock/MasterLockProvider.d.ts +12 -18
- package/dist/react/master-lock/MasterLockProvider.d.ts.map +1 -1
- package/dist/react/master-lock/MasterLockProvider.js +14 -20
- package/dist/react/master-lock/MasterLockProvider.js.map +1 -1
- package/dist/react/media/MediaUploadControl.d.ts +3 -7
- package/dist/react/media/MediaUploadControl.d.ts.map +1 -1
- package/dist/react/media/MediaUploadControl.js +4 -13
- package/dist/react/media/MediaUploadControl.js.map +1 -1
- package/dist/react/media/fileUploadClient.d.ts +0 -8
- package/dist/react/media/fileUploadClient.d.ts.map +1 -1
- package/dist/react/media/fileUploadClient.js +0 -8
- package/dist/react/media/fileUploadClient.js.map +1 -1
- package/dist/react/media/hlsSource.d.ts.map +1 -1
- package/dist/react/media/hlsSource.js +4 -2
- package/dist/react/media/hlsSource.js.map +1 -1
- package/dist/react/media/index.d.ts +0 -4
- package/dist/react/media/index.d.ts.map +1 -1
- package/dist/react/media/index.js +0 -4
- package/dist/react/media/index.js.map +1 -1
- package/dist/react/sessionAuth.d.ts +1 -2
- package/dist/react/sessionAuth.d.ts.map +1 -1
- package/dist/react/sessionAuth.js +2 -3
- package/dist/react/sessionAuth.js.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
- package/dist/styles-areas/activity-bar.css +1 -1
- package/dist/styles-areas/auth.css +1 -1
- package/dist/styles-areas/charts.css +1 -1
- package/dist/styles-areas/chat.css +1 -1
- package/dist/styles-areas/core.css +1 -1
- package/dist/styles-areas/data-table.css +1 -1
- package/dist/styles-areas/disk-usage.css +1 -1
- package/dist/styles-areas/fields.css +1 -1
- package/dist/styles-areas/master-detail.css +1 -1
- package/dist/styles-areas/master-lock.css +108 -2
- package/dist/styles-areas/media-gallery.css +1 -1
- package/dist/styles-areas/media.css +1 -17
- package/dist/styles-areas/nav.css +1 -1
- package/dist/styles-areas/notifications.css +1 -1
- package/dist/styles-areas/spreadsheet.css +1 -1
- package/dist/styles-areas/stats.css +1 -1
- package/dist/styles-areas/wizard.css +1 -1
- package/dist/styles-areas/workbook-viewer.css +1 -1
- package/dist/styles-utilities.css +0 -16
- package/dist/testing/_noBun.d.ts +2 -0
- package/dist/testing/_noBun.d.ts.map +1 -0
- package/dist/testing/_noBun.js +10 -0
- package/dist/testing/_noBun.js.map +1 -0
- package/package.json +42 -19
- package/scripts/checkAreaStyles.spec.ts +71 -0
- package/scripts/checkAreaStyles.ts +150 -0
- package/scripts/guardrailsEnforce.spec.ts +43 -0
- package/scripts/guardrailsEnforce.ts +46 -1
- package/scripts/styleAreas.ts +1 -2
- package/src/barrelsReachNoOptionalPeer.spec.ts +6 -3
- package/src/bundlerOnlyEntries.spec.ts +101 -0
- package/src/declaredImports.spec.ts +4 -0
- package/src/optionalPeersTable.spec.ts +127 -0
- package/src/react/chat/index.ts +8 -0
- package/src/react/chat/usePresence.spec.tsx +138 -0
- package/src/react/chat/usePresence.ts +185 -0
- package/src/react/components/SecretInput.tsx +12 -1
- package/src/react/components/TooltipImpl.tsx +54 -5
- package/src/react/index.ts +8 -0
- package/src/react/master-lock/MasterLockDialog.tsx +13 -9
- package/src/react/master-lock/MasterLockProvider.spec.tsx +17 -0
- package/src/react/master-lock/MasterLockProvider.tsx +16 -19
- package/src/react/media/MediaUploadControl.tsx +8 -29
- package/src/react/media/fileUploadClient.ts +7 -17
- package/src/react/media/hlsSource.ts +4 -1
- package/src/react/media/index.ts +0 -16
- package/src/react/sessionAuth.ts +2 -3
- package/src/scriptsResolve.spec.ts +66 -0
- package/src/styles-areas/activity-bar.css +1 -1
- package/src/styles-areas/auth.css +1 -1
- package/src/styles-areas/charts.css +1 -1
- package/src/styles-areas/chat.css +1 -1
- package/src/styles-areas/core.css +1 -1
- package/src/styles-areas/data-table.css +1 -1
- package/src/styles-areas/disk-usage.css +1 -1
- package/src/styles-areas/fields.css +1 -1
- package/src/styles-areas/master-detail.css +1 -1
- package/src/styles-areas/master-lock.css +108 -2
- package/src/styles-areas/media-gallery.css +1 -1
- package/src/styles-areas/media.css +1 -17
- package/src/styles-areas/nav.css +1 -1
- package/src/styles-areas/notifications.css +1 -1
- package/src/styles-areas/spreadsheet.css +1 -1
- package/src/styles-areas/stats.css +1 -1
- package/src/styles-areas/wizard.css +1 -1
- package/src/styles-areas/workbook-viewer.css +1 -1
- package/src/styles-utilities.css +0 -16
- package/src/testing/_noBun.ts +12 -0
- package/src/verifyGraph.spec.ts +11 -0
- package/dist/react/media/PrivateIndexView.d.ts +0 -6
- package/dist/react/media/PrivateIndexView.d.ts.map +0 -1
- package/dist/react/media/PrivateIndexView.js +0 -44
- package/dist/react/media/PrivateIndexView.js.map +0 -1
- package/dist/react/media/PrivateModeBadge.d.ts +0 -17
- package/dist/react/media/PrivateModeBadge.d.ts.map +0 -1
- package/dist/react/media/PrivateModeBadge.js +0 -24
- package/dist/react/media/PrivateModeBadge.js.map +0 -1
- package/dist/react/media/privateClient.d.ts +0 -34
- package/dist/react/media/privateClient.d.ts.map +0 -1
- package/dist/react/media/privateClient.js +0 -36
- package/dist/react/media/privateClient.js.map +0 -1
- package/dist/react/media/usePrivateUnlock.d.ts +0 -25
- package/dist/react/media/usePrivateUnlock.d.ts.map +0 -1
- package/dist/react/media/usePrivateUnlock.js +0 -68
- package/dist/react/media/usePrivateUnlock.js.map +0 -1
- package/src/react/media/PrivateIndexView.tsx +0 -113
- package/src/react/media/PrivateModeBadge.tsx +0 -50
- package/src/react/media/privateClient.spec.ts +0 -113
- package/src/react/media/privateClient.ts +0 -82
- package/src/react/media/usePrivateUnlock.ts +0 -132
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Loaded only on non-Bun runtimes (task 2087). The `cursedbelt/testing` export map routes the
|
|
2
|
+
// real module to Bun (the `bun` condition) and everything else here, so importing these helpers
|
|
3
|
+
// from Node fails with a sentence instead of `Named export 'waitFor' not found` — which is what
|
|
4
|
+
// Node said about `@testing-library/react`, a CommonJS module its ESM loader cannot name-import.
|
|
5
|
+
// Same shape as `cwip/testing`'s `_noBun`, and `tools/proveInstall.ts` excuses exactly this: a
|
|
6
|
+
// `bun` condition plus a plain `Error` of the package's own making.
|
|
7
|
+
throw new Error(
|
|
8
|
+
'cursedbelt/testing requires the Bun runtime — it drives @testing-library/react under bun:test + happy-dom. ' +
|
|
9
|
+
'Import it only from specs run with `bun test`.',
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export {};
|
package/src/verifyGraph.spec.ts
CHANGED
|
@@ -67,6 +67,17 @@ const MUST_RUN = [
|
|
|
67
67
|
// added, removing it argues with this list.
|
|
68
68
|
'typecheck:scripts',
|
|
69
69
|
'demo:check',
|
|
70
|
+
// 🔴 Added 2026-09-23 (task 102). `biome lint` was in no command at all — `done:serial`
|
|
71
|
+
// called a `lint` script that did not exist, so the 31 errors it would have shown sat
|
|
72
|
+
// unseen. They were cleared in the same landing (29 were in Tailwind's generated output,
|
|
73
|
+
// now excluded in `biome.jsonc`), so the stage landed green.
|
|
74
|
+
'lint',
|
|
75
|
+
// 🔴 Added 2026-09-23 with `no-raw-password` (task 077). This repo's own guardrails scan was
|
|
76
|
+
// in no command a gate ran, and it had been RED since task 269 gave every relative specifier
|
|
77
|
+
// its `.js`: `no-native-dialog` stopped recognising DeleteButton's `'../confirm.js'` import
|
|
78
|
+
// as the belt's own `confirm`. The rules this package ships to five apps were not being
|
|
79
|
+
// applied to the package itself.
|
|
80
|
+
'guardrails:report',
|
|
70
81
|
'styles',
|
|
71
82
|
'build',
|
|
72
83
|
'test',
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PrivateIndexView.d.ts","sourceRoot":"","sources":["../../../src/react/media/PrivateIndexView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAS5E,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,qBAmGrE"}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { formatBytes as fmtBytes } from 'cwip/format';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Button } from '../components/Button.js';
|
|
5
|
-
import { Input } from '../components/Input.js';
|
|
6
|
-
// The locked-index cleanup view: every private item's SIZE + created-date (never its title,
|
|
7
|
-
// thumbnail, or notes), so a user can prune media whose password they've lost. A password field
|
|
8
|
-
// runs "try-a-password" — items it decrypts light up with their revealed title; the rest stay
|
|
9
|
-
// opaque. Themed off cursedbelt tokens (with fallbacks so it renders anywhere).
|
|
10
|
-
const fmtDate = (ts) => new Date(ts).toISOString().slice(0, 10);
|
|
11
|
-
export function PrivateIndexView({ unlock }) {
|
|
12
|
-
const [rows, setRows] = React.useState([]);
|
|
13
|
-
const [revealed, setRevealed] = React.useState(new Map());
|
|
14
|
-
const [password, setPassword] = React.useState('');
|
|
15
|
-
const [trying, setTrying] = React.useState(false);
|
|
16
|
-
React.useEffect(() => {
|
|
17
|
-
unlock.loadIndex().then(setRows);
|
|
18
|
-
}, [unlock]);
|
|
19
|
-
async function tryPassword() {
|
|
20
|
-
if (!password)
|
|
21
|
-
return;
|
|
22
|
-
setTrying(true);
|
|
23
|
-
try {
|
|
24
|
-
const items = await unlock.reveal(password);
|
|
25
|
-
setRevealed(new Map(items.map((i) => [i.id, i.meta])));
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
setTrying(false);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const totalBytes = rows.reduce((s, r) => s + r.sizeBytes, 0);
|
|
32
|
-
const matchCount = revealed.size;
|
|
33
|
-
return (_jsxs("div", { className: 'flex flex-col gap-3 text-sm', children: [_jsxs("div", { className: 'flex items-center justify-between gap-3', children: [_jsxs("div", { className: 'text-[color:var(--muted-foreground)]', children: [rows.length, " locked ", rows.length === 1 ? 'file' : 'files', " \u00B7 ", fmtBytes(totalBytes)] }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx(Input, { type: 'password', value: password, onChange: (e) => setPassword(e.target.value), onEnter: () => tryPassword(), placeholder: 'Try a password\u2026', "aria-label": 'Try a password', className: 'h-auto w-auto bg-transparent px-2 py-1' }), _jsx(Button, { size: 'sm', onClick: tryPassword, disabled: trying || !password, loading: trying, tooltip: 'Try this password against every locked file', children: trying ? 'Trying…' : 'Reveal' })] })] }), matchCount > 0 ? (_jsxs("div", { className: 'text-[color:var(--accent)]', children: ["This password unlocks ", matchCount, " ", matchCount === 1 ? 'file' : 'files', "."] })) : null, _jsxs("div", { className: 'overflow-hidden rounded-lg border border-[color:var(--border-strong)]', children: [rows.map((r) => {
|
|
34
|
-
const meta = revealed.get(r.id);
|
|
35
|
-
const title = typeof meta?.title === 'string' ? meta.title : null;
|
|
36
|
-
return (_jsxs("div", { className: `flex items-center justify-between gap-3 border-b border-[color:var(--border-strong)] px-3 py-2 last:border-b-0 ${
|
|
37
|
-
// `--card-overlay` (white/navy per theme.css) isn't the tint this row wants —
|
|
38
|
-
// the original rgba(16,185,129,0.08) fallback was an emerald-accent highlight,
|
|
39
|
-
// not the neutral card-overlay token, so build the same tint from the real
|
|
40
|
-
// accent token via color-mix instead of a literal fallback color.
|
|
41
|
-
title ? 'bg-[color:color-mix(in_srgb,var(--accent-solid)_8%,transparent)]' : ''}`, children: [_jsx("div", { className: 'min-w-0 flex-1 truncate', children: title ? (_jsx("span", { className: 'font-medium text-[color:var(--accent)]', children: title })) : (_jsxs("span", { className: 'font-mono text-[color:var(--muted-foreground)]', children: ["\uD83D\uDD12 ", r.id.slice(0, 8), "\u2026"] })) }), _jsx("div", { className: 'shrink-0 tabular-nums text-[color:var(--muted-foreground)]', children: fmtBytes(r.sizeBytes) }), _jsx("div", { className: 'shrink-0 tabular-nums text-[color:var(--muted-foreground)]', children: fmtDate(r.createdAt) })] }, r.id));
|
|
42
|
-
}), rows.length === 0 ? (_jsx("div", { className: 'px-3 py-6 text-center text-[color:var(--muted-foreground)]', children: "No private files." })) : null] })] }));
|
|
43
|
-
}
|
|
44
|
-
//# sourceMappingURL=PrivateIndexView.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PrivateIndexView.js","sourceRoot":"","sources":["../../../src/react/media/PrivateIndexView.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAG/C,4FAA4F;AAC5F,gGAAgG;AAChG,8FAA8F;AAC9F,gFAAgF;AAEhF,MAAM,OAAO,GAAG,CAAC,EAAU,EAAU,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAEhF,MAAM,UAAU,gBAAgB,CAAC,EAAE,MAAM,EAA6B;IACpE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAoB,EAAE,CAAC,CAAC;IAC9D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAuC,IAAI,GAAG,EAAE,CAAC,CAAC;IAChG,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACnD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,KAAK,UAAU,WAAW;QACxB,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,SAAS,CAAC,IAAI,CAAC,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC5C,WAAW,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAC7D,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC;IAEjC,OAAO,CACL,eAAK,SAAS,EAAC,6BAA6B,aAC1C,eAAK,SAAS,EAAC,yCAAyC,aACtD,eAAK,SAAS,EAAC,sCAAsC,aAClD,IAAI,CAAC,MAAM,cAAU,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,cAAK,QAAQ,CAAC,UAAU,CAAC,IAC/E,EACN,eAAK,SAAS,EAAC,yBAAyB,aACtC,KAAC,KAAK,IACJ,IAAI,EAAC,UAAU,EACf,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC5C,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,EAAE,EAC5B,WAAW,EAAC,sBAAiB,gBAClB,gBAAgB,EAC3B,SAAS,EAAC,wCAAwC,GAClD,EACF,KAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,MAAM,IAAI,CAAC,QAAQ,EAC7B,OAAO,EAAE,MAAM,EACf,OAAO,EAAC,6CAA6C,YAEpD,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,GACvB,IACL,IACF,EAEL,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAChB,eAAK,SAAS,EAAC,4BAA4B,uCAClB,UAAU,OAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,SACnE,CACP,CAAC,CAAC,CAAC,IAAI,EAER,eAAK,SAAS,EAAC,uEAAuE,aACnF,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;wBACd,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;wBAChC,MAAM,KAAK,GAAG,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;wBAClE,OAAO,CACL,eAEE,SAAS,EAAE,kHAAkH;4BAC3H,8EAA8E;4BAC9E,+EAA+E;4BAC/E,2EAA2E;4BAC3E,kEAAkE;4BAClE,KAAK,CAAC,CAAC,CAAC,kEAAkE,CAAC,CAAC,CAAC,EAC/E,EAAE,aAEF,cAAK,SAAS,EAAC,yBAAyB,YACrC,KAAK,CAAC,CAAC,CAAC,CACP,eAAM,SAAS,EAAC,wCAAwC,YAAE,KAAK,GAAQ,CACxE,CAAC,CAAC,CAAC,CACF,gBAAM,SAAS,EAAC,gDAAgD,8BAC1D,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,cACf,CACR,GACG,EACN,cAAK,SAAS,EAAC,4DAA4D,YACxE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,GAClB,EACN,cAAK,SAAS,EAAC,4DAA4D,YACxE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,GACjB,KAvBD,CAAC,CAAC,EAAE,CAwBL,CACP,CAAC;oBACJ,CAAC,CAAC,EACD,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACnB,cAAK,SAAS,EAAC,4DAA4D,kCAErE,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,IACF,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export interface PrivateModeBadgeProps {
|
|
2
|
-
/** How many items are private (shown in the label). */
|
|
3
|
-
hiddenCount: number;
|
|
4
|
-
/** Whether the feed is currently filtered to private-only. */
|
|
5
|
-
active: boolean;
|
|
6
|
-
/** Toggle the private-only filter. */
|
|
7
|
-
onToggle: () => void;
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* The "🔓 N private" toggle pill shown in a media gallery's header once private
|
|
12
|
-
* mode is unlocked — click filters the feed to private-only items. One shared
|
|
13
|
-
* implementation for every gallery (video/photo/media); it was previously
|
|
14
|
-
* copy-pasted per plugin with per-plugin CSS classes.
|
|
15
|
-
*/
|
|
16
|
-
export declare function PrivateModeBadge({ hiddenCount, active, onToggle, className }: PrivateModeBadgeProps): import("react").JSX.Element;
|
|
17
|
-
//# sourceMappingURL=PrivateModeBadge.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PrivateModeBadge.d.ts","sourceRoot":"","sources":["../../../src/react/media/PrivateModeBadge.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IACpC,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,MAAM,EAAE,OAAO,CAAC;IAChB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,qBAAqB,+BA8BnG"}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { cn } from '../lib/cn.js';
|
|
3
|
-
import { Tooltip } from '../components/Tooltip.js';
|
|
4
|
-
/**
|
|
5
|
-
* The "🔓 N private" toggle pill shown in a media gallery's header once private
|
|
6
|
-
* mode is unlocked — click filters the feed to private-only items. One shared
|
|
7
|
-
* implementation for every gallery (video/photo/media); it was previously
|
|
8
|
-
* copy-pasted per plugin with per-plugin CSS classes.
|
|
9
|
-
*/
|
|
10
|
-
export function PrivateModeBadge({ hiddenCount, active, onToggle, className }) {
|
|
11
|
-
return (_jsx(Tooltip, { content: 'Private mode unlocked \u2014 click to show only private items', children: _jsxs("button", {
|
|
12
|
-
// guardrails-ignore no-raw-action-button: compact header pill with pressed
|
|
13
|
-
// state — Button's control-height/variant chrome doesn't fit a badge-sized
|
|
14
|
-
// toggle; this keeps the exact badge geometry the galleries share.
|
|
15
|
-
type: 'button', "data-cb-private-badge": '', "aria-pressed": active, onClick: onToggle, className: cn('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', 'hover:border-accent hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
16
|
-
// `text-accent-strong`, not plain `text-accent` — the /15 tint only clears
|
|
17
|
-
// 4.43:1 with the base accent hue, under AA (cbux-101). (Previously
|
|
18
|
-
// `text-accent-foreground`, a token this library never defines — an
|
|
19
|
-
// undefined Tailwind color utility compiles to no rule at all, so the
|
|
20
|
-
// pill silently fell back to inherited text color instead of an
|
|
21
|
-
// intentional one.)
|
|
22
|
-
active && 'border-accent bg-accent/15 text-accent-strong', className), children: [_jsx("span", { "aria-hidden": true, children: "\uD83D\uDD13" }), hiddenCount, " private"] }) }));
|
|
23
|
-
}
|
|
24
|
-
//# sourceMappingURL=PrivateModeBadge.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PrivateModeBadge.js","sourceRoot":"","sources":["../../../src/react/media/PrivateModeBadge.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAYnD;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAyB;IAClG,OAAO,CACL,KAAC,OAAO,IAAC,OAAO,EAAC,+DAA0D,YACzE;YACE,2EAA2E;YAC3E,2EAA2E;YAC3E,mEAAmE;YACnE,IAAI,EAAC,QAAQ,2BACS,EAAE,kBACV,MAAM,EACpB,OAAO,EAAE,QAAQ,EACjB,SAAS,EAAE,EAAE,CACX,+KAA+K,EAC/K,2CAA2C,EAC3C,yEAAyE;YACzE,2EAA2E;YAC3E,oEAAoE;YACpE,oEAAoE;YACpE,sEAAsE;YACtE,gEAAgE;YAChE,oBAAoB;YACpB,MAAM,IAAI,+CAA+C,EACzD,SAAS,CACV,aAED,+DAA2B,EAC1B,WAAW,gBACL,GACD,CACX,CAAC;AACJ,CAAC"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** A clear index row — non-sensitive facts only (matches the server's PrivateIndexRow). */
|
|
2
|
-
export type PrivateIndexRow = {
|
|
3
|
-
id: string;
|
|
4
|
-
sizeBytes: number;
|
|
5
|
-
createdAt: number;
|
|
6
|
-
status: 'pending' | 'processing' | 'ready' | 'failed';
|
|
7
|
-
};
|
|
8
|
-
export type RevealedItem = {
|
|
9
|
-
id: string;
|
|
10
|
-
meta: Record<string, unknown>;
|
|
11
|
-
};
|
|
12
|
-
export interface PrivateClientConfig {
|
|
13
|
-
/** Origin+prefix for the private routes. '' = same-origin; an absolute API origin for a
|
|
14
|
-
* split-origin deploy. */
|
|
15
|
-
apiBase?: string;
|
|
16
|
-
/** Route mount prefix (must match mountPrivateRoutes basePath). Default '/_private'. */
|
|
17
|
-
basePath?: string;
|
|
18
|
-
/** Extra headers for the GUARDED calls (unlock/index/reveal) — e.g. the app's Bearer. The
|
|
19
|
-
* token-embedded byte/key URLs need no header. */
|
|
20
|
-
authHeaders?: () => Record<string, string>;
|
|
21
|
-
}
|
|
22
|
-
export declare const privateBaseUrl: (cfg: PrivateClientConfig) => string;
|
|
23
|
-
/** Try `password` against every item; returns the ones it decrypts (with their metadata). */
|
|
24
|
-
export declare const revealPrivateWithPassword: (cfg: PrivateClientConfig, password: string) => Promise<RevealedItem[]>;
|
|
25
|
-
/** Reveal the items an UNLOCK TOKEN opens — no password re-entry. The token must belong to the
|
|
26
|
-
* session caller (the server enforces it). */
|
|
27
|
-
export declare const revealPrivateWithToken: (cfg: PrivateClientConfig, token: string) => Promise<RevealedItem[]>;
|
|
28
|
-
/** The token-embedded HLS master URL for a private video — same shape usePrivateUnlock.hlsUrl
|
|
29
|
-
* builds. Needs no auth header (the token IS the authorization). */
|
|
30
|
-
export declare const privateHlsUrlWithToken: (cfg: PrivateClientConfig, token: string, sourceId: string, videoId: string) => string;
|
|
31
|
-
/** The token-embedded URL for a private NON-video file (photo/doc) stored as an encrypted
|
|
32
|
-
* blob — served decrypted by the gated blob route. Needs no auth header. */
|
|
33
|
-
export declare const privateFileUrlWithToken: (cfg: PrivateClientConfig, token: string, fileId: string) => string;
|
|
34
|
-
//# sourceMappingURL=privateClient.d.ts.map
|
|
@@ -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
|
-
});
|