@taprootio/site-authoring 0.12.5 → 0.12.7

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
@@ -188,7 +188,7 @@ Which verbs apply depends on the site:
188
188
  `pull` snapshots its four settings documents, `theme push`, `footer push`,
189
189
  and `media upload` author them, and `deploy` stages and promotes them.
190
190
  Its pages, navigation, and redirects come from the Docs artifact, so
191
- `pages push`, `nav push`, the `redirects` verbs, `approve`, and the
191
+ `pages push`, `nav push`, the `redirects` and `forms` verbs, `approve`, and the
192
192
  `preview` verbs refuse with `surface.presentation_only` before any request.
193
193
  - A **prebuilt Docs** site (`none`) serves its artifact unchanged and takes no
194
194
  verb; each refuses with `surface.none`.
@@ -373,6 +373,16 @@ validates the file offline and replaces the whole map; `help redirects`
373
373
  states the entry shape (`path`, `kind` redirect or gone, `target`, `status`
374
374
  defaulting to 301), the normalization rule, and every refusal.
375
375
 
376
+ `forms pull` writes each live form to `forms/<key>.json` and records the
377
+ version it read; `forms push` validates every file, refuses a form the site
378
+ changed since (`forms.concurrent_modification`) and creates or updates the
379
+ rest, appending a version when the definition changed. `forms validate` checks
380
+ the files offline against the shared field schema with no credential. Both
381
+ verbs write one canonical key order, so a pulled file pushed back is unchanged.
382
+ Submissions are never pulled. `help forms` states the file shape and every
383
+ refusal, and placing a form on a page is the free-form `form`
384
+ component.
385
+
376
386
  `preview page <page-path-or-id>` renders one persisted draft behind the
377
387
  staging gate and mints a browser handoff URL, reported only in the final JSON
378
388
  result. **Each handoff is single-use.** Opening the URL consumes it; a reused,
@@ -537,8 +547,8 @@ completed deployment for the target, HTTP delivery of the authored routes and
537
547
  the favicon, images, module preloads and site bundle they declare (status and
538
548
  content type, no redirects followed), internal link targets, accidental
539
549
  local-only references, and the pinned runtime — the version the page names,
540
- its entry module and the capability modules the page declares, with a page
541
- that still names the pre-pinning runtime pointer reported as unverified until it is republished. The
550
+ its entry module and the capability modules the page declares (a page that
551
+ pins no runtime version and entry fails the runtime check). The
542
552
  browser dimension (a fresh and a returning load
543
553
  compared against the page's pinned entry, every document request in the browser,
544
554
  popups included, held to the site origin before it is sent, and cache reuse
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taprootio/site-authoring",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
4
4
  "description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -28,7 +28,7 @@
28
28
  "README.md"
29
29
  ],
30
30
  "dependencies": {
31
- "@taprootio/espalier": "4.20.1"
31
+ "@taprootio/espalier": "5.0.0"
32
32
  },
33
33
  "scripts": {
34
34
  "test": "node --test",
@@ -23,7 +23,7 @@ export const SITE_AUTHORING_CAPABILITIES = Object.freeze([
23
23
 
24
24
  /** What each capability lets an exchanged credential do, for help and `whoami`. */
25
25
  export const SITE_AUTHORING_CAPABILITY_SUMMARIES = Object.freeze({
26
- "delegation.content": "write pages and media",
26
+ "delegation.content": "write pages, media, and form definitions",
27
27
  "delegation.design": "set the theme, navigation, and footer",
28
28
  "delegation.deployments": "stage and deploy",
29
29
  });
package/src/cli.js CHANGED
@@ -20,6 +20,9 @@ import {
20
20
  VERB_STAGING_REVIEW,
21
21
  VERB_ENV,
22
22
  VERB_FOOTER_PUSH,
23
+ VERB_FORMS_PULL,
24
+ VERB_FORMS_PUSH,
25
+ VERB_FORMS_VALIDATE,
23
26
  VERB_HELP,
24
27
  VERB_LOGIN,
25
28
  VERB_LOGOUT,
@@ -71,14 +74,14 @@ import { VERB_HANDLERS } from "./verbs/index.js";
71
74
  /**
72
75
  * The verbs the local version gate applies to (TR00703).
73
76
  *
74
- * Exactly the three that make no request. Every other verb reaches Taproot,
77
+ * Exactly the ones that make no request. Every other verb reaches Taproot,
75
78
  * whose own refusal is authoritative and better informed than this recording,
76
79
  * so gating them here would only duplicate an answer — and gating `login`,
77
80
  * `logout`, or `env` would take away the commands an operator needs while they
78
81
  * are outdated. `--help` and `--version` are outside it for the same reason:
79
82
  * they are how someone finds out what they have.
80
83
  */
81
- const VERSION_GATED_OFFLINE_VERBS = Object.freeze([VERB_HELP, VERB_VALIDATE, VERB_WHOAMI]);
84
+ const VERSION_GATED_OFFLINE_VERBS = Object.freeze([VERB_HELP, VERB_VALIDATE, VERB_FORMS_VALIDATE, VERB_WHOAMI]);
82
85
 
83
86
  // One entry per verb, in help order. `tokens` is the exact leading positional
84
87
  // sequence; matching prefers the longest sequence, so a two-token family can
@@ -281,6 +284,47 @@ const VERBS = Object.freeze([
281
284
  + "goes live with that deploy: staging first, production when promoted. "
282
285
  + "See 'taproot-site help redirects'.",
283
286
  },
287
+ {
288
+ name: VERB_FORMS_PULL,
289
+ surface: SURFACE_STANDARD,
290
+ // Forms are authored with the pages that place them, so they ride the
291
+ // content capability; the site's forms permissions decide what it may do.
292
+ capabilities: [CAPABILITY_CONTENT],
293
+ tokens: ["forms", "pull"],
294
+ summary: "Write each live form to forms/<key>.json and record the versions read.",
295
+ note: "Needs a pulled workspace ('pull' first). It replaces a form's file with the site's copy and records the "
296
+ + "version, which 'forms push' uses to refuse a change made on the site since. An archived form is listed "
297
+ + "and not written. Submissions are never pulled: they are visitors' personal data and stay on the site. "
298
+ + "See 'taproot-site help forms'.",
299
+ },
300
+ {
301
+ name: VERB_FORMS_PUSH,
302
+ surface: SURFACE_STANDARD,
303
+ capabilities: [CAPABILITY_CONTENT],
304
+ tokens: ["forms", "push"],
305
+ positionals: "formKeys",
306
+ summary: "Validate forms/<key>.json and create or update each form on the site.",
307
+ note: "Every file is validated and compared with the site before anything is written. A form whose site "
308
+ + "version moved since the last pull is refused (forms.concurrent_modification), and a form the site has "
309
+ + "that this workspace never pulled is refused (forms.pull_required). Editing a definition appends a version; "
310
+ + "a page keeps the version it was published with until the next deploy. An after_submit page_path is looked "
311
+ + "up on the site, and a path that names no page is refused (forms.page_not_found), as is a generated, 404, legal, profile or integration page (forms.page_not_allowed). Name form keys to narrow "
312
+ + "the push. See 'taproot-site help forms'.",
313
+ },
314
+ {
315
+ name: VERB_FORMS_VALIDATE,
316
+ tokens: ["forms", "validate"],
317
+ offline: true,
318
+ // Offline, but it reads taproot-site.json to find the workspace.
319
+ readsLocalState: true,
320
+ positionals: "formKeys",
321
+ summary: "Validate the workspace's form files offline, against the shared field schema.",
322
+ note: "Reads forms/<key>.json and checks the file contract and every field definition with the same validator "
323
+ + "'forms push' and the published form use. Name form keys to narrow it; with none, every file under forms/ "
324
+ + "is checked. It cannot prove that the site accepts the key, that an after_submit page_path names a page, or "
325
+ + "that the caller may write forms. "
326
+ + "See 'taproot-site help forms'.",
327
+ },
284
328
  {
285
329
  name: VERB_THEME_PUSH,
286
330
  surface: SURFACE_DOCS_PRESENTATION,
@@ -556,6 +600,8 @@ function verbHelp(verb) {
556
600
  ? " [production | local | <url>]"
557
601
  : verb.positionals === "siteSelector"
558
602
  ? " <site-name-or-id>"
603
+ : verb.positionals === "formKeys"
604
+ ? " [form-key...]"
559
605
  : verb.positionals
560
606
  ? ` [${verb.positionals === "paths" ? "path" : "page-path"}...]`
561
607
  : "";
@@ -706,6 +752,7 @@ function parseReferenceArguments(arguments_) {
706
752
  || topic === "footer"
707
753
  || topic === "fixture"
708
754
  || topic === "redirects"
755
+ || topic === "forms"
709
756
  || topic === "walkthrough"
710
757
  || topic === "delivery"
711
758
  || topic === "import"
@@ -773,6 +820,7 @@ function referenceResult(parsed) {
773
820
  }
774
821
  case "nav":
775
822
  case "redirects":
823
+ case "forms":
776
824
  case "media":
777
825
  case "preview":
778
826
  case "fixture":
package/src/constants.js CHANGED
@@ -87,6 +87,9 @@ export const VERB_NAV_PUSH = "nav push";
87
87
  export const VERB_REDIRECTS_CHECK = "redirects check";
88
88
  export const VERB_REDIRECTS_PULL = "redirects pull";
89
89
  export const VERB_REDIRECTS_PUSH = "redirects push";
90
+ export const VERB_FORMS_PULL = "forms pull";
91
+ export const VERB_FORMS_PUSH = "forms push";
92
+ export const VERB_FORMS_VALIDATE = "forms validate";
90
93
  export const VERB_THEME_PUSH = "theme push";
91
94
  export const VERB_FOOTER_PUSH = "footer push";
92
95
  export const VERB_MEDIA_UPLOAD = "media upload";
@@ -286,6 +286,15 @@ const SPACER = Object.freeze({
286
286
  dividerWidth: num({ integer: true, minimum: 1 }),
287
287
  });
288
288
 
289
+ /**
290
+ * A form defined under Site → Forms (or with `forms push`), placed by key. The
291
+ * key is checked against the site's forms when the page is published, not here:
292
+ * this table only knows the shape.
293
+ */
294
+ const FORM = Object.freeze({
295
+ formKey: str(),
296
+ });
297
+
289
298
  function deepFreeze(value) {
290
299
  if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
291
300
  for (const child of Object.values(value)) deepFreeze(child);
@@ -555,6 +564,18 @@ const COMPONENT_DEFINITIONS = Object.freeze({
555
564
  imageMotion: "parallax-subtle",
556
565
  },
557
566
  ),
567
+ "form": component(
568
+ FORM,
569
+ "Form",
570
+ "A form from Site → Forms that visitors fill in and submit.",
571
+ { formKey: "" },
572
+ [
573
+ "Set formKey to the key of an existing form ('forms push' creates it); an unknown or archived key fails the publish.",
574
+ "The button text and what happens after a response is sent belong to the form, not the page: set submit_label and after_submit in the form's file (see 'help forms').",
575
+ "The form's field labels are its accessible names; write each so it makes sense out of context.",
576
+ ],
577
+ { formKey: "contact" },
578
+ ),
558
579
  });
559
580
 
560
581
  export const COMPONENT_TYPES = Object.freeze(Object.keys(COMPONENT_DEFINITIONS));
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 7,
2
+ "version": 9,
3
3
  "section": {
4
4
  "nodeType": "section",
5
5
  "label": "Section",
@@ -554,6 +554,11 @@
554
554
  "label": "Image Banner",
555
555
  "measure": "wide",
556
556
  "rootPlacement": "root-band"
557
+ },
558
+ "form": {
559
+ "label": "Form",
560
+ "measure": "wide",
561
+ "rootPlacement": "flow"
557
562
  }
558
563
  },
559
564
  "integrationPlacement": {
@@ -205,8 +205,6 @@ export function parseBootstrap(html) {
205
205
  return {
206
206
  version: text(parsed.version).slice(0, 64),
207
207
  entry: text(parsed.entry),
208
- // A release published before runtime pinning named a mutable pointer instead.
209
- legacyPointer: typeof parsed.runtimeManifestUrl === "string" && parsed.runtimeManifestUrl !== "",
210
208
  siteBundleUrl: text(parsed.siteBundleUrl),
211
209
  runtimeCapabilities,
212
210
  capabilities,
@@ -506,9 +504,6 @@ async function observe(context, origin, sampledRoutes) {
506
504
  }
507
505
  }
508
506
  runtime.pinned = runtime.version !== "" && Boolean(bootstrap?.entry);
509
- // Such a page still loads in a browser, but this check no longer reads the
510
- // pointer, so its runtime is reported unverified rather than as passed or failed.
511
- runtime.legacy = !runtime.pinned && Boolean(bootstrap?.legacyPointer);
512
507
  const entryUrl = runtime.pinned ? admit(bootstrap.entry) : undefined;
513
508
  if (entryUrl) {
514
509
  const { row } = await probe(context, entryUrl.href, "runtime-entry");
@@ -570,10 +565,7 @@ async function observe(context, origin, sampledRoutes) {
570
565
  ...assetResults.filter((row) => !row.ok).map((row) => `${row.kind} ${row.url}: ${row.failure}`),
571
566
  ...linkResults.filter((row) => !row.ok).map((row) => `link ${row.path}: ${row.failure}`),
572
567
  ...(runtime.declared ? [] : ["runtime: the home page declares no runtime bootstrap"]),
573
- ...(runtime.declared && runtime.legacy
574
- ? ["runtime: the page names a pre-pinning runtime pointer, which is not verified here; republish the site so it pins a runtime version"]
575
- : []),
576
- ...(runtime.declared && !runtime.pinned && !runtime.legacy
568
+ ...(runtime.declared && !runtime.pinned
577
569
  ? ["runtime: the page does not pin a runtime version and entry"]
578
570
  : []),
579
571
  ...(runtime.declared && runtime.pinned && !runtime.entry.ok
@@ -677,7 +669,7 @@ export async function checkDelivery({
677
669
 
678
670
  const verdict = observed.failuresTotal === 0
679
671
  ? "delivered"
680
- : observed.routeRows.every((row) => row.ok) && (observed.runtime.compatible || observed.runtime.legacy)
672
+ : observed.routeRows.every((row) => row.ok) && observed.runtime.compatible
681
673
  ? "degraded"
682
674
  : "failed";
683
675
  const report = {
@@ -757,7 +749,6 @@ function withinOutputBudget(report, failuresTotal) {
757
749
  declared: runtime.declared,
758
750
  version: runtime.version,
759
751
  pinned: runtime.pinned,
760
- legacy: runtime.legacy,
761
752
  capabilities: runtime.capabilities.length,
762
753
  capabilitiesResolvable: runtime.capabilitiesResolvable,
763
754
  compatible: runtime.compatible,
@@ -0,0 +1,217 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://taproot.io/schemas/field-definitions.json",
4
+ "title": "Taproot field definitions",
5
+ "description": "A flat, closed list of typed fields. shared/field-validation.ts and the API validator interpret it; shared/field-validation-corpus.json is the evidence they agree. Limits: 30 fields, 16 KB of submitted content. JSON Schema maxLength counts code points while the validators count UTF-16 code units, so the validators are authoritative and stricter for astral characters.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["fields"],
9
+ "properties": {
10
+ "fields": {
11
+ "type": "array",
12
+ "minItems": 1,
13
+ "maxItems": 30,
14
+ "items": { "$ref": "#/$defs/field" }
15
+ },
16
+ "contact_field": {
17
+ "description": "The id of the email field whose value the API copies to subject_email. It must name an email field. Omit it when the form has one email field (that field is the contact); it is required with two or more.",
18
+ "$ref": "#/$defs/id"
19
+ },
20
+ "submit_label": {
21
+ "description": "The text on the submit button. Omit it for Taproot's default. Must contain more than whitespace; the validator also rejects a blank label, which a schema cannot express.",
22
+ "type": "string",
23
+ "minLength": 1,
24
+ "maxLength": 40
25
+ },
26
+ "after_submit": {
27
+ "description": "What a visitor sees once a response is sent. Omit it to show Taproot's default message. A page is named by its resource id and must belong to the same site; the release freezes the path the page has when it is built.",
28
+ "oneOf": [
29
+ {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": ["show"],
33
+ "properties": {
34
+ "show": { "const": "message" },
35
+ "message": {
36
+ "description": "Omit it for Taproot's default. Must contain more than whitespace; the validator also rejects a blank message, which a schema cannot express.",
37
+ "type": "string",
38
+ "minLength": 1,
39
+ "maxLength": 500
40
+ }
41
+ }
42
+ },
43
+ {
44
+ "type": "object",
45
+ "additionalProperties": false,
46
+ "required": ["show", "page_resource_id"],
47
+ "properties": {
48
+ "show": { "const": "page" },
49
+ "page_resource_id": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }
50
+ }
51
+ }
52
+ ]
53
+ }
54
+ },
55
+ "$defs": {
56
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,39}$" },
57
+ "label": {
58
+ "description": "Must contain more than whitespace; the validator also rejects a blank label, which a schema cannot express.",
59
+ "type": "string",
60
+ "minLength": 1,
61
+ "maxLength": 200
62
+ },
63
+ "consentLabel": {
64
+ "description": "A consent field's label is the agreement the visitor accepts, so it is longer than any other label. Must contain more than whitespace; the validator also rejects a blank label.",
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "maxLength": 1000
68
+ },
69
+ "hint": {
70
+ "description": "Optional helper text drawn under the field. Must contain more than whitespace; the validator rejects a blank hint, which a schema cannot express.",
71
+ "type": "string",
72
+ "minLength": 1,
73
+ "maxLength": 200
74
+ },
75
+ "required": { "type": "boolean" },
76
+ "shortLength": { "type": "integer", "minimum": 0, "maximum": 500 },
77
+ "shortMaxLength": { "type": "integer", "minimum": 1, "maximum": 500 },
78
+ "longLength": { "type": "integer", "minimum": 0, "maximum": 5000 },
79
+ "longMaxLength": { "type": "integer", "minimum": 1, "maximum": 5000 },
80
+ "choices": {
81
+ "description": "Each choice must contain more than whitespace; the validator rejects blank choices, which a schema cannot express.",
82
+ "type": "array",
83
+ "minItems": 1,
84
+ "maxItems": 50,
85
+ "uniqueItems": true,
86
+ "items": { "type": "string", "minLength": 1, "maxLength": 100 }
87
+ },
88
+ "isoDate": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
89
+ "relativeDateBound": {
90
+ "description": "A date measured from the site's calendar date, resolved when the page loads and again when a submission arrives. offset_days is bounded to a year either side (366 covers a leap year); further out is a fixed date. A fixed date is the other form of a date bound.",
91
+ "type": "object",
92
+ "additionalProperties": false,
93
+ "required": ["from"],
94
+ "properties": {
95
+ "from": {
96
+ "enum": ["today", "start_of_month", "end_of_month", "start_of_next_month", "end_of_next_month"]
97
+ },
98
+ "offset_days": { "type": "integer", "minimum": -366, "maximum": 366 }
99
+ }
100
+ },
101
+ "dateBound": {
102
+ "oneOf": [{ "$ref": "#/$defs/isoDate" }, { "$ref": "#/$defs/relativeDateBound" }]
103
+ },
104
+ "field": {
105
+ "type": "object",
106
+ "required": ["id", "type", "label"],
107
+ "oneOf": [
108
+ {
109
+ "properties": {
110
+ "id": { "$ref": "#/$defs/id" },
111
+ "type": { "const": "text" },
112
+ "label": { "$ref": "#/$defs/label" },
113
+ "hint": { "$ref": "#/$defs/hint" },
114
+ "required": { "$ref": "#/$defs/required" },
115
+ "min_length": { "$ref": "#/$defs/shortLength" },
116
+ "max_length": { "$ref": "#/$defs/shortMaxLength" },
117
+ "format": { "enum": ["digits", "alphanumeric"] }
118
+ },
119
+ "additionalProperties": false
120
+ },
121
+ {
122
+ "properties": {
123
+ "id": { "$ref": "#/$defs/id" },
124
+ "type": { "const": "long_text" },
125
+ "label": { "$ref": "#/$defs/label" },
126
+ "hint": { "$ref": "#/$defs/hint" },
127
+ "required": { "$ref": "#/$defs/required" },
128
+ "min_length": { "$ref": "#/$defs/longLength" },
129
+ "max_length": { "$ref": "#/$defs/longMaxLength" }
130
+ },
131
+ "additionalProperties": false
132
+ },
133
+ {
134
+ "properties": {
135
+ "id": { "$ref": "#/$defs/id" },
136
+ "type": { "enum": ["email", "tel", "url"] },
137
+ "label": { "$ref": "#/$defs/label" },
138
+ "hint": { "$ref": "#/$defs/hint" },
139
+ "required": { "$ref": "#/$defs/required" },
140
+ "max_length": { "$ref": "#/$defs/shortMaxLength" }
141
+ },
142
+ "additionalProperties": false
143
+ },
144
+ {
145
+ "properties": {
146
+ "id": { "$ref": "#/$defs/id" },
147
+ "type": { "const": "number" },
148
+ "label": { "$ref": "#/$defs/label" },
149
+ "hint": { "$ref": "#/$defs/hint" },
150
+ "required": { "$ref": "#/$defs/required" },
151
+ "min": { "type": "number" },
152
+ "max": { "type": "number" },
153
+ "format": { "enum": ["integer"] }
154
+ },
155
+ "additionalProperties": false
156
+ },
157
+ {
158
+ "properties": {
159
+ "id": { "$ref": "#/$defs/id" },
160
+ "type": { "const": "date" },
161
+ "label": { "$ref": "#/$defs/label" },
162
+ "hint": { "$ref": "#/$defs/hint" },
163
+ "required": { "$ref": "#/$defs/required" },
164
+ "min": { "$ref": "#/$defs/dateBound" },
165
+ "max": { "$ref": "#/$defs/dateBound" }
166
+ },
167
+ "additionalProperties": false
168
+ },
169
+ {
170
+ "required": ["choices"],
171
+ "properties": {
172
+ "id": { "$ref": "#/$defs/id" },
173
+ "type": { "const": "single_choice" },
174
+ "label": { "$ref": "#/$defs/label" },
175
+ "hint": { "$ref": "#/$defs/hint" },
176
+ "required": { "$ref": "#/$defs/required" },
177
+ "choices": { "$ref": "#/$defs/choices" }
178
+ },
179
+ "additionalProperties": false
180
+ },
181
+ {
182
+ "required": ["choices"],
183
+ "properties": {
184
+ "id": { "$ref": "#/$defs/id" },
185
+ "type": { "const": "multi_choice" },
186
+ "label": { "$ref": "#/$defs/label" },
187
+ "hint": { "$ref": "#/$defs/hint" },
188
+ "required": { "$ref": "#/$defs/required" },
189
+ "choices": { "$ref": "#/$defs/choices" },
190
+ "min": { "type": "integer", "minimum": 0, "maximum": 50 },
191
+ "max": { "type": "integer", "minimum": 0, "maximum": 50 }
192
+ },
193
+ "additionalProperties": false
194
+ },
195
+ {
196
+ "properties": {
197
+ "id": { "$ref": "#/$defs/id" },
198
+ "type": { "const": "checkbox" },
199
+ "label": { "$ref": "#/$defs/label" },
200
+ "hint": { "$ref": "#/$defs/hint" },
201
+ "required": { "$ref": "#/$defs/required" }
202
+ },
203
+ "additionalProperties": false
204
+ },
205
+ {
206
+ "properties": {
207
+ "id": { "$ref": "#/$defs/id" },
208
+ "type": { "const": "consent" },
209
+ "label": { "$ref": "#/$defs/consentLabel" },
210
+ "hint": { "$ref": "#/$defs/hint" }
211
+ },
212
+ "additionalProperties": false
213
+ }
214
+ ]
215
+ }
216
+ }
217
+ }