@venue-family/sdk 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.
@@ -0,0 +1,21 @@
1
+ import type { VenueFamilyEmbedEvent } from './types.js';
2
+ /**
3
+ * Verify an incoming Venue Family webhook or conversion postback HMAC-SHA256 signature.
4
+ * Works in Node.js, Deno, Bun, and Web standard runtimes (Cloudflare Workers, Browsers).
5
+ */
6
+ export declare function verifyWebhookSignature(payload: string, signatureHeader: string, secret: string): Promise<boolean>;
7
+ export interface EmbedListenerOptions {
8
+ allowedOrigin?: string;
9
+ onResize?: (height: number) => void;
10
+ onTicketPurchase?: (event: Extract<VenueFamilyEmbedEvent, {
11
+ type: 'ticket-purchase-complete';
12
+ }>) => void;
13
+ onFormSubmitted?: (event: Extract<VenueFamilyEmbedEvent, {
14
+ type: 'form-submitted';
15
+ }>) => void;
16
+ onEvent?: (event: VenueFamilyEmbedEvent) => void;
17
+ }
18
+ /**
19
+ * Convenience helper to attach an iframe auto-resize and event listener to any web page.
20
+ */
21
+ export declare function attachEmbedListener(iframe: HTMLIFrameElement | string, options?: EmbedListenerOptions): () => void;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyWebhookSignature = verifyWebhookSignature;
4
+ exports.attachEmbedListener = attachEmbedListener;
5
+ /**
6
+ * Verify an incoming Venue Family webhook or conversion postback HMAC-SHA256 signature.
7
+ * Works in Node.js, Deno, Bun, and Web standard runtimes (Cloudflare Workers, Browsers).
8
+ */
9
+ async function verifyWebhookSignature(payload, signatureHeader, secret) {
10
+ if (!signatureHeader || !secret) {
11
+ return false;
12
+ }
13
+ const expectedPrefix = 'sha256=';
14
+ const provided = signatureHeader.startsWith(expectedPrefix)
15
+ ? signatureHeader.slice(expectedPrefix.length)
16
+ : signatureHeader;
17
+ // Standard Web Crypto API
18
+ const encoder = new TextEncoder();
19
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
20
+ const signatureBuffer = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
21
+ const hexHash = Array.from(new Uint8Array(signatureBuffer))
22
+ .map((b) => b.toString(16).padStart(2, '0'))
23
+ .join('');
24
+ return hexHash === provided;
25
+ }
26
+ /**
27
+ * Convenience helper to attach an iframe auto-resize and event listener to any web page.
28
+ */
29
+ function attachEmbedListener(iframe, options = {}) {
30
+ const allowedOrigin = options.allowedOrigin ?? 'https://venuefamily.com';
31
+ const handler = (event) => {
32
+ if (allowedOrigin !== '*' && event.origin !== allowedOrigin) {
33
+ return;
34
+ }
35
+ const data = event.data;
36
+ if (!data || typeof data !== 'object' || !('type' in data)) {
37
+ return;
38
+ }
39
+ const frameEl = typeof iframe === 'string' ? document.getElementById(iframe) : iframe;
40
+ if (data.type === 'resize' && frameEl && typeof data.height === 'number') {
41
+ frameEl.style.height = `${data.height}px`;
42
+ options.onResize?.(data.height);
43
+ }
44
+ if (data.type === 'scroll-into-view' && frameEl && typeof data.top === 'number') {
45
+ const rect = frameEl.getBoundingClientRect();
46
+ window.scrollTo({
47
+ top: window.scrollY + rect.top + data.top - 20,
48
+ behavior: 'smooth',
49
+ });
50
+ }
51
+ if (data.type === 'ticket-purchase-complete') {
52
+ options.onTicketPurchase?.(data);
53
+ }
54
+ if (data.type === 'form-submitted') {
55
+ options.onFormSubmitted?.(data);
56
+ }
57
+ options.onEvent?.(data);
58
+ };
59
+ window.addEventListener('message', handler);
60
+ return () => {
61
+ window.removeEventListener('message', handler);
62
+ };
63
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@venue-family/sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official TypeScript & JavaScript SDK for Venue Family API, events, and dynamic embeds.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "test": "vitest run"
15
+ },
16
+ "keywords": [
17
+ "venue-family",
18
+ "events",
19
+ "ticketing",
20
+ "sdk",
21
+ "typescript",
22
+ "api"
23
+ ],
24
+ "author": "Venue Family Team <support@venuefamily.com>",
25
+ "license": "MIT",
26
+ "devDependencies": {
27
+ "typescript": "^5.0.0"
28
+ }
29
+ }
package/src/client.ts ADDED
@@ -0,0 +1,275 @@
1
+ import type {
2
+ EventData,
3
+ EventDateData,
4
+ EventsFilterOptions,
5
+ FormData,
6
+ FormSignatureData,
7
+ LocationData,
8
+ LocationsFilterOptions,
9
+ ReviewData,
10
+ RoleData,
11
+ UserData,
12
+ VenueFamilyConfig,
13
+ VolunteerRoleData,
14
+ } from './types.js';
15
+
16
+ export class VenueFamilyClient {
17
+ private readonly baseUrl: string;
18
+ private readonly apiKey?: string;
19
+ private readonly organization?: string;
20
+ private readonly customFetch: typeof fetch;
21
+
22
+ constructor(config: VenueFamilyConfig = {}) {
23
+ this.baseUrl = (config.baseUrl ?? 'https://venuefamily.com/api').replace(/\/+$/, '');
24
+ this.apiKey = config.apiKey;
25
+ this.organization = config.organization;
26
+ this.customFetch = config.fetch ?? fetch.bind(globalThis);
27
+ }
28
+
29
+ public forOrganization(organization: string): VenueFamilyClient {
30
+ return new VenueFamilyClient({
31
+ baseUrl: this.baseUrl,
32
+ apiKey: this.apiKey,
33
+ organization,
34
+ fetch: this.customFetch,
35
+ });
36
+ }
37
+
38
+ private getOrgSlug(): string {
39
+ if (!this.organization) {
40
+ throw new Error('Organization slug is required. Pass organization in config or use client.forOrganization("slug").');
41
+ }
42
+ return this.organization;
43
+ }
44
+
45
+ private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
46
+ const url = `${this.baseUrl}/${path.replace(/^\/+/, '')}`;
47
+ const headers: Record<string, string> = {
48
+ Accept: 'application/json',
49
+ ...(options.headers as Record<string, string> || {}),
50
+ };
51
+
52
+ if (this.apiKey) {
53
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
54
+ headers['X-API-Key'] = this.apiKey;
55
+ }
56
+
57
+ const response = await this.customFetch(url, {
58
+ ...options,
59
+ headers,
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const errorBody = await response.text();
64
+ let parsed = null;
65
+ try {
66
+ parsed = JSON.parse(errorBody);
67
+ } catch {
68
+ // use raw body
69
+ }
70
+ throw new Error(parsed?.message || parsed?.error || `Venue Family API Error: ${response.status} ${response.statusText}`);
71
+ }
72
+
73
+ return response.json() as Promise<T>;
74
+ }
75
+
76
+ // --- Auth & User Management ---
77
+
78
+ public readonly auth = {
79
+ login: async (email: string, password: string, organizationSlug?: string, deviceName: string = 'TS SDK'): Promise<{ token: string; user: UserData }> => {
80
+ const body: Record<string, unknown> = { email, password, device_name: deviceName };
81
+ if (organizationSlug) body.organization_slug = organizationSlug;
82
+ return this.request('auth/login', {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify(body),
86
+ });
87
+ },
88
+
89
+ register: async (userData: Record<string, unknown>): Promise<{ token: string; user: UserData }> => {
90
+ return this.request('auth/register', {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify(userData),
94
+ });
95
+ },
96
+
97
+ user: async (): Promise<UserData> => {
98
+ const res = await this.request<{ user: UserData }>('auth/user');
99
+ return res.user;
100
+ },
101
+
102
+ updateProfile: async (attributes: Partial<UserData>): Promise<UserData> => {
103
+ const res = await this.request<{ user: UserData }>('auth/profile', {
104
+ method: 'PUT',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify(attributes),
107
+ });
108
+ return res.user;
109
+ },
110
+
111
+ updatePassword: async (currentPassword: string, password: string, passwordConfirmation: string): Promise<{ message: string }> => {
112
+ return this.request('auth/password', {
113
+ method: 'PUT',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({
116
+ current_password: currentPassword,
117
+ password,
118
+ password_confirmation: passwordConfirmation,
119
+ }),
120
+ });
121
+ },
122
+
123
+ logout: async (): Promise<{ message: string }> => {
124
+ return this.request('auth/logout', { method: 'POST' });
125
+ },
126
+ };
127
+
128
+ public readonly users = {
129
+ me: async (): Promise<UserData> => this.auth.user(),
130
+ updateProfile: async (attributes: Partial<UserData>): Promise<UserData> => this.auth.updateProfile(attributes),
131
+ myVolunteerRoles: async (): Promise<{ data: VolunteerRoleData[] }> => this.request('volunteer-roles/my-roles'),
132
+ volunteerOpportunities: async (): Promise<{ data: VolunteerRoleData[] }> => this.request('volunteer-opportunities'),
133
+ signUpForVolunteerRole: async (roleId: number): Promise<{ success: boolean }> => {
134
+ return this.request('volunteer-roles/sign-up', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ role_id: roleId }),
138
+ });
139
+ },
140
+ };
141
+
142
+ public readonly roles = {
143
+ all: async (): Promise<{ data: RoleData[] }> => this.request('roles'),
144
+ find: async (roleIdOrSlug: number | string): Promise<{ data: RoleData }> => this.request(`roles/${roleIdOrSlug}`),
145
+ };
146
+
147
+ public readonly signatures = {
148
+ find: async (signingToken: string): Promise<{ data: FormSignatureData }> => this.request(`signatures/${signingToken}`),
149
+ signNative: async (signingToken: string, signatureData: string, name: string, email?: string): Promise<{ success: boolean }> => {
150
+ return this.request(`signatures/${signingToken}/sign`, {
151
+ method: 'POST',
152
+ headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({
154
+ signature_data: signatureData,
155
+ name,
156
+ email,
157
+ mechanism: 'native',
158
+ }),
159
+ });
160
+ },
161
+ getSigningUrl: (signingToken: string): string => {
162
+ const rootUrl = this.baseUrl.replace(/\/api\/?$/, '');
163
+ return `${rootUrl}/forms/countersign/${signingToken}`;
164
+ },
165
+ };
166
+
167
+ // --- Events ---
168
+
169
+ public readonly events = {
170
+ all: async (filters: EventsFilterOptions = {}): Promise<{ data: EventData[] }> => {
171
+ const params = new URLSearchParams();
172
+ for (const [k, v] of Object.entries(filters)) {
173
+ if (v !== undefined) params.append(k, Array.isArray(v) ? v.join(',') : String(v));
174
+ }
175
+ const qs = params.toString() ? `?${params.toString()}` : '';
176
+ return this.request<{ data: EventData[] }>(`public/${this.getOrgSlug()}/events${qs}`);
177
+ },
178
+
179
+ upcoming: async (tags?: string | string[], location?: string): Promise<{ data: EventData[] }> => {
180
+ const filters: EventsFilterOptions = { filter: 'upcoming' };
181
+ if (tags) filters.tags = tags;
182
+ if (location) filters.location = location;
183
+ return this.events.all(filters);
184
+ },
185
+
186
+ search: async (queryTerm: string): Promise<{ data: EventData[] }> => {
187
+ return this.request<{ data: EventData[] }>(`public/${this.getOrgSlug()}/events/search?q=${encodeURIComponent(queryTerm)}`);
188
+ },
189
+
190
+ find: async (eventIdOrSlug: number | string): Promise<{ data: EventData }> => {
191
+ return this.request<{ data: EventData }>(`public/${this.getOrgSlug()}/events/${eventIdOrSlug}`);
192
+ },
193
+
194
+ dates: async (eventIdOrSlug: number | string): Promise<{ data: EventDateData[] }> => {
195
+ return this.request<{ data: EventDateData[] }>(`public/${this.getOrgSlug()}/events/${eventIdOrSlug}/dates`);
196
+ },
197
+
198
+ date: async (eventDateId: number): Promise<{ data: EventDateData }> => {
199
+ return this.request<{ data: EventDateData }>(`public/${this.getOrgSlug()}/eventDates/${eventDateId}`);
200
+ },
201
+ };
202
+
203
+ // --- Locations ---
204
+
205
+ public readonly locations = {
206
+ all: async (filters: LocationsFilterOptions = {}): Promise<{ data: LocationData[] }> => {
207
+ const params = new URLSearchParams();
208
+ for (const [k, v] of Object.entries(filters)) {
209
+ if (v !== undefined) params.append(k, Array.isArray(v) ? v.join(',') : String(v));
210
+ }
211
+ const qs = params.toString() ? `?${params.toString()}` : '';
212
+ return this.request<{ data: LocationData[] }>(`public/${this.getOrgSlug()}/locations${qs}`);
213
+ },
214
+
215
+ find: async (locationIdOrStub: number | string): Promise<{ data: LocationData }> => {
216
+ return this.request<{ data: LocationData }>(`public/${this.getOrgSlug()}/locations/${locationIdOrStub}`);
217
+ },
218
+
219
+ mapData: async (): Promise<unknown> => {
220
+ return this.request(`public/${this.getOrgSlug()}/locations/map`);
221
+ },
222
+ };
223
+
224
+ // --- Forms ---
225
+
226
+ public readonly forms = {
227
+ all: async (): Promise<{ data: FormData[] }> => {
228
+ return this.request<{ data: FormData[] }>(`public/${this.getOrgSlug()}/forms`);
229
+ },
230
+
231
+ find: async (formIdOrSlug: number | string): Promise<{ data: FormData }> => {
232
+ return this.request<{ data: FormData }>(`public/${this.getOrgSlug()}/forms/${formIdOrSlug}`);
233
+ },
234
+
235
+ submit: async (formIdOrSlug: number | string, data: Record<string, unknown>): Promise<{ success: boolean; submission_id?: number }> => {
236
+ return this.request(`public/${this.getOrgSlug()}/forms/${formIdOrSlug}/submit`, {
237
+ method: 'POST',
238
+ headers: { 'Content-Type': 'application/json' },
239
+ body: JSON.stringify({ fields: data, data }),
240
+ });
241
+ },
242
+
243
+ signature: async (signingToken: string): Promise<{ data: FormSignatureData }> => {
244
+ return this.signatures.find(signingToken);
245
+ },
246
+
247
+ sign: async (signingToken: string, signatureData: string, name: string, email?: string): Promise<{ success: boolean }> => {
248
+ return this.signatures.signNative(signingToken, signatureData, name, email);
249
+ },
250
+ };
251
+
252
+ // --- Reviews ---
253
+
254
+ public readonly reviews = {
255
+ all: async (): Promise<{ data: ReviewData[] }> => {
256
+ return this.request<{ data: ReviewData[] }>('reviews');
257
+ },
258
+
259
+ create: async (data: Record<string, unknown>): Promise<{ data: ReviewData }> => {
260
+ return this.request<{ data: ReviewData }>('reviews', {
261
+ method: 'POST',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ body: JSON.stringify(data),
264
+ });
265
+ },
266
+
267
+ updateStatus: async (reviewId: number, status: string): Promise<{ success: boolean }> => {
268
+ return this.request(`reviews/${reviewId}/status`, {
269
+ method: 'PATCH',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({ status }),
272
+ });
273
+ },
274
+ };
275
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './client.js';
3
+ export * from './webhooks.js';
package/src/types.ts ADDED
@@ -0,0 +1,218 @@
1
+ export interface VenueFamilyConfig {
2
+ apiKey?: string;
3
+ organization?: string;
4
+ baseUrl?: string;
5
+ fetch?: typeof fetch;
6
+ }
7
+
8
+ export interface TagData {
9
+ id?: number;
10
+ name: string;
11
+ slug: string;
12
+ }
13
+
14
+ export interface LocationStub {
15
+ id: number;
16
+ name: string;
17
+ stub?: string;
18
+ }
19
+
20
+ export interface UserOrganizationData {
21
+ id: number;
22
+ name: string;
23
+ slug: string;
24
+ }
25
+
26
+ export interface UserData {
27
+ id: number;
28
+ name: string;
29
+ email: string;
30
+ first_name?: string;
31
+ last_name?: string;
32
+ chosen_name?: string;
33
+ artist_name?: string;
34
+ phone?: string;
35
+ bio?: string;
36
+ interests?: Array<{ id: number; name: string; description?: string; icon?: string }>;
37
+ organizations?: UserOrganizationData[];
38
+ is_staff?: boolean;
39
+ profile_photo_url?: string;
40
+ created_at?: string;
41
+ }
42
+
43
+ export interface RoleData {
44
+ id: number;
45
+ name: string;
46
+ slug: string;
47
+ scope: 'all' | 'own';
48
+ abilities?: string[];
49
+ status?: string;
50
+ }
51
+
52
+ export interface VolunteerRoleData {
53
+ id: number;
54
+ name: string;
55
+ description?: string;
56
+ date?: string;
57
+ start_time?: string;
58
+ end_time?: string;
59
+ spots_needed?: number;
60
+ spots_filled?: number;
61
+ status?: string;
62
+ organization_slug?: string;
63
+ }
64
+
65
+ export interface FormSignatureData {
66
+ id: number;
67
+ token: string;
68
+ status: 'pending' | 'signed' | 'countersigned' | 'cancelled' | 'expired';
69
+ mechanism: 'native' | 'certified';
70
+ name?: string;
71
+ email?: string;
72
+ signer_type?: string;
73
+ signed_at?: string;
74
+ signing_url?: string;
75
+ }
76
+
77
+ export interface EventDateData {
78
+ id: number;
79
+ date: string;
80
+ start_time: string;
81
+ end_time?: string;
82
+ door_time?: string;
83
+ status: string;
84
+ is_sold_out: boolean;
85
+ tickets_remaining?: number;
86
+ locations?: LocationStub[];
87
+ }
88
+
89
+ export interface EventData {
90
+ id: number;
91
+ title: string;
92
+ slug: string;
93
+ subtitle?: string;
94
+ summary?: string;
95
+ description?: string;
96
+ type?: string;
97
+ status?: string;
98
+ cover_image_url?: string;
99
+ ticket_price?: string;
100
+ tags?: TagData[];
101
+ locations?: LocationStub[];
102
+ upcoming_dates?: EventDateData[];
103
+ }
104
+
105
+ export interface SpaceData {
106
+ id: number;
107
+ name: string;
108
+ stub?: string;
109
+ capacity?: number;
110
+ price_per_hour?: string;
111
+ }
112
+
113
+ export interface LocationData {
114
+ id: number;
115
+ name: string;
116
+ stub?: string;
117
+ capacity?: number;
118
+ price_per_hour?: string;
119
+ address?: string;
120
+ city?: string;
121
+ state?: string;
122
+ zip?: string;
123
+ spaces?: SpaceData[];
124
+ features?: string[];
125
+ }
126
+
127
+ export interface FormFieldData {
128
+ id: string;
129
+ type: string;
130
+ label: string;
131
+ required?: boolean;
132
+ options?: string[];
133
+ placeholder?: string;
134
+ }
135
+
136
+ export interface FormData {
137
+ id: number;
138
+ title: string;
139
+ slug: string;
140
+ description?: string;
141
+ fields: FormFieldData[];
142
+ }
143
+
144
+ export interface ReviewData {
145
+ id: number;
146
+ rating?: number;
147
+ author_name?: string;
148
+ author_email?: string;
149
+ content?: string;
150
+ status?: string;
151
+ created_at?: string;
152
+ }
153
+
154
+ export interface EventsFilterOptions {
155
+ filter?: 'upcoming' | 'past' | 'recurring' | 'all';
156
+ start_date?: string;
157
+ end_date?: string;
158
+ location?: string;
159
+ type?: string;
160
+ tags?: string | string[];
161
+ search?: string;
162
+ q?: string;
163
+ limit?: number;
164
+ per_page?: number;
165
+ page?: number;
166
+ }
167
+
168
+ export interface LocationsFilterOptions {
169
+ city?: string;
170
+ state?: string;
171
+ capacity_min?: number;
172
+ capacity_max?: number;
173
+ price_min?: number;
174
+ price_max?: number;
175
+ features?: string | string[];
176
+ search?: string;
177
+ sort_by?: 'name' | 'capacity' | 'price_per_hour' | 'created_at';
178
+ sort_order?: 'asc' | 'desc';
179
+ }
180
+
181
+ // --- Embed postMessage Event Types ---
182
+
183
+ export interface ResizeEmbedEvent {
184
+ type: 'resize';
185
+ height: number;
186
+ }
187
+
188
+ export interface ScrollIntoViewEmbedEvent {
189
+ type: 'scroll-into-view';
190
+ top: number;
191
+ }
192
+
193
+ export interface TicketPurchaseCompleteEmbedEvent {
194
+ type: 'ticket-purchase-complete';
195
+ orderNumber: string;
196
+ orderId: number;
197
+ status: string;
198
+ total: number; // in cents
199
+ quantity: number;
200
+ currency: string;
201
+ eventId: number;
202
+ eventName: string;
203
+ }
204
+
205
+ export interface FormSubmittedEmbedEvent {
206
+ type: 'form-submitted';
207
+ slug: string;
208
+ formId: number;
209
+ submissionId: number;
210
+ }
211
+
212
+ export type VenueFamilyEmbedEvent =
213
+ | ResizeEmbedEvent
214
+ | ScrollIntoViewEmbedEvent
215
+ | TicketPurchaseCompleteEmbedEvent
216
+ | FormSubmittedEmbedEvent
217
+ | { type: 'scroll-to-error' }
218
+ | { type: 'signin_success'; event?: unknown; organization?: unknown; eventDate?: unknown };