@solutiondesk/events 0.0.0-stage → 1.0.0-1edea54

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.
@@ -0,0 +1,516 @@
1
+ /**
2
+ * The HTTP part shared by the public SolutionDesk SDKs. Runs on any runtime with
3
+ * a WHATWG `fetch` (Node ≥ 18, Deno, Bun, edge runtimes, browsers) — and on
4
+ * purpose names no DOM or Node type, so the published `.d.ts` files need neither
5
+ * `lib: ["dom"]` nor `@types/node`.
6
+ */
7
+ /** The production API. */
8
+ declare const DEFAULT_BASE_URL = "https://api.solutiondesk.io";
9
+ /** The response parts the SDK reads — what every `fetch` implementation returns. */
10
+ interface FetchResponseLike {
11
+ readonly status: number;
12
+ readonly ok: boolean;
13
+ readonly headers: {
14
+ get(name: string): string | null;
15
+ };
16
+ text(): Promise<string>;
17
+ }
18
+ /** What the SDK passes to `fetch` (plus whatever you set in `fetchOptions`). */
19
+ interface FetchInitLike {
20
+ method: string;
21
+ headers: Record<string, string>;
22
+ body?: string;
23
+ }
24
+ /**
25
+ * A `fetch` implementation. The global `fetch` fits, so do `undici`, `node-fetch`
26
+ * and wrappers (e.g. one that adds tracing). Declared bivariantly so a fetch with
27
+ * a richer `init` type is accepted.
28
+ */
29
+ type FetchLike = {
30
+ bivarianceHack(input: string, init?: FetchInitLike): Promise<FetchResponseLike>;
31
+ }["bivarianceHack"];
32
+ /** A response kept for `If-None-Match` revalidation. */
33
+ interface CachedResponse {
34
+ etag: string;
35
+ body: unknown;
36
+ }
37
+ /**
38
+ * Where the client keeps answers for ETag revalidation. Any `Map` fits; bring
39
+ * your own to share it between clients or to bound it differently.
40
+ */
41
+ interface EtagCache {
42
+ get(key: string): CachedResponse | undefined;
43
+ set(key: string, value: CachedResponse): unknown;
44
+ }
45
+ /** Options every client factory takes. */
46
+ interface BaseClientOptions {
47
+ /** The API origin, without `/v1`. Defaults to `https://api.solutiondesk.io`. */
48
+ baseUrl?: string;
49
+ /** A `fetch` implementation; defaults to the global one. */
50
+ fetch?: FetchLike;
51
+ /** Headers sent with every request (e.g. `accept-language` forwarded from your visitor). */
52
+ headers?: Record<string, string>;
53
+ /**
54
+ * Revalidate with `If-None-Match`: the client remembers each answer's `ETag`
55
+ * and, when the API answers `304 Not Modified`, returns the remembered body.
56
+ * `true` keeps up to 500 answers in memory; or pass your own {@link EtagCache}.
57
+ * Off by default (your framework's fetch cache may already do this).
58
+ */
59
+ etagCache?: boolean | EtagCache;
60
+ }
61
+ /** Per-call options. */
62
+ interface RequestOptions {
63
+ /** Cancels the request (an `AbortSignal`). */
64
+ signal?: unknown;
65
+ /** Extra headers for this request. */
66
+ headers?: Record<string, string>;
67
+ /**
68
+ * Extra `fetch` options for this request, passed through unchanged — e.g.
69
+ * Next.js `{ next: { revalidate: 60, tags: ["legal"] } }` or `{ cache: "no-store" }`.
70
+ */
71
+ fetchOptions?: Record<string, unknown>;
72
+ }
73
+ /** Any non-2xx answer of the API (and a failed network request, with `status` 0). */
74
+ declare class SolutionDeskError extends Error {
75
+ /** The HTTP status; `0` when no answer arrived (network error, aborted). */
76
+ readonly status: number;
77
+ /** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
78
+ readonly body: unknown;
79
+ /** The requested URL. */
80
+ readonly url: string;
81
+ /** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
82
+ readonly fields: Record<string, string>;
83
+ constructor(message: string, init: {
84
+ status: number;
85
+ body?: unknown;
86
+ url: string;
87
+ cause?: unknown;
88
+ });
89
+ /** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
90
+ get isNotFound(): boolean;
91
+ /** `429`: over the rate limit; try again later. */
92
+ get isRateLimited(): boolean;
93
+ }
94
+ /** A bounded in-memory {@link EtagCache} (oldest entry dropped first). */
95
+ declare function createMemoryEtagCache(maxEntries?: number): EtagCache;
96
+
97
+ /**
98
+ * Public blog and event images come in a fixed set of widths (`<url>?w=<width>`,
99
+ * WebP, never wider than the original); the API refuses any other width.
100
+ */
101
+ declare const IMAGE_WIDTHS: readonly [320, 640, 960, 1280, 1920];
102
+ type ImageWidth = (typeof IMAGE_WIDTHS)[number];
103
+ /** The smallest offered width that is at least `width` (1920 for anything wider). */
104
+ declare function imageWidthFor(width: number): ImageWidth;
105
+ /**
106
+ * The address of an image at (at least) `width` pixels — e.g. for a Next.js
107
+ * custom image loader: `({ src, width }) => imageUrl(src, width)`. Without a
108
+ * width the original address is returned.
109
+ */
110
+ declare function imageUrl(url: string, width?: number): string;
111
+
112
+ /**
113
+ * Webhook signature check with Web Crypto only — the same code runs in Node,
114
+ * Deno, Bun, edge runtimes and browsers. The header SolutionDesk sends is
115
+ * `X-SolutionDesk-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`.
116
+ */
117
+ /** The header that carries the signature. */
118
+ declare const SIGNATURE_HEADER = "x-solutiondesk-signature";
119
+ /** The header that names the event (also in the body as `event`). */
120
+ declare const EVENT_HEADER = "x-solutiondesk-event";
121
+ interface VerifyWebhookOptions {
122
+ /** How old (or how far in the future) the signed time may be. Default 300 (5 minutes). */
123
+ toleranceSeconds?: number;
124
+ /** "Now" in milliseconds (or a `Date`) — for tests. Default `Date.now()`. */
125
+ now?: number | Date;
126
+ /**
127
+ * A Web Crypto implementation. Default `globalThis.crypto`. On Node 18 pass
128
+ * `require("node:crypto").webcrypto` (Node 19+ has it globally).
129
+ */
130
+ crypto?: {
131
+ subtle: object;
132
+ };
133
+ }
134
+ /** The raw request body, exactly as received. */
135
+ type WebhookBody = string | Uint8Array | ArrayBuffer;
136
+ /**
137
+ * Check a SolutionDesk webhook signature. `true` only when the header is well
138
+ * formed, its time lies within the tolerance and the HMAC matches — anything
139
+ * else (missing, tampered, expired, malformed) is `false`, never an exception.
140
+ *
141
+ * @param secret The webhook's signing secret (`whsec_…`), shown once in SolutionDesk.
142
+ * @param rawBody The request body exactly as received — not re-serialized JSON.
143
+ * @param header The `X-SolutionDesk-Signature` header.
144
+ */
145
+ declare function verifyWebhookSignature(secret: string, rawBody: WebhookBody, header: string | null | undefined, options?: VerifyWebhookOptions): Promise<boolean>;
146
+ /**
147
+ * Verify, then parse the body: the typed payload, or `null` when the signature
148
+ * doesn't check out (answer such a call with `400`/`401` and ignore it).
149
+ */
150
+ declare function parseWebhook<T>(secret: string, rawBody: WebhookBody, header: string | null | undefined, options?: VerifyWebhookOptions): Promise<T | null>;
151
+
152
+ /**
153
+ * The answers and inputs of the public event pages API
154
+ * (`/v1/event-pages/{workspace}/…`) and of the host registration route
155
+ * (`POST /v1/integration/events/{event}/registrations`). Written by hand so the
156
+ * package has no dependencies; a drift test in the SolutionDesk repository
157
+ * keeps them in line with the API's own contracts.
158
+ */
159
+ /** An event image (cover or gallery). Public, cached for a year; a changed image gets a new address. */
160
+ interface EventImageView {
161
+ id: string;
162
+ url: string;
163
+ /** Every web-ready width (WebP, `<url>?w=<width>`) for `<img srcset>`. */
164
+ srcset: string;
165
+ width: number | null;
166
+ height: number | null;
167
+ alt: string | null;
168
+ /** Who made it, shown small on the image. */
169
+ credit: string | null;
170
+ }
171
+ /** A talk as visitors see it. */
172
+ interface PublicEventSession {
173
+ id: string;
174
+ title: string;
175
+ description: string | null;
176
+ startsAt: string;
177
+ endsAt: string;
178
+ track: string | null;
179
+ speakers: {
180
+ name: string;
181
+ role: string | null;
182
+ bio: string | null;
183
+ }[];
184
+ /** May be picked in the form (published, selectable, not full). */
185
+ choosable: boolean;
186
+ /** Has a seat limit and no seat left. */
187
+ full: boolean;
188
+ }
189
+ /** A choice of a `select` / `multiselect` field. */
190
+ interface FormOption {
191
+ id: string;
192
+ label: string;
193
+ }
194
+ interface FormFieldBase {
195
+ /** The key of this field's value in `answers`. */
196
+ id: string;
197
+ label: string;
198
+ help?: string | undefined;
199
+ required: boolean;
200
+ }
201
+ /**
202
+ * One of the event's own form questions. The value to send in `answers[id]`:
203
+ * text for `text`/`textarea`/`email`/`phone`, a number for `number`, `YYYY-MM-DD`
204
+ * for `date`, the option `id` for `select`, a list of option ids for
205
+ * `multiselect`, a list of session ids for `sessions`, `true` for a ticked
206
+ * `checkbox`/`consent`/`newsletter`.
207
+ */
208
+ type FormField = (FormFieldBase & {
209
+ type: "text";
210
+ maxLength: number;
211
+ }) | (FormFieldBase & {
212
+ type: "textarea";
213
+ maxLength: number;
214
+ }) | (FormFieldBase & {
215
+ type: "email";
216
+ }) | (FormFieldBase & {
217
+ type: "phone";
218
+ }) | (FormFieldBase & {
219
+ type: "number";
220
+ min?: number | undefined;
221
+ max?: number | undefined;
222
+ }) | (FormFieldBase & {
223
+ type: "date";
224
+ }) | (FormFieldBase & {
225
+ type: "select";
226
+ options: FormOption[];
227
+ }) | (FormFieldBase & {
228
+ type: "multiselect";
229
+ options: FormOption[];
230
+ }) | (FormFieldBase & {
231
+ type: "checkbox";
232
+ })
233
+ /** A required box with its text and an optional link (privacy policy, photo consent …). */
234
+ | (FormFieldBase & {
235
+ type: "consent";
236
+ text: string;
237
+ linkUrl?: string | undefined;
238
+ required: true;
239
+ })
240
+ /** Pick talks from the programme (`sessions` with `choosable: true`). */
241
+ | (FormFieldBase & {
242
+ type: "sessions";
243
+ })
244
+ /** An optional, unticked box to subscribe to a newsletter (confirmed by its own mail). */
245
+ | (FormFieldBase & {
246
+ type: "newsletter";
247
+ listId: string;
248
+ required: false;
249
+ /** The list's consent text to show next to the box. */
250
+ text?: string | undefined;
251
+ });
252
+ type FormFieldType = FormField["type"];
253
+ /** The registration form. First name, last name and email are always asked. */
254
+ interface FormDefinition {
255
+ askCompany: boolean;
256
+ askJobTitle: boolean;
257
+ /** Only counts while `askCompany` is on. */
258
+ companyRequired: boolean;
259
+ /** Only counts while `askJobTitle` is on. */
260
+ jobTitleRequired: boolean;
261
+ fields: FormField[];
262
+ }
263
+ /** Whether a visitor can register right now, and if not why. */
264
+ type PublicRegistrationState = {
265
+ open: true;
266
+ waitlist: boolean;
267
+ } | {
268
+ open: false;
269
+ reason: "closed" | "invite_only" | "full" | "ended" | "cancelled";
270
+ };
271
+ /** The workspace an event belongs to. */
272
+ interface PublicEventWorkspace {
273
+ name: string;
274
+ slug: string;
275
+ logoUrl: string | null;
276
+ }
277
+ /** An event with everything its page shows. */
278
+ interface PublicEvent {
279
+ /** The language of the texts below (`lang`, else the visitor's, else the main one). */
280
+ locale: string;
281
+ /** The languages the event is offered in, main language first. */
282
+ languages: {
283
+ code: string;
284
+ label: string;
285
+ }[];
286
+ slug: string;
287
+ title: string;
288
+ /**
289
+ * Formatted text as safe HTML: paragraphs, headings, lists, links and
290
+ * emphasis — no styles, scripts or images. Style the tags yourself.
291
+ */
292
+ description: string | null;
293
+ /** The same as plain text, e.g. for previews and meta tags. */
294
+ descriptionText: string | null;
295
+ status: "published" | "registration_closed" | "ended" | "cancelled";
296
+ /** ISO times in UTC; show them in `timeZone`. */
297
+ startsAt: string;
298
+ endsAt: string;
299
+ timeZone: string;
300
+ /** Online events never expose their link here — it's sent after registering. */
301
+ location: {
302
+ kind: "onsite";
303
+ name: string | null;
304
+ address: string;
305
+ } | {
306
+ kind: "online";
307
+ };
308
+ access: "invite_only" | "open" | "open_with_approval";
309
+ /** The picture on top of the event; `null` when none is set. */
310
+ coverImage: EventImageView | null;
311
+ /** More pictures of the event, in the team's order. */
312
+ images: EventImageView[];
313
+ /** False for invite-only events whose programme is hidden from uninvited visitors. */
314
+ programmeVisible: boolean;
315
+ sessions: PublicEventSession[];
316
+ form: FormDefinition;
317
+ registration: PublicRegistrationState;
318
+ }
319
+ /** `GET /v1/event-pages/{workspace}/{event}[?invite=…]`. */
320
+ interface PublicEventPayload {
321
+ workspace: PublicEventWorkspace;
322
+ event: PublicEvent;
323
+ /** Present with a valid invitation token: the email is fixed to it. */
324
+ invitation: {
325
+ email: string;
326
+ name: string | null;
327
+ } | null;
328
+ }
329
+ /** An event in the list: the page's facts and cover, without programme, form and gallery. */
330
+ type PublicEventListItem = Omit<PublicEvent, "programmeVisible" | "sessions" | "form" | "images"> & {
331
+ /** The event page (on the workspace's events domain when it has one). */
332
+ url: string;
333
+ };
334
+ /** `GET /v1/event-pages/{workspace}[?when=upcoming|past]`. */
335
+ interface PublicEventsListPayload {
336
+ workspace: PublicEventWorkspace;
337
+ /** Upcoming: soonest first. Past: latest first. At most 100. */
338
+ events: PublicEventListItem[];
339
+ }
340
+ /** One event's notice for your own website (a bar on top or a window in the middle). */
341
+ interface PublicAnnouncement {
342
+ /** The event's slug. */
343
+ event: string;
344
+ /** The language of the texts. */
345
+ locale: string;
346
+ eventTitle: string;
347
+ startsAt: string;
348
+ endsAt: string;
349
+ timeZone: string;
350
+ style: "bar" | "modal";
351
+ title: string;
352
+ message: string | null;
353
+ buttonLabel: string;
354
+ /** The event's cover image; `null` when none is set. */
355
+ image: EventImageView | null;
356
+ /** Where the button leads (the event page unless the team set another page). */
357
+ url: string;
358
+ /** Bumped when the team shows it again to everyone; a closed round stays closed. */
359
+ round: number;
360
+ /** Only with `visitor`: that visitor already closed this round. */
361
+ dismissed?: boolean;
362
+ }
363
+ /** `GET /v1/event-pages/{workspace}/announcement`. */
364
+ interface PublicAnnouncementsPayload {
365
+ announcements: PublicAnnouncement[];
366
+ }
367
+ /** A value in `answers` (see {@link FormField} for which type each field takes). */
368
+ type RegistrationAnswer = string | number | boolean | string[];
369
+ /** `POST /v1/event-pages/{workspace}/{event}/registrations` — what the public form sends. */
370
+ interface PublicRegistrationInput {
371
+ firstName: string;
372
+ lastName: string;
373
+ email: string;
374
+ /** When the form asks for it (`askCompany`); required with `companyRequired`. */
375
+ company?: string;
376
+ /** When the form asks for it (`askJobTitle`); required with `jobTitleRequired`. */
377
+ jobTitle?: string;
378
+ /** Values of the event's own form fields, keyed by field `id`. */
379
+ answers?: Record<string, RegistrationAnswer>;
380
+ /** The invitation token from `?invite=`. */
381
+ invite?: string;
382
+ /** The language the person registered in (one of the event's `languages`); their mails use it. */
383
+ locale?: string;
384
+ /**
385
+ * Anti-bot honeypot: the value of a hidden "website" input that people never
386
+ * see. Must stay empty — a filled one is silently dropped (the answer still
387
+ * says `check_email`).
388
+ */
389
+ website?: string;
390
+ /**
391
+ * Anti-bot timer: milliseconds the form was open before sending. Under 1500
392
+ * is treated as a bot and silently dropped. Leave it out when you don't measure it.
393
+ */
394
+ elapsedMs?: number;
395
+ }
396
+ /**
397
+ * What happened. Public sign-ups always get `check_email` — whether the address
398
+ * was new, already registered or invalid stays private.
399
+ */
400
+ interface PublicRegistrationResult {
401
+ result: "check_email" | "confirmed" | "pending_approval" | "waitlisted";
402
+ }
403
+ /**
404
+ * `POST /v1/integration/events/{event}/registrations` — your server registers
405
+ * someone who is signed in to your product; you vouch for the email, so no
406
+ * confirmation mail is sent.
407
+ */
408
+ type HostRegistrationInput = Omit<PublicRegistrationInput, "website" | "elapsedMs" | "invite">;
409
+
410
+ interface EventsClientOptions extends BaseClientOptions {
411
+ /** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/event-pages/acme`. */
412
+ workspace: string;
413
+ /**
414
+ * The language to show events in by default (any of an event's `languages`).
415
+ * Without it the request's `Accept-Language` decides, then the event's main
416
+ * language. Override per call.
417
+ */
418
+ lang?: string;
419
+ }
420
+ interface LangParams {
421
+ /** The language to ask for; overrides the client's `lang`. */
422
+ lang?: string;
423
+ }
424
+ interface ListEventsParams extends LangParams {
425
+ /** `upcoming` (default): not ended yet, soonest first. `past`: ended, latest first. */
426
+ when?: "upcoming" | "past";
427
+ }
428
+ interface GetEventParams extends LangParams {
429
+ /** The invitation token from an invitation link (`?invite=`): fixes the email, shows a hidden programme. */
430
+ invite?: string;
431
+ }
432
+ interface AnnouncementParams extends LangParams {
433
+ /** Only this event's announcement (its slug). */
434
+ event?: string;
435
+ /**
436
+ * Your signed-in user as a SHA-256 hex digest (64 characters) of their id —
437
+ * answers then carry `dismissed` for announcements they closed on any device.
438
+ */
439
+ visitor?: string;
440
+ }
441
+ interface DismissAnnouncementInput {
442
+ /** The event's slug. */
443
+ event: string;
444
+ /** The same SHA-256 hex digest as in {@link AnnouncementParams.visitor}. */
445
+ visitor: string;
446
+ /** The announcement's `round`. */
447
+ round: number;
448
+ }
449
+ interface EventsClient {
450
+ /** The workspace this client reads. */
451
+ readonly workspace: string;
452
+ /**
453
+ * The workspace's published events (`GET /v1/event-pages/{workspace}`), at most
454
+ * 100. Invite-only events are never listed.
455
+ */
456
+ listEvents(params?: ListEventsParams, options?: RequestOptions): Promise<PublicEventsListPayload>;
457
+ /**
458
+ * Everything on an event's page (`GET /v1/event-pages/{workspace}/{event}`),
459
+ * including the registration form. Throws a `SolutionDeskError` with status
460
+ * 404 for an unknown or unpublished event.
461
+ */
462
+ getEvent(slug: string, params?: GetEventParams, options?: RequestOptions): Promise<PublicEventPayload>;
463
+ /** The events' announcements for your own website, soonest event first. */
464
+ getAnnouncements(params?: AnnouncementParams, options?: RequestOptions): Promise<PublicAnnouncementsPayload>;
465
+ /** Remember that a known visitor closed an announcement (this round). */
466
+ dismissAnnouncement(input: DismissAnnouncementInput, options?: RequestOptions): Promise<{
467
+ ok: true;
468
+ }>;
469
+ /**
470
+ * Register someone the way the public form does
471
+ * (`POST /v1/event-pages/{workspace}/{event}/registrations`). The person
472
+ * confirms by email; the answer is `{ result: "check_email" }` either way.
473
+ *
474
+ * From a browser this only works on the websites listed under Event
475
+ * settings → Your websites (CORS). Send the anti-bot fields `website`
476
+ * (honeypot, empty) and `elapsedMs`. A wrong answer throws a
477
+ * `SolutionDeskError` with status 400 and `fields` (field id → message).
478
+ * Limited to 10 registrations per IP address in 10 minutes and 3 an hour
479
+ * per email address.
480
+ */
481
+ register(slug: string, input: PublicRegistrationInput, options?: RequestOptions): Promise<PublicRegistrationResult>;
482
+ /** The address of an event image at (at least) `width` pixels: 320, 640, 960, 1280 or 1920. */
483
+ imageUrl(url: string, width?: number): string;
484
+ }
485
+ /**
486
+ * A client for the public event pages — no key, no login; only published
487
+ * events. Safe in the browser and on the server.
488
+ *
489
+ * @example
490
+ * const events = createEventsClient({ workspace: "acme", lang: "en" });
491
+ * const { events: upcoming } = await events.listEvents();
492
+ */
493
+ declare function createEventsClient(options: EventsClientOptions): EventsClient;
494
+ interface EventsHostClientOptions extends Omit<BaseClientOptions, "etagCache"> {
495
+ /** An API key of your workspace (Settings → API keys). Keep it on your server. */
496
+ apiKey: string;
497
+ }
498
+ interface EventsHostClient {
499
+ /**
500
+ * Register someone who is signed in to your product
501
+ * (`POST /v1/integration/events/{event}/registrations`). You vouch for the
502
+ * email, so the registration is placed at once — no confirmation mail —
503
+ * with the same rules otherwise: access, capacity, waiting list, required
504
+ * form answers. Answers `confirmed`, `pending_approval` or `waitlisted`; the
505
+ * person gets a mail with a link to change or cancel.
506
+ */
507
+ register(slug: string, input: HostRegistrationInput, options?: RequestOptions): Promise<PublicRegistrationResult>;
508
+ }
509
+ /**
510
+ * **Server only.** A client for the events part of the host API, authenticated
511
+ * with your workspace's API key (`Authorization: Bearer …`). Never ship the key
512
+ * to a browser.
513
+ */
514
+ declare function createEventsHostClient(options: EventsHostClientOptions): EventsHostClient;
515
+
516
+ export { type AnnouncementParams, type BaseClientOptions, type CachedResponse, DEFAULT_BASE_URL, type DismissAnnouncementInput, EVENT_HEADER, type EtagCache, type EventImageView, type EventsClient, type EventsClientOptions, type EventsHostClient, type EventsHostClientOptions, type FetchInitLike, type FetchLike, type FetchResponseLike, type FormDefinition, type FormField, type FormFieldType, type FormOption, type GetEventParams, type HostRegistrationInput, IMAGE_WIDTHS, type ImageWidth, type LangParams, type ListEventsParams, type PublicAnnouncement, type PublicAnnouncementsPayload, type PublicEvent, type PublicEventListItem, type PublicEventPayload, type PublicEventSession, type PublicEventWorkspace, type PublicEventsListPayload, type PublicRegistrationInput, type PublicRegistrationResult, type PublicRegistrationState, type RegistrationAnswer, type RequestOptions, SIGNATURE_HEADER, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createEventsClient, createEventsHostClient, createMemoryEtagCache, imageUrl, imageWidthFor, parseWebhook, verifyWebhookSignature };