@solutiondesk/events 0.0.0 → 1.0.0-69d9f69

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 wemogy GmbH
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,45 +1,196 @@
1
1
  # @solutiondesk/events
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Typed client for [SolutionDesk](https://solutiondesk.io) event pages. Show your workspace's events on your own website, build your own event page and registration form, show event announcements, and register signed-in users from your server.
4
+
5
+ - No dependencies, ESM and CommonJS, full TypeScript types.
6
+ - Runs wherever `fetch` does: Node.js 18+, Deno, Bun, edge runtimes and browsers.
7
+ - Reading needs no key: only published events are ever returned.
8
+
9
+ ```bash
10
+ npm install @solutiondesk/events
11
+ ```
12
+
13
+ The guide [Events on your website](https://help.solutiondesk.io/docs/team/events-embedding) explains the API behind this package and where to set it up in SolutionDesk; the [Host API](https://help.solutiondesk.io/docs/team/host-api) covers the calls that need an API key.
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { createEventsClient } from "@solutiondesk/events";
19
+
20
+ const events = createEventsClient({ workspace: "acme", lang: "en" });
21
+
22
+ const { events: upcoming } = await events.listEvents();
23
+ const { event } = await events.getEvent("kundentag-2026");
24
+ ```
25
+
26
+ `workspace` is your workspace's slug, as in the app's URL.
27
+
28
+ ## What you can do
29
+
30
+ | Method | API | Returns |
31
+ | --- | --- | --- |
32
+ | `listEvents({ when, lang })` | `GET /v1/event-pages/{workspace}` | Published events that haven't ended, soonest first; `when: "past"` for the ended ones, latest first. At most 100. Invite-only events are never listed. |
33
+ | `getEvent(slug, { invite, lang })` | `GET /v1/event-pages/{workspace}/{event}` | Everything on the event page: texts, times, location, images, programme (`sessions`), the registration `form` and whether registration is open. |
34
+ | `register(slug, input)` | `POST /v1/event-pages/{workspace}/{event}/registrations` | A registration as the public form sends it; the person confirms by email. |
35
+ | `getAnnouncements({ event, visitor, lang })` | `GET /v1/event-pages/{workspace}/announcement` | The event announcements for your website (bar or window), soonest event first. |
36
+ | `dismissAnnouncement({ event, visitor, round })` | `POST /v1/event-pages/{workspace}/announcement/dismiss` | Remembers that a signed-in visitor closed an announcement. |
37
+ | `imageUrl(url, width)` | | An event image at the next offered width up (320, 640, 960, 1280, 1920). |
38
+
39
+ An unknown workspace, a workspace without events and an unpublished event all answer `404`. Times are ISO 8601 in UTC; show them in the event's `timeZone`. `description` is safe HTML (paragraphs, headings, lists, links, emphasis), `descriptionText` the same as plain text. Online events never expose their link here — it's sent after registering.
40
+
41
+ ## Example: event page with registration (Next.js App Router)
42
+
43
+ Render the event on the server:
44
+
45
+ ```tsx
46
+ // app/events/[slug]/page.tsx
47
+ import { createEventsClient, SolutionDeskError } from "@solutiondesk/events";
48
+ import { notFound } from "next/navigation";
49
+ import { RegistrationForm } from "./registration-form";
50
+
51
+ const events = createEventsClient({ workspace: "acme", lang: "en" });
52
+
53
+ export default async function EventPage({ params }: { params: Promise<{ slug: string }> }) {
54
+ const { slug } = await params;
55
+ try {
56
+ const { event } = await events.getEvent(slug, {}, { fetchOptions: { next: { revalidate: 60 } } });
57
+ return (
58
+ <main lang={event.locale}>
59
+ <h1>{event.title}</h1>
60
+ {event.description && <div dangerouslySetInnerHTML={{ __html: event.description }} />}
61
+ {event.registration.open ? (
62
+ <RegistrationForm slug={event.slug} form={event.form} locale={event.locale} />
63
+ ) : (
64
+ <p>Registration is {event.registration.reason}.</p>
65
+ )}
66
+ </main>
67
+ );
68
+ } catch (error) {
69
+ if (error instanceof SolutionDeskError && error.isNotFound) notFound();
70
+ throw error;
71
+ }
72
+ }
73
+ ```
74
+
75
+ and register from the visitor's browser, with the two anti-bot fields:
76
+
77
+ ```tsx
78
+ // app/events/[slug]/registration-form.tsx
79
+ "use client";
80
+ import { createEventsClient, type FormDefinition, SolutionDeskError } from "@solutiondesk/events";
81
+ import { useRef, useState } from "react";
82
+
83
+ const events = createEventsClient({ workspace: "acme" });
84
+
85
+ export function RegistrationForm(props: { slug: string; form: FormDefinition; locale: string }) {
86
+ const openedAt = useRef(Date.now());
87
+ const [message, setMessage] = useState("");
88
+
89
+ async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
90
+ e.preventDefault();
91
+ const data = new FormData(e.currentTarget);
92
+ try {
93
+ await events.register(props.slug, {
94
+ firstName: String(data.get("firstName")),
95
+ lastName: String(data.get("lastName")),
96
+ email: String(data.get("email")),
97
+ answers: {}, // your questions from props.form.fields, keyed by field id
98
+ locale: props.locale,
99
+ website: String(data.get("website") ?? ""), // honeypot: hidden, must stay empty
100
+ elapsedMs: Date.now() - openedAt.current, // under 1.5 s counts as a bot
101
+ });
102
+ setMessage("Check your email to confirm.");
103
+ } catch (error) {
104
+ setMessage(error instanceof SolutionDeskError ? error.message : "Please try again.");
105
+ }
106
+ }
107
+
108
+ return (
109
+ <form onSubmit={onSubmit}>
110
+ <input name="firstName" required />
111
+ <input name="lastName" required />
112
+ <input name="email" type="email" required />
113
+ {/* Hidden from people; bots fill it in. */}
114
+ <input name="website" tabIndex={-1} autoComplete="off" style={{ display: "none" }} />
115
+ <button type="submit">Register</button>
116
+ <p aria-live="polite">{message}</p>
117
+ </form>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ### Registration rules
123
+
124
+ - **Where from:** browsers can register only from the websites listed under **Event settings → Your websites** (CORS); other websites can read but not register. A request from your server works too, but all of them then count against your server's IP address (see limits).
125
+ - **Anti-bot fields:** `website` is a honeypot, the value of a hidden input people never see, so it must stay empty. `elapsedMs` is how long the form was open. A filled honeypot or under 1500 ms is dropped silently: the answer is still `check_email`, nothing is stored. Leave `elapsedMs` out if you don't measure it.
126
+ - **The answer** is always `{ result: "check_email" }`: the person confirms by email. Whether the address was new or already registered stays private.
127
+ - **Answers** are keyed by the form field `id`: text for `text`, `textarea`, `email` and `phone`, a number for `number`, `YYYY-MM-DD` for `date`, the option `id` for `select`, a list of option ids for `multiselect`, a list of session ids for `sessions`, `true` for a ticked `checkbox`, `consent` or `newsletter`. Send `company` / `jobTitle` when the form asks for them (`askCompany`, `askJobTitle`); they are required when `companyRequired` / `jobTitleRequired` say so.
128
+ - **Invitations:** for an invitation link, pass its token as `getEvent(slug, { invite })` and as `invite` in the registration.
129
+ - **Wrong input** rejects with a `SolutionDeskError` with status `400`, `message` and `fields` (field id → message), ready to show next to the inputs.
130
+ - **Limits:** reading 120 requests a minute per IP address; registering 10 in 10 minutes per IP address and 3 an hour per email address. Over the limit: `429`.
131
+
132
+ ## Registering signed-in users from your server (API key)
133
+
134
+ When someone is signed in to your product, your server can register them directly. You vouch for the email, so there is no confirmation mail and the registration is placed at once (with the same rules: access, capacity, waiting list, required answers).
135
+
136
+ ```ts
137
+ // Server only — never ship the API key to a browser.
138
+ import { createEventsHostClient } from "@solutiondesk/events";
139
+
140
+ const host = createEventsHostClient({ apiKey: process.env.SOLUTIONDESK_API_KEY! });
141
+
142
+ const { result } = await host.register("kundentag-2026", {
143
+ firstName: "Ada",
144
+ lastName: "Lovelace",
145
+ email: "ada@example.com",
146
+ answers: { diet: "vegetarian" },
147
+ locale: "en",
148
+ });
149
+ // result: "confirmed" | "pending_approval" | "waitlisted"
150
+ ```
151
+
152
+ This calls `POST /v1/integration/events/{event}/registrations` with `Authorization: Bearer <apiKey>`. Create the key in SolutionDesk under **Settings → API keys** (the raw key is shown once). The person gets a mail with a link to change or cancel.
153
+
154
+ ## Announcements on your website
155
+
156
+ ```ts
157
+ const { announcements } = await events.getAnnouncements({ lang: "en" });
158
+ ```
159
+
160
+ Each announcement has a `style` (`bar` or `modal`), texts, the button's `url` and a `round`. To keep an announcement closed on all of a signed-in user's devices, send `visitor`: a SHA-256 hex digest of your user id. Answers then carry `dismissed`, and `dismissAnnouncement({ event, visitor, round })` stores a close.
161
+
162
+ ## Languages
163
+
164
+ Pass `lang` to the client (default for every call) or per call (any of the event's languages). Without it, the request's `Accept-Language` header decides (forward your visitor's with `headers`), then the event's main language. Every event carries `locale`, the language of its texts, and `languages`, everything it is offered in (main first, e.g. `[{ code: "en", label: "English" }, { code: "de", label: "Deutsch" }]`). Send `locale` with a registration and the person's mails come in that language.
165
+
166
+ ## Images
167
+
168
+ `coverImage` and `images` are public addresses cached for a year, with a ready `srcset`, `width`, `height` and `alt`. `imageUrl(url, width)` gives the WebP at the next offered width up, for example for a Next.js custom image loader.
169
+
170
+ ## Caching
4
171
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
172
+ The event endpoints send no `ETag`, so `etagCache` has no effect here. Cache with your framework instead: every method takes `{ fetchOptions }` as its last argument, which goes to `fetch` unchanged (e.g. Next.js `{ next: { revalidate: 60 } }`).
6
173
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
174
+ ## Errors
8
175
 
9
- ## Purpose
176
+ Every failed request rejects with a `SolutionDeskError` with `status` (`0` when no answer arrived), `message` (the API's text), `body`, `url` and, on a `400`, `fields`. `isNotFound` and `isRateLimited` are shortcuts.
177
+
178
+ ## Options
10
179
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@solutiondesk/events`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
180
+ ```ts
181
+ createEventsClient({
182
+ workspace: "acme", // required
183
+ lang: "en",
184
+ baseUrl: "https://api.solutiondesk.io",
185
+ fetch: customFetch,
186
+ headers: { "accept-language": visitorLanguages },
187
+ });
188
+ ```
189
+
190
+ Every method also takes request options as its last argument: `{ signal, headers, fetchOptions }`.
15
191
 
16
- ## What is OIDC Trusted Publishing?
192
+ `verifyWebhookSignature` and `SolutionDeskError` are exported here too, the same as in `@solutiondesk/blog` and `@solutiondesk/legal`.
17
193
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
194
+ ## License
19
195
 
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**
196
+ MIT © wemogy GmbH
package/dist/index.cjs ADDED
@@ -0,0 +1,326 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+
7
+ // ../sdk-core/src/http.ts
8
+ var DEFAULT_BASE_URL = "https://api.solutiondesk.io";
9
+ var SolutionDeskError = class extends Error {
10
+ constructor(message, init) {
11
+ super(message);
12
+ /** The HTTP status; `0` when no answer arrived (network error, aborted). */
13
+ __publicField(this, "status");
14
+ /** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
15
+ __publicField(this, "body");
16
+ /** The requested URL. */
17
+ __publicField(this, "url");
18
+ /** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
19
+ __publicField(this, "fields");
20
+ this.name = "SolutionDeskError";
21
+ this.status = init.status;
22
+ this.body = init.body ?? null;
23
+ this.url = init.url;
24
+ this.fields = fieldsOf(init.body);
25
+ if (init.cause !== void 0) {
26
+ Object.defineProperty(this, "cause", { value: init.cause, enumerable: false });
27
+ }
28
+ }
29
+ /** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
30
+ get isNotFound() {
31
+ return this.status === 404;
32
+ }
33
+ /** `429`: over the rate limit; try again later. */
34
+ get isRateLimited() {
35
+ return this.status === 429;
36
+ }
37
+ };
38
+ function fieldsOf(body) {
39
+ if (!body || typeof body !== "object") return {};
40
+ const fields = body.fields;
41
+ if (!fields || typeof fields !== "object") return {};
42
+ const out = {};
43
+ for (const [key, value] of Object.entries(fields)) {
44
+ if (typeof value === "string") out[key] = value;
45
+ }
46
+ return out;
47
+ }
48
+ function messageOf(body, status) {
49
+ if (body && typeof body === "object") {
50
+ const { error, message, statusCode } = body;
51
+ if (typeof statusCode === "number" && typeof message === "string" && message) return message;
52
+ if (typeof error === "string" && error) return error;
53
+ if (typeof message === "string" && message) return message;
54
+ if (Array.isArray(message) && typeof message[0] === "string") return message[0];
55
+ }
56
+ if (typeof body === "string" && body.trim() && body.length <= 300) return body.trim();
57
+ return `SolutionDesk answered ${status}.`;
58
+ }
59
+ function createMemoryEtagCache(maxEntries = 500) {
60
+ const entries = /* @__PURE__ */ new Map();
61
+ return {
62
+ get: (key) => entries.get(key),
63
+ set(key, value) {
64
+ entries.delete(key);
65
+ entries.set(key, value);
66
+ if (entries.size > maxEntries) {
67
+ const oldest = entries.keys().next().value;
68
+ if (oldest !== void 0) entries.delete(oldest);
69
+ }
70
+ }
71
+ };
72
+ }
73
+ function queryString(query) {
74
+ if (!query) return "";
75
+ const parts = [];
76
+ for (const [key, value] of Object.entries(query)) {
77
+ if (value === void 0 || value === null || value === "") continue;
78
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
79
+ }
80
+ return parts.length ? `?${parts.join("&")}` : "";
81
+ }
82
+ var seg = (value) => encodeURIComponent(String(value));
83
+ function parseBody(text) {
84
+ if (!text) return null;
85
+ try {
86
+ return JSON.parse(text);
87
+ } catch {
88
+ return text;
89
+ }
90
+ }
91
+ function createHttp(options = {}) {
92
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
93
+ const fetchImpl = options.fetch ?? globalThis.fetch;
94
+ const cache = options.etagCache === true ? createMemoryEtagCache() : options.etagCache ? options.etagCache : void 0;
95
+ const url = (path, query) => `${baseUrl}/v1${path}${queryString(query)}`;
96
+ async function requestWithMeta(req) {
97
+ if (!fetchImpl) {
98
+ throw new Error("No global fetch available: pass `fetch` to the client.");
99
+ }
100
+ const target = url(req.path, req.query);
101
+ const headers = {
102
+ accept: req.accept ?? "application/json",
103
+ ...options.headers,
104
+ ...req.options?.headers
105
+ };
106
+ if (req.body !== void 0) headers["content-type"] = "application/json";
107
+ if (req.apiKey) headers.authorization = `Bearer ${req.apiKey}`;
108
+ const cacheKey = req.method === "GET" && !req.apiKey ? target : void 0;
109
+ const cached = cacheKey && cache ? cache.get(cacheKey) : void 0;
110
+ if (cached && !Object.keys(headers).some((h) => h.toLowerCase() === "if-none-match")) {
111
+ headers["if-none-match"] = cached.etag;
112
+ }
113
+ const init = {
114
+ ...req.options?.fetchOptions,
115
+ method: req.method,
116
+ headers
117
+ };
118
+ if (req.body !== void 0) init.body = JSON.stringify(req.body);
119
+ if (req.options?.signal !== void 0) init.signal = req.options.signal;
120
+ let response;
121
+ try {
122
+ response = await fetchImpl(target, init);
123
+ } catch (cause) {
124
+ const reason = cause instanceof Error ? cause.message : String(cause);
125
+ throw new SolutionDeskError(`The request to SolutionDesk failed: ${reason}`, {
126
+ status: 0,
127
+ url: target,
128
+ cause
129
+ });
130
+ }
131
+ const finalUrl = response.url;
132
+ const meta = {
133
+ status: response.status,
134
+ finalUrl: typeof finalUrl === "string" && finalUrl ? finalUrl : null
135
+ };
136
+ if (response.status === 304 && cached) {
137
+ return { data: cached.body, ...meta };
138
+ }
139
+ const text = await response.text();
140
+ const body = response.ok && req.responseType === "text" ? text : parseBody(text);
141
+ if (!response.ok) {
142
+ throw new SolutionDeskError(messageOf(body, response.status), {
143
+ status: response.status,
144
+ body,
145
+ url: target
146
+ });
147
+ }
148
+ const etag = response.headers.get("etag");
149
+ if (cacheKey && cache && etag) cache.set(cacheKey, { etag, body });
150
+ return { data: body, ...meta };
151
+ }
152
+ return {
153
+ baseUrl,
154
+ url,
155
+ requestWithMeta,
156
+ request: async (req) => (await requestWithMeta(req)).data
157
+ };
158
+ }
159
+ function required(value, name) {
160
+ if (typeof value !== "string" || !value.trim()) {
161
+ throw new TypeError(`SolutionDesk: \`${name}\` is required.`);
162
+ }
163
+ return value;
164
+ }
165
+
166
+ // ../sdk-core/src/images.ts
167
+ var IMAGE_WIDTHS = [320, 640, 960, 1280, 1920];
168
+ function imageWidthFor(width) {
169
+ return IMAGE_WIDTHS.find((w) => w >= width) ?? 1920;
170
+ }
171
+ function imageUrl(url, width) {
172
+ if (width === void 0) return url;
173
+ const base = url.replace(/([?&])w=\d+(&|$)/, (_m, sep, end) => end ? sep : "");
174
+ const join = base.includes("?") ? /[?&]$/.test(base) ? "" : "&" : "?";
175
+ return `${base}${join}w=${imageWidthFor(width)}`;
176
+ }
177
+
178
+ // ../sdk-core/src/webhook.ts
179
+ var SIGNATURE_HEADER = "x-solutiondesk-signature";
180
+ var EVENT_HEADER = "x-solutiondesk-event";
181
+ function encoder() {
182
+ const Encoder = globalThis.TextEncoder;
183
+ if (!Encoder) throw new Error("TextEncoder is not available in this runtime.");
184
+ return new Encoder();
185
+ }
186
+ function bytesOf(body) {
187
+ if (typeof body === "string") return encoder().encode(body);
188
+ return body instanceof Uint8Array ? body : new Uint8Array(body);
189
+ }
190
+ function webhookBodyText(body) {
191
+ if (typeof body === "string") return body;
192
+ const Decoder = globalThis.TextDecoder;
193
+ if (!Decoder) throw new Error("TextDecoder is not available in this runtime.");
194
+ return new Decoder().decode(bytesOf(body));
195
+ }
196
+ function hex(buffer) {
197
+ let out = "";
198
+ for (const byte of new Uint8Array(buffer)) out += byte.toString(16).padStart(2, "0");
199
+ return out;
200
+ }
201
+ function constantTimeEqual(a, b) {
202
+ if (a.length !== b.length) return false;
203
+ let diff = 0;
204
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
205
+ return diff === 0;
206
+ }
207
+ function parseHeader(header) {
208
+ let t = Number.NaN;
209
+ const signatures = [];
210
+ for (const part of header.split(",")) {
211
+ const index = part.indexOf("=");
212
+ if (index < 0) continue;
213
+ const key = part.slice(0, index).trim();
214
+ const value = part.slice(index + 1).trim();
215
+ if (key === "t" && /^\d{1,12}$/.test(value)) t = Number(value);
216
+ else if (key === "v1" && /^[0-9a-f]{64}$/i.test(value)) signatures.push(value.toLowerCase());
217
+ }
218
+ return Number.isFinite(t) && signatures.length > 0 ? { t, signatures } : null;
219
+ }
220
+ async function verifyWebhookSignature(secret, rawBody, header, options = {}) {
221
+ if (!secret || !header || header.length > 1e3) return false;
222
+ const parsed = parseHeader(header);
223
+ if (!parsed) return false;
224
+ const now = options.now instanceof Date ? options.now.getTime() : options.now ?? Date.now();
225
+ const tolerance = options.toleranceSeconds ?? 300;
226
+ if (Math.abs(now / 1e3 - parsed.t) > tolerance) return false;
227
+ const subtle = options.crypto?.subtle ?? globalThis.crypto?.subtle;
228
+ if (!subtle) {
229
+ throw new Error(
230
+ "Web Crypto is not available: pass `crypto` (on Node 18: require('node:crypto').webcrypto)."
231
+ );
232
+ }
233
+ const text = encoder();
234
+ const key = await subtle.importKey(
235
+ "raw",
236
+ text.encode(secret),
237
+ { name: "HMAC", hash: "SHA-256" },
238
+ false,
239
+ ["sign"]
240
+ );
241
+ const prefix = text.encode(`${parsed.t}.`);
242
+ const body = bytesOf(rawBody);
243
+ const signed = new Uint8Array(prefix.length + body.length);
244
+ signed.set(prefix, 0);
245
+ signed.set(body, prefix.length);
246
+ const expected = hex(await subtle.sign("HMAC", key, signed));
247
+ let ok = false;
248
+ for (const signature of parsed.signatures) ok = constantTimeEqual(expected, signature) || ok;
249
+ return ok;
250
+ }
251
+ async function parseWebhook(secret, rawBody, header, options = {}) {
252
+ if (!await verifyWebhookSignature(secret, rawBody, header, options)) return null;
253
+ try {
254
+ return JSON.parse(webhookBodyText(rawBody));
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+
260
+ // src/client.ts
261
+ function createEventsClient(options) {
262
+ const workspace = required(options.workspace, "workspace");
263
+ const http = createHttp(options);
264
+ const base = `/event-pages/${seg(workspace)}`;
265
+ const langOf = (params) => params?.lang ?? options.lang;
266
+ return {
267
+ workspace,
268
+ listEvents: async (params, requestOptions) => http.request({
269
+ method: "GET",
270
+ path: base,
271
+ query: { when: params?.when, lang: langOf(params) },
272
+ options: requestOptions
273
+ }),
274
+ getEvent: async (slug, params, requestOptions) => http.request({
275
+ method: "GET",
276
+ path: `${base}/${seg(required(slug, "slug"))}`,
277
+ query: { invite: params?.invite, lang: langOf(params) },
278
+ options: requestOptions
279
+ }),
280
+ getAnnouncements: async (params, requestOptions) => http.request({
281
+ method: "GET",
282
+ path: `${base}/announcement`,
283
+ query: { event: params?.event, visitor: params?.visitor, lang: langOf(params) },
284
+ options: requestOptions
285
+ }),
286
+ dismissAnnouncement: async (input, requestOptions) => http.request({
287
+ method: "POST",
288
+ path: `${base}/announcement/dismiss`,
289
+ body: { event: input.event, visitor: input.visitor, round: input.round },
290
+ options: requestOptions
291
+ }),
292
+ register: async (slug, input, requestOptions) => http.request({
293
+ method: "POST",
294
+ path: `${base}/${seg(required(slug, "slug"))}/registrations`,
295
+ body: input,
296
+ options: requestOptions
297
+ }),
298
+ imageUrl
299
+ };
300
+ }
301
+ function createEventsHostClient(options) {
302
+ const apiKey = required(options.apiKey, "apiKey");
303
+ const http = createHttp({ ...options, etagCache: false });
304
+ return {
305
+ register: async (slug, input, requestOptions) => http.request({
306
+ method: "POST",
307
+ path: `/integration/events/${seg(required(slug, "slug"))}/registrations`,
308
+ body: input,
309
+ apiKey,
310
+ options: requestOptions
311
+ })
312
+ };
313
+ }
314
+
315
+ exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
316
+ exports.EVENT_HEADER = EVENT_HEADER;
317
+ exports.IMAGE_WIDTHS = IMAGE_WIDTHS;
318
+ exports.SIGNATURE_HEADER = SIGNATURE_HEADER;
319
+ exports.SolutionDeskError = SolutionDeskError;
320
+ exports.createEventsClient = createEventsClient;
321
+ exports.createEventsHostClient = createEventsHostClient;
322
+ exports.createMemoryEtagCache = createMemoryEtagCache;
323
+ exports.imageUrl = imageUrl;
324
+ exports.imageWidthFor = imageWidthFor;
325
+ exports.parseWebhook = parseWebhook;
326
+ exports.verifyWebhookSignature = verifyWebhookSignature;