filestow 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aplance Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,169 @@
1
- # Temporary Holding Version
1
+ # filestow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The TypeScript SDK for [Filestow](https://hub.docker.com/r/aplance/filestow), file storage for
4
+ your projects over any S3 store: a typed client for its project API, every type in the API, zod
5
+ schemas for them, and the checks for what Filestow sends to your server.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install filestow
11
+ ```
12
+
13
+ It has no dependencies and needs Node 20 or later, or any runtime with `fetch` and Web Crypto;
14
+ it is an ES module. The zod schemas, at `filestow/zod`, need `zod` 4 beside it.
15
+
16
+ ## Quick start
17
+
18
+ A project's server holds the key; a phone or browser never does. Make the project, its key and
19
+ a bucket in Filestow's console, then:
20
+
21
+ ```ts
22
+ import { Filestow } from "filestow";
23
+
24
+ const filestow = new Filestow({
25
+ url: "https://filestow.example.com",
26
+ key: process.env.FILESTOW_KEY,
27
+ filesUrl: "https://files.example.com", // the public bucket's address, for url()
28
+ });
29
+
30
+ // On the server: a ticket for exactly `size` bytes of this type, into this folder of
31
+ // its bucket; `bucket: "media"` instead stores it in no folder.
32
+ const upload = await filestow.createUpload({ folder: "covers", type: "image/jpeg", size, owner: "user:42" });
33
+
34
+ // On the client: PUT the bytes to upload.url with upload.headers.
35
+
36
+ // Back on the server, the same call any number of times.
37
+ const file = await filestow.completeUpload(upload.id);
38
+ // file.url is public and final. Save file.key: it builds the address, and names the
39
+ // file to look it up, list or restore its versions, or delete it.
40
+ ```
41
+
42
+ A server can store a file in one call instead: `upload({ bucket, body, type })`, where `body` is
43
+ anything `fetch` sends, or a Node stream, which is sent once.
44
+
45
+ A file of 100 MB or more (by default) comes with `upload.parts` in place of `upload.url`: the
46
+ client PUTs each part to its URL and keeps the ETag each answers with, and the server passes
47
+ them to `completeUpload(upload.id, parts)`. `signUploadParts` signs the parts again once their
48
+ URLs stop working.
49
+
50
+ A video comes back `processing`, with no URL yet; once its job is done, `get` returns it
51
+ `ready`, with its `url`, `posterUrl` and `durationMs`, and `hlsUrl` in a bucket that streams.
52
+ The project's webhook is told too (below).
53
+
54
+ A document in a bucket that makes previews gets its `previewUrl` once the preview is drawn, a
55
+ moment after it is stored; in a private bucket, sign it with `size: "preview"`.
56
+
57
+ `createUpload({ type, size, replaceKey })` or `upload({ body, type, replaceKey })` replaces the
58
+ file at that key, where it is: a replacement names no bucket or folder, and needs a `full` key.
59
+ Where its bucket keeps versions, its earlier bytes are kept: `versions(key)` lists them, each
60
+ with a signed link, and `restoreVersion(key, n)` puts one back; elsewhere they are deleted.
61
+
62
+ ## Addresses
63
+
64
+ `url(key)` builds a public file's address with no call: `filesUrl`, a slash and the key. A
65
+ project with several public buckets gives one address per bucket, `filesUrl: { media:
66
+ "https://media.example.com", docs: "https://docs.example.com" }`, and names the file's:
67
+ `url(file.key, { bucket: file.bucket })`.
68
+
69
+ Each kind that makes an image names its own smaller copies: images of themselves, videos of
70
+ their poster, documents of their preview. `url(file.key, { size: "thumb" })` builds one's
71
+ address, and `url(file.key, { size: "preview" })` a document's preview. In a private bucket,
72
+ `sign(keys, { size })` signs them instead of the files, leaving out a file without that copy,
73
+ and `sign(keys, { download,
74
+ downloadName })` opens a file in the browser (`download: false`) or saves it (`true`), under its
75
+ own name or the one given.
76
+
77
+ ## Methods
78
+
79
+ | What | Methods | Key scope |
80
+ | --- | --- | --- |
81
+ | Files | `list` (the live files, or the trash), `get`, `upload` (or `createUpload`, `signUploadParts` and `completeUpload`), `update` (name, alt text, tags), `place` (into a folder, or a new place in its order), `unplace`, `move` (to another bucket), `delete` (to trash, or `purge`), `restore`, `sign`, `url` (no call) | read, uploads or full |
82
+ | Versions | `versions`, `restoreVersion`, `deleteVersion` | read or full |
83
+ | Folders | `folders`, `folder`, `createFolder`, `updateFolder` (what is given changes; a null `maxFiles` removes its cap), `deleteFolder` (its files stay) | read or full |
84
+ | Buckets | `buckets`, each with its rules, what its files take, and where a public one serves them | read |
85
+ | Tags | `tags`, the ones in use, the most used first | read |
86
+ | Upload links | `links`, `createLink` (its page and its API address, once), `revokeLink` and `reactivateLink` (each answers the link, and twice is once), `deleteLink` (once it takes no more files) | read or full |
87
+ | Owners | `usage`, `deleteOwner` | read or full |
88
+ | Checks | `health` (no key), `verifyWebhook` and `verifyUsageCheck` (no call) | |
89
+
90
+ A key has a scope: `full`; `read` (lists, gets and signs; changes nothing); or `uploads` (new
91
+ files only: it cannot read, replace or delete files). It may be limited to some buckets, outside
92
+ which it finds nothing. Give each server the least it needs. Every call, with its scope, is in
93
+ the API reference at `/docs` on your Filestow, and in its contract at `/openapi.json`.
94
+
95
+ A bucket and a folder each say what they take now, kind by kind (`intake`: the formats, the
96
+ largest file, and whether files are made again on the way in), so an upload form can say it and
97
+ check a file before it asks for a ticket.
98
+
99
+ `list()` takes one bucket or several, one folder or several, an owner, kinds, a tag and a search,
100
+ in one folder's own order (its default with one folder), newest first or by name, a page at a
101
+ time. It lists the buckets and folders the project API may list; an owner's files list from all
102
+ of them; a file in two folders listed comes once. With `trashed: true` it gives the files in
103
+ trash instead, each with `trashedAt` and `purgeAt`. Pass a page's `nextCursor` as `cursor` for
104
+ the next, with the same filters; `folders()` and `links()` page the same way. What is added,
105
+ renamed, moved or deleted between two pages never breaks the next.
106
+
107
+ ## Webhooks and usage checks
108
+
109
+ Filestow calls a project's server at two addresses, each set on the project's page in the
110
+ console with a signing secret shown once: its webhook, told when a video is ready or failed
111
+ (`file.ready`, `file.failed`) or sent a `test`; and its usage check, asked which files are still
112
+ in use before the console moves any to trash. Both carry `Filestow-Signature`, checked against
113
+ the body exactly as it was sent, so the route reads the raw text before any JSON parser does.
114
+ When each is sent, how it is retried and what it answers is under Webhooks in the API
115
+ reference.
116
+
117
+ ```ts
118
+ import express from "express";
119
+ import { Filestow, type UsageCheckAnswer, type UsageCheckRequest } from "filestow";
120
+
121
+ const filestow = new Filestow({
122
+ url: "https://filestow.example.com",
123
+ key: process.env.FILESTOW_KEY,
124
+ webhookSecret: process.env.FILESTOW_WEBHOOK_SECRET,
125
+ usageCheckSecret: process.env.FILESTOW_USAGE_CHECK_SECRET,
126
+ });
127
+ const raw = express.text({ type: "application/json" });
128
+
129
+ app.post("/filestow/webhook", raw, async (req, res) => {
130
+ const event = await filestow.verifyWebhook({ body: req.body, signature: req.get("Filestow-Signature") ?? null });
131
+ if (!event) return res.sendStatus(401); // not Filestow's, or signed too long ago
132
+ if (event.type === "file.ready") await markReady(event.file!.key, event.file!.url);
133
+ if (event.type === "file.failed") await markFailed(event.file!.key, event.reason);
134
+ res.sendStatus(204); // event.id is the same on a repeat: act on it once
135
+ });
136
+
137
+ app.post("/filestow/usage-check", raw, async (req, res) => {
138
+ if (!(await filestow.verifyUsageCheck({ body: req.body, signature: req.get("Filestow-Signature") ?? null }))) {
139
+ return res.sendStatus(401);
140
+ }
141
+ const { keys } = JSON.parse(req.body) as UsageCheckRequest; // up to 500 keys a request
142
+ const answer: UsageCheckAnswer = { inUse: {} };
143
+ for (const use of await usesOf(keys)) answer.inUse[use.key] = `Used by ${use.what}`;
144
+ res.json(answer);
145
+ });
146
+ ```
147
+
148
+ ## Errors and retries
149
+
150
+ Every call returns its data or throws a `FilestowError`. Branch on its `code`, one of the API's
151
+ stable codes or the SDK's own `NETWORK_ERROR` (no answer) and `INVALID_RESPONSE` (an answer it
152
+ could not read), never on its `message`. It also carries `status` (0 when there was no answer),
153
+ `requestId` to quote when reporting a problem, `fieldErrors` for forms, and `retryAfterSeconds`.
154
+
155
+ A call that fails on the network, a 502 or 504, `SERVICE_UNAVAILABLE`, a request still
156
+ running, a busy file or a project moving to another store is tried again, twice by default
157
+ (`retries`), waiting as long as Filestow says, or a moment. Calls that change something carry
158
+ an idempotency key, so a repeat never happens twice; a stream body is sent once, never again.
159
+ One attempt may take 30 seconds (`timeoutMs`); an upload and its completion are not timed.
160
+ `fetch` in the options takes the place of the global one, for a proxy or a test.
161
+
162
+ ## Licence
163
+
164
+ MIT: see [LICENSE](LICENSE). Filestow itself is a Docker image, free to use:
165
+ [aplance/filestow](https://hub.docker.com/r/aplance/filestow).
166
+
167
+ ## Feedback
168
+
169
+ Questions and feedback: info@aplance.com
@@ -0,0 +1,270 @@
1
+ import type { Bucket, CompletedPart, CreateFolderRequest, CreateLinkRequest, CreateUploadRequest, File, FileKind, FileSort, FileTag, FileVersion, Folder, Health, NewLink, OwnerDeletion, OwnerUsage, SignedURL, UpdateFileRequest, UpdateFolderRequest, Upload, UploadLink, UploadParts } from "./gen/contract.js";
2
+ import type { WebhookEvent } from "./webhook.js";
3
+ export type FilestowOptions = {
4
+ /** Filestow's address, like `https://filestow.example.com`. */
5
+ url: string;
6
+ /** A project API key (`ak_...`). Every call but `health()` needs one. */
7
+ key?: string;
8
+ /**
9
+ * Where the project's public buckets serve their files, for `url()`: each
10
+ * bucket's `publicUrl`. One address when the project has one public bucket,
11
+ * or one per bucket, by its name.
12
+ */
13
+ filesUrl?: string | Record<string, string>;
14
+ /** The usage-check signing secret, for `verifyUsageCheck()`. */
15
+ usageCheckSecret?: string;
16
+ /** The webhook signing secret, for `verifyWebhook()`. */
17
+ webhookSecret?: string;
18
+ /**
19
+ * How long one attempt may take, in milliseconds. Default 30 000.
20
+ * `upload()` and `completeUpload()` are not timed: their time is the
21
+ * file's.
22
+ */
23
+ timeoutMs?: number;
24
+ /**
25
+ * How many times a call is tried again after a network failure, a 502 or
26
+ * 504, `SERVICE_UNAVAILABLE`, a request still running, a busy file or a
27
+ * project moving to another store. Default 2.
28
+ */
29
+ retries?: number;
30
+ /** A fetch to use instead of the global one. */
31
+ fetch?: typeof fetch;
32
+ };
33
+ /**
34
+ * A file to store: into a bucket, into a folder, which names its bucket, or
35
+ * both; a replacement names neither, and goes where its file is.
36
+ */
37
+ export type UploadInput = {
38
+ bucket?: string;
39
+ folder?: string;
40
+ /**
41
+ * The file: anything `fetch` can send, or a Node stream. A stream is sent
42
+ * once, never retried.
43
+ */
44
+ body: BodyInit | AsyncIterable<Uint8Array>;
45
+ /** The file's content type. */
46
+ type: string;
47
+ name?: string;
48
+ owner?: string;
49
+ ownerLimitBytes?: number;
50
+ /** For screen readers. Not with `replaceKey`. */
51
+ altText?: string;
52
+ /** Not with `replaceKey`. */
53
+ tags?: string[];
54
+ /**
55
+ * The key of a file to replace: the new bytes take its key, and where its
56
+ * bucket keeps versions its bytes as they were are kept. Needs a `full` key.
57
+ */
58
+ replaceKey?: string;
59
+ };
60
+ /** What to list. With none, every file of the buckets and folders the project API may list, newest first. */
61
+ export type ListInput = {
62
+ /** Files of a bucket, or of several, by name. */
63
+ bucket?: string | string[];
64
+ /** Files of a folder, or of several, by slug. */
65
+ folder?: string | string[];
66
+ /** Files of one owner. */
67
+ owner?: string;
68
+ /** Files of these kinds. */
69
+ kind?: FileKind | FileKind[];
70
+ /** Files that carry this tag. */
71
+ tag?: string;
72
+ /** Files whose name or key holds this text, in any case. */
73
+ q?: string;
74
+ /** `order`, one folder's own order and its default; `newest`, the default otherwise; or `name`. */
75
+ sort?: FileSort;
76
+ /** The files in trash instead of the live ones, each with its `trashedAt` and `purgeAt`. */
77
+ trashed?: boolean;
78
+ /** `nextCursor` from the previous page, with the same filters and sort. */
79
+ cursor?: string;
80
+ /** At most 100. */
81
+ limit?: number;
82
+ };
83
+ export type FilePage = {
84
+ files: File[];
85
+ /** Pass as `cursor` for the next page. Missing on the last page. */
86
+ nextCursor?: string;
87
+ };
88
+ /** Which folders to list. With none, every folder of the project. */
89
+ export type ListFoldersInput = {
90
+ /** Folders of one bucket, by its name. */
91
+ bucket?: string;
92
+ /** `nextCursor` from the previous page, with the same bucket. */
93
+ cursor?: string;
94
+ /** At most 100. */
95
+ limit?: number;
96
+ };
97
+ export type FolderPage = {
98
+ folders: Folder[];
99
+ /** Pass as `cursor` for the next page. Missing on the last page. */
100
+ nextCursor?: string;
101
+ };
102
+ /** Which upload links to list. With none, every link of the project. */
103
+ export type ListLinksInput = {
104
+ /** Links into one bucket, its folders' included, by its name. */
105
+ bucket?: string;
106
+ /** Links into one folder, by its slug. */
107
+ folder?: string;
108
+ /** `nextCursor` from the previous page, with the same bucket and folder. */
109
+ cursor?: string;
110
+ /** At most 100. */
111
+ limit?: number;
112
+ };
113
+ export type LinkPage = {
114
+ links: UploadLink[];
115
+ /** Pass as `cursor` for the next page. Missing on the last page. */
116
+ nextCursor?: string;
117
+ };
118
+ export type SignOptions = {
119
+ /** Seconds the links stay valid. */
120
+ expiresIn?: number;
121
+ /**
122
+ * True saves the file; false opens it in the browser. Left out, as it is
123
+ * served: images, video and audio open, the rest download.
124
+ */
125
+ download?: boolean;
126
+ /** The name it saves under, in place of the file's own. */
127
+ downloadName?: string;
128
+ /** One of the files' smaller copies by its name, or "preview" for a document's preview, signed in their place; a file without it is left out. */
129
+ size?: string;
130
+ };
131
+ /**
132
+ * The Filestow client. Every call returns the answer's data, or throws an
133
+ * {@link FilestowError}.
134
+ *
135
+ * Calls that change something carry an idempotency key, so a call lost to the
136
+ * network can be repeated and the first answer comes back instead of it
137
+ * happening twice.
138
+ */
139
+ export declare class Filestow {
140
+ #private;
141
+ constructor(options: FilestowOptions);
142
+ /**
143
+ * Start an upload: the client sends the bytes to `url`, then call
144
+ * `completeUpload`. A large file comes back with `parts` instead: the
145
+ * client sends each part to its URL and keeps the ETag each answers with.
146
+ */
147
+ createUpload(input: CreateUploadRequest): Promise<Upload>;
148
+ /** Sign a large upload's parts again, every one or those numbered, once their URLs stopped working. */
149
+ signUploadParts(uploadId: string, numbers?: number[]): Promise<UploadParts>;
150
+ /**
151
+ * Finish an upload and get the file; a large file names each part with its
152
+ * ETag. Calling it again returns the same file. Not timed: Filestow checks
153
+ * and processes the file before it answers.
154
+ */
155
+ completeUpload(uploadId: string, parts?: CompletedPart[]): Promise<File>;
156
+ /** Store a file from a server, in one call. Not timed: its time is the file's. */
157
+ upload(input: UploadInput): Promise<File>;
158
+ /**
159
+ * The public address of a file, with no network call: `filesUrl`, a slash
160
+ * and its key, and for one of its smaller copies `.` and its name. Name
161
+ * the file's bucket when `filesUrl` holds one address per bucket.
162
+ */
163
+ url(key: string, options?: {
164
+ bucket?: string;
165
+ size?: string;
166
+ }): string;
167
+ /** Signed links, for private files. Keys the project does not have are left out. */
168
+ sign(keys: string[], options?: SignOptions): Promise<SignedURL[]>;
169
+ /** One page of files: one folder's in its own order, otherwise newest first, as `sort` says. A file in two folders listed comes once. */
170
+ list(input?: ListInput): Promise<FilePage>;
171
+ /** One file, by its key (`file.key`). */
172
+ get(key: string): Promise<File>;
173
+ /** Change a file's name, alt text or tags, by its key. An empty alt text removes it; `tags` replaces them all. */
174
+ update(key: string, changes: UpdateFileRequest): Promise<File>;
175
+ /**
176
+ * Move a file to another bucket of the project, by its key: the bucket
177
+ * named, or the bucket of the folder named, where it goes first. Its key
178
+ * stays; it leaves the folders of the bucket it leaves, and takes the new
179
+ * bucket's smaller copies.
180
+ */
181
+ move(key: string, to: {
182
+ bucket: string;
183
+ folder?: string;
184
+ } | {
185
+ bucket?: string;
186
+ folder: string;
187
+ }): Promise<File>;
188
+ /**
189
+ * Put a file in a folder of its bucket, by slug: right after the file keyed
190
+ * `after`, or first. A file in the folder already takes the new place.
191
+ */
192
+ place(key: string, folder: string, options?: {
193
+ after?: string;
194
+ }): Promise<File>;
195
+ /** Take a file out of a folder, by slug. It stays in its bucket and its other folders. */
196
+ unplace(key: string, folder: string): Promise<File>;
197
+ /**
198
+ * Delete a file, by its key: to trash, where its address keeps working for
199
+ * the grace period, or with `purge`, for good at once.
200
+ */
201
+ delete(key: string, options?: {
202
+ purge?: boolean;
203
+ }): Promise<File>;
204
+ /** Bring a file back from trash, by its key. */
205
+ restore(key: string): Promise<File>;
206
+ /** Trash everything an owner has, private at once; restorable for `trash.retention_days` unless `purge`. */
207
+ deleteOwner(owner: string, options?: {
208
+ purge?: boolean;
209
+ }): Promise<OwnerDeletion>;
210
+ /** A file's versions, by its key: the bytes it kept as it was replaced, newest first, each with a signed link. */
211
+ versions(key: string): Promise<FileVersion[]>;
212
+ /** Restore version n of a file, by its key: its bytes take the key again, and the bytes there now are kept as a version. */
213
+ restoreVersion(key: string, n: number): Promise<File>;
214
+ /** Delete version n of a file for good, by its key. */
215
+ deleteVersion(key: string, n: number): Promise<File>;
216
+ /** The tags the project's live files carry, the most used first. */
217
+ tags(): Promise<FileTag[]>;
218
+ /** Bytes and files an owner has. Trash is not counted. */
219
+ usage(owner: string): Promise<OwnerUsage>;
220
+ /** The project's buckets: each with its rules, what its files take, and where a public one serves them. */
221
+ buckets(): Promise<Bucket[]>;
222
+ /** One page of the project's folders, by slug: collections of their buckets' files. */
223
+ folders(input?: ListFoldersInput): Promise<FolderPage>;
224
+ /** One folder, by its slug. */
225
+ folder(slug: string): Promise<Folder>;
226
+ /** Add a folder to one of the project's buckets, listing what the bucket takes or less. */
227
+ createFolder(input: CreateFolderRequest): Promise<Folder>;
228
+ /**
229
+ * Change a folder, by its slug: what is given changes, the rest stays. Its
230
+ * name, what it lists, its max file size per kind (`{}` removes them all),
231
+ * its cap (`null` removes it), and whether the API lists it.
232
+ */
233
+ updateFolder(slug: string, changes: UpdateFolderRequest): Promise<Folder>;
234
+ /** Delete a folder, by its slug. Its files stay in their bucket and their other folders. */
235
+ deleteFolder(slug: string): Promise<void>;
236
+ /** One page of the project's upload links, the newest first. */
237
+ links(input?: ListLinksInput): Promise<LinkPage>;
238
+ /** Make an upload link into a bucket, or a folder of it. Its address comes back this once. */
239
+ createLink(input: CreateLinkRequest): Promise<NewLink>;
240
+ /** Revoke an upload link: it stops taking files at once. The files it brought stay. */
241
+ revokeLink(id: string): Promise<UploadLink>;
242
+ /** Reactivate a revoked upload link, at the same address, while it would take files: not expired, not used up. */
243
+ reactivateLink(id: string): Promise<UploadLink>;
244
+ /** Delete an upload link that takes no more files: revoked, expired or used up. The files it brought stay. */
245
+ deleteLink(id: string): Promise<void>;
246
+ /**
247
+ * Health of Filestow and what it depends on. A degraded answer comes back
248
+ * with `status: "degraded"`; an unavailable one throws `SERVICE_UNAVAILABLE`.
249
+ */
250
+ health(): Promise<Health>;
251
+ /**
252
+ * Checks a usage-check request really came from Filestow: call it first in
253
+ * the server's usage-check route, with the raw body and the
254
+ * `Filestow-Signature` header.
255
+ */
256
+ verifyUsageCheck(request: {
257
+ body: string;
258
+ signature: string | null;
259
+ }): Promise<boolean>;
260
+ /**
261
+ * Checks a webhook delivery really came from Filestow, and reads it: call it
262
+ * first in the server's webhook route, with the raw body and the
263
+ * `Filestow-Signature` header. Null for anything Filestow did not sign, or
264
+ * signed too long ago. Every attempt of one event has the same `id`.
265
+ */
266
+ verifyWebhook(request: {
267
+ body: string;
268
+ signature: string | null;
269
+ }): Promise<WebhookEvent | null>;
270
+ }