@openbrf/plugin-sdk 0.0.0-stage → 1.2.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/LICENSE +661 -0
- package/LICENSE-EXCEPTION.md +28 -0
- package/README.md +94 -2
- package/dist/abort-signal.d.ts +23 -0
- package/dist/abort-signal.d.ts.map +1 -0
- package/dist/abort-signal.js +2 -0
- package/dist/abort-signal.js.map +1 -0
- package/dist/action-refusal.d.ts +101 -0
- package/dist/action-refusal.d.ts.map +1 -0
- package/dist/action-refusal.js +96 -0
- package/dist/action-refusal.js.map +1 -0
- package/dist/action-schema.d.ts +42 -0
- package/dist/action-schema.d.ts.map +1 -0
- package/dist/action-schema.js +67 -0
- package/dist/action-schema.js.map +1 -0
- package/dist/actions.d.ts +287 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +68 -0
- package/dist/actions.js.map +1 -0
- package/dist/api-version.d.ts +23 -0
- package/dist/api-version.d.ts.map +1 -0
- package/dist/api-version.js +27 -0
- package/dist/api-version.js.map +1 -0
- package/dist/blocks.d.ts +310 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +185 -0
- package/dist/blocks.js.map +1 -0
- package/dist/catalog.d.ts +707 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +567 -0
- package/dist/catalog.js.map +1 -0
- package/dist/define-plugin.d.ts +14 -0
- package/dist/define-plugin.d.ts.map +1 -0
- package/dist/define-plugin.js +15 -0
- package/dist/define-plugin.js.map +1 -0
- package/dist/entitlements.d.ts +117 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +62 -0
- package/dist/entitlements.js.map +1 -0
- package/dist/extensions.d.ts +183 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +67 -0
- package/dist/extensions.js.map +1 -0
- package/dist/finances.d.ts +294 -0
- package/dist/finances.d.ts.map +1 -0
- package/dist/finances.js +56 -0
- package/dist/finances.js.map +1 -0
- package/dist/findings.d.ts +156 -0
- package/dist/findings.d.ts.map +1 -0
- package/dist/findings.js +146 -0
- package/dist/findings.js.map +1 -0
- package/dist/host.d.ts +827 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +80 -0
- package/dist/host.js.map +1 -0
- package/dist/imports.d.ts +104 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +70 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity.d.ts +38 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +105 -0
- package/dist/integrity.js.map +1 -0
- package/dist/localized-text.d.ts +18 -0
- package/dist/localized-text.d.ts.map +1 -0
- package/dist/localized-text.js +17 -0
- package/dist/localized-text.js.map +1 -0
- package/dist/manifest.d.ts +1122 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +1118 -0
- package/dist/manifest.js.map +1 -0
- package/dist/module.d.ts +35 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +2 -0
- package/dist/module.js.map +1 -0
- package/dist/package-check.d.ts +66 -0
- package/dist/package-check.d.ts.map +1 -0
- package/dist/package-check.js +755 -0
- package/dist/package-check.js.map +1 -0
- package/dist/permissions.d.ts +224 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +277 -0
- package/dist/permissions.js.map +1 -0
- package/dist/recipients.d.ts +139 -0
- package/dist/recipients.d.ts.map +1 -0
- package/dist/recipients.js +223 -0
- package/dist/recipients.js.map +1 -0
- package/dist/records.d.ts +130 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +85 -0
- package/dist/records.js.map +1 -0
- package/dist/require-capability.d.ts +37 -0
- package/dist/require-capability.d.ts.map +1 -0
- package/dist/require-capability.js +88 -0
- package/dist/require-capability.js.map +1 -0
- package/dist/settings-schema.d.ts +130 -0
- package/dist/settings-schema.d.ts.map +1 -0
- package/dist/settings-schema.js +272 -0
- package/dist/settings-schema.js.map +1 -0
- package/dist/standard-schema.d.ts +43 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +2 -0
- package/dist/standard-schema.js.map +1 -0
- package/package.json +55 -3
- package/src/abort-signal.ts +22 -0
- package/src/action-refusal.ts +178 -0
- package/src/action-schema.ts +89 -0
- package/src/actions.ts +344 -0
- package/src/api-version.ts +28 -0
- package/src/blocks.ts +289 -0
- package/src/catalog.ts +679 -0
- package/src/define-plugin.ts +18 -0
- package/src/entitlements.ts +150 -0
- package/src/extensions.ts +212 -0
- package/src/finances.ts +317 -0
- package/src/findings.ts +163 -0
- package/src/host.ts +894 -0
- package/src/imports.ts +135 -0
- package/src/index.ts +316 -0
- package/src/integrity.ts +125 -0
- package/src/localized-text.ts +19 -0
- package/src/manifest.ts +1412 -0
- package/src/module.ts +38 -0
- package/src/package-check.ts +894 -0
- package/src/permissions.ts +295 -0
- package/src/recipients.ts +294 -0
- package/src/records.ts +172 -0
- package/src/require-capability.ts +106 -0
- package/src/settings-schema.ts +334 -0
- package/src/standard-schema.ts +39 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* An `https` origin: the scheme, the host and a port only where it is not 443.
|
|
4
|
+
*
|
|
5
|
+
* Written as the WHATWG URL parser writes an origin, so one place has one
|
|
6
|
+
* spelling: `https://API.example.com:443/` and `https://api.example.com` are the
|
|
7
|
+
* same recipient, and only the second is accepted. No path, because the board
|
|
8
|
+
* consents to a party and not to one of its endpoints; no credentials, because
|
|
9
|
+
* a manifest is published; and nothing but `https`, because what crosses the
|
|
10
|
+
* connection is the association's data. The host is a name: not an IP literal
|
|
11
|
+
* or `localhost`, which name no party the board can recognise, and not a name
|
|
12
|
+
* with a trailing dot, which is the same host under a second spelling.
|
|
13
|
+
*/
|
|
14
|
+
export declare const recipientOriginSchema: z.ZodString;
|
|
15
|
+
/** The host a recipient's origin names, which is what a board recognises. */
|
|
16
|
+
export declare function recipientHost(origin: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* The hosts of the declared recipients that receive personal data, joined.
|
|
19
|
+
*
|
|
20
|
+
* The answer a declaration gives to who receives the data (GDPR art.
|
|
21
|
+
* 30(1)(d)): what the consent screen fills its recipient field with and what
|
|
22
|
+
* the record of processing activities lists. A recipient receiving none is a
|
|
23
|
+
* place the plugin connects to, not a recipient in the sense of art. 4(9), so
|
|
24
|
+
* it is left out; an empty string when none receives any.
|
|
25
|
+
*/
|
|
26
|
+
export declare function receivingHosts(recipients: readonly Pick<PluginRecipient, "origin" | "personalData">[]): string;
|
|
27
|
+
/**
|
|
28
|
+
* The longest `receivingHosts` can be: every recipient the declaration may
|
|
29
|
+
* hold, each at the longest origin less its `https://`. A field the answer is
|
|
30
|
+
* written to has to take at least this, or a valid declaration fills in an
|
|
31
|
+
* answer the field then refuses.
|
|
32
|
+
*/
|
|
33
|
+
export declare const RECEIVING_HOSTS_MAX_LENGTH: number;
|
|
34
|
+
export declare const pluginRecipientSchema: z.ZodObject<{
|
|
35
|
+
id: z.ZodString;
|
|
36
|
+
origin: z.ZodString;
|
|
37
|
+
purpose: z.ZodObject<{
|
|
38
|
+
sv: z.ZodString;
|
|
39
|
+
en: z.ZodString;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
personalData: z.ZodArray<z.ZodEnum<{
|
|
42
|
+
account: "account";
|
|
43
|
+
apartment: "apartment";
|
|
44
|
+
auditTrail: "auditTrail";
|
|
45
|
+
email: "email";
|
|
46
|
+
financial: "financial";
|
|
47
|
+
freeText: "freeText";
|
|
48
|
+
health: "health";
|
|
49
|
+
name: "name";
|
|
50
|
+
personalIdentityNumber: "personalIdentityNumber";
|
|
51
|
+
phone: "phone";
|
|
52
|
+
photograph: "photograph";
|
|
53
|
+
postalAddress: "postalAddress";
|
|
54
|
+
protected: "protected";
|
|
55
|
+
residency: "residency";
|
|
56
|
+
}>>;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
export type PluginRecipient = z.infer<typeof pluginRecipientSchema>;
|
|
59
|
+
/**
|
|
60
|
+
* The declared recipients over a given reading of one recipient.
|
|
61
|
+
*
|
|
62
|
+
* The manifest reads a recipient as `pluginRecipientSchema` does and the
|
|
63
|
+
* catalog index reads the same fields strictly, as it reads everything. Both
|
|
64
|
+
* keep the rules below, which is why the list is built here rather than
|
|
65
|
+
* written twice.
|
|
66
|
+
*
|
|
67
|
+
* An id appears once and an origin appears once. Two declarations of one id
|
|
68
|
+
* would leave the consent snapshot and the board's screen naming different
|
|
69
|
+
* things by it; two of one origin would let a plugin state a narrow purpose
|
|
70
|
+
* and a wide one for the same party, and a board reading the first would have
|
|
71
|
+
* consented to the second.
|
|
72
|
+
*
|
|
73
|
+
* Undefaulted, on the terms of `declaredActionsSchema`: a manifest and a
|
|
74
|
+
* catalog entry add `.default([])`, and the install request does not, so an
|
|
75
|
+
* echo that left the field out is not read as consent to none.
|
|
76
|
+
*/
|
|
77
|
+
export declare function declaredRecipientsSchema<T extends z.ZodType<{
|
|
78
|
+
id: string;
|
|
79
|
+
origin: string;
|
|
80
|
+
}>>(recipient: T): z.ZodArray<T>;
|
|
81
|
+
export declare const pluginRecipientsSchema: z.ZodArray<z.ZodObject<{
|
|
82
|
+
id: z.ZodString;
|
|
83
|
+
origin: z.ZodString;
|
|
84
|
+
purpose: z.ZodObject<{
|
|
85
|
+
sv: z.ZodString;
|
|
86
|
+
en: z.ZodString;
|
|
87
|
+
}, z.core.$strict>;
|
|
88
|
+
personalData: z.ZodArray<z.ZodEnum<{
|
|
89
|
+
account: "account";
|
|
90
|
+
apartment: "apartment";
|
|
91
|
+
auditTrail: "auditTrail";
|
|
92
|
+
email: "email";
|
|
93
|
+
financial: "financial";
|
|
94
|
+
freeText: "freeText";
|
|
95
|
+
health: "health";
|
|
96
|
+
name: "name";
|
|
97
|
+
personalIdentityNumber: "personalIdentityNumber";
|
|
98
|
+
phone: "phone";
|
|
99
|
+
photograph: "photograph";
|
|
100
|
+
postalAddress: "postalAddress";
|
|
101
|
+
protected: "protected";
|
|
102
|
+
residency: "residency";
|
|
103
|
+
}>>;
|
|
104
|
+
}, z.core.$strip>>;
|
|
105
|
+
/**
|
|
106
|
+
* The canonical string a recipient is consented to and compared by.
|
|
107
|
+
*
|
|
108
|
+
* The id, the origin and the categories: what the plugin may reach and what
|
|
109
|
+
* goes there. The purpose is the plugin's words for it and is shown, not
|
|
110
|
+
* compared, so a republished version that rewords one is not refused for it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function canonicalRecipient(recipient: Pick<PluginRecipient, "id" | "origin" | "personalData">): string;
|
|
113
|
+
/**
|
|
114
|
+
* The declared recipients a consent given to `consented`, as
|
|
115
|
+
* `canonicalRecipient` wrote it, does not cover.
|
|
116
|
+
*
|
|
117
|
+
* A recipient is covered by a consented one with the same id and origin that
|
|
118
|
+
* received every category it now receives. Fewer categories are covered: the
|
|
119
|
+
* plugin's data reaching that party narrowed, and the board agreed to more.
|
|
120
|
+
* One more, a new id or an id moved to another origin is not.
|
|
121
|
+
*/
|
|
122
|
+
export declare function recipientsBeyondConsent<T extends Pick<PluginRecipient, "id" | "origin" | "personalData">>(declared: readonly T[], consented: readonly string[]): T[];
|
|
123
|
+
/** Whether two declarations name the same recipients, as multisets. */
|
|
124
|
+
export declare function sameRecipientDeclaration(left: readonly Pick<PluginRecipient, "id" | "origin" | "personalData">[], right: readonly Pick<PluginRecipient, "id" | "origin" | "personalData">[]): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* The recipients' categories that are not among the ones the plugin declares
|
|
127
|
+
* it handles, as issues at the path of each.
|
|
128
|
+
*
|
|
129
|
+
* What a recipient receives is part of what the plugin handles. One naming a
|
|
130
|
+
* category the plugin does not declare would put it on the consent screen, and
|
|
131
|
+
* into the record of who receives what, without the record of processing
|
|
132
|
+
* activities hearing of it. The manifest and the catalog entry repeat the same
|
|
133
|
+
* two fields, so both refuse it with this rule.
|
|
134
|
+
*/
|
|
135
|
+
export declare function recipientCategoryIssues(recipients: readonly Pick<PluginRecipient, "personalData">[], handled: readonly string[]): {
|
|
136
|
+
path: (string | number)[];
|
|
137
|
+
message: string;
|
|
138
|
+
}[];
|
|
139
|
+
//# sourceMappingURL=recipients.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipients.d.ts","sourceRoot":"","sources":["../src/recipients.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA4BxB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,aAO9B,CAAC;AAqDL,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,SAAS,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,EAAE,GACtE,MAAM,CAKR;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,QAEO,CAAC;AAE/C,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;iBAoBhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CACtC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,EACnD,SAAS,EAAE,CAAC,iBA0Bb;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;kBAElC,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,QAAQ,GAAG,cAAc,CAAC,GACjE,MAAM,CAMR;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,CAAC,SAAS,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,QAAQ,GAAG,cAAc,CAAC,EACjE,QAAQ,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,EAAE,CAiB3D;AAED,uEAAuE;AACvE,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,QAAQ,GAAG,cAAc,CAAC,EAAE,EACxE,KAAK,EAAE,SAAS,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,QAAQ,GAAG,cAAc,CAAC,EAAE,GACxE,OAAO,CAIT;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,SAAS,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,EAAE,EAC5D,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB;IAAE,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,CAclD"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { localizedTextSchema } from "./localized-text.js";
|
|
3
|
+
import { PLUGIN_PERSONAL_DATA_CATEGORIES } from "./permissions.js";
|
|
4
|
+
/**
|
|
5
|
+
* Where a plugin connects to outside the instance (ADR 0037).
|
|
6
|
+
*
|
|
7
|
+
* Declared in the manifest, and repeated in the catalog entry, for the reason
|
|
8
|
+
* actions are: the board reads it on the install consent screen before anything
|
|
9
|
+
* has been downloaded, and it is what the screen's question about where the
|
|
10
|
+
* plugin sends personal data is answered from.
|
|
11
|
+
*
|
|
12
|
+
* A declaration rather than a fence. A backend plugin runs at full process
|
|
13
|
+
* privilege (ADR 0003), so nothing here stops one opening a connection it did
|
|
14
|
+
* not declare; the catalog's reading of the server bundle is what holds a
|
|
15
|
+
* listed plugin to it.
|
|
16
|
+
*/
|
|
17
|
+
/** The longest origin a recipient may name. */
|
|
18
|
+
const RECIPIENT_ORIGIN_MAX_LENGTH = 200;
|
|
19
|
+
/** How many recipients a plugin may declare. */
|
|
20
|
+
const MAX_RECIPIENTS = 8;
|
|
21
|
+
/** What the joined hosts separate them with. */
|
|
22
|
+
const HOST_SEPARATOR = ", ";
|
|
23
|
+
/**
|
|
24
|
+
* An `https` origin: the scheme, the host and a port only where it is not 443.
|
|
25
|
+
*
|
|
26
|
+
* Written as the WHATWG URL parser writes an origin, so one place has one
|
|
27
|
+
* spelling: `https://API.example.com:443/` and `https://api.example.com` are the
|
|
28
|
+
* same recipient, and only the second is accepted. No path, because the board
|
|
29
|
+
* consents to a party and not to one of its endpoints; no credentials, because
|
|
30
|
+
* a manifest is published; and nothing but `https`, because what crosses the
|
|
31
|
+
* connection is the association's data. The host is a name: not an IP literal
|
|
32
|
+
* or `localhost`, which name no party the board can recognise, and not a name
|
|
33
|
+
* with a trailing dot, which is the same host under a second spelling.
|
|
34
|
+
*/
|
|
35
|
+
export const recipientOriginSchema = z
|
|
36
|
+
.string()
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(RECIPIENT_ORIGIN_MAX_LENGTH)
|
|
39
|
+
.refine(isCanonicalHttpsOrigin, {
|
|
40
|
+
error: 'must be an https origin as a browser writes it, such as "https://api.example.com": lowercase, with no path, no trailing slash, no port 443, and a host name that is not an IP address, localhost or written with a trailing dot',
|
|
41
|
+
});
|
|
42
|
+
const WhatwgUrl = globalThis.URL;
|
|
43
|
+
function isCanonicalHttpsOrigin(value) {
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = new WhatwgUrl(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return (parsed.protocol === "https:" &&
|
|
52
|
+
parsed.username === "" &&
|
|
53
|
+
parsed.password === "" &&
|
|
54
|
+
parsed.origin === value &&
|
|
55
|
+
isNamedHost(parsed.hostname));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether a host is a registered-style name: not a trailing-dot spelling, an
|
|
59
|
+
* IPv6 literal, a dotted IPv4 address or `localhost`. The parser has already
|
|
60
|
+
* turned any other IPv4 spelling (`0x7f.1`, `2130706433`) into the dotted one.
|
|
61
|
+
*/
|
|
62
|
+
function isNamedHost(hostname) {
|
|
63
|
+
return !(hostname.endsWith(".") ||
|
|
64
|
+
hostname.startsWith("[") ||
|
|
65
|
+
/^\d+\.\d+\.\d+\.\d+$/.test(hostname) ||
|
|
66
|
+
hostname === "localhost" ||
|
|
67
|
+
hostname.endsWith(".localhost"));
|
|
68
|
+
}
|
|
69
|
+
/** The host a recipient's origin names, which is what a board recognises. */
|
|
70
|
+
export function recipientHost(origin) {
|
|
71
|
+
return new WhatwgUrl(origin).host;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The hosts of the declared recipients that receive personal data, joined.
|
|
75
|
+
*
|
|
76
|
+
* The answer a declaration gives to who receives the data (GDPR art.
|
|
77
|
+
* 30(1)(d)): what the consent screen fills its recipient field with and what
|
|
78
|
+
* the record of processing activities lists. A recipient receiving none is a
|
|
79
|
+
* place the plugin connects to, not a recipient in the sense of art. 4(9), so
|
|
80
|
+
* it is left out; an empty string when none receives any.
|
|
81
|
+
*/
|
|
82
|
+
export function receivingHosts(recipients) {
|
|
83
|
+
return recipients
|
|
84
|
+
.filter((recipient) => recipient.personalData.length > 0)
|
|
85
|
+
.map((recipient) => recipientHost(recipient.origin))
|
|
86
|
+
.join(HOST_SEPARATOR);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The longest `receivingHosts` can be: every recipient the declaration may
|
|
90
|
+
* hold, each at the longest origin less its `https://`. A field the answer is
|
|
91
|
+
* written to has to take at least this, or a valid declaration fills in an
|
|
92
|
+
* answer the field then refuses.
|
|
93
|
+
*/
|
|
94
|
+
export const RECEIVING_HOSTS_MAX_LENGTH = MAX_RECIPIENTS * (RECIPIENT_ORIGIN_MAX_LENGTH - "https://".length) +
|
|
95
|
+
(MAX_RECIPIENTS - 1) * HOST_SEPARATOR.length;
|
|
96
|
+
export const pluginRecipientSchema = z.object({
|
|
97
|
+
/** Unique in the manifest. Lowercase letters, digits and single hyphens. */
|
|
98
|
+
id: z
|
|
99
|
+
.string()
|
|
100
|
+
.min(2)
|
|
101
|
+
.max(32)
|
|
102
|
+
.regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "must be lowercase letters, digits and single hyphens, starting with a letter"),
|
|
103
|
+
origin: recipientOriginSchema,
|
|
104
|
+
/** What the plugin connects there for, in Swedish and English. */
|
|
105
|
+
purpose: localizedTextSchema,
|
|
106
|
+
/**
|
|
107
|
+
* The personal-data categories this recipient receives, from the plugin's
|
|
108
|
+
* own `personalData`. Empty for a connection that carries none, such as
|
|
109
|
+
* fetching a list of models or the day's exchange rates: it is declared all
|
|
110
|
+
* the same, because the board is shown every place the plugin connects to.
|
|
111
|
+
*/
|
|
112
|
+
personalData: z.array(z.enum(PLUGIN_PERSONAL_DATA_CATEGORIES)).max(16),
|
|
113
|
+
});
|
|
114
|
+
/**
|
|
115
|
+
* The declared recipients over a given reading of one recipient.
|
|
116
|
+
*
|
|
117
|
+
* The manifest reads a recipient as `pluginRecipientSchema` does and the
|
|
118
|
+
* catalog index reads the same fields strictly, as it reads everything. Both
|
|
119
|
+
* keep the rules below, which is why the list is built here rather than
|
|
120
|
+
* written twice.
|
|
121
|
+
*
|
|
122
|
+
* An id appears once and an origin appears once. Two declarations of one id
|
|
123
|
+
* would leave the consent snapshot and the board's screen naming different
|
|
124
|
+
* things by it; two of one origin would let a plugin state a narrow purpose
|
|
125
|
+
* and a wide one for the same party, and a board reading the first would have
|
|
126
|
+
* consented to the second.
|
|
127
|
+
*
|
|
128
|
+
* Undefaulted, on the terms of `declaredActionsSchema`: a manifest and a
|
|
129
|
+
* catalog entry add `.default([])`, and the install request does not, so an
|
|
130
|
+
* echo that left the field out is not read as consent to none.
|
|
131
|
+
*/
|
|
132
|
+
export function declaredRecipientsSchema(recipient) {
|
|
133
|
+
return z
|
|
134
|
+
.array(recipient)
|
|
135
|
+
.max(MAX_RECIPIENTS)
|
|
136
|
+
.superRefine((recipients, ctx) => {
|
|
137
|
+
const ids = new Set();
|
|
138
|
+
const origins = new Set();
|
|
139
|
+
for (const [index, declared] of recipients.entries()) {
|
|
140
|
+
if (ids.has(declared.id)) {
|
|
141
|
+
ctx.addIssue({
|
|
142
|
+
code: "custom",
|
|
143
|
+
path: [index, "id"],
|
|
144
|
+
message: `two recipients are declared with the id "${declared.id}"`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (origins.has(declared.origin)) {
|
|
148
|
+
ctx.addIssue({
|
|
149
|
+
code: "custom",
|
|
150
|
+
path: [index, "origin"],
|
|
151
|
+
message: `two recipients are declared with the origin "${declared.origin}"`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
ids.add(declared.id);
|
|
155
|
+
origins.add(declared.origin);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export const pluginRecipientsSchema = declaredRecipientsSchema(pluginRecipientSchema);
|
|
160
|
+
/**
|
|
161
|
+
* The canonical string a recipient is consented to and compared by.
|
|
162
|
+
*
|
|
163
|
+
* The id, the origin and the categories: what the plugin may reach and what
|
|
164
|
+
* goes there. The purpose is the plugin's words for it and is shown, not
|
|
165
|
+
* compared, so a republished version that rewords one is not refused for it.
|
|
166
|
+
*/
|
|
167
|
+
export function canonicalRecipient(recipient) {
|
|
168
|
+
return [
|
|
169
|
+
recipient.id,
|
|
170
|
+
recipient.origin,
|
|
171
|
+
[...recipient.personalData].sort().join("|"),
|
|
172
|
+
].join(" ");
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The declared recipients a consent given to `consented`, as
|
|
176
|
+
* `canonicalRecipient` wrote it, does not cover.
|
|
177
|
+
*
|
|
178
|
+
* A recipient is covered by a consented one with the same id and origin that
|
|
179
|
+
* received every category it now receives. Fewer categories are covered: the
|
|
180
|
+
* plugin's data reaching that party narrowed, and the board agreed to more.
|
|
181
|
+
* One more, a new id or an id moved to another origin is not.
|
|
182
|
+
*/
|
|
183
|
+
export function recipientsBeyondConsent(declared, consented) {
|
|
184
|
+
const agreed = new Map();
|
|
185
|
+
for (const canonical of consented) {
|
|
186
|
+
// Neither an id nor a canonical origin has a space in it.
|
|
187
|
+
const [id, origin, categories = ""] = canonical.split(" ");
|
|
188
|
+
agreed.set(`${id} ${origin}`, new Set(categories === "" ? [] : categories.split("|")));
|
|
189
|
+
}
|
|
190
|
+
return declared.filter((recipient) => {
|
|
191
|
+
const categories = agreed.get(`${recipient.id} ${recipient.origin}`);
|
|
192
|
+
return (categories === undefined ||
|
|
193
|
+
recipient.personalData.some((category) => !categories.has(category)));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** Whether two declarations name the same recipients, as multisets. */
|
|
197
|
+
export function sameRecipientDeclaration(left, right) {
|
|
198
|
+
const a = left.map(canonicalRecipient).sort();
|
|
199
|
+
const b = right.map(canonicalRecipient).sort();
|
|
200
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The recipients' categories that are not among the ones the plugin declares
|
|
204
|
+
* it handles, as issues at the path of each.
|
|
205
|
+
*
|
|
206
|
+
* What a recipient receives is part of what the plugin handles. One naming a
|
|
207
|
+
* category the plugin does not declare would put it on the consent screen, and
|
|
208
|
+
* into the record of who receives what, without the record of processing
|
|
209
|
+
* activities hearing of it. The manifest and the catalog entry repeat the same
|
|
210
|
+
* two fields, so both refuse it with this rule.
|
|
211
|
+
*/
|
|
212
|
+
export function recipientCategoryIssues(recipients, handled) {
|
|
213
|
+
const declared = new Set(handled);
|
|
214
|
+
return recipients.flatMap((recipient, index) => recipient.personalData.flatMap((category, at) => declared.has(category)
|
|
215
|
+
? []
|
|
216
|
+
: [
|
|
217
|
+
{
|
|
218
|
+
path: ["recipients", index, "personalData", at],
|
|
219
|
+
message: `"${category}" is not among the plugin's own personalData; a recipient receives only what the plugin declares it handles`,
|
|
220
|
+
},
|
|
221
|
+
]));
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=recipients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipients.js","sourceRoot":"","sources":["../src/recipients.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin's own records (ADR 0033).
|
|
3
|
+
*
|
|
4
|
+
* A small keyed store: JSON values under a collection and a key, belonging to
|
|
5
|
+
* the plugin and to nothing else. It is for what a plugin has of its own to
|
|
6
|
+
* remember and cannot fit into a settings form - a list of screens, each with
|
|
7
|
+
* its own layout and rotation - and never for people. The data subject access
|
|
8
|
+
* report, purging, erasure and restriction all read the core's own tables, and
|
|
9
|
+
* a plugin's JSON is opaque to every one of them, so a name, an email address
|
|
10
|
+
* or anything copied out of the register kept here would be data the
|
|
11
|
+
* association holds and cannot report, purge or restrict. The host cannot tell
|
|
12
|
+
* a name from a label, so the rule is declared rather than enforced, with one
|
|
13
|
+
* exception: a personal identity number is refused on every write.
|
|
14
|
+
*
|
|
15
|
+
* The errors below are thrown by the host's copy of this package. A plugin
|
|
16
|
+
* imports the SDK for its types only, so it tells them apart by `name` rather
|
|
17
|
+
* than with `instanceof`.
|
|
18
|
+
*/
|
|
19
|
+
/** A value JSON can carry. */
|
|
20
|
+
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
|
|
21
|
+
readonly [key: string]: JsonValue;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A collection: lowercase letters, digits and single hyphens, at most 32
|
|
25
|
+
* characters, starting and ending with a letter or a digit.
|
|
26
|
+
*/
|
|
27
|
+
export declare const PLUGIN_RECORD_COLLECTION_PATTERN: RegExp;
|
|
28
|
+
/** A key: 1 to 128 letters, digits, `.`, `_`, `:` or `-`. */
|
|
29
|
+
export declare const PLUGIN_RECORD_KEY_PATTERN: RegExp;
|
|
30
|
+
/** What one plugin may hold, and the most one call reads. */
|
|
31
|
+
export declare const PLUGIN_RECORD_LIMITS: {
|
|
32
|
+
/** Bytes of one value, serialised as JSON in UTF-8. */
|
|
33
|
+
readonly valueBytes: number;
|
|
34
|
+
/** Records per plugin, across every collection. */
|
|
35
|
+
readonly records: 10000;
|
|
36
|
+
/** Bytes per plugin, across every collection, counted as for one value. */
|
|
37
|
+
readonly totalBytes: number;
|
|
38
|
+
/** Records one `list` returns, and its default. */
|
|
39
|
+
readonly listLimit: 200;
|
|
40
|
+
/**
|
|
41
|
+
* Writes of one plugin the host holds at once, the one under way and those
|
|
42
|
+
* waiting for their turn. A write sent past it is refused, not held.
|
|
43
|
+
*/
|
|
44
|
+
readonly queuedWrites: 100;
|
|
45
|
+
};
|
|
46
|
+
export interface PluginRecord<Value extends JsonValue = JsonValue> {
|
|
47
|
+
readonly collection: string;
|
|
48
|
+
readonly key: string;
|
|
49
|
+
readonly value: Value;
|
|
50
|
+
/**
|
|
51
|
+
* A counter, 1 when the record is written first and one more on every write
|
|
52
|
+
* after that. Not `updatedAt`, because two writes inside one millisecond
|
|
53
|
+
* carry the same time.
|
|
54
|
+
*/
|
|
55
|
+
readonly revision: number;
|
|
56
|
+
/** When the record was last written, as an ISO 8601 instant. */
|
|
57
|
+
readonly updatedAt: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The plugin's own records. Needs no permission, and refuses while the plugin
|
|
61
|
+
* is switched off, as every host service does.
|
|
62
|
+
*
|
|
63
|
+
* A collection or a key outside its pattern is refused with a RangeError, and a
|
|
64
|
+
* value JSON cannot carry exactly - `undefined`, a function, `NaN`, a `Date`, a
|
|
65
|
+
* class instance, a lone surrogate or a NUL character - with a TypeError.
|
|
66
|
+
*/
|
|
67
|
+
export interface PluginRecords {
|
|
68
|
+
get<Value extends JsonValue>(collection: string, key: string): Promise<PluginRecord<Value> | null>;
|
|
69
|
+
/**
|
|
70
|
+
* In key order, from after `after` when it is given. At most 200 per call,
|
|
71
|
+
* which is also the default.
|
|
72
|
+
*/
|
|
73
|
+
list<Value extends JsonValue>(collection: string, options?: {
|
|
74
|
+
after?: string;
|
|
75
|
+
limit?: number;
|
|
76
|
+
}): Promise<readonly PluginRecord<Value>[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Writes a value. With `expectedRevision`, only over that revision, and
|
|
79
|
+
* `null` means only where there is no record yet; otherwise it throws
|
|
80
|
+
* PluginRecordConflictError.
|
|
81
|
+
*
|
|
82
|
+
* A value carrying a personal identity number, in a string, a property name
|
|
83
|
+
* or a number, is refused with PluginRecordIdentityNumberError. A value
|
|
84
|
+
* over 64 KiB, and a write that would take the plugin past 10 000 records
|
|
85
|
+
* or 16 MiB, is refused with PluginRecordLimitError, and so is any write,
|
|
86
|
+
* an overwrite too, sent while 100 of the plugin's writes are already held,
|
|
87
|
+
* the one under way and those waiting.
|
|
88
|
+
*/
|
|
89
|
+
put<Value extends JsonValue>(collection: string, key: string, value: Value, options?: {
|
|
90
|
+
expectedRevision?: number | null;
|
|
91
|
+
}): Promise<PluginRecord<Value>>;
|
|
92
|
+
/**
|
|
93
|
+
* True when there was a record to delete. With `expectedRevision`, only that
|
|
94
|
+
* revision is deleted, and another one throws PluginRecordConflictError; no
|
|
95
|
+
* record at all is still `false`.
|
|
96
|
+
*/
|
|
97
|
+
delete(collection: string, key: string, options?: {
|
|
98
|
+
expectedRevision?: number;
|
|
99
|
+
}): Promise<boolean>;
|
|
100
|
+
}
|
|
101
|
+
/** A write's `expectedRevision` did not match the record it found. */
|
|
102
|
+
export declare class PluginRecordConflictError extends Error {
|
|
103
|
+
readonly pluginId: string;
|
|
104
|
+
/** The revision the store holds, or null when it holds no record. */
|
|
105
|
+
readonly actualRevision: number | null;
|
|
106
|
+
constructor(pluginId: string,
|
|
107
|
+
/** The revision the store holds, or null when it holds no record. */
|
|
108
|
+
actualRevision: number | null);
|
|
109
|
+
}
|
|
110
|
+
export type PluginRecordLimit = "valueBytes" | "records" | "totalBytes" | "queuedWrites";
|
|
111
|
+
/**
|
|
112
|
+
* A write, or a delete queued behind the writes, was past one of
|
|
113
|
+
* {@link PLUGIN_RECORD_LIMITS}, and nothing was changed.
|
|
114
|
+
*/
|
|
115
|
+
export declare class PluginRecordLimitError extends Error {
|
|
116
|
+
readonly pluginId: string;
|
|
117
|
+
readonly limit: PluginRecordLimit;
|
|
118
|
+
constructor(pluginId: string, limit: PluginRecordLimit);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A write carried a personal identity number.
|
|
122
|
+
*
|
|
123
|
+
* Neither the collection, the key nor the number is in the message: the error
|
|
124
|
+
* reaches the log, and the number is what the refusal is keeping out of it.
|
|
125
|
+
*/
|
|
126
|
+
export declare class PluginRecordIdentityNumberError extends Error {
|
|
127
|
+
readonly pluginId: string;
|
|
128
|
+
constructor(pluginId: string);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"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"}
|
package/dist/records.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin's own records (ADR 0033).
|
|
3
|
+
*
|
|
4
|
+
* A small keyed store: JSON values under a collection and a key, belonging to
|
|
5
|
+
* the plugin and to nothing else. It is for what a plugin has of its own to
|
|
6
|
+
* remember and cannot fit into a settings form - a list of screens, each with
|
|
7
|
+
* its own layout and rotation - and never for people. The data subject access
|
|
8
|
+
* report, purging, erasure and restriction all read the core's own tables, and
|
|
9
|
+
* a plugin's JSON is opaque to every one of them, so a name, an email address
|
|
10
|
+
* or anything copied out of the register kept here would be data the
|
|
11
|
+
* association holds and cannot report, purge or restrict. The host cannot tell
|
|
12
|
+
* a name from a label, so the rule is declared rather than enforced, with one
|
|
13
|
+
* exception: a personal identity number is refused on every write.
|
|
14
|
+
*
|
|
15
|
+
* The errors below are thrown by the host's copy of this package. A plugin
|
|
16
|
+
* imports the SDK for its types only, so it tells them apart by `name` rather
|
|
17
|
+
* than with `instanceof`.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A collection: lowercase letters, digits and single hyphens, at most 32
|
|
21
|
+
* characters, starting and ending with a letter or a digit.
|
|
22
|
+
*/
|
|
23
|
+
export const PLUGIN_RECORD_COLLECTION_PATTERN = /^(?=.{1,32}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
24
|
+
/** A key: 1 to 128 letters, digits, `.`, `_`, `:` or `-`. */
|
|
25
|
+
export const PLUGIN_RECORD_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
26
|
+
/** What one plugin may hold, and the most one call reads. */
|
|
27
|
+
export const PLUGIN_RECORD_LIMITS = {
|
|
28
|
+
/** Bytes of one value, serialised as JSON in UTF-8. */
|
|
29
|
+
valueBytes: 64 * 1024,
|
|
30
|
+
/** Records per plugin, across every collection. */
|
|
31
|
+
records: 10_000,
|
|
32
|
+
/** Bytes per plugin, across every collection, counted as for one value. */
|
|
33
|
+
totalBytes: 16 * 1024 * 1024,
|
|
34
|
+
/** Records one `list` returns, and its default. */
|
|
35
|
+
listLimit: 200,
|
|
36
|
+
/**
|
|
37
|
+
* Writes of one plugin the host holds at once, the one under way and those
|
|
38
|
+
* waiting for their turn. A write sent past it is refused, not held.
|
|
39
|
+
*/
|
|
40
|
+
queuedWrites: 100,
|
|
41
|
+
};
|
|
42
|
+
/** A write's `expectedRevision` did not match the record it found. */
|
|
43
|
+
export class PluginRecordConflictError extends Error {
|
|
44
|
+
pluginId;
|
|
45
|
+
actualRevision;
|
|
46
|
+
constructor(pluginId,
|
|
47
|
+
/** The revision the store holds, or null when it holds no record. */
|
|
48
|
+
actualRevision) {
|
|
49
|
+
super(`Plugin "${pluginId}" wrote a record over a revision it no longer has.`);
|
|
50
|
+
this.pluginId = pluginId;
|
|
51
|
+
this.actualRevision = actualRevision;
|
|
52
|
+
this.name = "PluginRecordConflictError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A write, or a delete queued behind the writes, was past one of
|
|
57
|
+
* {@link PLUGIN_RECORD_LIMITS}, and nothing was changed.
|
|
58
|
+
*/
|
|
59
|
+
export class PluginRecordLimitError extends Error {
|
|
60
|
+
pluginId;
|
|
61
|
+
limit;
|
|
62
|
+
constructor(pluginId, limit) {
|
|
63
|
+
super(`Plugin "${pluginId}" was refused a record operation past its ${limit} ` +
|
|
64
|
+
`limit of ${String(PLUGIN_RECORD_LIMITS[limit])}.`);
|
|
65
|
+
this.pluginId = pluginId;
|
|
66
|
+
this.limit = limit;
|
|
67
|
+
this.name = "PluginRecordLimitError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A write carried a personal identity number.
|
|
72
|
+
*
|
|
73
|
+
* Neither the collection, the key nor the number is in the message: the error
|
|
74
|
+
* reaches the log, and the number is what the refusal is keeping out of it.
|
|
75
|
+
*/
|
|
76
|
+
export class PluginRecordIdentityNumberError extends Error {
|
|
77
|
+
pluginId;
|
|
78
|
+
constructor(pluginId) {
|
|
79
|
+
super(`Plugin "${pluginId}" wrote a record carrying a personal identity ` +
|
|
80
|
+
"number, which a plugin's records may not hold.");
|
|
81
|
+
this.pluginId = pluginId;
|
|
82
|
+
this.name = "PluginRecordIdentityNumberError";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.js","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAWH;;;GAGG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAC3C,wCAAwC,CAAC;AAE3C,6DAA6D;AAC7D,MAAM,CAAC,MAAM,yBAAyB,GAAG,0BAA0B,CAAC;AAEpE,6DAA6D;AAC7D,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,uDAAuD;IACvD,UAAU,EAAE,EAAE,GAAG,IAAI;IACrB,mDAAmD;IACnD,OAAO,EAAE,MAAM;IACf,2EAA2E;IAC3E,UAAU,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;IAC5B,mDAAmD;IACnD,SAAS,EAAE,GAAG;IACd;;;OAGG;IACH,YAAY,EAAE,GAAG;CACT,CAAC;AAsEX,sEAAsE;AACtE,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAEvC,QAAQ;IAER,cAAc;IAHzB,YACW,QAAgB;IACzB,qEAAqE;IAC5D,cAA6B;QAEtC,KAAK,CACH,WAAW,QAAQ,oDAAoD,CACxE,CAAC;wBANO,QAAQ;8BAER,cAAc;QAKvB,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAKD;;;GAGG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAEpC,QAAQ;IACR,KAAK;IAFhB,YACW,QAAgB,EAChB,KAAwB;QAEjC,KAAK,CACH,WAAW,QAAQ,6CAA6C,KAAK,GAAG;YACtE,YAAY,MAAM,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,GAAG,CACrD,CAAC;wBANO,QAAQ;qBACR,KAAK;QAMd,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,OAAO,+BAAgC,SAAQ,KAAK;IACnC,QAAQ;IAA7B,YAAqB,QAAgB;QACnC,KAAK,CACH,WAAW,QAAQ,gDAAgD;YACjE,gDAAgD,CACnD,CAAC;wBAJiB,QAAQ;QAK3B,IAAI,CAAC,IAAI,GAAG,iCAAiC,CAAC;IAChD,CAAC;CACF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raising a plugin route above the capability floor the host gives it.
|
|
3
|
+
*
|
|
4
|
+
* The host reads one metadata key off a controller and off each of its routes,
|
|
5
|
+
* and answers a request only for a person who holds every capability listed
|
|
6
|
+
* there. A plugin's controllers get a floor from the plugin's own permissions
|
|
7
|
+
* (see "Routes" in the plugin contract); `RequireCapability` is how a route
|
|
8
|
+
* asks for more.
|
|
9
|
+
*
|
|
10
|
+
* Written without `@nestjs/common` on purpose, for the reason `definePlugin` is
|
|
11
|
+
* an identity function: the SDK is bundled into the plugin, and an import of
|
|
12
|
+
* its own would have to be resolved from the data volume. NestJS's
|
|
13
|
+
* `SetMetadata` does no more than this, on the same global `Reflect` the host's
|
|
14
|
+
* copy of NestJS writes to.
|
|
15
|
+
*/
|
|
16
|
+
/** The metadata key the host reads a route's or a controller's capabilities from. */
|
|
17
|
+
export declare const REQUIRED_CAPABILITIES_KEY = "openbrf:required-capabilities";
|
|
18
|
+
/**
|
|
19
|
+
* Every capability name the host knows, which is what a route may require.
|
|
20
|
+
*
|
|
21
|
+
* Listed here, not derived, because the SDK cannot import the host. The host
|
|
22
|
+
* checks the list against its own capabilities in a test, so a capability
|
|
23
|
+
* added there is not missing here for long; until the SDK is released with it,
|
|
24
|
+
* a plugin that needs the new name is a type error rather than a typo caught
|
|
25
|
+
* at request time.
|
|
26
|
+
*/
|
|
27
|
+
export declare const PLUGIN_ROUTE_CAPABILITIES: readonly ["association:manage", "association:read", "addressBook:read", "addressBook:write", "memberRegister:read", "apartmentRegister:read", "protectedData:reveal", "registerReport:export", "invitation:send", "signupRequest:decide", "self:manage", "residentDirectory:read", "issues:handle", "issues:report", "issues:configure", "documents:manage", "site:manage", "news:comment", "boardPosition:manage", "bookings:book", "bookings:manage", "bookings:configure", "events:manage", "events:attend", "systemRole:manage", "motions:submit", "motions:handle", "meetings:manage", "memberCharges:manage", "fees:manage", "dataProtection:manage", "boardMailbox:handle", "sublets:apply", "sublets:handle", "keyOrders:place", "keyOrders:handle", "chat:participate", "chat:moderate", "apartmentBinder:manage"];
|
|
28
|
+
/** A capability name a plugin route may require. */
|
|
29
|
+
export type PluginRouteCapability = (typeof PLUGIN_ROUTE_CAPABILITIES)[number];
|
|
30
|
+
/**
|
|
31
|
+
* Declares the capabilities a route requires. Several are combined with AND.
|
|
32
|
+
*
|
|
33
|
+
* Applied to a controller it covers every route in it. It can only add to the
|
|
34
|
+
* floor the host gives the plugin: a route cannot require less.
|
|
35
|
+
*/
|
|
36
|
+
export declare function RequireCapability(...capabilities: PluginRouteCapability[]): MethodDecorator & ClassDecorator;
|
|
37
|
+
//# sourceMappingURL=require-capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-capability.d.ts","sourceRoot":"","sources":["../src/require-capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,kCAAkC,CAAC;AAEzE;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,YACpC,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,wBAAwB,CAChB,CAAC;AAEX,oDAAoD;AACpD,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAO/E;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,YAAY,EAAE,qBAAqB,EAAE,GACvC,eAAe,GAAG,cAAc,CAmBlC"}
|