@tula-tech/contract 0.0.0-stage → 0.1.0-alpha.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,227 @@
1
+ /**
2
+ * The theme tokens every Tula front end shares: names, types and default values for light and
3
+ * dark.
4
+ *
5
+ * This module is plain data and pure functions with no dependency (no Zod), so `@tula-tech/react`
6
+ * loads it at run time from `@tula-tech/contract/theme`, and the Swift and Kotlin SDKs generate
7
+ * their constants from the same table. Everything here is JSON-serialisable on purpose: a
8
+ * theme can be written in `tula.config.ts`, stored by the dashboard's editor, or read by a
9
+ * code generator in another language.
10
+ *
11
+ * Tokens are append-only, like error codes: removing or renaming one is a breaking change for
12
+ * every app that sets it.
13
+ */
14
+ /**
15
+ * The tokens that differ between light and dark: colours and the card's shadow.
16
+ *
17
+ * Colours are `#rrggbb`, or on the web any form {@link isValidThemeValue} accepts (hex,
18
+ * `rgb()`-style functions, keywords); the defaults are all `#rrggbb` so that every platform can parse them and their contrast can be checked.
19
+ */
20
+ interface ThemeScheme {
21
+ /** Behind a component: the card, the menu, the dialog. */
22
+ background: string;
23
+ /** A quieter panel on top of the background: list rows, the hover state of a menu item. */
24
+ surface: string;
25
+ /** The inside of a text field. */
26
+ input: string;
27
+ /** Body text and headings. */
28
+ text: string;
29
+ /** Secondary text: hints, descriptions, an unmet checklist rule. */
30
+ textMuted: string;
31
+ /** The brand colour: the main button, the selected state. */
32
+ primary: string;
33
+ /** The main button while hovered or pressed. */
34
+ primaryHover: string;
35
+ /** Text and icons on top of `primary`. */
36
+ primaryText: string;
37
+ /** Links and text buttons. */
38
+ link: string;
39
+ /** Hairlines: the card's edge, dividers. */
40
+ border: string;
41
+ /** The edge of a control (a text field, a secondary button). At least 3:1 on `background`. */
42
+ borderStrong: string;
43
+ /** Error text, the edge of an invalid field, destructive actions. */
44
+ danger: string;
45
+ /** Behind an error message. */
46
+ dangerSurface: string;
47
+ /** A met checklist rule, a success message. */
48
+ success: string;
49
+ /** The keyboard focus ring. At least 3:1 on `background`. */
50
+ focus: string;
51
+ /** Dims the page behind a dialog. */
52
+ overlay: string;
53
+ /** The card's shadow (a CSS `box-shadow` value; other platforms map it to an elevation). */
54
+ shadow: string;
55
+ }
56
+ /**
57
+ * A complete theme: one {@link ThemeScheme} for light, one for dark, and the tokens both share.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const theme: Theme = { ...DEFAULT_THEME, radius: '6px' }
62
+ * ```
63
+ */
64
+ interface Theme {
65
+ /** Colours and shadow in the light scheme. */
66
+ light: ThemeScheme;
67
+ /** Colours and shadow in the dark scheme. */
68
+ dark: ThemeScheme;
69
+ /** Corner radius of cards, fields and buttons. */
70
+ radius: string;
71
+ /** Corner radius of small parts: badges, menu items, the checklist bar. */
72
+ radiusSmall: string;
73
+ /** The typeface of everything. */
74
+ fontFamily: string;
75
+ /** The typeface of codes and keys. */
76
+ fontFamilyMono: string;
77
+ /** Body text size. */
78
+ fontSize: string;
79
+ /** Size of hints, badges and the checklist. */
80
+ fontSizeSmall: string;
81
+ /** Size of a card's title. */
82
+ fontSizeTitle: string;
83
+ /** The spacing unit: gaps and paddings are multiples of it. */
84
+ spacing: string;
85
+ }
86
+ /**
87
+ * Part of a theme: only the tokens an app wants to change. What it leaves out keeps the
88
+ * default. Values are validated before use (see {@link isValidThemeValue}): one that is not a
89
+ * plain colour, length, font list or shadow is dropped and the default stays.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const brand: ThemeOverrides = {
94
+ * light: { primary: '#0f766e', primaryHover: '#115e59' },
95
+ * dark: { primary: '#5eead4', primaryText: '#042f2e' },
96
+ * radius: '6px',
97
+ * }
98
+ * ```
99
+ */
100
+ type ThemeOverrides = Partial<Omit<Theme, "light" | "dark">> & {
101
+ /** Tokens to change in the light scheme. */
102
+ light?: Partial<ThemeScheme>;
103
+ /** Tokens to change in the dark scheme. */
104
+ dark?: Partial<ThemeScheme>;
105
+ };
106
+ /** What kind of value a token holds, for a generator that has to map it to a platform type. */
107
+ type ThemeTokenType = "color" | "shadow" | "length" | "fontFamily";
108
+ /** One token of the theme, described for code generators and documentation. */
109
+ interface ThemeTokenDefinition {
110
+ /** The token's key in {@link ThemeScheme} (`scope: 'scheme'`) or {@link Theme} (`'shared'`). */
111
+ readonly key: string;
112
+ /** `scheme`: a value per colour scheme. `shared`: one value for both. */
113
+ readonly scope: "scheme" | "shared";
114
+ /** The kind of value. */
115
+ readonly type: ThemeTokenType;
116
+ /**
117
+ * The CSS custom property an app sets to change the token (in the light scheme, for a
118
+ * `scheme` token). A `scheme` token's dark value is set with {@link darkCssVariable}.
119
+ */
120
+ readonly cssVariable: `--tula-${string}`;
121
+ /** What the token is used for. */
122
+ readonly description: string;
123
+ }
124
+ /**
125
+ * Every token, in the order stylesheets and generated constants list them.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * for (const token of THEME_TOKENS) {
130
+ * // token.key 'primary', token.cssVariable '--tula-color-primary', token.type 'color'
131
+ * }
132
+ * ```
133
+ */
134
+ declare const THEME_TOKENS: readonly ThemeTokenDefinition[];
135
+ /**
136
+ * The default theme. Every text colour has a contrast of at least 4.5:1 on the backgrounds it
137
+ * is drawn on, and control edges and the focus ring at least 3:1, in both schemes (WCAG 2.2
138
+ * AA); the contract's tests compute it.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * DEFAULT_THEME.light.primary // '#5b4cf0'
143
+ * ```
144
+ */
145
+ declare const DEFAULT_THEME: Theme;
146
+ /**
147
+ * The CSS custom property that sets a `scheme` token's value in the dark scheme.
148
+ *
149
+ * Light and dark have separate properties so that an app can set both in one place (a
150
+ * stylesheet rule or an inline style) without a media query.
151
+ *
152
+ * @param cssVariable - The token's `cssVariable`.
153
+ * @returns The dark property's name.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * darkCssVariable('--tula-color-primary') // '--tula-dark-color-primary'
158
+ * ```
159
+ */
160
+ declare function darkCssVariable(cssVariable: `--tula-${string}`): `--tula-dark-${string}`;
161
+ /**
162
+ * Whether a string is a plain value of a token's type, safe to write into a stylesheet or a
163
+ * `style` attribute.
164
+ *
165
+ * Theme values are **untrusted**: a brand colour can come from a database a tenant edits, and
166
+ * a server-rendered theme is written into the page as text, where `red; background: url(…)`
167
+ * would become a second declaration. So a value is not escaped, it is checked against a strict
168
+ * grammar for its type and dropped when it does not match:
169
+ *
170
+ * - `color`: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`; `rgb()`, `rgba()`, `hsl()`, `hsla()`,
171
+ * `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()` with plain numeric arguments; or a colour
172
+ * keyword (letters only).
173
+ * - `length`: `0`, or a number with one of `px rem em % ch ex pt vw vh vmin vmax`.
174
+ * - `fontFamily`: a comma-separated list of quoted names or bare words.
175
+ * - `shadow`: `none`, or comma-separated layers of an optional `inset`, two to four lengths
176
+ * and at most one colour.
177
+ *
178
+ * Whatever the type, nothing longer than 200 characters and nothing containing `;` `{` `}`
179
+ * `<` `>` `\` `@` `!`, a line break, a comment opener, `url(`, `expression(` or `var(` passes.
180
+ *
181
+ * @param type - The token's type.
182
+ * @param value - The candidate value.
183
+ * @returns `true` when the value may be used.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * isValidThemeValue('color', '#0f766e') // true
188
+ * isValidThemeValue('color', 'red; background: url(https://evil.example/x)') // false
189
+ * ```
190
+ */
191
+ declare function isValidThemeValue(type: ThemeTokenType, value: string): boolean;
192
+ /**
193
+ * Turn a theme, or part of one, into CSS custom properties.
194
+ *
195
+ * Only the tokens present in `theme` are returned, so the result of a partial theme can be
196
+ * applied as an inline style on top of the stylesheet's defaults. Unknown keys are ignored,
197
+ * and so is any value that {@link isValidThemeValue} refuses: theme values are untrusted (they
198
+ * may come from tenant-editable data) and are never written out unless they are a plain value
199
+ * of their token's type.
200
+ * Light values use each token's `cssVariable`; dark values its {@link darkCssVariable}.
201
+ *
202
+ * @param theme - A theme or overrides.
203
+ * @returns Property names and values, in the order of {@link THEME_TOKENS}.
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * themeToCssVariables({ light: { primary: '#0f766e' }, dark: { primary: '#5eead4' }, radius: '6px' })
208
+ * // { '--tula-color-primary': '#0f766e', '--tula-dark-color-primary': '#5eead4', '--tula-radius': '6px' }
209
+ * ```
210
+ */
211
+ declare function themeToCssVariables(theme: ThemeOverrides): Record<string, string>;
212
+ /**
213
+ * The WCAG contrast ratio of two opaque colours, from 1 (the same colour) to 21 (black on
214
+ * white). Body text needs 4.5; large text, control edges and focus rings need 3.
215
+ *
216
+ * @param foreground - A `#rrggbb` colour.
217
+ * @param background - A `#rrggbb` colour.
218
+ * @returns The ratio.
219
+ * @throws TypeError for a colour that is not `#rrggbb`.
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * contrastRatio('#ffffff', '#5b4cf0') >= 4.5 // white text on the default primary passes AA
224
+ * ```
225
+ */
226
+ declare function contrastRatio(foreground: string, background: string): number;
227
+ export { DEFAULT_THEME, THEME_TOKENS, Theme, ThemeOverrides, ThemeScheme, ThemeTokenDefinition, ThemeTokenType, contrastRatio, darkCssVariable, isValidThemeValue, themeToCssVariables };
package/dist/theme.js ADDED
@@ -0,0 +1,16 @@
1
+ import {
2
+ THEME_TOKENS2,
3
+ DEFAULT_THEME2,
4
+ darkCssVariable2,
5
+ isValidThemeValue2,
6
+ themeToCssVariables2,
7
+ contrastRatio2
8
+ } from "./shared/chunk-pcn6ax54.js";
9
+ export {
10
+ DEFAULT_THEME2 as DEFAULT_THEME,
11
+ THEME_TOKENS2 as THEME_TOKENS,
12
+ contrastRatio2 as contrastRatio,
13
+ darkCssVariable2 as darkCssVariable,
14
+ isValidThemeValue2 as isValidThemeValue,
15
+ themeToCssVariables2 as themeToCssVariables
16
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Request header carrying the delivery's id: the event's id. A delivery that is repeated
3
+ * carries the same id, so a receiver drops duplicates by it.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const id = request.headers.get(WEBHOOK_ID_HEADER)
8
+ * ```
9
+ */
10
+ declare const WEBHOOK_ID_HEADER = "webhook-id";
11
+ /**
12
+ * Request header carrying when the delivery was sent, in whole seconds since the Unix epoch.
13
+ * It is signed, and a receiver refuses one too far from its own clock.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const sentAt = Number(request.headers.get(WEBHOOK_TIMESTAMP_HEADER)) * 1000
18
+ * ```
19
+ */
20
+ declare const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
21
+ /**
22
+ * Request header carrying the signatures: one or more of `v1,<base64>`, separated by spaces.
23
+ * A receiver accepts the delivery when any one of them is right.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const signatures = (request.headers.get(WEBHOOK_SIGNATURE_HEADER) ?? '').split(' ')
28
+ * ```
29
+ */
30
+ declare const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
31
+ /**
32
+ * What an endpoint's signing secret starts with. The rest is the base64 of the key's bytes.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * secret.startsWith(WEBHOOK_SECRET_PREFIX) // 'whsec_…'
37
+ * ```
38
+ */
39
+ declare const WEBHOOK_SECRET_PREFIX = "whsec_";
40
+ /**
41
+ * The version label of an HMAC-SHA256 signature: what stands before the comma.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const [version, signature] = entry.split(',') // version === WEBHOOK_SIGNATURE_VERSION
46
+ * ```
47
+ */
48
+ declare const WEBHOOK_SIGNATURE_VERSION = "v1";
49
+ /**
50
+ * How far a delivery's timestamp may be from the receiver's clock, either way, in seconds.
51
+ * Five minutes: what bounds the replay of a captured delivery.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const fresh = Math.abs(now - sentAt) <= WEBHOOK_TOLERANCE_SECONDS
56
+ * ```
57
+ */
58
+ declare const WEBHOOK_TOLERANCE_SECONDS: number;
59
+ /**
60
+ * The key bytes of a signing secret.
61
+ *
62
+ * @param secret - An endpoint's secret: `whsec_` and the base64 of 24 to 64 bytes.
63
+ * @returns The bytes, or `null` for anything else. Never throws, and says nothing of the value.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * const key = webhookSecretBytes(process.env.TULA_WEBHOOK_SECRET ?? '')
68
+ * ```
69
+ */
70
+ declare function webhookSecretBytes(secret: string): Uint8Array<ArrayBuffer> | null;
71
+ /**
72
+ * Write a signing secret from its key bytes.
73
+ *
74
+ * @param key - 24 to 64 random bytes.
75
+ * @returns `whsec_` and the base64 of the bytes.
76
+ * @throws RangeError when the key is shorter or longer than the scheme allows.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const secret = formatWebhookSecret(crypto.getRandomValues(new Uint8Array(32)))
81
+ * ```
82
+ */
83
+ declare function formatWebhookSecret(key: Uint8Array): string;
84
+ /**
85
+ * Sign one delivery: HMAC-SHA256 over `<id>.<timestamp>.<body>`, keyed with the secret's bytes.
86
+ *
87
+ * The body is the exact text that is sent. A receiver must verify the bytes it received, not
88
+ * a body it parsed and wrote out again.
89
+ *
90
+ * @param key - The key bytes ({@link webhookSecretBytes}).
91
+ * @param id - The delivery's id (the `webhook-id` header). It has no `.` in it.
92
+ * @param timestamp - Whole seconds since the Unix epoch (the `webhook-timestamp` header).
93
+ * @param body - The request body, as sent.
94
+ * @returns The signature as it stands in the header: `v1,<base64>`.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const signature = await signWebhook(key, event.id, Math.floor(Date.now() / 1000), body)
99
+ * // 'v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE='
100
+ * ```
101
+ */
102
+ declare function signWebhook(key: Uint8Array<ArrayBuffer>, id: string, timestamp: number, body: string): Promise<string>;
103
+ export { WEBHOOK_ID_HEADER, WEBHOOK_SECRET_PREFIX, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_VERSION, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TOLERANCE_SECONDS, formatWebhookSecret, signWebhook, webhookSecretBytes };
@@ -0,0 +1,22 @@
1
+ import {
2
+ WEBHOOK_ID_HEADER2,
3
+ WEBHOOK_TIMESTAMP_HEADER2,
4
+ WEBHOOK_SIGNATURE_HEADER2,
5
+ WEBHOOK_SECRET_PREFIX2,
6
+ WEBHOOK_SIGNATURE_VERSION2,
7
+ WEBHOOK_TOLERANCE_SECONDS2,
8
+ webhookSecretBytes2,
9
+ formatWebhookSecret2,
10
+ signWebhook2
11
+ } from "./shared/chunk-2xak18qy.js";
12
+ export {
13
+ WEBHOOK_ID_HEADER2 as WEBHOOK_ID_HEADER,
14
+ WEBHOOK_SECRET_PREFIX2 as WEBHOOK_SECRET_PREFIX,
15
+ WEBHOOK_SIGNATURE_HEADER2 as WEBHOOK_SIGNATURE_HEADER,
16
+ WEBHOOK_SIGNATURE_VERSION2 as WEBHOOK_SIGNATURE_VERSION,
17
+ WEBHOOK_TIMESTAMP_HEADER2 as WEBHOOK_TIMESTAMP_HEADER,
18
+ WEBHOOK_TOLERANCE_SECONDS2 as WEBHOOK_TOLERANCE_SECONDS,
19
+ formatWebhookSecret2 as formatWebhookSecret,
20
+ signWebhook2 as signWebhook,
21
+ webhookSecretBytes2 as webhookSecretBytes
22
+ };