@checkcourt/sdk 0.5.0 → 0.7.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/README.md CHANGED
@@ -178,6 +178,21 @@ ui.form({
178
178
  });
179
179
  ```
180
180
 
181
+ `ui.field.date` and `ui.field.time` render native date and time pickers. `default`, `min` and
182
+ `max` are `YYYY-MM-DD` strings for dates and `HH:MM` (24-hour) strings for times; `time` takes an
183
+ optional `step` in minutes. Both submit their value as a string:
184
+
185
+ ```ts
186
+ ui.form({
187
+ actionId: "block",
188
+ submitLabel: "Sperren",
189
+ fields: [
190
+ ui.field.date("day", "Tag", { min: "2026-01-01", required: true }),
191
+ ui.field.time("from", "Von", { min: "07:00", max: "22:00", step: 30, required: true }),
192
+ ],
193
+ });
194
+ ```
195
+
181
196
  Compare `context.installation_id` and `context.tenant_id` with what you stored from
182
197
  `app.installed` before you act on a request. For the context token alone (for example in
183
198
  the backend of an iframe extension), use `verifyExtensionContext(token, secret)`.
@@ -192,7 +207,8 @@ with the matching builder; each throws when the answer would break CheckCourt's
192
207
  | `court.annotation` | `ext.date`, `ext.courts` | `ui.annotations([{ court_id, label, variant? }])`, label up to 24 characters, one per court |
193
208
  | `member.list.column` | `ext.members` | `ui.column({ title, values: [{ member_id, text, variant? }] })`, title up to 20, text up to 24 characters |
194
209
  | `booking.hint` | `ext.draft` | `ui.hint([...])` with up to 6 text, badge, key_value or link blocks; answer within 1 second |
195
- | `booking_plan.action`, `sidebar.action` | `booking_plan.action`: the day as subject | `ui.doc([...])`, shown in a dialog after a click; `ui.hidden()` closes it |
210
+ | `booking_plan.action` | `ext.date`, `ext.courts` (and the day as subject) | `ui.doc([...])`, shown in a dialog after a click; `ui.hidden()` closes it |
211
+ | `sidebar.action` | nothing extra | `ui.doc([...])`, shown in a dialog after a click; `ui.hidden()` closes it |
196
212
  | `member.settings.section` | nothing extra | `ui.doc([...])`, a card on the member's own settings page |
197
213
 
198
214
  ```ts
@@ -115,7 +115,7 @@ export type ExtensionContextClaims = {
115
115
  export declare function verifyExtensionContext(token: string, secret: string, options?: {
116
116
  now?: Date | number;
117
117
  }): Promise<ExtensionContextClaims>;
118
- /** A court of the plan a `court.annotation` request covers. */
118
+ /** A court of the plan a `court.annotation` or `booking_plan.action` request covers. */
119
119
  export interface AnnotationCourt {
120
120
  id: number;
121
121
  name: string;
@@ -145,9 +145,9 @@ export interface ExtensionRenderRequest {
145
145
  context: ExtensionContextClaims;
146
146
  point: ExtensionPoint;
147
147
  subject: ExtensionSubject | null;
148
- /** `court.annotation`: the plan's day, YYYY-MM-DD. */
148
+ /** `court.annotation` and `booking_plan.action`: the plan's day, YYYY-MM-DD. */
149
149
  date?: string;
150
- /** `court.annotation`: every court of the plan, answered in one document. */
150
+ /** `court.annotation` (one document for all) and `booking_plan.action`: every court of the plan. */
151
151
  courts?: AnnotationCourt[];
152
152
  /** `member.list.column`: the members on the visible page. */
153
153
  members?: ColumnMember[];
@@ -162,6 +162,10 @@ export interface ExtensionActionRequest {
162
162
  subject: ExtensionSubject | null;
163
163
  actionId: string;
164
164
  values: UiFormValues;
165
+ /** `booking_plan.action`: the plan's day, YYYY-MM-DD. */
166
+ date?: string;
167
+ /** `booking_plan.action`: every court of the plan, so a form can offer a court select. */
168
+ courts?: AnnotationCourt[];
165
169
  }
166
170
  export type ExtensionRequest = ExtensionRenderRequest | ExtensionActionRequest;
167
171
  type HeaderSource = Headers | Record<string, string | string[] | undefined>;
@@ -169,7 +173,8 @@ type HeaderSource = Headers | Record<string, string | string[] | undefined>;
169
173
  * Verifies a declarative extension POST: the `CheckCourt-Signature` over the raw body (it binds
170
174
  * `action_id` and `values` to the token), the context token, and that body, header token and
171
175
  * claims agree. Renders at `court.annotation`, `member.list.column` and `booking.hint` also
172
- * carry `date` and `courts`, `members` or `draft`. Throws `ExtensionVerificationError`.
176
+ * carry `date` and `courts`, `members` or `draft`; `booking_plan.action` renders and actions
177
+ * carry the plan's `date` and `courts`. Throws `ExtensionVerificationError`.
173
178
  */
174
179
  export declare function verifyExtensionRequest(options: {
175
180
  secret: string;
@@ -84,17 +84,21 @@ function sameSubject(a, b) {
84
84
  const DATE = /^\d{4}-\d{2}-\d{2}$/;
85
85
  const TIME = /^\d{2}:\d{2}$/;
86
86
  const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
87
+ /** Parses the `date` and `courts` a court.annotation or booking_plan.action request carries. */
88
+ function planCourts(body) {
89
+ const { date, courts } = body;
90
+ if (typeof date !== "string" || !DATE.test(date))
91
+ throw fail("invalid_body", "date is not YYYY-MM-DD");
92
+ if (!Array.isArray(courts) || !courts.every((c) => isObject(c) && typeof c.id === "number" && typeof c.name === "string")) {
93
+ throw fail("invalid_body", "courts is not a list of { id, name }");
94
+ }
95
+ return { date, courts: courts.map((c) => ({ id: c.id, name: c.name })) };
96
+ }
87
97
  function surfaceFields(point, body) {
88
98
  switch (point) {
89
- case "court.annotation": {
90
- const { date, courts } = body;
91
- if (typeof date !== "string" || !DATE.test(date))
92
- throw fail("invalid_body", "date is not YYYY-MM-DD");
93
- if (!Array.isArray(courts) || !courts.every((c) => isObject(c) && typeof c.id === "number" && typeof c.name === "string")) {
94
- throw fail("invalid_body", "courts is not a list of { id, name }");
95
- }
96
- return { date, courts: courts.map((c) => ({ id: c.id, name: c.name })) };
97
- }
99
+ case "court.annotation":
100
+ case "booking_plan.action":
101
+ return planCourts(body);
98
102
  case "member.list.column": {
99
103
  const { members } = body;
100
104
  if (!Array.isArray(members) ||
@@ -134,7 +138,8 @@ function surfaceFields(point, body) {
134
138
  * Verifies a declarative extension POST: the `CheckCourt-Signature` over the raw body (it binds
135
139
  * `action_id` and `values` to the token), the context token, and that body, header token and
136
140
  * claims agree. Renders at `court.annotation`, `member.list.column` and `booking.hint` also
137
- * carry `date` and `courts`, `members` or `draft`. Throws `ExtensionVerificationError`.
141
+ * carry `date` and `courts`, `members` or `draft`; `booking_plan.action` renders and actions
142
+ * carry the plan's `date` and `courts`. Throws `ExtensionVerificationError`.
138
143
  */
139
144
  export async function verifyExtensionRequest(options) {
140
145
  try {
@@ -185,5 +190,6 @@ export async function verifyExtensionRequest(options) {
185
190
  subject,
186
191
  actionId: body.action_id,
187
192
  values: values,
193
+ ...(context.point === "booking_plan.action" ? planCourts(body) : {}),
188
194
  };
189
195
  }
@@ -242,11 +242,11 @@ export interface paths {
242
242
  * @description **Required scope:** `members:invite`
243
243
  *
244
244
  * Adds a member to the club. Three flows, decided by the input:
245
- * 1. **Email already has an account** (in no other club): the existing user is added to this club.
245
+ * 1. **Email already has an account** (in no other club): the account owner is invited. The membership stays pending (`awaitingAcceptance: true`) until they accept after their next login; until then the club cannot change the account's name, email, phone or password, and deleting the member only withdraws the invitation. `sendEmail` controls the invitation mail.
246
246
  * 2. **New email**: a new account is created and, unless `sendEmail=false`, a welcome mail with a claim link is sent.
247
247
  * 3. **`noOwnEmail=true`**: for members without their own mailbox. Creates an account with a synthetic address, stores the given email as forwarding address and requires a `password` (min 8 chars) the member uses to log in.
248
248
  *
249
- * `roleIds` (optional) assigns roles besides the base role Mitglied (member) and requires `roles:manage`, which only personal keys can hold: the caller must outrank each role and hold all of its scopes (403). Unknown ids fail with 404 before anything is created.
249
+ * `roleIds` (optional) assigns roles besides the base role Mitglied and requires `roles:manage`, which only personal keys can hold: the caller must outrank each role and hold all of its scopes (403). Unknown ids fail with 404 before anything is created.
250
250
  *
251
251
  * Fails with 422 when the club's member limit is reached, the member number is taken, the email already belongs to this club, or the data processing agreement (AVV) has not been signed yet.
252
252
  */
@@ -302,7 +302,7 @@ export interface paths {
302
302
  * End a membership
303
303
  * @description **Required scope:** `members:delete`
304
304
  *
305
- * Removes the member from the club with a full cascade: future bookings are cancelled (with email notification), open guest fees are resolved according to `orphanedFeeAction`, and if this was the user's only club the account is anonymized. Members who lead a team cannot be deleted until leadership is handed over, and the club's last administrator cannot be deleted (422). The response summarizes what happened.
305
+ * Removes the member from the club with a full cascade: future bookings are cancelled (with email notification), open guest fees are resolved according to `orphanedFeeAction`, and if this was the user's only club the account is anonymized (not for a pending invitation, which only withdraws the invitation). The caller must outrank the member (403): only administrators may delete members holding an equal or higher role; management keys act with their creator's rank but never on administrators. Members who lead a team cannot be deleted until leadership is handed over, and the club's last administrator cannot be deleted (422). The response summarizes what happened.
306
306
  */
307
307
  delete: operations["deleteMember"];
308
308
  options?: never;
@@ -311,7 +311,7 @@ export interface paths {
311
311
  * Update a member
312
312
  * @description **Required scope:** `members:write`
313
313
  *
314
- * `name` and `email` are required (send the current values to keep them); `phone` and `memberNumber` are optional. For forwarding-only accounts the email update changes the forwarding address, not the login address. If the user is also a member of another club, name/email/phone are left untouched (they are account-level) and only the member number changes. Sessions cannot edit their own account; management keys are exempt from that rule. Member numbers are unique per club (422 on collision). Roles are not part of this endpoint: use `PUT /members/{id}/roles`. Sending `role` fails with 400.
314
+ * `name` and `email` are required (send the current values to keep them); `phone` and `memberNumber` are optional. For forwarding-only accounts the email update changes the forwarding address, not the login address. If the user is also a member of another club, name/email/phone are left untouched (they are account-level) and only the member number changes. Sessions cannot edit their own account; management keys are exempt from that rule. Changing the email requires outranking the member (403): only administrators may change the email of members holding an equal or higher role; management keys act with their creator's rank but never on administrators. While the member's invitation is pending, changing name, email or phone fails with 422. Member numbers are unique per club (422 on collision). Roles are not part of this endpoint: use `PUT /members/{id}/roles`. Sending `role` fails with 400.
315
315
  */
316
316
  patch: operations["updateMember"];
317
317
  trace?: never;
@@ -3586,7 +3586,12 @@ export interface operations {
3586
3586
  [name: string]: unknown;
3587
3587
  };
3588
3588
  content: {
3589
- "application/json": components["schemas"]["Success"];
3589
+ "application/json": {
3590
+ /** @constant */
3591
+ success: true;
3592
+ /** @description True when an existing account was invited and its owner still has to accept */
3593
+ awaitingAcceptance: boolean;
3594
+ };
3590
3595
  };
3591
3596
  };
3592
3597
  400: components["responses"]["ValidationError"];
@@ -1 +1 @@
1
- export declare const OPENAPI_SPEC_SHA256 = "b01c1f9ed8ee89d62dc6daaf41f3f27f55fe06a524660f9790c4981785eed8da";
1
+ export declare const OPENAPI_SPEC_SHA256 = "838f8beb1f933593ea8eea8ea14eb78b12e0e8d61109254f5e72bec10b57e58a";
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/generate.mjs from the CheckCourt OpenAPI spec. Do not edit.
2
- export const OPENAPI_SPEC_SHA256 = "b01c1f9ed8ee89d62dc6daaf41f3f27f55fe06a524660f9790c4981785eed8da";
2
+ export const OPENAPI_SPEC_SHA256 = "838f8beb1f933593ea8eea8ea14eb78b12e0e8d61109254f5e72bec10b57e58a";
package/dist/ui.d.ts CHANGED
@@ -93,7 +93,33 @@ export type UiSwitchField = {
93
93
  label: string;
94
94
  default?: boolean;
95
95
  };
96
- 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;
97
123
  export type UiFormBlock = {
98
124
  type: "form";
99
125
  fields: UiFormField[];
@@ -263,6 +289,8 @@ export declare const ui: {
263
289
  number(name: string, label: string, options?: Opt<Omit<UiNumberField, "type" | "name" | "label">>): UiNumberField;
264
290
  select(name: string, label: string, options: UiSelectField["options"], extra?: Opt<Pick<UiSelectField, "default" | "required">>): UiSelectField;
265
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;
266
294
  };
267
295
  readonly divider: () => UiDividerBlock;
268
296
  readonly stack: (children: UiBlock[]) => UiStackBlock;
package/dist/ui.js CHANGED
@@ -40,6 +40,12 @@ const field = {
40
40
  switch(name, label, options = {}) {
41
41
  return { type: "switch", name, label, ...options };
42
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
+ },
43
49
  };
44
50
  function compact(value) {
45
51
  return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkcourt/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Official TypeScript SDK for the CheckCourt app platform",
5
5
  "private": false,
6
6
  "license": "MIT",