initiative-plugin-sdk 7.1.0 → 8.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.
- package/README.md +385 -51
- package/dist/build.d.ts +1 -1
- package/dist/build.js +32 -22
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +17 -6
- package/dist/client.js +12 -8
- package/dist/client.js.map +1 -1
- package/dist/contract.d.ts +452 -80
- package/dist/contract.js +22 -6
- package/dist/contract.js.map +1 -1
- package/dist/define.d.ts +78 -31
- package/dist/define.js +27 -9
- package/dist/define.js.map +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/plugin-api.generated.d.ts +97 -5
- package/dist/plugin-api.generated.js +15 -2
- package/dist/plugin-api.generated.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +21 -5
- package/dist/server.js.map +1 -1
- package/dist/statements.d.ts +111 -0
- package/dist/statements.js +205 -0
- package/dist/statements.js.map +1 -0
- package/dist/testing.d.ts +132 -21
- package/dist/testing.js +273 -51
- package/dist/testing.js.map +1 -1
- package/dist/validate.d.ts +13 -4
- package/dist/validate.js +240 -48
- package/dist/validate.js.map +1 -1
- package/manifest.contract.json +514 -23
- package/package.json +1 -1
- package/schemas/plugin-api.json +462 -33
- package/schemas/plugin-manifest.json +553 -22
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ holds its tokens, and builds its manifest.
|
|
|
16
16
|
| `initiative-plugin-sdk/widget` | What a widget is handed, and the scenes it returns |
|
|
17
17
|
| `initiative-plugin-sdk/parts` | The components a part's tree is built from: `Section`, `Stack`, `Field`, `Value`, `Text`, `Button` |
|
|
18
18
|
| `initiative-plugin-sdk/jsx-runtime` | JSX for a part's tree, with no React |
|
|
19
|
-
| `initiative-plugin-sdk/testing` | A declarative plug-in's requests and
|
|
19
|
+
| `initiative-plugin-sdk/testing` | A declarative plug-in's requests, maps and writes, run against recorded vendor answers |
|
|
20
20
|
| `initiative-plugin-sdk/plugin-api.json` | The plug-in API contract: the OpenAPI document of every route a plug-in may call |
|
|
21
21
|
| bin `initiative-plugin` | `init`, `build`, `pack`, `dev`, `validate`, `keygen`, `uid`, `schema` |
|
|
22
22
|
|
|
@@ -82,8 +82,9 @@ export default definePlugin({
|
|
|
82
82
|
[`manifest.contract.json`](manifest.contract.json): `vendor`, `connections`
|
|
83
83
|
(keyed by id), `webhooks`, `pages` (keyed by id),
|
|
84
84
|
`dashboards`, `fields` (keyed by metadata key), `parts` and `actions` (keyed
|
|
85
|
-
by id)
|
|
86
|
-
contract declares has a type
|
|
85
|
+
by id), `settings` (keyed by setting key) and `help`. The types are
|
|
86
|
+
generated from the contract, so every term the contract declares has a type
|
|
87
|
+
here.
|
|
87
88
|
- **Features follow from what is filled in.** A plug-in with widgets declares
|
|
88
89
|
`widgets`, and so on.
|
|
89
90
|
|
|
@@ -170,12 +171,81 @@ hooks: {
|
|
|
170
171
|
- `community_status` runs whenever the community's standing changes. The same
|
|
171
172
|
value rides every call as `community_status`, so a missed hook is caught up
|
|
172
173
|
on the next call.
|
|
173
|
-
- `entitlement` carries
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
- `entitlement` carries billing's statement of what this community holds of
|
|
175
|
+
the plug-in: `values`, whose keys are agreed between the plug-in and
|
|
176
|
+
billing, and a `version` that rises with each statement. Statements can
|
|
177
|
+
repeat or arrive out of order, so keep the highest version you have seen and
|
|
178
|
+
ignore a lower one. A deployment with no billing never sends one. The SDK
|
|
179
|
+
verifies each statement before your hook sees it: see
|
|
180
|
+
[Entitlement statements](#entitlement-statements).
|
|
181
|
+
|
|
182
|
+
### Entitlement statements
|
|
183
|
+
|
|
184
|
+
What a community holds of a plug-in (its allowance, the packs it bought) is
|
|
185
|
+
decided by billing, not by Initiative. So that a compromised Initiative cannot
|
|
186
|
+
grant itself an entitlement, billing signs each statement with a key of its
|
|
187
|
+
own, Initiative stores the statement and hands it on verbatim, and your
|
|
188
|
+
plug-in checks billing's signature. Initiative can withhold a statement; it
|
|
189
|
+
cannot mint or change one.
|
|
190
|
+
|
|
191
|
+
A statement is a compact JWS. Its header is
|
|
192
|
+
`{"alg": "EdDSA", "typ": "initiative-entitlement+jwt", "kid": …}`, and its
|
|
193
|
+
claims are `{"iss": "initiative-billing", "listing_uid", "community_ref",
|
|
194
|
+
"version", "values", "iat"}`. It has no expiry: it holds until a higher
|
|
195
|
+
version replaces it. The `entitlement` hook's body, and what
|
|
196
|
+
`client.entitlement()` reads, are both `{"statement": "<jws>"}`.
|
|
197
|
+
|
|
198
|
+
Billing's public key reaches your plug-in from the deployment, never from
|
|
199
|
+
Initiative: it is the runtime fact `entitlement_issuer_key`, an SPKI PEM in
|
|
200
|
+
`INITIATIVE_ENTITLEMENT_ISSUER_KEY_PEM`. `initiative-plugin build` adds that
|
|
201
|
+
fact to the runtime of every plug-in with an `entitlement` hook, so you need
|
|
202
|
+
not declare it, and refuses it under any other variable name.
|
|
203
|
+
|
|
204
|
+
The SDK checks every statement, on the hook and on `client.entitlement()`, in
|
|
205
|
+
this order, and refuses it at the first failure:
|
|
206
|
+
|
|
207
|
+
1. the key is configured and is Ed25519; without one, every statement is
|
|
208
|
+
refused, and the plug-in logs why once;
|
|
209
|
+
2. the header's `alg` is exactly `EdDSA` and its `typ` exactly
|
|
210
|
+
`initiative-entitlement+jwt`: the header never chooses the algorithm, so
|
|
211
|
+
`alg: none` or an HMAC trick goes nowhere;
|
|
212
|
+
3. the Ed25519 signature over the JWS signing input;
|
|
213
|
+
4. `iss` is `initiative-billing`, `listing_uid` is your plug-in's own `uid`,
|
|
214
|
+
`version` is a whole number of one or more, and `values` is an object.
|
|
215
|
+
|
|
216
|
+
Only then is your hook called, with `version` and `values` taken from the
|
|
217
|
+
verified claims and nothing else in the body. A refused statement is answered
|
|
218
|
+
`422 {"error": "statement-refused", "detail": "<reason>"}` and never reaches
|
|
219
|
+
your hook; `client.entitlement()` throws `EntitlementStatementError`, whose
|
|
220
|
+
`reason` is one of `no-key`, `bad-key`, `malformed`, `alg`, `typ`,
|
|
221
|
+
`signature`, `iss`, `listing` or `claims`.
|
|
222
|
+
|
|
223
|
+
**What it does not cover.** `community_ref` is billing's own pairwise
|
|
224
|
+
reference for the community, which your plug-in cannot relate to the
|
|
225
|
+
`installation` Initiative names. Initiative still vouches for which community a
|
|
226
|
+
statement is for, so a compromised Initiative could at worst copy a real,
|
|
227
|
+
signed statement from one community onto another. That gap is accepted.
|
|
228
|
+
|
|
229
|
+
**Alerts.** The SDK counts, for the process's life:
|
|
230
|
+
|
|
231
|
+
- every statement by outcome, `applied` (verified and handed on) or
|
|
232
|
+
`refused-<reason>`;
|
|
233
|
+
- every grant of an unlimited allowance (`monthly_allowance: null`) to a
|
|
234
|
+
community whose last statement the process saw did not grant one. The memory
|
|
235
|
+
is the process's own, so after a restart the first such statement counts
|
|
236
|
+
again.
|
|
237
|
+
|
|
238
|
+
`entitlementMetrics()` answers them as an object, and `entitlementMetricsText()`
|
|
239
|
+
in the Prometheus text format, to append to what your metrics endpoint serves:
|
|
240
|
+
`initiative_plugin_entitlement_statements_total{outcome="…"}` and
|
|
241
|
+
`initiative_plugin_entitlement_unlimited_grants_total`. Alert on any
|
|
242
|
+
`refused-*` and on any rise in unlimited grants. Neither ever refuses a
|
|
243
|
+
correctly signed statement.
|
|
244
|
+
|
|
245
|
+
**If you self-host.** A deployment that runs billing mints its statement
|
|
246
|
+
keypair and fills `INITIATIVE_ENTITLEMENT_ISSUER_KEY_PEM` for each plug-in that
|
|
247
|
+
declares the fact. A deployment with no billing leaves it unset: it sends no
|
|
248
|
+
statements, and any that did arrive would be refused.
|
|
179
249
|
|
|
180
250
|
### The vendor's own setup
|
|
181
251
|
|
|
@@ -214,6 +284,43 @@ vendor: {
|
|
|
214
284
|
key must be a vendor field, each answer is written at most once, and
|
|
215
285
|
`client_secret`, `pem` and `webhook_secret` go to `secret` fields.
|
|
216
286
|
|
|
287
|
+
### A token minted on demand
|
|
288
|
+
|
|
289
|
+
A static connection's `token` has Initiative mint its access token rather than
|
|
290
|
+
store one: it signs a JWT with a vendor key, posts it to the vendor's
|
|
291
|
+
`exchange_url`, and caches the answer until shortly before it expires.
|
|
292
|
+
`exchange_body` is a JSON body posted with it, such as the narrower
|
|
293
|
+
permissions a token is asked for:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
token: {
|
|
297
|
+
type: "jwt_bearer",
|
|
298
|
+
exchange_url: "https://api.tracker.example/installations/{installation_id}/tokens",
|
|
299
|
+
exchange_body: { permissions: { issues: "write", contents: "read" } },
|
|
300
|
+
iss: "{vendor.app_id}",
|
|
301
|
+
key: "{vendor.private_key}",
|
|
302
|
+
},
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`exchange_url`, `iss` and every string in `exchange_body` may name a vendor
|
|
306
|
+
value as `{vendor.<key>}` and one of the connection's own fields as `{<key>}`;
|
|
307
|
+
`validateManifest` refuses a name that neither declares.
|
|
308
|
+
|
|
309
|
+
`exchange_body` may instead be an expression answering the body from
|
|
310
|
+
`connection` alone, the connection's own non-secret fields, managed ones
|
|
311
|
+
included, so the body can differ by connection: a vendor may refuse a token
|
|
312
|
+
request naming a permission the installation does not hold. A body that names a
|
|
313
|
+
vendor value stays an object. An answer that is not an object fails the token
|
|
314
|
+
request, so the connection cannot be used until the expression answers one.
|
|
315
|
+
`runExchangeBody` from `initiative-plugin-sdk/testing` answers the body
|
|
316
|
+
Initiative posts for a connection's `fields` and `vendor` values.
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
// account_type is a managed field; only an organization's installation holds "members"
|
|
320
|
+
exchange_body:
|
|
321
|
+
'{"permissions": $merge([{"issues": "write"}, connection.account_type = "organization" ? {"members": "read"} : {}])}',
|
|
322
|
+
```
|
|
323
|
+
|
|
217
324
|
### Pages
|
|
218
325
|
|
|
219
326
|
A page is one of your plug-in's screens, framed by Initiative:
|
|
@@ -267,14 +374,15 @@ What a plug-in adds to the views and pages of Initiative's items: tasks,
|
|
|
267
374
|
calendar events, queue items, counters, gallery images and posts
|
|
268
375
|
(`ITEM_KINDS`). Initiative draws all of it with its own components, so no
|
|
269
376
|
plug-in code runs in a reader's browser, and the community's managers place
|
|
270
|
-
it; a plug-in never places itself.
|
|
271
|
-
|
|
272
|
-
run an action.
|
|
377
|
+
it; a plug-in never places itself. Any plug-in may declare them, container or
|
|
378
|
+
declarative.
|
|
273
379
|
|
|
274
380
|
**Metadata** is what the plug-in keeps on an item or on its own install: values
|
|
275
381
|
by key (a lowercase letter, then lowercase letters, digits, `_` and `.`), held
|
|
276
|
-
by Initiative
|
|
277
|
-
below)
|
|
382
|
+
by Initiative. A container writes them with its installation token
|
|
383
|
+
(`client.metadata`, below); for a declarative plug-in, Initiative writes them
|
|
384
|
+
from the plug-in's expressions ([Declarative integrations](#declarative-integrations)).
|
|
385
|
+
Initiative enforces its sizes: a value of at most 8192 bytes as JSON,
|
|
278
386
|
32 keys and 64 KiB on one item, and 256 keys and 1 MiB on the install. A
|
|
279
387
|
string of at most 255 characters can be found by.
|
|
280
388
|
|
|
@@ -304,16 +412,64 @@ fields: {
|
|
|
304
412
|
editor's Add picker shows; `tone` is a badge's tone when its value names none
|
|
305
413
|
(`accent`, `positive`, `negative`, `warning`, `neutral`, `muted`).
|
|
306
414
|
|
|
307
|
-
**
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
415
|
+
**A list field**, `list: true`, holds several values: its value is a list of
|
|
416
|
+
elements, each one value of the field's `kind` with a `ref` of 1 to 255
|
|
417
|
+
characters that tells it from the others. A `text`, `number`, `date`,
|
|
418
|
+
`datetime` or `checkbox` element is `{ ref, value }`; a `link`, `badge` or
|
|
419
|
+
`progress` element is the kind's own object with its `ref`, such as
|
|
420
|
+
`{ ref, url, text? }`. Initiative keeps each element as a value of its own,
|
|
421
|
+
held to the size of one value, and finds an element by its `ref`.
|
|
422
|
+
|
|
423
|
+
**States** say what state a value is in, in the plug-in's own words: up to 4
|
|
424
|
+
facets, each with up to 12 keys. A value names a key of each facet it is in as
|
|
425
|
+
`state`, and Initiative draws one badge per facet, labelled in the reader's
|
|
426
|
+
language. A list element, and a `link`, `badge` or `progress` value, carries
|
|
427
|
+
`state` beside its other properties; a `text`, `number`, `date`, `datetime` or
|
|
428
|
+
`checkbox` value that carries one is written as an element is, without the
|
|
429
|
+
`ref`: `{ value, state }`.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
fields: {
|
|
433
|
+
"tracker.issues": {
|
|
434
|
+
name: { en: "Issues" },
|
|
435
|
+
kind: "link",
|
|
436
|
+
list: true,
|
|
437
|
+
on: ["task"],
|
|
438
|
+
states: {
|
|
439
|
+
status: {
|
|
440
|
+
name: { en: "Status" },
|
|
441
|
+
values: { open: { label: { en: "Open" }, tone: "positive" }, closed: { label: { en: "Closed" }, tone: "muted" } },
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
actions: ["unlink"],
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
// an element: { ref: "12", url: "https://tracker.example/issues/12", text: "#12", state: { status: "open" } }
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
A list field's `actions` are offered on each of its elements, and each is
|
|
451
|
+
offered on every item kind the field is. They are a declarative plug-in's: an
|
|
452
|
+
action run from an element reads the element's `ref` as `element`, and a
|
|
453
|
+
container's handler is told the item alone.
|
|
454
|
+
|
|
455
|
+
**An action** runs on one item, from a part's button, from the item's own menu
|
|
456
|
+
with `menu: true`, or from an element of a list field that offers it. A
|
|
457
|
+
container plug-in's action names one of its write endpoints: Initiative calls
|
|
458
|
+
it as the installation, with `viewer` and `subject` set, and the handler does
|
|
459
|
+
the work, usually writing metadata. A declarative plug-in's names an endpoint,
|
|
460
|
+
a `write`, or both, and Initiative does the work itself. Either way Initiative
|
|
461
|
+
then answers the reader with the item's metadata as it stands. With `confirm`,
|
|
462
|
+
Initiative asks first; with `admin_only: true`, only the community's admins may
|
|
463
|
+
run it.
|
|
464
|
+
|
|
465
|
+
`params` names the endpoint's params Initiative asks the reader for before it
|
|
466
|
+
runs the action, in a form drawn from their own declarations: each one's type,
|
|
467
|
+
label, and `options` or `options_from`. The endpoint is sent the reader's
|
|
468
|
+
answers and nothing else, so every param it requires is among them.
|
|
313
469
|
|
|
314
470
|
```ts
|
|
315
471
|
actions: {
|
|
316
|
-
"new-link": { name: { en: "New link" }, endpoint: "create-link", on: ["task"], menu: true },
|
|
472
|
+
"new-link": { name: { en: "New link" }, endpoint: "create-link", params: ["url"], on: ["task"], menu: true },
|
|
317
473
|
},
|
|
318
474
|
```
|
|
319
475
|
|
|
@@ -331,8 +487,11 @@ plug-in's parts on one item.
|
|
|
331
487
|
| `button` | `action`: an action's id; `variant?` (`primary`, `secondary`, `ghost`) | no |
|
|
332
488
|
|
|
333
489
|
Every field and action a part names must be offered on each kind in the
|
|
334
|
-
part's `on`. A
|
|
335
|
-
the
|
|
490
|
+
part's `on`. A `text` node's text may name `{item.id}`, the item's id, and
|
|
491
|
+
`{settings.<key>}`, one of the plug-in's [settings](#settings-and-help), which
|
|
492
|
+
Initiative fills in when it draws the part; `validateManifest` refuses any
|
|
493
|
+
other `{…}`. A tree is plain data, `{ type, props?, children? }`, or JSX
|
|
494
|
+
through the SDK's runtime, which the build turns into the same data:
|
|
336
495
|
|
|
337
496
|
```tsx
|
|
338
497
|
/** @jsxImportSource initiative-plugin-sdk */
|
|
@@ -362,6 +521,37 @@ children join the node that holds it, and `null` and `false` children are left
|
|
|
362
521
|
out. `key` is JSX's own and is not passed on, which is why a field is named by
|
|
363
522
|
`field`.
|
|
364
523
|
|
|
524
|
+
### Settings and help
|
|
525
|
+
|
|
526
|
+
**Settings** are values the community's admins see and may change on the
|
|
527
|
+
install, keyed by setting key, at most 16. A part's text names one as
|
|
528
|
+
`{settings.<key>}`, and a declarative plug-in's expressions read them as
|
|
529
|
+
`settings.<key>`.
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
settings: {
|
|
533
|
+
prefix: { type: "string", label: { en: "Reference prefix" }, unique: true, assigned: "community_name" },
|
|
534
|
+
},
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
A `unique` setting's value is held by no other community on the deployment for
|
|
538
|
+
this plug-in, and is a lowercase letter, then lowercase letters and digits, 2
|
|
539
|
+
to 16 characters in all (`^[a-z][a-z0-9]{1,15}$`), so a reference such as
|
|
540
|
+
`acme-7` reads one way. With `assigned: "community_name"`, Initiative gives it a
|
|
541
|
+
value when the community installs the plug-in: the letters and digits of the
|
|
542
|
+
community's name, in lowercase, with a short random suffix when another
|
|
543
|
+
community holds that value. An admin may rename it. The earlier values stay the
|
|
544
|
+
community's, and expressions read them as `aliases.<key>`, so a reference made
|
|
545
|
+
under one still resolves.
|
|
546
|
+
|
|
547
|
+
**Help** is markdown, by language tag, at most 2000 characters in each.
|
|
548
|
+
Initiative shows it behind a "?" on the headers of the plug-in's parts and on
|
|
549
|
+
its settings:
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
help: { en: "Links issues to tasks. Name a task's reference, such as **acme-7**, in an issue's title to link it." },
|
|
553
|
+
```
|
|
554
|
+
|
|
365
555
|
## 2. Widgets
|
|
366
556
|
|
|
367
557
|
A widget is a module exporting `render`, typed from the endpoint it draws:
|
|
@@ -499,6 +689,11 @@ for (const { installation, active } of await initiative.installations()) {
|
|
|
499
689
|
}
|
|
500
690
|
```
|
|
501
691
|
|
|
692
|
+
An `Initiative` made by hand reads `client.entitlement()` only when it is given
|
|
693
|
+
something to check statements with: `entitlements: new EntitlementStatements({
|
|
694
|
+
listingUid: plugin.uid })`, which reads billing's key from
|
|
695
|
+
`INITIATIVE_ENTITLEMENT_ISSUER_KEY_PEM`. The server's own client has one.
|
|
696
|
+
|
|
502
697
|
- `asInstallation(installation, { initiative, scopes })` acts as the community,
|
|
503
698
|
optionally narrowed to one initiative the plug-in is placed in and fewer scopes.
|
|
504
699
|
- `asMember(installation, member, { purpose, initiative })` acts for one
|
|
@@ -527,7 +722,11 @@ for (const { installation, active } of await initiative.installations()) {
|
|
|
527
722
|
path after `/c/{community}`) by hand, with the same scope check.
|
|
528
723
|
- `client.callPlugin(publicId, endpointId, params)` calls another plug-in's public
|
|
529
724
|
endpoint through Initiative. It needs `plugins:<publicId>` among the plug-in's
|
|
530
|
-
scopes, granted by the community.
|
|
725
|
+
scopes, granted by the community. A plug-in that uses whichever plug-ins a
|
|
726
|
+
community lets it sets `callsPlugins: true` (the manifest's
|
|
727
|
+
`service.calls_plugins`) instead of naming each: a community that installs
|
|
728
|
+
another plug-in is asked there whether this one may use it, which grants the
|
|
729
|
+
scope.
|
|
531
730
|
- `client.metadata`, the plug-in's metadata, always on the installation's
|
|
532
731
|
token: `get(itemKind, ids)` answers each item's values by id;
|
|
533
732
|
`set(itemKind, id, values)` writes some of one item's values, a null
|
|
@@ -540,7 +739,8 @@ for (const { installation, active } of await initiative.installations()) {
|
|
|
540
739
|
token, `client.reportConfigStatus({ state, detail })`, and
|
|
541
740
|
`client.emitEvent({ eventType, payload, initiativeId })`.
|
|
542
741
|
- What the community holds of the plug-in: `client.entitlement()` reads the
|
|
543
|
-
statement the `entitlement` hook delivers,
|
|
742
|
+
statement the `entitlement` hook delivers, verified the same way, or null
|
|
743
|
+
when there is none, and
|
|
544
744
|
`client.reportUsage({ meter, period, quantity })` reports a meter's total
|
|
545
745
|
for one `YYYY-MM` period. The total is the whole period's, never an
|
|
546
746
|
increment, so repeating a report is harmless. Only meters the deployment
|
|
@@ -620,7 +820,11 @@ It holds nothing that raises a privilege. Resources, network rules, hostnames
|
|
|
620
820
|
and every key and password stay the deployment's:
|
|
621
821
|
|
|
622
822
|
- `facts` come from a fixed list: `app_url`, `app_origin`, `app_api_public`,
|
|
623
|
-
`app_api_internal`, `page_origin` and `
|
|
823
|
+
`app_api_internal`, `page_origin`, `port` and `entitlement_issuer_key`
|
|
824
|
+
(billing's statement key, added for you with an `entitlement` hook; see
|
|
825
|
+
[Entitlement statements](#entitlement-statements)). There is no other
|
|
826
|
+
channel to Initiative's own services: billing reaches a plug-in only through
|
|
827
|
+
Initiative.
|
|
624
828
|
- `settings` are names the operator fills.
|
|
625
829
|
- `secrets` are generated by the deployment and kept.
|
|
626
830
|
|
|
@@ -668,7 +872,9 @@ A plug-in that only calls a vendor's API and reshapes the answer needs no
|
|
|
668
872
|
container. Name the `hosts` it calls and give each endpoint a `request` and a
|
|
669
873
|
`map` in place of a handler: Initiative makes the call with the connection's
|
|
670
874
|
credential, and maps the answer itself. The plug-in has no `service` block, no
|
|
671
|
-
image and nothing to run, and its listing registers it as `declarative`.
|
|
875
|
+
image and nothing to run, and its listing registers it as `declarative`. It
|
|
876
|
+
may still keep values on Initiative's items, which its webhook events,
|
|
877
|
+
schedules and actions write and its fields and parts show.
|
|
672
878
|
|
|
673
879
|
```ts
|
|
674
880
|
// src/plugin.ts
|
|
@@ -744,8 +950,10 @@ it("lists a repository's open issues", async () => {
|
|
|
744
950
|
`connection` (the non-secret fields of the connection its request names),
|
|
745
951
|
`connections` (on an endpoint, each connection its `requires` names that the
|
|
746
952
|
call has, by id: `connections.workspace.owner`), `now`, and once a call has
|
|
747
|
-
been answered `response` (`status`, `headers`, `body`).
|
|
748
|
-
|
|
953
|
+
been answered `response` (`status`, `headers`, `body`). Every expression
|
|
954
|
+
also reads `settings`, the install's [settings](#settings-and-help) by key,
|
|
955
|
+
and `aliases`, each unique setting's earlier values. `build` parses every
|
|
956
|
+
one and fails with its place in the manifest when one does not parse.
|
|
749
957
|
- **Each evaluation is bounded** by the contract's `CAPS.expressionTimeMs`,
|
|
750
958
|
`CAPS.expressionDepth` and `CAPS.expressionOutputBytes`; past one, or on an
|
|
751
959
|
error, the call answers `unavailable: mapping-failed`. `evaluate` from
|
|
@@ -754,10 +962,40 @@ it("lists a repository's open issues", async () => {
|
|
|
754
962
|
request's `connection` names, as `auth` says: `Authorization: Bearer
|
|
755
963
|
<token>` unless `auth: { header, prefix }` says otherwise. Every address
|
|
756
964
|
must be https on one of `hosts`, exact or with one leading `*.` label. A
|
|
757
|
-
request or step
|
|
758
|
-
`requires` too,
|
|
759
|
-
`
|
|
760
|
-
|
|
965
|
+
request or step naming an `interactive` connection for every call names it
|
|
966
|
+
in the endpoint's `requires` too, and is on no endpoint whose `actors`
|
|
967
|
+
include `installation`, or `build` fails: a call made as the installation
|
|
968
|
+
has no member's credential. A call made as a member carries a `static`
|
|
969
|
+
connection's credential only when `requires` names it, and is refused
|
|
970
|
+
otherwise.
|
|
971
|
+
- **A connection per actor.** A write's request or step may name one
|
|
972
|
+
connection for each of the endpoint's `actors` in place of one for every
|
|
973
|
+
call, so a call made as the member carries the member's own credential and
|
|
974
|
+
one made as the installation the community's:
|
|
975
|
+
|
|
976
|
+
```ts
|
|
977
|
+
export const comment = defineEndpoint({
|
|
978
|
+
direction: "write",
|
|
979
|
+
actors: ["member", "installation"],
|
|
980
|
+
params: {
|
|
981
|
+
number: { type: "int", label: { en: "Issue" }, required: true },
|
|
982
|
+
text: { type: "string", label: { en: "Comment" }, required: true },
|
|
983
|
+
},
|
|
984
|
+
returns: { id: "int" },
|
|
985
|
+
request: {
|
|
986
|
+
method: "POST",
|
|
987
|
+
url: '"https://api.tracker.example/issues/" & params.number & "/comments"',
|
|
988
|
+
body: '{"body": params.text}',
|
|
989
|
+
connection: { member: "account", installation: "workspace" },
|
|
990
|
+
},
|
|
991
|
+
map: '{"id": response.body.id}',
|
|
992
|
+
});
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
`member` names an `interactive` connection and `installation` a `static`
|
|
996
|
+
one, for exactly the actors the endpoint declares. The member's needs no
|
|
997
|
+
`requires`: a call made as the member requires it. An expression's
|
|
998
|
+
`connection` holds the fields of the one the call carries.
|
|
761
999
|
- **`steps`** in place of `request` makes up to three calls in order; each
|
|
762
1000
|
reads the earlier ones as `steps.<name>`, and so does the map.
|
|
763
1001
|
- **`paging`** on a request reads more pages before the map runs:
|
|
@@ -773,21 +1011,109 @@ it("lists a repository's open issues", async () => {
|
|
|
773
1011
|
three `steps`, a map to `{values, account_label}`, and `refuse_when` with
|
|
774
1012
|
its `code`, which read each step as `steps.<name>`) and a
|
|
775
1013
|
`health` check run on an interval. **Webhooks** take `events`, each mapping
|
|
776
|
-
a delivery to one of the plug-in's emissions,
|
|
777
|
-
connection's state; a delivery may be
|
|
778
|
-
body `path`.
|
|
779
|
-
- **
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
`
|
|
784
|
-
|
|
785
|
-
|
|
1014
|
+
a delivery to one of the plug-in's emissions, changes to its values, or
|
|
1015
|
+
both, and `status` rows that set a connection's state; a delivery may be
|
|
1016
|
+
routed by a `header` instead of a body `path`.
|
|
1017
|
+
- **Scopes** are read scopes alone, at the manifest's top level, and
|
|
1018
|
+
`validateManifest` refuses any other: a declarative plug-in keeps its own
|
|
1019
|
+
values on items it can read, and does not call Initiative. Initiative keeps
|
|
1020
|
+
its values only on items those scopes read, such as tasks with
|
|
1021
|
+
`projects:read`.
|
|
1022
|
+
- **Values** are written from a `write` expression: Initiative makes the
|
|
1023
|
+
changes it answers, as the install. A **change** sets values by key,
|
|
1024
|
+
`{"set": {"<key>": <value>}}`, the shape of `client.metadata.set`: a null
|
|
1025
|
+
removes its key, and every value is held to the metadata caps. A list
|
|
1026
|
+
field's value is its whole list of elements; its elements change by their
|
|
1027
|
+
`ref` with `{"upsert": {"<key>": [<element>]}}`, which adds each element or
|
|
1028
|
+
merges it into the one with its `ref` (the properties it gives replace that
|
|
1029
|
+
element's, the rest stay, and its `state` merges by facet, so a write that
|
|
1030
|
+
knows one fact changes that fact alone), and `{"remove": {"<key>": ["<ref>"]}}`. A
|
|
1031
|
+
change gives at least one of `set`, `upsert` and `remove`, and changes each
|
|
1032
|
+
key with only one. A **targeted change** adds its `target`:
|
|
1033
|
+
`{"kind": "task", "id": 7}` for one item, `{"install": true}` for the
|
|
1034
|
+
install, or `{"holding": {"key": "<key>", "value": "<text>"}}` for every item
|
|
1035
|
+
on which the install holds that value under that key (a number or a boolean
|
|
1036
|
+
as JSON writes it, a list element by its `ref`). A target the install cannot
|
|
1037
|
+
read is skipped, one `write` answers at most 100 targeted changes
|
|
1038
|
+
(`CAPS.writeTargets`), and a `write` answering nothing changes nothing.
|
|
1039
|
+
- An **event** row's `write`, beside its `emit` and `map` or in their place,
|
|
1040
|
+
answers targeted changes from `headers`, `payload`, `connection`, `now`,
|
|
1041
|
+
`settings` and `aliases`. The first row whose `when` holds applies, its
|
|
1042
|
+
emission and its write both.
|
|
1043
|
+
- An **action** names an `endpoint`, a `write`, or both. The endpoint may be
|
|
1044
|
+
a read, fetching what the `write` keeps, or a write that changes something
|
|
1045
|
+
at the vendor. Initiative makes the endpoint's call with the `params` the
|
|
1046
|
+
reader answered, on the credential its `actors` say, its expressions also
|
|
1047
|
+
reading `item` (`{kind, id, title}`) and `element` (the `ref` of the list
|
|
1048
|
+
element it was run from, if any), then the one change `write` answers from
|
|
1049
|
+
`item`, `held` (the install's values on the item), `result` (the
|
|
1050
|
+
endpoint's mapped answer), `params`, `element`, `settings`, `aliases` and
|
|
1051
|
+
`now`.
|
|
1052
|
+
- A **schedule** makes a read as the installation on an interval: `endpoint`
|
|
1053
|
+
names a read endpoint whose `actors` include `installation`, `params`
|
|
1054
|
+
answers its params from `since` (when the schedule last succeeded, or
|
|
1055
|
+
null), `now`, `connection`, `settings` and `aliases`, and `write` answers
|
|
1056
|
+
targeted changes from `result`, `since`, `now`, `settings` and `aliases`.
|
|
1057
|
+
Its id is none of the plug-in's connections': Initiative keys connection
|
|
1058
|
+
health checks by connection id.
|
|
1059
|
+
|
|
1060
|
+
```ts
|
|
1061
|
+
schedules: {
|
|
1062
|
+
sync: {
|
|
1063
|
+
every: "1h",
|
|
1064
|
+
endpoint: "updated-issues", // returns numbers: int list, states: string list
|
|
1065
|
+
params: '{"since": since}',
|
|
1066
|
+
write:
|
|
1067
|
+
'[$map(result.numbers, function($n, $i) { {"target": {"holding": {"key": "issue.number", "value": $string($n)}}, "set": {"issue.state": result.states[$i]}} })]',
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
actions: {
|
|
1071
|
+
create: {
|
|
1072
|
+
name: { en: "Create an issue" },
|
|
1073
|
+
endpoint: "create-issue", // returns number: int, url: string
|
|
1074
|
+
on: ["task"],
|
|
1075
|
+
write: '{"set": {"issue.number": result.number, "issue.link": {"url": result.url, "text": "#" & result.number}}}',
|
|
1076
|
+
},
|
|
1077
|
+
link: {
|
|
1078
|
+
name: { en: "Link an issue" },
|
|
1079
|
+
endpoint: "link-issue", // takes number: int; returns number: int, url: string, state: string
|
|
1080
|
+
params: ["number"],
|
|
1081
|
+
on: ["task"],
|
|
1082
|
+
write: '{"upsert": {"issues.linked": [{"ref": $string(result.number), "url": result.url, "state": {"status": result.state}}]}}',
|
|
1083
|
+
},
|
|
1084
|
+
unlink: { name: { en: "Unlink" }, on: ["task"], write: '{"remove": {"issues.linked": [element]}}' }, // a list field's element action
|
|
1085
|
+
},
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
Wrap a list a `write` answers in `[ ]`: a JSONata path matching one item
|
|
1089
|
+
answers it alone, and a run refuses a lone change where a list belongs.
|
|
1090
|
+
- **One plug-in is one kind.** A declarative plug-in is a manifest Initiative
|
|
1091
|
+
runs, with no plug-in code: it has no handlers, hooks or pages. A container
|
|
1092
|
+
plug-in ships code, which Initiative calls, and uses none of the declarative
|
|
1093
|
+
terms. Any plug-in may keep values on items in Initiative: a container
|
|
1094
|
+
writes them with its installation token, and for a declarative plug-in
|
|
1095
|
+
Initiative writes them from the plug-in's expressions. `validateManifest`
|
|
1096
|
+
and `build` refuse a mix.
|
|
1097
|
+
|
|
1098
|
+
`runEndpoint`, `runAfterConnect`, `runHealth`, `runAction` and `runSchedule`
|
|
1099
|
+
render each request as Initiative sends it, less the credential, answer it from
|
|
1100
|
+
the recorded `responses` in order, and return the requests beside the result or
|
|
786
1101
|
the code the run answered. `runEndpoint` takes each connection's fields by id
|
|
787
|
-
in `connections
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1102
|
+
in `connections`, and the `actor` the call is made as, by default the
|
|
1103
|
+
endpoint's first: a request naming a connection per actor carries that
|
|
1104
|
+
actor's. It does not refuse a call the way Initiative would for who is making
|
|
1105
|
+
it. `runAction` takes the `item`, what the install `held` on it, the `params`
|
|
1106
|
+
the reader answered, the `element` it was run from and its endpoint's `actor`,
|
|
1107
|
+
and answers the endpoint's `result` and the `change`; `runSchedule` takes
|
|
1108
|
+
`since`, makes its read as the installation, and answers the endpoint's
|
|
1109
|
+
`result` and the `writes`;
|
|
1110
|
+
`runWebhook` answers a delivery's `event`, `writes` and `status`. Every runner
|
|
1111
|
+
but `runExchangeBody` takes the install's `settings` and `aliases`. A run fails
|
|
1112
|
+
when a request has no recorded answer left, when an answer is left over, when
|
|
1113
|
+
the map's answer does not fit the declared returns, or when a `write` answers
|
|
1114
|
+
what is not a change: one that changes a key twice, gives a list field anything
|
|
1115
|
+
but a list of elements, or upserts or removes elements of a field that is not a
|
|
1116
|
+
list.
|
|
791
1117
|
|
|
792
1118
|
## A private plug-in
|
|
793
1119
|
|
|
@@ -845,9 +1171,10 @@ npx initiative-plugin schema # the JSON Schema it checks again
|
|
|
845
1171
|
`validateManifest` runs the bundled JSON Schema, then the checks a schema
|
|
846
1172
|
cannot express: features against the blocks present, ids that must name
|
|
847
1173
|
something the manifest declares, connection, vendor setup and schedule rules,
|
|
848
|
-
what a part's nodes
|
|
849
|
-
|
|
850
|
-
|
|
1174
|
+
what a part's nodes, a list field's element actions and an action's `params`
|
|
1175
|
+
name, the placeholders in a part's text, a part's size and depth, and every
|
|
1176
|
+
term the contract does not declare (a deployment discards those without saying
|
|
1177
|
+
so). The deployment also enforces byte-size caps.
|
|
851
1178
|
|
|
852
1179
|
## The contract
|
|
853
1180
|
|
|
@@ -929,7 +1256,14 @@ either the file's version or the client is stale.
|
|
|
929
1256
|
| `initiatives:read` | The initiatives the plug-in is placed in. |
|
|
930
1257
|
| `initiatives:moderate` | Acting as a moderator in an initiative it is placed in: everything there, within the plug-in's other scopes. Only on a token that asks for it (`level=moderator`) and is narrowed to that initiative. |
|
|
931
1258
|
| `community:admin` | Acting with a community admin's standing across the community, within the plug-in's other scopes. Only on a token that asks for it (`level=community_admin`) and is not narrowed to an initiative. |
|
|
932
|
-
| `plugins:<public id>` | Calling that plug-in's public endpoints through Initiative. One per plug-in. |
|
|
1259
|
+
| `plugins:<public id>` | Calling that plug-in's public endpoints through Initiative, and reading the values it shows on items the plug-in can read. One per plug-in. |
|
|
1260
|
+
|
|
1261
|
+
A container plug-in's `scopes` go in its manifest's `service` block. A
|
|
1262
|
+
declarative plug-in asks for read scopes alone, at the manifest's top level: it
|
|
1263
|
+
keeps its own values on items those scopes read, and does not call Initiative.
|
|
1264
|
+
A container plug-in with `callsPlugins: true` needs no `plugins:` scope in its
|
|
1265
|
+
manifest: a community that installs another plug-in is asked there whether
|
|
1266
|
+
this one may use it, and that grants `plugins:<public id>` for it.
|
|
933
1267
|
|
|
934
1268
|
Writing implies reading. Within its scopes a plug-in still sees only what is open
|
|
935
1269
|
to the initiative, shared with the plug-in, or created by it — unless a token asks
|
package/dist/build.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface BuiltPlugin {
|
|
|
52
52
|
listing?: ListingDeclaration;
|
|
53
53
|
}
|
|
54
54
|
/** What the listing writers read of a plug-in: its definition, or a {@link BuiltPlugin}'s. */
|
|
55
|
-
export type ListedPlugin = Pick<AnyPlugin, "publicId" | "uid" | "name" | "scopes" | "hosts" | "hooks" | "listing">;
|
|
55
|
+
export type ListedPlugin = Pick<AnyPlugin, "publicId" | "uid" | "name" | "scopes" | "callsPlugins" | "hosts" | "hooks" | "listing">;
|
|
56
56
|
/** What a definition builds: the plug-in and its manifest, or every problem that stops it. */
|
|
57
57
|
export type Compiled = {
|
|
58
58
|
plugin: ListedPlugin;
|