@proveanything/smartlinks 2.0.70 → 2.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.
@@ -3,6 +3,8 @@
3
3
  import type { AppDataDeclaration, AppHeadlessDeclaration } from './headless.js';
4
4
  import type { RealtimeChannelDeclaration } from './realtime.js';
5
5
  import type { AppManifestAdminCards } from './adminCards.js';
6
+ import type { PaginatedResponse } from './appObjects.js';
7
+ import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse } from './bookings.js';
6
8
  /**
7
9
  * A bundle (widget or container) as returned by the collection widgets endpoint.
8
10
  *
@@ -416,7 +418,60 @@ export interface ServerFunctionInteractionEvent {
416
418
  batchId?: string;
417
419
  eventType?: string;
418
420
  }
421
+ /**
422
+ * `ctx.sl.bookings` — THIS app's own bookings, slots and blocks (docs/bookings.md, "In a server
423
+ * function"). Gated by `sl:bookings:read` / `sl:bookings:write` only. Writes are admin-side: any
424
+ * status, the admin zone, any contact — the app's `publicCreate.bookings` rules don't apply, because
425
+ * the function IS the app's rules (hours, queue tokens, payment). The platform's guarantees still do:
426
+ * a clash fails with an Error carrying `code` (SLOT_FULL, RESOURCE_TAKEN, PER_CONTACT_LIMIT,
427
+ * HOLD_EXPIRED, …), `status` and `details`.
428
+ */
429
+ export interface ServerFunctionBookings {
430
+ /** Book; pass `idempotencyKey` (e.g. the payment intent id) so a retry returns the first booking (`replayed: true`). */
431
+ create(input: CreateBookingInput): Promise<Booking & {
432
+ replayed: boolean;
433
+ }>;
434
+ get(bookingId: string): Promise<Booking | null>;
435
+ query(params?: BookingListQueryParams): Promise<PaginatedResponse<Booking>>;
436
+ /** The signed-in caller's own bookings (empty when anonymous). */
437
+ mine(params?: BookingListQueryParams): Promise<PaginatedResponse<Booking>>;
438
+ update(bookingId: string, fields: UpdateBookingInput): Promise<Booking>;
439
+ /** held / pending / waitlisted → confirmed (idempotent). */
440
+ confirm(bookingId: string): Promise<Booking>;
441
+ /** Cancel and give the places back (idempotent). */
442
+ cancel(bookingId: string, opts?: {
443
+ reason?: string;
444
+ }): Promise<Booking>;
445
+ /** All-or-nothing move to new claims. */
446
+ move(bookingId: string, input: MoveBookingInput): Promise<Booking>;
447
+ extendHold(bookingId: string, holdMinutes: number): Promise<Booking>;
448
+ complete(bookingId: string): Promise<Booking>;
449
+ noShow(bookingId: string): Promise<Booking>;
450
+ /** Busy intervals per resource — feed them to `utils.freeTimes` with your schedule. */
451
+ freebusy(q: FreeBusyQuery): Promise<FreeBusyResponse>;
452
+ slots: {
453
+ create(input: CreateSlotInput): Promise<BookingSlot>;
454
+ createMany(inputs: CreateSlotInput[], opts?: {
455
+ seriesId?: string;
456
+ }): Promise<BookingSlot[]>;
457
+ get(slotId: string): Promise<BookingSlot | null>;
458
+ query(params?: SlotListQueryParams): Promise<PaginatedResponse<BookingSlot>>;
459
+ update(slotId: string, fields: UpdateSlotInput): Promise<BookingSlot>;
460
+ delete(slotId: string): Promise<{
461
+ success: boolean;
462
+ }>;
463
+ };
464
+ blocks: {
465
+ create(input: CreateBlockInput): Promise<BookingAllocation>;
466
+ query(params?: BlockListQueryParams): Promise<PaginatedResponse<BookingAllocation>>;
467
+ delete(blockId: string): Promise<{
468
+ success: boolean;
469
+ }>;
470
+ };
471
+ }
419
472
  export interface ServerFunctionSl {
473
+ /** THIS app's bookings, slots and blocks (`sl:bookings:read` / `sl:bookings:write`). */
474
+ bookings: ServerFunctionBookings;
420
475
  /**
421
476
  * THIS app's records (own namespace): create / update / upsert / delete / get / query / listTypes,
422
477
  * plus `claim(fields)` — a race-safe UNIQUE create. The record names a unique key — `singletonPer`
@@ -39,8 +39,26 @@ export interface Asset {
39
39
  proofId: string | null;
40
40
  /** App that owns this asset, e.g. 'homepage' */
41
41
  appId: string | null;
42
- /** CDN URL of the original file */
42
+ /**
43
+ * Address of the asset's current **file**. Versioned: it changes when the file is replaced (the old one moves
44
+ * to `versions[]`). For static content, use `assetUrl` instead.
45
+ */
43
46
  url: string;
47
+ /**
48
+ * The **asset's** own address: `https://cdn.smartlinks.app/sites/{collectionId}/a/{assetId}`.
49
+ *
50
+ * It's the address of the asset (the named thing, e.g. "Hero image"), not of one file. It always serves the
51
+ * asset's current file: replace the file and everything using `assetUrl` shows the new one, with no redeploy
52
+ * and no extra API call (the CDN copy is cleared; browsers re-check within a minute). Deleting the asset takes
53
+ * the address down; restoring it brings it back.
54
+ *
55
+ * Use it in static content: a built site's HTML and CSS, emails, banners. Product and collection images are
56
+ * different: they are references in the record (`AssetRef`), and a new image is a record change.
57
+ *
58
+ * `null` for an outside link, and for an asset made before asset addresses existed until it's given one
59
+ * ({@link asset.ensureAssetUrl}).
60
+ */
61
+ assetUrl: string | null;
44
62
  /**
45
63
  * CDN URL of the WebP thumbnail (max 512px longest edge, no crop).
46
64
  * Always .webp — null until thumbnail generation has run.
@@ -336,6 +354,12 @@ export interface CreateResumableUploadOptions {
336
354
  * uploads. When provided, the public resumable route is used.
337
355
  */
338
356
  token?: string;
357
+ /**
358
+ * Replace this asset's file instead of creating a new asset (admin). The id and `assetUrl` stay the same,
359
+ * so everything using `assetUrl` shows the new file; the previous file goes into `versions[]`. Any size —
360
+ * use this to replace a large video.
361
+ */
362
+ replaceAssetId?: string;
339
363
  }
340
364
  export interface ResumableStartOptions {
341
365
  /** Progress callback (0-100), driven by bytes confirmed by storage. */
@@ -0,0 +1,301 @@
1
+ import type { PaginatedResponse } from './appObjects.js';
2
+ export type BookingStatus = 'held' | 'pending' | 'confirmed' | 'completed' | 'cancelled' | 'no_show' | 'waitlisted';
3
+ export type BookingVisibility = 'public' | 'owner' | 'admin';
4
+ /** A claim on a resource's time: seats on a slot (`capacity`) or exclusive time (`exclusive`). */
5
+ export interface BookingAllocation {
6
+ id: string;
7
+ bookingId: string | null;
8
+ slotId: string | null;
9
+ resourceType: string;
10
+ resourceId: string;
11
+ /** The time actually blocked — buffers included. */
12
+ startsAt: string;
13
+ endsAt: string;
14
+ quantity: number;
15
+ mode: 'exclusive' | 'capacity';
16
+ active: boolean;
17
+ sourceSystem?: string | null;
18
+ data: Record<string, any>;
19
+ }
20
+ export interface Booking {
21
+ id: string;
22
+ collectionId: string;
23
+ appId: string;
24
+ visibility: BookingVisibility;
25
+ /** Admin / owner callers only. */
26
+ contactId?: string;
27
+ status: BookingStatus;
28
+ /** The customer-visible time (buffers excluded). */
29
+ startsAt: string;
30
+ endsAt: string;
31
+ timezone?: string | null;
32
+ quantity: number;
33
+ holdExpiresAt?: string | null;
34
+ /** Platform flags, e.g. 'hold_expired'. */
35
+ flags: string[];
36
+ /** 'admin' | 'owner' | 'hold_expired' | an app-defined reason. */
37
+ cancelReason?: string | null;
38
+ idempotencyKey?: string | null;
39
+ seriesId?: string | null;
40
+ ref?: string | null;
41
+ customId?: string | null;
42
+ sourceSystem?: string | null;
43
+ productId?: string | null;
44
+ proofId?: string;
45
+ parentType?: string | null;
46
+ parentId?: string | null;
47
+ createdAt: string;
48
+ updatedAt: string;
49
+ cancelledAt?: string | null;
50
+ data: Record<string, any>;
51
+ /** Owner + admin only. */
52
+ owner?: Record<string, any>;
53
+ /** Admin only. */
54
+ admin?: Record<string, any>;
55
+ allocations: BookingAllocation[];
56
+ /**
57
+ * Anonymous public creates only, when the app enables `publicCreate.bookings.anonymous.edit.editToken`.
58
+ * Returned ONCE — store it to let the guest view / cancel / move / confirm their booking.
59
+ */
60
+ editToken?: string;
61
+ }
62
+ export interface BookingSlot {
63
+ id: string;
64
+ collectionId: string;
65
+ appId: string;
66
+ visibility: BookingVisibility;
67
+ resourceType?: string | null;
68
+ resourceId?: string | null;
69
+ startsAt: string;
70
+ endsAt: string;
71
+ timezone?: string | null;
72
+ capacity: number;
73
+ booked: number;
74
+ /** capacity - booked */
75
+ remaining: number;
76
+ perContactLimit?: number | null;
77
+ status: 'open' | 'closed' | 'cancelled';
78
+ seriesId?: string | null;
79
+ ref?: string | null;
80
+ customId?: string | null;
81
+ sourceSystem?: string | null;
82
+ productId?: string | null;
83
+ createdAt: string;
84
+ updatedAt: string;
85
+ data: Record<string, any>;
86
+ owner?: Record<string, any>;
87
+ admin?: Record<string, any>;
88
+ }
89
+ /** One claim in a booking request: seats on a slot, or exclusive time on a resource. */
90
+ export type BookingClaimInput = {
91
+ slotId: string;
92
+ quantity?: number;
93
+ } | {
94
+ resourceType: string;
95
+ resourceId: string;
96
+ /** Defaults to the booking's startsAt / endsAt. */
97
+ startsAt?: string | Date;
98
+ endsAt?: string | Date;
99
+ /** Admin only (public bookings take buffers from the app's rules). Minutes. */
100
+ bufferBefore?: number;
101
+ bufferAfter?: number;
102
+ };
103
+ export interface CreateBookingInput {
104
+ /** Seats on one slot … */
105
+ slotId?: string;
106
+ quantity?: number;
107
+ /** … or exclusive time on one resource … */
108
+ resourceType?: string;
109
+ resourceId?: string;
110
+ resource?: {
111
+ type: string;
112
+ id: string;
113
+ };
114
+ startsAt?: string | Date;
115
+ endsAt?: string | Date;
116
+ /** … or several claims at once (all-or-nothing): a stylist and a chair, two tables. */
117
+ allocations?: BookingClaimInput[];
118
+ /** Admin only — public bookings get the status the app's rules enforce. Default 'confirmed'. */
119
+ status?: 'held' | 'pending' | 'confirmed' | 'waitlisted';
120
+ /** For status 'held' (admin: default 15, max 1440). */
121
+ holdMinutes?: number;
122
+ /** Admin only: blocked time around an exclusive claim, in minutes. */
123
+ bufferBefore?: number;
124
+ bufferAfter?: number;
125
+ /** A retried create with the same key returns the first booking instead of booking twice. */
126
+ idempotencyKey?: string;
127
+ /** Admin only — public bookings use the signed-in caller's contact. */
128
+ contactId?: string;
129
+ timezone?: string;
130
+ visibility?: BookingVisibility;
131
+ ref?: string;
132
+ customId?: string;
133
+ sourceSystem?: string;
134
+ productId?: string;
135
+ proofId?: string;
136
+ parentType?: string;
137
+ parentId?: string;
138
+ seriesId?: string;
139
+ data?: Record<string, any>;
140
+ owner?: Record<string, any>;
141
+ /** Admin only. */
142
+ admin?: Record<string, any>;
143
+ }
144
+ export interface UpdateBookingInput {
145
+ data?: Record<string, any>;
146
+ owner?: Record<string, any>;
147
+ admin?: Record<string, any>;
148
+ visibility?: BookingVisibility;
149
+ ref?: string;
150
+ timezone?: string;
151
+ productId?: string;
152
+ proofId?: string;
153
+ contactId?: string;
154
+ sourceSystem?: string;
155
+ customId?: string;
156
+ flags?: string[];
157
+ }
158
+ export interface MoveBookingInput {
159
+ slotId?: string;
160
+ resourceType?: string;
161
+ resourceId?: string;
162
+ resource?: {
163
+ type: string;
164
+ id: string;
165
+ };
166
+ startsAt?: string | Date;
167
+ endsAt?: string | Date;
168
+ allocations?: BookingClaimInput[];
169
+ bufferBefore?: number;
170
+ bufferAfter?: number;
171
+ }
172
+ export interface BookingListQueryParams {
173
+ status?: string;
174
+ contactId?: string;
175
+ slotId?: string;
176
+ resourceType?: string;
177
+ resourceId?: string;
178
+ /** Bookings overlapping [from, to). */
179
+ from?: string | Date;
180
+ to?: string | Date;
181
+ ref?: string;
182
+ customId?: string;
183
+ sourceSystem?: string;
184
+ productId?: string;
185
+ seriesId?: string;
186
+ /** Default 'starts_at:asc'. */
187
+ sort?: string;
188
+ limit?: number;
189
+ offset?: number;
190
+ }
191
+ export interface CreateSlotInput {
192
+ startsAt: string | Date;
193
+ endsAt: string | Date;
194
+ capacity: number;
195
+ perContactLimit?: number;
196
+ /** What the slot runs on (an instructor, a room) … */
197
+ resourceType?: string;
198
+ resourceId?: string;
199
+ /** … and whether the slot blocks that resource's time (no double-booking across slots). */
200
+ blockResource?: boolean;
201
+ status?: 'open' | 'closed' | 'cancelled';
202
+ timezone?: string;
203
+ visibility?: BookingVisibility;
204
+ seriesId?: string;
205
+ ref?: string;
206
+ customId?: string;
207
+ sourceSystem?: string;
208
+ productId?: string;
209
+ data?: Record<string, any>;
210
+ owner?: Record<string, any>;
211
+ admin?: Record<string, any>;
212
+ }
213
+ export type UpdateSlotInput = Partial<Omit<CreateSlotInput, 'blockResource' | 'seriesId' | 'resourceType' | 'resourceId'>>;
214
+ export interface SlotListQueryParams {
215
+ status?: string;
216
+ /** Only slots with places left. */
217
+ available?: boolean;
218
+ from?: string | Date;
219
+ to?: string | Date;
220
+ resourceType?: string;
221
+ resourceId?: string;
222
+ productId?: string;
223
+ seriesId?: string;
224
+ ref?: string;
225
+ customId?: string;
226
+ sort?: string;
227
+ limit?: number;
228
+ offset?: number;
229
+ }
230
+ export interface CreateBlockInput {
231
+ resourceType: string;
232
+ resourceId: string;
233
+ startsAt: string | Date;
234
+ endsAt: string | Date;
235
+ sourceSystem?: string;
236
+ data?: Record<string, any>;
237
+ }
238
+ export interface BlockListQueryParams {
239
+ resourceType?: string;
240
+ resourceId?: string;
241
+ from?: string | Date;
242
+ to?: string | Date;
243
+ limit?: number;
244
+ offset?: number;
245
+ }
246
+ /** A resource reference: `{ type, id }` or the string form `'record:staff-1'`. */
247
+ export type BookingResourceRef = {
248
+ type: string;
249
+ id: string;
250
+ } | string;
251
+ export interface FreeBusyQuery {
252
+ resources: BookingResourceRef[];
253
+ from: string | Date;
254
+ to: string | Date;
255
+ }
256
+ export interface FreeBusyInterval {
257
+ start: string;
258
+ end: string;
259
+ }
260
+ export interface FreeBusyResponse {
261
+ from: string;
262
+ to: string;
263
+ resources: Array<{
264
+ resourceType: string;
265
+ resourceId: string;
266
+ busy: FreeBusyInterval[];
267
+ }>;
268
+ }
269
+ /** Public calls on a booking made by an anonymous guest: pass the edit token returned at create. */
270
+ export interface BookingAccessOptions {
271
+ editToken?: string;
272
+ }
273
+ export type BookingList = PaginatedResponse<Booking>;
274
+ export type SlotList = PaginatedResponse<BookingSlot>;
275
+ export type BlockList = PaginatedResponse<BookingAllocation>;
276
+ /** Error codes a booking call can fail with (HTTP 409 unless noted). */
277
+ export type BookingErrorCode = 'SLOT_FULL' | 'SLOT_CLOSED' | 'RESOURCE_TAKEN' | 'PER_CONTACT_LIMIT' | 'QUANTITY_LIMIT' | 'HOLD_EXPIRED' | 'INVALID_STATE' | 'SLOT_OVERBOOKED' | 'SLOT_HAS_BOOKINGS' | 'TOO_SOON' | 'TOO_FAR_AHEAD' | 'IN_THE_PAST' | 'DURATION_NOT_ALLOWED' | 'OUTSIDE_SCHEDULE' | 'NO_SCHEDULE' | 'SCHEDULE_INVALID' | 'CUTOFF_PASSED' | 'FORBIDDEN' | 'NOT_FOUND' | 'BAD_REQUEST';
278
+ export interface ScheduleHours {
279
+ /** 'HH:MM' (24h) */
280
+ start: string;
281
+ /** 'HH:MM' (24h); '24:00' allowed. Hours never cross midnight — split them. */
282
+ end: string;
283
+ }
284
+ export type ScheduleDay = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
285
+ export interface ScheduleOverride {
286
+ /** One date (YYYY-MM-DD) … */
287
+ date?: string;
288
+ /** … or an inclusive range. */
289
+ from?: string;
290
+ to?: string;
291
+ /** [] = closed */
292
+ hours: ScheduleHours[];
293
+ }
294
+ export interface StandardSchedule {
295
+ /** IANA timezone, e.g. 'Europe/London'. */
296
+ timezone: string;
297
+ /** A missing day is closed. */
298
+ weekly?: Partial<Record<ScheduleDay, ScheduleHours[]>>;
299
+ /** Dated exceptions; the last matching one wins over `weekly`. */
300
+ overrides?: ScheduleOverride[];
301
+ }
@@ -0,0 +1,3 @@
1
+ // src/types/bookings.ts
2
+ // Generic booking objects (Slots, Bookings, Allocations) — see docs/bookings.md.
3
+ export {};
@@ -37,6 +37,7 @@ export * from "./iframeResponder.js";
37
37
  export * from "./ai.js";
38
38
  export * from "./appManifest.js";
39
39
  export * from "./appObjects.js";
40
+ export * from "./bookings.js";
40
41
  export * from "./loyalty.js";
41
42
  export * from "./translations.js";
42
43
  export * from "./integrations.js";
@@ -39,6 +39,7 @@ export * from "./iframeResponder.js";
39
39
  export * from "./ai.js";
40
40
  export * from "./appManifest.js";
41
41
  export * from "./appObjects.js";
42
+ export * from "./bookings.js";
42
43
  export * from "./loyalty.js";
43
44
  export * from "./translations.js";
44
45
  export * from "./integrations.js";
@@ -0,0 +1,57 @@
1
+ import type { StandardSchedule, FreeBusyInterval } from '../types/bookings.js';
2
+ export interface TimeInterval {
3
+ /** epoch ms (UTC) */
4
+ start: number;
5
+ /** epoch ms (UTC) */
6
+ end: number;
7
+ }
8
+ /** Throws a readable Error when `schedule` isn't the standard shape. */
9
+ export declare function validateSchedule(schedule: StandardSchedule): true;
10
+ /** The UTC instant (ms) of local date `ymd` + `minutes` past midnight in `tz`. */
11
+ export declare function zonedToUtc(ymd: string, minutes: number, tz: string): number;
12
+ /** The schedule's open hours overlapping [from, to), as merged UTC intervals. */
13
+ export declare function openIntervals(schedule: StandardSchedule, from: string | number | Date, to: string | number | Date): TimeInterval[];
14
+ /** True when [start, end) lies entirely inside the schedule's open hours. */
15
+ export declare function withinSchedule(schedule: StandardSchedule, start: string | number | Date, end: string | number | Date): boolean;
16
+ /** `open` minus `busy` (both lists of intervals). */
17
+ export declare function subtractIntervals(open: TimeInterval[], busy: TimeInterval[]): TimeInterval[];
18
+ export interface FreeTimesOptions {
19
+ /** Open hours: a standard schedule, or open intervals you've worked out yourself. */
20
+ hours: StandardSchedule | TimeInterval[];
21
+ /** Busy intervals, e.g. `resources[i].busy` from bookings.freebusy (ISO strings or ms). */
22
+ busy?: Array<FreeBusyInterval | TimeInterval>;
23
+ from: string | number | Date;
24
+ to: string | number | Date;
25
+ /** Appointment length, minutes. */
26
+ duration: number;
27
+ /** Minutes between candidate start times. Default: `duration`. */
28
+ step?: number;
29
+ /** Blocked time around each appointment, minutes (must also fit in a free gap). */
30
+ bufferBefore?: number;
31
+ bufferAfter?: number;
32
+ /** No start sooner than this many minutes from `now`. */
33
+ minNotice?: number;
34
+ /** "Now", for notice (default Date.now()). */
35
+ now?: number;
36
+ /**
37
+ * Whether buffers must also fall inside opening hours. Default false: a buffer may run past
38
+ * closing (cleaning up after the last appointment) but never into busy time.
39
+ */
40
+ buffersWithinHours?: boolean;
41
+ }
42
+ export interface FreeTimeSlot {
43
+ /** ISO start of the appointment (buffers excluded). */
44
+ start: string;
45
+ end: string;
46
+ }
47
+ /**
48
+ * Bookable start times: open hours minus busy time, stepped at `step`, each long enough for
49
+ * `duration` (+ buffers against busy time), not sooner than `minNotice`.
50
+ *
51
+ * const fb = await bookings.freebusy(collectionId, appId, { resources: ['record:sam'], from, to }, true)
52
+ * const times = utils.freeTimes({ hours: samsSchedule, busy: fb.resources[0].busy, from, to, duration: 30 })
53
+ *
54
+ * Start times are aligned to `step` from local midnight in the schedule's timezone (so 30-minute
55
+ * steps land on :00 and :30), or from each open interval's start when you pass intervals.
56
+ */
57
+ export declare function freeTimes(opts: FreeTimesOptions): FreeTimeSlot[];