notefeed 0.32.0 → 0.33.1

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 CHANGED
@@ -25,6 +25,8 @@ for await (const note of client.notes()) console.log(note.date, note.title); //
25
25
 
26
26
  Every note has a date, now unless you give one: `client.post(text, { date: "2025-03-01T09:30:00Z" })` (a `Date`, an ISO 8601 date-time with its offset, or a date alone for midnight UTC; any time from 1970 to 9999), or `notefeed post "…" --date 2025-03-01`. The note takes its place in the feed by that date. A date in the future schedules the note: readers see it from then on. `await client.update(id, { date })` (`notefeed update <id> --date …`) moves a note.
27
27
 
28
+ A note posted with `draft: true` (`notefeed post "…" --draft`) is a draft: readers don't see it, or its pictures, until `client.update(id, { draft: false })` (`notefeed update <id> --publish`) publishes it at its date; `draft: true` (`--draft`) hides it again.
29
+
28
30
  `Client.fromEnv()` reads `NOTEFEED_URL` / `NOTEFEED_FEED` / `NOTEFEED_PASSWORD`; so does the `notefeed` command, unless given `--url` / `--feed` / `--password`.
29
31
 
30
32
  A feed can have its own password, which gates reading, listing and posting to it. Pass `feedPassword: "..."` to `new Client(...)` (or per call, e.g. `client.post(text, { feedPassword })`), or set `NOTEFEED_FEED_PASSWORD`, which `fromEnv()` and the `notefeed` command both read (there is deliberately no flag, so it stays out of shell history). It is sent as `X-Feed-Password`; `Authorization` stays the instance password. A feed password is 1 to 256 printable ASCII characters with no space at the start or end; an empty one is the same as none.
package/dist/cli.js CHANGED
@@ -7,9 +7,9 @@ import { Client, ConfigError, NotefeedError } from "./client.js";
7
7
  class UsageError extends Error {
8
8
  }
9
9
  const USAGE = [
10
- "usage: notefeed post <text | - | --file PATH> [--type MEDIA_TYPE] [--title TITLE] [--tag TAG]... [--attach PATH]... [--date WHEN] [--url URL] [--feed FEED] [--password PASSWORD]",
10
+ "usage: notefeed post <text | - | --file PATH> [--type MEDIA_TYPE] [--title TITLE] [--tag TAG]... [--attach PATH]... [--date WHEN] [--draft] [--url URL] [--feed FEED] [--password PASSWORD]",
11
11
  " notefeed edit <id> <text | - | --file PATH> [--type MEDIA_TYPE] [--url URL] [--feed FEED] [--password PASSWORD]",
12
- " notefeed update <id> [--title TITLE] [--alt ALT] [--tags LIST] [--date WHEN] [--url URL] [--feed FEED] [--password PASSWORD]",
12
+ " notefeed update <id> [--title TITLE] [--alt ALT] [--tags LIST] [--date WHEN] [--draft | --publish] [--url URL] [--feed FEED] [--password PASSWORD]",
13
13
  " notefeed delete <id> [--url URL] [--feed FEED] [--password PASSWORD]",
14
14
  " notefeed notes [--limit N] [--tag TAG] [--json] [--url URL] [--feed FEED] [--password PASSWORD]",
15
15
  ].join("\n");
@@ -32,6 +32,8 @@ export async function main(argv, io = process) {
32
32
  tag: { type: "string", multiple: true },
33
33
  tags: { type: "string" },
34
34
  date: { type: "string" },
35
+ draft: { type: "boolean" },
36
+ publish: { type: "boolean" },
35
37
  attach: { type: "string", multiple: true },
36
38
  limit: { type: "string" },
37
39
  json: { type: "boolean" },
@@ -55,7 +57,7 @@ export async function main(argv, io = process) {
55
57
  const noText = attachments.length > 0 && text === undefined && !values.file;
56
58
  const input = noText ? { content: null, type: undefined, name: undefined } : await read(text, values.file, values.type, io);
57
59
  // The server refuses a file name next to attachments: the text has none then.
58
- const note = await client(values).post(input.content, { type: input.type, title: values.title, tags: values.tag, date: values.date, name: attachments.length ? undefined : input.name, attachments });
60
+ const note = await client(values).post(input.content, { type: input.type, title: values.title, tags: values.tag, date: values.date, draft: values.draft, name: attachments.length ? undefined : input.name, attachments });
59
61
  io.stdout.write(`${[...(noText ? [] : [note.url]), ...note.attachments.map((a) => a.url)].join("\n")}\n`);
60
62
  return 0;
61
63
  }
@@ -66,11 +68,15 @@ export async function main(argv, io = process) {
66
68
  return 0;
67
69
  }
68
70
  if (command === "update" && text !== undefined && !rest.length) {
69
- if (values.title === undefined && values.alt === undefined && values.tags === undefined && values.date === undefined)
70
- throw new UsageError("give --title, --alt, --tags and/or --date");
71
+ if (values.draft && values.publish)
72
+ throw new UsageError("give --draft or --publish, not both");
73
+ // --draft makes the note a draft (#265), --publish publishes it; either alone is an update.
74
+ const draft = values.draft ? true : values.publish ? false : undefined;
75
+ if (values.title === undefined && values.alt === undefined && values.tags === undefined && values.date === undefined && draft === undefined)
76
+ throw new UsageError("give --title, --alt, --tags, --date and/or --draft or --publish");
71
77
  // --tags is the whole new list, comma-separated; "" removes them all.
72
78
  const tags = values.tags?.split(",").map((t) => t.trim()).filter(Boolean);
73
- const note = await client(values).update(text, { title: values.title, alt: values.alt, tags, date: values.date });
79
+ const note = await client(values).update(text, { title: values.title, alt: values.alt, tags, date: values.date, draft });
74
80
  io.stdout.write(`${note.url}\n`);
75
81
  return 0;
76
82
  }
package/dist/client.d.ts CHANGED
@@ -89,6 +89,7 @@ export declare class Client {
89
89
  alt?: string;
90
90
  name?: string;
91
91
  date?: string | Date;
92
+ draft?: boolean;
92
93
  readId?: string;
93
94
  licenseKey?: string;
94
95
  attachments?: Attachment[];
@@ -102,12 +103,13 @@ export declare class Client {
102
103
  feedPassword?: string;
103
104
  type?: string;
104
105
  }): Promise<Note>;
105
- /** Set a note's title, alt text (for pictures), tags and/or date (as in post(); a future one schedules the note); `""` removes a title or alt, so a markdown note's title follows its text again; `tags` replaces the whole list (`[]` removes them). Same options as post(). */
106
+ /** Set a note's title, alt text (for pictures), tags, date (as in post(); a future one schedules the note) and/or whether it is a draft (`false` publishes it, `true` hides it from readers again; its date stays); `""` removes a title or alt, so a markdown note's title follows its text again; `tags` replaces the whole list (`[]` removes them). Same options as post(). */
106
107
  update(id: string, meta: {
107
108
  title?: string;
108
109
  alt?: string;
109
110
  tags?: string[];
110
111
  date?: string | Date;
112
+ draft?: boolean;
111
113
  }, options?: {
112
114
  feed?: string;
113
115
  feedPassword?: string;
package/dist/client.js CHANGED
@@ -160,6 +160,7 @@ export class Client {
160
160
  ...(own.title && { "X-Note-Title": headerValue(own.title) }),
161
161
  ...(own.tags?.length && { "X-Note-Tags": own.tags.join(",") }),
162
162
  ...(own.date && { "X-Note-Date": dateString(own.date) }),
163
+ ...(own.draft && { "X-Note-Draft": "true" }),
163
164
  ...(own.readId && { "X-Read-Id": own.readId }),
164
165
  ...(own.licenseKey && { "X-License-Key": own.licenseKey }),
165
166
  };
@@ -177,6 +178,7 @@ export class Client {
177
178
  ...(options.alt && { "X-Note-Alt": headerValue(options.alt) }),
178
179
  ...(options.name && { "X-Note-Name": headerValue(options.name) }),
179
180
  ...(options.date && { "X-Note-Date": dateString(options.date) }),
181
+ ...(options.draft && { "X-Note-Draft": "true" }),
180
182
  ...(options.readId && { "X-Read-Id": options.readId }),
181
183
  ...(options.licenseKey && { "X-License-Key": options.licenseKey }),
182
184
  };
@@ -189,7 +191,7 @@ export class Client {
189
191
  const headers = { ...this.opts(options.feedPassword).headers, "Content-Type": type };
190
192
  return this.call(editNote({ client: this.api, path: { feed, id }, body, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
191
193
  }
192
- /** Set a note's title, alt text (for pictures), tags and/or date (as in post(); a future one schedules the note); `""` removes a title or alt, so a markdown note's title follows its text again; `tags` replaces the whole list (`[]` removes them). Same options as post(). */
194
+ /** Set a note's title, alt text (for pictures), tags, date (as in post(); a future one schedules the note) and/or whether it is a draft (`false` publishes it, `true` hides it from readers again; its date stays); `""` removes a title or alt, so a markdown note's title follows its text again; `tags` replaces the whole list (`[]` removes them). Same options as post(). */
193
195
  async update(id, meta, options = {}) {
194
196
  const feed = this.feedFor(options.feed);
195
197
  const { date, ...rest } = meta;
@@ -22,7 +22,7 @@ export declare const listNotes: <ThrowOnError extends boolean = false>(options:
22
22
  /**
23
23
  * Post a note
24
24
  *
25
- * The body is the note, a file, and `Content-Type` says which kind: `text/markdown` (UTF-8, at most 102400 bytes), or an image, `image/png`, `image/jpeg`, `image/gif` or `image/webp` (at most NOTEFEED_MAX_IMAGE_BYTES as sent, default 10 MiB). Nothing is guessed: any other type, or none, is `415`, and so is a body that is not what the type says (an image is recognized by its first bytes; SVG is refused). Markdown is stored byte for byte. An image is stored processed, not as sent: turned the way its EXIF orientation says, scaled to at most 2560 px on its long side, encoded again in its own format (JPEG and WebP at quality 85, PNG lossless, an animated GIF or WebP keeps its frames), and without metadata (EXIF, GPS position, XMP, comments). Send it full size; one of more than 100 megapixels is `413`, and a body that does not decode as the image it declares is `415`. Creates the feed with its first note, optionally protected by its own password (`X-Feed-Password`; 1 to 256 printable ASCII characters, with no space at the start or end) and with the read id in `X-Read-Id`; both are only used by the post that creates the feed. Where licenses are sold, the post that creates the feed can bring a license key in `X-License-Key`, with or without a password; a feed password needs one, or the post is refused with `402`. Posting to a protected feed needs its password. Also served at `POST /{feed}`, the short form curl one-liners use. The response names the note's `file` and where it is served, `file_url`, under the feed's read id (public like the read link). Metadata goes in headers: `X-Note-Title`, `X-Note-Tags` (at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed), `X-Note-Alt` (images), `X-Note-Name` (the original file name), `X-Note-Date` (where it is placed in the feed: a past date backdates it, a future one schedules it). A markdown note with its pictures is one `multipart/form-data` request: a `text` part (the markdown, a field or a file part; a file part keeps its line breaks, a field's are sent as CRLF), `file` parts (up to 10 pictures, each with its file name and image `Content-Type`) and `alt.<file name>` fields. The pictures are stored first, each as its own note named by its file name, then the text with its references to them (`![](chart.png)`, `[x]: chart.png`, as written or percent-decoded) swapped for the stored files; a picture it never refers to is appended as `![](file)`. Several pictures may have the same file name (a phone calls every photo `image.jpg`): each is stored, and they are appended, but a text that refers to such a name is 400. Everything is checked before the first write, and a failed write removes what the request stored: all or nothing. `X-Note-Alt` and `X-Note-Name` are 400. The answer has `attachments`, the stored pictures in the order of the `file` parts. The `text` part is optional: `X-Note-Title` goes on the text note, `X-Note-Tags` on every note, `X-Note-Date` on the text note (its pictures take the milliseconds just before it); with no `text` part only the pictures are stored, each with the title, and the answer's top level is the first picture. A refused multipart post that would have created a protected feed creates nothing.
25
+ * The body is the note, a file, and `Content-Type` says which kind: `text/markdown` (UTF-8, at most 102400 bytes), or an image, `image/png`, `image/jpeg`, `image/gif` or `image/webp` (at most NOTEFEED_MAX_IMAGE_BYTES as sent, default 10 MiB). Nothing is guessed: any other type, or none, is `415`, and so is a body that is not what the type says (an image is recognized by its first bytes; SVG is refused). Markdown is stored byte for byte. An image is stored processed, not as sent: turned the way its EXIF orientation says, scaled to at most 2560 px on its long side, encoded again in its own format (JPEG and WebP at quality 85, PNG lossless, an animated GIF or WebP keeps its frames), and without metadata (EXIF, GPS position, XMP, comments). Send it full size; one of more than 100 megapixels is `413`, and a body that does not decode as the image it declares is `415`. Creates the feed with its first note, optionally protected by its own password (`X-Feed-Password`; 1 to 256 printable ASCII characters, with no space at the start or end) and with the read id in `X-Read-Id`; both are only used by the post that creates the feed. Where licenses are sold, the post that creates the feed can bring a license key in `X-License-Key`, with or without a password; a feed password needs one, or the post is refused with `402`. Posting to a protected feed needs its password. Also served at `POST /{feed}`, the short form curl one-liners use. The response names the note's `file` and where it is served, `file_url`, under the feed's read id (public like the read link). Metadata goes in headers: `X-Note-Title`, `X-Note-Tags` (at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed), `X-Note-Alt` (images), `X-Note-Name` (the original file name), `X-Note-Date` (where it is placed in the feed: a past date backdates it, a future one schedules it), `X-Note-Draft` (`true`: a draft, which readers do not see until it is published). A markdown note with its pictures is one `multipart/form-data` request: a `text` part (the markdown, a field or a file part; a file part keeps its line breaks, a field's are sent as CRLF), `file` parts (up to 10 pictures, each with its file name and image `Content-Type`) and `alt.<file name>` fields. The pictures are stored first, each as its own note named by its file name, then the text with its references to them (`![](chart.png)`, `[x]: chart.png`, as written or percent-decoded) swapped for the stored files; a picture it never refers to is appended as `![](file)`. Several pictures may have the same file name (a phone calls every photo `image.jpg`): each is stored, and they are appended, but a text that refers to such a name is 400. Everything is checked before the first write, and a failed write removes what the request stored: all or nothing. `X-Note-Alt` and `X-Note-Name` are 400. The answer has `attachments`, the stored pictures in the order of the `file` parts. The `text` part is optional: `X-Note-Title` goes on the text note, `X-Note-Tags` on every note, `X-Note-Date` on the text note (its pictures take the milliseconds just before it); with no `text` part only the pictures are stored, each with the title, and the answer's top level is the first picture. A refused multipart post that would have created a protected feed creates nothing.
26
26
  */
27
27
  export declare const postNote: <ThrowOnError extends boolean = false>(options: Options<PostNoteData, ThrowOnError>) => RequestResult<PostNoteResponses, PostNoteErrors, ThrowOnError>;
28
28
  /**
@@ -13,7 +13,7 @@ export const listNotes = (options) => (options.client ?? client).get({
13
13
  /**
14
14
  * Post a note
15
15
  *
16
- * The body is the note, a file, and `Content-Type` says which kind: `text/markdown` (UTF-8, at most 102400 bytes), or an image, `image/png`, `image/jpeg`, `image/gif` or `image/webp` (at most NOTEFEED_MAX_IMAGE_BYTES as sent, default 10 MiB). Nothing is guessed: any other type, or none, is `415`, and so is a body that is not what the type says (an image is recognized by its first bytes; SVG is refused). Markdown is stored byte for byte. An image is stored processed, not as sent: turned the way its EXIF orientation says, scaled to at most 2560 px on its long side, encoded again in its own format (JPEG and WebP at quality 85, PNG lossless, an animated GIF or WebP keeps its frames), and without metadata (EXIF, GPS position, XMP, comments). Send it full size; one of more than 100 megapixels is `413`, and a body that does not decode as the image it declares is `415`. Creates the feed with its first note, optionally protected by its own password (`X-Feed-Password`; 1 to 256 printable ASCII characters, with no space at the start or end) and with the read id in `X-Read-Id`; both are only used by the post that creates the feed. Where licenses are sold, the post that creates the feed can bring a license key in `X-License-Key`, with or without a password; a feed password needs one, or the post is refused with `402`. Posting to a protected feed needs its password. Also served at `POST /{feed}`, the short form curl one-liners use. The response names the note's `file` and where it is served, `file_url`, under the feed's read id (public like the read link). Metadata goes in headers: `X-Note-Title`, `X-Note-Tags` (at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed), `X-Note-Alt` (images), `X-Note-Name` (the original file name), `X-Note-Date` (where it is placed in the feed: a past date backdates it, a future one schedules it). A markdown note with its pictures is one `multipart/form-data` request: a `text` part (the markdown, a field or a file part; a file part keeps its line breaks, a field's are sent as CRLF), `file` parts (up to 10 pictures, each with its file name and image `Content-Type`) and `alt.<file name>` fields. The pictures are stored first, each as its own note named by its file name, then the text with its references to them (`![](chart.png)`, `[x]: chart.png`, as written or percent-decoded) swapped for the stored files; a picture it never refers to is appended as `![](file)`. Several pictures may have the same file name (a phone calls every photo `image.jpg`): each is stored, and they are appended, but a text that refers to such a name is 400. Everything is checked before the first write, and a failed write removes what the request stored: all or nothing. `X-Note-Alt` and `X-Note-Name` are 400. The answer has `attachments`, the stored pictures in the order of the `file` parts. The `text` part is optional: `X-Note-Title` goes on the text note, `X-Note-Tags` on every note, `X-Note-Date` on the text note (its pictures take the milliseconds just before it); with no `text` part only the pictures are stored, each with the title, and the answer's top level is the first picture. A refused multipart post that would have created a protected feed creates nothing.
16
+ * The body is the note, a file, and `Content-Type` says which kind: `text/markdown` (UTF-8, at most 102400 bytes), or an image, `image/png`, `image/jpeg`, `image/gif` or `image/webp` (at most NOTEFEED_MAX_IMAGE_BYTES as sent, default 10 MiB). Nothing is guessed: any other type, or none, is `415`, and so is a body that is not what the type says (an image is recognized by its first bytes; SVG is refused). Markdown is stored byte for byte. An image is stored processed, not as sent: turned the way its EXIF orientation says, scaled to at most 2560 px on its long side, encoded again in its own format (JPEG and WebP at quality 85, PNG lossless, an animated GIF or WebP keeps its frames), and without metadata (EXIF, GPS position, XMP, comments). Send it full size; one of more than 100 megapixels is `413`, and a body that does not decode as the image it declares is `415`. Creates the feed with its first note, optionally protected by its own password (`X-Feed-Password`; 1 to 256 printable ASCII characters, with no space at the start or end) and with the read id in `X-Read-Id`; both are only used by the post that creates the feed. Where licenses are sold, the post that creates the feed can bring a license key in `X-License-Key`, with or without a password; a feed password needs one, or the post is refused with `402`. Posting to a protected feed needs its password. Also served at `POST /{feed}`, the short form curl one-liners use. The response names the note's `file` and where it is served, `file_url`, under the feed's read id (public like the read link). Metadata goes in headers: `X-Note-Title`, `X-Note-Tags` (at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed), `X-Note-Alt` (images), `X-Note-Name` (the original file name), `X-Note-Date` (where it is placed in the feed: a past date backdates it, a future one schedules it), `X-Note-Draft` (`true`: a draft, which readers do not see until it is published). A markdown note with its pictures is one `multipart/form-data` request: a `text` part (the markdown, a field or a file part; a file part keeps its line breaks, a field's are sent as CRLF), `file` parts (up to 10 pictures, each with its file name and image `Content-Type`) and `alt.<file name>` fields. The pictures are stored first, each as its own note named by its file name, then the text with its references to them (`![](chart.png)`, `[x]: chart.png`, as written or percent-decoded) swapped for the stored files; a picture it never refers to is appended as `![](file)`. Several pictures may have the same file name (a phone calls every photo `image.jpg`): each is stored, and they are appended, but a text that refers to such a name is 400. Everything is checked before the first write, and a failed write removes what the request stored: all or nothing. `X-Note-Alt` and `X-Note-Name` are 400. The answer has `attachments`, the stored pictures in the order of the `file` parts. The `text` part is optional: `X-Note-Title` goes on the text note, `X-Note-Tags` on every note, `X-Note-Date` on the text note (its pictures take the milliseconds just before it); with no `text` part only the pictures are stored, each with the title, and the answer's top level is the first picture. A refused multipart post that would have created a protected feed creates nothing.
17
17
  */
18
18
  export const postNote = (options) => (options.client ?? client).post({
19
19
  bodySerializer: null,
@@ -46,6 +46,10 @@ export type Note = {
46
46
  * Present, and true, when the note's date is after now: it is stored, and readers see it from its date on. Absent for every other note
47
47
  */
48
48
  scheduled?: true;
49
+ /**
50
+ * Present, and true, for a draft: only whoever can post to the feed sees it; readers do not, until it is published (PATCH `draft: false`). Absent for every other note
51
+ */
52
+ draft?: true;
49
53
  /**
50
54
  * The note's page in the web UI
51
55
  */
@@ -95,6 +99,10 @@ export type Created = {
95
99
  * Where the note's file is served, absolute, under the feed's read id (public like the read link); null while the feed has no read link
96
100
  */
97
101
  file_url: string | null;
102
+ /**
103
+ * Present, and true, when the note was stored as a draft (`X-Note-Draft: true`): readers do not see it, or its file, until it is published
104
+ */
105
+ draft?: true;
98
106
  };
99
107
  /**
100
108
  * The note the post is about: its text note, or with no `text` part its first picture
@@ -121,6 +129,10 @@ export type Posted = {
121
129
  * Where the note's file is served, absolute, under the feed's read id (public like the read link); null while the feed has no read link
122
130
  */
123
131
  file_url: string | null;
132
+ /**
133
+ * Present, and true, when the note was stored as a draft (`X-Note-Draft: true`): readers do not see it, or its file, until it is published
134
+ */
135
+ draft?: true;
124
136
  /**
125
137
  * A multipart request's pictures, one per `file` part, in their order; absent for a raw body
126
138
  */
@@ -171,6 +183,10 @@ export type NoteEdited = {
171
183
  * Present, and true, when the note's date is after now: it is stored, and readers see it from its date on. Absent for every other note
172
184
  */
173
185
  scheduled?: true;
186
+ /**
187
+ * Present, and true, for a draft: only whoever can post to the feed sees it; readers do not, until it is published (PATCH `draft: false`). Absent for every other note
188
+ */
189
+ draft?: true;
174
190
  /**
175
191
  * The note's page in the web UI
176
192
  */
@@ -210,7 +226,7 @@ export type Error = {
210
226
  /**
211
227
  * Stable machine-readable code; absent only on a 500
212
228
  */
213
- code?: 'invalid_feed' | 'reserved_feed' | 'auth' | 'rate_limited' | 'too_many_attempts' | 'feed_limit' | 'note_limit' | 'image_limit' | 'images_off' | 'license_required' | 'empty_note' | 'too_large' | 'unsupported_type' | 'invalid_body' | 'invalid_request' | 'not_found' | 'feed_exists' | 'taken' | 'removed' | 'blocked' | 'unreadable' | 'unavailable' | 'busy';
229
+ code?: 'invalid_feed' | 'reserved_feed' | 'auth' | 'rate_limited' | 'too_many_attempts' | 'feed_limit' | 'note_limit' | 'image_limit' | 'images_off' | 'license_required' | 'empty_note' | 'too_large' | 'unsupported_type' | 'invalid_body' | 'invalid_request' | 'not_found' | 'feed_exists' | 'taken' | 'title_image' | 'removed' | 'blocked' | 'unreadable' | 'unavailable' | 'busy';
214
230
  };
215
231
  export type NoteMeta = {
216
232
  /**
@@ -229,6 +245,10 @@ export type NoteMeta = {
229
245
  * An ISO 8601 date and time with its offset (`2026-10-01T09:30:00Z`, `2026-10-01T11:30:00+02:00`) or a date alone (midnight UTC). When the note is placed in the feed, from 1970 to 9999. A time after now schedules it: readers see it from then on. The note's attachments move with it.
230
246
  */
231
247
  date?: string;
248
+ /**
249
+ * true makes the note a draft, which readers do not see; false publishes it. Its attachments follow (an attachment's own state can't be set: 400), and its date stays: a past date places it there, a future one schedules it. A note whose picture is the feed's title image cannot become a draft (409, `title_image`)
250
+ */
251
+ draft?: boolean;
232
252
  };
233
253
  export type PasswordJson = {
234
254
  /**
@@ -483,6 +503,10 @@ export type PostNoteData = {
483
503
  * An ISO 8601 date and time with its offset (`2026-10-01T09:30:00Z`, `2026-10-01T11:30:00+02:00`) or a date alone (midnight UTC). When the note is placed in the feed, from 1970 to 9999. A time after now schedules it: readers see it from then on. Empty is now.
484
504
  */
485
505
  'X-Note-Date'?: string;
506
+ /**
507
+ * `true` stores the note, and its pictures, as a draft: only whoever can post to the feed sees it, readers do not until it is published. Left out or `false`: published
508
+ */
509
+ 'X-Note-Draft'?: 'true' | 'false';
486
510
  };
487
511
  path: {
488
512
  /**
@@ -495,7 +519,7 @@ export type PostNoteData = {
495
519
  };
496
520
  export type PostNoteErrors = {
497
521
  /**
498
- * Invalid or reserved feed name; a blank note; a bad title, alt text, tags or `X-Note-Date`; `X-Note-Created` (the header's name before 0.28.0); a new password that is not printable ASCII; a license key that is none; a multipart body with an unexpected part, no `text` and no `file`, more than 10 files, a bad file name, a text that refers to a file name given twice, a text sent with pictures that takes too long to read (`NOTEFEED_PARSE_TIMEOUT_MS`), or `X-Note-Alt` / `X-Note-Name`
522
+ * Invalid or reserved feed name; a blank note; a bad title, alt text, tags, `X-Note-Date` or `X-Note-Draft`; `X-Note-Created` (the header's name before 0.28.0); a new password that is not printable ASCII; a license key that is none; a multipart body with an unexpected part, no `text` and no `file`, more than 10 files, a bad file name, a text that refers to a file name given twice, a text sent with pictures that takes too long to read (`NOTEFEED_PARSE_TIMEOUT_MS`), or `X-Note-Alt` / `X-Note-Name`
499
523
  */
500
524
  400: Error;
501
525
  /**
@@ -676,7 +700,7 @@ export type PatchNoteData = {
676
700
  };
677
701
  export type PatchNoteErrors = {
678
702
  /**
679
- * Invalid or reserved feed name; nothing to change; a bad title, alt text, tag or date; too many tags; alt for a note that has none; bad JSON
703
+ * Invalid or reserved feed name; nothing to change; a bad title, alt text, tag, date or draft; too many tags; alt for a note that has none; bad JSON
680
704
  */
681
705
  400: Error;
682
706
  /**
@@ -687,6 +711,10 @@ export type PatchNoteErrors = {
687
711
  * No such note
688
712
  */
689
713
  404: Error;
714
+ /**
715
+ * The note, or a picture posted with it, is the feed's title image, so it cannot become a draft (`title_image`)
716
+ */
717
+ 409: Error;
690
718
  /**
691
719
  * Content-Type is not application/json
692
720
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notefeed",
3
- "version": "0.32.0",
3
+ "version": "0.33.1",
4
4
  "description": "Client and CLI for notefeed, a self-hosted inbox for markdown notes that you read back as RSS.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Marcel Bruckner",