@proveanything/smartlinks 2.0.69 → 2.0.71
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/dist/api/asset.d.ts +5 -0
- package/dist/api/asset.js +11 -0
- package/dist/api/authKit.d.ts +28 -4
- package/dist/api/authKit.js +35 -1
- package/dist/api/bookings.d.ts +89 -0
- package/dist/api/bookings.js +179 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/docs/API_SUMMARY.md +503 -11
- package/dist/docs/app-objects.md +4 -0
- package/dist/docs/assets.md +63 -2
- package/dist/docs/auth-kit.md +46 -5
- package/dist/docs/bookings.md +324 -0
- package/dist/docs/members.md +182 -0
- package/dist/docs/overview.md +477 -475
- package/dist/docs/server-functions.md +4 -1
- package/dist/http.js +3 -0
- package/dist/openapi.yaml +5254 -4037
- package/dist/testing/index.js +1 -1
- package/dist/types/appManifest.d.ts +16 -4
- package/dist/types/asset.d.ts +25 -1
- package/dist/types/authKit.d.ts +58 -7
- package/dist/types/bookings.d.ts +301 -0
- package/dist/types/bookings.js +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/freeTimes.d.ts +57 -0
- package/dist/utils/freeTimes.js +206 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/API_SUMMARY.md +503 -11
- package/docs/app-objects.md +4 -0
- package/docs/assets.md +63 -2
- package/docs/auth-kit.md +46 -5
- package/docs/bookings.md +324 -0
- package/docs/members.md +182 -0
- package/docs/overview.md +477 -475
- package/docs/server-functions.md +4 -1
- package/openapi.yaml +5254 -4037
- package/package.json +1 -1
package/dist/testing/index.js
CHANGED
|
@@ -241,7 +241,7 @@ export function createFunctionTestContext(opts) {
|
|
|
241
241
|
mem.contacts.push(Object.assign(Object.assign({}, d), { email, phone, contactId }));
|
|
242
242
|
return { contactId, created: true };
|
|
243
243
|
}), 'contacts.upsert'),
|
|
244
|
-
forCaller: gated('contacts', 'write', ct.forCaller ? ct.forCaller.bind(ct) : (async () => (opts.caller && opts.caller.userId ? { contactId: `contact-of-${opts.caller.userId}
|
|
244
|
+
forCaller: gated('contacts', 'write', ct.forCaller ? ct.forCaller.bind(ct) : (async () => (opts.caller && opts.caller.userId ? { contactId: `contact-of-${opts.caller.userId}`, email: null, phone: null, firstName: null, lastName: null, displayName: null, tags: [], customFields: {} } : null)), 'contacts.forCaller'),
|
|
245
245
|
};
|
|
246
246
|
const it = impl.interactions || {};
|
|
247
247
|
sl.interactions = {
|
|
@@ -437,16 +437,16 @@ export interface ServerFunctionSl {
|
|
|
437
437
|
/**
|
|
438
438
|
* Contacts (`sl:contacts:write`). `upsert` finds by email/phone or creates, and only FILLS EMPTY
|
|
439
439
|
* fields — never overwrites (details typed by a visitor are unverified). `forCaller` is the signed-in
|
|
440
|
-
* caller's own contact (null when anonymous)
|
|
440
|
+
* caller's own contact with its tags and custom fields (null when anonymous) — check a member's level
|
|
441
|
+
* (`customFields.level`) here before returning member-only content. It includes staff-only fields:
|
|
442
|
+
* return only what the caller may see. Never return contact ids to public callers.
|
|
441
443
|
*/
|
|
442
444
|
contacts: {
|
|
443
445
|
upsert(details: ContactUpsertDetails): Promise<{
|
|
444
446
|
contactId: string;
|
|
445
447
|
created: boolean;
|
|
446
448
|
}>;
|
|
447
|
-
forCaller(): Promise<
|
|
448
|
-
contactId: string;
|
|
449
|
-
} | null>;
|
|
449
|
+
forCaller(): Promise<CallerContact | null>;
|
|
450
450
|
};
|
|
451
451
|
/**
|
|
452
452
|
* Interactions (`sl:interactions:write`). `record` stores an event of one of THIS app's interaction
|
|
@@ -502,6 +502,18 @@ export interface ServerFunctionCaller {
|
|
|
502
502
|
/** How the function was triggered. */
|
|
503
503
|
via: AppFunctionTriggerType;
|
|
504
504
|
}
|
|
505
|
+
/** The signed-in caller's own contact, as a server function sees it (`ctx.sl.contacts.forCaller()`). */
|
|
506
|
+
export interface CallerContact {
|
|
507
|
+
contactId: string;
|
|
508
|
+
email: string | null;
|
|
509
|
+
phone: string | null;
|
|
510
|
+
firstName: string | null;
|
|
511
|
+
lastName: string | null;
|
|
512
|
+
displayName: string | null;
|
|
513
|
+
tags: string[];
|
|
514
|
+
/** The collection's custom contact fields — including staff-only ones (a member's level, a paid-until date). */
|
|
515
|
+
customFields: Record<string, unknown>;
|
|
516
|
+
}
|
|
505
517
|
/**
|
|
506
518
|
* The context handed to every SmartLinks server function. The platform builds a
|
|
507
519
|
* fresh one per invocation and pre-scopes each handle to the function's declared
|
package/dist/types/asset.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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. */
|
package/dist/types/authKit.d.ts
CHANGED
|
@@ -128,7 +128,9 @@ export interface AppleLoginOptions {
|
|
|
128
128
|
* `ACCOUNT_EXISTS_UNVERIFIED` case also carries `requiresEmailVerification: true`
|
|
129
129
|
* in `SmartlinksApiError.details`.
|
|
130
130
|
*/
|
|
131
|
-
export type AuthKitErrorCode = 'MISSING_APPLE_TOKEN' | 'APPLE_AUTH_NOT_CONFIGURED' | 'INVALID_APPLE_TOKEN' | 'ACCOUNT_EXISTS_UNVERIFIED' | 'APPLE_AUTH_FAILED';
|
|
131
|
+
export type AuthKitErrorCode = 'MISSING_APPLE_TOKEN' | 'APPLE_AUTH_NOT_CONFIGURED' | 'INVALID_APPLE_TOKEN' | 'ACCOUNT_EXISTS_UNVERIFIED' | 'APPLE_AUTH_FAILED' | 'SIGN_UP_CLOSED';
|
|
132
|
+
/** A second-step method: a code by email or text message, or from an authenticator app. */
|
|
133
|
+
export type MfaFactor = 'email' | 'sms' | 'totp';
|
|
132
134
|
/**
|
|
133
135
|
* Details carried on the `MFA_REQUIRED` error thrown by any session-issuing login method
|
|
134
136
|
* (see the gate list above) when the client's MFA policy requires a step-up. Surfaced as
|
|
@@ -139,18 +141,20 @@ export interface MfaRequiredDetails {
|
|
|
139
141
|
/** Short-lived (10 min) and single-use. Pass it to every challenge/finalize call below. */
|
|
140
142
|
mfaSessionToken: string;
|
|
141
143
|
/** Only factors the user actually enrolled. */
|
|
142
|
-
availableFactors:
|
|
143
|
-
/** `
|
|
144
|
-
preferredFactor:
|
|
144
|
+
availableFactors: MfaFactor[];
|
|
145
|
+
/** `totp`, then `sms`, then `email`: the first one the user enrolled. */
|
|
146
|
+
preferredFactor: MfaFactor;
|
|
147
|
+
/** `totp` is "your authenticator app" (nothing is sent for it). */
|
|
145
148
|
maskedDestinations: {
|
|
146
149
|
email?: string;
|
|
147
150
|
sms?: string;
|
|
151
|
+
totp?: string;
|
|
148
152
|
};
|
|
149
153
|
}
|
|
150
154
|
/** Response from {@link authKit.mfaChallengeSend}. */
|
|
151
155
|
export interface MfaChallengeSendResponse {
|
|
152
156
|
success: true;
|
|
153
|
-
factor:
|
|
157
|
+
factor: MfaFactor;
|
|
154
158
|
/** Masked, e.g. `"j***@x.com"` or `"+1******1234"`. */
|
|
155
159
|
destination: string;
|
|
156
160
|
/** ISO timestamp, 10 minutes from send. */
|
|
@@ -179,10 +183,22 @@ export interface MfaEnrollSendResponse {
|
|
|
179
183
|
destination: string;
|
|
180
184
|
expiresAt: string;
|
|
181
185
|
}
|
|
182
|
-
/** Response from {@link authKit.
|
|
186
|
+
/** Response from {@link authKit.enrollTotpMfa}: show `otpauthUri` as a QR code, and `secret` for typing in. */
|
|
187
|
+
export interface MfaTotpEnrollResponse {
|
|
188
|
+
mfaSessionToken: string;
|
|
189
|
+
/** Base32, for people who type it into their app instead of scanning. Returned only here. */
|
|
190
|
+
secret: string;
|
|
191
|
+
/** `otpauth://totp/…` — render it as a QR code for the app to scan. */
|
|
192
|
+
otpauthUri: string;
|
|
193
|
+
/** What the app shows the entry as: the site, and the account. */
|
|
194
|
+
issuer: string;
|
|
195
|
+
account: string;
|
|
196
|
+
expiresAt: string;
|
|
197
|
+
}
|
|
198
|
+
/** Response from {@link authKit.confirmEmailMfa} / {@link authKit.confirmSmsMfa} / {@link authKit.confirmTotpMfa}. */
|
|
183
199
|
export interface MfaEnrolledResponse {
|
|
184
200
|
enrolled: true;
|
|
185
|
-
factor:
|
|
201
|
+
factor: MfaFactor;
|
|
186
202
|
}
|
|
187
203
|
/** Response from {@link authKit.getMfaFactors}. */
|
|
188
204
|
export interface MfaFactorsResponse {
|
|
@@ -195,6 +211,11 @@ export interface MfaFactorsResponse {
|
|
|
195
211
|
enrolledAt: string;
|
|
196
212
|
destination: string;
|
|
197
213
|
};
|
|
214
|
+
/** `destination` is "Authenticator app". */
|
|
215
|
+
totp?: {
|
|
216
|
+
enrolledAt: string;
|
|
217
|
+
destination: string;
|
|
218
|
+
};
|
|
198
219
|
};
|
|
199
220
|
recoveryCodesRemaining: number;
|
|
200
221
|
mfaEnabledForClient: boolean;
|
|
@@ -415,6 +436,36 @@ export interface AuthKitConfig {
|
|
|
415
436
|
* site, its own passwords). Switch with {@link authKit.setIdentity}; never via create/update.
|
|
416
437
|
*/
|
|
417
438
|
identity?: AuthKitIdentity;
|
|
439
|
+
/**
|
|
440
|
+
* Who may join a site with its own logins. `open` (default): anyone can create an account.
|
|
441
|
+
* `invite`: only people the site's admins invite ({@link authKit.invite}). Every way of signing up
|
|
442
|
+
* (password, magic link, phone, Google, Apple, LinkedIn, WhatsApp) then refuses anyone else with
|
|
443
|
+
* `SIGN_UP_CLOSED` (403), and "send me a link / a code" answers as if sent without sending anything,
|
|
444
|
+
* so nobody can find out who is a member. Hide your "Create an account" option when it's `invite`.
|
|
445
|
+
* Setting `invite` needs own logins first (`OWN_LOGINS_REQUIRED`, 409).
|
|
446
|
+
*/
|
|
447
|
+
signUp?: 'open' | 'invite';
|
|
448
|
+
}
|
|
449
|
+
/** Body of {@link authKit.invite}. */
|
|
450
|
+
export interface AuthKitInviteRequest {
|
|
451
|
+
email: string;
|
|
452
|
+
firstName?: string;
|
|
453
|
+
lastName?: string;
|
|
454
|
+
displayName?: string;
|
|
455
|
+
/** Set on their contact in this collection, e.g. `{ level: 'member' }` (staff-only and visible fields alike). */
|
|
456
|
+
customFields?: Record<string, unknown>;
|
|
457
|
+
/** Where the emailed link opens: the site's sign-in page (one of its own addresses). Default: the kit's `redirectUrl`. */
|
|
458
|
+
redirectUrl?: string;
|
|
459
|
+
}
|
|
460
|
+
/** Result of {@link authKit.invite}. The sign-in link itself only goes to their inbox. */
|
|
461
|
+
export interface AuthKitInviteResponse {
|
|
462
|
+
ok: true;
|
|
463
|
+
userId: string;
|
|
464
|
+
contactId: string;
|
|
465
|
+
/** true when their account was made by this invite; false when they had one (they got a new link). */
|
|
466
|
+
created: boolean;
|
|
467
|
+
/** When the emailed link stops working (it works once). */
|
|
468
|
+
expiresAt: string;
|
|
418
469
|
}
|
|
419
470
|
/** An AuthKit's member pool. */
|
|
420
471
|
export interface AuthKitIdentity {
|
|
@@ -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
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/types/index.js
CHANGED
|
@@ -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[];
|