notefeed 0.31.1 → 0.33.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 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
@@ -1,7 +1,7 @@
1
- import type { Created, Error as ApiError, Feed, FeedSettings, Note } from "./generated/types.gen.js";
1
+ import type { Created, Error as ApiError, Feed, FeedSettings, Licenses, Note } from "./generated/types.gen.js";
2
2
  /** The stable error codes the API answers with. */
3
3
  export type ErrorCode = NonNullable<ApiError["code"]>;
4
- export type { Created, Feed, FeedSettings, Note };
4
+ export type { Created, Feed, FeedSettings, Licenses, Note };
5
5
  /** A picture posted with a note: `![](name)` in the markdown refers to it. `type` as for post(). */
6
6
  export type Attachment = {
7
7
  name: string;
@@ -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;
@@ -184,6 +186,9 @@ export declare class Client {
184
186
  pageSize?: number;
185
187
  tag?: string;
186
188
  }): AsyncGenerator<Note>;
189
+ /** What licenses are sold (plans, amounts in cents), where a person buys one (`buy_url`) and where buyers manage their purchases
190
+ * at the payment provider, signed in by e-mail (`manage_url`); no key opens that page. Only where licenses are sold. */
191
+ licenses(): Promise<Licenses>;
187
192
  readNote(readId: string, id: string): Promise<Note>;
188
193
  private feedFor;
189
194
  private opts;
package/dist/client.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * OpenAPI description (./generated, `npm run generate` in the repo). No runtime dependencies.
4
4
  */
5
5
  import { createClient, createConfig } from "./generated/client/index.js";
6
- import { changeFeedPassword, createFeed, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getNote, getReadNote, listNotes, listReadNotes, patchNote, postNote, putFeedLicense, recoverFeedPassword, removeFeedImage, setFeedImage, updateFeed } from "./generated/sdk.gen.js";
6
+ import { changeFeedPassword, createFeed, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getLicenses, getNote, getReadNote, listNotes, listReadNotes, patchNote, postNote, putFeedLicense, recoverFeedPassword, removeFeedImage, setFeedImage, updateFeed } from "./generated/sdk.gen.js";
7
7
  /** Any failure talking to notefeed. `status` and `code` are null when there was no API answer. */
8
8
  export class NotefeedError extends Error {
9
9
  status;
@@ -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;
@@ -267,6 +269,11 @@ export class Client {
267
269
  readNotes(readId, options = {}) {
268
270
  return this.pages((before) => listReadNotes({ client: this.api, path: { readId }, query: { limit: options.pageSize, before, tag: options.tag }, ...this.opts() }));
269
271
  }
272
+ /** What licenses are sold (plans, amounts in cents), where a person buys one (`buy_url`) and where buyers manage their purchases
273
+ * at the payment provider, signed in by e-mail (`manage_url`); no key opens that page. Only where licenses are sold. */
274
+ async licenses() {
275
+ return this.call(getLicenses({ client: this.api, signal: AbortSignal.timeout(this.timeoutMs) }));
276
+ }
270
277
  async readNote(readId, id) {
271
278
  return this.call(getReadNote({ client: this.api, path: { readId, id }, ...this.opts() }));
272
279
  }
@@ -1,2 +1,2 @@
1
- export { changeFeedPassword, createFeed, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, type Options, patchNote, postNote, putFeedLicense, recoverFeedPassword, removeFeedImage, removeFeedPassword, setFeedImage, updateFeed } from './sdk.gen.js';
2
- export type { ChangeFeedPasswordData, ChangeFeedPasswordError, ChangeFeedPasswordErrors, ChangeFeedPasswordResponse, ChangeFeedPasswordResponses, ClientOptions, Created, CreateFeedData, CreateFeedError, CreateFeedErrors, CreateFeedJson, CreateFeedResponse, CreateFeedResponses, DeleteFeedData, DeleteFeedError, DeleteFeedErrors, DeleteFeedLicenseData, DeleteFeedLicenseError, DeleteFeedLicenseErrors, DeleteFeedLicenseResponse, DeleteFeedLicenseResponses, DeleteFeedResponse, DeleteFeedResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, EditNoteData, EditNoteError, EditNoteErrors, EditNoteResponse, EditNoteResponses, Error, Feed, FeedSettings, FreeTier, GetFeedData, GetFeedError, GetFeedErrors, GetFeedResponse, GetFeedResponses, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetOpenApiData, GetOpenApiResponse, GetOpenApiResponses, GetReadFeedData, GetReadFeedError, GetReadFeedErrors, GetReadFeedResponse, GetReadFeedResponses, GetReadNoteData, GetReadNoteError, GetReadNoteErrors, GetReadNoteResponse, GetReadNoteResponses, LicenseKey, ListNotesData, ListNotesError, ListNotesErrors, ListNotesResponse, ListNotesResponses, ListReadNotesData, ListReadNotesError, ListReadNotesErrors, ListReadNotesResponse, ListReadNotesResponses, MultipartNote, Note, NoteEdited, NoteList, NoteMeta, PasswordJson, PatchNoteData, PatchNoteError, PatchNoteErrors, PatchNoteResponse, PatchNoteResponses, Posted, PostNoteData, PostNoteError, PostNoteErrors, PostNoteResponse, PostNoteResponses, PutFeedLicenseData, PutFeedLicenseError, PutFeedLicenseErrors, PutFeedLicenseResponse, PutFeedLicenseResponses, ReadFeed, RecoverFeedPasswordData, RecoverFeedPasswordError, RecoverFeedPasswordErrors, RecoverFeedPasswordResponse, RecoverFeedPasswordResponses, RecoveryJson, RemoveFeedImageData, RemoveFeedImageError, RemoveFeedImageErrors, RemoveFeedImageResponse, RemoveFeedImageResponses, RemoveFeedPasswordData, RemoveFeedPasswordError, RemoveFeedPasswordErrors, RemoveFeedPasswordResponse, RemoveFeedPasswordResponses, SetFeedImageData, SetFeedImageError, SetFeedImageErrors, SetFeedImageResponse, SetFeedImageResponses, UpdateFeedData, UpdateFeedError, UpdateFeedErrors, UpdateFeedResponse, UpdateFeedResponses } from './types.gen.js';
1
+ export { changeFeedPassword, createFeed, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getLicenses, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, type Options, patchNote, postNote, putFeedLicense, recoverFeedPassword, removeFeedImage, removeFeedPassword, setFeedImage, updateFeed } from './sdk.gen.js';
2
+ export type { ChangeFeedPasswordData, ChangeFeedPasswordError, ChangeFeedPasswordErrors, ChangeFeedPasswordResponse, ChangeFeedPasswordResponses, ClientOptions, Created, CreateFeedData, CreateFeedError, CreateFeedErrors, CreateFeedJson, CreateFeedResponse, CreateFeedResponses, DeleteFeedData, DeleteFeedError, DeleteFeedErrors, DeleteFeedLicenseData, DeleteFeedLicenseError, DeleteFeedLicenseErrors, DeleteFeedLicenseResponse, DeleteFeedLicenseResponses, DeleteFeedResponse, DeleteFeedResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, EditNoteData, EditNoteError, EditNoteErrors, EditNoteResponse, EditNoteResponses, Error, Feed, FeedSettings, FreeTier, GetFeedData, GetFeedError, GetFeedErrors, GetFeedResponse, GetFeedResponses, GetLicensesData, GetLicensesError, GetLicensesErrors, GetLicensesResponse, GetLicensesResponses, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetOpenApiData, GetOpenApiResponse, GetOpenApiResponses, GetReadFeedData, GetReadFeedError, GetReadFeedErrors, GetReadFeedResponse, GetReadFeedResponses, GetReadNoteData, GetReadNoteError, GetReadNoteErrors, GetReadNoteResponse, GetReadNoteResponses, LicenseKey, Licenses, ListNotesData, ListNotesError, ListNotesErrors, ListNotesResponse, ListNotesResponses, ListReadNotesData, ListReadNotesError, ListReadNotesErrors, ListReadNotesResponse, ListReadNotesResponses, MultipartNote, Note, NoteEdited, NoteList, NoteMeta, PasswordJson, PatchNoteData, PatchNoteError, PatchNoteErrors, PatchNoteResponse, PatchNoteResponses, Posted, PostNoteData, PostNoteError, PostNoteErrors, PostNoteResponse, PostNoteResponses, PutFeedLicenseData, PutFeedLicenseError, PutFeedLicenseErrors, PutFeedLicenseResponse, PutFeedLicenseResponses, ReadFeed, RecoverFeedPasswordData, RecoverFeedPasswordError, RecoverFeedPasswordErrors, RecoverFeedPasswordResponse, RecoverFeedPasswordResponses, RecoveryJson, RemoveFeedImageData, RemoveFeedImageError, RemoveFeedImageErrors, RemoveFeedImageResponse, RemoveFeedImageResponses, RemoveFeedPasswordData, RemoveFeedPasswordError, RemoveFeedPasswordErrors, RemoveFeedPasswordResponse, RemoveFeedPasswordResponses, SetFeedImageData, SetFeedImageError, SetFeedImageErrors, SetFeedImageResponse, SetFeedImageResponses, UpdateFeedData, UpdateFeedError, UpdateFeedErrors, UpdateFeedResponse, UpdateFeedResponses } from './types.gen.js';
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated by @hey-api/openapi-ts
2
- export { changeFeedPassword, createFeed, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, patchNote, postNote, putFeedLicense, recoverFeedPassword, removeFeedImage, removeFeedPassword, setFeedImage, updateFeed } from './sdk.gen.js';
2
+ export { changeFeedPassword, createFeed, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getLicenses, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, patchNote, postNote, putFeedLicense, recoverFeedPassword, removeFeedImage, removeFeedPassword, setFeedImage, updateFeed } from './sdk.gen.js';
@@ -1,5 +1,5 @@
1
1
  import type { Client, ClientMeta, Options as Options2, RequestResult, TDataShape } from './client/index.js';
2
- import type { ChangeFeedPasswordData, ChangeFeedPasswordErrors, ChangeFeedPasswordResponses, CreateFeedData, CreateFeedErrors, CreateFeedResponses, DeleteFeedData, DeleteFeedErrors, DeleteFeedLicenseData, DeleteFeedLicenseErrors, DeleteFeedLicenseResponses, DeleteFeedResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, EditNoteData, EditNoteErrors, EditNoteResponses, GetFeedData, GetFeedErrors, GetFeedResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetOpenApiData, GetOpenApiResponses, GetReadFeedData, GetReadFeedErrors, GetReadFeedResponses, GetReadNoteData, GetReadNoteErrors, GetReadNoteResponses, ListNotesData, ListNotesErrors, ListNotesResponses, ListReadNotesData, ListReadNotesErrors, ListReadNotesResponses, PatchNoteData, PatchNoteErrors, PatchNoteResponses, PostNoteData, PostNoteErrors, PostNoteResponses, PutFeedLicenseData, PutFeedLicenseErrors, PutFeedLicenseResponses, RecoverFeedPasswordData, RecoverFeedPasswordErrors, RecoverFeedPasswordResponses, RemoveFeedImageData, RemoveFeedImageErrors, RemoveFeedImageResponses, RemoveFeedPasswordData, RemoveFeedPasswordErrors, RemoveFeedPasswordResponses, SetFeedImageData, SetFeedImageErrors, SetFeedImageResponses, UpdateFeedData, UpdateFeedErrors, UpdateFeedResponses } from './types.gen.js';
2
+ import type { ChangeFeedPasswordData, ChangeFeedPasswordErrors, ChangeFeedPasswordResponses, CreateFeedData, CreateFeedErrors, CreateFeedResponses, DeleteFeedData, DeleteFeedErrors, DeleteFeedLicenseData, DeleteFeedLicenseErrors, DeleteFeedLicenseResponses, DeleteFeedResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, EditNoteData, EditNoteErrors, EditNoteResponses, GetFeedData, GetFeedErrors, GetFeedResponses, GetLicensesData, GetLicensesErrors, GetLicensesResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetOpenApiData, GetOpenApiResponses, GetReadFeedData, GetReadFeedErrors, GetReadFeedResponses, GetReadNoteData, GetReadNoteErrors, GetReadNoteResponses, ListNotesData, ListNotesErrors, ListNotesResponses, ListReadNotesData, ListReadNotesErrors, ListReadNotesResponses, PatchNoteData, PatchNoteErrors, PatchNoteResponses, PostNoteData, PostNoteErrors, PostNoteResponses, PutFeedLicenseData, PutFeedLicenseErrors, PutFeedLicenseResponses, RecoverFeedPasswordData, RecoverFeedPasswordErrors, RecoverFeedPasswordResponses, RemoveFeedImageData, RemoveFeedImageErrors, RemoveFeedImageResponses, RemoveFeedPasswordData, RemoveFeedPasswordErrors, RemoveFeedPasswordResponses, SetFeedImageData, SetFeedImageErrors, SetFeedImageResponses, UpdateFeedData, UpdateFeedErrors, UpdateFeedResponses } from './types.gen.js';
3
3
  export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
4
4
  /**
5
5
  * You can provide a client instance returned by `createClient()` instead of
@@ -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
  /**
@@ -131,6 +131,12 @@ export declare const listReadNotes: <ThrowOnError extends boolean = false>(optio
131
131
  * Public, like the read link. A read id the operator removed is `410`.
132
132
  */
133
133
  export declare const getReadNote: <ThrowOnError extends boolean = false>(options: Options<GetReadNoteData, ThrowOnError>) => RequestResult<GetReadNoteResponses, GetReadNoteErrors, ThrowOnError>;
134
+ /**
135
+ * What licenses are sold, where to buy them and where to manage them
136
+ *
137
+ * Only on an instance that sells licenses (404 elsewhere). Public, like the license page. A license lifts a feed's free tier; a person buys it on `buy_url` and gets a key to put on a feed. Cancelling, the payment method and invoices are at the payment provider, where buyers sign in with the e-mail address they bought with (`manage_url`); no license key opens it (ADR 0050).
138
+ */
139
+ export declare const getLicenses: <ThrowOnError extends boolean = false>(options?: Options<GetLicensesData, ThrowOnError>) => RequestResult<GetLicensesResponses, GetLicensesErrors, ThrowOnError>;
134
140
  /**
135
141
  * This API's OpenAPI document
136
142
  *
@@ -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,
@@ -225,6 +225,12 @@ export const listReadNotes = (options) => (options.client ?? client).get({ url:
225
225
  * Public, like the read link. A read id the operator removed is `410`.
226
226
  */
227
227
  export const getReadNote = (options) => (options.client ?? client).get({ url: '/api/v1/read/{readId}/notes/{id}', ...options });
228
+ /**
229
+ * What licenses are sold, where to buy them and where to manage them
230
+ *
231
+ * Only on an instance that sells licenses (404 elsewhere). Public, like the license page. A license lifts a feed's free tier; a person buys it on `buy_url` and gets a key to put on a feed. Cancelling, the payment method and invoices are at the payment provider, where buyers sign in with the e-mail address they bought with (`manage_url`); no license key opens it (ADR 0050).
232
+ */
233
+ export const getLicenses = (options) => (options?.client ?? client).get({ url: '/api/v1/licenses', ...options });
228
234
  /**
229
235
  * This API's OpenAPI document
230
236
  *
@@ -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
  /**
@@ -346,6 +366,49 @@ export type ReadFeed = {
346
366
  */
347
367
  image_url: string | null;
348
368
  };
369
+ export type Licenses = {
370
+ /**
371
+ * What is sold right now, monthly then yearly; empty while the payment provider can't be read
372
+ */
373
+ plans: Array<{
374
+ /**
375
+ * How long one payment lasts; the license renews until cancelled
376
+ */
377
+ period: 'month' | 'year';
378
+ /**
379
+ * The plan's name, as the payment provider holds it
380
+ */
381
+ name: string;
382
+ /**
383
+ * The plan's description from the payment provider, possibly empty
384
+ */
385
+ description: string;
386
+ /**
387
+ * The price per period in the currency's smallest unit (cents)
388
+ */
389
+ amount: number;
390
+ /**
391
+ * ISO 4217 code, e.g. EUR
392
+ */
393
+ currency: string;
394
+ /**
395
+ * Whether the amount includes VAT
396
+ */
397
+ tax_included: boolean;
398
+ }>;
399
+ /**
400
+ * The license page: a person buys there, and gets the key after paying
401
+ */
402
+ buy_url: string;
403
+ /**
404
+ * The payment provider's sign-in for buyers, by the e-mail address bought with: cancelling, the payment method, invoices, keys. No license key opens it
405
+ */
406
+ manage_url: string;
407
+ /**
408
+ * How managing a purchase works, in a sentence to show a person
409
+ */
410
+ manage: string;
411
+ };
349
412
  export type ListNotesData = {
350
413
  body?: never;
351
414
  headers?: {
@@ -440,6 +503,10 @@ export type PostNoteData = {
440
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.
441
504
  */
442
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';
443
510
  };
444
511
  path: {
445
512
  /**
@@ -452,7 +519,7 @@ export type PostNoteData = {
452
519
  };
453
520
  export type PostNoteErrors = {
454
521
  /**
455
- * 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`
456
523
  */
457
524
  400: Error;
458
525
  /**
@@ -633,7 +700,7 @@ export type PatchNoteData = {
633
700
  };
634
701
  export type PatchNoteErrors = {
635
702
  /**
636
- * 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
637
704
  */
638
705
  400: Error;
639
706
  /**
@@ -644,6 +711,10 @@ export type PatchNoteErrors = {
644
711
  * No such note
645
712
  */
646
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;
647
718
  /**
648
719
  * Content-Type is not application/json
649
720
  */
@@ -1423,6 +1494,26 @@ export type GetReadNoteResponses = {
1423
1494
  200: Note;
1424
1495
  };
1425
1496
  export type GetReadNoteResponse = GetReadNoteResponses[keyof GetReadNoteResponses];
1497
+ export type GetLicensesData = {
1498
+ body?: never;
1499
+ path?: never;
1500
+ query?: never;
1501
+ url: '/api/v1/licenses';
1502
+ };
1503
+ export type GetLicensesErrors = {
1504
+ /**
1505
+ * Licenses are not sold on this instance
1506
+ */
1507
+ 404: Error;
1508
+ };
1509
+ export type GetLicensesError = GetLicensesErrors[keyof GetLicensesErrors];
1510
+ export type GetLicensesResponses = {
1511
+ /**
1512
+ * The plans, the license page and the provider's sign-in for buyers
1513
+ */
1514
+ 200: Licenses;
1515
+ };
1516
+ export type GetLicensesResponse = GetLicensesResponses[keyof GetLicensesResponses];
1426
1517
  export type GetOpenApiData = {
1427
1518
  body?: never;
1428
1519
  path?: never;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { AuthError, Client, ConfigError, ImagesOffError, InvalidRequestError, LimitReachedError, NotefeedError, NotFoundError, NoteTooLargeError, RateLimitedError, } from "./client.js";
2
- export type { Attachment, ClientOptions, Created, ErrorCode, Feed, FeedSettings, Note } from "./client.js";
2
+ export type { Attachment, ClientOptions, Created, ErrorCode, Feed, FeedSettings, Licenses, Note } from "./client.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notefeed",
3
- "version": "0.31.1",
3
+ "version": "0.33.0",
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",