notefeed 0.27.0 → 0.29.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
@@ -8,8 +8,8 @@ export NOTEFEED_URL=https://notes.example.com NOTEFEED_FEED=homelab-7f3k2q9x4m8w
8
8
  npx notefeed post "# Backup finished"
9
9
  backup.sh 2>&1 | npx notefeed post -
10
10
  npx notefeed notes --limit 5
11
- npx notefeed edit 20260929T140512Z-backup-finished "# Backup finished, verified"
12
- npx notefeed delete 20260929T140512Z-backup-finished
11
+ npx notefeed edit Vq3xH8mTn2KfLw0aZr9bYe "# Backup finished, verified"
12
+ npx notefeed delete Vq3xH8mTn2KfLw0aZr9bYe
13
13
  ```
14
14
 
15
15
  ```js
@@ -18,12 +18,12 @@ import { Client } from "notefeed";
18
18
  const client = new Client({ url: "https://notes.example.com", feed: "homelab-7f3k2q9x4m8wz" }); // password: "..." if the instance has one
19
19
  const created = await client.post("# Deploy done\nversion 1.4.2");
20
20
  console.log(created.url, created.read_url);
21
- for await (const note of client.notes()) console.log(note.created_at, note.title); // newest first, page by page
21
+ for await (const note of client.notes()) console.log(note.date, note.title); // newest first, page by page
22
22
  ```
23
23
 
24
24
  `read_url` is `null` for a feed without a read link (rare; see the operations docs).
25
25
 
26
- To post a note in the past, give when it was written: `client.post(text, { createdAt: "2025-03-01T09:30:00Z" })` (a `Date`, an ISO 8601 date-time with its offset, or a date alone for midnight UTC; from 1970 to now), or `notefeed post "…" --created 2025-03-01`. The note takes its place in the feed by that time.
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
28
  `Client.fromEnv()` reads `NOTEFEED_URL` / `NOTEFEED_FEED` / `NOTEFEED_PASSWORD`; so does the `notefeed` command, unless given `--url` / `--feed` / `--password`.
29
29
 
@@ -33,6 +33,6 @@ A posted note can be changed or removed: `await client.edit(id, content)` replac
33
33
 
34
34
  A feed can have a title and a description, and can be deleted: `await client.feedInfo()` returns `{ name, title, description, protected, read_url }`, `await client.updateFeed({ title, description })` replaces both (an empty string clears one) and returns the feed, and `await client.deleteFeed()` removes the feed with all its notes for good. All three take the same `{ feed, feedPassword }` options as `post`, and a feed that doesn't exist is a `NotFoundError`. Anyone who can post to a feed can do this, and deleting is permanent; there are no `notefeed` commands for it.
35
35
 
36
- A picture is a note of its own: `await client.post(bytes, { type: "image/png", alt: "…" })` takes a `Uint8Array` or a `Blob` (PNG, JPEG, GIF or WebP; a Blob's own type is used if you leave `type` out) and returns what a markdown post returns, with the picture's `file` and `file_url` added. Write `![](file)` in a markdown note to show it; the feed is created by its first note, a picture that is too large is a `NoteTooLargeError`, and an instance that takes no pictures (`NOTEFEED_IMAGE_UPLOADS=0`) answers every picture with an `ImagesOffError`. The server checks the bytes against the declared type. `await client.update(id, { title, alt })` changes a note's title or a picture's alt text, and `await client.edit(id, content)` replaces its content. `await client.updateFeed({ title, description, image: file })` makes a picture the feed's title image (`image: ""` removes it; leaving `image` out keeps it). `notefeed post --file photo.png` posts a file (the type comes from the extension, or `--type`). To post a note with its pictures in one call, `await client.post(text, { attachments: [{ name: "chart.png", content: bytes, type: "image/png" }] })` posts them with the text in one request, and the server swaps `![](chart.png)` for each file name (`null` content posts only the pictures) (`notefeed post "…" --attach chart.png` does the same); nothing is posted when one is refused. Pictures are public to anyone with the feed's read link, and metadata such as EXIF is not removed.
36
+ A picture is a note of its own: `await client.post(bytes, { type: "image/png", alt: "…" })` takes a `Uint8Array` or a `Blob` (PNG, JPEG, GIF or WebP; a Blob's own type is used if you leave `type` out) and returns what a markdown post returns, with the picture's `file` and `file_url` added. Write `![](file)` in a markdown note to show it; the feed is created by its first note, a picture that is too large is a `NoteTooLargeError`, and an instance that takes no pictures (`NOTEFEED_IMAGE_UPLOADS=0`) answers every picture with an `ImagesOffError`. The server checks the bytes against the declared type. `await client.update(id, { title, alt, tags, date })` changes a note's title, a picture's alt text, a note's tags (the whole list; `notefeed update <id> --tags a,b`) or its date, and `await client.edit(id, content)` replaces its content. `await client.updateFeed({ title, description, image: file })` makes a picture the feed's title image (`image: ""` removes it; leaving `image` out keeps it). `notefeed post --file photo.png` posts a file (the type comes from the extension, or `--type`). To post a note with its pictures in one call, `await client.post(text, { attachments: [{ name: "chart.png", content: bytes, type: "image/png" }] })` posts them with the text in one request, and the server swaps `![](chart.png)` for each file name (`null` content posts only the pictures) (`notefeed post "…" --attach chart.png` does the same); nothing is posted when one is refused. Pictures are public to anyone with the feed's read link, and metadata such as EXIF is not removed.
37
37
 
38
38
  Full documentation: https://docs.notefeed.me/integrations/clients/
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]... [--created 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] [--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] [--url URL] [--feed FEED] [--password PASSWORD]",
12
+ " notefeed update <id> [--title TITLE] [--alt ALT] [--tags LIST] [--date WHEN] [--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");
@@ -30,7 +30,8 @@ export async function main(argv, io = process) {
30
30
  title: { type: "string" },
31
31
  alt: { type: "string" },
32
32
  tag: { type: "string", multiple: true },
33
- created: { type: "string" },
33
+ tags: { type: "string" },
34
+ date: { type: "string" },
34
35
  attach: { type: "string", multiple: true },
35
36
  limit: { type: "string" },
36
37
  json: { type: "boolean" },
@@ -54,7 +55,7 @@ export async function main(argv, io = process) {
54
55
  const noText = attachments.length > 0 && text === undefined && !values.file;
55
56
  const input = noText ? { content: null, type: undefined, name: undefined } : await read(text, values.file, values.type, io);
56
57
  // The server refuses a file name next to attachments: the text has none then.
57
- const note = await client(values).post(input.content, { type: input.type, title: values.title, tags: values.tag, createdAt: values.created, name: attachments.length ? undefined : input.name, attachments });
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 });
58
59
  io.stdout.write(`${[...(noText ? [] : [note.url]), ...note.attachments.map((a) => a.url)].join("\n")}\n`);
59
60
  return 0;
60
61
  }
@@ -65,9 +66,11 @@ export async function main(argv, io = process) {
65
66
  return 0;
66
67
  }
67
68
  if (command === "update" && text !== undefined && !rest.length) {
68
- if (values.title === undefined && values.alt === undefined)
69
- throw new UsageError("give --title and/or --alt");
70
- const note = await client(values).update(text, { title: values.title, alt: values.alt });
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
+ // --tags is the whole new list, comma-separated; "" removes them all.
72
+ 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 });
71
74
  io.stdout.write(`${note.url}\n`);
72
75
  return 0;
73
76
  }
@@ -82,9 +85,9 @@ export async function main(argv, io = process) {
82
85
  let left = limit;
83
86
  for await (const n of client(values).notes({ pageSize: Math.min(limit, 100), tag: values.tag?.[0] })) {
84
87
  // To the second, UTC: the same form as the Python CLI, so scripts read either the same way.
85
- const when = new Date(n.created_at).toISOString().replace(/\.\d+Z$/, "Z");
88
+ const when = new Date(n.date).toISOString().replace(/\.\d+Z$/, "Z");
86
89
  // --json: exactly the documented fields, like the Python CLI, even if the server adds more.
87
- const fields = { id: n.id, type: n.type, title: n.title, ...(n.content !== undefined && { content: n.content }), file: n.file, file_url: n.file_url, size: n.size, tags: n.tags, created_at: when, url: n.url };
90
+ const fields = { id: n.id, type: n.type, title: n.title, ...(n.content !== undefined && { content: n.content }), file: n.file, file_url: n.file_url, size: n.size, tags: n.tags, date: when, url: n.url };
88
91
  io.stdout.write(values.json ? `${JSON.stringify(fields)}\n` : `${when} ${n.title || n.id} ${n.url}\n`);
89
92
  if (--left === 0)
90
93
  break;
package/dist/client.d.ts CHANGED
@@ -67,15 +67,15 @@ export declare class Client {
67
67
  *
68
68
  * `feedPassword` overrides the client's, for a feed that has its own password. `tags` label the note (at most 10, each 1 to 32 characters of
69
69
  * letters, digits, `-`, `_`, `.`, `:`; not verified). `title` is its title (at most 100 characters, one line); left out, it is taken from the
70
- * text. `alt` is a picture's alternative text, `name` the file's original name. `createdAt` posts the note in the past: when it was written, a
71
- * Date or an ISO 8601 date-time with its offset (`2026-10-01T09:30:00Z`) or a date alone (midnight UTC), from 1970 to now. `readId` is the read id the feed gets when this post creates it
70
+ * text. `alt` is a picture's alternative text, `name` the file's original name. `date` is the note's date, if not now: a
71
+ * Date or an ISO 8601 date-time with its offset (`2026-10-01T09:30:00Z`) or a date alone (midnight UTC), from 1970 to 9999; a future date schedules the note: readers see it from then on. `readId` is the read id the feed gets when this post creates it
72
72
  * (3 to 64 characters of `a-z`, `0-9`, `-`, `_`; random when left out; ignored for a feed that exists; a `taken` error when another feed has it).
73
73
  * `licenseKey` goes with `feedPassword` on the post that creates the feed, on an instance that sells licenses: a feed password is for
74
74
  * licensed feeds, and the new feed gets the license (a `license_required` error without the key; ignored on an existing feed).
75
75
  *
76
76
  * `attachments` are pictures posted with the note in one request: `![](name)` in the markdown shows one (a picture the text never refers to is
77
77
  * added at the end), and `Created.attachments` lists them in order. `tags` label the pictures too. `content` may be `null` when there are
78
- * attachments: only the pictures are posted, `title`, `tags` and `createdAt` go on each, and the result is the first picture plus `attachments`. Checked
78
+ * attachments: only the pictures are posted, `title`, `tags` and `date` go on each, and the result is the first picture plus `attachments`. Checked
79
79
  * here, before anything is sent (ConfigError): a name given twice, an attachment that is not a picture, content that is not markdown, `alt` or
80
80
  * `name` with attachments. What a name may be is the server's rule (docs/using/pictures.md, "Posting a note with its pictures"): a name it refuses is an
81
81
  * InvalidRequestError, `attachment "<name>": ...`, and whenever the server refuses any part (its message names it) nothing is posted.
@@ -88,7 +88,7 @@ export declare class Client {
88
88
  tags?: string[];
89
89
  alt?: string;
90
90
  name?: string;
91
- createdAt?: string | Date;
91
+ date?: string | Date;
92
92
  readId?: string;
93
93
  licenseKey?: string;
94
94
  attachments?: Attachment[];
@@ -102,10 +102,12 @@ export declare class Client {
102
102
  feedPassword?: string;
103
103
  type?: string;
104
104
  }): Promise<Note>;
105
- /** Set a note's title and/or alt text (alt for pictures); `""` removes one, so a markdown note's title follows its text again. Same options as post(). */
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
106
  update(id: string, meta: {
107
107
  title?: string;
108
108
  alt?: string;
109
+ tags?: string[];
110
+ date?: string | Date;
109
111
  }, options?: {
110
112
  feed?: string;
111
113
  feedPassword?: string;
@@ -125,6 +127,21 @@ export declare class Client {
125
127
  feed?: string;
126
128
  feedPassword?: string;
127
129
  }): Promise<Feed>;
130
+ /** Put the license the key names on the feed, taking it off the feed it was on; a feed needs no password for it. Only on an instance that sells licenses. Same options as post(). */
131
+ attachLicense(key: string, options?: {
132
+ feed?: string;
133
+ feedPassword?: string;
134
+ }): Promise<Feed>;
135
+ /** Take the feed's license off: it is a free feed again, and the key can go on another feed. Same options as post(). */
136
+ detachLicense(options?: {
137
+ feed?: string;
138
+ feedPassword?: string;
139
+ }): Promise<Feed>;
140
+ /** Set the feed's password. On a protected feed it changes it: `feedPassword` (or the client's) is the current one. An open feed takes one without, which claims it: its name was all that protected it. Every request for the feed then needs the new password; its read link stays open. Same options as post(). */
141
+ setFeedPassword(password: string, options?: {
142
+ feed?: string;
143
+ feedPassword?: string;
144
+ }): Promise<void>;
128
145
  /** Delete the feed with all its notes, settings and password, for good; its name is free again. Same options as post(). */
129
146
  deleteFeed(options?: {
130
147
  feed?: string;
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 { deleteFeed, deleteNote, editNote, getFeed, getNote, getReadNote, listNotes, listReadNotes, patchNote, postNote, updateFeed } from "./generated/sdk.gen.js";
6
+ import { changeFeedPassword, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getNote, getReadNote, listNotes, listReadNotes, patchNote, postNote, putFeedLicense, 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;
@@ -68,8 +68,8 @@ const FEED_RE = /^[a-z0-9_-]{1,64}$/; // same rule as the server; reserved names
68
68
  // A header holds bytes, and fetch refuses a character above U+00FF: text that may not be ASCII goes as its UTF-8 bytes written as
69
69
  // latin1 characters, which is what the server reads back.
70
70
  const headerValue = (text) => Array.from(new TextEncoder().encode(text), (b) => String.fromCharCode(b)).join("");
71
- // X-Note-Created: a Date as its UTC time, a string as given (the server checks it).
72
- const createdHeader = (at) => (typeof at === "string" ? at : at.toISOString());
71
+ // X-Note-Date: a Date as its UTC time, a string as given (the server checks it).
72
+ const dateString = (at) => (typeof at === "string" ? at : at.toISOString());
73
73
  // What a post sends: a string is markdown; bytes and Blobs need a media type (a Blob's own, if it has one).
74
74
  function asFile(content, type) {
75
75
  if (typeof content === "string")
@@ -117,15 +117,15 @@ export class Client {
117
117
  *
118
118
  * `feedPassword` overrides the client's, for a feed that has its own password. `tags` label the note (at most 10, each 1 to 32 characters of
119
119
  * letters, digits, `-`, `_`, `.`, `:`; not verified). `title` is its title (at most 100 characters, one line); left out, it is taken from the
120
- * text. `alt` is a picture's alternative text, `name` the file's original name. `createdAt` posts the note in the past: when it was written, a
121
- * Date or an ISO 8601 date-time with its offset (`2026-10-01T09:30:00Z`) or a date alone (midnight UTC), from 1970 to now. `readId` is the read id the feed gets when this post creates it
120
+ * text. `alt` is a picture's alternative text, `name` the file's original name. `date` is the note's date, if not now: a
121
+ * Date or an ISO 8601 date-time with its offset (`2026-10-01T09:30:00Z`) or a date alone (midnight UTC), from 1970 to 9999; a future date schedules the note: readers see it from then on. `readId` is the read id the feed gets when this post creates it
122
122
  * (3 to 64 characters of `a-z`, `0-9`, `-`, `_`; random when left out; ignored for a feed that exists; a `taken` error when another feed has it).
123
123
  * `licenseKey` goes with `feedPassword` on the post that creates the feed, on an instance that sells licenses: a feed password is for
124
124
  * licensed feeds, and the new feed gets the license (a `license_required` error without the key; ignored on an existing feed).
125
125
  *
126
126
  * `attachments` are pictures posted with the note in one request: `![](name)` in the markdown shows one (a picture the text never refers to is
127
127
  * added at the end), and `Created.attachments` lists them in order. `tags` label the pictures too. `content` may be `null` when there are
128
- * attachments: only the pictures are posted, `title`, `tags` and `createdAt` go on each, and the result is the first picture plus `attachments`. Checked
128
+ * attachments: only the pictures are posted, `title`, `tags` and `date` go on each, and the result is the first picture plus `attachments`. Checked
129
129
  * here, before anything is sent (ConfigError): a name given twice, an attachment that is not a picture, content that is not markdown, `alt` or
130
130
  * `name` with attachments. What a name may be is the server's rule (docs/using/pictures.md, "Posting a note with its pictures"): a name it refuses is an
131
131
  * InvalidRequestError, `attachment "<name>": ...`, and whenever the server refuses any part (its message names it) nothing is posted.
@@ -159,7 +159,7 @@ export class Client {
159
159
  "Content-Type": null, // fetch sets multipart/form-data with its boundary
160
160
  ...(own.title && { "X-Note-Title": headerValue(own.title) }),
161
161
  ...(own.tags?.length && { "X-Note-Tags": own.tags.join(",") }),
162
- ...(own.createdAt && { "X-Note-Created": createdHeader(own.createdAt) }),
162
+ ...(own.date && { "X-Note-Date": dateString(own.date) }),
163
163
  ...(own.readId && { "X-Read-Id": own.readId }),
164
164
  ...(own.licenseKey && { "X-License-Key": own.licenseKey }),
165
165
  };
@@ -176,7 +176,7 @@ export class Client {
176
176
  ...(options.tags?.length && { "X-Note-Tags": options.tags.join(",") }),
177
177
  ...(options.alt && { "X-Note-Alt": headerValue(options.alt) }),
178
178
  ...(options.name && { "X-Note-Name": headerValue(options.name) }),
179
- ...(options.createdAt && { "X-Note-Created": createdHeader(options.createdAt) }),
179
+ ...(options.date && { "X-Note-Date": dateString(options.date) }),
180
180
  ...(options.readId && { "X-Read-Id": options.readId }),
181
181
  ...(options.licenseKey && { "X-License-Key": options.licenseKey }),
182
182
  };
@@ -189,10 +189,12 @@ export class Client {
189
189
  const headers = { ...this.opts(options.feedPassword).headers, "Content-Type": type };
190
190
  return this.call(editNote({ client: this.api, path: { feed, id }, body, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
191
191
  }
192
- /** Set a note's title and/or alt text (alt for pictures); `""` removes one, so a markdown note's title follows its text again. Same options as post(). */
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(). */
193
193
  async update(id, meta, options = {}) {
194
194
  const feed = this.feedFor(options.feed);
195
- return this.call(patchNote({ client: this.api, path: { feed, id }, body: meta, ...this.opts(options.feedPassword) }));
195
+ const { date, ...rest } = meta;
196
+ const body = { ...rest, ...(date !== undefined && { date: dateString(date) }) };
197
+ return this.call(patchNote({ client: this.api, path: { feed, id }, body, ...this.opts(options.feedPassword) }));
196
198
  }
197
199
  /** Remove a note for good. The feed stays, even with no notes left. Same options as post(). */
198
200
  async delete(id, options = {}) {
@@ -210,6 +212,21 @@ export class Client {
210
212
  const body = { title: settings.title, description: settings.description, ...(settings.image === undefined ? {} : { image: settings.image }), ...(settings.read_id === undefined ? {} : { read_id: settings.read_id }) };
211
213
  return this.call(updateFeed({ client: this.api, path: { feed }, body, ...this.opts(options.feedPassword) }));
212
214
  }
215
+ /** Put the license the key names on the feed, taking it off the feed it was on; a feed needs no password for it. Only on an instance that sells licenses. Same options as post(). */
216
+ async attachLicense(key, options = {}) {
217
+ const feed = this.feedFor(options.feed);
218
+ return this.call(putFeedLicense({ client: this.api, path: { feed }, body: { key }, ...this.opts(options.feedPassword) }));
219
+ }
220
+ /** Take the feed's license off: it is a free feed again, and the key can go on another feed. Same options as post(). */
221
+ async detachLicense(options = {}) {
222
+ const feed = this.feedFor(options.feed);
223
+ return this.call(deleteFeedLicense({ client: this.api, path: { feed }, ...this.opts(options.feedPassword) }));
224
+ }
225
+ /** Set the feed's password. On a protected feed it changes it: `feedPassword` (or the client's) is the current one. An open feed takes one without, which claims it: its name was all that protected it. Every request for the feed then needs the new password; its read link stays open. Same options as post(). */
226
+ async setFeedPassword(password, options = {}) {
227
+ const feed = this.feedFor(options.feed);
228
+ await this.call(changeFeedPassword({ client: this.api, path: { feed }, body: { password }, ...this.opts(options.feedPassword) }));
229
+ }
213
230
  /** Delete the feed with all its notes, settings and password, for good; its name is free again. Same options as post(). */
214
231
  async deleteFeed(options = {}) {
215
232
  const feed = this.feedFor(options.feed);
@@ -1,2 +1,2 @@
1
- export { changeFeedPassword, deleteFeed, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, type Options, patchNote, postNote, removeFeedPassword, updateFeed } from './sdk.gen.js';
2
- export type { ChangeFeedPasswordData, ChangeFeedPasswordError, ChangeFeedPasswordErrors, ChangeFeedPasswordResponse, ChangeFeedPasswordResponses, ClientOptions, Created, DeleteFeedData, DeleteFeedError, DeleteFeedErrors, 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, 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, ReadFeed, RemoveFeedPasswordData, RemoveFeedPasswordError, RemoveFeedPasswordErrors, RemoveFeedPasswordResponse, RemoveFeedPasswordResponses, UpdateFeedData, UpdateFeedError, UpdateFeedErrors, UpdateFeedResponse, UpdateFeedResponses } from './types.gen.js';
1
+ export { changeFeedPassword, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, type Options, patchNote, postNote, putFeedLicense, removeFeedPassword, updateFeed } from './sdk.gen.js';
2
+ export type { ChangeFeedPasswordData, ChangeFeedPasswordError, ChangeFeedPasswordErrors, ChangeFeedPasswordResponse, ChangeFeedPasswordResponses, ClientOptions, Created, 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, RemoveFeedPasswordData, RemoveFeedPasswordError, RemoveFeedPasswordErrors, RemoveFeedPasswordResponse, RemoveFeedPasswordResponses, 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, deleteFeed, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, patchNote, postNote, removeFeedPassword, updateFeed } from './sdk.gen.js';
2
+ export { changeFeedPassword, deleteFeed, deleteFeedLicense, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, patchNote, postNote, putFeedLicense, removeFeedPassword, 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, DeleteFeedData, DeleteFeedErrors, 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, RemoveFeedPasswordData, RemoveFeedPasswordErrors, RemoveFeedPasswordResponses, UpdateFeedData, UpdateFeedErrors, UpdateFeedResponses } from './types.gen.js';
2
+ import type { ChangeFeedPasswordData, ChangeFeedPasswordErrors, ChangeFeedPasswordResponses, 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, RemoveFeedPasswordData, RemoveFeedPasswordErrors, RemoveFeedPasswordResponses, 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, default 5 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). It is stored byte for byte, with no resizing and no metadata stripped (EXIF such as GPS position stays in an image). 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, a feed password needs one: the post that creates the feed protected brings the license key in `X-License-Key`, or 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-Created` (when it was written, to post it in the past). 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)`. 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` and `X-Note-Created` on every note; 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, default 5 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). It is stored byte for byte, with no resizing and no metadata stripped (EXIF such as GPS position stays in an image). 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)`. 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
  /**
@@ -36,15 +36,15 @@ export declare const deleteNote: <ThrowOnError extends boolean = false>(options:
36
36
  */
37
37
  export declare const getNote: <ThrowOnError extends boolean = false>(options: Options<GetNoteData, ThrowOnError>) => RequestResult<GetNoteResponses, GetNoteErrors, ThrowOnError>;
38
38
  /**
39
- * Change a note's title or alt text
39
+ * Change a note's title, alt text, tags or date
40
40
  *
41
- * Sets the note's title and/or alt text (alt only for images). An empty string removes one: a markdown note's title follows its text again. At least one is needed. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't change notes.
41
+ * Sets the note's title, alt text (only for images), tags and/or date. An empty title or alt removes it: a markdown note's title follows its text again. `tags` replaces the whole list (an empty list removes them), with the same rules as on a post. A new date moves the note in the feed, with its attachments; a date after now schedules it. Id and links stay, and so does its RSS item's guid. At least one is needed. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't change notes.
42
42
  */
43
43
  export declare const patchNote: <ThrowOnError extends boolean = false>(options: Options<PatchNoteData, ThrowOnError>) => RequestResult<PatchNoteResponses, PatchNoteErrors, ThrowOnError>;
44
44
  /**
45
45
  * Replace a note's content
46
46
  *
47
- * The body is the new file, with the same rules as posting: a `Content-Type` that is one of the accepted types, a body that is what it declares. A note keeps its type, so the type must be the note's own (`415` otherwise). Id, creation time and metadata stay: the title of a markdown note without one set follows the new text. Change the title or alt text with `PATCH`. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't edit. 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)`. 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. On a `PUT` the `text` part is required and the note must be a markdown note; the note keeps its own title and tags, `X-Note-Tags` goes on the new pictures. A raw body ignores `X-Note-Tags`. The note is looked up first: a missing note is `404` and an image note `415` whatever the body holds.
47
+ * The body is the new file, with the same rules as posting: a `Content-Type` that is one of the accepted types, a body that is what it declares. A note keeps its type, so the type must be the note's own (`415` otherwise). Id, creation time and metadata stay: the title of a markdown note without one set follows the new text. Change the title, alt text or tags with `PATCH`. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't edit. 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)`. 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. On a `PUT` the `text` part is required and the note must be a markdown note; the note keeps its own title and tags, `X-Note-Tags` goes on the new pictures. A raw body ignores `X-Note-Tags`. The note is looked up first: a missing note is `404` and an image note `415` whatever the body holds.
48
48
  */
49
49
  export declare const editNote: <ThrowOnError extends boolean = false>(options: Options<EditNoteData, ThrowOnError>) => RequestResult<EditNoteResponses, EditNoteErrors, ThrowOnError>;
50
50
  /**
@@ -72,11 +72,23 @@ export declare const updateFeed: <ThrowOnError extends boolean = false>(options:
72
72
  */
73
73
  export declare const removeFeedPassword: <ThrowOnError extends boolean = false>(options: Options<RemoveFeedPasswordData, ThrowOnError>) => RequestResult<RemoveFeedPasswordResponses, RemoveFeedPasswordErrors, ThrowOnError>;
74
74
  /**
75
- * Change a feed's password
75
+ * Set or change a feed's password
76
76
  *
77
- * Needs the current password in `X-Feed-Password`. A feed can only get a password when it is created, so an open feed answers 409.
77
+ * On a protected feed: changes the password, with the current one in `X-Feed-Password`. On an open feed: sets one, with no current password, for whoever calls it. An open feed's name is all that protects it, so whoever knows the name may claim the feed this way. From then on everyone who posts to the feed, reads it by its name or changes it needs the password; the read link and RSS stay open.
78
78
  */
79
79
  export declare const changeFeedPassword: <ThrowOnError extends boolean = false>(options: Options<ChangeFeedPasswordData, ThrowOnError>) => RequestResult<ChangeFeedPasswordResponses, ChangeFeedPasswordErrors, ThrowOnError>;
80
+ /**
81
+ * Take a feed's license off
82
+ *
83
+ * Only on an instance that sells licenses (404 elsewhere). The feed is a free one again, and the key can be put on another feed; the subscription runs on. A feed without a license answers the same. Needs the feed's password if it has one, and counts against the post rate limit.
84
+ */
85
+ export declare const deleteFeedLicense: <ThrowOnError extends boolean = false>(options: Options<DeleteFeedLicenseData, ThrowOnError>) => RequestResult<DeleteFeedLicenseResponses, DeleteFeedLicenseErrors, ThrowOnError>;
86
+ /**
87
+ * Put a license on a feed
88
+ *
89
+ * Only on an instance that sells licenses (404 elsewhere). Puts the license the key names on the feed, taking it off the feed it was on; a feed that has an active license already answers 400 (take it off first). A feed needs no password for a license. A key notefeed doesn't know is 400 and counts as a failed attempt, like a wrong password. Needs the feed's password if it has one, and counts against the post rate limit. Only on a feed that exists: it is created by its first note, which can bring the key in `X-License-Key`.
90
+ */
91
+ export declare const putFeedLicense: <ThrowOnError extends boolean = false>(options: Options<PutFeedLicenseData, ThrowOnError>) => RequestResult<PutFeedLicenseResponses, PutFeedLicenseErrors, ThrowOnError>;
80
92
  /**
81
93
  * Get a feed's title and description by its read id
82
94
  *
@@ -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, default 5 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). It is stored byte for byte, with no resizing and no metadata stripped (EXIF such as GPS position stays in an image). 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, a feed password needs one: the post that creates the feed protected brings the license key in `X-License-Key`, or 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-Created` (when it was written, to post it in the past). 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)`. 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` and `X-Note-Created` on every note; 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, default 5 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). It is stored byte for byte, with no resizing and no metadata stripped (EXIF such as GPS position stays in an image). 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)`. 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,
@@ -44,9 +44,9 @@ export const getNote = (options) => (options.client ?? client).get({
44
44
  ...options
45
45
  });
46
46
  /**
47
- * Change a note's title or alt text
47
+ * Change a note's title, alt text, tags or date
48
48
  *
49
- * Sets the note's title and/or alt text (alt only for images). An empty string removes one: a markdown note's title follows its text again. At least one is needed. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't change notes.
49
+ * Sets the note's title, alt text (only for images), tags and/or date. An empty title or alt removes it: a markdown note's title follows its text again. `tags` replaces the whole list (an empty list removes them), with the same rules as on a post. A new date moves the note in the feed, with its attachments; a date after now schedules it. Id and links stay, and so does its RSS item's guid. At least one is needed. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't change notes.
50
50
  */
51
51
  export const patchNote = (options) => (options.client ?? client).patch({
52
52
  security: [{ scheme: 'bearer', type: 'http' }],
@@ -60,7 +60,7 @@ export const patchNote = (options) => (options.client ?? client).patch({
60
60
  /**
61
61
  * Replace a note's content
62
62
  *
63
- * The body is the new file, with the same rules as posting: a `Content-Type` that is one of the accepted types, a body that is what it declares. A note keeps its type, so the type must be the note's own (`415` otherwise). Id, creation time and metadata stay: the title of a markdown note without one set follows the new text. Change the title or alt text with `PATCH`. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't edit. 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)`. 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. On a `PUT` the `text` part is required and the note must be a markdown note; the note keeps its own title and tags, `X-Note-Tags` goes on the new pictures. A raw body ignores `X-Note-Tags`. The note is looked up first: a missing note is `404` and an image note `415` whatever the body holds.
63
+ * The body is the new file, with the same rules as posting: a `Content-Type` that is one of the accepted types, a body that is what it declares. A note keeps its type, so the type must be the note's own (`415` otherwise). Id, creation time and metadata stay: the title of a markdown note without one set follows the new text. Change the title, alt text or tags with `PATCH`. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't edit. 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)`. 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. On a `PUT` the `text` part is required and the note must be a markdown note; the note keeps its own title and tags, `X-Note-Tags` goes on the new pictures. A raw body ignores `X-Note-Tags`. The note is looked up first: a missing note is `404` and an image note `415` whatever the body holds.
64
64
  */
65
65
  export const editNote = (options) => (options.client ?? client).put({
66
66
  bodySerializer: null,
@@ -117,9 +117,9 @@ export const removeFeedPassword = (options) => (options.client ?? client).delete
117
117
  ...options
118
118
  });
119
119
  /**
120
- * Change a feed's password
120
+ * Set or change a feed's password
121
121
  *
122
- * Needs the current password in `X-Feed-Password`. A feed can only get a password when it is created, so an open feed answers 409.
122
+ * On a protected feed: changes the password, with the current one in `X-Feed-Password`. On an open feed: sets one, with no current password, for whoever calls it. An open feed's name is all that protects it, so whoever knows the name may claim the feed this way. From then on everyone who posts to the feed, reads it by its name or changes it needs the password; the read link and RSS stay open.
123
123
  */
124
124
  export const changeFeedPassword = (options) => (options.client ?? client).put({
125
125
  security: [{ scheme: 'bearer', type: 'http' }],
@@ -130,6 +130,30 @@ export const changeFeedPassword = (options) => (options.client ?? client).put({
130
130
  ...options.headers
131
131
  }
132
132
  });
133
+ /**
134
+ * Take a feed's license off
135
+ *
136
+ * Only on an instance that sells licenses (404 elsewhere). The feed is a free one again, and the key can be put on another feed; the subscription runs on. A feed without a license answers the same. Needs the feed's password if it has one, and counts against the post rate limit.
137
+ */
138
+ export const deleteFeedLicense = (options) => (options.client ?? client).delete({
139
+ security: [{ scheme: 'bearer', type: 'http' }],
140
+ url: '/api/v1/feeds/{feed}/license',
141
+ ...options
142
+ });
143
+ /**
144
+ * Put a license on a feed
145
+ *
146
+ * Only on an instance that sells licenses (404 elsewhere). Puts the license the key names on the feed, taking it off the feed it was on; a feed that has an active license already answers 400 (take it off first). A feed needs no password for a license. A key notefeed doesn't know is 400 and counts as a failed attempt, like a wrong password. Needs the feed's password if it has one, and counts against the post rate limit. Only on a feed that exists: it is created by its first note, which can bring the key in `X-License-Key`.
147
+ */
148
+ export const putFeedLicense = (options) => (options.client ?? client).put({
149
+ security: [{ scheme: 'bearer', type: 'http' }],
150
+ url: '/api/v1/feeds/{feed}/license',
151
+ ...options,
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ ...options.headers
155
+ }
156
+ });
133
157
  /**
134
158
  * Get a feed's title and description by its read id
135
159
  *
@@ -3,7 +3,7 @@ export type ClientOptions = {
3
3
  };
4
4
  export type Note = {
5
5
  /**
6
- * The note's id: a UTC time to the second plus a random UUID for notes made here; any name without a dot for a file placed by hand
6
+ * The note's id: 22 random characters (base64url) for notes made here, older notes may have another form; any name without a dot for a file placed by hand. It says nothing about the note's date
7
7
  */
8
8
  id: string;
9
9
  /**
@@ -39,9 +39,13 @@ export type Note = {
39
39
  */
40
40
  name?: string;
41
41
  /**
42
- * When the note was posted (UTC)
42
+ * Where the note is placed in the feed (UTC): when it was posted, unless a date was given or changed; lists are ordered by it
43
43
  */
44
- created_at: string;
44
+ date: string;
45
+ /**
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
+ */
48
+ scheduled?: true;
45
49
  /**
46
50
  * The note's page in the web UI
47
51
  */
@@ -124,7 +128,7 @@ export type Posted = {
124
128
  };
125
129
  export type NoteEdited = {
126
130
  /**
127
- * The note's id: a UTC time to the second plus a random UUID for notes made here; any name without a dot for a file placed by hand
131
+ * The note's id: 22 random characters (base64url) for notes made here, older notes may have another form; any name without a dot for a file placed by hand. It says nothing about the note's date
128
132
  */
129
133
  id: string;
130
134
  /**
@@ -160,9 +164,13 @@ export type NoteEdited = {
160
164
  */
161
165
  name?: string;
162
166
  /**
163
- * When the note was posted (UTC)
167
+ * Where the note is placed in the feed (UTC): when it was posted, unless a date was given or changed; lists are ordered by it
168
+ */
169
+ date: string;
170
+ /**
171
+ * 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
164
172
  */
165
- created_at: string;
173
+ scheduled?: true;
166
174
  /**
167
175
  * The note's page in the web UI
168
176
  */
@@ -213,6 +221,14 @@ export type NoteMeta = {
213
221
  * Alternative text of an image note, at most 500 characters, one line (control and text-direction override characters are refused)
214
222
  */
215
223
  alt?: string;
224
+ /**
225
+ * The note's tags, the whole new list (`["ci","done"]`): at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed. Empty removes them all; omitted leaves them as they are.
226
+ */
227
+ tags?: Array<string>;
228
+ /**
229
+ * 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
+ */
231
+ date?: string;
216
232
  };
217
233
  export type PasswordJson = {
218
234
  /**
@@ -220,6 +236,12 @@ export type PasswordJson = {
220
236
  */
221
237
  password: string;
222
238
  };
239
+ export type LicenseKey = {
240
+ /**
241
+ * The license key, as the checkout or the receipt gave it
242
+ */
243
+ key: string;
244
+ };
223
245
  export type FeedSettings = {
224
246
  /**
225
247
  * Display title, at most 100 characters, one line (control and text-direction override characters are refused); empty means none (the feed's name is shown)
@@ -371,7 +393,7 @@ export type PostNoteData = {
371
393
  */
372
394
  'X-Read-Id'?: string;
373
395
  /**
374
- * Only on an instance that sells licenses, with `X-Feed-Password` on the post that creates the feed: the key of the license to put on the new feed, since a feed password is for licensed feeds (402 without one). Ignored on an existing feed and without a password; 400 for a key that is none
396
+ * Only on an instance that sells licenses, on the post that creates the feed: the key of the license to put on the new feed, with or without `X-Feed-Password` (a feed password is for licensed feeds: with one and no key, 402). 400 for a key that is none, 402 for one whose license has lapsed; either way nothing is created. Ignored on an existing feed: put a license on it with `PUT /api/v1/feeds/{feed}/license`
375
397
  */
376
398
  'X-License-Key'?: string;
377
399
  /**
@@ -387,13 +409,13 @@ export type PostNoteData = {
387
409
  */
388
410
  'X-Note-Alt'?: string;
389
411
  /**
390
- * For an image body: the picture's original file name, kept with the note (at most 200 characters)
412
+ * The note's file name (at most 200 characters; `/`, `\` and control characters are dropped): for a picture, its original name. The read link serves the newest note with that name at `/r/{readId}/{name}`, a stable address for a file that changes (a name ending in a lower-case extension, such as `post-to-notefeed.md`); a note's own file name (`<id>.<ext>`) comes first
391
413
  */
392
414
  'X-Note-Name'?: string;
393
415
  /**
394
- * When the note was written, if not now: 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). Not in the future, not before 1970. The note takes its place in the feed by this time, as if posted then. Empty is now.
416
+ * 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.
395
417
  */
396
- 'X-Note-Created'?: string;
418
+ 'X-Note-Date'?: string;
397
419
  };
398
420
  path: {
399
421
  /**
@@ -406,7 +428,7 @@ export type PostNoteData = {
406
428
  };
407
429
  export type PostNoteErrors = {
408
430
  /**
409
- * Invalid or reserved feed name; a blank note; a bad title, alt text, tags or `X-Note-Created`; 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 or repeated file name, a text sent with pictures that takes too long to read (`NOTEFEED_PARSE_TIMEOUT_MS`), or `X-Note-Alt` / `X-Note-Name`
431
+ * 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 or repeated file name, a text sent with pictures that takes too long to read (`NOTEFEED_PARSE_TIMEOUT_MS`), or `X-Note-Alt` / `X-Note-Name`
410
432
  */
411
433
  400: Error;
412
434
  /**
@@ -587,7 +609,7 @@ export type PatchNoteData = {
587
609
  };
588
610
  export type PatchNoteErrors = {
589
611
  /**
590
- * Invalid or reserved feed name; nothing to change; a bad title or alt text; alt for a note that has none; bad JSON
612
+ * 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
591
613
  */
592
614
  400: Error;
593
615
  /**
@@ -924,9 +946,9 @@ export type ChangeFeedPasswordErrors = {
924
946
  */
925
947
  402: Error;
926
948
  /**
927
- * The feed has no password
949
+ * No such feed
928
950
  */
929
- 409: Error;
951
+ 404: Error;
930
952
  /**
931
953
  * Too many wrong passwords from this client
932
954
  */
@@ -939,11 +961,109 @@ export type ChangeFeedPasswordErrors = {
939
961
  export type ChangeFeedPasswordError = ChangeFeedPasswordErrors[keyof ChangeFeedPasswordErrors];
940
962
  export type ChangeFeedPasswordResponses = {
941
963
  /**
942
- * Changed; the old password and unlock cookies stop working
964
+ * Set or changed; the old password and unlock cookies stop working
943
965
  */
944
966
  204: void;
945
967
  };
946
968
  export type ChangeFeedPasswordResponse = ChangeFeedPasswordResponses[keyof ChangeFeedPasswordResponses];
969
+ export type DeleteFeedLicenseData = {
970
+ body?: never;
971
+ headers?: {
972
+ /**
973
+ * The feed's own password, when it has one: to post, list or get, and (as the current password) to change or remove it. On the `POST` that creates a feed it sets the feed's password; on a `POST` to an existing feed that has none it answers 409. An empty value is the same as no header, so a `POST` with an empty one creates an open feed.
974
+ */
975
+ 'X-Feed-Password'?: string;
976
+ };
977
+ path: {
978
+ /**
979
+ * The feed's name. It is the write key: anyone who knows it can post.
980
+ */
981
+ feed: string;
982
+ };
983
+ query?: never;
984
+ url: '/api/v1/feeds/{feed}/license';
985
+ };
986
+ export type DeleteFeedLicenseErrors = {
987
+ /**
988
+ * Invalid or reserved feed name
989
+ */
990
+ 400: Error;
991
+ /**
992
+ * The instance has a password, or the feed has its own, and it is missing or wrong
993
+ */
994
+ 401: Error;
995
+ /**
996
+ * No such feed, or licenses are not sold on this instance
997
+ */
998
+ 404: Error;
999
+ /**
1000
+ * Too many posts, edits and deletes, or wrong passwords, from this client
1001
+ */
1002
+ 429: Error;
1003
+ /**
1004
+ * The key service is unreachable: the instance is down for maintenance; try again after Retry-After
1005
+ */
1006
+ 503: Error;
1007
+ };
1008
+ export type DeleteFeedLicenseError = DeleteFeedLicenseErrors[keyof DeleteFeedLicenseErrors];
1009
+ export type DeleteFeedLicenseResponses = {
1010
+ /**
1011
+ * The feed as it is now
1012
+ */
1013
+ 200: Feed;
1014
+ };
1015
+ export type DeleteFeedLicenseResponse = DeleteFeedLicenseResponses[keyof DeleteFeedLicenseResponses];
1016
+ export type PutFeedLicenseData = {
1017
+ body: LicenseKey;
1018
+ headers?: {
1019
+ /**
1020
+ * The feed's own password, when it has one: to post, list or get, and (as the current password) to change or remove it. On the `POST` that creates a feed it sets the feed's password; on a `POST` to an existing feed that has none it answers 409. An empty value is the same as no header, so a `POST` with an empty one creates an open feed.
1021
+ */
1022
+ 'X-Feed-Password'?: string;
1023
+ };
1024
+ path: {
1025
+ /**
1026
+ * The feed's name. It is the write key: anyone who knows it can post.
1027
+ */
1028
+ feed: string;
1029
+ };
1030
+ query?: never;
1031
+ url: '/api/v1/feeds/{feed}/license';
1032
+ };
1033
+ export type PutFeedLicenseErrors = {
1034
+ /**
1035
+ * Invalid or reserved feed name, bad JSON, a key that is none, or a feed with an active license already
1036
+ */
1037
+ 400: Error;
1038
+ /**
1039
+ * The instance has a password, or the feed has its own, and it is missing or wrong
1040
+ */
1041
+ 401: Error;
1042
+ /**
1043
+ * The key's license has lapsed (`license_required`)
1044
+ */
1045
+ 402: Error;
1046
+ /**
1047
+ * No such feed, or licenses are not sold on this instance
1048
+ */
1049
+ 404: Error;
1050
+ /**
1051
+ * Too many posts, edits and deletes, or wrong passwords and keys, from this client
1052
+ */
1053
+ 429: Error;
1054
+ /**
1055
+ * The key service is unreachable: the instance is down for maintenance; try again after Retry-After
1056
+ */
1057
+ 503: Error;
1058
+ };
1059
+ export type PutFeedLicenseError = PutFeedLicenseErrors[keyof PutFeedLicenseErrors];
1060
+ export type PutFeedLicenseResponses = {
1061
+ /**
1062
+ * The feed as it is now: `free_tier` is null
1063
+ */
1064
+ 200: Feed;
1065
+ };
1066
+ export type PutFeedLicenseResponse = PutFeedLicenseResponses[keyof PutFeedLicenseResponses];
947
1067
  export type GetReadFeedData = {
948
1068
  body?: never;
949
1069
  path: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notefeed",
3
- "version": "0.27.0",
3
+ "version": "0.29.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",