@vuoma/sdk 0.1.1 → 0.2.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.
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { bookingInput, visitInput, bookingId, feeAmount, validBookingResult, validQuote, validVisitResult, availabilityRange, validAvailability, validBookingCatalog, accountInput, displayName, emailCode, validCapability, accountResolution } from "./member-contracts.js"
2
+
1
3
  // Public package: no environment reads, provider clients, persistence or secrets.
2
4
  const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u
3
- const appKeyPattern = /^vuoma_pk_[0-9a-f]{32}$/u
4
5
  const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
5
6
  const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u
6
7
  const maximumRange = 63 * 24 * 60 * 60 * 1000
@@ -135,26 +136,45 @@ export function createVuomaClient(options) {
135
136
  if (typeof organizationSlug !== "string" || organizationSlug.length < 2 || organizationSlug.length > 80 || !slugPattern.test(organizationSlug)) throw new TypeError("Use an exact organization slug.")
136
137
  const organizationId = options.organizationId
137
138
  if (organizationId !== undefined && !uuid(organizationId)) throw new TypeError("Use a valid organization ID.")
138
- const appKey = options.appKey
139
- if (appKey !== undefined && (typeof appKey !== "string" || !appKeyPattern.test(appKey))) throw new TypeError("Use a publishable Vuoma app key.")
140
139
  const requestFetch = options.fetch ?? globalThis.fetch
141
140
  if (typeof requestFetch !== "function") throw new TypeError("A fetch implementation is required.")
142
141
  const tokenAdapter = options.tokenAdapter
143
142
  if (tokenAdapter !== undefined && (!object(tokenAdapter) || typeof tokenAdapter.getSession !== "function")) throw new TypeError("Provide a member session adapter.")
143
+ const bookingTokenAdapter = options.bookingTokenAdapter
144
+ if (bookingTokenAdapter !== undefined && (!object(bookingTokenAdapter) || typeof bookingTokenAdapter.getCredential !== "function" || tokenAdapter)) throw new TypeError("Provide either a member session adapter or a booking credential adapter.")
144
145
  const maximumBytes = options.maximumResponseBytes ?? 2 * 1024 * 1024
145
146
  if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1024 || maximumBytes > 16 * 1024 * 1024) throw new TypeError("Use a response limit between 1 KiB and 16 MiB.")
146
147
  const memberRoot = `/api/v1/o/${organizationSlug}/member`
148
+ const selfRoot = `/api/v1/o/${organizationSlug}/me`
149
+ const authRoot = `/api/v1/o/${organizationSlug}/auth`
147
150
 
148
151
  async function readSession() {
149
152
  if (!tokenAdapter) throw invalidSession()
150
153
  return verifySession(await tokenAdapter.getSession(), organizationSlug, organizationId)
151
154
  }
152
155
 
156
+ async function readBookingSession() {
157
+ if (!bookingTokenAdapter) return readSession()
158
+ const credential = await bookingTokenAdapter.getCredential()
159
+ if (!object(credential) || credential.organization_slug !== organizationSlug) throw invalidSession()
160
+ const claims = memberClaims(credential.access_token)
161
+ if (organizationId && claims.app_metadata.organization_id !== organizationId) throw invalidSession()
162
+ return { session: credential, identity: `${organizationSlug}:${claims.sub}:${claims.session_id}:${claims.app_metadata.organization_id}` }
163
+ }
164
+ function checkSignal(signal) {
165
+ if (signal?.aborted) { const error = new Error("The request was cancelled."); error.name = "AbortError"; throw error }
166
+ }
167
+ function checked(value, valid) { if (!valid(value)) throw invalidResponse(); return value }
168
+ function setupResult(value) {
169
+ if (value.ok !== true || value.organization_slug != null && value.organization_slug !== organizationSlug) throw invalidResponse()
170
+ return value
171
+ }
172
+
153
173
  async function request(path, { method = "GET", body, accessToken, signal, query, privateResponse = false, empty = false } = {}) {
174
+ checkSignal(signal)
154
175
  const url = new URL(path, origin)
155
176
  if (query) for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value)
156
177
  const headers = { accept: "application/json" }
157
- if (appKey !== undefined) headers["x-vuoma-app-key"] = appKey
158
178
  if (body !== undefined) headers["content-type"] = "application/json"
159
179
  if (accessToken) headers.authorization = `Bearer ${accessToken}`
160
180
  const response = await requestFetch(url.href, {
@@ -162,11 +182,13 @@ export function createVuomaClient(options) {
162
182
  cache: privateResponse ? "no-store" : "default",
163
183
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
164
184
  })
185
+ checkSignal(signal)
165
186
  // Custom fetch adapters must also honor the no-redirect destination boundary.
166
187
  if (response.redirected || (response.url && response.url !== url.href)) throw invalidResponse()
167
188
  const declared = Number(response.headers.get("content-length"))
168
189
  if (Number.isFinite(declared) && declared > maximumBytes) throw invalidResponse()
169
190
  const text = await response.text()
191
+ checkSignal(signal)
170
192
  if (utf8Length(text) > maximumBytes) throw invalidResponse()
171
193
  let value
172
194
  try { value = text ? JSON.parse(text) : null } catch { if (response.ok) throw invalidResponse() }
@@ -185,12 +207,14 @@ export function createVuomaClient(options) {
185
207
  return value
186
208
  }
187
209
 
188
- async function accountRequest(path, requestOptions = {}, parse = value => value) {
189
- const initial = await readSession()
190
- const value = await request(`${memberRoot}${path}`, { ...requestOptions, accessToken: initial.session.access_token, privateResponse: true })
191
- const current = await readSession()
210
+ async function accountRequest(path, requestOptions = {}, parse = value => value, booking = false) {
211
+ const read = booking ? readBookingSession : readSession
212
+ const initial = await read()
213
+ const value = await request(`${selfRoot}${path}`, { ...requestOptions, accessToken: initial.session.access_token, privateResponse: true })
214
+ const current = await read()
215
+ checkSignal(requestOptions.signal)
192
216
  if (current.identity !== initial.identity) throw new VuomaApiError(401, "member_session_changed", "Your signed-in account changed. Try again.")
193
- return parse(value, initial.session.customer.id)
217
+ return parse(value, initial.session.customer?.id)
194
218
  }
195
219
 
196
220
  function profile(value, customerId) {
@@ -225,24 +249,74 @@ export function createVuomaClient(options) {
225
249
  if (value.schedule.sessions.length > 250) throw invalidResponse()
226
250
  return value
227
251
  },
252
+ async getBookingCatalog({ signal } = {}) {
253
+ return checked(await request(`${memberRoot}/booking-catalog`, { signal }), validBookingCatalog)
254
+ },
255
+ async getAppointmentAvailability({ from, to, signal }) {
256
+ const range = availabilityRange({ from, to })
257
+ return checked(await request(`${memberRoot}/availability`, { query: range, signal }), value => validAvailability(value, range))
258
+ },
228
259
  async getCatalog({ includeCreditPurchases = false, signal } = {}) {
229
260
  return catalog(await request(`${memberRoot}/catalog`, { signal, query: includeCreditPurchases ? { include_credit_purchases: "true" } : undefined }))
230
261
  },
231
262
  }),
232
263
  auth: Object.freeze({
264
+ async requestAccountCode({ mode, email, signupIntent, displayName: name, signal }) {
265
+ accountInput({ mode, email, signupIntent })
266
+ const body = mode === "signup" ? { email, signup_intent: signupIntent, ...(["new", "auto"].includes(signupIntent) ? { display_name: displayName(name) } : {}) } : { email }
267
+ const value = setupResult(await request(`${authRoot}/${mode === "signup" ? "sign-up" : "recovery"}`, { method: "POST", body, signal, privateResponse: true }))
268
+ if (value.mode !== mode || value.requires_email_code !== true) throw invalidResponse()
269
+ return { ok: true, mode, requires_email_code: true }
270
+ },
271
+ async confirmAccountCode({ mode, email, signupIntent, code, signal }) {
272
+ accountInput({ mode, email, signupIntent })
273
+ const resolve = mode === "signup" && ["auto", "activate"].includes(signupIntent)
274
+ const path = mode === "signup" ? resolve ? "account" : "sign-up" : "recovery"
275
+ const value = setupResult(await request(`${authRoot}/${path}/confirm-code`, { method: "POST", body: { email, code: emailCode(code) }, signal, privateResponse: true }))
276
+ if (!validCapability(value.capability)) throw invalidResponse()
277
+ let resolution
278
+ try { if (resolve) resolution = accountResolution(value.account_resolution) } catch { throw invalidResponse() }
279
+ return { organization_slug: organizationSlug, value: value.capability, ...(resolution ? { account_resolution: resolution } : {}) }
280
+ },
281
+ async completePassword({ capability, password, signal }) {
282
+ if (!object(capability) || capability.organization_slug !== organizationSlug || !validCapability(capability.value)) throw invalidSession()
283
+ if (typeof password !== "string" || !password || password.length > 1024) throw new TypeError("Provide a password.")
284
+ setupResult(await request(`${authRoot}/password`, { method: "POST", body: { capability: capability.value, password, password_confirmation: password }, signal, privateResponse: true }))
285
+ },
233
286
  async signIn({ email, password, signal }) {
234
287
  if (typeof email !== "string" || !email.trim() || email.length > 320 || typeof password !== "string" || !password || password.length > 1024) throw new TypeError("Provide an email address and password.")
235
- return sessionResponse(await request(`${memberRoot}/auth/sign-in`, { method: "POST", body: { email, password }, signal, privateResponse: true }), organizationSlug, organizationId)
288
+ return sessionResponse(await request(`${authRoot}/sign-in`, { method: "POST", body: { email, password }, signal, privateResponse: true }), organizationSlug, organizationId)
236
289
  },
237
290
  async refresh({ refreshToken: token, signal }) {
238
- return sessionResponse(await request("/api/v1/member/auth/refresh", { method: "POST", body: { refresh_token: refreshToken(token) }, signal, privateResponse: true }), organizationSlug, organizationId)
291
+ return sessionResponse(await request(`${authRoot}/refresh`, { method: "POST", body: { refresh_token: refreshToken(token) }, signal, privateResponse: true }), organizationSlug, organizationId)
239
292
  },
240
293
  async signOut({ refreshToken: token, signal }) {
241
- await request("/api/v1/member/auth/sign-out", { method: "POST", body: { refresh_token: refreshToken(token) }, signal, privateResponse: true, empty: true })
294
+ await request(`${authRoot}/sign-out`, { method: "POST", body: { refresh_token: refreshToken(token) }, signal, privateResponse: true, empty: true })
242
295
  },
243
296
  }),
244
297
  member: Object.freeze({
245
- getProfile({ signal } = {}) { return accountRequest("/profile", { signal }, profile) },
298
+ bookClass(input, { signal } = {}) {
299
+ const body = bookingInput(input)
300
+ return accountRequest("/commerce/bookings", { method: "POST", body, signal }, value => checked(value, validBookingResult), true)
301
+ },
302
+ getCancellationQuote(id, { signal } = {}) {
303
+ bookingId(id)
304
+ return accountRequest(`/commerce/bookings/${id}/cancellation-quote`, { signal, query: { include_booking_policy_context: "true" } }, value => checked(value, result => result.api_version === 1 && result.booking_id === id && validQuote(result.quote)), true)
305
+ },
306
+ cancelBooking(id, { expectedFeeAmount, signal } = {}) {
307
+ bookingId(id)
308
+ const body = expectedFeeAmount === undefined ? {} : { expected_fee_amount: feeAmount(expectedFeeAmount) }
309
+ return accountRequest(`/commerce/bookings/${id}/cancel`, { method: "POST", body, signal }, value => checked(value, result => result.api_version === 1 && result.booking_id === id && result.status === "cancelled" && result.canceled === true && (result.cancellation_quote === null || validQuote(result.cancellation_quote))), true)
310
+ },
311
+ bookAppointmentVisit(input, { signal } = {}) {
312
+ const body = visitInput(input)
313
+ return accountRequest("/commerce/visits", { method: "POST", body, signal }, value => checked(value, result => validVisitResult(result, body)), true)
314
+ },
315
+ getAppointmentVisit(input, { signal } = {}) {
316
+ const body = visitInput(input)
317
+ return accountRequest(`/commerce/visits/${body.source_request_id}`, { signal }, value => checked(value, result => validVisitResult(result, body)), true)
318
+ },
319
+ getProfile({ signal } = {}) { return accountRequest("/profile", { signal, query: { view: "basic" } }, profile) },
246
320
  updateProfile(changes, { signal } = {}) {
247
321
  const allowed = new Set(["first_name", "last_name", "phone", "birth_date"])
248
322
  if (!object(changes) || !Object.keys(changes).length || Object.entries(changes).some(([key, value]) =>
@@ -0,0 +1,98 @@
1
+ // Reviewed public wire checks. No private package or server imports.
2
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value)
3
+ const uuid = value => typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value)
4
+ const integer = (value, min = 0, max = Number.MAX_SAFE_INTEGER) => Number.isSafeInteger(value) && value >= min && value <= max
5
+ const text = (value, max, min = 0) => typeof value === 'string' && value.length >= min && value.length <= max
6
+ const timestamp = value => text(value, 64) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(value) && Number.isFinite(Date.parse(value)) && new Date(`${value.slice(0, 10)}T00:00:00Z`).toISOString().slice(0, 10) === value.slice(0, 10)
7
+ const exact = (value, allowed) => object(value) && Object.keys(value).every(key => allowed.includes(key))
8
+ const selectionKeys = ['service_offering_id', 'starts_at', 'ends_at', 'credit_cost']
9
+ const selection = value => object(value) && uuid(value.service_offering_id) && timestamp(value.starts_at) && timestamp(value.ends_at) && integer(value.credit_cost, 1, 100_000) && Date.parse(value.ends_at) > Date.parse(value.starts_at) && Date.parse(value.ends_at) - Date.parse(value.starts_at) <= 86_400_000
10
+ const check = (condition, message = 'Invalid member request.') => { if (!condition) throw new TypeError(message) }
11
+
12
+ export function bookingInput(value) {
13
+ const fields = ['service_session_id', 'entitlement_id', 'source_request_id', 'policy_version', 'policy_consent', 'payment_authorized', 'payment_authorization_id']
14
+ check(exact(value, fields))
15
+ const result = { ...value }
16
+ for (const key of ['service_session_id', 'source_request_id', 'entitlement_id']) {
17
+ if (key === 'entitlement_id' && result[key] == null) continue
18
+ check(typeof result[key] === 'string')
19
+ result[key] = result[key].trim().toLowerCase()
20
+ check(uuid(result[key]))
21
+ }
22
+ const policy = fields.slice(3)
23
+ if (policy.some(key => key in result)) check(uuid(result.policy_version) && result.policy_consent === true && result.payment_authorized === true && uuid(result.payment_authorization_id))
24
+ return result
25
+ }
26
+ export function visitInput(value) {
27
+ check(exact(value, ['source_request_id', 'appointments']) && uuid(value.source_request_id) && Array.isArray(value.appointments) && value.appointments.length >= 1 && value.appointments.length <= 10)
28
+ let previousEnd = null
29
+ const appointments = value.appointments.map(item => {
30
+ check(exact(item, selectionKeys) && selection(item) && (previousEnd === null || Date.parse(item.starts_at) >= previousEnd))
31
+ previousEnd = Date.parse(item.ends_at)
32
+ return { ...item }
33
+ })
34
+ return { source_request_id: value.source_request_id, appointments }
35
+ }
36
+ export function bookingId(value) { check(uuid(value), 'Use a valid booking ID.'); return value }
37
+ export function feeAmount(value) { check(integer(value), 'Use a nonnegative fee amount in minor currency units.'); return value }
38
+ export function validBookingResult(value) {
39
+ return object(value) && value.api_version === 1 && object(value.booking) && uuid(value.booking.id) && ['confirmed', 'waitlisted', 'cancelled', 'attended', 'no_show'].includes(value.booking.status) && object(value.redemption) && uuid(value.redemption.id) && ['waitlisted', 'reserved', 'consumed', 'released', 'reversed'].includes(value.redemption.status)
40
+ }
41
+ export function validQuote(quote) {
42
+ if (!object(quote) || !(quote.policy_version === null || uuid(quote.policy_version)) || !integer(quote.fee_amount) || !/^[a-z]{3}$/u.test(quote.currency ?? '') || !timestamp(quote.quoted_at) ||
43
+ !['can_cancel', 'late', 'grace_applies', 'demand_observed', 'waive_if_filled', 'credit_return'].every(key => typeof quote[key] === 'boolean') ||
44
+ !['late_cancellation_deadline', 'grace_until'].every(key => quote[key] === null || timestamp(quote[key])) ||
45
+ !['no_policy', 'fees_disabled', 'legacy_booking', 'on_time', 'waitlisted', 'booking_grace', 'quiet_class', 'late_fee_if_unfilled', 'seat_refilled', 'teacher_assignment', 'started'].includes(quote.reason)) return false
46
+ if (quote.fee_amount > 0 && (!quote.can_cancel || !quote.late || quote.grace_applies || !quote.policy_version || quote.reason !== 'late_fee_if_unfilled')) return false
47
+ return !quote.grace_applies || Boolean(quote.grace_until)
48
+ }
49
+ export function validVisitResult(value, request) {
50
+ if (!object(value) || value.api_version !== 1 || !uuid(value.visit_id) || value.source_request_id !== request.source_request_id || !integer(value.credit_cost, 1) || !Array.isArray(value.bookings) || value.bookings.length !== request.appointments.length) return false
51
+ const bookingIds = new Set(), sessionIds = new Set()
52
+ let total = 0
53
+ for (const [index, booking] of value.bookings.entries()) {
54
+ const expected = request.appointments[index]
55
+ if (!selection(booking) || !uuid(booking.booking_id) || !uuid(booking.service_session_id) || !['confirmed', 'cancelled', 'attended', 'no_show'].includes(booking.status) || bookingIds.has(booking.booking_id) || sessionIds.has(booking.service_session_id) || booking.service_offering_id !== expected.service_offering_id || Date.parse(booking.starts_at) !== Date.parse(expected.starts_at) || Date.parse(booking.ends_at) !== Date.parse(expected.ends_at) || booking.credit_cost !== expected.credit_cost) return false
56
+ bookingIds.add(booking.booking_id); sessionIds.add(booking.service_session_id); total += booking.credit_cost
57
+ }
58
+ return total === value.credit_cost
59
+ }
60
+ const location = value => object(value) && /^[a-z]{8}$/u.test(value.public_id ?? '') && text(value.name, 160, 1) && text(value.timezone, 100, 1) && ['city', 'region'].every(key => value[key] === null || text(value[key], 4000))
61
+ export function availabilityRange({ from, to }) {
62
+ check(timestamp(from) && timestamp(to) && Date.parse(to) > Date.parse(from) && Date.parse(to) - Date.parse(from) <= 14 * 86_400_000, 'Choose RFC 3339 timestamps spanning at most 14 days.')
63
+ return { from: new Date(from).toISOString(), to: new Date(to).toISOString() }
64
+ }
65
+ export function validAvailability(value, range) {
66
+ if (!object(value) || value.api_version !== 1 || !timestamp(value.generated_at) || !object(value.range) || value.range.from !== range.from || value.range.to !== range.to || typeof value.has_more !== 'boolean' || !Array.isArray(value.slots) || value.slots.length > 20_000) return false
67
+ const ids = new Set()
68
+ for (const slot of value.slots) {
69
+ if (!selection(slot) || !uuid(slot.service_id) || !integer(slot.duration_minutes, 1, 1440) || Date.parse(slot.ends_at) - Date.parse(slot.starts_at) !== slot.duration_minutes * 60_000 || !location(slot.location) || !object(slot.service) || slot.service.id !== slot.service_id || !text(slot.service.name, 160, 1) || !(slot.service.description === null || text(slot.service.description, 4000)) || slot.candidate_id !== `appointment:${slot.service_offering_id}:${slot.duration_minutes}:${Date.parse(slot.starts_at)}` || Date.parse(slot.starts_at) < Date.parse(range.from) || Date.parse(slot.starts_at) >= Date.parse(range.to) || ids.has(slot.candidate_id)) return false
70
+ ids.add(slot.candidate_id)
71
+ }
72
+ return true
73
+ }
74
+ export function validBookingCatalog(value) {
75
+ if (!object(value) || value.api_version !== 1 || !timestamp(value.generated_at) || !object(value.experience) || !['classes', 'appointments'].every(key => typeof value.experience[key] === 'boolean') || !['classes', 'appointments'].includes(value.experience.default_view) || !Array.isArray(value.locations) || value.locations.length > 1000 || !value.locations.every(location) || !Array.isArray(value.services) || value.services.length > 1000 || (value.cancellation_notice_minutes !== undefined && !integer(value.cancellation_notice_minutes, 0, 10080))) return false
76
+ const locations = new Set(value.locations.map(item => item.public_id)), services = new Set(), offerings = new Set()
77
+ if (locations.size !== value.locations.length || value.experience.appointments !== (value.services.length > 0) || (value.experience.classes || value.experience.appointments) && !value.experience[value.experience.default_view]) return false
78
+ for (const service of value.services) {
79
+ if (!object(service) || !uuid(service.id) || services.has(service.id) || !text(service.name, 160, 1) || !(service.description === null || text(service.description, 4000)) || !Array.isArray(service.offerings) || service.offerings.length < 1 || service.offerings.length > 1000) return false
80
+ services.add(service.id)
81
+ for (const offering of service.offerings) {
82
+ if (!object(offering) || !uuid(offering.id) || offerings.has(offering.id) || !integer(offering.duration_minutes, 1, 1440) || !locations.has(offering.location_public_id) || !(offering.credit_cost === null || integer(offering.credit_cost, 1, 100000))) return false
83
+ offerings.add(offering.id)
84
+ }
85
+ }
86
+ return true
87
+ }
88
+ export function accountInput({ mode, email, signupIntent }) {
89
+ check(['signup', 'recovery'].includes(mode) && typeof email === 'string' && email.trim().includes('@') && email.length <= 320 && !/[\u0000-\u001f\u007f]/u.test(email), 'Provide a valid account mode and email address.')
90
+ if (mode === 'signup') check(['new', 'migrate', 'auto', 'activate'].includes(signupIntent), 'Choose a signup intent explicitly.')
91
+ }
92
+ export function displayName(value) { check(text(value, 160, 1) && Boolean(value.trim()) && !/[\u0000-\u001f\u007f]/u.test(value), 'Provide a display name.'); return value }
93
+ export function emailCode(value) { check(typeof value === 'string' && /^\d{6}$/u.test(value.trim()), 'Provide the six-digit email code.'); return value.trim() }
94
+ export function validCapability(value) { return typeof value === 'string' && /^[A-Za-z0-9_-]{43}$/u.test(value) }
95
+ export function accountResolution(value) {
96
+ check(object(value) && ['existing_account', 'existing_profile', 'new_profile'].includes(value.kind) && text(value.display_name, 160, 1) && Boolean(value.display_name.trim()) && !/[\u0000-\u001f\u007f]/u.test(value.display_name) && text(value.masked_email, 320) && /^[^@\s*]\*{3}@[^@\s*]\*{3}(?:\.[a-z0-9.-]+)?$/iu.test(value.masked_email) && (value.masked_phone === null || /^\*{3} \*{3} [0-9]{4}$/u.test(value.masked_phone ?? '')))
97
+ return { kind: value.kind, display_name: value.display_name, masked_email: value.masked_email, masked_phone: value.masked_phone }
98
+ }
@@ -0,0 +1,31 @@
1
+ // Generated from reviewed API contracts. Run npm run api:generate; do not edit.
2
+ export interface OrganizationReadInputs {
3
+ "organization.read": Record<string, never>
4
+ "locations.read": Record<string, never>
5
+ "services.read": Record<string, never>
6
+ "schedule.read": { "week"?: string; "month"?: string }
7
+ "users.directory.read": { "q"?: string; "limit"?: number; "offset"?: number; "facet"?: "all" | "customers"; "customer_status"?: "active" | "inactive" | "archived" }
8
+ "task_lists.read": Record<string, never>
9
+ "inventory.read": Record<string, never>
10
+ }
11
+ export interface OrganizationReadResults {
12
+ "organization.read": { "api_version": 1; "data": { "organization": { "id": string; "slug": string; "name": string; "timezone": string; "currency_code": string; "brand_name"?: (string | null); [key: string]: unknown }; "access": { [key: string]: unknown } } }
13
+ "locations.read": { "api_version": 1; "data": { "locations": Array<{ "id": string; "name": string; "timezone": string; "status": string; "formatted_address"?: (string | null); [key: string]: unknown }> } }
14
+ "services.read": { "api_version": 1; "data": { "services": Array<{ "id": string; "name": string; "description"?: (string | null); "duration_minutes": number; "default_capacity"?: (number | null); "status": "active" | "inactive"; [key: string]: unknown }>; "past_session_variations": Array<{ [key: string]: unknown }> } }
15
+ "schedule.read": { "api_version": 1; "data": { "week_start": string; "sessions": Array<{ [key: string]: unknown }>; "staffing_windows": Array<{ [key: string]: unknown }>; "staffing_load_error": (string | null); "calendar": { "month": string; "days": Array<{ [key: string]: unknown }>; [key: string]: unknown } } }
16
+ "users.directory.read": { "api_version": 1; "data": { "users": Array<{ "id": string; "display_name": (string | null); "email"?: (string | null); "phone_e164"?: (string | null); "is_customer"?: boolean; "is_staff"?: boolean; "has_workspace_access"?: boolean; "person_status"?: "active" | "archived"; "customer_status"?: ("active" | "inactive" | "archived" | null) }>; "filters": { "facet": "all" | "customers"; "customer_status": ("active" | "inactive" | "archived" | null) }; "pagination": { "total": number; "limit": number; "offset": number; "has_more": boolean; "query": string } } }
17
+ "task_lists.read": { "api_version": 1; "data": { "task_lists": Array<{ [key: string]: unknown }>; "locations": Array<{ [key: string]: unknown }>; "can_manage_organization": boolean; [key: string]: unknown } }
18
+ "inventory.read": { "api_version": 1; "data": { "inventory": { "locations": Array<{ [key: string]: unknown }>; "stocks": Array<{ [key: string]: unknown }>; "alerts": Array<{ [key: string]: unknown }>; "movements": Array<{ [key: string]: unknown }>; "currency_code": string; [key: string]: unknown } } }
19
+ }
20
+ export type OrganizationReadOperationId = keyof OrganizationReadInputs
21
+ export interface OrganizationWriteInputs {
22
+ "messages.send": { "path"?: Record<string, never>; "body": { "channel_id": string; "body": string; "attachment_ids"?: Array<string>; "title"?: string; "preview"?: unknown; "action"?: unknown } }
23
+ "messages.read_state.update": { "path": { "channel_id": string }; "body": { "message_id": string } }
24
+ "messages.mute.update": { "path": { "channel_id": string }; "body": { "is_muted": boolean } }
25
+ }
26
+ export interface OrganizationWriteResults {
27
+ "messages.send": { "api_version": 1; "message": { "attachments"?: Array<{ "file_name": string; "content_type": "image/jpeg" | "image/png" | "image/webp" | "image/gif" | "image/heic" | "application/pdf" | "text/plain" | "text/csv" | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | "application/vnd.openxmlformats-officedocument.presentationml.presentation"; "byte_size": number; "id": string }>; "id": string; "body": string; "kind": "announcement" | "deleted" | "notification"; "notification": ({ "source_kind": "service_session_work_slot" | "service_session_provider" | "staffing_window_assignment" | "staffing_window"; "source_id": string; "session_id": (string | null); "session_starts_at": (string | null); "staffing_window_id": (string | null); "starts_at": string; "time_zone": string; "status": "open" | "claimed" | "withdrawn" | "cancelled"; "label": string } | null); "author_name": string; "is_mine": boolean; "created_at": string; "edited_at": (string | null); "deleted_at": (string | null) } }
28
+ "messages.read_state.update": { "api_version": 1; "state": { "channel_id": string; "message_id": string; "read_at": string } }
29
+ "messages.mute.update": { "api_version": 1; "state": { "channel_id": string; "is_muted": boolean } }
30
+ }
31
+ export type OrganizationWriteOperationId = keyof OrganizationWriteInputs