notefeed 0.6.0 → 0.8.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 +12 -0
- package/dist/cli.js +71 -15
- package/dist/client.d.ts +85 -8
- package/dist/client.js +167 -14
- package/dist/generated/index.d.ts +2 -2
- package/dist/generated/index.js +1 -1
- package/dist/generated/sdk.gen.d.ts +56 -2
- package/dist/generated/sdk.gen.js +106 -2
- package/dist/generated/types.gen.d.ts +590 -21
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +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
13
|
```
|
|
12
14
|
|
|
13
15
|
```js
|
|
@@ -19,6 +21,16 @@ console.log(created.url, created.read_url);
|
|
|
19
21
|
for await (const note of client.notes()) console.log(note.created_at, note.title); // newest first, page by page
|
|
20
22
|
```
|
|
21
23
|
|
|
24
|
+
`read_url` is `null` for a feed without a read link (rare; see the operations docs).
|
|
25
|
+
|
|
22
26
|
`Client.fromEnv()` reads `NOTEFEED_URL` / `NOTEFEED_FEED` / `NOTEFEED_PASSWORD`; so does the `notefeed` command, unless given `--url` / `--feed` / `--password`.
|
|
23
27
|
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
A posted note can be changed or removed: `await client.edit(id, content)` replaces its content and returns the note (its id and URLs stay), and `await client.delete(id)` removes it. Both take the same `{ feed, feedPassword }` options as `post`, and a missing note is a `NotFoundError`. `notefeed edit <id> <text | - | --file PATH>` prints the note URL; `notefeed delete <id>` prints nothing. Anyone who can post to a feed can edit and delete its notes, and deleting is permanent.
|
|
31
|
+
|
|
32
|
+
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.
|
|
33
|
+
|
|
34
|
+
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 `` in a markdown note to show it; the feed is created by its first note, and a picture that is too large is a `NoteTooLargeError`. 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 first and swaps `` for each file name (`notefeed post "…" --attach chart.png` does the same); a failure carries the pictures already posted as `posted`. Pictures are public to anyone with the feed's read link, and metadata such as EXIF is not removed.
|
|
35
|
+
|
|
24
36
|
Full documentation: https://docs.notefeed.me/clients/
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
/** `notefeed post
|
|
1
|
+
/** `notefeed post`, `edit`, `update`, `delete` and `notes`: post, change, remove and read notes, markdown or any accepted file, from the command line. */
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename, extname } from "node:path";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
5
6
|
import { Client, ConfigError, NotefeedError } from "./client.js";
|
|
6
7
|
class UsageError extends Error {
|
|
7
8
|
}
|
|
8
9
|
const USAGE = [
|
|
9
|
-
"usage: notefeed post <text | - | --file PATH> [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
10
|
-
" notefeed
|
|
10
|
+
"usage: notefeed post <text | - | --file PATH> [--type MEDIA_TYPE] [--title TITLE] [--tag TAG]... [--attach PATH]... [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
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]",
|
|
13
|
+
" notefeed delete <id> [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
14
|
+
" notefeed notes [--limit N] [--tag TAG] [--json] [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
11
15
|
].join("\n");
|
|
16
|
+
// The media type of a file by its extension: the types the server accepts.
|
|
17
|
+
const TYPES = { ".md": "text/markdown", ".markdown": "text/markdown", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
12
18
|
/** Returns the exit code: 0 ok, 1 server/network error, 2 usage/config error. */
|
|
13
19
|
export async function main(argv, io = process) {
|
|
14
20
|
try {
|
|
@@ -20,6 +26,11 @@ export async function main(argv, io = process) {
|
|
|
20
26
|
feed: { type: "string" },
|
|
21
27
|
password: { type: "string" },
|
|
22
28
|
file: { type: "string" },
|
|
29
|
+
type: { type: "string" },
|
|
30
|
+
title: { type: "string" },
|
|
31
|
+
alt: { type: "string" },
|
|
32
|
+
tag: { type: "string", multiple: true },
|
|
33
|
+
attach: { type: "string", multiple: true },
|
|
23
34
|
limit: { type: "string" },
|
|
24
35
|
json: { type: "boolean" },
|
|
25
36
|
version: { type: "boolean" },
|
|
@@ -37,21 +48,39 @@ export async function main(argv, io = process) {
|
|
|
37
48
|
}
|
|
38
49
|
const [command, text, ...rest] = positionals;
|
|
39
50
|
if (command === "post" && !rest.length) {
|
|
40
|
-
const
|
|
41
|
-
const
|
|
51
|
+
const input = await read(text, values.file, values.type, io);
|
|
52
|
+
const attachments = await Promise.all((values.attach ?? []).map(attachment));
|
|
53
|
+
const note = await client(values).post(input.content, { type: input.type, title: values.title, tags: values.tag, name: input.name, attachments });
|
|
54
|
+
io.stdout.write(`${[note.url, ...note.attachments.map((a) => a.url)].join("\n")}\n`);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
if (command === "edit" && text !== undefined && rest.length <= 1) {
|
|
58
|
+
const input = await read(rest[0], values.file, values.type, io);
|
|
59
|
+
const note = await client(values).edit(text, input.content, { type: input.type });
|
|
42
60
|
io.stdout.write(`${note.url}\n`);
|
|
43
61
|
return 0;
|
|
44
62
|
}
|
|
63
|
+
if (command === "update" && text !== undefined && !rest.length) {
|
|
64
|
+
if (values.title === undefined && values.alt === undefined)
|
|
65
|
+
throw new UsageError("give --title and/or --alt");
|
|
66
|
+
const note = await client(values).update(text, { title: values.title, alt: values.alt });
|
|
67
|
+
io.stdout.write(`${note.url}\n`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
if (command === "delete" && text !== undefined && !rest.length) {
|
|
71
|
+
await client(values).delete(text);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
45
74
|
if (command === "notes" && text === undefined) {
|
|
46
75
|
const limit = Number(values.limit ?? 20);
|
|
47
76
|
if (!Number.isInteger(limit) || limit < 1)
|
|
48
77
|
throw new UsageError("--limit must be a whole number, 1 or more");
|
|
49
78
|
let left = limit;
|
|
50
|
-
for await (const n of client(values).notes({ pageSize: Math.min(limit, 100) })) {
|
|
79
|
+
for await (const n of client(values).notes({ pageSize: Math.min(limit, 100), tag: values.tag?.[0] })) {
|
|
51
80
|
// To the second, UTC: the same form as the Python CLI, so scripts read either the same way.
|
|
52
81
|
const when = new Date(n.created_at).toISOString().replace(/\.\d+Z$/, "Z");
|
|
53
82
|
// --json: exactly the documented fields, like the Python CLI, even if the server adds more.
|
|
54
|
-
const fields = { id: n.id, title: n.title,
|
|
83
|
+
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 };
|
|
55
84
|
io.stdout.write(values.json ? `${JSON.stringify(fields)}\n` : `${when} ${n.title || n.id} ${n.url}\n`);
|
|
56
85
|
if (--left === 0)
|
|
57
86
|
break;
|
|
@@ -62,6 +91,9 @@ export async function main(argv, io = process) {
|
|
|
62
91
|
}
|
|
63
92
|
catch (e) {
|
|
64
93
|
io.stderr.write(`notefeed: ${e.message.split("\n")[0]}\n`);
|
|
94
|
+
if (e instanceof NotefeedError)
|
|
95
|
+
for (const p of e.posted)
|
|
96
|
+
io.stderr.write(`posted: ${p.url}\n`);
|
|
65
97
|
if (e instanceof ConfigError || e instanceof UsageError)
|
|
66
98
|
return 2;
|
|
67
99
|
if (e instanceof NotefeedError)
|
|
@@ -69,7 +101,7 @@ export async function main(argv, io = process) {
|
|
|
69
101
|
return 2; // parseArgs rejects unknown options with a TypeError
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
|
-
// Flags win over NOTEFEED_URL, NOTEFEED_FEED and NOTEFEED_PASSWORD.
|
|
104
|
+
// Flags win over NOTEFEED_URL, NOTEFEED_FEED and NOTEFEED_PASSWORD. NOTEFEED_FEED_PASSWORD has no flag: it would end up in shell history.
|
|
73
105
|
function client(values) {
|
|
74
106
|
const url = values.url || process.env.NOTEFEED_URL;
|
|
75
107
|
const feed = values.feed || process.env.NOTEFEED_FEED;
|
|
@@ -77,28 +109,52 @@ function client(values) {
|
|
|
77
109
|
throw new UsageError("no URL given; pass --url or set NOTEFEED_URL");
|
|
78
110
|
if (!feed)
|
|
79
111
|
throw new UsageError("no feed given; pass --feed or set NOTEFEED_FEED");
|
|
80
|
-
return new Client({
|
|
112
|
+
return new Client({
|
|
113
|
+
url,
|
|
114
|
+
feed,
|
|
115
|
+
password: values.password || process.env.NOTEFEED_PASSWORD,
|
|
116
|
+
feedPassword: process.env.NOTEFEED_FEED_PASSWORD,
|
|
117
|
+
});
|
|
81
118
|
}
|
|
82
|
-
|
|
119
|
+
// What a post or an edit sends: text on the command line or stdin is markdown; a file is sent as it is, as `--type` or its extension says.
|
|
120
|
+
async function read(text, file, type, io) {
|
|
83
121
|
if (file) {
|
|
122
|
+
const media = type ?? TYPES[extname(file).toLowerCase()];
|
|
123
|
+
if (!media)
|
|
124
|
+
throw new UsageError(`cannot tell the type of ${file}: pass --type (${Object.values(TYPES).filter((t, i, a) => a.indexOf(t) === i).join(", ")})`);
|
|
125
|
+
let bytes;
|
|
84
126
|
try {
|
|
85
|
-
|
|
127
|
+
bytes = await readFile(file);
|
|
86
128
|
}
|
|
87
129
|
catch (e) {
|
|
88
|
-
if (e instanceof UsageError)
|
|
89
|
-
throw e;
|
|
90
130
|
throw new UsageError(`cannot read ${file}: ${e.code ?? e.message}`);
|
|
91
131
|
}
|
|
132
|
+
return { content: media === "text/markdown" ? utf8(bytes, file) : bytes, type: media, name: basename(file) };
|
|
92
133
|
}
|
|
93
134
|
if (text === "-") {
|
|
94
135
|
const chunks = [];
|
|
95
136
|
for await (const c of io.stdin)
|
|
96
137
|
chunks.push(typeof c === "string" ? Buffer.from(c) : c);
|
|
97
|
-
return utf8(Buffer.concat(chunks), "stdin");
|
|
138
|
+
return { content: utf8(Buffer.concat(chunks), "stdin"), type: type ?? "text/markdown" };
|
|
98
139
|
}
|
|
99
140
|
if (text === undefined)
|
|
100
141
|
throw new UsageError('give the note text, "-" for stdin, or --file PATH');
|
|
101
|
-
return text;
|
|
142
|
+
return { content: text, type: type ?? "text/markdown" };
|
|
143
|
+
}
|
|
144
|
+
// A picture to post first: its name is the file's, its type comes from the extension.
|
|
145
|
+
async function attachment(path) {
|
|
146
|
+
const type = TYPES[extname(path).toLowerCase()];
|
|
147
|
+
if (!type || type === "text/markdown")
|
|
148
|
+
throw new UsageError(`cannot attach ${path}: a picture (${Object.keys(TYPES).filter((e) => TYPES[e] !== "text/markdown").join(", ")}) is expected`);
|
|
149
|
+
const name = basename(path);
|
|
150
|
+
if (!/^(?!\.+$)[A-Za-z0-9._-]+$/.test(name))
|
|
151
|
+
throw new UsageError(`cannot attach ${path}: its file name "${name}" is what the text refers to it by, so it may only have letters, digits, ., _ and -: rename the file`);
|
|
152
|
+
try {
|
|
153
|
+
return { name, content: await readFile(path), type };
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
throw new UsageError(`cannot read ${path}: ${e.code ?? e.message}`);
|
|
157
|
+
}
|
|
102
158
|
}
|
|
103
159
|
function utf8(bytes, source) {
|
|
104
160
|
try {
|
package/dist/client.d.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import type { Created, Error as ApiError, Note } from "./generated/types.gen.js";
|
|
1
|
+
import type { Created, Error as ApiError, Feed, FeedSettings, 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, Note };
|
|
4
|
+
export type { Created, Feed, FeedSettings, Note };
|
|
5
|
+
/** A picture posted with a note: `` in the markdown refers to it. `type` as for post(). */
|
|
6
|
+
export type Attachment = {
|
|
7
|
+
name: string;
|
|
8
|
+
content: Uint8Array | Blob;
|
|
9
|
+
type?: string;
|
|
10
|
+
alt?: string;
|
|
11
|
+
};
|
|
5
12
|
/** `timeoutMs` (default 10000) limits each whole request, including reading the answer. */
|
|
6
13
|
export type ClientOptions = {
|
|
7
14
|
url: string;
|
|
8
15
|
feed?: string;
|
|
9
16
|
password?: string;
|
|
17
|
+
feedPassword?: string;
|
|
10
18
|
timeoutMs?: number;
|
|
11
19
|
};
|
|
12
20
|
/** Any failure talking to notefeed. `status` and `code` are null when there was no API answer. */
|
|
@@ -14,6 +22,9 @@ export declare class NotefeedError extends Error {
|
|
|
14
22
|
readonly status: number | null;
|
|
15
23
|
readonly code: ErrorCode | null;
|
|
16
24
|
constructor(message: string, status?: number | null, code?: ErrorCode | null);
|
|
25
|
+
/** Set when a post with `attachments` failed on one of them: its name, and the images already posted (the text note is not). */
|
|
26
|
+
attachment: string | null;
|
|
27
|
+
posted: Created[];
|
|
17
28
|
}
|
|
18
29
|
/** Raised before sending: no URL, no feed, an invalid feed name or password. */
|
|
19
30
|
export declare class ConfigError extends NotefeedError {
|
|
@@ -29,10 +40,10 @@ export declare class RateLimitedError extends NotefeedError {
|
|
|
29
40
|
/** No such note, or a malformed read id. */
|
|
30
41
|
export declare class NotFoundError extends NotefeedError {
|
|
31
42
|
}
|
|
32
|
-
/** The instance's feed or note limit is reached. */
|
|
43
|
+
/** The instance's feed or note limit, or the feed's image limit, is reached. */
|
|
33
44
|
export declare class LimitReachedError extends NotefeedError {
|
|
34
45
|
}
|
|
35
|
-
/** The note is over the server's size limit. */
|
|
46
|
+
/** The note or image is over the server's size limit. */
|
|
36
47
|
export declare class NoteTooLargeError extends NotefeedError {
|
|
37
48
|
}
|
|
38
49
|
/** The server refused the request itself: invalid or reserved feed, empty note, bad body or parameter. */
|
|
@@ -43,24 +54,90 @@ export declare class Client {
|
|
|
43
54
|
readonly feed: string | null;
|
|
44
55
|
private readonly api;
|
|
45
56
|
private readonly timeoutMs;
|
|
57
|
+
private readonly feedPassword;
|
|
46
58
|
/** A notefeed server. The feed set here is the default for every call; each call can override it. */
|
|
47
59
|
constructor(options: ClientOptions);
|
|
48
|
-
/** NOTEFEED_URL, NOTEFEED_FEED and
|
|
60
|
+
/** NOTEFEED_URL, NOTEFEED_FEED, NOTEFEED_PASSWORD and NOTEFEED_FEED_PASSWORD. */
|
|
49
61
|
static fromEnv(env?: Record<string, string | undefined>): Client;
|
|
50
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Post a note: a string is markdown, bytes or a Blob are a file whose media type is `type` (a Blob's own `type` if left out): `text/markdown`,
|
|
64
|
+
* `image/png`, `image/jpeg`, `image/gif` or `image/webp`. The server accepts nothing else and checks that the body is what the type says.
|
|
65
|
+
* The feed is created by its first note. `Created.file` is the note's file name: write `` in a markdown note to show a picture, or
|
|
66
|
+
* pass it as `image` to updateFeed; `file_url` is for a link outside notefeed.
|
|
67
|
+
*
|
|
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
|
+
* 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. `readId` is the read id the feed gets when this post creates it
|
|
71
|
+
* (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).
|
|
72
|
+
*
|
|
73
|
+
* `attachments` (only with a markdown string) are pictures posted first, one note each, in order: `` in the markdown shows one (an
|
|
74
|
+
* attachment the text never refers to is added at the end), and `Created.attachments` lists them. `tags` label the images too. The options
|
|
75
|
+
* are checked before anything is sent. If an upload fails, its error is thrown with `attachment` (the name) and `posted` (the images already
|
|
76
|
+
* posted, which stay) set; the text note is not posted.
|
|
77
|
+
*/
|
|
78
|
+
post(content: string | Uint8Array | Blob, options?: {
|
|
79
|
+
feed?: string;
|
|
80
|
+
feedPassword?: string;
|
|
81
|
+
type?: string;
|
|
82
|
+
title?: string;
|
|
83
|
+
tags?: string[];
|
|
84
|
+
alt?: string;
|
|
85
|
+
name?: string;
|
|
86
|
+
readId?: string;
|
|
87
|
+
attachments?: Attachment[];
|
|
88
|
+
}): Promise<Created & {
|
|
89
|
+
attachments: Created[];
|
|
90
|
+
}>;
|
|
91
|
+
private postOne;
|
|
92
|
+
/** Replace a note's content (a string is markdown; bytes and Blobs need the note's own `type`, as for post()). Its id, URLs and metadata stay. Same options as post(). */
|
|
93
|
+
edit(id: string, content: string | Uint8Array | Blob, options?: {
|
|
94
|
+
feed?: string;
|
|
95
|
+
feedPassword?: string;
|
|
96
|
+
type?: string;
|
|
97
|
+
}): Promise<Note>;
|
|
98
|
+
/** 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(). */
|
|
99
|
+
update(id: string, meta: {
|
|
100
|
+
title?: string;
|
|
101
|
+
alt?: string;
|
|
102
|
+
}, options?: {
|
|
103
|
+
feed?: string;
|
|
104
|
+
feedPassword?: string;
|
|
105
|
+
}): Promise<Note>;
|
|
106
|
+
/** Remove a note for good. The feed stays, even with no notes left. Same options as post(). */
|
|
107
|
+
delete(id: string, options?: {
|
|
108
|
+
feed?: string;
|
|
109
|
+
feedPassword?: string;
|
|
110
|
+
}): Promise<void>;
|
|
111
|
+
/** The feed's name, title, description, whether it has a password, and its read link (null while it has no notes). Same options as post(). */
|
|
112
|
+
feedInfo(options?: {
|
|
113
|
+
feed?: string;
|
|
114
|
+
feedPassword?: string;
|
|
115
|
+
}): Promise<Feed>;
|
|
116
|
+
/** Replace the feed's title and description (both; an empty string clears one), and set (`image`: a file name from uploadImage) or clear (`""`) the title image; leave `image` out to keep it. `read_id` gives the feed another read id (3 to 64 characters of `a-z`, `0-9`, `-`, `_`; `""` for a random one; left out keeps it); the old one is freed, and relative image links in notes follow while full URLs do not. The feed must already exist. Same options as post(). */
|
|
117
|
+
updateFeed(settings: FeedSettings, options?: {
|
|
118
|
+
feed?: string;
|
|
119
|
+
feedPassword?: string;
|
|
120
|
+
}): Promise<Feed>;
|
|
121
|
+
/** Delete the feed with all its notes, settings and password, for good; its name is free again. Same options as post(). */
|
|
122
|
+
deleteFeed(options?: {
|
|
51
123
|
feed?: string;
|
|
52
|
-
|
|
53
|
-
|
|
124
|
+
feedPassword?: string;
|
|
125
|
+
}): Promise<void>;
|
|
126
|
+
/** Every note in the feed, newest first, fetched a page at a time; stop iterating whenever you like. `tag`: only notes carrying it. */
|
|
54
127
|
notes(options?: {
|
|
55
128
|
feed?: string;
|
|
129
|
+
feedPassword?: string;
|
|
56
130
|
pageSize?: number;
|
|
131
|
+
tag?: string;
|
|
57
132
|
}): AsyncGenerator<Note>;
|
|
58
133
|
note(id: string, options?: {
|
|
59
134
|
feed?: string;
|
|
135
|
+
feedPassword?: string;
|
|
60
136
|
}): Promise<Note>;
|
|
61
137
|
/** Like notes(), by the feed's read id: public, read-only, needs no password. */
|
|
62
138
|
readNotes(readId: string, options?: {
|
|
63
139
|
pageSize?: number;
|
|
140
|
+
tag?: string;
|
|
64
141
|
}): AsyncGenerator<Note>;
|
|
65
142
|
readNote(readId: string, id: string): Promise<Note>;
|
|
66
143
|
private feedFor;
|
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 { getNote, getReadNote, listNotes, listReadNotes, postNote } from "./generated/sdk.gen.js";
|
|
6
|
+
import { deleteFeed, deleteNote, editNote, getFeed, getNote, getReadNote, listNotes, listReadNotes, patchNote, postNote, 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;
|
|
@@ -14,6 +14,9 @@ export class NotefeedError extends Error {
|
|
|
14
14
|
this.code = code;
|
|
15
15
|
this.name = new.target.name;
|
|
16
16
|
}
|
|
17
|
+
/** Set when a post with `attachments` failed on one of them: its name, and the images already posted (the text note is not). */
|
|
18
|
+
attachment = null;
|
|
19
|
+
posted = [];
|
|
17
20
|
}
|
|
18
21
|
/** Raised before sending: no URL, no feed, an invalid feed name or password. */
|
|
19
22
|
export class ConfigError extends NotefeedError {
|
|
@@ -32,10 +35,10 @@ export class RateLimitedError extends NotefeedError {
|
|
|
32
35
|
/** No such note, or a malformed read id. */
|
|
33
36
|
export class NotFoundError extends NotefeedError {
|
|
34
37
|
}
|
|
35
|
-
/** The instance's feed or note limit is reached. */
|
|
38
|
+
/** The instance's feed or note limit, or the feed's image limit, is reached. */
|
|
36
39
|
export class LimitReachedError extends NotefeedError {
|
|
37
40
|
}
|
|
38
|
-
/** The note is over the server's size limit. */
|
|
41
|
+
/** The note or image is over the server's size limit. */
|
|
39
42
|
export class NoteTooLargeError extends NotefeedError {
|
|
40
43
|
}
|
|
41
44
|
/** The server refused the request itself: invalid or reserved feed, empty note, bad body or parameter. */
|
|
@@ -46,23 +49,53 @@ const BY_CODE = {
|
|
|
46
49
|
not_found: NotFoundError,
|
|
47
50
|
feed_limit: LimitReachedError,
|
|
48
51
|
note_limit: LimitReachedError,
|
|
52
|
+
image_limit: LimitReachedError,
|
|
49
53
|
too_large: NoteTooLargeError,
|
|
50
54
|
invalid_feed: InvalidRequestError,
|
|
51
55
|
reserved_feed: InvalidRequestError,
|
|
52
56
|
empty_note: InvalidRequestError,
|
|
53
57
|
invalid_body: InvalidRequestError,
|
|
54
58
|
invalid_request: InvalidRequestError,
|
|
59
|
+
feed_exists: InvalidRequestError,
|
|
60
|
+
taken: InvalidRequestError,
|
|
55
61
|
unsupported_type: InvalidRequestError,
|
|
56
62
|
};
|
|
63
|
+
const ATTACHMENT_NAME = /^(?!\.+$)[A-Za-z0-9._-]+$/; // same rule as the server's MCP tool: safe to write in 
|
|
64
|
+
const IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
57
65
|
const FEED_RE = /^[a-z0-9_-]{1,64}$/; // same rule as the server; reserved names still come back as a 400
|
|
66
|
+
// 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
|
|
67
|
+
// latin1 characters, which is what the server reads back.
|
|
68
|
+
const headerValue = (text) => Array.from(new TextEncoder().encode(text), (b) => String.fromCharCode(b)).join("");
|
|
69
|
+
// What a post sends: a string is markdown; bytes and Blobs need a media type (a Blob's own, if it has one).
|
|
70
|
+
function asFile(content, type) {
|
|
71
|
+
if (typeof content === "string")
|
|
72
|
+
return { body: new Blob([content]), type: type ?? "text/markdown" };
|
|
73
|
+
const media = type ?? (content instanceof Blob ? content.type : "");
|
|
74
|
+
if (!media)
|
|
75
|
+
throw new ConfigError("give the file's media type as `type`, e.g. image/png");
|
|
76
|
+
return { body: content instanceof Blob ? content : new Blob([content]), type: media };
|
|
77
|
+
}
|
|
78
|
+
// The markdown with each `` of `sent` (name → file) pointing at its file; a name the text never refers to is appended as ``.
|
|
79
|
+
function placeImages(markdown, sent) {
|
|
80
|
+
const used = new Set();
|
|
81
|
+
const text = markdown.replace(/(!\[[^\]]*\]\()([^)\s]+)(\))/g, (m, head, dest, tail) => {
|
|
82
|
+
if (!sent.has(dest))
|
|
83
|
+
return m;
|
|
84
|
+
used.add(dest);
|
|
85
|
+
return head + sent.get(dest) + tail;
|
|
86
|
+
});
|
|
87
|
+
const rest = [...sent].filter(([name]) => !used.has(name)).map(([, file]) => ``);
|
|
88
|
+
return rest.length === 0 ? text : [text.trimEnd(), ...rest].filter((p) => p !== "").join("\n\n");
|
|
89
|
+
}
|
|
58
90
|
export class Client {
|
|
59
91
|
url;
|
|
60
92
|
feed;
|
|
61
93
|
api;
|
|
62
94
|
timeoutMs;
|
|
95
|
+
feedPassword;
|
|
63
96
|
/** A notefeed server. The feed set here is the default for every call; each call can override it. */
|
|
64
97
|
constructor(options) {
|
|
65
|
-
const { url, feed, password, timeoutMs } = options ?? {};
|
|
98
|
+
const { url, feed, password, feedPassword, timeoutMs } = options ?? {};
|
|
66
99
|
if (!url)
|
|
67
100
|
throw new ConfigError("no url given");
|
|
68
101
|
this.url = url.replace(/\/+$/, "");
|
|
@@ -71,29 +104,139 @@ export class Client {
|
|
|
71
104
|
// Never echo the value: it would end up in terminals and CI logs.
|
|
72
105
|
if (pw && /[\x00-\x1f\x7f]/.test(pw))
|
|
73
106
|
throw new ConfigError("password contains invalid characters");
|
|
107
|
+
this.feedPassword = checkFeedPassword(feedPassword);
|
|
74
108
|
this.timeoutMs = timeoutMs ?? 10_000;
|
|
75
109
|
this.api = createClient(createConfig({ baseUrl: this.url, headers: pw ? { Authorization: `Bearer ${pw}` } : {} }));
|
|
76
110
|
}
|
|
77
|
-
/** NOTEFEED_URL, NOTEFEED_FEED and
|
|
111
|
+
/** NOTEFEED_URL, NOTEFEED_FEED, NOTEFEED_PASSWORD and NOTEFEED_FEED_PASSWORD. */
|
|
78
112
|
static fromEnv(env = process.env) {
|
|
79
|
-
return new Client({
|
|
113
|
+
return new Client({
|
|
114
|
+
url: env.NOTEFEED_URL ?? "",
|
|
115
|
+
feed: env.NOTEFEED_FEED || undefined,
|
|
116
|
+
password: env.NOTEFEED_PASSWORD,
|
|
117
|
+
feedPassword: env.NOTEFEED_FEED_PASSWORD,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Post a note: a string is markdown, bytes or a Blob are a file whose media type is `type` (a Blob's own `type` if left out): `text/markdown`,
|
|
122
|
+
* `image/png`, `image/jpeg`, `image/gif` or `image/webp`. The server accepts nothing else and checks that the body is what the type says.
|
|
123
|
+
* The feed is created by its first note. `Created.file` is the note's file name: write `` in a markdown note to show a picture, or
|
|
124
|
+
* pass it as `image` to updateFeed; `file_url` is for a link outside notefeed.
|
|
125
|
+
*
|
|
126
|
+
* `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
|
|
127
|
+
* letters, digits, `-`, `_`, `.`, `:`; not verified). `title` is its title (at most 100 characters, one line); left out, it is taken from the
|
|
128
|
+
* text. `alt` is a picture's alternative text, `name` the file's original name. `readId` is the read id the feed gets when this post creates it
|
|
129
|
+
* (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).
|
|
130
|
+
*
|
|
131
|
+
* `attachments` (only with a markdown string) are pictures posted first, one note each, in order: `` in the markdown shows one (an
|
|
132
|
+
* attachment the text never refers to is added at the end), and `Created.attachments` lists them. `tags` label the images too. The options
|
|
133
|
+
* are checked before anything is sent. If an upload fails, its error is thrown with `attachment` (the name) and `posted` (the images already
|
|
134
|
+
* posted, which stay) set; the text note is not posted.
|
|
135
|
+
*/
|
|
136
|
+
async post(content, options = {}) {
|
|
137
|
+
const { attachments = [], ...own } = options;
|
|
138
|
+
if (attachments.length === 0)
|
|
139
|
+
return { ...(await this.postOne(content, own)), attachments: [] };
|
|
140
|
+
if (typeof content !== "string")
|
|
141
|
+
throw new ConfigError("attachments go with a markdown string");
|
|
142
|
+
if ((own.type ?? "text/markdown") !== "text/markdown")
|
|
143
|
+
throw new ConfigError("attachments go with a markdown string");
|
|
144
|
+
const types = attachments.map((a) => {
|
|
145
|
+
if (!ATTACHMENT_NAME.test(a.name))
|
|
146
|
+
throw new ConfigError(`attachment name "${a.name}": use letters, digits, ., _ and - only`);
|
|
147
|
+
const type = asFile(a.content, a.type).type;
|
|
148
|
+
if (!IMAGE_TYPES.includes(type))
|
|
149
|
+
throw new ConfigError(`attachment "${a.name}" must be a picture (${IMAGE_TYPES.join(", ")})`);
|
|
150
|
+
return type;
|
|
151
|
+
});
|
|
152
|
+
const dup = attachments.find((a, i) => attachments.findIndex((b) => b.name === a.name) !== i);
|
|
153
|
+
if (dup)
|
|
154
|
+
throw new ConfigError(`attachment "${dup.name}" is given twice`);
|
|
155
|
+
const posted = [];
|
|
156
|
+
const sent = new Map();
|
|
157
|
+
for (const [i, a] of attachments.entries()) {
|
|
158
|
+
try {
|
|
159
|
+
const done = await this.postOne(a.content, { feed: own.feed, feedPassword: own.feedPassword, type: types[i], alt: a.alt, name: a.name, tags: own.tags, readId: own.readId });
|
|
160
|
+
posted.push(done);
|
|
161
|
+
sent.set(a.name, done.file);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
if (e instanceof NotefeedError) {
|
|
165
|
+
e.message = `attachment "${a.name}": ${e.message}`;
|
|
166
|
+
e.attachment = a.name;
|
|
167
|
+
e.posted = posted;
|
|
168
|
+
}
|
|
169
|
+
throw e;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
return { ...(await this.postOne(placeImages(content, sent), own)), attachments: posted };
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (e instanceof NotefeedError)
|
|
177
|
+
e.posted = posted; // the text failed: the images stay, `attachment` stays null
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async postOne(content, options) {
|
|
182
|
+
const feed = this.feedFor(options.feed);
|
|
183
|
+
const { body, type } = asFile(content, options.type);
|
|
184
|
+
const headers = {
|
|
185
|
+
...this.opts(options.feedPassword).headers,
|
|
186
|
+
"Content-Type": type,
|
|
187
|
+
...(options.title && { "X-Note-Title": headerValue(options.title) }),
|
|
188
|
+
...(options.tags?.length && { "X-Note-Tags": options.tags.join(",") }),
|
|
189
|
+
...(options.alt && { "X-Note-Alt": headerValue(options.alt) }),
|
|
190
|
+
...(options.name && { "X-Note-Name": headerValue(options.name) }),
|
|
191
|
+
...(options.readId && { "X-Read-Id": options.readId }),
|
|
192
|
+
};
|
|
193
|
+
return this.call(postNote({ client: this.api, path: { feed }, body, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
|
|
194
|
+
}
|
|
195
|
+
/** Replace a note's content (a string is markdown; bytes and Blobs need the note's own `type`, as for post()). Its id, URLs and metadata stay. Same options as post(). */
|
|
196
|
+
async edit(id, content, options = {}) {
|
|
197
|
+
const feed = this.feedFor(options.feed);
|
|
198
|
+
const { body, type } = asFile(content, options.type);
|
|
199
|
+
const headers = { ...this.opts(options.feedPassword).headers, "Content-Type": type };
|
|
200
|
+
return this.call(editNote({ client: this.api, path: { feed, id }, body, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
|
|
80
201
|
}
|
|
81
|
-
|
|
202
|
+
/** 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(). */
|
|
203
|
+
async update(id, meta, options = {}) {
|
|
82
204
|
const feed = this.feedFor(options.feed);
|
|
83
|
-
return this.call(
|
|
205
|
+
return this.call(patchNote({ client: this.api, path: { feed, id }, body: meta, ...this.opts(options.feedPassword) }));
|
|
84
206
|
}
|
|
85
|
-
/**
|
|
207
|
+
/** Remove a note for good. The feed stays, even with no notes left. Same options as post(). */
|
|
208
|
+
async delete(id, options = {}) {
|
|
209
|
+
const feed = this.feedFor(options.feed);
|
|
210
|
+
await this.call(deleteNote({ client: this.api, path: { feed, id }, ...this.opts(options.feedPassword) }));
|
|
211
|
+
}
|
|
212
|
+
/** The feed's name, title, description, whether it has a password, and its read link (null while it has no notes). Same options as post(). */
|
|
213
|
+
async feedInfo(options = {}) {
|
|
214
|
+
const feed = this.feedFor(options.feed);
|
|
215
|
+
return this.call(getFeed({ client: this.api, path: { feed }, ...this.opts(options.feedPassword) }));
|
|
216
|
+
}
|
|
217
|
+
/** Replace the feed's title and description (both; an empty string clears one), and set (`image`: a file name from uploadImage) or clear (`""`) the title image; leave `image` out to keep it. `read_id` gives the feed another read id (3 to 64 characters of `a-z`, `0-9`, `-`, `_`; `""` for a random one; left out keeps it); the old one is freed, and relative image links in notes follow while full URLs do not. The feed must already exist. Same options as post(). */
|
|
218
|
+
async updateFeed(settings, options = {}) {
|
|
219
|
+
const feed = this.feedFor(options.feed);
|
|
220
|
+
const body = { title: settings.title, description: settings.description, ...(settings.image === undefined ? {} : { image: settings.image }), ...(settings.read_id === undefined ? {} : { read_id: settings.read_id }) };
|
|
221
|
+
return this.call(updateFeed({ client: this.api, path: { feed }, body, ...this.opts(options.feedPassword) }));
|
|
222
|
+
}
|
|
223
|
+
/** Delete the feed with all its notes, settings and password, for good; its name is free again. Same options as post(). */
|
|
224
|
+
async deleteFeed(options = {}) {
|
|
225
|
+
const feed = this.feedFor(options.feed);
|
|
226
|
+
await this.call(deleteFeed({ client: this.api, path: { feed }, ...this.opts(options.feedPassword) }));
|
|
227
|
+
}
|
|
228
|
+
/** Every note in the feed, newest first, fetched a page at a time; stop iterating whenever you like. `tag`: only notes carrying it. */
|
|
86
229
|
notes(options = {}) {
|
|
87
230
|
const feed = this.feedFor(options.feed);
|
|
88
|
-
return this.pages((before) => listNotes({ client: this.api, path: { feed }, query: { limit: options.pageSize, before }, ...this.opts() }));
|
|
231
|
+
return this.pages((before) => listNotes({ client: this.api, path: { feed }, query: { limit: options.pageSize, before, tag: options.tag }, ...this.opts(options.feedPassword) }));
|
|
89
232
|
}
|
|
90
233
|
async note(id, options = {}) {
|
|
91
234
|
const feed = this.feedFor(options.feed);
|
|
92
|
-
return this.call(getNote({ client: this.api, path: { feed, id }, ...this.opts() }));
|
|
235
|
+
return this.call(getNote({ client: this.api, path: { feed, id }, ...this.opts(options.feedPassword) }));
|
|
93
236
|
}
|
|
94
237
|
/** Like notes(), by the feed's read id: public, read-only, needs no password. */
|
|
95
238
|
readNotes(readId, options = {}) {
|
|
96
|
-
return this.pages((before) => listReadNotes({ client: this.api, path: { readId }, query: { limit: options.pageSize, before }, ...this.opts() }));
|
|
239
|
+
return this.pages((before) => listReadNotes({ client: this.api, path: { readId }, query: { limit: options.pageSize, before, tag: options.tag }, ...this.opts() }));
|
|
97
240
|
}
|
|
98
241
|
async readNote(readId, id) {
|
|
99
242
|
return this.call(getReadNote({ client: this.api, path: { readId, id }, ...this.opts() }));
|
|
@@ -104,8 +247,9 @@ export class Client {
|
|
|
104
247
|
throw new ConfigError("no feed given");
|
|
105
248
|
return checkFeed(f);
|
|
106
249
|
}
|
|
107
|
-
opts() {
|
|
108
|
-
|
|
250
|
+
opts(feedPassword) {
|
|
251
|
+
const fp = checkFeedPassword(feedPassword) ?? this.feedPassword;
|
|
252
|
+
return { signal: AbortSignal.timeout(this.timeoutMs), headers: fp ? { "X-Feed-Password": fp } : {} };
|
|
109
253
|
}
|
|
110
254
|
// `before` is the previous page's `next`: older notes only, so notes posted meanwhile never repeat.
|
|
111
255
|
async *pages(fetchPage) {
|
|
@@ -128,6 +272,8 @@ export class Client {
|
|
|
128
272
|
if (response.redirected) {
|
|
129
273
|
throw new NotefeedError(`${this.url} redirected to ${new URL(response.url).origin}: use that address as the URL`, response.status);
|
|
130
274
|
}
|
|
275
|
+
if (response.status === 204 && error === undefined)
|
|
276
|
+
return undefined; // delete: no body
|
|
131
277
|
if (response.ok) {
|
|
132
278
|
if (error === undefined && isObject(data))
|
|
133
279
|
return data;
|
|
@@ -155,3 +301,10 @@ function checkFeed(feed) {
|
|
|
155
301
|
throw new ConfigError("invalid feed name: use 1-64 of a-z, 0-9, _ and -");
|
|
156
302
|
return feed;
|
|
157
303
|
}
|
|
304
|
+
function checkFeedPassword(value) {
|
|
305
|
+
const v = value?.trim() || null;
|
|
306
|
+
// Never echo the value: it would end up in terminals and CI logs.
|
|
307
|
+
if (v && /[\x00-\x1f\x7f]/.test(v))
|
|
308
|
+
throw new ConfigError("feed password contains invalid characters");
|
|
309
|
+
return v;
|
|
310
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getNote, getOpenApi, getReadNote, listNotes, listReadNotes, type Options, postNote } from './sdk.gen.js';
|
|
2
|
-
export type { ClientOptions, Created, Error, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetOpenApiData, GetOpenApiResponse, GetOpenApiResponses, GetReadNoteData, GetReadNoteError, GetReadNoteErrors, GetReadNoteResponse, GetReadNoteResponses, ListNotesData, ListNotesError, ListNotesErrors, ListNotesResponse, ListNotesResponses, ListReadNotesData, ListReadNotesError, ListReadNotesErrors, ListReadNotesResponse, ListReadNotesResponses, Note, NoteList,
|
|
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, 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, Note, NoteList, NoteMeta, PasswordJson, PatchNoteData, PatchNoteError, PatchNoteErrors, PatchNoteResponse, PatchNoteResponses, PostNoteData, PostNoteError, PostNoteErrors, PostNoteResponse, PostNoteResponses, ReadFeed, RemoveFeedPasswordData, RemoveFeedPasswordError, RemoveFeedPasswordErrors, RemoveFeedPasswordResponse, RemoveFeedPasswordResponses, UpdateFeedData, UpdateFeedError, UpdateFeedErrors, UpdateFeedResponse, UpdateFeedResponses } from './types.gen.js';
|
package/dist/generated/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// This file is auto-generated by @hey-api/openapi-ts
|
|
2
|
-
export { getNote, getOpenApi, getReadNote, listNotes, listReadNotes, postNote } from './sdk.gen.js';
|
|
2
|
+
export { changeFeedPassword, deleteFeed, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, patchNote, postNote, removeFeedPassword, updateFeed } from './sdk.gen.js';
|