initiative-plugin-sdk 2.0.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +656 -0
  3. package/dist/build.d.ts +52 -0
  4. package/dist/build.js +229 -0
  5. package/dist/build.js.map +1 -0
  6. package/dist/cli.d.ts +24 -0
  7. package/dist/cli.js +160 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/client.d.ts +288 -0
  10. package/dist/client.js +539 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/contract.d.ts +1247 -0
  13. package/dist/contract.js +167 -0
  14. package/dist/contract.js.map +1 -0
  15. package/dist/define.d.ts +336 -0
  16. package/dist/define.js +120 -0
  17. package/dist/define.js.map +1 -0
  18. package/dist/dev.d.ts +28 -0
  19. package/dist/dev.js +147 -0
  20. package/dist/dev.js.map +1 -0
  21. package/dist/expression.d.ts +28 -0
  22. package/dist/expression.js +67 -0
  23. package/dist/expression.js.map +1 -0
  24. package/dist/init.d.ts +14 -0
  25. package/dist/init.js +52 -0
  26. package/dist/init.js.map +1 -0
  27. package/dist/keys.d.ts +85 -0
  28. package/dist/keys.js +109 -0
  29. package/dist/keys.js.map +1 -0
  30. package/dist/manifest.d.ts +8 -0
  31. package/dist/manifest.js +8 -0
  32. package/dist/manifest.js.map +1 -0
  33. package/dist/pack.d.ts +35 -0
  34. package/dist/pack.js +84 -0
  35. package/dist/pack.js.map +1 -0
  36. package/dist/plugin-api.generated.d.ts +4263 -0
  37. package/dist/plugin-api.generated.js +1019 -0
  38. package/dist/plugin-api.generated.js.map +1 -0
  39. package/dist/server.d.ts +75 -0
  40. package/dist/server.js +408 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/testing.d.ts +116 -0
  43. package/dist/testing.js +386 -0
  44. package/dist/testing.js.map +1 -0
  45. package/dist/tokens.d.ts +161 -0
  46. package/dist/tokens.js +215 -0
  47. package/dist/tokens.js.map +1 -0
  48. package/dist/validate.d.ts +86 -0
  49. package/dist/validate.js +997 -0
  50. package/dist/validate.js.map +1 -0
  51. package/dist/widget.d.ts +235 -0
  52. package/dist/widget.js +26 -0
  53. package/dist/widget.js.map +1 -0
  54. package/examples/minimal/README.md +48 -0
  55. package/examples/minimal/assets/avatar.png +0 -0
  56. package/examples/minimal/package.json +22 -0
  57. package/examples/minimal/src/plugin.ts +77 -0
  58. package/examples/minimal/src/widgets/open-issues.ts +11 -0
  59. package/examples/minimal/test/fixtures/issues-statistics.json +9 -0
  60. package/examples/minimal/test/issue-counts.test.ts +48 -0
  61. package/examples/minimal/tsconfig.json +14 -0
  62. package/manifest.contract.json +1886 -0
  63. package/package.json +92 -0
  64. package/schemas/plugin-manifest.json +1747 -0
@@ -0,0 +1,1747 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://initiative.morels.me/schemas/plugin-manifest-v1.json",
4
+ "title": "Initiative plug-in manifest (the 'definition' body)",
5
+ "description": "What a plug-in declares it can do. A plug-in is a container, which names its `service` and answers Initiative's calls, or declarative, which names its `hosts` and no service, and whose endpoints, connection checks and webhook events Initiative runs itself from the requests and JSONata expressions written here; one plug-in is never both. This is the 'definition' field of the document served at /.well-known/initiative-plugin.json, NOT that whole document: a registrar also requires protocol_version, public_id and kind alongside it, and refuses a definition served bare. Generated from the platform's own validator vocabulary. A manifest that satisfies this schema is well-formed, not necessarily acceptable. Cross-references (the endpoint a widget binds, a requires term's connection, an endpoint's service prefix), the direction-specific rules on an endpoint, the features/blocks cross-check in both directions, UTF-8 byte-size caps, the rules tying a connection's flow and token to its scope and fields, what a webhooks block names, what a vendor setup writes to, whether every expression parses, what a declarative request and its steps name, and the bounds and unique ids of schedules are enforced by the platform on publish and are not expressible here.",
6
+ "type": "object",
7
+ "required": [
8
+ "plugin_kind",
9
+ "features"
10
+ ],
11
+ "properties": {
12
+ "plugin_kind": {
13
+ "const": "service",
14
+ "description": "The kind every plug-in manifest has, container or declarative: a declarative plug-in is one with no 'service' block."
15
+ },
16
+ "service": {
17
+ "type": "object",
18
+ "required": [
19
+ "public_id"
20
+ ],
21
+ "properties": {
22
+ "public_id": {
23
+ "type": "string",
24
+ "maxLength": 120,
25
+ "pattern": "^[\\-.0123456789_abcdefghijklmnopqrstuvwxyz]*\\.[\\-.0123456789_abcdefghijklmnopqrstuvwxyz]*$",
26
+ "description": "'<publisher>.<slug>'. The name the deployment's registration is matched by, and the namespace this plug-in's events are emitted under."
27
+ },
28
+ "protocol": {
29
+ "enum": [
30
+ 1
31
+ ],
32
+ "default": 1
33
+ },
34
+ "scopes": {
35
+ "type": "array",
36
+ "maxItems": 54,
37
+ "uniqueItems": true,
38
+ "items": {
39
+ "anyOf": [
40
+ {
41
+ "enum": [
42
+ "projects:read",
43
+ "projects:write",
44
+ "documents:read",
45
+ "documents:write",
46
+ "queues:read",
47
+ "queues:write",
48
+ "counter_groups:read",
49
+ "counter_groups:write",
50
+ "calendars:read",
51
+ "calendars:write",
52
+ "dashboards:read",
53
+ "dashboards:write",
54
+ "posts:read",
55
+ "posts:write",
56
+ "galleries:read",
57
+ "galleries:write",
58
+ "wikis:read",
59
+ "wikis:write",
60
+ "comments:read",
61
+ "comments:write",
62
+ "relationships:read",
63
+ "relationships:write",
64
+ "tags:read",
65
+ "tags:write",
66
+ "sharing:read",
67
+ "sharing:write",
68
+ "members:read",
69
+ "initiatives:read",
70
+ "initiatives:moderate",
71
+ "community:admin"
72
+ ]
73
+ },
74
+ {
75
+ "$ref": "#/$defs/pluginScope"
76
+ }
77
+ ]
78
+ },
79
+ "description": "The scopes this plug-in asks a community to grant: what its installation and member tokens act with. The fixed scopes, and 'plugins:<public_id>' for each plug-in this one calls (at most 24 of those). The community grants some or all of them when it installs the plug-in, and a token never carries more than was granted. Writing implies reading. Absent means none."
80
+ }
81
+ },
82
+ "description": "A container plug-in's service, which Initiative calls. Absent for a declarative plug-in, whose public id is its listing's."
83
+ },
84
+ "features": {
85
+ "type": "array",
86
+ "maxItems": 4,
87
+ "items": {
88
+ "enum": [
89
+ "dashboards",
90
+ "embeds",
91
+ "endpoints",
92
+ "widgets"
93
+ ]
94
+ },
95
+ "description": "What this plug-in contributes. Cross-checked against the blocks present in both directions: a feature with no block, or a block with no feature, is refused."
96
+ },
97
+ "default_name": {
98
+ "type": "string",
99
+ "maxLength": 255
100
+ },
101
+ "hosts": {
102
+ "type": "array",
103
+ "minItems": 1,
104
+ "maxItems": 8,
105
+ "uniqueItems": true,
106
+ "items": {
107
+ "$ref": "#/$defs/host"
108
+ },
109
+ "description": "Declarative plug-ins, which must name at least one: every host their requests, paging and links may reach. A container plug-in names none."
110
+ },
111
+ "auth": {
112
+ "$ref": "#/$defs/vendorAuth",
113
+ "description": "Declarative plug-ins only."
114
+ },
115
+ "vendor": {
116
+ "$ref": "#/$defs/vendor"
117
+ },
118
+ "connections": {
119
+ "type": "array",
120
+ "maxItems": 20,
121
+ "items": {
122
+ "$ref": "#/$defs/connection"
123
+ }
124
+ },
125
+ "webhooks": {
126
+ "$ref": "#/$defs/webhooks"
127
+ },
128
+ "schedules": {
129
+ "type": "array",
130
+ "maxItems": 8,
131
+ "items": {
132
+ "$ref": "#/$defs/schedule"
133
+ },
134
+ "description": "What Initiative calls the plug-in's schedule hook for, and how often. At most 8."
135
+ },
136
+ "endpoints": {
137
+ "type": "array",
138
+ "maxItems": 64,
139
+ "items": {
140
+ "$ref": "#/$defs/endpoint"
141
+ }
142
+ },
143
+ "community_summary": {
144
+ "$ref": "#/$defs/namespacedId",
145
+ "description": "A read endpoint whose declared returns describe this community's standing with your service — what it has used, what it is allowed. A deployment may render them on the community's own settings page, beside its own figures. Whether it does is the deployment's decision and not this manifest's: a plug-in the operator did not ship is declaring where it would like to appear, which is not the same as appearing."
146
+ },
147
+ "widgets": {
148
+ "type": "array",
149
+ "maxItems": 12,
150
+ "items": {
151
+ "$ref": "#/$defs/widget"
152
+ }
153
+ },
154
+ "embeds": {
155
+ "type": "array",
156
+ "maxItems": 12,
157
+ "items": {
158
+ "$ref": "#/$defs/embed"
159
+ }
160
+ },
161
+ "dashboards": {
162
+ "type": "array",
163
+ "maxItems": 8,
164
+ "items": {
165
+ "$ref": "#/$defs/bundledDashboard"
166
+ },
167
+ "description": "Ready-made arrangements of this plug-in's own widgets. Publishing the plug-in publishes one ordinary dashboard listing per entry, offered to communities that install the plug-in."
168
+ }
169
+ },
170
+ "$defs": {
171
+ "identifier": {
172
+ "type": "string",
173
+ "minLength": 1,
174
+ "maxLength": 64,
175
+ "pattern": "^[\\-0123456789_abcdefghijklmnopqrstuvwxyz]+$"
176
+ },
177
+ "path": {
178
+ "type": "string",
179
+ "minLength": 1,
180
+ "maxLength": 200,
181
+ "pattern": "^(?!.*(?://|\\.\\.))/[\\-./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz]*$",
182
+ "description": "A route on the plug-in's own service, never an address. The deployment joins it to the base URL its registration supplies."
183
+ },
184
+ "localizedText": {
185
+ "type": "object",
186
+ "description": "Localized text, keyed by language tag. At least one usable entry; the platform falls back to the reader's language, then to any entry. Values should be strings: text longer than 120 characters is truncated, and an entry that is not a string, is not a language tag, or falls past the first 40 is ignored rather than refused.",
187
+ "minProperties": 1
188
+ },
189
+ "requires": {
190
+ "type": "object",
191
+ "description": "Connection ids that must hold a value before this is offered. Exactly one of 'all_of' or 'any_of'; each id must name a connection this manifest declares. Absent means always available.",
192
+ "properties": {
193
+ "all_of": {
194
+ "type": "array",
195
+ "minItems": 1,
196
+ "maxItems": 10,
197
+ "items": {
198
+ "$ref": "#/$defs/identifier"
199
+ }
200
+ },
201
+ "any_of": {
202
+ "type": "array",
203
+ "minItems": 1,
204
+ "maxItems": 10,
205
+ "items": {
206
+ "$ref": "#/$defs/identifier"
207
+ }
208
+ }
209
+ },
210
+ "oneOf": [
211
+ {
212
+ "required": [
213
+ "all_of"
214
+ ]
215
+ },
216
+ {
217
+ "required": [
218
+ "any_of"
219
+ ]
220
+ }
221
+ ]
222
+ },
223
+ "accessHint": {
224
+ "type": "object",
225
+ "description": "What the credential will be used for. Display-only: it is shown beside the form so an admin can mint a minimal credential, and no other system's permissions are enforced from it.",
226
+ "properties": {
227
+ "api": {
228
+ "type": "string",
229
+ "maxLength": 120
230
+ },
231
+ "scopes": {
232
+ "type": "array",
233
+ "maxItems": 24,
234
+ "items": {
235
+ "type": "string",
236
+ "maxLength": 120
237
+ }
238
+ }
239
+ }
240
+ },
241
+ "connectionField": {
242
+ "type": "object",
243
+ "required": [
244
+ "key",
245
+ "type",
246
+ "label"
247
+ ],
248
+ "properties": {
249
+ "key": {
250
+ "type": "string",
251
+ "$ref": "#/$defs/identifier"
252
+ },
253
+ "type": {
254
+ "enum": [
255
+ "bool",
256
+ "int",
257
+ "secret",
258
+ "select",
259
+ "string",
260
+ "url"
261
+ ]
262
+ },
263
+ "required": {
264
+ "type": "boolean",
265
+ "default": false
266
+ },
267
+ "label": {
268
+ "$ref": "#/$defs/localizedText"
269
+ },
270
+ "options": {
271
+ "type": "array",
272
+ "description": "Required when type is 'select'.",
273
+ "maxItems": 24,
274
+ "minItems": 1,
275
+ "items": {
276
+ "type": "string",
277
+ "maxLength": 120
278
+ }
279
+ },
280
+ "managed": {
281
+ "type": "boolean",
282
+ "default": false,
283
+ "description": "Returned by the plug-in's after_connect hook when a flow finishes; it is not typed into the settings form."
284
+ }
285
+ },
286
+ "if": {
287
+ "properties": {
288
+ "type": {
289
+ "const": "select"
290
+ }
291
+ },
292
+ "required": [
293
+ "type"
294
+ ]
295
+ },
296
+ "then": {
297
+ "required": [
298
+ "options"
299
+ ]
300
+ }
301
+ },
302
+ "vendor": {
303
+ "type": "object",
304
+ "description": "What an operator supplies once per deployment for the vendor's own client: its id, its secret, its signing key. Declared here and never valued here: the values are entered on the deployment, or written there by the vendor's own setup flow, and referenced from a connection's flow or token as '{vendor.<key>}'.",
305
+ "required": [
306
+ "fields"
307
+ ],
308
+ "properties": {
309
+ "label": {
310
+ "$ref": "#/$defs/localizedText",
311
+ "description": "What the vendor client is called, for the operator's form."
312
+ },
313
+ "fields": {
314
+ "type": "array",
315
+ "minItems": 1,
316
+ "maxItems": 12,
317
+ "items": {
318
+ "$ref": "#/$defs/vendorField"
319
+ }
320
+ },
321
+ "setup": {
322
+ "$ref": "#/$defs/vendorSetup",
323
+ "description": "How the vendor's client can be made for a deployment by the vendor itself, rather than registered by hand. Absent: the operator enters every value."
324
+ }
325
+ }
326
+ },
327
+ "vendorField": {
328
+ "type": "object",
329
+ "required": [
330
+ "key",
331
+ "type",
332
+ "label"
333
+ ],
334
+ "properties": {
335
+ "key": {
336
+ "type": "string",
337
+ "$ref": "#/$defs/identifier"
338
+ },
339
+ "type": {
340
+ "enum": [
341
+ "secret",
342
+ "string",
343
+ "url"
344
+ ],
345
+ "description": "A 'secret' is written once and never shown again."
346
+ },
347
+ "required": {
348
+ "type": "boolean",
349
+ "default": false,
350
+ "description": "The plug-in is not live on a deployment until every required value is set."
351
+ },
352
+ "label": {
353
+ "$ref": "#/$defs/localizedText"
354
+ }
355
+ }
356
+ },
357
+ "vendorSetup": {
358
+ "description": "A flow at the vendor that creates the vendor's client and answers with its values. Initiative runs it from the operator's browser and writes what the vendor answers into the deployment's vendor values, so none is typed or copied. One member per flow, told apart by 'kind'.",
359
+ "oneOf": [
360
+ {
361
+ "$ref": "#/$defs/githubAppManifestSetup"
362
+ }
363
+ ]
364
+ },
365
+ "githubAppManifestSetup": {
366
+ "type": "object",
367
+ "description": "A GitHub App created from a manifest (GitHub's app manifest flow). The operator confirms it on GitHub, and Initiative exchanges the code GitHub returns for the new app's values. Initiative fills in every address itself (the callback, setup, webhook and redirect URLs), so the manifest names none.",
368
+ "required": [
369
+ "kind",
370
+ "app",
371
+ "values"
372
+ ],
373
+ "properties": {
374
+ "kind": {
375
+ "const": "github_app_manifest"
376
+ },
377
+ "app": {
378
+ "$ref": "#/$defs/githubAppManifest"
379
+ },
380
+ "values": {
381
+ "type": "object",
382
+ "description": "Which vendor value each of GitHub's answers is written to: one of this plug-in's vendor field keys to one field of GitHub's manifest-conversion response. Every key must be a field of the vendor block, each response field is written at most once, and 'client_secret', 'pem' and 'webhook_secret' are written only to a 'secret' field.",
383
+ "minProperties": 1,
384
+ "maxProperties": 6,
385
+ "propertyNames": {
386
+ "$ref": "#/$defs/identifier"
387
+ },
388
+ "additionalProperties": {
389
+ "enum": [
390
+ "client_id",
391
+ "client_secret",
392
+ "id",
393
+ "pem",
394
+ "slug",
395
+ "webhook_secret"
396
+ ]
397
+ }
398
+ }
399
+ }
400
+ },
401
+ "githubAppManifest": {
402
+ "type": "object",
403
+ "description": "What GitHub is asked to create: the parts of a GitHub App manifest a plug-in decides.",
404
+ "required": [
405
+ "name",
406
+ "url"
407
+ ],
408
+ "properties": {
409
+ "name": {
410
+ "type": "string",
411
+ "minLength": 1,
412
+ "maxLength": 120,
413
+ "description": "The name GitHub offers for the new app. The operator may change it on GitHub, where an app's name is unique."
414
+ },
415
+ "url": {
416
+ "type": "string",
417
+ "maxLength": 300,
418
+ "pattern": "^https://\\S+$",
419
+ "description": "The app's homepage, https."
420
+ },
421
+ "public": {
422
+ "type": "boolean",
423
+ "default": false,
424
+ "description": "Any GitHub account may install the new app. Absent: only the account that owns it."
425
+ },
426
+ "default_permissions": {
427
+ "type": "object",
428
+ "maxProperties": 100,
429
+ "propertyNames": {
430
+ "$ref": "#/$defs/identifier"
431
+ },
432
+ "additionalProperties": {
433
+ "enum": [
434
+ "read",
435
+ "write"
436
+ ]
437
+ },
438
+ "description": "The permissions the app is created with, by GitHub's name for each, such as 'issues'. At most 100."
439
+ },
440
+ "default_events": {
441
+ "type": "array",
442
+ "maxItems": 100,
443
+ "uniqueItems": true,
444
+ "items": {
445
+ "$ref": "#/$defs/identifier"
446
+ },
447
+ "description": "The webhook events the app subscribes to, by GitHub's name for each, such as 'issues'. GitHub accepts an event only beside a permission that covers it. At most 100."
448
+ }
449
+ }
450
+ },
451
+ "endpointParam": {
452
+ "type": "object",
453
+ "required": [
454
+ "key",
455
+ "type",
456
+ "label"
457
+ ],
458
+ "properties": {
459
+ "key": {
460
+ "type": "string",
461
+ "$ref": "#/$defs/identifier"
462
+ },
463
+ "type": {
464
+ "enum": [
465
+ "bool",
466
+ "datetime",
467
+ "int",
468
+ "select",
469
+ "string",
470
+ "url"
471
+ ]
472
+ },
473
+ "required": {
474
+ "type": "boolean",
475
+ "default": false
476
+ },
477
+ "label": {
478
+ "$ref": "#/$defs/localizedText"
479
+ },
480
+ "options": {
481
+ "type": "array",
482
+ "description": "Required when type is 'select'. The values themselves — a consumer that shows a menu writes its own words for them.",
483
+ "maxItems": 24,
484
+ "minItems": 1,
485
+ "items": {
486
+ "type": "string",
487
+ "maxLength": 120
488
+ }
489
+ },
490
+ "options_from": {
491
+ "type": "object",
492
+ "description": "Where this parameter's values come from when only the plug-in can know them. Names a read endpoint in THIS manifest and which of its returns holds the values; a consumer building a form asks the deployment to resolve it rather than showing a text box. For everything a manifest cannot list because the answer differs per install and changes after it — a repository, a channel, a board, a project. `options` is the other case: a set that is the same on every deployment forever. Declaring values, not a control: what to draw is still the consumer's. Resolution is server-side, inherits the source endpoint's `requires` and `cache_ttl_seconds`, and a source that cannot be resolved leaves the parameter enterable rather than unusable.",
493
+ "properties": {
494
+ "endpoint": {
495
+ "$ref": "#/$defs/namespacedId",
496
+ "description": "A read endpoint this same manifest declares. Not another plug-in's: reading across plug-ins has no consent story, and an id from elsewhere is refused on publish."
497
+ },
498
+ "key": {
499
+ "$ref": "#/$defs/identifier",
500
+ "description": "Which of that endpoint's returns holds the values. Must be one it declares, and a list: a menu comes from a column of values, not from one."
501
+ },
502
+ "label_key": {
503
+ "$ref": "#/$defs/identifier",
504
+ "description": "Optional. A second return, parallel to `key`, holding what a person reads — a board's title beside its opaque id. Absent means the value is its own label."
505
+ },
506
+ "needs": {
507
+ "type": "object",
508
+ "description": "Optional. What to send that endpoint, written as one of ITS parameter names to one of THIS endpoint's. A repository's labels, a board's fields, a field's values — past the first source in a form, most of them answer differently depending on what has been chosen already, and a source that could not be told a sibling's answer could only ever offer the whole account's worth. Every named sibling must be a parameter this endpoint declares, and none of them this one. Until all of them have a value the source is not called and the parameter stays enterable, exactly as for a source that will not resolve.",
509
+ "propertyNames": {
510
+ "$ref": "#/$defs/identifier"
511
+ },
512
+ "additionalProperties": {
513
+ "$ref": "#/$defs/identifier"
514
+ },
515
+ "maxProperties": 12
516
+ }
517
+ },
518
+ "required": [
519
+ "endpoint",
520
+ "key"
521
+ ]
522
+ },
523
+ "list": {
524
+ "type": "boolean",
525
+ "default": false,
526
+ "description": "Several values rather than one. Cardinality is a fact about the value, so it is yours; what to draw for it is the consumer's. Without it, a plug-in wanting several of something declares a string and documents a comma — which nothing downstream can validate or complete."
527
+ }
528
+ },
529
+ "if": {
530
+ "properties": {
531
+ "type": {
532
+ "const": "select"
533
+ }
534
+ },
535
+ "required": [
536
+ "type"
537
+ ]
538
+ },
539
+ "then": {
540
+ "required": [
541
+ "options"
542
+ ]
543
+ }
544
+ },
545
+ "endpointReturn": {
546
+ "type": "object",
547
+ "required": [
548
+ "key",
549
+ "type"
550
+ ],
551
+ "properties": {
552
+ "key": {
553
+ "$ref": "#/$defs/identifier"
554
+ },
555
+ "type": {
556
+ "enum": [
557
+ "bool",
558
+ "datetime",
559
+ "int",
560
+ "string",
561
+ "url"
562
+ ],
563
+ "description": "The parameter vocabulary minus 'select', because a select is a control and the value behind one is a string."
564
+ },
565
+ "label": {
566
+ "$ref": "#/$defs/localizedText"
567
+ },
568
+ "list": {
569
+ "type": "boolean",
570
+ "description": "Several values rather than one. It matters to a consumer with somewhere to put exactly one — a form field, a tile's number — which is why it is a flag rather than a second set of types."
571
+ }
572
+ }
573
+ },
574
+ "connection": {
575
+ "type": "object",
576
+ "required": [
577
+ "id",
578
+ "scope",
579
+ "label",
580
+ "fields"
581
+ ],
582
+ "properties": {
583
+ "id": {
584
+ "$ref": "#/$defs/identifier"
585
+ },
586
+ "scope": {
587
+ "enum": [
588
+ "interactive",
589
+ "static"
590
+ ],
591
+ "description": "Who the credential belongs to. 'static' is one credential the whole community uses; 'interactive' is each member's own account at a vendor that authorizes people. It does not say how the credential is obtained: a static connection with a 'flow' is still one credential for the whole community, run by an admin through the vendor's own pages rather than typed into a form. An interactive connection always declares a 'flow'."
592
+ },
593
+ "label": {
594
+ "$ref": "#/$defs/localizedText"
595
+ },
596
+ "fields": {
597
+ "type": "array",
598
+ "maxItems": 12,
599
+ "items": {
600
+ "$ref": "#/$defs/connectionField"
601
+ },
602
+ "description": "Without a 'flow', what an admin types. With one, only the values the plug-in's after_connect hook returns, and every field is 'managed'. The tokens a flow obtains are held apart from these, under reserved keys the plug-in never declares."
603
+ },
604
+ "flow": {
605
+ "$ref": "#/$defs/connectionFlow",
606
+ "description": "How the connection is established. Initiative runs it: the redirect, the code exchange, refreshing and revoking. The plug-in never holds the vendor client's secret or a refresh token."
607
+ },
608
+ "token": {
609
+ "$ref": "#/$defs/connectionToken",
610
+ "description": "How a usable access token is got for this connection. Absent: the tokens the flow stored, refreshed as needed. Static connections only."
611
+ },
612
+ "access_hint": {
613
+ "$ref": "#/$defs/accessHint"
614
+ },
615
+ "health": {
616
+ "$ref": "#/$defs/connectionHealth"
617
+ }
618
+ }
619
+ },
620
+ "connectionFlow": {
621
+ "type": "object",
622
+ "description": "An OAuth 2.0 authorization code flow (RFC 6749 §4.1) that Initiative runs for a connection, with the redirect addresses the deployment publishes.",
623
+ "required": [
624
+ "type",
625
+ "authorize_url",
626
+ "token_url",
627
+ "client_id"
628
+ ],
629
+ "properties": {
630
+ "type": {
631
+ "enum": [
632
+ "oauth2"
633
+ ]
634
+ },
635
+ "authorize_url": {
636
+ "type": "string",
637
+ "minLength": 1,
638
+ "maxLength": 300,
639
+ "description": "The vendor's authorization endpoint, https. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
640
+ },
641
+ "token_url": {
642
+ "type": "string",
643
+ "minLength": 1,
644
+ "maxLength": 300,
645
+ "description": "The vendor's token endpoint, https. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
646
+ },
647
+ "client_id": {
648
+ "type": "string",
649
+ "minLength": 1,
650
+ "maxLength": 300,
651
+ "description": "The vendor client's id, normally '{vendor.client_id}'. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
652
+ },
653
+ "client_secret": {
654
+ "type": "string",
655
+ "maxLength": 300,
656
+ "description": "The vendor client's secret, normally '{vendor.client_secret}'. Absent for a public client. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
657
+ },
658
+ "scopes": {
659
+ "type": "array",
660
+ "maxItems": 24,
661
+ "items": {
662
+ "type": "string",
663
+ "minLength": 1,
664
+ "maxLength": 120
665
+ },
666
+ "description": "The vendor scopes asked for, sent space-separated."
667
+ },
668
+ "pkce": {
669
+ "type": "boolean",
670
+ "default": true,
671
+ "description": "Send an S256 code challenge (RFC 7636)."
672
+ },
673
+ "authorize_params": {
674
+ "type": "object",
675
+ "maxProperties": 12,
676
+ "propertyNames": {
677
+ "$ref": "#/$defs/identifier"
678
+ },
679
+ "additionalProperties": {
680
+ "type": "string",
681
+ "maxLength": 2000
682
+ },
683
+ "description": "Extra query parameters for the authorization request. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
684
+ },
685
+ "install_url": {
686
+ "type": "string",
687
+ "maxLength": 300,
688
+ "description": "Static connections only. The vendor's install page, for a connection an organization installs: the person installs first, the vendor returns to the setup address with the installation's id, and one authorization trip follows so the plug-in can check who installed it. Requires 'after_connect'. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
689
+ },
690
+ "after_connect": {
691
+ "anyOf": [
692
+ {
693
+ "type": "boolean"
694
+ },
695
+ {
696
+ "$ref": "#/$defs/afterConnect"
697
+ }
698
+ ],
699
+ "default": false,
700
+ "description": "What runs once the code is exchanged, with the fresh access token, to learn the connection's managed values and an account label, or to refuse it. A container plug-in sets true, and Initiative calls its after_connect hook; a declarative plug-in gives the call and its mapping."
701
+ },
702
+ "revoke": {
703
+ "enum": [
704
+ "github_grant",
705
+ "hook",
706
+ "rfc7009"
707
+ ],
708
+ "description": "How a grant is ended at the vendor when the connection ends: 'rfc7009' posts to 'revoke_url' with the client's credentials; 'github_grant' sends DELETE to 'revoke_url' (GitHub's 'Delete an app authorization') with the client's credentials as HTTP Basic auth and the access token in the JSON body as 'access_token'; 'hook' calls the plug-in's revoke hook with the tokens, so a container plug-in's only. Absent: the tokens are deleted and nothing is sent."
709
+ },
710
+ "revoke_url": {
711
+ "type": "string",
712
+ "maxLength": 300,
713
+ "description": "The vendor's revocation endpoint: RFC 7009's for 'rfc7009', or GitHub's grant address (https://api.github.com/applications/{vendor.client_id}/grant) for 'github_grant'. Required when 'revoke' is either. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
714
+ }
715
+ }
716
+ },
717
+ "connectionToken": {
718
+ "type": "object",
719
+ "description": "An access token minted on demand rather than stored: Initiative signs a JWT with the vendor key and exchanges it, and caches the answer until shortly before it expires.",
720
+ "required": [
721
+ "type",
722
+ "exchange_url",
723
+ "iss",
724
+ "key"
725
+ ],
726
+ "properties": {
727
+ "type": {
728
+ "enum": [
729
+ "jwt_bearer"
730
+ ]
731
+ },
732
+ "exchange_url": {
733
+ "type": "string",
734
+ "minLength": 1,
735
+ "maxLength": 300,
736
+ "description": "Where the signed JWT is posted, https. The answer's 'token' (or 'access_token') and 'expires_at' (or 'expires_in') are read. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
737
+ },
738
+ "iss": {
739
+ "type": "string",
740
+ "minLength": 1,
741
+ "maxLength": 300,
742
+ "description": "The JWT's issuer, normally '{vendor.app_id}'. May name a vendor value as '{vendor.<key>}' and one of this connection's own fields as '{<key>}'."
743
+ },
744
+ "key": {
745
+ "type": "string",
746
+ "minLength": 1,
747
+ "maxLength": 300,
748
+ "description": "The signing key, as a vendor value: '{vendor.private_key}'. A PEM private key."
749
+ },
750
+ "alg": {
751
+ "enum": [
752
+ "ES256",
753
+ "RS256"
754
+ ],
755
+ "default": "RS256"
756
+ },
757
+ "lifetime": {
758
+ "type": "integer",
759
+ "minimum": 1,
760
+ "maximum": 600,
761
+ "default": 540,
762
+ "description": "How long the signed JWT lives, in seconds."
763
+ }
764
+ }
765
+ },
766
+ "webhooks": {
767
+ "type": "object",
768
+ "description": "The vendor's webhooks, received by Initiative at one address per plug-in on the deployment, '/api/v1/plugin-hooks/<public_id>'. Initiative checks each delivery's signature, drops one it has already delivered, finds the communities it belongs to by a value in its body, and forwards it to the plug-in's webhook hook once for each. A container plug-in's hook receives each delivery; a declarative plug-in maps it with 'events' and 'status' instead.",
769
+ "required": [
770
+ "verify",
771
+ "dedup",
772
+ "route"
773
+ ],
774
+ "properties": {
775
+ "verify": {
776
+ "$ref": "#/$defs/webhookVerify"
777
+ },
778
+ "dedup": {
779
+ "type": "string",
780
+ "minLength": 1,
781
+ "maxLength": 64,
782
+ "pattern": "^[\\-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]+$",
783
+ "description": "The header carrying the vendor's id for one delivery, such as 'X-GitHub-Delivery'. A delivery with an id already forwarded to a community is not forwarded to it again."
784
+ },
785
+ "route": {
786
+ "$ref": "#/$defs/webhookRoute"
787
+ },
788
+ "events": {
789
+ "type": "array",
790
+ "minItems": 1,
791
+ "maxItems": 32,
792
+ "items": {
793
+ "$ref": "#/$defs/webhookEvent"
794
+ },
795
+ "description": "Declarative plug-ins: how a delivery becomes one of this plug-in's events. The first row whose 'when' holds emits; a delivery no row matches is dropped."
796
+ },
797
+ "status": {
798
+ "type": "array",
799
+ "minItems": 1,
800
+ "maxItems": 12,
801
+ "items": {
802
+ "$ref": "#/$defs/webhookStatus"
803
+ },
804
+ "description": "Declarative plug-ins: deliveries that say what state a connection is in at the vendor. The first row whose 'when' holds sets it."
805
+ }
806
+ }
807
+ },
808
+ "webhookVerify": {
809
+ "type": "object",
810
+ "description": "How a delivery's signature is checked: an HMAC over the raw body under a vendor value.",
811
+ "required": [
812
+ "scheme",
813
+ "header",
814
+ "encoding",
815
+ "secret"
816
+ ],
817
+ "properties": {
818
+ "scheme": {
819
+ "enum": [
820
+ "hmac_sha1",
821
+ "hmac_sha256"
822
+ ]
823
+ },
824
+ "header": {
825
+ "type": "string",
826
+ "minLength": 1,
827
+ "maxLength": 64,
828
+ "pattern": "^[\\-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]+$",
829
+ "description": "The header carrying the signature, such as 'X-Hub-Signature-256'."
830
+ },
831
+ "prefix": {
832
+ "type": "string",
833
+ "maxLength": 64,
834
+ "description": "What precedes the signature in the header, such as 'sha256='. Absent: nothing does."
835
+ },
836
+ "encoding": {
837
+ "enum": [
838
+ "base64",
839
+ "hex"
840
+ ],
841
+ "description": "How the signature is written."
842
+ },
843
+ "secret": {
844
+ "type": "string",
845
+ "minLength": 1,
846
+ "maxLength": 300,
847
+ "description": "The signing secret, as one vendor value: '{vendor.<key>}'."
848
+ }
849
+ }
850
+ },
851
+ "webhookRoute": {
852
+ "type": "object",
853
+ "description": "Which communities a delivery belongs to: a value in its body or in one of its headers, matched against one field of a static connection as each community's connect stored it. Exactly one of 'path' and 'header'.",
854
+ "required": [
855
+ "connection",
856
+ "field"
857
+ ],
858
+ "properties": {
859
+ "path": {
860
+ "type": "string",
861
+ "minLength": 1,
862
+ "maxLength": 200,
863
+ "pattern": "^[\\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz]+$",
864
+ "description": "Where the value is in the JSON body, as keys joined by '.', such as 'installation.id'."
865
+ },
866
+ "header": {
867
+ "type": "string",
868
+ "minLength": 1,
869
+ "maxLength": 64,
870
+ "pattern": "^[\\-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]+$",
871
+ "description": "The header carrying the value, such as 'X-Shopify-Shop-Domain'."
872
+ },
873
+ "connection": {
874
+ "$ref": "#/$defs/identifier",
875
+ "description": "A static connection this plug-in declares."
876
+ },
877
+ "field": {
878
+ "$ref": "#/$defs/identifier",
879
+ "description": "A field that connection declares, whose stored value the body's value is matched against."
880
+ }
881
+ }
882
+ },
883
+ "schedule": {
884
+ "type": "object",
885
+ "description": "An interval at which Initiative calls the plug-in's schedule hook, once for each community that installed it, with when the call last succeeded there.",
886
+ "required": [
887
+ "id",
888
+ "every"
889
+ ],
890
+ "properties": {
891
+ "id": {
892
+ "$ref": "#/$defs/identifier",
893
+ "description": "Unique within the manifest. The hook is told which schedule is due by it."
894
+ },
895
+ "every": {
896
+ "type": "string",
897
+ "maxLength": 5,
898
+ "pattern": "^[0-9]+[mh]$",
899
+ "description": "How often: a whole number of minutes ('15m') or hours ('6h'), at least 5 minutes and at most 1440 minutes."
900
+ }
901
+ }
902
+ },
903
+ "namespacedId": {
904
+ "type": "string",
905
+ "maxLength": 200,
906
+ "pattern": "^[\\-.0123456789_abcdefghijklmnopqrstuvwxyz]+$",
907
+ "description": "Namespaced under the plug-in's own service id — 'plugin.<public_id>.<name>'. The prefix is checked against the declaring registration at ingress."
908
+ },
909
+ "pluginScope": {
910
+ "type": "string",
911
+ "pattern": "^plugins:[\\-.0123456789_abcdefghijklmnopqrstuvwxyz]*\\.[\\-.0123456789_abcdefghijklmnopqrstuvwxyz]*$",
912
+ "description": "'plugins:<public_id>': permission to call another plug-in's public endpoints through the deployment, as this community or as one of its members. Not one of the fixed scopes: the family is open, one per plug-in, and the public id after the prefix is the plug-in being called. The deployment never lets a plug-in address another directly; the call goes through it, and the plug-in called is handed its own references for whoever the call is for."
913
+ },
914
+ "endpoint": {
915
+ "type": "object",
916
+ "required": [
917
+ "id",
918
+ "direction"
919
+ ],
920
+ "properties": {
921
+ "id": {
922
+ "$ref": "#/$defs/namespacedId"
923
+ },
924
+ "label": {
925
+ "$ref": "#/$defs/localizedText",
926
+ "description": "What this endpoint IS, in words somebody picks it out of a list by. Every direction, and an emission most of all: it is the one thing here chosen without ever being called."
927
+ },
928
+ "description": {
929
+ "$ref": "#/$defs/localizedText",
930
+ "description": "A second line, where the label needs one."
931
+ },
932
+ "returns": {
933
+ "type": "array",
934
+ "maxItems": 24,
935
+ "items": {
936
+ "$ref": "#/$defs/endpointReturn"
937
+ },
938
+ "description": "What this hands back, by name and type — the response for a read or a write, the payload for an emission. Declared rather than discovered, because a consumer binds one of these before the endpoint has ever run and a bad binding has to be refusable when somebody arranges it."
939
+ },
940
+ "group": {
941
+ "$ref": "#/$defs/identifier",
942
+ "description": "Where a consumer that groups a plug-in's endpoints should file this one. Opaque here: the grouping is the consumer's, and an endpoint that says nothing sits in the flat list."
943
+ },
944
+ "needs_subject": {
945
+ "$ref": "#/$defs/identifier",
946
+ "description": "What a caller must already have in hand for this to mean anything. Opaque here — the vocabulary belongs to whoever consumes it; the automation service names the subjects a run can be about."
947
+ },
948
+ "direction": {
949
+ "enum": [
950
+ "emit",
951
+ "read",
952
+ "write"
953
+ ],
954
+ "description": "'read' and 'write' are called through the deployment and answer in place; 'emit' travels the other way — the plug-in posts it to a subscriber that registered a URL, so it carries no parameters and nothing to gate."
955
+ },
956
+ "params": {
957
+ "type": "array",
958
+ "maxItems": 12,
959
+ "items": {
960
+ "$ref": "#/$defs/endpointParam"
961
+ },
962
+ "description": "What a caller may send. Read and write only."
963
+ },
964
+ "actors": {
965
+ "type": "array",
966
+ "minItems": 1,
967
+ "maxItems": 2,
968
+ "items": {
969
+ "enum": [
970
+ "installation",
971
+ "member"
972
+ ]
973
+ },
974
+ "description": "Whose credential the call runs on, best first. Read and write only. An endpoint offering only 'member' refuses when that member has connected nothing, rather than quietly acting as the plug-in instead."
975
+ },
976
+ "admin_only": {
977
+ "type": "boolean",
978
+ "default": false,
979
+ "description": "Only the community's admins read or call this endpoint — a figure for the community's settings page, say. Anyone who can see where it is used may otherwise."
980
+ },
981
+ "public": {
982
+ "type": "boolean",
983
+ "default": false,
984
+ "description": "Read and write only. Other plug-ins may call this endpoint through the deployment, when the community has let them use this plug-in ('plugins:<your public id>'). A caller acts as the community or as one of its members, and `actors` says which of the two this endpoint takes: one that names neither is not callable this way. A write endpoint is reachable only like this; a widget binds reads alone. Absent means only the deployment's own surfaces reach it."
985
+ },
986
+ "requires": {
987
+ "$ref": "#/$defs/requires"
988
+ },
989
+ "cache_ttl_seconds": {
990
+ "type": "integer",
991
+ "default": 0,
992
+ "description": "Read only. How long a response may be reused. Clamped into 0..86400 rather than refused, so a value outside that range is accepted and takes effect at the bound — which is why no range is asserted here."
993
+ },
994
+ "identity": {
995
+ "$ref": "#/$defs/endpointIdentity",
996
+ "description": "'write' and 'emit': what this touched, or what it is about. Read endpoints have none — they touched nothing, so there is no echo to suppress."
997
+ },
998
+ "unavailable": {
999
+ "type": "array",
1000
+ "maxItems": 12,
1001
+ "uniqueItems": true,
1002
+ "items": {
1003
+ "$ref": "#/$defs/identifier"
1004
+ },
1005
+ "description": "Read and write only. The codes this endpoint may answer 'unavailable' with beyond Initiative's own (invalid, mapping-failed, not-authorized, not-found, range-too-large), so a consumer can put each into words."
1006
+ },
1007
+ "request": {
1008
+ "$ref": "#/$defs/vendorRequest",
1009
+ "description": "Declarative plug-ins: the one call that answers this endpoint. Not beside 'steps'."
1010
+ },
1011
+ "steps": {
1012
+ "type": "array",
1013
+ "minItems": 1,
1014
+ "maxItems": 3,
1015
+ "items": {
1016
+ "$ref": "#/$defs/requestStep"
1017
+ },
1018
+ "description": "Declarative plug-ins: up to 3 calls made in order, each able to read the answers of the ones before it. Not beside 'request'."
1019
+ },
1020
+ "map": {
1021
+ "$ref": "#/$defs/expression",
1022
+ "description": "Declarative plug-ins: the endpoint's answer, from 'response' (the last call's answer) and 'steps': an object of its declared returns, or {\"unavailable\": \"<code>\"} naming one of its codes."
1023
+ },
1024
+ "errors": {
1025
+ "type": "array",
1026
+ "maxItems": 12,
1027
+ "items": {
1028
+ "$ref": "#/$defs/errorRule"
1029
+ },
1030
+ "description": "Declarative plug-ins: vendor answers this endpoint gives its own meaning, tried on every answer before the defaults. The defaults: 401 and 403 are not-authorized, 404 not-found, 400, 422 and any other 4xx invalid, and 429, 3xx and 5xx transient."
1031
+ }
1032
+ },
1033
+ "additionalProperties": false
1034
+ },
1035
+ "widget": {
1036
+ "type": "object",
1037
+ "required": [
1038
+ "id",
1039
+ "meta",
1040
+ "module_source"
1041
+ ],
1042
+ "properties": {
1043
+ "id": {
1044
+ "$ref": "#/$defs/identifier"
1045
+ },
1046
+ "meta": {
1047
+ "type": "object",
1048
+ "description": "The widget's own name and description, as the picker shows them. Must name the widget in at least one language."
1049
+ },
1050
+ "module_source": {
1051
+ "type": "string",
1052
+ "minLength": 1,
1053
+ "description": "The widget's browser-side module. Stored as an opaque string and executed only inside the sandbox; the platform never parses it. Capped in UTF-8 bytes, which this schema cannot express."
1054
+ },
1055
+ "endpoints": {
1056
+ "type": "array",
1057
+ "maxItems": 8,
1058
+ "items": {
1059
+ "$ref": "#/$defs/namespacedId"
1060
+ },
1061
+ "description": "Read endpoint ids this manifest also declares. Only a read answers with something to draw."
1062
+ },
1063
+ "sample_data": {
1064
+ "type": "object",
1065
+ "description": "What each endpoint would answer with, keyed by declared read endpoint id, so a preview renders with no network call. Written in the endpoint's own returns and read through them exactly as a live answer is. Keys naming an undeclared endpoint are dropped."
1066
+ },
1067
+ "requires": {
1068
+ "$ref": "#/$defs/requires"
1069
+ }
1070
+ }
1071
+ },
1072
+ "embed": {
1073
+ "type": "object",
1074
+ "required": [
1075
+ "id",
1076
+ "path",
1077
+ "name"
1078
+ ],
1079
+ "properties": {
1080
+ "id": {
1081
+ "$ref": "#/$defs/identifier"
1082
+ },
1083
+ "path": {
1084
+ "$ref": "#/$defs/path"
1085
+ },
1086
+ "name": {
1087
+ "$ref": "#/$defs/localizedText"
1088
+ },
1089
+ "scopes": {
1090
+ "type": "array",
1091
+ "maxItems": 2,
1092
+ "items": {
1093
+ "enum": [
1094
+ "community",
1095
+ "initiative"
1096
+ ]
1097
+ },
1098
+ "default": [
1099
+ "community"
1100
+ ],
1101
+ "description": "Where the surface renders. Declaring both gives it a community-wide entry and an entry inside each initiative."
1102
+ },
1103
+ "admin_only": {
1104
+ "type": "boolean",
1105
+ "default": false,
1106
+ "description": "Only the community's admins open this surface, whatever roles a placement allows — a settings page, say. Who else may open a surface is chosen in the community, per initiative and per role, not declared here."
1107
+ },
1108
+ "capabilities": {
1109
+ "type": "array",
1110
+ "maxItems": 8,
1111
+ "items": {
1112
+ "enum": [
1113
+ "camera",
1114
+ "clipboard-read",
1115
+ "clipboard-write",
1116
+ "display-capture",
1117
+ "fullscreen",
1118
+ "geolocation",
1119
+ "microphone"
1120
+ ]
1121
+ },
1122
+ "description": "Browser features the frame is granted. A surface that names nothing is framed with all of them denied."
1123
+ },
1124
+ "requires": {
1125
+ "$ref": "#/$defs/requires"
1126
+ }
1127
+ }
1128
+ },
1129
+ "bundledDashboard": {
1130
+ "type": "object",
1131
+ "required": [
1132
+ "uid",
1133
+ "public_id",
1134
+ "name",
1135
+ "widgets"
1136
+ ],
1137
+ "properties": {
1138
+ "uid": {
1139
+ "type": "string",
1140
+ "minLength": 14,
1141
+ "maxLength": 14,
1142
+ "pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+$",
1143
+ "description": "This dashboard's own catalog id — publisher-assigned, immutable, never reused. It becomes a listing of its own, so this is a real catalog identity and not the plug-in's."
1144
+ },
1145
+ "public_id": {
1146
+ "type": "string",
1147
+ "minLength": 1,
1148
+ "maxLength": 120,
1149
+ "pattern": "^[\\-.0123456789_abcdefghijklmnopqrstuvwxyz]+$",
1150
+ "description": "'<publisher>.<slug>', and not the plug-in's own — a bundled dashboard is a separate listing."
1151
+ },
1152
+ "name": {
1153
+ "type": "string",
1154
+ "minLength": 1,
1155
+ "maxLength": 255
1156
+ },
1157
+ "description": {
1158
+ "type": "string",
1159
+ "maxLength": 500
1160
+ },
1161
+ "layout": {
1162
+ "type": "object",
1163
+ "properties": {
1164
+ "columns": {
1165
+ "type": "integer",
1166
+ "minimum": 1,
1167
+ "maximum": 12
1168
+ }
1169
+ }
1170
+ },
1171
+ "widgets": {
1172
+ "type": "array",
1173
+ "minItems": 1,
1174
+ "maxItems": 50,
1175
+ "items": {
1176
+ "$ref": "#/$defs/bundledDashboardWidget"
1177
+ }
1178
+ }
1179
+ }
1180
+ },
1181
+ "bundledDashboardWidget": {
1182
+ "type": "object",
1183
+ "required": [
1184
+ "type",
1185
+ "binding"
1186
+ ],
1187
+ "properties": {
1188
+ "id": {
1189
+ "$ref": "#/$defs/identifier"
1190
+ },
1191
+ "type": {
1192
+ "$ref": "#/$defs/identifier",
1193
+ "description": "One of this manifest's own widget ids — bare, with no uid. The platform stamps the plug-in's uid on when it publishes, so the two can never disagree."
1194
+ },
1195
+ "title": {
1196
+ "type": "string",
1197
+ "maxLength": 255
1198
+ },
1199
+ "grid": {
1200
+ "type": "object",
1201
+ "properties": {
1202
+ "x": {
1203
+ "type": "integer",
1204
+ "minimum": 0,
1205
+ "maximum": 12
1206
+ },
1207
+ "y": {
1208
+ "type": "integer",
1209
+ "minimum": 0
1210
+ },
1211
+ "w": {
1212
+ "type": "integer",
1213
+ "minimum": 1,
1214
+ "maximum": 12
1215
+ },
1216
+ "h": {
1217
+ "type": "integer",
1218
+ "minimum": 1
1219
+ }
1220
+ }
1221
+ },
1222
+ "binding": {
1223
+ "type": "object",
1224
+ "required": [
1225
+ "endpoint_id"
1226
+ ],
1227
+ "properties": {
1228
+ "endpoint_id": {
1229
+ "$ref": "#/$defs/namespacedId",
1230
+ "description": "One of this manifest's own read endpoint ids. Only a read answers with something to draw."
1231
+ },
1232
+ "params": {
1233
+ "type": "object",
1234
+ "maxProperties": 12,
1235
+ "additionalProperties": {
1236
+ "type": [
1237
+ "string",
1238
+ "integer",
1239
+ "boolean"
1240
+ ],
1241
+ "maxLength": 2000
1242
+ }
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1247
+ },
1248
+ "endpointIdentity": {
1249
+ "type": "object",
1250
+ "description": "Which of this endpoint's returns identify the thing it touched. A consumer keeps a change an automation made from firing that automation again, and for a plug-in there was no key — so guessing would silently drop a fire somebody was waiting on, and a rate cap was the only guard. Declare the SAME kind and key on the write and on the emission about it, and the two produce the same address.",
1251
+ "required": [
1252
+ "kind",
1253
+ "key"
1254
+ ],
1255
+ "properties": {
1256
+ "kind": {
1257
+ "$ref": "#/$defs/identifier",
1258
+ "description": "Your own word for what sort of thing this is ('issue'). Namespaced by your public id downstream, because two plug-ins declaring the same kind mean two different things."
1259
+ },
1260
+ "key": {
1261
+ "type": "array",
1262
+ "description": "Returns of this endpoint, in order, joined to form the address. Every one must be a single value rather than a list — half an address matches nothing, and one built from the parts that happened to be there matches the wrong thing.",
1263
+ "minItems": 1,
1264
+ "maxItems": 4,
1265
+ "items": {
1266
+ "$ref": "#/$defs/identifier"
1267
+ }
1268
+ }
1269
+ },
1270
+ "additionalProperties": false
1271
+ },
1272
+ "expression": {
1273
+ "type": "string",
1274
+ "minLength": 1,
1275
+ "maxLength": 16384,
1276
+ "description": "A JSONata expression (https://jsonata.org): standard JSONata, with no functions added. It reads one document. On the way out that holds 'params', the call's parameters as the caller sent them; 'connection', the non-secret fields of the connection the request names; 'connections', on an endpoint's requests and map, each connection its 'requires' names that the call has, by its id, holding the same fields; 'now', the time of the call in ISO 8601, which $now() and $millis() also answer; and 'steps', each earlier step's answer by its name. On the way back it also holds 'response', the answer being read: {status, headers, body}, with header names in lowercase. Each evaluation is bounded: at most 1000 milliseconds, 500 levels of nesting and an answer of 1048576 bytes as JSON. An expression that fails or passes a bound answers 'unavailable: mapping-failed'."
1277
+ },
1278
+ "host": {
1279
+ "type": "string",
1280
+ "minLength": 1,
1281
+ "maxLength": 253,
1282
+ "pattern": "^(\\*\\.)?[a-z0-9-]+(\\.[a-z0-9-]+)+$",
1283
+ "description": "A host a declarative plug-in calls, in lowercase: exact ('api.github.com'), or with one leading '*.' that stands for exactly one label ('*.myshopify.com'). No scheme, port or path: every call is https on port 443, and goes only to a public address."
1284
+ },
1285
+ "headerName": {
1286
+ "type": "string",
1287
+ "minLength": 1,
1288
+ "maxLength": 64,
1289
+ "pattern": "^[\\-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]+$"
1290
+ },
1291
+ "queryName": {
1292
+ "type": "string",
1293
+ "minLength": 1,
1294
+ "maxLength": 64,
1295
+ "pattern": "^[\\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]_abcdefghijklmnopqrstuvwxyz~]+$"
1296
+ },
1297
+ "vendorAuth": {
1298
+ "type": "object",
1299
+ "description": "How Initiative puts a connection's credential on a declarative plug-in's requests: one header, holding the prefix and the token joined by a space. Absent: 'Authorization: Bearer <token>'.",
1300
+ "properties": {
1301
+ "header": {
1302
+ "$ref": "#/$defs/headerName",
1303
+ "default": "Authorization"
1304
+ },
1305
+ "prefix": {
1306
+ "type": "string",
1307
+ "maxLength": 64,
1308
+ "default": "Bearer",
1309
+ "description": "Written before the token. Empty: the token alone."
1310
+ }
1311
+ },
1312
+ "additionalProperties": false
1313
+ },
1314
+ "vendorRequest": {
1315
+ "type": "object",
1316
+ "description": "One call Initiative makes to the vendor for a declarative plug-in, rendered from expressions. Initiative adds the credential itself, as 'auth' says, and follows no redirect.",
1317
+ "required": [
1318
+ "method",
1319
+ "url"
1320
+ ],
1321
+ "properties": {
1322
+ "method": {
1323
+ "enum": [
1324
+ "DELETE",
1325
+ "GET",
1326
+ "PATCH",
1327
+ "POST",
1328
+ "PUT"
1329
+ ]
1330
+ },
1331
+ "url": {
1332
+ "$ref": "#/$defs/expression",
1333
+ "description": "The address, as an expression answering a string: https, on one of the plug-in's hosts. Query parameters may be written into it or given in 'query'."
1334
+ },
1335
+ "query": {
1336
+ "type": "object",
1337
+ "maxProperties": 24,
1338
+ "propertyNames": {
1339
+ "$ref": "#/$defs/queryName"
1340
+ },
1341
+ "additionalProperties": {
1342
+ "$ref": "#/$defs/expression"
1343
+ },
1344
+ "description": "Query parameters by name, each an expression. One answering null or nothing is left out, and a list repeats the parameter once per value."
1345
+ },
1346
+ "headers": {
1347
+ "type": "object",
1348
+ "maxProperties": 12,
1349
+ "propertyNames": {
1350
+ "$ref": "#/$defs/headerName"
1351
+ },
1352
+ "additionalProperties": {
1353
+ "$ref": "#/$defs/expression"
1354
+ },
1355
+ "description": "Headers by name, each an expression answering a string; one answering null or nothing is left out. Never the credential's header, which Initiative sets."
1356
+ },
1357
+ "body": {
1358
+ "$ref": "#/$defs/expression",
1359
+ "description": "The JSON body, as an expression. Not beside 'graphql'."
1360
+ },
1361
+ "graphql": {
1362
+ "$ref": "#/$defs/graphqlRequest"
1363
+ },
1364
+ "connection": {
1365
+ "$ref": "#/$defs/identifier",
1366
+ "description": "Which connection's credential the request carries: a static connection's is the community's, an interactive one's the acting member's. A call made as a member carries a static connection's credential only when the endpoint's 'requires' names that connection, and is refused otherwise. Required on an endpoint's requests. Absent on 'after_connect' and 'health', which carry the credential of the connection they belong to."
1367
+ },
1368
+ "paging": {
1369
+ "$ref": "#/$defs/paging"
1370
+ }
1371
+ },
1372
+ "additionalProperties": false
1373
+ },
1374
+ "graphqlRequest": {
1375
+ "type": "object",
1376
+ "description": "A GraphQL request, sent by POST as the JSON body {query, variables}.",
1377
+ "required": [
1378
+ "query"
1379
+ ],
1380
+ "properties": {
1381
+ "query": {
1382
+ "type": "string",
1383
+ "minLength": 1,
1384
+ "maxLength": 16384,
1385
+ "description": "The GraphQL document, as text. What varies goes in 'variables'."
1386
+ },
1387
+ "variables": {
1388
+ "$ref": "#/$defs/expression",
1389
+ "description": "The variables, as an expression answering an object."
1390
+ }
1391
+ },
1392
+ "additionalProperties": false
1393
+ },
1394
+ "requestStep": {
1395
+ "type": "object",
1396
+ "required": [
1397
+ "name",
1398
+ "request"
1399
+ ],
1400
+ "properties": {
1401
+ "name": {
1402
+ "$ref": "#/$defs/identifier",
1403
+ "description": "Unique among its steps. Later steps, the map and the predicates beside it read this step's answer as 'steps.<name>'."
1404
+ },
1405
+ "request": {
1406
+ "$ref": "#/$defs/vendorRequest"
1407
+ }
1408
+ },
1409
+ "additionalProperties": false
1410
+ },
1411
+ "paging": {
1412
+ "description": "How a request reads more than one page, one member per method, told apart by 'kind'. Every page is read before the answer is mapped: 'response.body' is then every page's items appended in order, and the status and headers are the last page's. 'max_pages' is how many pages may be read; finding more after that many truncates or refuses, as 'on_limit' says, and a refusal answers 'unavailable: range-too-large'.",
1413
+ "oneOf": [
1414
+ {
1415
+ "$ref": "#/$defs/pageNumberPaging"
1416
+ },
1417
+ {
1418
+ "$ref": "#/$defs/linkHeaderPaging"
1419
+ },
1420
+ {
1421
+ "$ref": "#/$defs/cursorPaging"
1422
+ }
1423
+ ]
1424
+ },
1425
+ "pageNumberPaging": {
1426
+ "type": "object",
1427
+ "description": "Numbered pages from 1, read until one holds fewer than 'per_page' items. Both parameters are added to the request's query.",
1428
+ "required": [
1429
+ "kind",
1430
+ "page_param",
1431
+ "per_page",
1432
+ "max_pages",
1433
+ "on_limit"
1434
+ ],
1435
+ "properties": {
1436
+ "kind": {
1437
+ "const": "page_number"
1438
+ },
1439
+ "page_param": {
1440
+ "$ref": "#/$defs/queryName",
1441
+ "description": "The parameter carrying the page number."
1442
+ },
1443
+ "per_page_param": {
1444
+ "$ref": "#/$defs/queryName",
1445
+ "description": "The parameter carrying 'per_page'. Absent: the vendor's page size is fixed, and 'per_page' says what it is."
1446
+ },
1447
+ "per_page": {
1448
+ "type": "integer",
1449
+ "minimum": 1,
1450
+ "maximum": 100
1451
+ },
1452
+ "items": {
1453
+ "$ref": "#/$defs/pageItems"
1454
+ },
1455
+ "max_pages": {
1456
+ "$ref": "#/$defs/maxPages"
1457
+ },
1458
+ "on_limit": {
1459
+ "enum": [
1460
+ "refuse",
1461
+ "truncate"
1462
+ ]
1463
+ }
1464
+ },
1465
+ "additionalProperties": false
1466
+ },
1467
+ "linkHeaderPaging": {
1468
+ "type": "object",
1469
+ "description": "The address in the Link header's rel=\"next\", followed until there is none. It must be https on one of the plug-in's hosts.",
1470
+ "required": [
1471
+ "kind",
1472
+ "max_pages",
1473
+ "on_limit"
1474
+ ],
1475
+ "properties": {
1476
+ "kind": {
1477
+ "const": "link_header"
1478
+ },
1479
+ "items": {
1480
+ "$ref": "#/$defs/pageItems"
1481
+ },
1482
+ "max_pages": {
1483
+ "$ref": "#/$defs/maxPages"
1484
+ },
1485
+ "on_limit": {
1486
+ "enum": [
1487
+ "refuse",
1488
+ "truncate"
1489
+ ]
1490
+ }
1491
+ },
1492
+ "additionalProperties": false
1493
+ },
1494
+ "cursorPaging": {
1495
+ "type": "object",
1496
+ "description": "A cursor read from each page and sent with the next, while 'more' holds. Exactly one of 'param' and 'variable' says where it is sent.",
1497
+ "required": [
1498
+ "kind",
1499
+ "next",
1500
+ "more",
1501
+ "max_pages",
1502
+ "on_limit"
1503
+ ],
1504
+ "properties": {
1505
+ "kind": {
1506
+ "const": "cursor"
1507
+ },
1508
+ "next": {
1509
+ "$ref": "#/$defs/expression",
1510
+ "description": "The next page's cursor, from this page's 'response'."
1511
+ },
1512
+ "more": {
1513
+ "$ref": "#/$defs/expression",
1514
+ "description": "Whether there is a next page, from this page's 'response'."
1515
+ },
1516
+ "param": {
1517
+ "$ref": "#/$defs/queryName",
1518
+ "description": "The query parameter the cursor is sent in."
1519
+ },
1520
+ "variable": {
1521
+ "type": "string",
1522
+ "minLength": 1,
1523
+ "maxLength": 64,
1524
+ "pattern": "^[_A-Za-z][_0-9A-Za-z]*$",
1525
+ "description": "The GraphQL variable the cursor is sent in."
1526
+ },
1527
+ "items": {
1528
+ "$ref": "#/$defs/pageItems"
1529
+ },
1530
+ "max_pages": {
1531
+ "$ref": "#/$defs/maxPages"
1532
+ },
1533
+ "on_limit": {
1534
+ "enum": [
1535
+ "refuse",
1536
+ "truncate"
1537
+ ]
1538
+ }
1539
+ },
1540
+ "additionalProperties": false
1541
+ },
1542
+ "pageItems": {
1543
+ "$ref": "#/$defs/expression",
1544
+ "description": "A page's items, from that page's 'response'. Absent: its body."
1545
+ },
1546
+ "maxPages": {
1547
+ "type": "integer",
1548
+ "minimum": 1,
1549
+ "maximum": 10
1550
+ },
1551
+ "statusMatch": {
1552
+ "description": "An HTTP status: one code, or a range of a hundred ('4xx').",
1553
+ "anyOf": [
1554
+ {
1555
+ "type": "integer",
1556
+ "minimum": 100,
1557
+ "maximum": 599
1558
+ },
1559
+ {
1560
+ "enum": [
1561
+ "2xx",
1562
+ "3xx",
1563
+ "4xx",
1564
+ "5xx"
1565
+ ]
1566
+ }
1567
+ ]
1568
+ },
1569
+ "errorRule": {
1570
+ "type": "object",
1571
+ "description": "A vendor answer an endpoint gives its own meaning. Rules are tried in order on every answer, a 2xx included, and the first that matches decides.",
1572
+ "required": [
1573
+ "status",
1574
+ "code"
1575
+ ],
1576
+ "properties": {
1577
+ "status": {
1578
+ "$ref": "#/$defs/statusMatch"
1579
+ },
1580
+ "when": {
1581
+ "$ref": "#/$defs/expression",
1582
+ "description": "Also required to hold, over the answer as 'response'. Absent: the status alone matches."
1583
+ },
1584
+ "code": {
1585
+ "$ref": "#/$defs/identifier",
1586
+ "description": "What the call answers: one of the endpoint's 'unavailable' codes, one of Initiative's own (invalid, mapping-failed, not-authorized, not-found, range-too-large), or 'transient' for a passing failure such as a throttle, which is answered as one to retry."
1587
+ }
1588
+ },
1589
+ "additionalProperties": false
1590
+ },
1591
+ "afterConnect": {
1592
+ "type": "object",
1593
+ "description": "A declarative plug-in's after_connect: a request, or up to 3 steps, made with the access token just obtained, and the answers mapped to what the hook would answer. 'params' holds the flow's own parameters, such as the installation_id an install page returned.",
1594
+ "required": [
1595
+ "map"
1596
+ ],
1597
+ "properties": {
1598
+ "request": {
1599
+ "$ref": "#/$defs/vendorRequest",
1600
+ "description": "The one call. Not beside 'steps'."
1601
+ },
1602
+ "steps": {
1603
+ "type": "array",
1604
+ "minItems": 1,
1605
+ "maxItems": 3,
1606
+ "items": {
1607
+ "$ref": "#/$defs/requestStep"
1608
+ },
1609
+ "description": "Up to 3 calls made in order, each able to read the answers of the ones before it. Not beside 'request'."
1610
+ },
1611
+ "map": {
1612
+ "$ref": "#/$defs/expression",
1613
+ "description": "The connection's managed values and its account label, from 'response' (the last call's answer) and 'steps': {\"values\": {…}, \"account_label\": \"…\"}."
1614
+ },
1615
+ "refuse_when": {
1616
+ "$ref": "#/$defs/expression",
1617
+ "description": "Refuses the connection when it holds. It reads the map's answer as 'result', beside 'response' and 'steps'."
1618
+ },
1619
+ "code": {
1620
+ "$ref": "#/$defs/identifier",
1621
+ "description": "What a refusal answers. Given exactly when 'refuse_when' is."
1622
+ }
1623
+ },
1624
+ "oneOf": [
1625
+ {
1626
+ "required": [
1627
+ "request"
1628
+ ]
1629
+ },
1630
+ {
1631
+ "required": [
1632
+ "steps"
1633
+ ]
1634
+ }
1635
+ ],
1636
+ "additionalProperties": false
1637
+ },
1638
+ "connectionHealth": {
1639
+ "type": "object",
1640
+ "description": "Declarative plug-ins: a request Initiative makes on an interval, with this connection's credential, to learn whether the connection still works. A state other than 'ok' is reported once two answers in a row give it.",
1641
+ "required": [
1642
+ "request",
1643
+ "every",
1644
+ "states"
1645
+ ],
1646
+ "properties": {
1647
+ "request": {
1648
+ "$ref": "#/$defs/vendorRequest"
1649
+ },
1650
+ "every": {
1651
+ "type": "string",
1652
+ "maxLength": 5,
1653
+ "pattern": "^[0-9]+[mh]$",
1654
+ "description": "How often: a whole number of minutes ('15m') or hours ('6h'), at least 5 minutes and at most 1440 minutes."
1655
+ },
1656
+ "states": {
1657
+ "type": "array",
1658
+ "minItems": 1,
1659
+ "maxItems": 12,
1660
+ "items": {
1661
+ "$ref": "#/$defs/healthState"
1662
+ },
1663
+ "description": "Tried in order; the first that matches gives the state. An answer none matches is 'ok' when it is 2xx and 'unavailable' otherwise."
1664
+ }
1665
+ },
1666
+ "additionalProperties": false
1667
+ },
1668
+ "healthState": {
1669
+ "type": "object",
1670
+ "description": "The answers one row matches, and the state they mean. A row naming neither 'status' nor 'when' matches any answer.",
1671
+ "required": [
1672
+ "state"
1673
+ ],
1674
+ "properties": {
1675
+ "status": {
1676
+ "$ref": "#/$defs/statusMatch"
1677
+ },
1678
+ "when": {
1679
+ "$ref": "#/$defs/expression",
1680
+ "description": "Also required to hold, over the answer as 'response'."
1681
+ },
1682
+ "state": {
1683
+ "enum": [
1684
+ "ok",
1685
+ "removed",
1686
+ "suspended",
1687
+ "unavailable"
1688
+ ]
1689
+ }
1690
+ },
1691
+ "additionalProperties": false
1692
+ },
1693
+ "webhookEvent": {
1694
+ "type": "object",
1695
+ "description": "One way a delivery becomes an event. Its expressions read 'headers' (names in lowercase), 'payload' (the parsed body), 'connection' (the routed connection's non-secret fields) and 'now'.",
1696
+ "required": [
1697
+ "when",
1698
+ "emit",
1699
+ "map"
1700
+ ],
1701
+ "properties": {
1702
+ "when": {
1703
+ "$ref": "#/$defs/expression",
1704
+ "description": "Whether this row applies."
1705
+ },
1706
+ "emit": {
1707
+ "$ref": "#/$defs/namespacedId",
1708
+ "description": "An emit endpoint this manifest declares."
1709
+ },
1710
+ "map": {
1711
+ "$ref": "#/$defs/expression",
1712
+ "description": "The event's payload: an object of that endpoint's declared returns."
1713
+ }
1714
+ },
1715
+ "additionalProperties": false
1716
+ },
1717
+ "webhookStatus": {
1718
+ "type": "object",
1719
+ "description": "A delivery that says what state a connection is in, read as an event's expressions are.",
1720
+ "required": [
1721
+ "when",
1722
+ "connection",
1723
+ "state"
1724
+ ],
1725
+ "properties": {
1726
+ "when": {
1727
+ "$ref": "#/$defs/expression",
1728
+ "description": "Whether this row applies."
1729
+ },
1730
+ "connection": {
1731
+ "$ref": "#/$defs/identifier",
1732
+ "description": "A connection this plug-in declares."
1733
+ },
1734
+ "state": {
1735
+ "enum": [
1736
+ "ok",
1737
+ "removed",
1738
+ "suspended",
1739
+ "unavailable"
1740
+ ],
1741
+ "description": "'ok', 'suspended' or 'removed'."
1742
+ }
1743
+ },
1744
+ "additionalProperties": false
1745
+ }
1746
+ }
1747
+ }