@checkcourt/sdk 0.4.0 → 0.6.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/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export { createCheckCourtClient, unwrap, retryAfterMs, TENANT_HEADER } from "./c
2
2
  export type { CheckCourtClient, CheckCourtClientOptions, RetryOptions } from "./client.js";
3
3
  export { apiKeyAuth, installationAuth, userAuth } from "./auth.js";
4
4
  export { getInstallation, getMe, type AppInstallation, type Me } from "./installation.js";
5
+ export { deleteObjectMetadata, getObjectMetadata, publishAppEvent, putObjectMetadata, type ObjectMetadata, type ObjectMetadataEntry, type ObjectMetadataWritten, type PublishAppEventRequest, type PublishedAppEvent, } from "./connections.js";
6
+ export { sendNotification, NOTIFICATION_TITLE_MAX, NOTIFICATION_BODY_MAX, type SendNotificationInput, type SendNotificationResponse, } from "./notifications.js";
5
7
  export type { AuthContext, AuthStrategy, InstallationAuth, StoredUserTokens, UserAuth } from "./auth.js";
6
8
  export * from "./errors.js";
7
9
  export * from "./oauth.js";
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { createCheckCourtClient, unwrap, retryAfterMs, TENANT_HEADER } from "./client.js";
2
2
  export { apiKeyAuth, installationAuth, userAuth } from "./auth.js";
3
3
  export { getInstallation, getMe } from "./installation.js";
4
+ export { deleteObjectMetadata, getObjectMetadata, publishAppEvent, putObjectMetadata, } from "./connections.js";
5
+ export { sendNotification, NOTIFICATION_TITLE_MAX, NOTIFICATION_BODY_MAX, } from "./notifications.js";
4
6
  export * from "./errors.js";
5
7
  export * from "./oauth.js";
6
8
  export * from "./webhooks.js";
@@ -32,6 +32,30 @@ export declare const EXTENSION_POINTS: {
32
32
  readonly targets: readonly ["tenant"];
33
33
  readonly kinds: readonly ["declarative"];
34
34
  };
35
+ readonly "court.annotation": {
36
+ readonly targets: readonly ["tenant"];
37
+ readonly kinds: readonly ["declarative"];
38
+ };
39
+ readonly "member.list.column": {
40
+ readonly targets: readonly ["tenant"];
41
+ readonly kinds: readonly ["declarative"];
42
+ };
43
+ readonly "member.settings.section": {
44
+ readonly targets: readonly ["user"];
45
+ readonly kinds: readonly ["declarative", "iframe"];
46
+ };
47
+ readonly "booking.hint": {
48
+ readonly targets: readonly ["tenant"];
49
+ readonly kinds: readonly ["declarative"];
50
+ };
51
+ readonly "booking_plan.action": {
52
+ readonly targets: readonly ["tenant"];
53
+ readonly kinds: readonly ["declarative"];
54
+ };
55
+ readonly "sidebar.action": {
56
+ readonly targets: readonly ["tenant", "user"];
57
+ readonly kinds: readonly ["declarative"];
58
+ };
35
59
  };
36
60
  export type ExtensionPoint = keyof typeof EXTENSION_POINTS;
37
61
  /** Scope the installation needs (in `tenantScopes` or `userScopes`) before the point hands it a subject id. */
@@ -42,21 +66,47 @@ export declare const EXTENSION_POINT_SCOPE: {
42
66
  readonly "member.profile.section": "members:read";
43
67
  readonly "dashboard.widget": null;
44
68
  readonly "kiosk.tile": null;
69
+ readonly "court.annotation": "courts:read";
70
+ readonly "member.list.column": "members:read";
71
+ readonly "member.settings.section": null;
72
+ readonly "booking.hint": "bookings:read";
73
+ readonly "booking_plan.action": "courts:read";
74
+ readonly "sidebar.action": null;
45
75
  };
76
+ /** Buttons CheckCourt draws from the manifest alone, before the app is ever called. */
77
+ export declare const STATIC_ACTION_POINTS: readonly ["booking_plan.action", "sidebar.action"];
78
+ export type StaticActionPoint = (typeof STATIC_ACTION_POINTS)[number];
79
+ /** Longest `label` of a static action. */
80
+ export declare const STATIC_ACTION_LABEL_MAX = 24;
81
+ /** lucide icon names a static action may use as `icon`. */
82
+ export declare const APP_ACTION_ICONS: readonly ["bell", "calendar", "calendar-check", "camera", "chart-column", "circle-help", "clipboard-list", "clock", "cloud-rain", "door-open", "file-text", "flag", "heart-pulse", "info", "key-round", "lightbulb", "link", "list-checks", "lock-open", "map-pin", "megaphone", "message-square", "receipt", "send", "sparkles", "star", "sun", "thermometer", "ticket", "triangle-alert", "trophy", "user-round", "users", "wallet", "wrench"];
83
+ export type AppActionIcon = (typeof APP_ACTION_ICONS)[number];
46
84
  /** Every scope an app may request. Role, app, webhook, key, billing and AVV management are reserved for people. */
47
- export declare const GRANTABLE_SCOPES: readonly ["courts:read", "courts:read_confidential", "courts:write", "bookings:read", "bookings:read_confidential", "bookings:write", "bookings:cancel", "bookings:edit", "bookings:export", "members:read", "members:read_confidential", "members:invite", "members:write", "members:delete", "teams:read", "teams:write", "policies:read", "policies:read_confidential", "policies:write", "categories:read", "categories:write", "invites:read", "invites:write", "settings:read", "settings:read_confidential", "settings:write", "guest_fees:read", "guest_fees:write", "audit:read", "announcements:read", "announcements:write", "events:read", "events:write", "posts:read", "posts:write", "posts:moderate", "court_layout:read", "court_layout:write", "work_hours:read", "work_hours:write", "work_hours:manage", "compliance:read", "kiosk:manage", "trainer_blocks:write", "analytics:read", "embeds:manage", "webhooks:read"];
85
+ export declare const GRANTABLE_SCOPES: readonly ["courts:read", "courts:read_confidential", "courts:write", "bookings:read", "bookings:read_confidential", "bookings:write", "bookings:cancel", "bookings:edit", "bookings:export", "members:read", "members:read_confidential", "members:invite", "members:write", "members:delete", "teams:read", "teams:write", "policies:read", "policies:read_confidential", "policies:write", "categories:read", "categories:write", "invites:read", "invites:write", "settings:read", "settings:read_confidential", "settings:write", "guest_fees:read", "guest_fees:write", "audit:read", "announcements:read", "announcements:write", "events:read", "events:write", "posts:read", "posts:write", "posts:moderate", "court_layout:read", "court_layout:write", "work_hours:read", "work_hours:write", "work_hours:manage", "compliance:read", "kiosk:manage", "trainer_blocks:write", "analytics:read", "embeds:manage", "webhooks:read", "notifications:send"];
48
86
  export type GrantableScope = (typeof GRANTABLE_SCOPES)[number];
49
87
  type ExtensionOf<P extends ExtensionPoint> = {
50
88
  point: P;
51
89
  kind: (typeof EXTENSION_POINTS)[P]["kinds"][number];
52
90
  /** https; http://localhost is accepted outside production. */
53
91
  url: string;
54
- } & (P extends "booking.action" ? {
92
+ } & (P extends StaticActionPoint ? {
93
+ /** Button text, 1 to 24 characters (`STATIC_ACTION_LABEL_MAX`). */
94
+ label: string;
95
+ icon?: AppActionIcon;
96
+ } : P extends "booking.action" ? {
55
97
  label: string;
98
+ icon?: never;
99
+ } : P extends "member.list.column" ? {
100
+ label?: string;
101
+ icon?: never;
56
102
  } : {
57
103
  label?: string;
104
+ icon?: never;
58
105
  });
59
- /** One entry of `extensions`; `kind` and `label` are checked per point at compile time. */
106
+ /**
107
+ * One entry of `extensions`; `kind`, `label` and `icon` are checked per point at compile time.
108
+ * Every point may appear once, except `booking.action`.
109
+ */
60
110
  export type ManifestExtension = {
61
111
  [P in ExtensionPoint]: ExtensionOf<P>;
62
112
  }[ExtensionPoint];
@@ -97,6 +147,72 @@ export interface DataProcessing {
97
147
  storageLocation: "EU" | "non-EU";
98
148
  avvRequired: boolean;
99
149
  }
150
+ /** Club objects apps can attach metadata to. */
151
+ export declare const SHARED_OBJECT_TYPES: readonly ["booking", "court", "member"];
152
+ export type SharedObjectType = (typeof SHARED_OBJECT_TYPES)[number];
153
+ /** Read scope both the sharing and the reading app need in `tenantScopes` for an object type. */
154
+ export declare const SHARED_OBJECT_SCOPE: {
155
+ readonly booking: "bookings:read";
156
+ readonly court: "courts:read";
157
+ readonly member: "members:read";
158
+ };
159
+ /** Metadata keys and event names: `^[a-z][a-z0-9_]{0,39}$`. */
160
+ export declare const CONNECTION_NAME_PATTERN: RegExp;
161
+ /** Serialized size limit of a metadata value and of event data. */
162
+ export declare const MAX_SHARED_VALUE_BYTES = 4096;
163
+ /** A metadata key the app writes on club objects and lets other apps read once the club approves. */
164
+ export interface SharedMetadata {
165
+ key: string;
166
+ object: SharedObjectType;
167
+ /** Shown to the club when it approves a connection, 1 to 200 characters, e.g. "Videolink". */
168
+ description: string;
169
+ /** 1 to 10 categories, shown in the approval dialog, e.g. ["Videoaufzeichnung"]. */
170
+ data_categories: readonly string[];
171
+ }
172
+ /** Metadata of another app this app wants to read: that app's slug, the key and the object type. */
173
+ export interface ReadMetadata {
174
+ app: string;
175
+ key: string;
176
+ object: SharedObjectType;
177
+ }
178
+ export type AppEventProperty = {
179
+ type: "string";
180
+ description?: string;
181
+ /** Excludes `maxLength`. */
182
+ enum?: readonly string[];
183
+ maxLength?: number;
184
+ } | {
185
+ type: "number" | "integer";
186
+ description?: string;
187
+ minimum?: number;
188
+ maximum?: number;
189
+ } | {
190
+ type: "boolean";
191
+ description?: string;
192
+ };
193
+ /** JSON-Schema subset for event data: flat objects of strings, numbers, integers and booleans (at most 30 properties). */
194
+ export interface AppEventSchema {
195
+ type: "object";
196
+ /** Keys match `^[a-zA-Z][a-zA-Z0-9_]{0,39}$`. */
197
+ properties: Record<string, AppEventProperty>;
198
+ required?: readonly string[];
199
+ /** `false` rejects unknown keys. */
200
+ additionalProperties?: boolean;
201
+ }
202
+ /** An event the app publishes with `publishAppEvent`; subscribers receive it as `app.<your slug>.<name>`. */
203
+ export interface EmittedEvent {
204
+ name: string;
205
+ /** 1 to 200 characters, shown in the approval dialog. */
206
+ description: string;
207
+ data_categories: readonly string[];
208
+ /** Published data is validated against it. */
209
+ schema?: AppEventSchema;
210
+ }
211
+ /** An event of another app this app wants to receive. */
212
+ export interface SubscribedEvent {
213
+ app: string;
214
+ event: string;
215
+ }
100
216
  export interface Manifest {
101
217
  manifestVersion?: typeof MANIFEST_VERSION;
102
218
  installTargets: readonly InstallTarget[];
@@ -110,6 +226,18 @@ export interface Manifest {
110
226
  extensions?: readonly ManifestExtension[];
111
227
  settingsSchema?: SettingsSchema;
112
228
  dataProcessing: DataProcessing;
229
+ /** Metadata other apps may read (at most 20). Club installations only. */
230
+ shares?: {
231
+ metadata: readonly SharedMetadata[];
232
+ };
233
+ /** Metadata of other apps this app reads (at most 50). Club installations only. */
234
+ reads?: {
235
+ metadata: readonly ReadMetadata[];
236
+ };
237
+ /** Events this app publishes to connected apps (at most 20). Club installations only. */
238
+ emits?: readonly EmittedEvent[];
239
+ /** Events of other apps this app receives (at most 50). Club installations only. */
240
+ subscribes?: readonly SubscribedEvent[];
113
241
  }
114
242
  /**
115
243
  * Identity function that gives editor completion and compile-time checks for a manifest.
package/dist/manifest.js CHANGED
@@ -10,6 +10,12 @@ export const EXTENSION_POINTS = {
10
10
  "dashboard.widget": { targets: ["tenant", "user"], kinds: ["declarative"] },
11
11
  "booking.action": { targets: ["tenant", "user"], kinds: ["declarative"] },
12
12
  "kiosk.tile": { targets: ["tenant"], kinds: ["declarative"] },
13
+ "court.annotation": { targets: ["tenant"], kinds: ["declarative"] },
14
+ "member.list.column": { targets: ["tenant"], kinds: ["declarative"] },
15
+ "member.settings.section": { targets: ["user"], kinds: ["declarative", "iframe"] },
16
+ "booking.hint": { targets: ["tenant"], kinds: ["declarative"] },
17
+ "booking_plan.action": { targets: ["tenant"], kinds: ["declarative"] },
18
+ "sidebar.action": { targets: ["tenant", "user"], kinds: ["declarative"] },
13
19
  };
14
20
  /** Scope the installation needs (in `tenantScopes` or `userScopes`) before the point hands it a subject id. */
15
21
  export const EXTENSION_POINT_SCOPE = {
@@ -19,7 +25,55 @@ export const EXTENSION_POINT_SCOPE = {
19
25
  "member.profile.section": "members:read",
20
26
  "dashboard.widget": null,
21
27
  "kiosk.tile": null,
28
+ "court.annotation": "courts:read",
29
+ "member.list.column": "members:read",
30
+ "member.settings.section": null,
31
+ "booking.hint": "bookings:read",
32
+ "booking_plan.action": "courts:read",
33
+ "sidebar.action": null,
22
34
  };
35
+ /** Buttons CheckCourt draws from the manifest alone, before the app is ever called. */
36
+ export const STATIC_ACTION_POINTS = ["booking_plan.action", "sidebar.action"];
37
+ /** Longest `label` of a static action. */
38
+ export const STATIC_ACTION_LABEL_MAX = 24;
39
+ /** lucide icon names a static action may use as `icon`. */
40
+ export const APP_ACTION_ICONS = [
41
+ "bell",
42
+ "calendar",
43
+ "calendar-check",
44
+ "camera",
45
+ "chart-column",
46
+ "circle-help",
47
+ "clipboard-list",
48
+ "clock",
49
+ "cloud-rain",
50
+ "door-open",
51
+ "file-text",
52
+ "flag",
53
+ "heart-pulse",
54
+ "info",
55
+ "key-round",
56
+ "lightbulb",
57
+ "link",
58
+ "list-checks",
59
+ "lock-open",
60
+ "map-pin",
61
+ "megaphone",
62
+ "message-square",
63
+ "receipt",
64
+ "send",
65
+ "sparkles",
66
+ "star",
67
+ "sun",
68
+ "thermometer",
69
+ "ticket",
70
+ "triangle-alert",
71
+ "trophy",
72
+ "user-round",
73
+ "users",
74
+ "wallet",
75
+ "wrench",
76
+ ];
23
77
  /** Every scope an app may request. Role, app, webhook, key, billing and AVV management are reserved for people. */
24
78
  export const GRANTABLE_SCOPES = [
25
79
  "courts:read",
@@ -69,7 +123,21 @@ export const GRANTABLE_SCOPES = [
69
123
  "analytics:read",
70
124
  "embeds:manage",
71
125
  "webhooks:read",
126
+ // App-only: no club role holds it; a member app gets it from the member's consent.
127
+ "notifications:send",
72
128
  ];
129
+ /** Club objects apps can attach metadata to. */
130
+ export const SHARED_OBJECT_TYPES = ["booking", "court", "member"];
131
+ /** Read scope both the sharing and the reading app need in `tenantScopes` for an object type. */
132
+ export const SHARED_OBJECT_SCOPE = {
133
+ booking: "bookings:read",
134
+ court: "courts:read",
135
+ member: "members:read",
136
+ };
137
+ /** Metadata keys and event names: `^[a-z][a-z0-9_]{0,39}$`. */
138
+ export const CONNECTION_NAME_PATTERN = /^[a-z][a-z0-9_]{0,39}$/;
139
+ /** Serialized size limit of a metadata value and of event data. */
140
+ export const MAX_SHARED_VALUE_BYTES = 4096;
73
141
  /**
74
142
  * Identity function that gives editor completion and compile-time checks for a manifest.
75
143
  * CheckCourt validates the rest on upload (scope and event pairing, URL rules, lengths).
@@ -0,0 +1,29 @@
1
+ import { type CheckCourtClient } from "./client.js";
2
+ import type { components } from "./generated/schema.js";
3
+ export type SendNotificationResponse = components["schemas"]["SendNotificationResponse"];
4
+ export declare const NOTIFICATION_TITLE_MAX = 80;
5
+ export declare const NOTIFICATION_BODY_MAX = 500;
6
+ export interface SendNotificationInput {
7
+ /**
8
+ * Who gets it: the CheckCourt user id (club apps with `members:read`), the `psn_…` pseudonym
9
+ * from an extension context, or the pairwise `usr_…` id from `getMe`. A member app may only
10
+ * notify its own member. Anything else answers 404.
11
+ */
12
+ recipient: string;
13
+ /** One line of plain text, 1 to 80 characters. */
14
+ title: string;
15
+ /** Plain text, 1 to 500 characters; line breaks are kept. */
16
+ body: string;
17
+ /** A path inside CheckCourt (`/booking?date=…`) or an https link on an origin your app registered. */
18
+ url?: string;
19
+ /** Your own label for the kind of message, e.g. `reminder`. */
20
+ category?: string;
21
+ /** The same key within 24 hours returns the first notification instead of sending again. */
22
+ idempotencyKey?: string;
23
+ }
24
+ /**
25
+ * `POST /api/v1/app/notifications`: CheckCourt delivers a message to one member, in CheckCourt and
26
+ * by email if they allow it. Needs `notifications:send`. The answer never says whether the member
27
+ * muted your app.
28
+ */
29
+ export declare function sendNotification(client: CheckCourtClient, input: SendNotificationInput): Promise<SendNotificationResponse>;
@@ -0,0 +1,14 @@
1
+ import { unwrap } from "./client.js";
2
+ export const NOTIFICATION_TITLE_MAX = 80;
3
+ export const NOTIFICATION_BODY_MAX = 500;
4
+ /**
5
+ * `POST /api/v1/app/notifications`: CheckCourt delivers a message to one member, in CheckCourt and
6
+ * by email if they allow it. Needs `notifications:send`. The answer never says whether the member
7
+ * muted your app.
8
+ */
9
+ export async function sendNotification(client, input) {
10
+ const { idempotencyKey, ...rest } = input;
11
+ return unwrap(client.POST("/app/notifications", {
12
+ body: { ...rest, ...(idempotencyKey !== undefined ? { idempotency_key: idempotencyKey } : {}) },
13
+ }));
14
+ }
package/dist/ui.d.ts CHANGED
@@ -62,6 +62,10 @@ export type UiTextField = {
62
62
  default?: string;
63
63
  required?: boolean;
64
64
  max_length?: number;
65
+ /** Renders a multi-line text box instead of a single-line input. The value stays a string. */
66
+ multiline?: boolean;
67
+ /** Visible rows of the text box when `multiline` is set; 1 to 12. */
68
+ rows?: number;
65
69
  };
66
70
  export type UiNumberField = {
67
71
  type: "number";
@@ -89,7 +93,33 @@ export type UiSwitchField = {
89
93
  label: string;
90
94
  default?: boolean;
91
95
  };
92
- export type UiFormField = UiTextField | UiNumberField | UiSelectField | UiSwitchField;
96
+ export type UiDateField = {
97
+ type: "date";
98
+ name: string;
99
+ label: string;
100
+ /** YYYY-MM-DD. */
101
+ default?: string;
102
+ /** YYYY-MM-DD. */
103
+ min?: string;
104
+ /** YYYY-MM-DD. */
105
+ max?: string;
106
+ required?: boolean;
107
+ };
108
+ export type UiTimeField = {
109
+ type: "time";
110
+ name: string;
111
+ label: string;
112
+ /** HH:MM, 24-hour. */
113
+ default?: string;
114
+ /** HH:MM, 24-hour. */
115
+ min?: string;
116
+ /** HH:MM, 24-hour. */
117
+ max?: string;
118
+ required?: boolean;
119
+ /** Granularity of the time picker in minutes. */
120
+ step?: number;
121
+ };
122
+ export type UiFormField = UiTextField | UiNumberField | UiSelectField | UiSwitchField | UiDateField | UiTimeField;
93
123
  export type UiFormBlock = {
94
124
  type: "form";
95
125
  fields: UiFormField[];
@@ -137,6 +167,65 @@ export interface UiHiddenDocument {
137
167
  }
138
168
  /** Anything a declarative extension may answer with. */
139
169
  export type UiResponse = UiDocument | UiHiddenDocument;
170
+ /** Longest badge label of a `court.annotation`. */
171
+ export declare const ANNOTATION_LABEL_MAX = 24;
172
+ /** Badges CheckCourt shows per court across all apps, by app name; the rest is not shown. */
173
+ export declare const MAX_ANNOTATIONS_PER_COURT = 2;
174
+ /** Longest `column.title` of a `member.list.column`. */
175
+ export declare const COLUMN_TITLE_MAX = 20;
176
+ /** Longest cell text of a `member.list.column`. */
177
+ export declare const COLUMN_TEXT_MAX = 24;
178
+ /** App columns CheckCourt shows on the member list, by app name. */
179
+ export declare const MAX_APP_COLUMNS = 2;
180
+ /** App entries CheckCourt shows in the sidebar, by app name. */
181
+ export declare const MAX_SIDEBAR_ACTIONS = 2;
182
+ /** CheckCourt waits this long for a `booking.hint` answer, then shows nothing. */
183
+ export declare const BOOKING_HINT_TIMEOUT_MS = 1000;
184
+ /** Block types a `booking.hint` document may contain, top level only. */
185
+ export declare const BOOKING_HINT_BLOCKS: readonly ["text", "badge", "key_value", "link"];
186
+ export declare const MAX_HINT_BLOCKS = 6;
187
+ /** Apps whose hints CheckCourt shows in the booking dialog, by app name. */
188
+ export declare const MAX_BOOKING_HINTS = 2;
189
+ /** One badge in a court header of the booking plan. */
190
+ export interface CourtAnnotation {
191
+ court_id: number;
192
+ /** 1 to 24 characters. */
193
+ label: string;
194
+ variant?: BadgeVariant;
195
+ }
196
+ /** The answer to `court.annotation`: at most one annotation per court. */
197
+ export interface AnnotationsDocument {
198
+ ui: typeof UI_VERSION;
199
+ annotations: CourtAnnotation[];
200
+ cache?: UiCache;
201
+ }
202
+ /** One cell of an app column on the member list; a badge when `variant` is set, plain text otherwise. */
203
+ export interface ColumnValue {
204
+ member_id: string;
205
+ /** 1 to 24 characters. */
206
+ text: string;
207
+ variant?: BadgeVariant;
208
+ }
209
+ /** The answer to `member.list.column`: at most one value per member. */
210
+ export interface ColumnDocument {
211
+ ui: typeof UI_VERSION;
212
+ column: {
213
+ title: string;
214
+ values: ColumnValue[];
215
+ };
216
+ cache?: UiCache;
217
+ }
218
+ export type UiHintBlock = UiTextBlock | UiBadgeBlock | UiKeyValueBlock | UiLinkBlock;
219
+ /** The answer to `booking.hint`: display-only blocks; toasts are ignored. */
220
+ export interface UiHintDocument extends UiDocument {
221
+ blocks: UiHintBlock[];
222
+ toast?: never;
223
+ }
224
+ /** Options of `ui.annotations`, `ui.column` and `ui.hint`. */
225
+ export interface UiSurfaceOptions {
226
+ /** Seconds CheckCourt may reuse this render, sent as `cache.max_age`; `0` disables caching. Capped at 300. */
227
+ maxAge?: number;
228
+ }
140
229
  /** Values a submitted form sends back, keyed by field name. Empty optional fields are omitted. */
141
230
  export type UiFormValues = Record<string, string | number | boolean>;
142
231
  type Opt<T> = {
@@ -154,6 +243,24 @@ export interface UiDocumentOptions {
154
243
  */
155
244
  export declare const ui: {
156
245
  readonly doc: (blocks: UiBlock[], options?: UiDocumentOptions) => UiDocument;
246
+ /**
247
+ * The answer to `court.annotation`: badges for some of the requested courts, at most one per
248
+ * court. Throws on a label outside 1 to 24 characters or a court listed twice.
249
+ */
250
+ readonly annotations: (annotations: CourtAnnotation[], options?: UiSurfaceOptions) => AnnotationsDocument;
251
+ /**
252
+ * The answer to `member.list.column`: a title and one value per member you have something for.
253
+ * Throws on a title over 20 or a text over 24 characters, or a member listed twice.
254
+ */
255
+ readonly column: (column: {
256
+ title: string;
257
+ values: ColumnValue[];
258
+ }, options?: UiSurfaceOptions) => ColumnDocument;
259
+ /**
260
+ * The answer to `booking.hint`: at most 6 text, badge, key_value or link blocks. Throws on any
261
+ * other block, which CheckCourt would reject together with the whole hint.
262
+ */
263
+ readonly hint: (blocks: UiHintBlock[], options?: UiSurfaceOptions) => UiHintDocument;
157
264
  /** Nothing relevant here: no card at all. Answering `204 No Content` does the same. */
158
265
  readonly hidden: (options?: UiDocumentOptions) => UiHiddenDocument;
159
266
  readonly text: (text: string, options?: {
@@ -182,6 +289,8 @@ export declare const ui: {
182
289
  number(name: string, label: string, options?: Opt<Omit<UiNumberField, "type" | "name" | "label">>): UiNumberField;
183
290
  select(name: string, label: string, options: UiSelectField["options"], extra?: Opt<Pick<UiSelectField, "default" | "required">>): UiSelectField;
184
291
  switch(name: string, label: string, options?: Opt<Pick<UiSwitchField, "default">>): UiSwitchField;
292
+ date(name: string, label: string, options?: Opt<Omit<UiDateField, "type" | "name" | "label">>): UiDateField;
293
+ time(name: string, label: string, options?: Opt<Omit<UiTimeField, "type" | "name" | "label">>): UiTimeField;
185
294
  };
186
295
  readonly divider: () => UiDividerBlock;
187
296
  readonly stack: (children: UiBlock[]) => UiStackBlock;
package/dist/ui.js CHANGED
@@ -6,6 +6,27 @@ export const BADGE_VARIANTS = ["default", "secondary", "outline", "destructive"]
6
6
  export const BUTTON_VARIANTS = ["default", "secondary", "outline", "destructive"];
7
7
  /** CheckCourt caps every render lifetime at this many seconds. */
8
8
  export const UI_CACHE_MAX_AGE_LIMIT = 300;
9
+ /** Longest badge label of a `court.annotation`. */
10
+ export const ANNOTATION_LABEL_MAX = 24;
11
+ /** Badges CheckCourt shows per court across all apps, by app name; the rest is not shown. */
12
+ export const MAX_ANNOTATIONS_PER_COURT = 2;
13
+ /** Longest `column.title` of a `member.list.column`. */
14
+ export const COLUMN_TITLE_MAX = 20;
15
+ /** Longest cell text of a `member.list.column`. */
16
+ export const COLUMN_TEXT_MAX = 24;
17
+ /** App columns CheckCourt shows on the member list, by app name. */
18
+ export const MAX_APP_COLUMNS = 2;
19
+ /** App entries CheckCourt shows in the sidebar, by app name. */
20
+ export const MAX_SIDEBAR_ACTIONS = 2;
21
+ /** CheckCourt waits this long for a `booking.hint` answer, then shows nothing. */
22
+ export const BOOKING_HINT_TIMEOUT_MS = 1000;
23
+ /** Block types a `booking.hint` document may contain, top level only. */
24
+ export const BOOKING_HINT_BLOCKS = ["text", "badge", "key_value", "link"];
25
+ export const MAX_HINT_BLOCKS = 6;
26
+ /** Apps whose hints CheckCourt shows in the booking dialog, by app name. */
27
+ export const MAX_BOOKING_HINTS = 2;
28
+ /** Annotations or column values one document may carry. */
29
+ const MAX_SURFACE_ENTRIES = 200;
9
30
  const field = {
10
31
  text(name, label, options = {}) {
11
32
  return { type: "text", name, label, ...options };
@@ -19,6 +40,12 @@ const field = {
19
40
  switch(name, label, options = {}) {
20
41
  return { type: "switch", name, label, ...options };
21
42
  },
43
+ date(name, label, options = {}) {
44
+ return compact({ type: "date", name, label, ...options });
45
+ },
46
+ time(name, label, options = {}) {
47
+ return compact({ type: "time", name, label, ...options });
48
+ },
22
49
  };
23
50
  function compact(value) {
24
51
  return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
@@ -31,6 +58,30 @@ function cacheOf(maxAge) {
31
58
  }
32
59
  return { max_age: maxAge };
33
60
  }
61
+ function shortText(value, max, what) {
62
+ if (typeof value !== "string")
63
+ throw new TypeError(`${what} must be a string`);
64
+ const text = value.trim();
65
+ if (text.length < 1 || text.length > max) {
66
+ throw new RangeError(`${what} must be 1 to ${max} characters, got ${text.length}`);
67
+ }
68
+ return text;
69
+ }
70
+ function variantOf(variant, what) {
71
+ if (variant === undefined)
72
+ return undefined;
73
+ if (!BADGE_VARIANTS.includes(variant)) {
74
+ throw new TypeError(`${what} must be one of ${BADGE_VARIANTS.join(", ")}`);
75
+ }
76
+ return variant;
77
+ }
78
+ function entriesOf(list, what) {
79
+ if (!Array.isArray(list))
80
+ throw new TypeError(`${what} must be an array`);
81
+ if (list.length > MAX_SURFACE_ENTRIES)
82
+ throw new RangeError(`${what}: at most ${MAX_SURFACE_ENTRIES} entries`);
83
+ return list;
84
+ }
34
85
  /**
35
86
  * Builds `ui: "v1"` documents. Strings are plain text (no HTML, no Markdown); CheckCourt
36
87
  * renders them with its own design system.
@@ -39,6 +90,68 @@ export const ui = {
39
90
  doc(blocks, options = {}) {
40
91
  return compact({ ui: UI_VERSION, blocks, toast: options.toast, cache: cacheOf(options.maxAge) });
41
92
  },
93
+ /**
94
+ * The answer to `court.annotation`: badges for some of the requested courts, at most one per
95
+ * court. Throws on a label outside 1 to 24 characters or a court listed twice.
96
+ */
97
+ annotations(annotations, options = {}) {
98
+ const seen = new Set();
99
+ const list = entriesOf(annotations, "annotations").map((a, i) => {
100
+ if (!Number.isInteger(a.court_id) || a.court_id <= 0) {
101
+ throw new TypeError(`annotations[${i}].court_id must be a positive integer`);
102
+ }
103
+ if (seen.has(a.court_id))
104
+ throw new RangeError(`annotations: court ${a.court_id} appears twice`);
105
+ seen.add(a.court_id);
106
+ return compact({
107
+ court_id: a.court_id,
108
+ label: shortText(a.label, ANNOTATION_LABEL_MAX, `annotations[${i}].label`),
109
+ variant: variantOf(a.variant, `annotations[${i}].variant`),
110
+ });
111
+ });
112
+ return compact({ ui: UI_VERSION, annotations: list, cache: cacheOf(options.maxAge) });
113
+ },
114
+ /**
115
+ * The answer to `member.list.column`: a title and one value per member you have something for.
116
+ * Throws on a title over 20 or a text over 24 characters, or a member listed twice.
117
+ */
118
+ column(column, options = {}) {
119
+ const seen = new Set();
120
+ const values = entriesOf(column.values, "column.values").map((v, i) => {
121
+ if (typeof v.member_id !== "string" || v.member_id.length < 1 || v.member_id.length > 100) {
122
+ throw new TypeError(`column.values[${i}].member_id must be a member id`);
123
+ }
124
+ if (seen.has(v.member_id))
125
+ throw new RangeError(`column.values: member ${v.member_id} appears twice`);
126
+ seen.add(v.member_id);
127
+ return compact({
128
+ member_id: v.member_id,
129
+ text: shortText(v.text, COLUMN_TEXT_MAX, `column.values[${i}].text`),
130
+ variant: variantOf(v.variant, `column.values[${i}].variant`),
131
+ });
132
+ });
133
+ return compact({
134
+ ui: UI_VERSION,
135
+ column: { title: shortText(column.title, COLUMN_TITLE_MAX, "column.title"), values },
136
+ cache: cacheOf(options.maxAge),
137
+ });
138
+ },
139
+ /**
140
+ * The answer to `booking.hint`: at most 6 text, badge, key_value or link blocks. Throws on any
141
+ * other block, which CheckCourt would reject together with the whole hint.
142
+ */
143
+ hint(blocks, options = {}) {
144
+ if (!Array.isArray(blocks))
145
+ throw new TypeError("blocks must be an array");
146
+ if (blocks.length > MAX_HINT_BLOCKS)
147
+ throw new RangeError(`booking.hint: at most ${MAX_HINT_BLOCKS} blocks`);
148
+ for (const [i, block] of blocks.entries()) {
149
+ if (!BOOKING_HINT_BLOCKS.includes(block.type)) {
150
+ throw new TypeError(`blocks[${i}]: ${block.type} is not allowed in booking.hint`);
151
+ }
152
+ }
153
+ return compact({ ui: UI_VERSION, blocks, cache: cacheOf(options.maxAge) });
154
+ },
42
155
  /** Nothing relevant here: no card at all. Answering `204 No Content` does the same. */
43
156
  hidden(options = {}) {
44
157
  return compact({ ui: UI_VERSION, hidden: true, toast: options.toast, cache: cacheOf(options.maxAge) });
@@ -1,6 +1,6 @@
1
1
  import type { WebhookEvent } from "./events.js";
2
- export type { AppLifecycleEventData, BookingEventData, CourtLockEventData, EventDataMap, EventObjectType, EventType, MemberEventData, SubscribableEventType, AppLifecycleEventType, WebhookEvent, WebhookEventOf, } from "./events.js";
3
- export { APP_LIFECYCLE_EVENT_TYPES, EVENT_TYPES, SUBSCRIBABLE_EVENT_TYPES } from "./events.js";
2
+ export type { AppEvent, AppEventType, AppMetadataChangedEvent, AppMetadataChangedEventData, ConnectionEvent, AppLifecycleEventData, BookingEventData, CourtLockEventData, EventDataMap, EventObjectType, EventType, MemberEventData, SubscribableEventType, AppLifecycleEventType, WebhookEvent, WebhookEventOf, } from "./events.js";
3
+ export { APP_LIFECYCLE_EVENT_TYPES, EVENT_TYPES, METADATA_CHANGED_EVENT_TYPE, SUBSCRIBABLE_EVENT_TYPES, appEventType, isAppEvent, isMetadataChangedEvent, } from "./events.js";
4
4
  export { WebhookSignatureError, type WebhookSignatureFailure } from "./errors.js";
5
5
  export declare const SIGNATURE_HEADER = "CheckCourt-Signature";
6
6
  export declare const EVENT_ID_HEADER = "CheckCourt-Event-Id";
package/dist/webhooks.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { WebhookSignatureError } from "./errors.js";
2
2
  import { decodeUtf8, hexDecode, hexEncode, toBytes, utf8 } from "./internal/encoding.js";
3
3
  import { hmacSha256, hmacSha256Verify } from "./internal/hmac.js";
4
- export { APP_LIFECYCLE_EVENT_TYPES, EVENT_TYPES, SUBSCRIBABLE_EVENT_TYPES } from "./events.js";
4
+ export { APP_LIFECYCLE_EVENT_TYPES, EVENT_TYPES, METADATA_CHANGED_EVENT_TYPE, SUBSCRIBABLE_EVENT_TYPES, appEventType, isAppEvent, isMetadataChangedEvent, } from "./events.js";
5
5
  export { WebhookSignatureError } from "./errors.js";
6
6
  export const SIGNATURE_HEADER = "CheckCourt-Signature";
7
7
  export const EVENT_ID_HEADER = "CheckCourt-Event-Id";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkcourt/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Official TypeScript SDK for the CheckCourt app platform",
5
5
  "private": false,
6
6
  "license": "MIT",