@jwilleke/ngdpbase-types 5.42.0 → 5.44.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/lib/src/context/ApiContext.d.ts +10 -3
- package/lib/src/context/ApiContext.js +37 -4
- package/lib/src/context/BaseContext.d.ts +20 -20
- package/lib/src/context/BaseContext.js +41 -24
- package/lib/src/context/JobContext.d.ts +31 -0
- package/lib/src/context/JobContext.js +34 -1
- package/lib/src/ice/runExport.d.ts +8 -3
- package/lib/src/ice/searchExportSource.d.ts +44 -0
- package/lib/src/ice/types.d.ts +6 -0
- package/lib/src/managers/AddonsManager.d.ts +0 -28
- package/lib/src/managers/AgentTokenManager.d.ts +8 -0
- package/lib/src/managers/AttachmentManager.d.ts +11 -0
- package/lib/src/managers/AuditManager.d.ts +0 -1
- package/lib/src/managers/AuthManager.d.ts +38 -0
- package/lib/src/managers/AuthManager.js +84 -12
- package/lib/src/managers/BackupManager.d.ts +9 -2
- package/lib/src/managers/BackupManager.js +42 -19
- package/lib/src/managers/BaseManager.d.ts +17 -3
- package/lib/src/managers/BaseManager.js +12 -8
- package/lib/src/managers/ConfigurationManager.d.ts +28 -0
- package/lib/src/managers/ConsentManager.d.ts +1 -1
- package/lib/src/managers/DatabaseManager.js +17 -8
- package/lib/src/managers/EmailManager.d.ts +1 -1
- package/lib/src/managers/ExportManager.d.ts +1 -44
- package/lib/src/managers/ExportManager.js +3 -115
- package/lib/src/managers/ImportManager.d.ts +7 -0
- package/lib/src/managers/PageManager.d.ts +194 -23
- package/lib/src/managers/PolicyEvaluator.d.ts +182 -24
- package/lib/src/managers/ShareManager.d.ts +3 -2
- package/lib/src/managers/ThemeManager.d.ts +20 -1
- package/lib/src/managers/UserManager.d.ts +19 -5
- package/lib/src/managers/ValidationManager.d.ts +3 -1
- package/lib/src/providers/AgentTokenAuthProvider.js +1 -1
- package/lib/src/providers/BaseAuthProvider.d.ts +2 -0
- package/lib/src/providers/FileLoggingProvider.d.ts +12 -1
- package/lib/src/providers/FileLoggingProvider.js +37 -4
- package/lib/src/security/PolicyDecisionPoint.d.ts +107 -4
- package/lib/src/security/PolicyInformationPoint.d.ts +113 -7
- package/lib/src/security/ownedRecords.d.ts +69 -0
- package/lib/src/security/permissions.generated.d.ts +8 -0
- package/lib/src/security/signedOutWrites.d.ts +69 -0
- package/lib/src/security/signedOutWrites.js +146 -0
- package/lib/src/types/Page.d.ts +0 -7
- package/lib/src/types/Policy.d.ts +65 -8
- package/lib/src/types/Policy.js +6 -0
- package/lib/src/types/Provider.d.ts +12 -0
- package/lib/src/types/User.d.ts +6 -0
- package/lib/src/utils/addonConfigLayer.d.ts +6 -3
- package/lib/src/utils/addonConfigLayer.js +61 -3
- package/lib/src/utils/addonHost.d.ts +25 -0
- package/lib/src/utils/addonHost.js +29 -1
- package/lib/src/utils/addonPageSync.d.ts +0 -12
- package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
- package/lib/src/utils/auditEventNames.generated.js +2 -0
- package/lib/src/utils/auditEvents.d.ts +2 -0
- package/lib/src/utils/auditEvents.js +3 -1
- package/lib/src/utils/availability.d.ts +15 -0
- package/lib/src/utils/availability.js +88 -0
- package/lib/src/utils/frontmatterAccess.d.ts +15 -24
- package/lib/src/utils/legacyStoreKeys.d.ts +16 -0
- package/lib/src/utils/pageTitleConflict.d.ts +32 -0
- package/lib/src/utils/pageWritePolicies.d.ts +74 -0
- package/lib/src/utils/permissionCatalog.d.ts +2 -0
- package/lib/src/utils/permissionCatalog.js +19 -0
- package/lib/src/utils/plainAttention.d.ts +19 -0
- package/lib/src/utils/plainAttention.js +18 -0
- package/lib/src/utils/policyAttributes.d.ts +232 -0
- package/lib/src/utils/policyAttributes.js +404 -0
- package/lib/src/utils/privateStoreAccess.d.ts +24 -4
- package/lib/src/utils/privateStoreAccess.js +19 -4
- package/lib/src/utils/roleGrants.d.ts +1 -1
- package/lib/src/utils/username.d.ts +13 -2
- package/lib/src/utils/username.js +18 -3
- package/package.json +7 -7
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* again because a `.d.ts` may not import a `.ts`. Four copies of one shape is
|
|
13
13
|
* how a field gets added to one and missed in the others.
|
|
14
14
|
*/
|
|
15
|
-
/** Who a policy grants to. The evaluator matches `role` and `
|
|
15
|
+
/** Who a policy grants to. The evaluator matches `role`, `job` (#1999), `authenticated` and `anyone` (#2261), its `SUBJECT_MATCHERS`. */
|
|
16
16
|
export interface PolicySubject {
|
|
17
|
-
/** `role`,
|
|
17
|
+
/** `role`, `job` for a scheduled job acting as itself (#1999), or `authenticated` / `anyone` (#2261). */
|
|
18
18
|
type: string;
|
|
19
|
-
|
|
19
|
+
/** The role or the job id. `authenticated` and `anyone` take none. */
|
|
20
|
+
value?: string;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* What a policy covers.
|
|
@@ -28,13 +29,48 @@ export interface PolicyResource {
|
|
|
28
29
|
type: string;
|
|
29
30
|
pattern: string;
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* An argument of a comparison (#2255, #2261, #2330), as XACML 3.0 writes one in
|
|
34
|
+
* lowercase. An attribute is `{ "attributeid": "subject:department-id",
|
|
35
|
+
* "datatype": "string" }`: `attributeid` starts `subject:`, `resource:` or
|
|
36
|
+
* `environment:`. A literal is `{ "value": "sales", "datatype": "string" }`,
|
|
37
|
+
* and for `X-is-in` its `value` may be an array. `datatype` is required and is
|
|
38
|
+
* one of the XACML JSON profile's short names.
|
|
39
|
+
*/
|
|
40
|
+
export type ConditionOperand = {
|
|
41
|
+
attributeid: string;
|
|
42
|
+
datatype: string;
|
|
43
|
+
} | {
|
|
44
|
+
value: unknown;
|
|
45
|
+
datatype: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A policy's condition (#2330): one function, named for its XACML 3.0 function
|
|
49
|
+
* in lowercase, whose value is the array of its arguments.
|
|
50
|
+
*
|
|
51
|
+
* - `string-equal`, `integer-equal`, `boolean-equal`, `string-is-in`,
|
|
52
|
+
* `integer-is-in`, `integer-greater-than`, `integer-greater-than-or-equal`,
|
|
53
|
+
* `integer-less-than`, `integer-less-than-or-equal` take two
|
|
54
|
+
* {@link ConditionOperand}s;
|
|
55
|
+
* - `and` and `or` take a list of conditions, `not` takes one;
|
|
56
|
+
*
|
|
57
|
+
* nested at most 4 deep.
|
|
58
|
+
*
|
|
59
|
+
* Each node is true, false or unknown. A comparison reading an attribute that
|
|
60
|
+
* is missing is unknown; in `or` a true branch decides, in `and` a false one
|
|
61
|
+
* does, and `not` of unknown is unknown. An allow applies only when the
|
|
62
|
+
* condition is true; a deny applies when it is true or unknown.
|
|
63
|
+
*/
|
|
64
|
+
export type PolicyCondition = Record<string, Array<ConditionOperand | PolicyCondition>>;
|
|
65
|
+
/** The prefixes that mark a condition value as an attribute rather than a literal (#2255). */
|
|
66
|
+
export declare const ATTRIBUTE_CATEGORIES: readonly ["subject", "resource", "environment"];
|
|
67
|
+
/** True when `value` names an attribute (`subject:id`, `resource:owner`), not a literal. */
|
|
68
|
+
export declare function isAttributeName(value: unknown): value is string;
|
|
31
69
|
/** One entry of `ngdpbase.access.policies`. */
|
|
32
70
|
export interface Policy {
|
|
33
71
|
id: string;
|
|
34
72
|
name?: string;
|
|
35
73
|
description?: string;
|
|
36
|
-
/** Lower runs first, so a higher-priority deny lands last and wins. */
|
|
37
|
-
priority?: number;
|
|
38
74
|
/**
|
|
39
75
|
* `allow` or `deny`. Typed as a string because the value comes from
|
|
40
76
|
* configuration, where an operator can write anything; the evaluator treats
|
|
@@ -44,6 +80,8 @@ export interface Policy {
|
|
|
44
80
|
subjects?: PolicySubject[];
|
|
45
81
|
resources?: PolicyResource[];
|
|
46
82
|
actions?: string[];
|
|
83
|
+
/** Applies only when this holds (#2255); see {@link PolicyCondition}. */
|
|
84
|
+
condition?: PolicyCondition;
|
|
47
85
|
[key: string]: unknown;
|
|
48
86
|
}
|
|
49
87
|
/**
|
|
@@ -88,11 +126,18 @@ export interface DecisionRequest {
|
|
|
88
126
|
action: string;
|
|
89
127
|
resource?: DecisionResource;
|
|
90
128
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* `system-category
|
|
129
|
+
* Where a page or file sits, for the resource types that match on it: the
|
|
130
|
+
* `vault` (#1539) or the `shared-folder` (#2125). A page's own attributes —
|
|
131
|
+
* its `system-category`, its `owner` — are never passed: the PDP asks the
|
|
132
|
+
* PIP for them from the page id (#2255).
|
|
94
133
|
*/
|
|
95
134
|
attributes?: Record<string, unknown>;
|
|
135
|
+
/**
|
|
136
|
+
* The page's frontmatter when the PIP itself already holds it (the page
|
|
137
|
+
* door, the list filter), so the PDP's lookup reads no page twice. Only the
|
|
138
|
+
* PIP sets it; a route never does (#2255).
|
|
139
|
+
*/
|
|
140
|
+
pageMetadata?: Record<string, unknown> | null;
|
|
96
141
|
/**
|
|
97
142
|
* Whether a share's `resources` cover the thing being asked about.
|
|
98
143
|
*
|
|
@@ -128,6 +173,18 @@ export interface Decision {
|
|
|
128
173
|
*/
|
|
129
174
|
applicable: boolean;
|
|
130
175
|
reason: string;
|
|
176
|
+
/**
|
|
177
|
+
* The ids of the policies that decided (#2255): the denies that matched,
|
|
178
|
+
* or, when none did, the allows that matched. Empty when no policy matched
|
|
179
|
+
* or something other than policy decided (a ceiling, step-up).
|
|
180
|
+
*/
|
|
181
|
+
policyIds: string[];
|
|
182
|
+
/**
|
|
183
|
+
* The ids of allow policies that matched subject, resource and action but
|
|
184
|
+
* whose condition was false or unknown (#2288), so a refusal can say which
|
|
185
|
+
* allow was not met. Ids only, never attribute values. Absent when none.
|
|
186
|
+
*/
|
|
187
|
+
conditionNotMet?: string[];
|
|
131
188
|
}
|
|
132
189
|
/**
|
|
133
190
|
* The reason a deny carries when the subject holds the permission but its
|
package/lib/src/types/Policy.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
/** The prefixes that mark a condition value as an attribute rather than a literal (#2255). */
|
|
3
|
+
export const ATTRIBUTE_CATEGORIES = ['subject', 'resource', 'environment'];
|
|
4
|
+
/** True when `value` names an attribute (`subject:id`, `resource:owner`), not a literal. */
|
|
5
|
+
export function isAttributeName(value) {
|
|
6
|
+
return typeof value === 'string' && ATTRIBUTE_CATEGORIES.some((c) => value.startsWith(`${c}:`));
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
9
|
* The reason a deny carries when the subject holds the permission but its
|
|
4
10
|
* sign-in is not fresh enough to act (#1635). Declared once: the PDP writes it
|
|
@@ -319,6 +319,18 @@ export interface PageProvider extends BaseProvider {
|
|
|
319
319
|
reason: string;
|
|
320
320
|
detail?: string;
|
|
321
321
|
}>;
|
|
322
|
+
/**
|
|
323
|
+
* Where a trashed page would come back to, and its frontmatter, so the
|
|
324
|
+
* write door can decide the restore before it happens (#1815, security
|
|
325
|
+
* review L5): its title, or its vault name when it was deleted from a
|
|
326
|
+
* vault. Null when the uuid is not in the trash.
|
|
327
|
+
*
|
|
328
|
+
* @param uuid - Page UUID
|
|
329
|
+
*/
|
|
330
|
+
deletedPageForRestore?(uuid: string): Promise<{
|
|
331
|
+
name: string;
|
|
332
|
+
metadata: PageFrontmatter;
|
|
333
|
+
} | null>;
|
|
322
334
|
/**
|
|
323
335
|
* Get all page titles
|
|
324
336
|
* @returns Sorted array of page titles
|
package/lib/src/types/User.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ export interface UserPreferences {
|
|
|
56
56
|
/** Additional custom preferences */
|
|
57
57
|
[key: string]: unknown;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The account fields whose change is recorded on `user-edit` with the field name
|
|
61
|
+
* only, and that a policy condition may never read as `account:` or `profile:`
|
|
62
|
+
* (#2261): the password, the email, the roles and the like.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SENSITIVE_USER_FIELDS: readonly ["password", "roles", "isActive", "isExternal", "isTest", "email", "profileLocked", "username"];
|
|
59
65
|
/**
|
|
60
66
|
* User object
|
|
61
67
|
*
|
|
@@ -128,10 +128,13 @@ export interface AddonJobPolicyLayer {
|
|
|
128
128
|
problems: string[];
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* An add-on's resource types (#1823). Each name starts with `<slug>-`, the
|
|
132
|
+
* same fence as its jobs and its ledger tables. `owner`, when declared, is
|
|
133
|
+
* an identity @id. A bad declaration refuses the add-on, so none of its keys
|
|
134
|
+
* merge. A turned-off add-on is not checked: its file is not merged.
|
|
134
135
|
*/
|
|
136
|
+
export declare function addonResourceTypeProblems(slug: string, defaults: Record<string, unknown>): string[];
|
|
137
|
+
export declare function addonResourceTypeLayer(addons: readonly AddonDefaults[]): AddonJobPolicyLayer;
|
|
135
138
|
export declare function addonJobPolicyLayer(addons: readonly AddonDefaults[]): AddonJobPolicyLayer;
|
|
136
139
|
/** The configuration key that makes an addon site-critical (#1883). */
|
|
137
140
|
export declare function addonSiteCriticalKey(slug: string): string;
|
|
@@ -316,6 +316,63 @@ export function addonSensitiveValuesLayer(addons) {
|
|
|
316
316
|
* `<slug>.`, the prefix its job ids carry. Only an enabled addon's policies
|
|
317
317
|
* reach the merge, so only an enabled addon is checked.
|
|
318
318
|
*/
|
|
319
|
+
const RESOURCE_TYPES_KEY = 'ngdpbase.access.resource-types';
|
|
320
|
+
const ATTRIBUTE_TYPES = new Set(['string', 'integer', 'boolean', 'identity', 'list']);
|
|
321
|
+
/**
|
|
322
|
+
* An add-on's resource types (#1823). Each name starts with `<slug>-`, the
|
|
323
|
+
* same fence as its jobs and its ledger tables. `owner`, when declared, is
|
|
324
|
+
* an identity @id. A bad declaration refuses the add-on, so none of its keys
|
|
325
|
+
* merge. A turned-off add-on is not checked: its file is not merged.
|
|
326
|
+
*/
|
|
327
|
+
export function addonResourceTypeProblems(slug, defaults) {
|
|
328
|
+
const types = defaults[RESOURCE_TYPES_KEY];
|
|
329
|
+
if (types === undefined)
|
|
330
|
+
return [];
|
|
331
|
+
if (!isPlainObject(types))
|
|
332
|
+
return [`${RESOURCE_TYPES_KEY} must be an object of resource types`];
|
|
333
|
+
const problems = [];
|
|
334
|
+
const prefix = `${slug}-`;
|
|
335
|
+
for (const [name, entry] of Object.entries(types)) {
|
|
336
|
+
if (!name.startsWith(prefix) || name.length === prefix.length) {
|
|
337
|
+
problems.push(`resource type '${name}' must start with '${prefix}'`);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (!isPlainObject(entry)) {
|
|
341
|
+
problems.push(`resource type '${name}' must be an object`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (entry.attributes === undefined)
|
|
345
|
+
continue;
|
|
346
|
+
if (!isPlainObject(entry.attributes)) {
|
|
347
|
+
problems.push(`resource type '${name}' attributes must be an object`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
for (const [attr, decl] of Object.entries(entry.attributes)) {
|
|
351
|
+
if (!isPlainObject(decl) || typeof decl.type !== 'string' || !ATTRIBUTE_TYPES.has(decl.type)) {
|
|
352
|
+
problems.push(`resource type '${name}' attribute '${attr}' needs a type of string, integer, boolean, identity or list`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (attr === 'owner' && decl.type !== 'identity') {
|
|
356
|
+
problems.push(`resource type '${name}' attribute owner must be { "type": "identity" }, the owner's identity @id`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return problems;
|
|
361
|
+
}
|
|
362
|
+
export function addonResourceTypeLayer(addons) {
|
|
363
|
+
const layer = { refused: new Map(), problems: [] };
|
|
364
|
+
for (const addon of addons) {
|
|
365
|
+
if (!addon.enabled)
|
|
366
|
+
continue;
|
|
367
|
+
const found = addonResourceTypeProblems(addon.slug, addon.defaults);
|
|
368
|
+
if (found.length === 0)
|
|
369
|
+
continue;
|
|
370
|
+
const reason = `its resource types were refused: ${found.join('; ')}`;
|
|
371
|
+
layer.refused.set(addon.slug, reason);
|
|
372
|
+
layer.problems.push(`${addonProblemSubject(addon.slug, addon.enabled)}: ${reason}; the add-on is not loaded`);
|
|
373
|
+
}
|
|
374
|
+
return layer;
|
|
375
|
+
}
|
|
319
376
|
export function addonJobPolicyLayer(addons) {
|
|
320
377
|
const layer = { refused: new Map(), problems: [] };
|
|
321
378
|
for (const addon of addons) {
|
|
@@ -432,9 +489,10 @@ export function mergeWithAddonLayer(shipped, addons, custom) {
|
|
|
432
489
|
const criticalLayer = addonSiteCriticalLayer(addons, custom);
|
|
433
490
|
const sensitiveLayer = addonSensitiveValuesLayer(addons);
|
|
434
491
|
const jobPolicyLayer = addonJobPolicyLayer(addons);
|
|
492
|
+
const resourceTypeLayer = addonResourceTypeLayer(addons);
|
|
435
493
|
const siteCritical = criticalLayer.siteCritical;
|
|
436
494
|
const refused = new Map(layer.refused);
|
|
437
|
-
for (const [slug, reason] of [...databaseLayer.refused, ...criticalLayer.refused, ...sensitiveLayer.refused, ...jobPolicyLayer.refused])
|
|
495
|
+
for (const [slug, reason] of [...databaseLayer.refused, ...criticalLayer.refused, ...sensitiveLayer.refused, ...jobPolicyLayer.refused, ...resourceTypeLayer.refused])
|
|
438
496
|
refused.set(slug, refused.has(slug) ? `${refused.get(slug)}; ${reason}` : reason);
|
|
439
497
|
const databases = databaseLayer.declarations.map((d) => ({ ...d, enabled: d.enabled && !refused.has(d.declaredBy) }));
|
|
440
498
|
const withAddons = addons
|
|
@@ -448,14 +506,14 @@ export function mergeWithAddonLayer(shipped, addons, custom) {
|
|
|
448
506
|
return deepMergeObjects(acc, rest);
|
|
449
507
|
}, shipped);
|
|
450
508
|
if (Object.keys(layer.entries).length === 0) {
|
|
451
|
-
return { merged: deepMergeConfigs(withAddons, custom), problems: [...layer.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems, ...jobPolicyLayer.problems], refused, databases, siteCritical };
|
|
509
|
+
return { merged: deepMergeConfigs(withAddons, custom), problems: [...layer.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems, ...jobPolicyLayer.problems, ...resourceTypeLayer.problems], refused, databases, siteCritical };
|
|
452
510
|
}
|
|
453
511
|
const shippedCategories = isPlainObject(withAddons[SYSTEM_CATEGORY_KEY]) ? withAddons[SYSTEM_CATEGORY_KEY] : {};
|
|
454
512
|
const layered = { ...withAddons, [SYSTEM_CATEGORY_KEY]: deepMergeObjects(shippedCategories, layer.entries) };
|
|
455
513
|
const merged = deepMergeObjects(layered, custom);
|
|
456
514
|
const stood = ownerDeclarationsStand(merged[SYSTEM_CATEGORY_KEY], layer, customCategories);
|
|
457
515
|
merged[SYSTEM_CATEGORY_KEY] = stood.categories;
|
|
458
|
-
return { merged: merged, problems: [...layer.problems, ...stood.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems, ...jobPolicyLayer.problems], refused, databases, siteCritical };
|
|
516
|
+
return { merged: merged, problems: [...layer.problems, ...stood.problems, ...databaseLayer.problems, ...criticalLayer.problems, ...sensitiveLayer.problems, ...jobPolicyLayer.problems, ...resourceTypeLayer.problems], refused, databases, siteCritical };
|
|
459
517
|
}
|
|
460
518
|
/**
|
|
461
519
|
* The full merge for a caller with the two files already read (the manager,
|
|
@@ -48,7 +48,10 @@ import { recordAuditEvent } from './auditEvents.js';
|
|
|
48
48
|
import { addonAuditEventName } from './auditEventNames.js';
|
|
49
49
|
import type { IceReceiver, IceShim } from '../ice/types.js';
|
|
50
50
|
import type { AddonReviews, ReviewHandler } from '../types/Review.js';
|
|
51
|
+
import { type SignedOutWriteRoute } from '../security/signedOutWrites.js';
|
|
51
52
|
import type { AddonManifest } from '../managers/AddonsManager.js';
|
|
53
|
+
import type { Decision } from '../types/Policy.js';
|
|
54
|
+
import type { OwnedListAsk, OwnedListDecision, OwnedRecordAsk } from '../security/ownedRecords.js';
|
|
52
55
|
/** The values core offers an add-on, under the names they were imported as. */
|
|
53
56
|
export interface AddonHost {
|
|
54
57
|
readonly ApiContext: typeof ApiContext;
|
|
@@ -117,6 +120,28 @@ export interface AddonHost {
|
|
|
117
120
|
* context; throw to leave the request open with your message as the reason.
|
|
118
121
|
*/
|
|
119
122
|
readonly registerReviewHandler: (handlerId: string, handler: ReviewHandler) => void;
|
|
123
|
+
/**
|
|
124
|
+
* #2298: every request but GET, HEAD and OPTIONS from someone who is not
|
|
125
|
+
* signed in is refused before your route runs. A route that signed-out people must
|
|
126
|
+
* reach (a public form) is declared here, in `register()`, with the reason:
|
|
127
|
+
* `host.allowSignedOutWrite({ method: 'POST', path: '/api/<slug>/submit/:id', reason })`.
|
|
128
|
+
* The path must be under `/api/<slug>/` or `/addons/<slug>/` (literal
|
|
129
|
+
* segments and `:name` only), and the slug must not be one of core's own
|
|
130
|
+
* prefixes (`RESERVED_ADDON_SLUGS`), or this throws. It only lets the request reach your route: the route still asks its
|
|
131
|
+
* own permission through `ApiContext.requirePermission`.
|
|
132
|
+
*/
|
|
133
|
+
readonly allowSignedOutWrite: (route: SignedOutWriteRoute) => void;
|
|
134
|
+
/**
|
|
135
|
+
* #1823: decisions for this add-on's own records. `decide` is one stored
|
|
136
|
+
* row; `list` is the filter for a list. The resource type must start with
|
|
137
|
+
* this add-on's slug. The manager reads `owner` from the row it stored and
|
|
138
|
+
* passes that, then hands a deny to `ApiContext.enforceDeny` with the
|
|
139
|
+
* resource so the refusal is audited. This records nothing itself.
|
|
140
|
+
*/
|
|
141
|
+
readonly access: {
|
|
142
|
+
decide(subject: ActorContext, request: OwnedRecordAsk): Promise<Decision>;
|
|
143
|
+
list(subject: ActorContext, request: OwnedListAsk): Promise<OwnedListDecision>;
|
|
144
|
+
};
|
|
120
145
|
/**
|
|
121
146
|
* #1979 (ICE, D6): register an import shim, as `<slug>.<shim.name>`. Call it
|
|
122
147
|
* in `register`. Refused when the manifest's `shims` does not name it.
|
|
@@ -47,6 +47,7 @@ import { recordAuditEvent } from './auditEvents.js';
|
|
|
47
47
|
import { addonAuditEventName } from './auditEventNames.js';
|
|
48
48
|
import { auditDeclarationsFrom } from './auditRegistry.js';
|
|
49
49
|
import logger from './logger.js';
|
|
50
|
+
import { declareAddonSignedOutWrite } from '../security/signedOutWrites.js';
|
|
50
51
|
/** A host built outside a site (the test kit, with no setter given) has no account store. */
|
|
51
52
|
const noPersonSwitch = () => Promise.reject(new Error('setEnabledForPerson is not available: this host was built without a site'));
|
|
52
53
|
/**
|
|
@@ -121,6 +122,31 @@ export function buildAddonHost(slug, addonDefaults, setEnabledForPerson = noPers
|
|
|
121
122
|
decide: async (ctx, id, decision, note) => reviewDoor().decide(ctx, id, decision, note)
|
|
122
123
|
};
|
|
123
124
|
Object.freeze(reviews);
|
|
125
|
+
const recordDoor = () => {
|
|
126
|
+
const door = engine?.getManager('PolicyDecisionPoint');
|
|
127
|
+
if (typeof door?.decideOwnedRecord !== 'function' || typeof door.decideOwnedList !== 'function') {
|
|
128
|
+
throw new Error('record access is not available: this host was built without a site\'s PolicyDecisionPoint');
|
|
129
|
+
}
|
|
130
|
+
return door;
|
|
131
|
+
};
|
|
132
|
+
const ownType = (type) => {
|
|
133
|
+
if (typeof type !== 'string' || !type.startsWith(`${slug}-`)) {
|
|
134
|
+
const message = `Add-on '${slug}' may decide only its own resource types: a type starts '${slug}-'; '${String(type)}' does not`;
|
|
135
|
+
logger.error(`[addons] ${message}`);
|
|
136
|
+
throw new Error(message);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const access = {
|
|
140
|
+
decide: async (subject, request) => {
|
|
141
|
+
ownType(request?.resource?.type);
|
|
142
|
+
return recordDoor().decideOwnedRecord(subject, request);
|
|
143
|
+
},
|
|
144
|
+
list: async (subject, request) => {
|
|
145
|
+
ownType(request?.resource?.type);
|
|
146
|
+
return recordDoor().decideOwnedList(subject, request);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
Object.freeze(access);
|
|
124
150
|
const hostAddonAuditEventName = (name) => {
|
|
125
151
|
const checked = addonAuditEventName(name);
|
|
126
152
|
refuseUndeclared(slug, name, declared);
|
|
@@ -153,6 +179,8 @@ export function buildAddonHost(slug, addonDefaults, setEnabledForPerson = noPers
|
|
|
153
179
|
zonedParts,
|
|
154
180
|
keyedSerial: () => new KeyedSerial(),
|
|
155
181
|
reviews,
|
|
156
|
-
registerReviewHandler: (handlerId, handler) => reviewDoor().registerHandler(slug, handlerId, handler)
|
|
182
|
+
registerReviewHandler: (handlerId, handler) => reviewDoor().registerHandler(slug, handlerId, handler),
|
|
183
|
+
allowSignedOutWrite: (route) => declareAddonSignedOutWrite(slug, route),
|
|
184
|
+
access
|
|
157
185
|
});
|
|
158
186
|
}
|
|
@@ -55,15 +55,3 @@ export interface SyncRowState {
|
|
|
55
55
|
* @param userModifiedFlag - The live copy is explicitly marked edited
|
|
56
56
|
*/
|
|
57
57
|
export declare function syncRowState(seedStatus: SeededAddonPageStatus, userModifiedFlag?: boolean): SyncRowState;
|
|
58
|
-
/**
|
|
59
|
-
* Default edit access for a page ngdpbase ships (#971, #1411): a `general`
|
|
60
|
-
* page is instance-owned and gets no stamp; any other category — system,
|
|
61
|
-
* documentation, addon, unclassified — is administrator-edit only. A source
|
|
62
|
-
* that declares its own `access` overrides this.
|
|
63
|
-
*
|
|
64
|
-
* @param category - The page's resolved `system-category`
|
|
65
|
-
* @returns The access object to stamp, or undefined to leave the page unprotected
|
|
66
|
-
*/
|
|
67
|
-
export declare function defaultShippedPageAccess(category: unknown): {
|
|
68
|
-
edit: string[];
|
|
69
|
-
} | undefined;
|
|
@@ -224,6 +224,8 @@ export declare const AUDIT_EVENT: {
|
|
|
224
224
|
readonly USER_EDIT: "user-edit";
|
|
225
225
|
/** User profile read; not recorded, read volume */
|
|
226
226
|
readonly USER_READ: "user-read";
|
|
227
|
+
/** An administrator read or changed a page inside another person's vault that is not encrypted (ngdpbase.vaults.admin-access, #2255): the administrator is the actor, and the vault's owner, the vault and the page are named */
|
|
228
|
+
readonly VAULT_ADMIN_ACCESS: "vault-admin-access";
|
|
227
229
|
/** Not an action: read from a proxy grant at the vault door; the grant itself is recorded as proxy-grant */
|
|
228
230
|
readonly VAULT_PROXY: "vault-proxy";
|
|
229
231
|
/** A vault page's leftovers moved into its vault (#1842): its shared-pool files (moved, or copied where other public pages use them) and its footnotes and comments from the public folders. Names the owner, vault and page uuid and the counts, never a title or file name. Run by the vault's owner, or for a vault that isn't sealed by someone holding admin-system */
|
|
@@ -231,6 +231,8 @@ export const AUDIT_EVENT = {
|
|
|
231
231
|
USER_EDIT: 'user-edit',
|
|
232
232
|
/** User profile read; not recorded, read volume */
|
|
233
233
|
USER_READ: 'user-read',
|
|
234
|
+
/** An administrator read or changed a page inside another person's vault that is not encrypted (ngdpbase.vaults.admin-access, #2255): the administrator is the actor, and the vault's owner, the vault and the page are named */
|
|
235
|
+
VAULT_ADMIN_ACCESS: 'vault-admin-access',
|
|
234
236
|
/** Not an action: read from a proxy grant at the vault door; the grant itself is recorded as proxy-grant */
|
|
235
237
|
VAULT_PROXY: 'vault-proxy',
|
|
236
238
|
/** A vault page's leftovers moved into its vault (#1842): its shared-pool files (moved, or copied where other public pages use them) and its footnotes and comments from the public folders. Names the owner, vault and page uuid and the counts, never a title or file name. Run by the vault's owner, or for a vault that isn't sealed by someone holding admin-system */
|
|
@@ -142,6 +142,8 @@ export interface TokenInput extends CommonInput {
|
|
|
142
142
|
previousExpiresAt?: string;
|
|
143
143
|
/** Only meaningful for `revoke`; ignored otherwise. */
|
|
144
144
|
revokedBy?: string;
|
|
145
|
+
/** Only meaningful for `mint` (#2287): the sign-in level stored with the token; absent when it stored none. */
|
|
146
|
+
aal?: 1 | 2 | 3;
|
|
145
147
|
}
|
|
146
148
|
/**
|
|
147
149
|
* What an attachment record says about the file. A file in an ENCRYPTED store
|
|
@@ -289,7 +289,7 @@ export async function recordAuditEvent(sink, event, onError) {
|
|
|
289
289
|
* every event is useless as a filter.
|
|
290
290
|
*/
|
|
291
291
|
export function buildTokenAuditEvent(input) {
|
|
292
|
-
const { op, id, owner, ownerUser, name, scopes, expiresAt, previousExpiresAt, revokedBy, viaToken } = input;
|
|
292
|
+
const { op, id, owner, ownerUser, name, scopes, expiresAt, previousExpiresAt, revokedBy, viaToken, aal } = input;
|
|
293
293
|
const who = whoOf(input);
|
|
294
294
|
const metadata = {
|
|
295
295
|
id,
|
|
@@ -302,6 +302,8 @@ export function buildTokenAuditEvent(input) {
|
|
|
302
302
|
if (op === 'mint') {
|
|
303
303
|
metadata.scopes = [...(scopes ?? [])];
|
|
304
304
|
metadata.expiresAt = expiresAt;
|
|
305
|
+
// #2287: the level the token holds, or null when it stored none; the record that answers "was this minted after a second factor?"
|
|
306
|
+
metadata.aal = aal ?? null;
|
|
305
307
|
}
|
|
306
308
|
if (op === 'extend') {
|
|
307
309
|
metadata.previousExpiresAt = previousExpiresAt;
|
|
@@ -100,6 +100,21 @@ export declare function zonedParts(now: Date, timeZone: string): {
|
|
|
100
100
|
year: number;
|
|
101
101
|
month: number;
|
|
102
102
|
};
|
|
103
|
+
/** Whether a schedule is switched on: `enabled: false` turns it off, as the site gate reads it. */
|
|
104
|
+
export declare function scheduleIsEnabled(schedule: Schedule): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* The first thing in a schedule that is not in its exact allowed shape, or
|
|
107
|
+
* `null`. The shape is an allowlist, so a misspelled key reads as a problem,
|
|
108
|
+
* never as an open window: top-level keys `name`, `description`, `message`
|
|
109
|
+
* (text), `timeZone` (a known IANA zone), `enabled` (true or false), `rules`
|
|
110
|
+
* (a list) and `exceptions` (only empty); each rule an object with `type`
|
|
111
|
+
* (`allow`, `deny` or absent), `days` (a non-empty list of weekday names) and
|
|
112
|
+
* `startTime` and `endTime` (HH:MM, both or neither). A `deny` rule closes its
|
|
113
|
+
* whole day, so times on it are a problem rather than silently ignored.
|
|
114
|
+
* Names the path (`rules[1].dayz`), never a value. The site gate lets a
|
|
115
|
+
* malformed schedule stay open; a policy must not (#2261).
|
|
116
|
+
*/
|
|
117
|
+
export declare function scheduleProblem(schedule: Schedule, fallbackZone: string): string | null;
|
|
103
118
|
/**
|
|
104
119
|
* Whether a schedule is open at `now`.
|
|
105
120
|
*
|
|
@@ -78,6 +78,94 @@ function minutesOf(hhmm) {
|
|
|
78
78
|
return null;
|
|
79
79
|
return hours * 60 + mins;
|
|
80
80
|
}
|
|
81
|
+
const WEEKDAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
82
|
+
function knownZone(timeZone) {
|
|
83
|
+
try {
|
|
84
|
+
new Intl.DateTimeFormat('en-US', { timeZone });
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Whether a schedule is switched on: `enabled: false` turns it off, as the site gate reads it. */
|
|
92
|
+
export function scheduleIsEnabled(schedule) {
|
|
93
|
+
return toBoolean(schedule.enabled, true);
|
|
94
|
+
}
|
|
95
|
+
// Exactly the keys the site gate reads (resolveAvailability, scheduleIsOpen), plus `name`/`description`, which only label.
|
|
96
|
+
// `exceptions` is declared on Schedule but nothing reads it, so only the empty list the shipped schedules carry is accepted.
|
|
97
|
+
const SCHEDULE_KEYS = ['name', 'description', 'timeZone', 'rules', 'exceptions', 'enabled', 'message'];
|
|
98
|
+
const RULE_KEYS = ['type', 'days', 'startTime', 'endTime'];
|
|
99
|
+
function isPlainObject(value) {
|
|
100
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The first thing in a schedule that is not in its exact allowed shape, or
|
|
104
|
+
* `null`. The shape is an allowlist, so a misspelled key reads as a problem,
|
|
105
|
+
* never as an open window: top-level keys `name`, `description`, `message`
|
|
106
|
+
* (text), `timeZone` (a known IANA zone), `enabled` (true or false), `rules`
|
|
107
|
+
* (a list) and `exceptions` (only empty); each rule an object with `type`
|
|
108
|
+
* (`allow`, `deny` or absent), `days` (a non-empty list of weekday names) and
|
|
109
|
+
* `startTime` and `endTime` (HH:MM, both or neither). A `deny` rule closes its
|
|
110
|
+
* whole day, so times on it are a problem rather than silently ignored.
|
|
111
|
+
* Names the path (`rules[1].dayz`), never a value. The site gate lets a
|
|
112
|
+
* malformed schedule stay open; a policy must not (#2261).
|
|
113
|
+
*/
|
|
114
|
+
export function scheduleProblem(schedule, fallbackZone) {
|
|
115
|
+
const top = schedule;
|
|
116
|
+
if (!isPlainObject(top))
|
|
117
|
+
return 'the schedule is not an object';
|
|
118
|
+
for (const key of Object.keys(top)) {
|
|
119
|
+
if (!SCHEDULE_KEYS.includes(key))
|
|
120
|
+
return `${key} is not a schedule setting`;
|
|
121
|
+
}
|
|
122
|
+
for (const key of ['name', 'description', 'message']) {
|
|
123
|
+
if (top[key] !== undefined && typeof top[key] !== 'string')
|
|
124
|
+
return `${key} is not text`;
|
|
125
|
+
}
|
|
126
|
+
if (top.exceptions !== undefined && !(Array.isArray(top.exceptions) && top.exceptions.length === 0)) {
|
|
127
|
+
return 'exceptions is not read by anything and must be empty';
|
|
128
|
+
}
|
|
129
|
+
const zone = schedule.timeZone === undefined ? fallbackZone : schedule.timeZone;
|
|
130
|
+
if (typeof zone !== 'string' || !zone.trim() || !knownZone(zone)) {
|
|
131
|
+
return `${schedule.timeZone === undefined ? 'the site time zone (ngdpbase.time-zone)' : 'timeZone'} is not a known time zone`;
|
|
132
|
+
}
|
|
133
|
+
if (schedule.enabled !== undefined && typeof schedule.enabled !== 'boolean')
|
|
134
|
+
return 'enabled is not true or false';
|
|
135
|
+
if (schedule.rules === undefined)
|
|
136
|
+
return null;
|
|
137
|
+
if (!Array.isArray(schedule.rules))
|
|
138
|
+
return 'rules is not a list';
|
|
139
|
+
for (const [i, rule] of schedule.rules.entries()) {
|
|
140
|
+
const at = `rules[${i}]`;
|
|
141
|
+
if (!isPlainObject(rule))
|
|
142
|
+
return `${at} is not an object`;
|
|
143
|
+
for (const key of Object.keys(rule)) {
|
|
144
|
+
if (!RULE_KEYS.includes(key))
|
|
145
|
+
return `${at}.${key} is not a rule setting`;
|
|
146
|
+
}
|
|
147
|
+
// Only `allow`, `deny` or no type: scheduleIsOpen reads anything but 'deny' as allow, so a typo like "Deny" would fail open.
|
|
148
|
+
if (rule.type !== undefined && rule.type !== 'allow' && rule.type !== 'deny')
|
|
149
|
+
return `${at}.type is not allow or deny`;
|
|
150
|
+
if (rule.days !== undefined) {
|
|
151
|
+
if (!Array.isArray(rule.days) || rule.days.length === 0)
|
|
152
|
+
return `${at}.days is not a non-empty list`;
|
|
153
|
+
if (rule.days.some((d) => typeof d !== 'string' || !WEEKDAYS.includes(d.trim().toLowerCase())))
|
|
154
|
+
return `${at}.days is not a list of weekday names`;
|
|
155
|
+
}
|
|
156
|
+
const given = ['startTime', 'endTime'].filter((f) => rule[f] !== undefined);
|
|
157
|
+
if (rule.type === 'deny' && given.length > 0)
|
|
158
|
+
return `${at}.${given[0]} is ignored on a deny rule, which closes its whole day`;
|
|
159
|
+
// With neither time a rule covers its whole day; with one, or a bad one, it cannot be read.
|
|
160
|
+
if (given.length === 0)
|
|
161
|
+
continue;
|
|
162
|
+
for (const f of ['startTime', 'endTime']) {
|
|
163
|
+
if (minutesOf(rule[f]) === null)
|
|
164
|
+
return `${at}.${f} is not a time of day (HH:MM), or only one of startTime and endTime is given`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
81
169
|
/**
|
|
82
170
|
* Whether a schedule is open at `now`.
|
|
83
171
|
*
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* frontmatterAccess — the single implementation of ACL Tier 1 (#1054).
|
|
3
3
|
*
|
|
4
|
-
* A page's frontmatter can restrict who may act on it
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A page's frontmatter can restrict who may act on it with `audience`, which
|
|
5
|
+
* covers every action (#1964). The `access[<action>]` lists it once honoured
|
|
6
|
+
* were removed in #2255: what a user may do on a page is decided by its
|
|
7
|
+
* system-category and policy. Two subsystems needed the audience answer and
|
|
8
|
+
* each had its own version of it, which is how they came to disagree:
|
|
7
9
|
*
|
|
8
10
|
* - `PolicyInformationPoint.checkFrontmatterAccess` decided a request, correctly.
|
|
9
11
|
* - `VersioningFileProvider.getRecentChanges` decided a listing, and only
|
|
@@ -20,8 +22,6 @@
|
|
|
20
22
|
*/
|
|
21
23
|
/** The frontmatter fields Tier 1 reads. */
|
|
22
24
|
export interface AccessMetadataLike {
|
|
23
|
-
/** `{ view: ['role'], edit: [...] }` — per-action principal lists. */
|
|
24
|
-
access?: unknown;
|
|
25
25
|
/** Who may act on the page at all; administrators excepted (#1964). */
|
|
26
26
|
audience?: unknown;
|
|
27
27
|
}
|
|
@@ -39,11 +39,6 @@ export interface FrontmatterDecision {
|
|
|
39
39
|
* neither read nor edit the page.
|
|
40
40
|
*/
|
|
41
41
|
export declare function audiencePrincipals(metadata: AccessMetadataLike | null | undefined): string[] | null;
|
|
42
|
-
/**
|
|
43
|
-
* The page's `access[action]` list, or null when it states none. It applies
|
|
44
|
-
* to that action only, and to everyone — administrators included.
|
|
45
|
-
*/
|
|
46
|
-
export declare function accessPrincipals(metadata: AccessMetadataLike | null | undefined, action: string): string[] | null;
|
|
47
42
|
/**
|
|
48
43
|
* The entry `addon:<slug>` (#1964): "people who turned on the per-person
|
|
49
44
|
* add-on <slug>". It is matched only against the viewer's enabled add-ons,
|
|
@@ -54,11 +49,10 @@ export declare const ADDON_PRINCIPAL_PREFIX = "addon:";
|
|
|
54
49
|
* Whether deciding `action` on this page needs the viewer's enabled add-ons —
|
|
55
50
|
* so a caller asks for them only when the page names one (#1964).
|
|
56
51
|
*/
|
|
57
|
-
export declare function namesAddonPrincipal(metadata: AccessMetadataLike | null | undefined,
|
|
52
|
+
export declare function namesAddonPrincipal(metadata: AccessMetadataLike | null | undefined, _action: string): boolean;
|
|
58
53
|
/**
|
|
59
|
-
* Every add-on slug the page names in `audience
|
|
60
|
-
*
|
|
61
|
-
* nobody (#1964).
|
|
54
|
+
* Every add-on slug the page names in `audience`, once each, in the order
|
|
55
|
+
* first named — for the warning about a slug that can match nobody (#1964).
|
|
62
56
|
*/
|
|
63
57
|
export declare function addonAudienceSlugs(metadata: AccessMetadataLike | null | undefined): string[];
|
|
64
58
|
/**
|
|
@@ -75,16 +69,13 @@ export declare const AUDIENCE_ADMIN_MATCH = "admin-system";
|
|
|
75
69
|
* plus their username. Callers holding a `UserContext` should pass
|
|
76
70
|
* `[...roles, username]`; the provider layer already works in these terms.
|
|
77
71
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* only lets them through to `access` and then policy.
|
|
86
|
-
* - __`access[action]`__ applies to that action, to everyone, administrators
|
|
87
|
-
* included. When the page states it, it decides.
|
|
72
|
+
* `audience` applies to every action (#1964, operator 2026-10-23). Someone it
|
|
73
|
+
* leaves out may neither read nor edit the page: every action is refused. The
|
|
74
|
+
* one exception is an administrator — a viewer holding `admin-system`, which
|
|
75
|
+
* the caller decides and passes as `isAdmin` (this module never reads a role
|
|
76
|
+
* for it). For `view`, a viewer the audience admits is allowed; for any other
|
|
77
|
+
* action it only lets them through to policy. A page's `access` field is not
|
|
78
|
+
* read (#2255).
|
|
88
79
|
*
|
|
89
80
|
* An `addon:<slug>` entry matches only when `<slug>` is in `enabledAddons`,
|
|
90
81
|
* the viewer's enabled per-person add-ons (#1964); a caller that does not
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** One leftover key and what to tell the operator about it. */
|
|
2
|
+
export interface LegacyStoreKey {
|
|
3
|
+
key: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
setting: 'owner' | 'encrypt';
|
|
6
|
+
/** One sentence: the key, that it has no effect, and what replaces it. */
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Every legacy per-kind key in the operator's custom config, in file order.
|
|
11
|
+
* `categories` is the merged `ngdpbase.system-category` map, used to name the
|
|
12
|
+
* entry that declares the kind's vault (the `default` vault is `general`'s).
|
|
13
|
+
*/
|
|
14
|
+
export declare function legacyStoreKeys(customConfig: Record<string, unknown> | null | undefined, categories: unknown): LegacyStoreKey[];
|
|
15
|
+
/** The needs-attention reason for a non-empty list: every key, in one sentence. */
|
|
16
|
+
export declare function legacyStoreKeysReason(found: LegacyStoreKey[]): string;
|