@checkcourt/sdk 0.0.0-stage → 0.3.1

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.
@@ -0,0 +1 @@
1
+ export declare const OPENAPI_SPEC_SHA256 = "19b06dbaaf58a088e75aa53fb54d4b9ab3b3f847371cb4a3dcd90125a34fe262";
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/generate.mjs from the CheckCourt OpenAPI spec. Do not edit.
2
+ export const OPENAPI_SPEC_SHA256 = "19b06dbaaf58a088e75aa53fb54d4b9ab3b3f847371cb4a3dcd90125a34fe262";
@@ -0,0 +1,68 @@
1
+ export declare const CONTEXT_PARAM = "context";
2
+ export declare const CHANNEL_PARAM = "channel";
3
+ export declare const IFRAME_MIN_HEIGHT = 80;
4
+ export declare const IFRAME_MAX_HEIGHT = 2000;
5
+ export declare const MAX_TOAST_LENGTH = 200;
6
+ /** Top-level areas `navigate()` may target; anything else is dropped by CheckCourt. */
7
+ export declare const NAVIGABLE_SECTIONS: readonly ["dashboard", "booking", "my-bookings", "club", "team", "trainer", "fees", "arbeitsstunden", "settings", "admin"];
8
+ /** CSS variables CheckCourt sends with every theme message. */
9
+ export declare const THEME_TOKEN_NAMES: readonly ["--background", "--foreground", "--card", "--card-foreground", "--muted", "--muted-foreground", "--primary", "--primary-foreground", "--secondary", "--secondary-foreground", "--accent", "--accent-foreground", "--destructive", "--border", "--input", "--ring", "--radius"];
10
+ export type ThemeTokenName = (typeof THEME_TOKEN_NAMES)[number];
11
+ export type FrameMessage = {
12
+ type: "checkcourt:resize";
13
+ channel: string;
14
+ height: number;
15
+ } | {
16
+ type: "checkcourt:toast";
17
+ channel: string;
18
+ kind: "success" | "error";
19
+ message: string;
20
+ } | {
21
+ type: "checkcourt:navigate";
22
+ channel: string;
23
+ path: string;
24
+ };
25
+ export interface ThemeMessage {
26
+ type: "checkcourt:theme";
27
+ channel: string;
28
+ mode: "light" | "dark";
29
+ tokens: Partial<Record<ThemeTokenName, string>>;
30
+ }
31
+ export interface Theme {
32
+ mode: "light" | "dark";
33
+ tokens: Partial<Record<ThemeTokenName, string>>;
34
+ }
35
+ export declare function resizeMessage(channel: string, height: number): FrameMessage;
36
+ export declare function toastMessage(channel: string, kind: "success" | "error", message: string): FrameMessage;
37
+ export declare function navigateMessage(channel: string, path: string): FrameMessage;
38
+ /** Mirrors CheckCourt's check: same-site path into one of the club areas, query and hash allowed. */
39
+ export declare function isNavigablePath(path: string): boolean;
40
+ export declare function isThemeMessage(data: unknown, channel: string): data is ThemeMessage;
41
+ /** Sets the tokens as CSS variables on `<html>`, toggles the `dark` class and `color-scheme`. */
42
+ export declare function applyTheme(theme: Theme, root?: HTMLElement): void;
43
+ export interface ConnectOptions {
44
+ /** Keep the frame as tall as the document via ResizeObserver. Default true. */
45
+ autoResize?: boolean;
46
+ /** Apply every theme CheckCourt sends with `applyTheme`. Default true. */
47
+ applyTheme?: boolean;
48
+ /** For tests; defaults to `window`. */
49
+ window?: Window;
50
+ }
51
+ export interface ExtensionFrame {
52
+ /** The context token from the URL. Send it to your backend and verify it there, never trust it in the browser. */
53
+ readonly context: string | null;
54
+ readonly channel: string;
55
+ /** The latest theme, once CheckCourt has sent one. */
56
+ readonly theme: Theme | null;
57
+ resize(height?: number): void;
58
+ toast(kind: "success" | "error", message: string): void;
59
+ navigate(path: string): void;
60
+ /** Called for the current theme (if known) and every change. Returns an unsubscribe function. */
61
+ onTheme(callback: (theme: Theme) => void): () => void;
62
+ disconnect(): void;
63
+ }
64
+ /**
65
+ * Connects an iframe extension to CheckCourt: reads `context` and `channel` from the URL and
66
+ * sends messages with the channel nonce. Throws when the page was not opened by CheckCourt.
67
+ */
68
+ export declare function connectExtensionFrame(options?: ConnectOptions): ExtensionFrame;
package/dist/iframe.js ADDED
@@ -0,0 +1,171 @@
1
+ export const CONTEXT_PARAM = "context";
2
+ export const CHANNEL_PARAM = "channel";
3
+ export const IFRAME_MIN_HEIGHT = 80;
4
+ export const IFRAME_MAX_HEIGHT = 2000;
5
+ export const MAX_TOAST_LENGTH = 200;
6
+ /** Top-level areas `navigate()` may target; anything else is dropped by CheckCourt. */
7
+ export const NAVIGABLE_SECTIONS = [
8
+ "dashboard",
9
+ "booking",
10
+ "my-bookings",
11
+ "club",
12
+ "team",
13
+ "trainer",
14
+ "fees",
15
+ "arbeitsstunden",
16
+ "settings",
17
+ "admin",
18
+ ];
19
+ /** CSS variables CheckCourt sends with every theme message. */
20
+ export const THEME_TOKEN_NAMES = [
21
+ "--background",
22
+ "--foreground",
23
+ "--card",
24
+ "--card-foreground",
25
+ "--muted",
26
+ "--muted-foreground",
27
+ "--primary",
28
+ "--primary-foreground",
29
+ "--secondary",
30
+ "--secondary-foreground",
31
+ "--accent",
32
+ "--accent-foreground",
33
+ "--destructive",
34
+ "--border",
35
+ "--input",
36
+ "--ring",
37
+ "--radius",
38
+ ];
39
+ export function resizeMessage(channel, height) {
40
+ return { type: "checkcourt:resize", channel, height: Math.max(0, Math.ceil(height)) };
41
+ }
42
+ export function toastMessage(channel, kind, message) {
43
+ const trimmed = message.trim();
44
+ if (trimmed.length === 0 || trimmed.length > MAX_TOAST_LENGTH) {
45
+ throw new RangeError(`Toast message must be 1 to ${MAX_TOAST_LENGTH} characters`);
46
+ }
47
+ return { type: "checkcourt:toast", channel, kind, message: trimmed };
48
+ }
49
+ export function navigateMessage(channel, path) {
50
+ if (!isNavigablePath(path)) {
51
+ throw new RangeError(`Path must start with /${NAVIGABLE_SECTIONS.join(", /")}: ${path}`);
52
+ }
53
+ return { type: "checkcourt:navigate", channel, path };
54
+ }
55
+ /** Mirrors CheckCourt's check: same-site path into one of the club areas, query and hash allowed. */
56
+ export function isNavigablePath(path) {
57
+ if (typeof path !== "string" || path.length > 500 || !path.startsWith("/") || path.startsWith("//"))
58
+ return false;
59
+ if (/[\\\u0000-\u001f\u007f]/.test(path))
60
+ return false;
61
+ let url;
62
+ try {
63
+ url = new URL(path, "https://checkcourt.invalid");
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ if (url.origin !== "https://checkcourt.invalid")
69
+ return false;
70
+ const section = url.pathname.split("/")[1] ?? "";
71
+ return NAVIGABLE_SECTIONS.includes(section);
72
+ }
73
+ export function isThemeMessage(data, channel) {
74
+ const msg = data;
75
+ return (!!msg &&
76
+ typeof msg === "object" &&
77
+ msg.type === "checkcourt:theme" &&
78
+ msg.channel === channel &&
79
+ (msg.mode === "light" || msg.mode === "dark") &&
80
+ !!msg.tokens &&
81
+ typeof msg.tokens === "object");
82
+ }
83
+ /** Sets the tokens as CSS variables on `<html>`, toggles the `dark` class and `color-scheme`. */
84
+ export function applyTheme(theme, root = document.documentElement) {
85
+ for (const name of THEME_TOKEN_NAMES) {
86
+ const value = theme.tokens[name];
87
+ if (typeof value === "string" && value)
88
+ root.style.setProperty(name, value);
89
+ }
90
+ root.classList.toggle("dark", theme.mode === "dark");
91
+ root.style.colorScheme = theme.mode;
92
+ }
93
+ /**
94
+ * Connects an iframe extension to CheckCourt: reads `context` and `channel` from the URL and
95
+ * sends messages with the channel nonce. Throws when the page was not opened by CheckCourt.
96
+ */
97
+ export function connectExtensionFrame(options = {}) {
98
+ const win = options.window ?? window;
99
+ const params = new URLSearchParams(win.location.search);
100
+ const channel = params.get(CHANNEL_PARAM);
101
+ if (!channel)
102
+ throw new Error(`Missing ?${CHANNEL_PARAM}= parameter: this page must be opened by CheckCourt`);
103
+ const context = params.get(CONTEXT_PARAM);
104
+ const doc = win.document;
105
+ // The sandbox gives the frame an opaque origin, so no narrower target origin can match.
106
+ const post = (message) => win.parent.postMessage(message, "*");
107
+ let theme = null;
108
+ const listeners = new Set();
109
+ const onMessage = (event) => {
110
+ if (event.source !== win.parent || !isThemeMessage(event.data, channel))
111
+ return;
112
+ theme = { mode: event.data.mode, tokens: event.data.tokens };
113
+ if (options.applyTheme !== false)
114
+ applyTheme(theme, doc.documentElement);
115
+ for (const listener of listeners)
116
+ listener(theme);
117
+ };
118
+ win.addEventListener("message", onMessage);
119
+ let lastHeight = -1;
120
+ const measure = () => Math.ceil(doc.documentElement.scrollHeight);
121
+ const resize = (height = measure()) => {
122
+ const rounded = Math.ceil(height);
123
+ if (rounded === lastHeight)
124
+ return;
125
+ lastHeight = rounded;
126
+ post(resizeMessage(channel, rounded));
127
+ };
128
+ let observer = null;
129
+ let timer;
130
+ let connected = true;
131
+ const autoResize = () => {
132
+ if (connected)
133
+ resize();
134
+ };
135
+ if (options.autoResize !== false) {
136
+ // No requestAnimationFrame: browsers skip it in off-screen cross-origin frames, so the height would never arrive.
137
+ if (typeof ResizeObserver !== "undefined") {
138
+ observer = new ResizeObserver(autoResize);
139
+ observer.observe(doc.documentElement);
140
+ }
141
+ resize();
142
+ queueMicrotask(autoResize);
143
+ timer = win.setTimeout(autoResize, 0);
144
+ win.addEventListener("load", autoResize);
145
+ }
146
+ return {
147
+ context,
148
+ channel,
149
+ get theme() {
150
+ return theme;
151
+ },
152
+ resize,
153
+ toast: (kind, message) => post(toastMessage(channel, kind, message)),
154
+ navigate: (path) => post(navigateMessage(channel, path)),
155
+ onTheme(callback) {
156
+ listeners.add(callback);
157
+ if (theme)
158
+ callback(theme);
159
+ return () => listeners.delete(callback);
160
+ },
161
+ disconnect() {
162
+ connected = false;
163
+ win.removeEventListener("message", onMessage);
164
+ win.removeEventListener("load", autoResize);
165
+ observer?.disconnect();
166
+ if (timer !== undefined)
167
+ win.clearTimeout(timer);
168
+ listeners.clear();
169
+ },
170
+ };
171
+ }
@@ -0,0 +1,12 @@
1
+ export { createCheckCourtClient, unwrap, retryAfterMs, TENANT_HEADER } from "./client.js";
2
+ export type { CheckCourtClient, CheckCourtClientOptions, RetryOptions } from "./client.js";
3
+ export { apiKeyAuth, installationAuth, userAuth } from "./auth.js";
4
+ export { getInstallation, getMe, type AppInstallation, type Me } from "./installation.js";
5
+ export type { AuthContext, AuthStrategy, InstallationAuth, StoredUserTokens, UserAuth } from "./auth.js";
6
+ export * from "./errors.js";
7
+ export * from "./oauth.js";
8
+ export * from "./webhooks.js";
9
+ export * from "./extensions.js";
10
+ export * from "./manifest.js";
11
+ export type { paths, components, operations } from "./generated/schema.js";
12
+ export { OPENAPI_SPEC_SHA256 } from "./generated/spec-hash.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { createCheckCourtClient, unwrap, retryAfterMs, TENANT_HEADER } from "./client.js";
2
+ export { apiKeyAuth, installationAuth, userAuth } from "./auth.js";
3
+ export { getInstallation, getMe } from "./installation.js";
4
+ export * from "./errors.js";
5
+ export * from "./oauth.js";
6
+ export * from "./webhooks.js";
7
+ export * from "./extensions.js";
8
+ export * from "./manifest.js";
9
+ export { OPENAPI_SPEC_SHA256 } from "./generated/spec-hash.js";
@@ -0,0 +1,15 @@
1
+ import { type CheckCourtClient } from "./client.js";
2
+ import type { components } from "./generated/schema.js";
3
+ export type AppInstallation = components["schemas"]["AppInstallation"];
4
+ /**
5
+ * `GET /api/v1/app/installation`: the calling club app's own installation with the club's
6
+ * settings. Only with an installation token (`cca_`); read it after `app.installed`,
7
+ * `app.upgraded` and `app.settings_updated`, whose payloads carry ids only.
8
+ */
9
+ export declare function getInstallation(client: CheckCourtClient): Promise<AppInstallation>;
10
+ export type Me = components["schemas"]["Me"];
11
+ /**
12
+ * `GET /api/v1/me`: who connected the member app. Only with a member token (`ccu_`). `user_id` is a
13
+ * pseudonym stable for your app and different for every other app; it carries no name or email.
14
+ */
15
+ export declare function getMe(client: CheckCourtClient): Promise<Me>;
@@ -0,0 +1,16 @@
1
+ import { unwrap } from "./client.js";
2
+ /**
3
+ * `GET /api/v1/app/installation`: the calling club app's own installation with the club's
4
+ * settings. Only with an installation token (`cca_`); read it after `app.installed`,
5
+ * `app.upgraded` and `app.settings_updated`, whose payloads carry ids only.
6
+ */
7
+ export async function getInstallation(client) {
8
+ return unwrap(client.GET("/app/installation"));
9
+ }
10
+ /**
11
+ * `GET /api/v1/me`: who connected the member app. Only with a member token (`ccu_`). `user_id` is a
12
+ * pseudonym stable for your app and different for every other app; it carries no name or email.
13
+ */
14
+ export async function getMe(client) {
15
+ return unwrap(client.GET("/me"));
16
+ }
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_BASE_URL = "https://app.checkcourt.de";
2
+ /** Accepts the origin with or without a trailing `/api/v1`, so CLI-style URLs work too. */
3
+ export declare function normalizeBaseUrl(baseUrl: string | undefined): string;
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_BASE_URL = "https://app.checkcourt.de";
2
+ /** Accepts the origin with or without a trailing `/api/v1`, so CLI-style URLs work too. */
3
+ export function normalizeBaseUrl(baseUrl) {
4
+ return (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/api\/v1$/, "");
5
+ }
@@ -0,0 +1,8 @@
1
+ export declare function utf8(input: string): Uint8Array<ArrayBuffer>;
2
+ export declare function decodeUtf8(input: Uint8Array): string;
3
+ export declare function toBytes(input: string | Uint8Array | ArrayBuffer): Uint8Array<ArrayBuffer>;
4
+ export declare function base64UrlEncode(bytes: Uint8Array): string;
5
+ export declare function base64UrlDecode(input: string): Uint8Array<ArrayBuffer> | null;
6
+ export declare function hexEncode(bytes: Uint8Array): string;
7
+ export declare function hexDecode(input: string): Uint8Array<ArrayBuffer> | null;
8
+ export declare function randomBytes(length: number): Uint8Array<ArrayBuffer>;
@@ -0,0 +1,55 @@
1
+ const encoder = new TextEncoder();
2
+ const decoder = new TextDecoder("utf-8", { fatal: true });
3
+ export function utf8(input) {
4
+ return encoder.encode(input);
5
+ }
6
+ export function decodeUtf8(input) {
7
+ return decoder.decode(input);
8
+ }
9
+ export function toBytes(input) {
10
+ if (typeof input === "string")
11
+ return utf8(input);
12
+ if (input instanceof ArrayBuffer)
13
+ return new Uint8Array(input);
14
+ return new Uint8Array(input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength));
15
+ }
16
+ export function base64UrlEncode(bytes) {
17
+ let binary = "";
18
+ for (const byte of bytes)
19
+ binary += String.fromCharCode(byte);
20
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
21
+ }
22
+ export function base64UrlDecode(input) {
23
+ if (!/^[A-Za-z0-9_-]*$/.test(input) || input.length % 4 === 1)
24
+ return null;
25
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((input.length + 3) % 4);
26
+ try {
27
+ const binary = atob(padded);
28
+ const bytes = new Uint8Array(binary.length);
29
+ for (let i = 0; i < binary.length; i++)
30
+ bytes[i] = binary.charCodeAt(i);
31
+ return bytes;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ export function hexEncode(bytes) {
38
+ let out = "";
39
+ for (const byte of bytes)
40
+ out += byte.toString(16).padStart(2, "0");
41
+ return out;
42
+ }
43
+ export function hexDecode(input) {
44
+ if (input.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(input))
45
+ return null;
46
+ const bytes = new Uint8Array(input.length / 2);
47
+ for (let i = 0; i < bytes.length; i++)
48
+ bytes[i] = parseInt(input.slice(i * 2, i * 2 + 2), 16);
49
+ return bytes;
50
+ }
51
+ export function randomBytes(length) {
52
+ const bytes = new Uint8Array(length);
53
+ crypto.getRandomValues(bytes);
54
+ return bytes;
55
+ }
@@ -0,0 +1,4 @@
1
+ export declare function hmacSha256(secret: string, data: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>;
2
+ /** WebCrypto's verify compares in constant time. */
3
+ export declare function hmacSha256Verify(secret: string, data: Uint8Array<ArrayBuffer>, signature: Uint8Array<ArrayBuffer>): Promise<boolean>;
4
+ export declare function sha256(data: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>;
@@ -0,0 +1,17 @@
1
+ import { utf8 } from "./encoding.js";
2
+ async function importKey(secret) {
3
+ return crypto.subtle.importKey("raw", utf8(secret), { name: "HMAC", hash: "SHA-256" }, false, [
4
+ "sign",
5
+ "verify",
6
+ ]);
7
+ }
8
+ export async function hmacSha256(secret, data) {
9
+ return new Uint8Array(await crypto.subtle.sign("HMAC", await importKey(secret), data));
10
+ }
11
+ /** WebCrypto's verify compares in constant time. */
12
+ export async function hmacSha256Verify(secret, data, signature) {
13
+ return crypto.subtle.verify("HMAC", await importKey(secret), signature, data);
14
+ }
15
+ export async function sha256(data) {
16
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
17
+ }
@@ -0,0 +1,119 @@
1
+ import type { SubscribableEventType } from "./events.js";
2
+ export declare const MANIFEST_VERSION = 1;
3
+ export declare const INSTALL_TARGETS: readonly ["tenant", "user"];
4
+ export type InstallTarget = (typeof INSTALL_TARGETS)[number];
5
+ export declare const USER_LINKING_MODES: readonly ["none", "optional", "required"];
6
+ export type UserLinking = (typeof USER_LINKING_MODES)[number];
7
+ export declare const EXTENSION_KINDS: readonly ["declarative", "iframe"];
8
+ export type ExtensionKind = (typeof EXTENSION_KINDS)[number];
9
+ /** Where each point is shown and how it may render. */
10
+ export declare const EXTENSION_POINTS: {
11
+ readonly "app.settings": {
12
+ readonly targets: readonly ["tenant"];
13
+ readonly kinds: readonly ["declarative", "iframe"];
14
+ };
15
+ readonly "booking.detail.panel": {
16
+ readonly targets: readonly ["tenant", "user"];
17
+ readonly kinds: readonly ["declarative", "iframe"];
18
+ };
19
+ readonly "member.profile.section": {
20
+ readonly targets: readonly ["tenant", "user"];
21
+ readonly kinds: readonly ["declarative", "iframe"];
22
+ };
23
+ readonly "dashboard.widget": {
24
+ readonly targets: readonly ["tenant", "user"];
25
+ readonly kinds: readonly ["declarative"];
26
+ };
27
+ readonly "booking.action": {
28
+ readonly targets: readonly ["tenant", "user"];
29
+ readonly kinds: readonly ["declarative"];
30
+ };
31
+ readonly "kiosk.tile": {
32
+ readonly targets: readonly ["tenant"];
33
+ readonly kinds: readonly ["declarative"];
34
+ };
35
+ };
36
+ export type ExtensionPoint = keyof typeof EXTENSION_POINTS;
37
+ /** Scope the installation needs (in `tenantScopes` or `userScopes`) before the point hands it a subject id. */
38
+ export declare const EXTENSION_POINT_SCOPE: {
39
+ readonly "app.settings": null;
40
+ readonly "booking.detail.panel": "bookings:read";
41
+ readonly "booking.action": "bookings:read";
42
+ readonly "member.profile.section": "members:read";
43
+ readonly "dashboard.widget": null;
44
+ readonly "kiosk.tile": null;
45
+ };
46
+ /** 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"];
48
+ export type GrantableScope = (typeof GRANTABLE_SCOPES)[number];
49
+ type ExtensionOf<P extends ExtensionPoint> = {
50
+ point: P;
51
+ kind: (typeof EXTENSION_POINTS)[P]["kinds"][number];
52
+ /** https; http://localhost is accepted outside production. */
53
+ url: string;
54
+ } & (P extends "booking.action" ? {
55
+ label: string;
56
+ } : {
57
+ label?: string;
58
+ });
59
+ /** One entry of `extensions`; `kind` and `label` are checked per point at compile time. */
60
+ export type ManifestExtension = {
61
+ [P in ExtensionPoint]: ExtensionOf<P>;
62
+ }[ExtensionPoint];
63
+ interface SettingsPropertyBase {
64
+ /** 1 to 80 characters. */
65
+ title: string;
66
+ /** Up to 300 characters. */
67
+ description?: string;
68
+ }
69
+ export type SettingsProperty = (SettingsPropertyBase & {
70
+ type: "string";
71
+ /** Rendered as a select. Excludes `maxLength`. */
72
+ enum?: readonly string[];
73
+ default?: string;
74
+ maxLength?: number;
75
+ }) | (SettingsPropertyBase & {
76
+ type: "number";
77
+ default?: number;
78
+ minimum?: number;
79
+ maximum?: number;
80
+ }) | (SettingsPropertyBase & {
81
+ type: "boolean";
82
+ default?: boolean;
83
+ });
84
+ /** The JSON-Schema subset CheckCourt renders as the settings form (at most 30 properties). */
85
+ export interface SettingsSchema {
86
+ type: "object";
87
+ /** Keys match `^[a-zA-Z][a-zA-Z0-9_]{0,39}$`. */
88
+ properties: Record<string, SettingsProperty>;
89
+ required?: readonly string[];
90
+ additionalProperties?: false;
91
+ }
92
+ export interface DataProcessing {
93
+ /** At least one, e.g. "Buchungsdaten". */
94
+ categories: readonly string[];
95
+ /** Up to 500 characters. */
96
+ purpose: string;
97
+ storageLocation: "EU" | "non-EU";
98
+ avvRequired: boolean;
99
+ }
100
+ export interface Manifest {
101
+ manifestVersion?: typeof MANIFEST_VERSION;
102
+ installTargets: readonly InstallTarget[];
103
+ /** Lets a club installation link member accounts via OAuth. Only with `tenant`. */
104
+ userLinking?: UserLinking;
105
+ tenantScopes?: readonly GrantableScope[];
106
+ userScopes?: readonly GrantableScope[];
107
+ /** Lifecycle events (`app.*`) arrive without being listed. */
108
+ events?: readonly SubscribableEventType[];
109
+ /** At most 20. */
110
+ extensions?: readonly ManifestExtension[];
111
+ settingsSchema?: SettingsSchema;
112
+ dataProcessing: DataProcessing;
113
+ }
114
+ /**
115
+ * Identity function that gives editor completion and compile-time checks for a manifest.
116
+ * CheckCourt validates the rest on upload (scope and event pairing, URL rules, lengths).
117
+ */
118
+ export declare function defineManifest<const M extends Manifest>(manifest: M): M;
119
+ export {};
@@ -0,0 +1,79 @@
1
+ export const MANIFEST_VERSION = 1;
2
+ export const INSTALL_TARGETS = ["tenant", "user"];
3
+ export const USER_LINKING_MODES = ["none", "optional", "required"];
4
+ export const EXTENSION_KINDS = ["declarative", "iframe"];
5
+ /** Where each point is shown and how it may render. */
6
+ export const EXTENSION_POINTS = {
7
+ "app.settings": { targets: ["tenant"], kinds: ["declarative", "iframe"] },
8
+ "booking.detail.panel": { targets: ["tenant", "user"], kinds: ["declarative", "iframe"] },
9
+ "member.profile.section": { targets: ["tenant", "user"], kinds: ["declarative", "iframe"] },
10
+ "dashboard.widget": { targets: ["tenant", "user"], kinds: ["declarative"] },
11
+ "booking.action": { targets: ["tenant", "user"], kinds: ["declarative"] },
12
+ "kiosk.tile": { targets: ["tenant"], kinds: ["declarative"] },
13
+ };
14
+ /** Scope the installation needs (in `tenantScopes` or `userScopes`) before the point hands it a subject id. */
15
+ export const EXTENSION_POINT_SCOPE = {
16
+ "app.settings": null,
17
+ "booking.detail.panel": "bookings:read",
18
+ "booking.action": "bookings:read",
19
+ "member.profile.section": "members:read",
20
+ "dashboard.widget": null,
21
+ "kiosk.tile": null,
22
+ };
23
+ /** Every scope an app may request. Role, app, webhook, key, billing and AVV management are reserved for people. */
24
+ export const GRANTABLE_SCOPES = [
25
+ "courts:read",
26
+ "courts:read_confidential",
27
+ "courts:write",
28
+ "bookings:read",
29
+ "bookings:read_confidential",
30
+ "bookings:write",
31
+ "bookings:cancel",
32
+ "bookings:edit",
33
+ "bookings:export",
34
+ "members:read",
35
+ "members:read_confidential",
36
+ "members:invite",
37
+ "members:write",
38
+ "members:delete",
39
+ "teams:read",
40
+ "teams:write",
41
+ "policies:read",
42
+ "policies:read_confidential",
43
+ "policies:write",
44
+ "categories:read",
45
+ "categories:write",
46
+ "invites:read",
47
+ "invites:write",
48
+ "settings:read",
49
+ "settings:read_confidential",
50
+ "settings:write",
51
+ "guest_fees:read",
52
+ "guest_fees:write",
53
+ "audit:read",
54
+ "announcements:read",
55
+ "announcements:write",
56
+ "events:read",
57
+ "events:write",
58
+ "posts:read",
59
+ "posts:write",
60
+ "posts:moderate",
61
+ "court_layout:read",
62
+ "court_layout:write",
63
+ "work_hours:read",
64
+ "work_hours:write",
65
+ "work_hours:manage",
66
+ "compliance:read",
67
+ "kiosk:manage",
68
+ "trainer_blocks:write",
69
+ "analytics:read",
70
+ "embeds:manage",
71
+ "webhooks:read",
72
+ ];
73
+ /**
74
+ * Identity function that gives editor completion and compile-time checks for a manifest.
75
+ * CheckCourt validates the rest on upload (scope and event pairing, URL rules, lengths).
76
+ */
77
+ export function defineManifest(manifest) {
78
+ return manifest;
79
+ }