@gmickel/gno 2.0.0 → 2.1.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 +17 -5
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.0.0.zip → gno-browser-clipper-v2.1.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +8 -0
- package/src/serve/public/components/PublishExportDialog.tsx +266 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/globals.css +35 -0
- package/src/serve/public/lib/publish-export.ts +81 -1
- package/src/serve/public/pages/Collections.tsx +12 -46
- package/src/serve/public/pages/DocView.tsx +14 -52
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +0 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ gno daemon --detach # headless indexing + resident MCP gateway
|
|
|
117
117
|
|
|
118
118
|
<!-- public-truth:current-version -->
|
|
119
119
|
|
|
120
|
-
> Current
|
|
120
|
+
> Current source version: **v2.1.0**. See [CHANGELOG.md](./CHANGELOG.md).
|
|
121
121
|
|
|
122
122
|
<!-- /public-truth -->
|
|
123
123
|
|
|
@@ -852,7 +852,12 @@ Or use the Web UI:
|
|
|
852
852
|
- **Collections page** → collection menu → **Export for gno.sh**
|
|
853
853
|
- **Document view** → **Export for gno.sh**
|
|
854
854
|
|
|
855
|
-
|
|
855
|
+
Both local dialogs require an explicit access choice. Upload the artifact at
|
|
856
|
+
[gno.sh/studio](https://gno.sh/studio), review its access and audience, then press
|
|
857
|
+
Publish. CLI/API exports keep their public default; pass visibility explicitly
|
|
858
|
+
for restricted content. Hosted plan availability is checked in Studio.
|
|
859
|
+
|
|
860
|
+
Choose the audience:
|
|
856
861
|
|
|
857
862
|
| Mode | Use When |
|
|
858
863
|
| :-------------- | :------------------------------------------------------------- |
|
|
@@ -875,13 +880,20 @@ and image fields accept only uncredentialed public HTTP(S) targets.
|
|
|
875
880
|
Resolved local PNG, JPEG, GIF, WebP, and AVIF references are bundled,
|
|
876
881
|
content-addressed, and deduplicated; external public HTTPS images remain
|
|
877
882
|
external. The exact serialized artifact is capped at 100 MiB. Public images
|
|
878
|
-
use
|
|
879
|
-
every request, and encrypted image bytes exist only inside ciphertext before
|
|
883
|
+
use generation routes that check current access without caching, secret-link
|
|
884
|
+
images are authorized on every request, and encrypted image bytes exist only inside ciphertext before
|
|
880
885
|
the browser creates scoped Blob URLs. Invite-only bundled-image delivery is
|
|
881
886
|
currently fail-closed; use an asset-free invite, secret link, or encrypted
|
|
882
887
|
share when local images are required.
|
|
883
888
|
|
|
884
|
-
|
|
889
|
+
Explicitly select an existing publication to update it, or publish a separate
|
|
890
|
+
copy. Content-only updates preserve the active URL; access changes invalidate
|
|
891
|
+
old routes or tokens. Changing into or out of encryption requires a fresh
|
|
892
|
+
local export. Unpublish stops hosted access but retains source and history.
|
|
893
|
+
Delete permanently stops access before durable background cleanup; failed
|
|
894
|
+
cleanup stays denied and can be retried. Neither operation deletes local
|
|
895
|
+
files or independently published copies, and downloads cannot be recalled.
|
|
896
|
+
See [Publishing](docs/PUBLISHING.md) for the full lifecycle and retention limits.
|
|
885
897
|
|
|
886
898
|
Encrypted source-backed publish on `gno.sh` is intentionally disabled. For encrypted shares, use:
|
|
887
899
|
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
8dd833b01ebc0a0cb5747de65a9cd60b06cafcf645c2a74d697c918fe7c642d7 gno-browser-clipper-v2.1.0.zip
|
package/package.json
CHANGED
package/spec/cli.md
CHANGED
|
@@ -2297,6 +2297,14 @@ gno publish export <target> \
|
|
|
2297
2297
|
[--json]
|
|
2298
2298
|
```
|
|
2299
2299
|
|
|
2300
|
+
The CLI and `POST /api/publish/export` MUST preserve the existing `public`
|
|
2301
|
+
default when visibility is omitted. Local Web UI export dialogs MUST instead
|
|
2302
|
+
require an explicit mode. Both successful export results MUST report the
|
|
2303
|
+
selected mode in `artifact.spaces[].visibility`. Invalid modes and missing required encryption input
|
|
2304
|
+
MUST fail without an artifact. Encrypted CLI export requires `--passphrase`;
|
|
2305
|
+
the local API requires `encryptionPassphrase`. Neither sends that input to
|
|
2306
|
+
gno.sh. Export is a local operation, not hosted activation or deletion.
|
|
2307
|
+
|
|
2300
2308
|
Public V1 spaces MUST carry a `manifest` conforming to
|
|
2301
2309
|
[`publish-artifact.schema.json`](./output-schemas/publish-artifact.schema.json).
|
|
2302
2310
|
The manifest contains schema version `1.0`, a deterministic projection
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { Loader2Icon } from "lucide-react";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import type { PublishVisibility } from "../../../publish/artifact";
|
|
5
|
+
|
|
6
|
+
import { apiFetch } from "../hooks/use-api";
|
|
7
|
+
import {
|
|
8
|
+
buildPublishExportRequest,
|
|
9
|
+
downloadPublishArtifactFile,
|
|
10
|
+
PUBLISH_ACCESS_OPTIONS,
|
|
11
|
+
type PublishExportResponse,
|
|
12
|
+
} from "../lib/publish-export";
|
|
13
|
+
import { Button } from "./ui/button";
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
} from "./ui/dialog";
|
|
22
|
+
import { Input } from "./ui/input";
|
|
23
|
+
|
|
24
|
+
interface PublishExportDialogProps {
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
target: string;
|
|
27
|
+
title: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Mount a fresh dialog for each export so access and secrets never carry over. */
|
|
31
|
+
export function PublishExportDialog({
|
|
32
|
+
onClose,
|
|
33
|
+
target,
|
|
34
|
+
title,
|
|
35
|
+
}: PublishExportDialogProps) {
|
|
36
|
+
const [visibility, setVisibility] = useState<PublishVisibility | null>(null);
|
|
37
|
+
const [passphrase, setPassphrase] = useState("");
|
|
38
|
+
const [passphraseConfirmation, setPassphraseConfirmation] = useState("");
|
|
39
|
+
const [audienceConfirmed, setAudienceConfirmed] = useState(false);
|
|
40
|
+
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
const [busy, setBusy] = useState(false);
|
|
42
|
+
const exporting = useRef(false);
|
|
43
|
+
const [returnFocus] = useState(() => {
|
|
44
|
+
const active = document.activeElement;
|
|
45
|
+
const menuTriggerId = active
|
|
46
|
+
?.closest('[role="menu"]')
|
|
47
|
+
?.getAttribute("aria-labelledby");
|
|
48
|
+
const trigger = menuTriggerId
|
|
49
|
+
? document.getElementById(menuTriggerId)
|
|
50
|
+
: active;
|
|
51
|
+
return trigger instanceof HTMLElement ? trigger : null;
|
|
52
|
+
});
|
|
53
|
+
const selected = PUBLISH_ACCESS_OPTIONS.find(
|
|
54
|
+
({ value }) => value === visibility
|
|
55
|
+
);
|
|
56
|
+
const encryptionReady =
|
|
57
|
+
visibility !== "encrypted" ||
|
|
58
|
+
(passphrase.trim().length > 0 && passphrase === passphraseConfirmation);
|
|
59
|
+
|
|
60
|
+
const handleExport = async () => {
|
|
61
|
+
if (exporting.current) return;
|
|
62
|
+
setError(null);
|
|
63
|
+
try {
|
|
64
|
+
const body = buildPublishExportRequest({
|
|
65
|
+
target,
|
|
66
|
+
visibility,
|
|
67
|
+
passphrase,
|
|
68
|
+
passphraseConfirmation,
|
|
69
|
+
audienceConfirmed,
|
|
70
|
+
});
|
|
71
|
+
exporting.current = true;
|
|
72
|
+
setBusy(true);
|
|
73
|
+
const result = await apiFetch<PublishExportResponse>(
|
|
74
|
+
"/api/publish/export",
|
|
75
|
+
{
|
|
76
|
+
method: "POST",
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
if (result.error || !result.data) {
|
|
81
|
+
setError(
|
|
82
|
+
result.error ?? "The export did not return an artifact. Try again."
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Refuse a mismatched response rather than downloading a broader artifact.
|
|
87
|
+
if (
|
|
88
|
+
result.data.artifact.spaces.length !== 1 ||
|
|
89
|
+
result.data.artifact.spaces[0]?.visibility !== visibility ||
|
|
90
|
+
result.data.artifact.version !== (visibility === "encrypted" ? 2 : 1)
|
|
91
|
+
) {
|
|
92
|
+
setError(
|
|
93
|
+
"The returned artifact does not match the reviewed access. Nothing was downloaded."
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
downloadPublishArtifactFile(result.data);
|
|
98
|
+
setPassphrase("");
|
|
99
|
+
setPassphraseConfirmation("");
|
|
100
|
+
onClose();
|
|
101
|
+
} catch (cause) {
|
|
102
|
+
setError(
|
|
103
|
+
cause instanceof Error ? cause.message : "Failed to export. Try again."
|
|
104
|
+
);
|
|
105
|
+
} finally {
|
|
106
|
+
exporting.current = false;
|
|
107
|
+
setBusy(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Dialog
|
|
113
|
+
open
|
|
114
|
+
onOpenChange={(open) => {
|
|
115
|
+
if (!open && !exporting.current) onClose();
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<DialogContent
|
|
119
|
+
className="publish-export-dialog bg-card"
|
|
120
|
+
onCloseAutoFocus={(event) => {
|
|
121
|
+
if (returnFocus?.isConnected) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
returnFocus.focus();
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
showCloseButton={!busy}
|
|
127
|
+
>
|
|
128
|
+
<DialogHeader className="publish-export-dialog-header">
|
|
129
|
+
<DialogTitle>Export for gno.sh</DialogTitle>
|
|
130
|
+
<DialogDescription>
|
|
131
|
+
Review who can read “{title}”. This downloads a local file; upload
|
|
132
|
+
and publish it separately in Studio.
|
|
133
|
+
</DialogDescription>
|
|
134
|
+
</DialogHeader>
|
|
135
|
+
<div className="publish-export-dialog-body">
|
|
136
|
+
<fieldset className="space-y-2" disabled={busy}>
|
|
137
|
+
<legend className="mb-2 font-medium text-sm">
|
|
138
|
+
Who can read this?
|
|
139
|
+
</legend>
|
|
140
|
+
{PUBLISH_ACCESS_OPTIONS.map((option) => (
|
|
141
|
+
<label
|
|
142
|
+
className="flex cursor-pointer items-start gap-3 rounded-md border border-border p-3 transition-colors hover:bg-muted/40 has-[:checked]:border-primary has-[:checked]:bg-primary/10 focus-within:ring-2 focus-within:ring-primary/50"
|
|
143
|
+
key={option.value}
|
|
144
|
+
>
|
|
145
|
+
<input
|
|
146
|
+
checked={visibility === option.value}
|
|
147
|
+
className="mt-1 accent-primary"
|
|
148
|
+
name="publish-access"
|
|
149
|
+
onChange={() => {
|
|
150
|
+
setVisibility(option.value);
|
|
151
|
+
setAudienceConfirmed(false);
|
|
152
|
+
setPassphrase("");
|
|
153
|
+
setPassphraseConfirmation("");
|
|
154
|
+
setError(null);
|
|
155
|
+
}}
|
|
156
|
+
type="radio"
|
|
157
|
+
value={option.value}
|
|
158
|
+
/>
|
|
159
|
+
<span className="min-w-0 space-y-1">
|
|
160
|
+
<span className="block font-medium text-sm">
|
|
161
|
+
{option.label}
|
|
162
|
+
</span>
|
|
163
|
+
<span className="block text-muted-foreground text-sm">
|
|
164
|
+
{option.audience}
|
|
165
|
+
</span>
|
|
166
|
+
<span className="block font-mono text-muted-foreground text-xs">
|
|
167
|
+
{option.availability}
|
|
168
|
+
</span>
|
|
169
|
+
</span>
|
|
170
|
+
</label>
|
|
171
|
+
))}
|
|
172
|
+
</fieldset>
|
|
173
|
+
<p className="text-muted-foreground text-xs">
|
|
174
|
+
Local GNO cannot check your hosted account. Studio checks plan
|
|
175
|
+
availability and Egress Policy before publishing. See{" "}
|
|
176
|
+
<a
|
|
177
|
+
className="text-primary underline"
|
|
178
|
+
href="https://gno.sh/pricing"
|
|
179
|
+
rel="noopener noreferrer"
|
|
180
|
+
target="_blank"
|
|
181
|
+
>
|
|
182
|
+
current plans
|
|
183
|
+
</a>
|
|
184
|
+
.
|
|
185
|
+
</p>
|
|
186
|
+
{visibility === "encrypted" && (
|
|
187
|
+
<fieldset className="space-y-3" disabled={busy}>
|
|
188
|
+
<legend className="mb-2 font-medium text-sm">
|
|
189
|
+
Local encryption
|
|
190
|
+
</legend>
|
|
191
|
+
<p className="text-muted-foreground text-sm">
|
|
192
|
+
Your local GNO server encrypts the notes and bundled assets. The
|
|
193
|
+
passphrase is never sent to gno.sh or included in the downloaded
|
|
194
|
+
file. Keep it safe: lost passphrases cannot be recovered.
|
|
195
|
+
Switching into or out of encrypted sharing requires a new local
|
|
196
|
+
export.
|
|
197
|
+
</p>
|
|
198
|
+
<label className="block space-y-1 text-sm">
|
|
199
|
+
<span>Passphrase</span>
|
|
200
|
+
<Input
|
|
201
|
+
autoComplete="new-password"
|
|
202
|
+
onChange={(event) => setPassphrase(event.target.value)}
|
|
203
|
+
type="password"
|
|
204
|
+
value={passphrase}
|
|
205
|
+
/>
|
|
206
|
+
</label>
|
|
207
|
+
<label className="block space-y-1 text-sm">
|
|
208
|
+
<span>Confirm passphrase</span>
|
|
209
|
+
<Input
|
|
210
|
+
autoComplete="new-password"
|
|
211
|
+
onChange={(event) =>
|
|
212
|
+
setPassphraseConfirmation(event.target.value)
|
|
213
|
+
}
|
|
214
|
+
type="password"
|
|
215
|
+
value={passphraseConfirmation}
|
|
216
|
+
/>
|
|
217
|
+
</label>
|
|
218
|
+
{passphraseConfirmation &&
|
|
219
|
+
passphrase !== passphraseConfirmation && (
|
|
220
|
+
<p className="text-destructive text-sm" role="status">
|
|
221
|
+
The passphrases do not match.
|
|
222
|
+
</p>
|
|
223
|
+
)}
|
|
224
|
+
</fieldset>
|
|
225
|
+
)}
|
|
226
|
+
{selected && (
|
|
227
|
+
<label className="flex cursor-pointer items-start gap-2 text-sm">
|
|
228
|
+
<input
|
|
229
|
+
checked={audienceConfirmed}
|
|
230
|
+
className="mt-1 accent-primary"
|
|
231
|
+
disabled={busy}
|
|
232
|
+
onChange={(event) => setAudienceConfirmed(event.target.checked)}
|
|
233
|
+
type="checkbox"
|
|
234
|
+
/>
|
|
235
|
+
<span>
|
|
236
|
+
I reviewed the {selected.label.toLowerCase()} audience and want
|
|
237
|
+
to export with this access.
|
|
238
|
+
</span>
|
|
239
|
+
</label>
|
|
240
|
+
)}
|
|
241
|
+
{error && (
|
|
242
|
+
<p className="text-destructive text-sm" role="alert">
|
|
243
|
+
{error}
|
|
244
|
+
</p>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
<DialogFooter className="publish-export-dialog-footer">
|
|
248
|
+
<Button disabled={busy} onClick={onClose} variant="outline">
|
|
249
|
+
Cancel
|
|
250
|
+
</Button>
|
|
251
|
+
<Button
|
|
252
|
+
disabled={
|
|
253
|
+
busy || !selected || !audienceConfirmed || !encryptionReady
|
|
254
|
+
}
|
|
255
|
+
onClick={() => {
|
|
256
|
+
void handleExport();
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{busy && <Loader2Icon className="mr-2 size-4 animate-spin" />}
|
|
260
|
+
{busy ? "Exporting…" : "Download export"}
|
|
261
|
+
</Button>
|
|
262
|
+
</DialogFooter>
|
|
263
|
+
</DialogContent>
|
|
264
|
+
</Dialog>
|
|
265
|
+
);
|
|
266
|
+
}
|