@openbrf/plugin-sdk 1.0.0 → 1.4.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 (104) hide show
  1. package/README.md +95 -0
  2. package/dist/abort-signal.d.ts +23 -0
  3. package/dist/abort-signal.d.ts.map +1 -0
  4. package/dist/abort-signal.js +2 -0
  5. package/dist/abort-signal.js.map +1 -0
  6. package/dist/action-refusal.d.ts +101 -0
  7. package/dist/action-refusal.d.ts.map +1 -0
  8. package/dist/action-refusal.js +96 -0
  9. package/dist/action-refusal.js.map +1 -0
  10. package/dist/action-schema.d.ts.map +1 -1
  11. package/dist/action-schema.js +4 -1
  12. package/dist/action-schema.js.map +1 -1
  13. package/dist/actions.d.ts +94 -6
  14. package/dist/actions.d.ts.map +1 -1
  15. package/dist/actions.js.map +1 -1
  16. package/dist/blocks.d.ts +310 -0
  17. package/dist/blocks.d.ts.map +1 -0
  18. package/dist/blocks.js +185 -0
  19. package/dist/blocks.js.map +1 -0
  20. package/dist/catalog.d.ts +1092 -41
  21. package/dist/catalog.d.ts.map +1 -1
  22. package/dist/catalog.js +472 -15
  23. package/dist/catalog.js.map +1 -1
  24. package/dist/entitlements.d.ts +117 -0
  25. package/dist/entitlements.d.ts.map +1 -0
  26. package/dist/entitlements.js +62 -0
  27. package/dist/entitlements.js.map +1 -0
  28. package/dist/extensions.d.ts +183 -0
  29. package/dist/extensions.d.ts.map +1 -0
  30. package/dist/extensions.js +67 -0
  31. package/dist/extensions.js.map +1 -0
  32. package/dist/finances.d.ts +294 -0
  33. package/dist/finances.d.ts.map +1 -0
  34. package/dist/finances.js +56 -0
  35. package/dist/finances.js.map +1 -0
  36. package/dist/findings.d.ts +92 -1
  37. package/dist/findings.d.ts.map +1 -1
  38. package/dist/findings.js +91 -0
  39. package/dist/findings.js.map +1 -1
  40. package/dist/host.d.ts +729 -4
  41. package/dist/host.d.ts.map +1 -1
  42. package/dist/host.js +54 -0
  43. package/dist/host.js.map +1 -1
  44. package/dist/imports.d.ts +104 -0
  45. package/dist/imports.d.ts.map +1 -0
  46. package/dist/imports.js +70 -0
  47. package/dist/imports.js.map +1 -0
  48. package/dist/index.d.ts +17 -7
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -6
  51. package/dist/index.js.map +1 -1
  52. package/dist/localized-text.d.ts +18 -0
  53. package/dist/localized-text.d.ts.map +1 -0
  54. package/dist/localized-text.js +17 -0
  55. package/dist/localized-text.js.map +1 -0
  56. package/dist/manifest.d.ts +859 -0
  57. package/dist/manifest.d.ts.map +1 -1
  58. package/dist/manifest.js +957 -12
  59. package/dist/manifest.js.map +1 -1
  60. package/dist/package-check.d.ts.map +1 -1
  61. package/dist/package-check.js +9 -1
  62. package/dist/package-check.js.map +1 -1
  63. package/dist/permissions.d.ts +170 -11
  64. package/dist/permissions.d.ts.map +1 -1
  65. package/dist/permissions.js +210 -2
  66. package/dist/permissions.js.map +1 -1
  67. package/dist/recipients.d.ts +139 -0
  68. package/dist/recipients.d.ts.map +1 -0
  69. package/dist/recipients.js +223 -0
  70. package/dist/recipients.js.map +1 -0
  71. package/dist/records.d.ts +130 -0
  72. package/dist/records.d.ts.map +1 -0
  73. package/dist/records.js +85 -0
  74. package/dist/records.js.map +1 -0
  75. package/dist/require-capability.d.ts +37 -0
  76. package/dist/require-capability.d.ts.map +1 -0
  77. package/dist/require-capability.js +88 -0
  78. package/dist/require-capability.js.map +1 -0
  79. package/dist/settings-schema.d.ts +35 -0
  80. package/dist/settings-schema.d.ts.map +1 -1
  81. package/dist/settings-schema.js +55 -2
  82. package/dist/settings-schema.js.map +1 -1
  83. package/package.json +4 -4
  84. package/src/abort-signal.ts +22 -0
  85. package/src/action-refusal.ts +178 -0
  86. package/src/action-schema.ts +4 -1
  87. package/src/actions.ts +103 -6
  88. package/src/blocks.ts +289 -0
  89. package/src/catalog.ts +630 -72
  90. package/src/entitlements.ts +150 -0
  91. package/src/extensions.ts +212 -0
  92. package/src/finances.ts +317 -0
  93. package/src/findings.ts +94 -0
  94. package/src/host.ts +768 -4
  95. package/src/imports.ts +135 -0
  96. package/src/index.ts +209 -0
  97. package/src/localized-text.ts +19 -0
  98. package/src/manifest.ts +1231 -12
  99. package/src/package-check.ts +15 -0
  100. package/src/permissions.ts +223 -3
  101. package/src/recipients.ts +294 -0
  102. package/src/records.ts +172 -0
  103. package/src/require-capability.ts +106 -0
  104. package/src/settings-schema.ts +64 -2
package/src/host.ts CHANGED
@@ -1,5 +1,14 @@
1
- import type { PluginActions } from "./actions.ts";
2
- import type { PluginPermission } from "./permissions.ts";
1
+ import type { ActionRequest, PluginActions } from "./actions.ts";
2
+ import type { PluginBlocks } from "./blocks.ts";
3
+ import type { PluginExtensions } from "./extensions.ts";
4
+ import type { PluginEntitlements } from "./entitlements.ts";
5
+ import type { PluginFinances } from "./finances.ts";
6
+ import type { PluginImports } from "./imports.ts";
7
+ import type {
8
+ PluginPermission,
9
+ PluginPersonalDataCategory,
10
+ } from "./permissions.ts";
11
+ import type { PluginRecords } from "./records.ts";
3
12
  import type { PluginSettingsValues } from "./settings-schema.ts";
4
13
 
5
14
  /**
@@ -47,8 +56,26 @@ export interface PluginSettings {
47
56
  * The stored values, with declared defaults applied. Reads the current
48
57
  * values on each call rather than caching, so a change made in the admin
49
58
  * interface reaches a long-running job without a restart.
59
+ *
60
+ * A secret field is in it in clear, which is the one place its value
61
+ * leaves the host: never write it to the log, a job's payload or anything
62
+ * a route returns.
50
63
  */
51
64
  read(): Promise<PluginSettingsValues>;
65
+ /**
66
+ * Replaces or clears one of the plugin's own secret fields: `null` clears
67
+ * it.
68
+ *
69
+ * For a credential the far end replaces, such as an OAuth refresh token
70
+ * that is new after every use. Only a field the manifest declares as
71
+ * `secret`, checked against its declared bounds; the change is written to
72
+ * the audit log under the field's key, never with its value.
73
+ *
74
+ * Added after this contract version shipped, like `actions`: a plugin built
75
+ * against a newer host than the one it is installed on must feature-detect
76
+ * before using it.
77
+ */
78
+ writeSecret(key: string, value: string | null): Promise<void>;
52
79
  }
53
80
 
54
81
  export interface PluginMailMessage {
@@ -147,6 +174,12 @@ export interface PluginApartment {
147
174
  export interface PluginResident {
148
175
  personId: string;
149
176
  name: string;
177
+ /**
178
+ * The family name alone, for a reader that shows no first names: a household
179
+ * as the name on the door shows it, without the first names that say who
180
+ * lives alone and name the children.
181
+ */
182
+ lastName: string;
150
183
  /** MEMBER holds the tenant-ownership; RESIDENT occupies without holding it. */
151
184
  role: "MEMBER" | "RESIDENT";
152
185
  apartment: PluginApartment | null;
@@ -156,6 +189,22 @@ export interface PluginResident {
156
189
  email?: string | null;
157
190
  /** Present only with the addressBook:readContact permission. */
158
191
  phone?: string | null;
192
+ /**
193
+ * Whether a publication consent to the person's name on screens inside the
194
+ * building stands (the `NAME_IN_BUILDING` scope), recorded by the board on
195
+ * the person's panel like every other scope. A different consent from a name
196
+ * on the website, because the two are different things to agree to.
197
+ */
198
+ nameInBuilding: boolean;
199
+ }
200
+
201
+ /** Narrows a read of the residents. */
202
+ export interface PluginResidentFilter {
203
+ /**
204
+ * Only the residents of this entrance, so a plugin that shows one entrance
205
+ * never loads the others. An id no address has returns nobody.
206
+ */
207
+ addressId?: string;
159
208
  }
160
209
 
161
210
  export interface PluginOccupancySummary {
@@ -175,14 +224,631 @@ export interface PluginOccupancySummary {
175
224
  * personal identity number is never returned, and nothing is writable.
176
225
  * Requires addressBook:read; the contact fields additionally require
177
226
  * addressBook:readContact.
227
+ *
228
+ * A person who has objected to this plugin's processing, and whose objection
229
+ * the board granted, is absent as well (ADR 0035) - absent, not flagged, so a
230
+ * plugin cannot list who objected. Closing the objection brings the person
231
+ * back on the next read.
178
232
  */
179
233
  export interface PluginAddressBook {
180
234
  apartments(): Promise<PluginApartment[]>;
181
- /** Current residencies only. Moved-out rows are not plugin business. */
182
- residents(): Promise<PluginResident[]>;
235
+ /**
236
+ * Current residencies only. Moved-out rows are not plugin business.
237
+ *
238
+ * The filter was added after this contract version shipped; an older host
239
+ * ignores it and returns every entrance, so a plugin that must not load the
240
+ * others filters the result by `apartment.address.id` as well.
241
+ */
242
+ residents(filter?: PluginResidentFilter): Promise<PluginResident[]>;
183
243
  summary(): Promise<PluginOccupancySummary>;
184
244
  }
185
245
 
246
+ /** The bounds `host.audit.record` holds an entry to. */
247
+ export const PLUGIN_AUDIT_LIMITS = {
248
+ /** Characters in `event`, which is also part of a text key. */
249
+ event: 64,
250
+ /** Characters in `targetKind`. */
251
+ targetKind: 64,
252
+ /** Characters in `targetId`. */
253
+ targetId: 200,
254
+ /** Values in `detail`. */
255
+ detailValues: 8,
256
+ /** Characters in a key of `detail`, letters and digits. */
257
+ detailKey: 64,
258
+ /** Characters in a string value of `detail`. */
259
+ detailValue: 200,
260
+ } as const;
261
+
262
+ /** One entry a plugin writes to the instance's audit log. */
263
+ export interface PluginAuditEntry {
264
+ /**
265
+ * What happened, as lowercase letters, digits, dots and hyphens, starting
266
+ * with a letter: `screen.residents-enabled`. The audit log shows it through
267
+ * the plugin's own text key `audit.<event>`.
268
+ */
269
+ event: string;
270
+ /** What the act was done to, in the plugin's own words: `screen`. */
271
+ targetKind?: string;
272
+ /** Which one: the screen's id. */
273
+ targetId?: string;
274
+ /**
275
+ * At most eight values about the act, never a copy of what the plugin read.
276
+ * A key is letters and digits, starting with a letter: `screenCount`. A
277
+ * person is named as the actor, never here, and a key or value holding a
278
+ * personal identity number is refused.
279
+ */
280
+ detail?: Record<string, string | number | boolean>;
281
+ }
282
+
283
+ /**
284
+ * Writing to the instance's audit log (ADR 0035).
285
+ *
286
+ * The entry is written as `PLUGIN_RECORDED`, with the plugin's id and the
287
+ * event, on the plugin channel. With a request, the actor is the person the
288
+ * host marked on it, as for `actions`; with `null`, the entry has no actor - a
289
+ * plugin's own job, say. An entry that breaks a bound is refused with a
290
+ * RangeError and nothing is written. Needs no permission: an entry is about
291
+ * what the plugin did, which the association is entitled to a record of.
292
+ */
293
+ export interface PluginAudit {
294
+ record(request: ActionRequest | null, entry: PluginAuditEntry): Promise<void>;
295
+ }
296
+
297
+ /** What a theme install is told about the package besides its bytes. */
298
+ export interface PluginThemeInstallOptions {
299
+ /**
300
+ * The package's sha512, as whoever published it stated it: `sha512-<base64>`
301
+ * or 128 hex characters, the two spellings a catalog entry may use. The
302
+ * bytes are checked against it before anything is unpacked.
303
+ */
304
+ sha512: string;
305
+ }
306
+
307
+ /**
308
+ * A finding the install lint reported without refusing the theme: a token or
309
+ * a manifest field this core does not know, which the token contract says to
310
+ * ignore rather than refuse. Rule codes and numbers, never prose.
311
+ */
312
+ export interface PluginThemeInstallWarning {
313
+ rule: string;
314
+ detail: Readonly<Record<string, string | number | boolean>>;
315
+ }
316
+
317
+ export interface PluginThemeInstallResult {
318
+ /** The theme's id, from its manifest. */
319
+ id: string;
320
+ version: string;
321
+ warnings: readonly PluginThemeInstallWarning[];
322
+ }
323
+
324
+ /** A theme this plugin installed, as `host.themes.list` returns it. */
325
+ export interface PluginInstalledTheme {
326
+ id: string;
327
+ version: string;
328
+ /** ISO 8601. When the version now installed was installed first. */
329
+ installedAt: string;
330
+ }
331
+
332
+ /**
333
+ * Installing themes on the instance (ADR 0046). Requires themes:install.
334
+ *
335
+ * A theme a plugin hands over takes the path a theme from the catalog takes:
336
+ * its sha512 checked before anything is unpacked, the package read and held to
337
+ * the same install lint, the statutory register contrast included, and the
338
+ * files and the row written together under the same lock per theme id. A
339
+ * package the lint refuses is refused here with the same findings, and what
340
+ * the instance already had stays as it was.
341
+ *
342
+ * `install` takes the request the plugin's own route received and installs as
343
+ * the person behind it, who must hold association:manage, the capability the
344
+ * theme screen requires. A request a connected app's token reached is refused,
345
+ * and a background job has no request, so a plugin installs a theme only when
346
+ * an administrator asks it to.
347
+ *
348
+ * A plugin installs over a theme it installed itself and over nothing else:
349
+ * not the catalog's, not one composed on the instance, not another plugin's.
350
+ * It installs and nothing else. Activating and removing stay on the theme
351
+ * screen, and a theme stays installed when the plugin is removed.
352
+ *
353
+ * A refusal is thrown as an error carrying a `reason` code and, for
354
+ * `lint-failed`, the `findings`. Let it propagate from a route and the client
355
+ * is answered with its status, reason and findings, as the theme screen is.
356
+ */
357
+ export interface PluginThemes {
358
+ install(
359
+ request: ActionRequest,
360
+ archive: Uint8Array,
361
+ options: PluginThemeInstallOptions,
362
+ ): Promise<PluginThemeInstallResult>;
363
+ /** The themes this plugin installed, so it can offer an update. */
364
+ list(): Promise<PluginInstalledTheme[]>;
365
+ }
366
+
367
+ /**
368
+ * Who a news item or an event is for: the members, or anyone at all.
369
+ *
370
+ * On every item the content services return, because the host cannot know
371
+ * where the plugin puts it. A screen inside the building may show a members'
372
+ * item and a public calendar feed should not; which of the two the plugin is
373
+ * doing is what its consent screen tells the board (ADR 0032).
374
+ */
375
+ export type PluginContentAudience = "anyone" | "members";
376
+
377
+ /** One published news item, as a plugin reads it. */
378
+ export interface PluginNewsItem {
379
+ readonly id: string;
380
+ readonly title: string;
381
+ /** The body's text, one string per paragraph or heading, in order. */
382
+ readonly paragraphs: readonly string[];
383
+ /**
384
+ * The pictures the body shows, in order.
385
+ *
386
+ * Empty while a news body holds prose only, which is all the core's news
387
+ * editor writes and all the website shows of one.
388
+ */
389
+ readonly pictures: readonly { mediaFileId: string; alt: string }[];
390
+ readonly audience: PluginContentAudience;
391
+ /** ISO 8601 instant. */
392
+ readonly publishedAt: string;
393
+ /**
394
+ * Whether the board marked the item "show on screens": that it may be shown
395
+ * on a screen in the building (ADR 0050). Off until the board sets it. A
396
+ * plugin that puts items where anybody passing can read them shows only the
397
+ * marked ones.
398
+ */
399
+ readonly onScreens: boolean;
400
+ }
401
+
402
+ /**
403
+ * The association's published news. Requires the news:read permission.
404
+ *
405
+ * What the website publishes and nothing more: a draft, an item taken down, a
406
+ * deleted one and one the board wrote to Styrelsen or Administratörer in the
407
+ * feed are absent. Never an author, a comment, a like or who has read an item.
408
+ *
409
+ * To a plugin that declares a device route, an item whose title, paragraphs or
410
+ * picture alt texts name somebody the register holds, one of their email
411
+ * addresses or phone numbers, or an apartment number is absent as well, and
412
+ * the read goes on to older items to make up the count, as far back as the
413
+ * newest 500 published items (ADR 0032).
414
+ */
415
+ export interface PluginNews {
416
+ /**
417
+ * Published items, newest first.
418
+ *
419
+ * `limit` is 1 to 50 and defaults to 50. `since` is an ISO 8601 instant and
420
+ * keeps the items published at or after it. `audience` keeps only the items
421
+ * for that audience; without it both are returned, each saying which it is.
422
+ * A value outside those bounds is refused with a RangeError.
423
+ */
424
+ published(options?: {
425
+ audience?: PluginContentAudience;
426
+ limit?: number;
427
+ since?: string;
428
+ }): Promise<readonly PluginNewsItem[]>;
429
+ }
430
+
431
+ /** One occurrence of a published event. */
432
+ export interface PluginOccurrence {
433
+ readonly eventId: string;
434
+ readonly occurrenceId: string;
435
+ /** The board's own words, as written and never translated. */
436
+ readonly title: string;
437
+ readonly description: string | null;
438
+ readonly category: string | null;
439
+ readonly location: string | null;
440
+ /** ISO 8601 instant. */
441
+ readonly startsAt: string;
442
+ /** ISO 8601 instant. */
443
+ readonly endsAt: string;
444
+ /** Whether the board has called this one occurrence off. */
445
+ readonly calledOff: boolean;
446
+ /**
447
+ * Places left when the event takes sign-ups with a limit, and null when it
448
+ * takes none or has no limit. Never who took them.
449
+ */
450
+ readonly placesLeft: number | null;
451
+ readonly audience: PluginContentAudience;
452
+ /**
453
+ * Whether the board marked the event, with all its occurrences, "show on
454
+ * screens" (ADR 0050), as a news item's mark.
455
+ */
456
+ readonly onScreens: boolean;
457
+ }
458
+
459
+ /**
460
+ * The notice of a general meeting the board has called.
461
+ *
462
+ * What the notice states (EFL 6 kap. 22 §): the time, the place, how to take
463
+ * part digitally and the matters on the agenda. Never who was summoned, who
464
+ * attended, the voting register or a proxy.
465
+ */
466
+ export interface PluginMeetingNotice {
467
+ readonly meetingId: string;
468
+ readonly kind: "ordinary" | "extraordinary";
469
+ /** ISO 8601 instant. */
470
+ readonly startsAt: string;
471
+ readonly place: string;
472
+ readonly digitalParticipation: string | null;
473
+ readonly agenda: readonly { position: number; title: string }[];
474
+ }
475
+
476
+ /**
477
+ * The association's calendar. Requires the calendar:read permission.
478
+ *
479
+ * Published events only: a series that is not published is absent, and an
480
+ * occurrence the board called off is present and says so.
481
+ *
482
+ * To a plugin that declares a device route, an event whose title,
483
+ * description, category or location names somebody the register holds or an
484
+ * apartment number is absent with every occurrence, and a notice whose place,
485
+ * digital participation or any agenda title does is absent whole (ADR 0032).
486
+ */
487
+ export interface PluginCalendar {
488
+ /**
489
+ * Occurrences starting in [from, to), soonest first.
490
+ *
491
+ * `from` and `to` are ISO 8601 instants at most 92 days apart, and
492
+ * `audience` works as it does for the news. A range that is not one is
493
+ * refused with a RangeError.
494
+ */
495
+ occurrences(range: {
496
+ from: string;
497
+ to: string;
498
+ audience?: PluginContentAudience;
499
+ }): Promise<readonly PluginOccurrence[]>;
500
+ /**
501
+ * Notices of the general meetings called and not yet held, soonest first.
502
+ *
503
+ * A notice summons the members, so every one of them is for the members.
504
+ */
505
+ meetingNotices(): Promise<readonly PluginMeetingNotice[]>;
506
+ }
507
+
508
+ /** A bookable resource the house offers, and how it is booked. */
509
+ export interface PluginBookableResource {
510
+ readonly id: string;
511
+ /** The board's own name for it, as written. */
512
+ readonly name: string;
513
+ readonly mode: "timeSlots" | "wholeDay" | "dateRange";
514
+ /** The slot length in the time-slot mode, and null otherwise. */
515
+ readonly slotMinutes: number | null;
516
+ /** Minutes after local midnight the resource opens, in the time-slot mode. */
517
+ readonly opensAtMinute: number | null;
518
+ /** Minutes after local midnight the resource closes, in the time-slot mode. */
519
+ readonly closesAtMinute: number | null;
520
+ }
521
+
522
+ /** A period a resource is taken. Never by whom. */
523
+ export interface PluginBusyInterval {
524
+ readonly resourceId: string;
525
+ /** ISO 8601 instant. */
526
+ readonly start: string;
527
+ /** ISO 8601 instant. */
528
+ readonly end: string;
529
+ }
530
+
531
+ /**
532
+ * Free and busy for the house's bookable resources. Requires the
533
+ * bookings:readAvailability permission.
534
+ *
535
+ * Less than a resident's own booking screen shows: when a resource is taken,
536
+ * and nothing about who took it - no booker, no apartment and no booking's
537
+ * identifier.
538
+ *
539
+ * To a plugin that declares a device route, a resource whose name names
540
+ * somebody the register holds or an apartment number is absent from both
541
+ * reads, and `busy` starts no earlier than the moment of the call, so no
542
+ * booking that has ended is in it (ADR 0032).
543
+ */
544
+ export interface PluginBookings {
545
+ /** The resources open for booking, by name. A withdrawn one is absent. */
546
+ resources(): Promise<readonly PluginBookableResource[]>;
547
+ /**
548
+ * When each resource is taken in [from, to), per resource and in order.
549
+ *
550
+ * `from` and `to` are ISO 8601 instants at most 31 days apart, refused with
551
+ * a RangeError otherwise. Bookings that overlap or touch are merged into one
552
+ * interval, so a run of slots cannot be read as one household's habits. A
553
+ * resource id that is not an offered resource's answers nothing.
554
+ */
555
+ busy(range: {
556
+ from: string;
557
+ to: string;
558
+ resourceIds?: readonly string[];
559
+ }): Promise<readonly PluginBusyInterval[]>;
560
+ }
561
+
562
+ /**
563
+ * The bytes of a picture, as a web stream of `Uint8Array` chunks.
564
+ *
565
+ * The host hands a real `ReadableStream`. This package compiles against the
566
+ * language alone and cannot name the platform's type, so it is looked up in the
567
+ * plugin's own program, as the block's abort signal is: where that has a
568
+ * `ReadableStream`, from the DOM library or from Node's types, this is it, and
569
+ * the stream can go into a `Response` or `Readable.fromWeb` as it is. Where it
570
+ * has none, this is the part of one a plugin can read without them.
571
+ */
572
+ export type PluginMediaStream = typeof globalThis extends {
573
+ readonly ReadableStream: { readonly prototype: infer Stream };
574
+ }
575
+ ? Stream
576
+ : PluginMediaStreamShape;
577
+
578
+ /** What every `ReadableStream` has, for a program without the platform's types. */
579
+ interface PluginMediaStreamShape {
580
+ readonly locked: boolean;
581
+ cancel(reason?: unknown): Promise<void>;
582
+ getReader(): {
583
+ read(): Promise<
584
+ { done: false; value: Uint8Array } | { done: true; value?: undefined }
585
+ >;
586
+ cancel(reason?: unknown): Promise<void>;
587
+ releaseLock(): void;
588
+ };
589
+ }
590
+
591
+ /**
592
+ * A stored picture the plugin opened.
593
+ *
594
+ * What a reply serving it needs and nothing else: not the name it was uploaded
595
+ * under, which is somebody's own words, and not who uploaded it.
596
+ */
597
+ export interface PluginMediaFile {
598
+ /** Identified from the bytes when they were uploaded: an image type. */
599
+ readonly contentType: string;
600
+ /** The size of the picture as uploaded, in bytes. */
601
+ readonly byteSize: number;
602
+ /**
603
+ * A keyed hash of the bytes, hex encoded. The same value the core serves the
604
+ * picture's entity tag from, so a plugin may use it as one too.
605
+ */
606
+ readonly checksum: string;
607
+ /**
608
+ * Who the picture is for: `anyone`, or the `members`, as the board held it.
609
+ * The host does not filter for you, because it cannot know where you put it;
610
+ * see {@link PluginContentAudience}.
611
+ */
612
+ readonly audience: PluginContentAudience;
613
+ /**
614
+ * The picture's bytes. Read it to the end or cancel it: the host holds the
615
+ * stored file open until one or the other.
616
+ */
617
+ readonly stream: PluginMediaStream;
618
+ }
619
+
620
+ /**
621
+ * The association's stored pictures. Requires the media:read permission.
622
+ *
623
+ * Only a picture held for anyone or for the members whose upload declared that
624
+ * it shows no identifiable person (ADR 0032). A photograph of people needs a
625
+ * publication consent, and the consent the core records is for the website,
626
+ * not for wherever a plugin puts the picture. Nothing here lists or finds a
627
+ * picture: a plugin opens one whose id it already holds, such as one the board
628
+ * picked in the plugin's own view.
629
+ */
630
+ export interface PluginMedia {
631
+ /**
632
+ * Opens one picture.
633
+ *
634
+ * Null when the file does not exist and when it is not one a plugin may
635
+ * read, the two alike, so the answer says nothing about a file the plugin
636
+ * may not see. A `mediaFileId` that is not a string is refused with a
637
+ * RangeError.
638
+ */
639
+ open(mediaFileId: string): Promise<PluginMediaFile | null>;
640
+ }
641
+
642
+ /** One of the association's addresses: an entrance, and nobody behind it. */
643
+ export interface PluginAddress {
644
+ readonly id: string;
645
+ readonly street: string;
646
+ /** The street number, as written: "12", "12 B". */
647
+ readonly number: string;
648
+ }
649
+
650
+ /**
651
+ * The association's addresses. Requires the addresses:read permission.
652
+ *
653
+ * A fact about the building: never an apartment, a resident or a member, which
654
+ * is what addressBook:read is for.
655
+ */
656
+ export interface PluginAddresses {
657
+ /** Every address, in the order the core's own screens show them. */
658
+ list(): Promise<readonly PluginAddress[]>;
659
+ }
660
+
661
+ /**
662
+ * What a request to a recipient sends as personal data, and about whom.
663
+ *
664
+ * The host writes one audit entry per subject before anything is sent, and
665
+ * a second after, with the outcome; each reaches that person's data subject
666
+ * access report. A disclosure that names nobody, such as text a board member
667
+ * typed, is recorded once with no person.
668
+ */
669
+ export interface PluginDisclosure {
670
+ /**
671
+ * The categories the body carries. One or more, each among those the
672
+ * recipient's declaration says it receives, or nothing is sent.
673
+ */
674
+ personalData: readonly PluginPersonalDataCategory[];
675
+ /**
676
+ * The ids of the persons the body is about, at most 100. Each must be a
677
+ * person the host would hand the plugin: a person with protected personal
678
+ * data, one under a restriction of processing, or an id the register does
679
+ * not hold refuses the request, and the error does not say which.
680
+ */
681
+ subjects: readonly string[];
682
+ }
683
+
684
+ export interface PluginRecipientRequest {
685
+ /** `GET` when left out. */
686
+ method?: "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
687
+ /**
688
+ * Sent as given, apart from the ones the host writes: `host`,
689
+ * `content-length`, `transfer-encoding`, `connection` and the other
690
+ * hop-by-hop headers are refused.
691
+ */
692
+ headers?: Readonly<Record<string, string>>;
693
+ /** At most 16 MiB. */
694
+ body?: string | Uint8Array;
695
+ /**
696
+ * Required whenever the body carries personal data. A request without it
697
+ * is recorded nowhere, which is right for fetching a list of models and
698
+ * wrong for anything about a person.
699
+ */
700
+ disclosure?: PluginDisclosure;
701
+ /**
702
+ * The whole exchange, name lookup and redirects included. 60 seconds when
703
+ * left out, at most 300.
704
+ */
705
+ timeoutMs?: number;
706
+ }
707
+
708
+ export interface PluginRecipientResponse {
709
+ /** Whatever the recipient answered, 4xx and 5xx included. */
710
+ status: number;
711
+ /** Lowercased names; a repeated header is joined with ", ". */
712
+ headers: Readonly<Record<string, string>>;
713
+ /** The whole body, at most 16 MiB. */
714
+ body: Uint8Array;
715
+ /** The body as UTF-8. */
716
+ text(): string;
717
+ /** The body parsed as JSON. */
718
+ json(): unknown;
719
+ }
720
+
721
+ /** Why `host.recipients.fetch` sent nothing, or got no answer. */
722
+ export const PLUGIN_RECIPIENT_ERROR_CODES = [
723
+ /** The manifest declares no recipient by that id. */
724
+ "recipient-not-declared",
725
+ /** The path is not a path on the recipient's origin. */
726
+ "path-refused",
727
+ /** The method, a header, the body or the timeout is not one the host sends. */
728
+ "request-refused",
729
+ /** The disclosure names a category the recipient is not declared to receive. */
730
+ "personal-data-not-declared",
731
+ /** The disclosure names a person the plugin may not send data about. */
732
+ "subject-refused",
733
+ /** The origin's host is not an address this instance may connect to. */
734
+ "address-refused",
735
+ /** The recipient redirected to another origin, or more than five times. */
736
+ "redirect-refused",
737
+ /** No answer: the connection failed or the deadline passed. */
738
+ "unreachable",
739
+ /** The answer's body was longer than 16 MiB. */
740
+ "response-too-large",
741
+ ] as const;
742
+
743
+ export type PluginRecipientErrorCode =
744
+ (typeof PLUGIN_RECIPIENT_ERROR_CODES)[number];
745
+
746
+ /**
747
+ * A request to a recipient the host refused or could not complete.
748
+ *
749
+ * The message never repeats the path, a header or the body: a plugin may put
750
+ * a resident's name in any of them, and the error ends up in the log.
751
+ */
752
+ export class PluginRecipientError extends Error {
753
+ constructor(
754
+ readonly pluginId: string,
755
+ readonly recipient: string,
756
+ readonly code: PluginRecipientErrorCode,
757
+ ) {
758
+ super(
759
+ `Plugin "${pluginId}" could not reach its recipient "${recipient}": ${code}.`,
760
+ );
761
+ this.name = "PluginRecipientError";
762
+ }
763
+ }
764
+
765
+ /**
766
+ * The way out to the places the manifest's `recipients` declares (ADR 0037).
767
+ *
768
+ * `fetch` reaches the declared origin of the recipient named and nothing
769
+ * else, through the instance's outbound address rules, and follows a
770
+ * redirect only within that origin. A request whose body carries personal
771
+ * data says so in `disclosure`, and the host records it per person before
772
+ * and after the send.
773
+ *
774
+ * Needs no permission: the declaration is what the board consented to.
775
+ * Added after this contract version shipped, like `actions`: a plugin built
776
+ * against a newer host than the one it is installed on must feature-detect
777
+ * before using it.
778
+ */
779
+ export interface PluginRecipients {
780
+ fetch(
781
+ id: string,
782
+ /** Starts with `/`, and may carry a query: `/v1/messages?beta=true`. */
783
+ path: string,
784
+ init?: PluginRecipientRequest,
785
+ ): Promise<PluginRecipientResponse>;
786
+ }
787
+
788
+ /**
789
+ * One device token, as the plugin and the board see it. Never the token.
790
+ */
791
+ export interface DeviceTokenSummary {
792
+ readonly id: string;
793
+ /** The id of the device route it was issued for. */
794
+ readonly route: string;
795
+ /** The plugin's own name for the device, such as its screen id. */
796
+ readonly subject: string;
797
+ /** The board's name for it, such as "Entrance B, ground floor". */
798
+ readonly label: string;
799
+ readonly issuedAt: string;
800
+ /** Updated at most once a minute while the device calls. */
801
+ readonly lastUsedAt: string | null;
802
+ readonly revokedAt: string | null;
803
+ }
804
+
805
+ /**
806
+ * Device tokens for the plugin's own device routes (ADR 0031).
807
+ *
808
+ * A device token reaches one device route of this plugin, names a device and
809
+ * never a person, and is cut at once when revoked. The plugin issues one only
810
+ * on a board member's request, and the board can revoke any of them from the
811
+ * core's own devices screen, whatever the plugin's screen says.
812
+ */
813
+ export interface PluginDevices {
814
+ /**
815
+ * Issues a device token, and returns it once. It is stored as a digest and
816
+ * cannot be read again.
817
+ *
818
+ * `request` is the one the plugin's own route received. It has to come from
819
+ * the web interface, not a connected app's token, from a person who holds
820
+ * `site:manage` at that moment; anything else is refused. `route` is the id
821
+ * of one of the plugin's declared device routes. `subject` is the plugin's
822
+ * handle on the device, 1 to 64 letters, digits, `.`, `_`, `:` or `-`, and
823
+ * `label` the board's name for it, at most 80 characters. A plugin holds at
824
+ * most 200 live tokens.
825
+ */
826
+ issue(
827
+ request: ActionRequest,
828
+ options: { route: string; subject: string; label: string },
829
+ ): Promise<{ token: string; device: DeviceTokenSummary }>;
830
+
831
+ /** The plugin's own tokens, newest first. Revoked ones included. */
832
+ list(filter?: {
833
+ route?: string;
834
+ subject?: string;
835
+ }): Promise<readonly DeviceTokenSummary[]>;
836
+
837
+ /**
838
+ * Revokes one of the plugin's own tokens, at once. True when it was live.
839
+ *
840
+ * Pass the request when a person asked for it, so the audit log names them;
841
+ * without one the revocation is recorded as the plugin's.
842
+ */
843
+ revoke(tokenId: string, request?: ActionRequest): Promise<boolean>;
844
+
845
+ /**
846
+ * Which token a request on one of the plugin's device routes came with.
847
+ * Throws for any other request.
848
+ */
849
+ current(request: object): DeviceTokenSummary;
850
+ }
851
+
186
852
  /**
187
853
  * The host object a plugin's module factory receives.
188
854
  *
@@ -219,6 +885,104 @@ export interface PluginHost {
219
885
  * finding with the message deliberately withheld.
220
886
  */
221
887
  actions: PluginActions;
888
+ /**
889
+ * The association's fee notices and charges, delivery receipts against them,
890
+ * and the events that say one was issued.
891
+ *
892
+ * Added after this contract version shipped, like `actions`: a plugin must
893
+ * feature-detect it (`host.finances === undefined`) before using it.
894
+ */
895
+ finances: PluginFinances;
896
+ /**
897
+ * The website blocks this plugin declared, and their render functions.
898
+ *
899
+ * Added after this contract version shipped, like `actions`: a plugin built
900
+ * against a newer host than the one it is installed on must feature-detect
901
+ * before using it.
902
+ */
903
+ blocks: PluginBlocks;
904
+ /**
905
+ * The instance's audit log, for what this plugin did.
906
+ *
907
+ * Added after this contract version shipped, like `actions`: a plugin built
908
+ * against a newer host than the one it is installed on must feature-detect
909
+ * before using it.
910
+ */
911
+ audit: PluginAudit;
912
+ /**
913
+ * The extension points this plugin offers or extends (ADR 0030): a host
914
+ * plugin defines its points and calls the addons serving there, and an
915
+ * addon contributes its handler.
916
+ *
917
+ * Added after this contract version shipped, like `actions`: a plugin built
918
+ * against a newer host than the one it is installed on must feature-detect
919
+ * before using it.
920
+ */
921
+ extensions: PluginExtensions;
922
+ /**
923
+ * The association's published content: its news, its calendar, when its
924
+ * bookable resources are taken, its pictures and its addresses (ADR 0032).
925
+ *
926
+ * Added after this contract version shipped, like `actions`: a plugin built
927
+ * against a newer host than the one it is installed on must feature-detect
928
+ * before using them.
929
+ */
930
+ news: PluginNews;
931
+ calendar: PluginCalendar;
932
+ bookings: PluginBookings;
933
+ media: PluginMedia;
934
+ addresses: PluginAddresses;
935
+ /**
936
+ * The way out to the recipients this plugin declared.
937
+ *
938
+ * Added after this contract version shipped, like `actions`: a plugin built
939
+ * against a newer host than the one it is installed on must feature-detect
940
+ * before using it.
941
+ */
942
+ recipients: PluginRecipients;
943
+ /**
944
+ * The plugin's own records: configuration it keeps under a collection and a
945
+ * key, and never anything about a person (ADR 0033).
946
+ *
947
+ * Added after this contract version shipped, like `actions`: a plugin built
948
+ * against a newer host than the one it is installed on must feature-detect
949
+ * before using it.
950
+ */
951
+ records: PluginRecords;
952
+ /**
953
+ * Device tokens for the plugin's declared device routes.
954
+ *
955
+ * Added after this contract version shipped, like `actions`: a plugin built
956
+ * against a newer host than the one it is installed on must feature-detect
957
+ * before using it.
958
+ */
959
+ devices: PluginDevices;
960
+ /**
961
+ * Installing themes, and listing the ones this plugin installed.
962
+ *
963
+ * Added after this contract version shipped. An older host refuses a
964
+ * manifest asking for themes:install before it loads the plugin, so a plugin
965
+ * that declares the permission never meets a host without this member.
966
+ */
967
+ themes: PluginThemes;
968
+ /**
969
+ * Handing a member list to the board for review (ADR 0043).
970
+ *
971
+ * Added after this contract version shipped. An older host refuses a
972
+ * manifest asking for addressBook:proposeImport before it loads the plugin,
973
+ * so a plugin that declares the permission never meets a host without this
974
+ * member; checking for it is a defensive guard only.
975
+ */
976
+ imports: PluginImports;
977
+ /**
978
+ * The plugin's licence, judged by the host (ADR 0042).
979
+ *
980
+ * Present only on a plugin whose manifest declares a `licence`, and added
981
+ * after this contract version shipped, so a plugin feature-detects it as it
982
+ * does `actions`. A paid plugin on a host without it has no way to tell a
983
+ * licensed instance from an unlicensed one, and should stay dormant.
984
+ */
985
+ entitlements?: PluginEntitlements;
222
986
  }
223
987
 
224
988
  /**