@acms-subir/client 0.1.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 ADDED
@@ -0,0 +1,182 @@
1
+ # @acms-subir/client
2
+
3
+ Dependency-free ESM TypeScript client for ACMS public API v1. Supports Expo/React
4
+ Native, browsers and Node 18+ using global `fetch`, `URL`, `Headers` and
5
+ `AbortController`. Supply `fetch` when your environment needs an adapter.
6
+
7
+ ```sh
8
+ npm install @acms-subir/client
9
+ ```
10
+
11
+ ```ts
12
+ import { createClient, isAcmsError } from '@acms-subir/client';
13
+
14
+ const client = createClient({ baseUrl: 'https://example.com', locale: 'es' });
15
+ const site = await client.site();
16
+ const home = await client.home();
17
+ const page = await client.page('about/team');
18
+ for await (const post of client.paginate.posts({ collection: 'blog', limit: 50 })) {
19
+ console.log(post.title);
20
+ }
21
+ const english = client.withLocale('en');
22
+ ```
23
+
24
+ `baseUrl` accepts a site origin or a full `/api/v1` URL, with an optional
25
+ hosting prefix. All methods return **unwrapped `data`**; `openapi()` returns the
26
+ raw document. Every method accepts `locale` and `signal` in its options object.
27
+ Locale is sent only for routes that support translations. `withLocale` shares
28
+ the cache and transport configuration with the original client.
29
+
30
+ | Method | Options / behavior |
31
+ | --- | --- |
32
+ | `site()`, `theme()`, `home()` | Identity, tokens, resolved homepage |
33
+ | `page(slug, opts)` | Resolved page including sections; nested slugs supported |
34
+ | `content(opts)` | `kind`, `collection`, `page`, `limit`, `cursor` |
35
+ | `contentById(id, opts)` | Optional `kind` to disambiguate imported IDs |
36
+ | `posts(opts)` | Content filtered to posts; `collection`, `tag`, pagination |
37
+ | `post(slug, opts)` | Scans post summaries, then loads detail by ID; pass `collection` for ambiguous slugs |
38
+ | `pages(opts)` | Page **summaries**, with pagination; use `page()` for sections |
39
+ | `events(opts)` | Occurrences with `from`, `to`, `calendar`, `q`, pagination |
40
+ | `event(slug, opts)` | First matching event from occurrences in the requested window |
41
+ | `calendars()` | Public calendars |
42
+ | `resources(opts)`, `resource(slug, opts)` | Public resources; slug lookup scans list pages |
43
+ | `availability(opts)` | Required `resource`, `link`; optional `from`, `days`, `minutes` |
44
+ | `book(opts)` | Resource booking or RSVP input, optional `idempotencyKey` |
45
+ | `openapi()` | Raw OpenAPI document |
46
+ | `registerDevice({ token, platform, locale?, topics? })` | Register or refresh an Expo push token (call on every app start); `{ created, device }` |
47
+ | `updateDevice(token, { topics?, locale? })` | Change a device's topics or language |
48
+ | `unregisterDevice(token)` | Opt out (idempotent) |
49
+ | `pushTopics()` | Topics to offer as notification toggles |
50
+
51
+ The server has no post-slug, event-detail or resource-detail endpoint. Those
52
+ convenience methods may make multiple requests. `event()` only finds events
53
+ with occurrences in its window (default: next 30 UTC days); an unlisted
54
+ calendar requires `calendar`. It does not fetch arbitrary historical events.
55
+ If post slugs collide and no collection is supplied, the first listed match wins.
56
+
57
+ Pagination uses `page`/`nextPage`, not opaque cursors. `cursor` is a numeric
58
+ alias for `page`; an explicit `page` wins. `tag` filters **each fetched page
59
+ locally**, so a page can be empty with a non-null `nextPage`. Iterators continue
60
+ through these pages. `paginate.content/posts/pages/events/resources` yield
61
+ individual items (events yields occurrences). They do not deduplicate changing
62
+ server results or provide snapshot consistency; deduplicate IDs in your app.
63
+
64
+ ## Bookings, errors and cancellation
65
+
66
+ ```ts
67
+ // Persist a UUID v4 with this booking intent before submitting; reuse it until
68
+ // the outcome is known. Use your platform's secure UUID/storage facilities.
69
+ const idempotencyKey = crypto.randomUUID();
70
+ try {
71
+ const result = await client.book({
72
+ resource: 'hall', link: 'meeting', start: '2026-10-05T14:00:00Z', minutes: 60,
73
+ name: 'Example Guest', email: 'guest@example.com', idempotencyKey,
74
+ });
75
+ // Store result.manageToken securely; it grants booking-management access.
76
+ } catch (error) {
77
+ if (isAcmsError(error)) {
78
+ console.log(error.status, error.code, error.message, error.retryAfter);
79
+ // error.idempotencyKey preserves even an automatically generated key.
80
+ }
81
+ }
82
+
83
+ const controller = new AbortController();
84
+ const pending = client.home({ signal: controller.signal });
85
+ controller.abort();
86
+ await pending.catch(() => {});
87
+ ```
88
+
89
+ GET and booking POST requests retry network failures, 5xx and 429 responses at
90
+ most twice (three attempts total), with 250/500ms backoff and at least the
91
+ server's `Retry-After` delay (seconds or HTTP date). Booking retries always
92
+ reuse the same serialized body and UUID. By default each `book()` call creates
93
+ a key using `crypto.randomUUID`, falling back to a Math.random UUID v4 where
94
+ unavailable. For recovery across calls/app restarts, supply and persist your own
95
+ secure UUID. Never create a new intent merely because a response timed out.
96
+
97
+ `timeoutMs` defaults to 30,000 and covers the entire request, including retries,
98
+ cache access and body reading. A longer rate-limit delay may exhaust that
99
+ budget. Explicit aborts and timeouts are not automatically retried; an uncertain
100
+ booking must be retried with its existing key. `AcmsError` exposes `status`,
101
+ `code`, `message`, `retryAfter` (seconds), optional validation `issues` and
102
+ `idempotencyKey`. Local failures use status 0 and `network_error`, `timeout`,
103
+ `aborted`, or `invalid_response`; non-JSON HTTP errors use `http_error`.
104
+ Convenience lookups apply the timeout separately to each underlying request.
105
+
106
+ ## Push notifications
107
+
108
+ Available when the site enables push (`acms({ mobile: { push: true } })`);
109
+ otherwise these return a 404 `AcmsError`. Tokens come from
110
+ `expo-notifications` (`getExpoPushTokenAsync`). The site stores the token,
111
+ platform, language and topics only.
112
+
113
+ ```ts
114
+ const { topics } = await client.pushTopics(); // [{ id: 'news', label: 'News', kind: 'general' }, …]
115
+ await client.registerDevice({ token, platform: 'ios', locale: 'es', topics: ['news', 'events'] });
116
+ await client.updateDevice(token, { topics: ['events'] });
117
+ await client.unregisterDevice(token);
118
+ ```
119
+
120
+ ## Cache and headers
121
+
122
+ ETag revalidation defaults to an in-memory URL-keyed cache. Each read contacts
123
+ the server with `If-None-Match`; a 304 reuses the saved data. This is not an
124
+ offline fallback. Treat returned cached objects as immutable. Use `cache: false`
125
+ to disable it. Availability, booking results and errors are never cached.
126
+ Responses without an ETag or with `no-store` invalidate an older entry.
127
+
128
+ ```ts
129
+ import type { ClientCache } from '@acms-subir/client';
130
+ const cache: ClientCache = {
131
+ async get(url) { /* retrieve { etag, body } from app storage */ return null; },
132
+ async set(url, entry) { /* persist entry; null means delete */ },
133
+ };
134
+ const persistent = createClient({ baseUrl: 'https://example.com/api/v1', cache });
135
+ ```
136
+
137
+ Adapters may be synchronous or asynchronous. Keep persistent caches scoped to
138
+ one site's public API and account for storage limits. Cache adapter failures
139
+ propagate to the caller. Custom `headers` are supported; do not embed privileged
140
+ credentials. Requests omit browser credentials. Conditional headers are managed
141
+ by the client; a per-call booking key overrides a configured header key.
142
+
143
+ ## Development
144
+
145
+ ```sh
146
+ npm run build -w packages/client
147
+ npm run test -w packages/client
148
+ npm run check:types -w packages/client
149
+ npm run generate:types -w packages/client # after reviewing core schema changes
150
+ ```
151
+
152
+ The contract test imports the actual core OpenAPI generator using core's virtual
153
+ module test stubs, compares all GET/POST paths to the client method registry,
154
+ and checks generated request/response types byte-for-byte. HEAD and OPTIONS
155
+ are transport operations and have no separate data-returning methods. No core
156
+ code, Zod, or Node built-ins ship in this package's runtime.
157
+
158
+ ## Editor apps (staff sign-in)
159
+
160
+ For sites with `acms({ mobile: { editor: { redirectUris } } })`. Staff sign in
161
+ in the system browser (OAuth 2.0 + PKCE); the app never sees a password.
162
+
163
+ ```ts
164
+ import { createEditorAuth, createEditorSession, createEditorClient } from '@acms-subir/client';
165
+
166
+ const auth = createEditorAuth({ baseUrl: 'https://example.com', clientId: 'org.example.staff', redirectUri: 'org.example.staff://auth' });
167
+ const session = createEditorSession({ auth, storage /* get/set backed by expo-secure-store */, onSignedOut });
168
+ const api = createEditorClient({ baseUrl: 'https://example.com', session });
169
+
170
+ const { url, verifier, state } = await auth.start({ deviceName: 'Ana’s iPhone' });
171
+ // open `url` with expo-web-browser openAuthSessionAsync, then:
172
+ const { code } = auth.parseRedirect(returnedUrl, state);
173
+ await session.signIn(await auth.exchangeCode({ code, codeVerifier: verifier }));
174
+
175
+ const doc = await api.getDocument('post', id);
176
+ await api.saveDocument('post', id, { revision: doc.revision, fields: { title: 'New title' } });
177
+ ```
178
+
179
+ In React Native pass `crypto: { randomBytes, sha256 }` from expo-crypto to
180
+ `createEditorAuth` (Web Crypto is the default). Refreshes are single-flight;
181
+ a 401 is retried once after a refresh, then `onAuthError` runs. Writes carry
182
+ the `revision` you loaded (409 when someone saved first).
@@ -0,0 +1,291 @@
1
+ import { AcmsError } from './errors.js';
2
+ import type { EditorToken, EditorMe, EditorDocumentList, EditorDocument, EditorSections, EditorSectionResult, EditorRevision, EditorDocumentTranslation, EditorSectionTranslation, EditorMedia, EditorMediaList, EditorBooking, EditorBookingList, EditorCreateInput, EditorSaveInput, EditorListQuery, EditorSectionsQuery, EditorMediaQuery, EditorBookingsQuery } from './types.js';
3
+ /** Randomness + SHA-256. Defaults to Web Crypto; in React Native pass expo-crypto:
4
+ * { randomBytes: (n) => Crypto.getRandomBytes(n), sha256: (d) => Crypto.digest(Crypto.CryptoDigestAlgorithm.SHA256, d) } */
5
+ export interface PkceCrypto {
6
+ randomBytes(length: number): Uint8Array | Promise<Uint8Array>;
7
+ sha256(data: Uint8Array): ArrayBuffer | Uint8Array | Promise<ArrayBuffer | Uint8Array>;
8
+ }
9
+ export declare const webCrypto: PkceCrypto;
10
+ /** base64url without padding (no btoa: works in every JS runtime) */
11
+ export declare function base64url(bytes: Uint8Array): string;
12
+ /** a 43-character verifier (32 random bytes), RFC 7636 */
13
+ export declare function createCodeVerifier(crypto?: PkceCrypto): Promise<string>;
14
+ export declare function codeChallengeFor(verifier: string, crypto?: PkceCrypto): Promise<string>;
15
+ export declare function createPkce(crypto?: PkceCrypto): Promise<{
16
+ verifier: string;
17
+ challenge: string;
18
+ method: 'S256';
19
+ }>;
20
+ export declare function createState(crypto?: PkceCrypto): Promise<string>;
21
+ export interface EditorTokens {
22
+ accessToken: string;
23
+ refreshToken: string;
24
+ /** epoch ms when the access token expires */
25
+ expiresAt: number;
26
+ user: EditorToken['user'];
27
+ device: EditorToken['device'];
28
+ }
29
+ export declare const toTokens: (t: EditorToken, now?: number) => EditorTokens;
30
+ export interface EditorAuthOptions {
31
+ /** Site origin (or its /api/v1 base) */
32
+ baseUrl: string;
33
+ /** Your app's id, shown on the consent screen (letters, digits, . _ : -) */
34
+ clientId: string;
35
+ /** Must be listed in acms({ mobile: { editor: { redirectUris } } }) */
36
+ redirectUri: string;
37
+ fetch?: typeof globalThis.fetch;
38
+ crypto?: PkceCrypto;
39
+ timeoutMs?: number;
40
+ }
41
+ export interface EditorAuth {
42
+ /** where to open the system browser (expo-web-browser openAuthSessionAsync) */
43
+ buildAuthorizeUrl(o: {
44
+ codeChallenge: string;
45
+ state: string;
46
+ deviceName?: string;
47
+ }): string;
48
+ /** PKCE + state + URL in one step; keep verifier and state until the redirect returns */
49
+ start(o?: {
50
+ deviceName?: string;
51
+ }): Promise<{
52
+ url: string;
53
+ verifier: string;
54
+ state: string;
55
+ }>;
56
+ /** read the redirect: returns the code, throws on error, cancel or state mismatch */
57
+ parseRedirect(url: string, expectedState: string): {
58
+ code: string;
59
+ };
60
+ exchangeCode(o: {
61
+ code: string;
62
+ codeVerifier: string;
63
+ }): Promise<EditorTokens>;
64
+ /** rotates: the old refresh token stops working; reusing it signs the device out */
65
+ refresh(refreshToken: string): Promise<EditorTokens>;
66
+ revoke(token: string): Promise<void>;
67
+ }
68
+ export declare function createEditorAuth(options: EditorAuthOptions): EditorAuth;
69
+ export interface TokenStorage {
70
+ get(): EditorTokens | null | Promise<EditorTokens | null>;
71
+ /** null signs out */
72
+ set(tokens: EditorTokens | null): void | Promise<void>;
73
+ }
74
+ export interface EditorSession {
75
+ getAccessToken(): Promise<string | null>;
76
+ /** single-flight: concurrent callers share one refresh request */
77
+ refresh(): Promise<string | null>;
78
+ signIn(tokens: EditorTokens): Promise<void>;
79
+ signOut(): Promise<void>;
80
+ tokens(): Promise<EditorTokens | null>;
81
+ }
82
+ export declare function createEditorSession(o: {
83
+ auth: EditorAuth;
84
+ storage: TokenStorage;
85
+ onSignedOut?: () => void;
86
+ refreshSkewMs?: number;
87
+ now?: () => number;
88
+ }): EditorSession;
89
+ export declare const authEndpoints: {
90
+ readonly exchangeCode: {
91
+ readonly method: "POST";
92
+ readonly path: "/auth/token";
93
+ };
94
+ readonly revoke: {
95
+ readonly method: "POST";
96
+ readonly path: "/auth/revoke";
97
+ };
98
+ };
99
+ export declare const editorEndpoints: {
100
+ readonly me: {
101
+ readonly method: "GET";
102
+ readonly path: "/admin/me";
103
+ };
104
+ readonly listDocuments: {
105
+ readonly method: "GET";
106
+ readonly path: "/admin/documents";
107
+ };
108
+ readonly createDocument: {
109
+ readonly method: "POST";
110
+ readonly path: "/admin/documents";
111
+ };
112
+ readonly getDocument: {
113
+ readonly method: "GET";
114
+ readonly path: "/admin/documents/{kind}/{id}";
115
+ };
116
+ readonly saveDocument: {
117
+ readonly method: "PUT";
118
+ readonly path: "/admin/documents/{kind}/{id}";
119
+ };
120
+ readonly publishDocument: {
121
+ readonly method: "POST";
122
+ readonly path: "/admin/documents/{kind}/{id}/publish";
123
+ };
124
+ readonly unpublishDocument: {
125
+ readonly method: "POST";
126
+ readonly path: "/admin/documents/{kind}/{id}/unpublish";
127
+ };
128
+ readonly scheduleDocument: {
129
+ readonly method: "POST";
130
+ readonly path: "/admin/documents/{kind}/{id}/schedule";
131
+ };
132
+ readonly discardDocument: {
133
+ readonly method: "POST";
134
+ readonly path: "/admin/documents/{kind}/{id}/discard";
135
+ };
136
+ readonly getDocumentTranslation: {
137
+ readonly method: "GET";
138
+ readonly path: "/admin/documents/{kind}/{id}/translations/{locale}";
139
+ };
140
+ readonly setDocumentTranslation: {
141
+ readonly method: "PUT";
142
+ readonly path: "/admin/documents/{kind}/{id}/translations/{locale}";
143
+ };
144
+ readonly reviewDocumentTranslation: {
145
+ readonly method: "POST";
146
+ readonly path: "/admin/documents/{kind}/{id}/translations/{locale}/review";
147
+ };
148
+ readonly draftDocumentTranslation: {
149
+ readonly method: "POST";
150
+ readonly path: "/admin/documents/{kind}/{id}/translations/{locale}/ai-draft";
151
+ };
152
+ readonly listSections: {
153
+ readonly method: "GET";
154
+ readonly path: "/admin/sections";
155
+ };
156
+ readonly updateSection: {
157
+ readonly method: "PATCH";
158
+ readonly path: "/admin/sections/{pageId}/{sectionId}";
159
+ };
160
+ readonly publishSection: {
161
+ readonly method: "POST";
162
+ readonly path: "/admin/sections/{pageId}/{sectionId}/publish";
163
+ };
164
+ readonly getSectionTranslation: {
165
+ readonly method: "GET";
166
+ readonly path: "/admin/sections/{pageId}/{sectionId}/translations/{locale}";
167
+ };
168
+ readonly setSectionTranslation: {
169
+ readonly method: "PUT";
170
+ readonly path: "/admin/sections/{pageId}/{sectionId}/translations/{locale}";
171
+ };
172
+ readonly reviewSectionTranslation: {
173
+ readonly method: "POST";
174
+ readonly path: "/admin/sections/{pageId}/{sectionId}/translations/{locale}/review";
175
+ };
176
+ readonly draftSectionTranslation: {
177
+ readonly method: "POST";
178
+ readonly path: "/admin/sections/{pageId}/{sectionId}/translations/{locale}/ai-draft";
179
+ };
180
+ readonly listMedia: {
181
+ readonly method: "GET";
182
+ readonly path: "/admin/media";
183
+ };
184
+ readonly uploadMedia: {
185
+ readonly method: "POST";
186
+ readonly path: "/admin/media";
187
+ };
188
+ readonly listBookings: {
189
+ readonly method: "GET";
190
+ readonly path: "/admin/bookings";
191
+ };
192
+ readonly approveBooking: {
193
+ readonly method: "POST";
194
+ readonly path: "/admin/bookings/{id}/approve";
195
+ };
196
+ readonly declineBooking: {
197
+ readonly method: "POST";
198
+ readonly path: "/admin/bookings/{id}/decline";
199
+ };
200
+ readonly cancelBooking: {
201
+ readonly method: "POST";
202
+ readonly path: "/admin/bookings/{id}/cancel";
203
+ };
204
+ };
205
+ export type DocKind = 'post' | 'page' | 'event';
206
+ /** React Native's FormData file shape, or a Blob/File on the web and Node */
207
+ export type UploadFile = Blob | {
208
+ uri: string;
209
+ name: string;
210
+ type: string;
211
+ };
212
+ export interface EditorRequestOptions {
213
+ signal?: AbortSignal;
214
+ }
215
+ export interface EditorClientOptions {
216
+ baseUrl: string;
217
+ /** a session from createEditorSession, or the two functions below */
218
+ session?: Pick<EditorSession, 'getAccessToken' | 'refresh'>;
219
+ getAccessToken?: () => string | null | Promise<string | null>;
220
+ /** return a fresh access token, or null when the person must sign in again */
221
+ refresh?: () => Promise<string | null>;
222
+ /** called when the API rejects the device (signed out, revoked, account changed) */
223
+ onAuthError?: (error: AcmsError) => void;
224
+ fetch?: typeof globalThis.fetch;
225
+ timeoutMs?: number;
226
+ }
227
+ export declare function createEditorClient(options: EditorClientOptions): {
228
+ me: (o?: EditorRequestOptions) => Promise<EditorMe>;
229
+ listDocuments: (q?: EditorListQuery, o?: EditorRequestOptions) => Promise<EditorDocumentList>;
230
+ createDocument: (input: EditorCreateInput, o?: EditorRequestOptions) => Promise<EditorDocument>;
231
+ getDocument: (kind: DocKind, id: string, o?: EditorRequestOptions) => Promise<EditorDocument>;
232
+ /** send the revision you loaded; 409 means someone saved first */
233
+ saveDocument: (kind: DocKind, id: string, input: EditorSaveInput, o?: EditorRequestOptions) => Promise<EditorDocument>;
234
+ publishDocument: (kind: DocKind, id: string, revision: string, o?: EditorRequestOptions) => Promise<EditorDocument>;
235
+ unpublishDocument: (kind: DocKind, id: string, revision: string, o?: EditorRequestOptions) => Promise<EditorDocument>;
236
+ scheduleDocument: (kind: DocKind, id: string, revision: string, at: string | Date, o?: EditorRequestOptions) => Promise<EditorDocument>;
237
+ discardDocument: (kind: DocKind, id: string, revision: string, o?: EditorRequestOptions) => Promise<EditorDocument>;
238
+ getDocumentTranslation: (kind: DocKind, id: string, locale: string, o?: EditorRequestOptions) => Promise<EditorDocumentTranslation>;
239
+ setDocumentTranslation: (kind: DocKind, id: string, locale: string, input: {
240
+ revision: string;
241
+ values: Record<string, string>;
242
+ }, o?: EditorRequestOptions) => Promise<EditorDocumentTranslation>;
243
+ reviewDocumentTranslation: (kind: DocKind, id: string, locale: string, input: {
244
+ revision: string;
245
+ paths?: string[];
246
+ }, o?: EditorRequestOptions) => Promise<EditorDocumentTranslation>;
247
+ draftDocumentTranslation: (kind: DocKind, id: string, locale: string, input: {
248
+ revision: string;
249
+ paths?: string[];
250
+ includeReviewed?: boolean;
251
+ }, o?: EditorRequestOptions) => Promise<EditorDocumentTranslation>;
252
+ listSections: (q?: EditorSectionsQuery, o?: EditorRequestOptions) => Promise<EditorSections>;
253
+ updateSection: (pageId: string, sectionId: string, input: {
254
+ revision: number;
255
+ data: Record<string, unknown>;
256
+ }, o?: EditorRequestOptions) => Promise<EditorSectionResult>;
257
+ publishSection: (pageId: string, sectionId: string, revision: number, o?: EditorRequestOptions) => Promise<EditorRevision>;
258
+ getSectionTranslation: (pageId: string, sectionId: string, locale: string, o?: EditorRequestOptions) => Promise<EditorSectionTranslation>;
259
+ setSectionTranslation: (pageId: string, sectionId: string, locale: string, input: {
260
+ revision: number;
261
+ values: Record<string, string>;
262
+ }, o?: EditorRequestOptions) => Promise<EditorSectionTranslation>;
263
+ reviewSectionTranslation: (pageId: string, sectionId: string, locale: string, input: {
264
+ revision: number;
265
+ paths?: string[];
266
+ }, o?: EditorRequestOptions) => Promise<EditorSectionTranslation>;
267
+ draftSectionTranslation: (pageId: string, sectionId: string, locale: string, input: {
268
+ revision: number;
269
+ paths?: string[];
270
+ includeReviewed?: boolean;
271
+ }, o?: EditorRequestOptions) => Promise<EditorSectionTranslation>;
272
+ listMedia: (q?: EditorMediaQuery, o?: EditorRequestOptions) => Promise<EditorMediaList>;
273
+ /** compress images on the device first (same limits as the admin: 30 MB images, 25 MB documents) */
274
+ uploadMedia: (file: UploadFile, meta?: {
275
+ name?: string;
276
+ alt?: string;
277
+ width?: number;
278
+ height?: number;
279
+ }, o?: EditorRequestOptions) => Promise<EditorMedia>;
280
+ listBookings: (q?: EditorBookingsQuery, o?: EditorRequestOptions) => Promise<EditorBookingList>;
281
+ approveBooking: (id: string, input?: {
282
+ message?: string;
283
+ }, o?: EditorRequestOptions) => Promise<EditorBooking>;
284
+ declineBooking: (id: string, input?: {
285
+ message?: string;
286
+ }, o?: EditorRequestOptions) => Promise<EditorBooking>;
287
+ cancelBooking: (id: string, input?: {
288
+ message?: string;
289
+ }, o?: EditorRequestOptions) => Promise<EditorBooking>;
290
+ };
291
+ export type EditorClient = ReturnType<typeof createEditorClient>;