@jwilleke/ngdpbase-types 5.41.1 → 5.43.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.js +1 -1
- 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/managers/AddonsManager.d.ts +8 -28
- package/lib/src/managers/AgentTokenManager.d.ts +8 -0
- package/lib/src/managers/AttachmentManager.d.ts +59 -0
- package/lib/src/managers/AuditManager.d.ts +477 -0
- package/lib/src/managers/AuthManager.d.ts +247 -15
- package/lib/src/managers/AuthManager.js +588 -45
- package/lib/src/managers/ConfigurationManager.d.ts +15 -0
- package/lib/src/managers/ConsentManager.d.ts +15 -0
- package/lib/src/managers/ImportManager.d.ts +15 -0
- package/lib/src/managers/PageManager.d.ts +185 -23
- package/lib/src/managers/PolicyEvaluator.d.ts +99 -24
- package/lib/src/managers/ShareManager.d.ts +3 -2
- 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/BaseAttachmentProvider.d.ts +4 -0
- package/lib/src/providers/BaseAuditProvider.d.ts +492 -0
- package/lib/src/providers/BaseAuthProvider.d.ts +4 -0
- package/lib/src/providers/BaseCredentialsProvider.d.ts +2 -2
- package/lib/src/providers/BaseCredentialsProvider.js +1 -1
- package/lib/src/providers/BasicAttachmentProvider.d.ts +9 -0
- package/lib/src/providers/FileCredentialsProvider.js +1 -1
- package/lib/src/providers/NullAuditProvider.d.ts +82 -0
- package/lib/src/providers/TotpAuthProvider.d.ts +55 -0
- package/lib/src/providers/TotpAuthProvider.js +145 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +57 -1
- package/lib/src/security/PolicyInformationPoint.d.ts +86 -6
- package/lib/src/security/permissions.generated.d.ts +8 -0
- package/lib/src/types/Consent.d.ts +17 -4
- package/lib/src/types/Consent.js +11 -1
- package/lib/src/types/Page.d.ts +0 -7
- package/lib/src/types/Policy.d.ts +67 -6
- package/lib/src/types/Policy.js +6 -0
- package/lib/src/types/Provider.d.ts +18 -0
- package/lib/src/types/User.d.ts +6 -0
- package/lib/src/types/express.d.ts +22 -0
- package/lib/src/utils/addonPageSync.d.ts +0 -12
- package/lib/src/utils/auditChain.d.ts +83 -0
- 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/auditHeadWitness.d.ts +97 -0
- package/lib/src/utils/auditLifecycle.d.ts +71 -0
- package/lib/src/utils/browserDescription.d.ts +18 -0
- package/lib/src/utils/browserDescription.js +12 -0
- package/lib/src/utils/frontmatterAccess.d.ts +15 -24
- package/lib/src/utils/legacyStoreKeys.d.ts +16 -0
- package/lib/src/utils/pageWritePolicies.d.ts +55 -0
- package/lib/src/utils/permissionCatalog.d.ts +2 -0
- package/lib/src/utils/permissionCatalog.js +19 -0
- package/lib/src/utils/policyAttributes.d.ts +141 -0
- package/lib/src/utils/policyAttributes.js +219 -0
- package/lib/src/utils/postureRecord.d.ts +73 -0
- package/lib/src/utils/privateStoreAccess.d.ts +24 -4
- package/lib/src/utils/privateStoreAccess.js +19 -4
- package/lib/src/utils/privateStorePath.d.ts +6 -0
- package/lib/src/utils/privateStorePath.js +8 -0
- package/lib/src/utils/securityPosture.d.ts +52 -0
- package/lib/src/utils/tlsConfig.d.ts +59 -0
- package/lib/src/utils/totpSeedCipher.d.ts +4 -0
- package/lib/src/utils/totpSeedCipher.js +45 -0
- package/lib/src/utils/username.d.ts +13 -2
- package/lib/src/utils/username.js +18 -3
- package/package.json +5 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page-write policies after #2255, and the migration of a site's stored
|
|
3
|
+
* copies of the old ones.
|
|
4
|
+
*
|
|
5
|
+
* Until #2255 the shipped role policies granted `page-edit`, `page-delete` and
|
|
6
|
+
* `page-rename` on every page (`page: *`), and an `access: { edit: [admin] }`
|
|
7
|
+
* stamp on each shipped page was the only thing keeping editors out of the
|
|
8
|
+
* system and documentation pages. Those grants now sit on the
|
|
9
|
+
* `system-category` resource type, one policy per group of categories, and the
|
|
10
|
+
* stamp is gone.
|
|
11
|
+
*
|
|
12
|
+
* A site that ever saved `ngdpbase.access.policies` (the configuration editor,
|
|
13
|
+
* the Shared folders screen) holds a full copy of the old role policies, and a
|
|
14
|
+
* stored entry replaces the shipped one with the same id whole
|
|
15
|
+
* (`src/utils/configFiles.ts`). Its `page-edit` on `page: *` would reopen every
|
|
16
|
+
* pushed page to editors. So, before the merge, in memory:
|
|
17
|
+
*
|
|
18
|
+
* - a stored entry with a shipped id below loses the page writes it held on
|
|
19
|
+
* `page: *`; any it held that the shipped category policies do not give that
|
|
20
|
+
* role is kept, on the general category only, as a new policy
|
|
21
|
+
* `<id>-page-write`;
|
|
22
|
+
* - every other policy that grants a page write on a `page` resource to a role
|
|
23
|
+
* other than admin, and every deny policy (a deny now overrides an allow
|
|
24
|
+
* whatever its priority), is reported and left as it is: it is the site's
|
|
25
|
+
* own choice.
|
|
26
|
+
*
|
|
27
|
+
* Pure: the caller logs and reports. The site's file is not rewritten; saving
|
|
28
|
+
* the policy list from the configuration editor writes the migrated list.
|
|
29
|
+
*/
|
|
30
|
+
import type { Policy } from '../types/Policy.js';
|
|
31
|
+
/** What the migration did and found, one sentence each. */
|
|
32
|
+
export interface PageWritePolicyReport {
|
|
33
|
+
/** Stored shipped policies rewritten in memory. */
|
|
34
|
+
rewritten: string[];
|
|
35
|
+
/** Policies left alone that now decide differently, or reopen pushed pages. */
|
|
36
|
+
review: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Rewrite the stored copies of the moved shipped policies, in `policies` (the
|
|
40
|
+
* custom layer's `ngdpbase.access.policies`), and return what was done and
|
|
41
|
+
* what an administrator should review. Returns the list to store in place of
|
|
42
|
+
* the input; the input is not changed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function migrateStoredPageWritePolicies(policies: unknown): {
|
|
45
|
+
policies: unknown;
|
|
46
|
+
report: PageWritePolicyReport;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Policies in force that an administrator should review after #2255: an
|
|
50
|
+
* allow that grants a page write on a `page` resource to a role other than
|
|
51
|
+
* admin (it reaches pushed pages), an allow that grants anything but
|
|
52
|
+
* page-read to `anyone` or `authenticated` (#2261, #2283), and every deny (a deny now overrides any
|
|
53
|
+
* allow, whatever the priorities).
|
|
54
|
+
*/
|
|
55
|
+
export declare function pageWritePoliciesToReview(policies: readonly Policy[]): string[];
|
|
@@ -25,6 +25,8 @@ export declare function grantablePermissions<T>(definitions: Record<string, T> |
|
|
|
25
25
|
export declare const RETIRED_PERMISSIONS: ReadonlyMap<string, readonly string[]>;
|
|
26
26
|
/** The names with every retired permission replaced by what it stood for, de-duplicated, order kept. */
|
|
27
27
|
export declare function withRetiredExpanded(names: readonly string[]): string[];
|
|
28
|
+
/** The names a policy may grant `action` under (#2255): a page write and its pushed-pages twin, else the action alone. */
|
|
29
|
+
export declare function pageWriteAlternatives(action: string): string[];
|
|
28
30
|
/**
|
|
29
31
|
* What the standard OpenID Connect sign-in scopes let an app see, in plain
|
|
30
32
|
* words (#1572). They are not permissions, so they are not in the catalog;
|
|
@@ -50,6 +50,25 @@ export function withRetiredExpanded(names) {
|
|
|
50
50
|
}
|
|
51
51
|
return out;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The pushed-pages permission for each page write (#2255). A page in a
|
|
55
|
+
* pushed category (shipped by the system or an add-on: `system`,
|
|
56
|
+
* `documentation`, `addon` as shipped) is changed by holders of
|
|
57
|
+
* `pushed-pages-<verb>`; any other page by holders of `page-<verb>`. Which
|
|
58
|
+
* categories are which is the policies' to say — each grants one or the
|
|
59
|
+
* other on `system-category` resources — so a page write is asked as either
|
|
60
|
+
* name, and the policies on the page's category decide.
|
|
61
|
+
*/
|
|
62
|
+
const PUSHED_PAGE_WRITES = {
|
|
63
|
+
'page-edit': 'pushed-pages-edit',
|
|
64
|
+
'page-delete': 'pushed-pages-delete',
|
|
65
|
+
'page-rename': 'pushed-pages-rename'
|
|
66
|
+
};
|
|
67
|
+
/** The names a policy may grant `action` under (#2255): a page write and its pushed-pages twin, else the action alone. */
|
|
68
|
+
export function pageWriteAlternatives(action) {
|
|
69
|
+
const pushed = PUSHED_PAGE_WRITES[action];
|
|
70
|
+
return pushed ? [action, pushed] : [action];
|
|
71
|
+
}
|
|
53
72
|
/**
|
|
54
73
|
* What the standard OpenID Connect sign-in scopes let an app see, in plain
|
|
55
74
|
* words (#1572). They are not permissions, so they are not in the catalog;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary of a policy `condition` (#2261): attribute types, the
|
|
3
|
+
* operator table, the nesting limit, and how one comparison is decided.
|
|
4
|
+
* Pure: no I/O, no manager. The evaluator and the validator both import it,
|
|
5
|
+
* so the set of operators lives here and nowhere else.
|
|
6
|
+
*/
|
|
7
|
+
/** The types an attribute can be declared with. */
|
|
8
|
+
export declare const ATTRIBUTE_TYPES: readonly ["string", "integer", "boolean", "identity", "list"];
|
|
9
|
+
export type AttributeType = (typeof ATTRIBUTE_TYPES)[number];
|
|
10
|
+
/**
|
|
11
|
+
* How deep a condition may nest. Every node counts, leaves included, so
|
|
12
|
+
* `any-of` > `all-of` > `not` > leaf is 4 and one more level is refused.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MAX_CONDITION_DEPTH = 4;
|
|
15
|
+
/**
|
|
16
|
+
* How wide a condition may be (#2261). Depth alone leaves the tree as wide as
|
|
17
|
+
* a config file can make it, and the evaluator walks it for every page a
|
|
18
|
+
* listing or search asks about. A literal `in` list, the children of one
|
|
19
|
+
* `any-of` or `all-of`, and the nodes of the whole condition are each capped.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MAX_IN_ITEMS = 256;
|
|
22
|
+
export declare const MAX_GROUP_CHILDREN = 256;
|
|
23
|
+
export declare const MAX_CONDITION_NODES = 512;
|
|
24
|
+
/** What a comparison against an attribute accepts, by operator. */
|
|
25
|
+
interface LeafOperator {
|
|
26
|
+
/** Types the attribute on the left may have. */
|
|
27
|
+
left: readonly AttributeType[];
|
|
28
|
+
}
|
|
29
|
+
/** The operators that compare one attribute with a value. */
|
|
30
|
+
export declare const LEAF_OPERATORS: Readonly<Record<string, LeafOperator>>;
|
|
31
|
+
/** The operators that combine conditions. `not` takes one condition, these take a list. */
|
|
32
|
+
export declare const GROUP_OPERATORS: readonly ["any-of", "all-of"];
|
|
33
|
+
export declare const NOT_OPERATOR = "not";
|
|
34
|
+
/** Every operator, in the order the validator lists them. */
|
|
35
|
+
export declare const CONDITION_OPERATORS: readonly string[];
|
|
36
|
+
/** The type a literal value has, or `null` when it is not a usable literal. */
|
|
37
|
+
export declare function literalType(value: unknown): 'string' | 'integer' | 'boolean' | null;
|
|
38
|
+
/** Can a literal be compared with an attribute of this type? An identity is a string. */
|
|
39
|
+
export declare function literalFits(attributeType: string, value: unknown): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Does a value read for an attribute fit the type it is declared with? A
|
|
42
|
+
* value that does not is treated as missing by the reader, so a comparison on
|
|
43
|
+
* it is unknown and fails closed, not a definite false or true (#2261). Never
|
|
44
|
+
* coerces: "5" is not an integer. An empty string is a string, a value and not
|
|
45
|
+
* a missing one.
|
|
46
|
+
*/
|
|
47
|
+
export declare function valueFits(attributeType: string, value: unknown): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Decides one comparison once both sides are read. `undefined` is unknown: a
|
|
50
|
+
* side that is missing, or not of a type the operator can compare. Both sides
|
|
51
|
+
* must be the same kind of value (a string, a boolean or a whole number), so
|
|
52
|
+
* "5" never equals 5 and NaN never compares. `in` takes an array, or a Set the
|
|
53
|
+
* evaluator built from a literal list; every entry of an array must be the
|
|
54
|
+
* kind of value on the left. An empty string is a value, not a missing one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function compareLeaf(operator: string, left: unknown, right: unknown): boolean | undefined;
|
|
57
|
+
/** The assurance level a sign-in reached: NIST SP 800-63B, 1 to 3. */
|
|
58
|
+
export type SubjectAal = 1 | 2 | 3;
|
|
59
|
+
/** `value` as an assurance level, or `undefined` when it is not 1, 2 or 3. Never coerces. */
|
|
60
|
+
export declare function toAal(value: unknown): SubjectAal | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* What a subject carries that bears on its assurance level (#2261 slice 3).
|
|
63
|
+
* Read from the request's subject, never from a request field: a caller
|
|
64
|
+
* cannot send an `aal` and have it believed.
|
|
65
|
+
*/
|
|
66
|
+
export interface AalFacts {
|
|
67
|
+
isAuthenticated?: boolean;
|
|
68
|
+
/** The session's sign-in record; absent on a session from before #1523. */
|
|
69
|
+
signIn?: {
|
|
70
|
+
aal?: unknown;
|
|
71
|
+
} | null;
|
|
72
|
+
/** The highest level the account's methods can reach; the level never exceeds it. */
|
|
73
|
+
aalCap?: number;
|
|
74
|
+
/** A token request: the level stored when the token was made. */
|
|
75
|
+
viaToken?: {
|
|
76
|
+
aal?: unknown;
|
|
77
|
+
} | null;
|
|
78
|
+
/** A share visitor has no sign-in. */
|
|
79
|
+
viaShare?: unknown;
|
|
80
|
+
/** A bearer JWT (Authentik): verified, but it records no sign-in level, so the subject has none (#2287). */
|
|
81
|
+
bearerWithoutLevel?: unknown;
|
|
82
|
+
/** A background job: the level stored when the job was requested. */
|
|
83
|
+
grantedAal?: unknown;
|
|
84
|
+
/** A background job (`JobSubject`): it has no session to read. */
|
|
85
|
+
resolveRolesNow?: unknown;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The subject's `subject:aal`, or `undefined` when it has none (#2261).
|
|
89
|
+
*
|
|
90
|
+
* - Not signed in, or a share visitor: none.
|
|
91
|
+
* - An agent token: the level stored with the token. A token made before this
|
|
92
|
+
* stored one has none.
|
|
93
|
+
* - A background job: the level stored with the job's request. A job with
|
|
94
|
+
* none (a schedule, boot, a request from before this) has none.
|
|
95
|
+
* - A bearer JWT that records no level (Authentik): none. It is not a session
|
|
96
|
+
* from before sign-ins were recorded, so the AAL 1 rule below does not apply.
|
|
97
|
+
* - A session: its sign-in record's level; a session from before sign-ins
|
|
98
|
+
* were recorded (#1523) is AAL 1, as `AuthManager` treats it.
|
|
99
|
+
*
|
|
100
|
+
* Capped at `aalCap`, as a role is. Reads the record; it never works the level
|
|
101
|
+
* out from factors.
|
|
102
|
+
*/
|
|
103
|
+
export declare function effectiveAal(subject: AalFacts | null | undefined): SubjectAal | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* The fields of a request's subject that bear on its level, copied by name so
|
|
106
|
+
* a subject rebuilt for a decision keeps them (the #1164 lesson: a field not
|
|
107
|
+
* forwarded is silently lost). Reads nothing else from it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function aalFactsOf(subject: object | null | undefined): AalFacts;
|
|
110
|
+
/** Where a subject attribute may be read from: `account:<field>`, `profile:<field>` or `sign-in`. */
|
|
111
|
+
export declare const SUBJECT_SOURCES: readonly ["account:", "profile:", "sign-in"];
|
|
112
|
+
/** One declared subject attribute, as `ngdpbase.access.subject-attributes` holds it. */
|
|
113
|
+
export interface DeclaredSubjectAttribute {
|
|
114
|
+
type?: string;
|
|
115
|
+
from?: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* `id` and `aal` mean what they mean here, whatever the configuration (or an
|
|
119
|
+
* add-on's default-config.json) says (#2287). `aal` is read from the sign-in
|
|
120
|
+
* record and `id` from the account; a site that could redefine either could
|
|
121
|
+
* make `subject:aal` read a field it controls and pass a "second factor" rule.
|
|
122
|
+
*/
|
|
123
|
+
export declare const PINNED_SUBJECT_ATTRIBUTES: Readonly<Record<string, DeclaredSubjectAttribute>>;
|
|
124
|
+
/** The subject attributes a site declares, with `id` and `aal` pinned to their built-in meaning. Anything not an object is ignored. */
|
|
125
|
+
export declare function declaredSubjectAttributes(raw: unknown): Record<string, DeclaredSubjectAttribute>;
|
|
126
|
+
/**
|
|
127
|
+
* What is wrong with the value of `ngdpbase.access.subject-attributes` itself
|
|
128
|
+
* (#2287), one message each: not an object; `id` or `aal` declared with any
|
|
129
|
+
* meaning but the built-in one (the built-in is kept regardless; the same
|
|
130
|
+
* declaration as the built-in is harmless and not reported); and any other
|
|
131
|
+
* declaration `subjectDeclarationProblem` refuses. Used on save, on load and at boot.
|
|
132
|
+
*/
|
|
133
|
+
export declare function subjectAttributesProblems(raw: unknown, sensitive: readonly string[]): string[];
|
|
134
|
+
/**
|
|
135
|
+
* What is wrong with a subject attribute's declaration, or `null`: a `type`
|
|
136
|
+
* that is not known, a `from` that is not a subject source, or an account field
|
|
137
|
+
* that must never be read (`sensitive`, the password, email, roles and the
|
|
138
|
+
* like). Said so a condition naming it is refused rather than silently unknown.
|
|
139
|
+
*/
|
|
140
|
+
export declare function subjectDeclarationProblem(name: string, decl: DeclaredSubjectAttribute | undefined, sensitive: readonly string[]): string | null;
|
|
141
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary of a policy `condition` (#2261): attribute types, the
|
|
3
|
+
* operator table, the nesting limit, and how one comparison is decided.
|
|
4
|
+
* Pure: no I/O, no manager. The evaluator and the validator both import it,
|
|
5
|
+
* so the set of operators lives here and nowhere else.
|
|
6
|
+
*/
|
|
7
|
+
/** The types an attribute can be declared with. */
|
|
8
|
+
export const ATTRIBUTE_TYPES = ['string', 'integer', 'boolean', 'identity', 'list'];
|
|
9
|
+
/**
|
|
10
|
+
* How deep a condition may nest. Every node counts, leaves included, so
|
|
11
|
+
* `any-of` > `all-of` > `not` > leaf is 4 and one more level is refused.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_CONDITION_DEPTH = 4;
|
|
14
|
+
/**
|
|
15
|
+
* How wide a condition may be (#2261). Depth alone leaves the tree as wide as
|
|
16
|
+
* a config file can make it, and the evaluator walks it for every page a
|
|
17
|
+
* listing or search asks about. A literal `in` list, the children of one
|
|
18
|
+
* `any-of` or `all-of`, and the nodes of the whole condition are each capped.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_IN_ITEMS = 256;
|
|
21
|
+
export const MAX_GROUP_CHILDREN = 256;
|
|
22
|
+
export const MAX_CONDITION_NODES = 512;
|
|
23
|
+
const SCALARS = ['string', 'integer', 'boolean', 'identity'];
|
|
24
|
+
/** The operators that compare one attribute with a value. */
|
|
25
|
+
export const LEAF_OPERATORS = {
|
|
26
|
+
equals: { left: SCALARS },
|
|
27
|
+
'not-equals': { left: SCALARS },
|
|
28
|
+
in: { left: ['string', 'integer', 'identity'] },
|
|
29
|
+
'greater-or-equal': { left: ['integer'] },
|
|
30
|
+
'less-or-equal': { left: ['integer'] }
|
|
31
|
+
};
|
|
32
|
+
/** The operators that combine conditions. `not` takes one condition, these take a list. */
|
|
33
|
+
export const GROUP_OPERATORS = ['any-of', 'all-of'];
|
|
34
|
+
export const NOT_OPERATOR = 'not';
|
|
35
|
+
/** Every operator, in the order the validator lists them. */
|
|
36
|
+
export const CONDITION_OPERATORS = [...Object.keys(LEAF_OPERATORS), ...GROUP_OPERATORS, NOT_OPERATOR];
|
|
37
|
+
/** The type a literal value has, or `null` when it is not a usable literal. */
|
|
38
|
+
export function literalType(value) {
|
|
39
|
+
if (typeof value === 'string')
|
|
40
|
+
return 'string';
|
|
41
|
+
if (typeof value === 'boolean')
|
|
42
|
+
return 'boolean';
|
|
43
|
+
if (typeof value === 'number' && Number.isInteger(value))
|
|
44
|
+
return 'integer';
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** Can a literal be compared with an attribute of this type? An identity is a string. */
|
|
48
|
+
export function literalFits(attributeType, value) {
|
|
49
|
+
const type = literalType(value);
|
|
50
|
+
if (type === null)
|
|
51
|
+
return false;
|
|
52
|
+
return attributeType === 'identity' ? type === 'string' : attributeType === type;
|
|
53
|
+
}
|
|
54
|
+
/** A value a comparison can use: a string, a boolean, or a whole number (not NaN, ±Infinity or 2.5). */
|
|
55
|
+
function isComparable(value) {
|
|
56
|
+
return typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isInteger(value));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Does a value read for an attribute fit the type it is declared with? A
|
|
60
|
+
* value that does not is treated as missing by the reader, so a comparison on
|
|
61
|
+
* it is unknown and fails closed, not a definite false or true (#2261). Never
|
|
62
|
+
* coerces: "5" is not an integer. An empty string is a string, a value and not
|
|
63
|
+
* a missing one.
|
|
64
|
+
*/
|
|
65
|
+
export function valueFits(attributeType, value) {
|
|
66
|
+
switch (attributeType) {
|
|
67
|
+
case 'integer':
|
|
68
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
69
|
+
case 'boolean':
|
|
70
|
+
return typeof value === 'boolean';
|
|
71
|
+
case 'list':
|
|
72
|
+
return Array.isArray(value) && value.every(isComparable);
|
|
73
|
+
default: // string, identity
|
|
74
|
+
return typeof value === 'string';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Decides one comparison once both sides are read. `undefined` is unknown: a
|
|
79
|
+
* side that is missing, or not of a type the operator can compare. Both sides
|
|
80
|
+
* must be the same kind of value (a string, a boolean or a whole number), so
|
|
81
|
+
* "5" never equals 5 and NaN never compares. `in` takes an array, or a Set the
|
|
82
|
+
* evaluator built from a literal list; every entry of an array must be the
|
|
83
|
+
* kind of value on the left. An empty string is a value, not a missing one.
|
|
84
|
+
*/
|
|
85
|
+
export function compareLeaf(operator, left, right) {
|
|
86
|
+
if (!isComparable(left))
|
|
87
|
+
return undefined;
|
|
88
|
+
switch (operator) {
|
|
89
|
+
case 'equals':
|
|
90
|
+
return isComparable(right) && typeof right === typeof left ? left === right : undefined;
|
|
91
|
+
case 'not-equals':
|
|
92
|
+
return isComparable(right) && typeof right === typeof left ? left !== right : undefined;
|
|
93
|
+
case 'in':
|
|
94
|
+
if (right instanceof Set)
|
|
95
|
+
return right.has(left);
|
|
96
|
+
return Array.isArray(right) && right.every((v) => isComparable(v) && typeof v === typeof left) ? right.includes(left) : undefined;
|
|
97
|
+
case 'greater-or-equal':
|
|
98
|
+
return typeof left === 'number' && typeof right === 'number' && Number.isInteger(right) ? left >= right : undefined;
|
|
99
|
+
case 'less-or-equal':
|
|
100
|
+
return typeof left === 'number' && typeof right === 'number' && Number.isInteger(right) ? left <= right : undefined;
|
|
101
|
+
default:
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** `value` as an assurance level, or `undefined` when it is not 1, 2 or 3. Never coerces. */
|
|
106
|
+
export function toAal(value) {
|
|
107
|
+
return value === 1 || value === 2 || value === 3 ? value : undefined;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The subject's `subject:aal`, or `undefined` when it has none (#2261).
|
|
111
|
+
*
|
|
112
|
+
* - Not signed in, or a share visitor: none.
|
|
113
|
+
* - An agent token: the level stored with the token. A token made before this
|
|
114
|
+
* stored one has none.
|
|
115
|
+
* - A background job: the level stored with the job's request. A job with
|
|
116
|
+
* none (a schedule, boot, a request from before this) has none.
|
|
117
|
+
* - A bearer JWT that records no level (Authentik): none. It is not a session
|
|
118
|
+
* from before sign-ins were recorded, so the AAL 1 rule below does not apply.
|
|
119
|
+
* - A session: its sign-in record's level; a session from before sign-ins
|
|
120
|
+
* were recorded (#1523) is AAL 1, as `AuthManager` treats it.
|
|
121
|
+
*
|
|
122
|
+
* Capped at `aalCap`, as a role is. Reads the record; it never works the level
|
|
123
|
+
* out from factors.
|
|
124
|
+
*/
|
|
125
|
+
export function effectiveAal(subject) {
|
|
126
|
+
if (!subject?.isAuthenticated || subject.viaShare || subject.bearerWithoutLevel)
|
|
127
|
+
return undefined;
|
|
128
|
+
let raw;
|
|
129
|
+
if (subject.viaToken)
|
|
130
|
+
raw = toAal(subject.viaToken.aal);
|
|
131
|
+
else if (subject.resolveRolesNow)
|
|
132
|
+
raw = toAal(subject.grantedAal);
|
|
133
|
+
else
|
|
134
|
+
raw = toAal(subject.signIn?.aal) ?? (subject.signIn ? undefined : 1);
|
|
135
|
+
if (raw === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
return toAal(Math.min(raw, typeof subject.aalCap === 'number' && subject.aalCap >= 1 ? subject.aalCap : 3));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The fields of a request's subject that bear on its level, copied by name so
|
|
141
|
+
* a subject rebuilt for a decision keeps them (the #1164 lesson: a field not
|
|
142
|
+
* forwarded is silently lost). Reads nothing else from it.
|
|
143
|
+
*/
|
|
144
|
+
export function aalFactsOf(subject) {
|
|
145
|
+
const s = (subject ?? {});
|
|
146
|
+
return {
|
|
147
|
+
signIn: s.signIn,
|
|
148
|
+
aalCap: typeof s.aalCap === 'number' ? s.aalCap : undefined,
|
|
149
|
+
viaToken: s.viaToken,
|
|
150
|
+
viaShare: s.viaShare,
|
|
151
|
+
bearerWithoutLevel: s.bearerWithoutLevel,
|
|
152
|
+
grantedAal: s.grantedAal,
|
|
153
|
+
resolveRolesNow: s.resolveRolesNow
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Where a subject attribute may be read from: `account:<field>`, `profile:<field>` or `sign-in`. */
|
|
157
|
+
export const SUBJECT_SOURCES = ['account:', 'profile:', 'sign-in'];
|
|
158
|
+
/**
|
|
159
|
+
* `id` and `aal` mean what they mean here, whatever the configuration (or an
|
|
160
|
+
* add-on's default-config.json) says (#2287). `aal` is read from the sign-in
|
|
161
|
+
* record and `id` from the account; a site that could redefine either could
|
|
162
|
+
* make `subject:aal` read a field it controls and pass a "second factor" rule.
|
|
163
|
+
*/
|
|
164
|
+
export const PINNED_SUBJECT_ATTRIBUTES = {
|
|
165
|
+
id: { type: 'identity', from: 'account:id' },
|
|
166
|
+
aal: { type: 'integer', from: 'sign-in' }
|
|
167
|
+
};
|
|
168
|
+
/** The subject attributes a site declares, with `id` and `aal` pinned to their built-in meaning. Anything not an object is ignored. */
|
|
169
|
+
export function declaredSubjectAttributes(raw) {
|
|
170
|
+
const declared = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
171
|
+
return { ...declared, ...PINNED_SUBJECT_ATTRIBUTES };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* What is wrong with the value of `ngdpbase.access.subject-attributes` itself
|
|
175
|
+
* (#2287), one message each: not an object; `id` or `aal` declared with any
|
|
176
|
+
* meaning but the built-in one (the built-in is kept regardless; the same
|
|
177
|
+
* declaration as the built-in is harmless and not reported); and any other
|
|
178
|
+
* declaration `subjectDeclarationProblem` refuses. Used on save, on load and at boot.
|
|
179
|
+
*/
|
|
180
|
+
export function subjectAttributesProblems(raw, sensitive) {
|
|
181
|
+
if (raw === null || raw === undefined)
|
|
182
|
+
return [];
|
|
183
|
+
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
184
|
+
return ['ngdpbase.access.subject-attributes must be an object of attribute name to { type, from }'];
|
|
185
|
+
return Object.entries(raw).flatMap(([name, decl]) => {
|
|
186
|
+
const pinned = Object.hasOwn(PINNED_SUBJECT_ATTRIBUTES, name) ? PINNED_SUBJECT_ATTRIBUTES[name] : undefined;
|
|
187
|
+
if (pinned) {
|
|
188
|
+
const same = !!decl && typeof decl === 'object' && decl.type === pinned.type && decl.from === pinned.from;
|
|
189
|
+
return same ? [] : [`subject attribute '${name}' in ngdpbase.access.subject-attributes cannot be redefined: it is built in (type ${pinned.type}, from ${pinned.from}) and keeps that meaning; remove it`];
|
|
190
|
+
}
|
|
191
|
+
const problem = subjectDeclarationProblem(name, decl, sensitive);
|
|
192
|
+
return problem ? [problem] : [];
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* What is wrong with a subject attribute's declaration, or `null`: a `type`
|
|
197
|
+
* that is not known, a `from` that is not a subject source, or an account field
|
|
198
|
+
* that must never be read (`sensitive`, the password, email, roles and the
|
|
199
|
+
* like). Said so a condition naming it is refused rather than silently unknown.
|
|
200
|
+
*/
|
|
201
|
+
export function subjectDeclarationProblem(name, decl, sensitive) {
|
|
202
|
+
const where = `subject attribute '${name}' in ngdpbase.access.subject-attributes`;
|
|
203
|
+
if (!decl || typeof decl !== 'object')
|
|
204
|
+
return `${where} must be an object with a type and a from`;
|
|
205
|
+
if (!ATTRIBUTE_TYPES.includes(decl.type))
|
|
206
|
+
return `${where} has type ${JSON.stringify(decl.type)}; use one of ${ATTRIBUTE_TYPES.join(', ')}`;
|
|
207
|
+
const from = decl.from;
|
|
208
|
+
const source = typeof from === 'string' ? SUBJECT_SOURCES.find((s) => from === s || (s !== 'sign-in' && from.startsWith(s))) : undefined;
|
|
209
|
+
if (!source)
|
|
210
|
+
return `${where} reads from ${JSON.stringify(from)}; start it with ${SUBJECT_SOURCES.join(', ')}`;
|
|
211
|
+
if (source !== 'sign-in') {
|
|
212
|
+
const field = from.slice(source.length);
|
|
213
|
+
if (!field)
|
|
214
|
+
return `${where} reads from '${source}' with no field; write ${source}<field>`;
|
|
215
|
+
if (sensitive.includes(field))
|
|
216
|
+
return `'${source}${field}' cannot be used in a condition`;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record the security posture at boot and compare it against the previous boot
|
|
3
|
+
* (#1156, D19 of docs/security-posture.md).
|
|
4
|
+
*
|
|
5
|
+
* `config-change` (#1150) audits every change made through `setProperty()`,
|
|
6
|
+
* which leaves two holes:
|
|
7
|
+
*
|
|
8
|
+
* - An `app-custom-config.json` edited __directly on disk__ never passes
|
|
9
|
+
* through `setProperty()` and emits nothing.
|
|
10
|
+
* - The state an instance __started in__ is never stated, only its deltas.
|
|
11
|
+
*
|
|
12
|
+
* Recording the posture at every start closes the second. Comparing that record
|
|
13
|
+
* against the previous start's closes the first: a change made on disk, or
|
|
14
|
+
* while the process was stopped, shows up as a difference between two
|
|
15
|
+
* consecutive boots __even though nothing observed the edit itself__.
|
|
16
|
+
*
|
|
17
|
+
* This is not the self-scoring D20 rejects. The comparison is against this
|
|
18
|
+
* instance's own previous state, a fact it holds, rather than against a
|
|
19
|
+
* recommended value set nobody can define.
|
|
20
|
+
*/
|
|
21
|
+
import type { PostureGroup } from './securityPosture.js';
|
|
22
|
+
/** What a secret ingredient records instead of its value. */
|
|
23
|
+
export declare const SECRET_PLACEHOLDER = "[secret]";
|
|
24
|
+
/** The posture as recorded: one flat map of ingredient to value. */
|
|
25
|
+
export type FlatPosture = Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Flatten the grouped posture for recording.
|
|
28
|
+
*
|
|
29
|
+
* A secret records that it is __set__ and never what it is set to. An entry
|
|
30
|
+
* naming a key alongside its value would reintroduce the disclosure
|
|
31
|
+
* `ngdpbase.config.sensitive-values` (its `secret` entries) exists to prevent, by a different route and
|
|
32
|
+
* into a file with longer retention than the logs it already guards.
|
|
33
|
+
*/
|
|
34
|
+
export declare function flattenPosture(groups: readonly PostureGroup[]): FlatPosture;
|
|
35
|
+
export interface PostureDiff {
|
|
36
|
+
/**
|
|
37
|
+
* False when there is no previous record to compare against.
|
|
38
|
+
*
|
|
39
|
+
* Kept separate from "nothing changed", because they are different facts and
|
|
40
|
+
* only one of them is reassuring.
|
|
41
|
+
*/
|
|
42
|
+
comparable: boolean;
|
|
43
|
+
changed: Array<{
|
|
44
|
+
key: string;
|
|
45
|
+
from: unknown;
|
|
46
|
+
to: unknown;
|
|
47
|
+
}>;
|
|
48
|
+
added: string[];
|
|
49
|
+
removed: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compare the posture now against the posture at the previous boot.
|
|
53
|
+
*
|
|
54
|
+
* __No previous record is `comparable: false`, never "no change".__ Only the
|
|
55
|
+
* last 1000 log lines are loaded for search (`FileAuditProvider.ts:610`), so on
|
|
56
|
+
* a busy instance the previous boot's record can fall outside the window.
|
|
57
|
+
* Reporting that as "nothing changed" would be a false all-clear at exactly the
|
|
58
|
+
* moment an operator is relying on the check.
|
|
59
|
+
*
|
|
60
|
+
* An ingredient added to or removed from the __view__ is reported separately
|
|
61
|
+
* from a __value__ change: removing an ingredient changes nothing about what
|
|
62
|
+
* the instance does (D4, D16), and folding both into "posture changed" would
|
|
63
|
+
* lose the distinction that matters when reading the log back.
|
|
64
|
+
*/
|
|
65
|
+
export declare function diffPostures(previous: FlatPosture | null, current: FlatPosture): PostureDiff;
|
|
66
|
+
/**
|
|
67
|
+
* The line an operator reads at startup.
|
|
68
|
+
*
|
|
69
|
+
* A difference found here says explicitly that nothing observed the edit —
|
|
70
|
+
* otherwise the obvious question is why it is being reported at boot rather
|
|
71
|
+
* than as a `config-change`, and the answer is the whole point.
|
|
72
|
+
*/
|
|
73
|
+
export declare function describePostureDiff(diff: PostureDiff): string;
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* every vault below it (docs/private-stores.md, Access).
|
|
4
4
|
*
|
|
5
5
|
* The owner, a share link the owner issued, or someone acting for the owner
|
|
6
|
-
* under a proxy grant that carries `vault-proxy` (#1762). No role reaches in
|
|
7
|
-
*
|
|
6
|
+
* under a proxy grant that carries `vault-proxy` (#1762). No role reaches in
|
|
7
|
+
* through this rule. At a door, an administrator may also enter a vault that
|
|
8
|
+
* is not encrypted when `ngdpbase.vaults.admin-access` is true (#2255,
|
|
9
|
+
* {@link mayEnterVaultAtDoor}), and each such access is recorded.
|
|
8
10
|
*
|
|
9
11
|
* A share link gets in only for a vault it names (#1388), and only when the
|
|
10
12
|
* caller names that vault: a question about the owner's container as a whole
|
|
@@ -25,8 +27,12 @@
|
|
|
25
27
|
* slice 4).
|
|
26
28
|
*/
|
|
27
29
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
30
|
+
/** The administrator's way into a vault (#2255): `PolicyDecisionPoint.adminMayEnterVault`. */
|
|
31
|
+
interface AdminVaultEntry {
|
|
32
|
+
adminMayEnterVault?(ctx: unknown, owner: string, vault: string, resource: string, record?: boolean): Promise<boolean>;
|
|
33
|
+
}
|
|
28
34
|
/** The PDP surface a vault decision needs. */
|
|
29
|
-
export interface VaultDecider {
|
|
35
|
+
export interface VaultDecider extends AdminVaultEntry {
|
|
30
36
|
decide(subject: unknown, request: {
|
|
31
37
|
action: string;
|
|
32
38
|
resource: {
|
|
@@ -36,10 +42,12 @@ export interface VaultDecider {
|
|
|
36
42
|
attributes: Record<string, unknown>;
|
|
37
43
|
}): Promise<{
|
|
38
44
|
permit: boolean;
|
|
45
|
+
policyIds?: string[];
|
|
46
|
+
conditionNotMet?: string[];
|
|
39
47
|
}>;
|
|
40
48
|
}
|
|
41
49
|
/** The PDP surface {@link holdsInVault} needs. */
|
|
42
|
-
export interface VaultHolder {
|
|
50
|
+
export interface VaultHolder extends AdminVaultEntry {
|
|
43
51
|
holds(subject: unknown, action: string, on: {
|
|
44
52
|
resource: {
|
|
45
53
|
type: string;
|
|
@@ -48,6 +56,17 @@ export interface VaultHolder {
|
|
|
48
56
|
attributes: Record<string, unknown>;
|
|
49
57
|
}): Promise<boolean>;
|
|
50
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The container rule at a door (#2255): the owner or their delegate
|
|
61
|
+
* ({@link mayActInPrivateContainer}), or else an administrator let in by
|
|
62
|
+
* `ngdpbase.vaults.admin-access` to a vault that is not encrypted, recorded
|
|
63
|
+
* when `record`. Lists never ask this; they keep the owner's rule alone.
|
|
64
|
+
*/
|
|
65
|
+
export declare function mayEnterVaultAtDoor(pdp: AdminVaultEntry | null | undefined, ctx: ActorContext, where: {
|
|
66
|
+
owner: string;
|
|
67
|
+
vault: string;
|
|
68
|
+
resource: string;
|
|
69
|
+
}, record?: boolean): Promise<boolean>;
|
|
51
70
|
export declare function mayActInPrivateContainer(ctx: ActorContext, owner: string, opts?: {
|
|
52
71
|
vault?: string;
|
|
53
72
|
}): boolean;
|
|
@@ -80,3 +99,4 @@ export declare function holdsInVault(pdp: VaultHolder | null | undefined, ctx: A
|
|
|
80
99
|
vault: string;
|
|
81
100
|
resource: string;
|
|
82
101
|
}): Promise<boolean>;
|
|
102
|
+
export {};
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* every vault below it (docs/private-stores.md, Access).
|
|
4
4
|
*
|
|
5
5
|
* The owner, a share link the owner issued, or someone acting for the owner
|
|
6
|
-
* under a proxy grant that carries `vault-proxy` (#1762). No role reaches in
|
|
7
|
-
*
|
|
6
|
+
* under a proxy grant that carries `vault-proxy` (#1762). No role reaches in
|
|
7
|
+
* through this rule. At a door, an administrator may also enter a vault that
|
|
8
|
+
* is not encrypted when `ngdpbase.vaults.admin-access` is true (#2255,
|
|
9
|
+
* {@link mayEnterVaultAtDoor}), and each such access is recorded.
|
|
8
10
|
*
|
|
9
11
|
* A share link gets in only for a vault it names (#1388), and only when the
|
|
10
12
|
* caller names that vault: a question about the owner's container as a whole
|
|
@@ -29,6 +31,19 @@ import { shareNamesVault } from '../types/Share.js';
|
|
|
29
31
|
import { mixesProxyWithDelegation, proxyCarries } from './proxyClaim.js';
|
|
30
32
|
import { isSharedFolderOwner } from './privateStorePath.js';
|
|
31
33
|
import { permitsInSharedFolder } from './sharedFolderAccess.js';
|
|
34
|
+
/**
|
|
35
|
+
* The container rule at a door (#2255): the owner or their delegate
|
|
36
|
+
* ({@link mayActInPrivateContainer}), or else an administrator let in by
|
|
37
|
+
* `ngdpbase.vaults.admin-access` to a vault that is not encrypted, recorded
|
|
38
|
+
* when `record`. Lists never ask this; they keep the owner's rule alone.
|
|
39
|
+
*/
|
|
40
|
+
export async function mayEnterVaultAtDoor(pdp, ctx, where, record = true) {
|
|
41
|
+
if (mayActInPrivateContainer(ctx, where.owner, { vault: where.vault }))
|
|
42
|
+
return true;
|
|
43
|
+
if (isSharedFolderOwner(where.owner) || typeof pdp?.adminMayEnterVault !== 'function')
|
|
44
|
+
return false;
|
|
45
|
+
return pdp.adminMayEnterVault(ctx, where.owner, where.vault, where.resource, record);
|
|
46
|
+
}
|
|
32
47
|
export function mayActInPrivateContainer(ctx, owner, opts = {}) {
|
|
33
48
|
// #2125: the shared folders' container has no owner who acts; a folder is
|
|
34
49
|
// decided by permission (`permitsInSharedFolder`), never by this rule.
|
|
@@ -67,7 +82,7 @@ export function mayActInPrivateContainer(ctx, owner, opts = {}) {
|
|
|
67
82
|
export async function permitsInVault(pdp, ctx, action, where) {
|
|
68
83
|
if (isSharedFolderOwner(where.owner))
|
|
69
84
|
return permitsInSharedFolder(pdp, ctx, action, where.vault);
|
|
70
|
-
if (!pdp || !
|
|
85
|
+
if (!pdp || !(await mayEnterVaultAtDoor(pdp, ctx, where)))
|
|
71
86
|
return false;
|
|
72
87
|
return (await pdp.decide(ctx, { action, resource: { type: 'page', id: where.resource }, attributes: { vault: where.vault } })).permit;
|
|
73
88
|
}
|
|
@@ -78,7 +93,7 @@ export async function permitsInVault(pdp, ctx, action, where) {
|
|
|
78
93
|
* folders are not asked: `permitsInVault` decides them by permission alone.
|
|
79
94
|
*/
|
|
80
95
|
export async function holdsInVault(pdp, ctx, action, where) {
|
|
81
|
-
if (isSharedFolderOwner(where.owner) || typeof pdp?.holds !== 'function' || !
|
|
96
|
+
if (isSharedFolderOwner(where.owner) || typeof pdp?.holds !== 'function' || !(await mayEnterVaultAtDoor(pdp, ctx, where, false)))
|
|
82
97
|
return false;
|
|
83
98
|
return pdp.holds(ctx, action, { resource: { type: 'page', id: where.resource }, attributes: { vault: where.vault } });
|
|
84
99
|
}
|
|
@@ -155,6 +155,12 @@ export interface PrivatePageName {
|
|
|
155
155
|
* also its URL. No account can be called this (`RESERVED_USERNAMES`).
|
|
156
156
|
*/
|
|
157
157
|
export declare const SHARED_FOLDER_OWNER = "shared-folders";
|
|
158
|
+
/**
|
|
159
|
+
* Whether `id` has the shape a store gives a file (`crypto.randomUUID()`), so
|
|
160
|
+
* a takeout's file can be stored under it again (#2188). Anything else, a
|
|
161
|
+
* path or a pool's content hash among them, is never reused as a file name.
|
|
162
|
+
*/
|
|
163
|
+
export declare function isRestorableFileId(id: string): boolean;
|
|
158
164
|
/** A shared folder page's name, and its URL without the leading slash: `shared/{folder}/{title}`. */
|
|
159
165
|
export declare const SHARED_PAGE_NAME_PREFIX = "shared/";
|
|
160
166
|
/** The first path segment of a shared folder's URL: `/shared/{folder}/{title}`. */
|