@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 +17 -1
- package/dist/extensions.d.ts +9 -4
- package/dist/extensions.js +16 -10
- package/dist/generated/schema.d.ts +10 -5
- package/dist/generated/spec-hash.d.ts +1 -1
- package/dist/generated/spec-hash.js +1 -1
- package/dist/ui.d.ts +29 -1
- package/dist/ui.js +6 -0
- package/package.json +1 -1
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`, `
|
|
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
|
package/dist/extensions.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
package/dist/extensions.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
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
|
|
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
|
|
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
|
|
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":
|
|
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 = "
|
|
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 = "
|
|
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
|
|
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));
|