@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
|
@@ -28,6 +28,7 @@ import type { AgentTokenGrant, PermissionSubject, ProxyClaim } from '../managers
|
|
|
28
28
|
import type { ShareGrant } from '../types/Share.js';
|
|
29
29
|
import { type ReauthMode } from '../managers/AuthManager.js';
|
|
30
30
|
import { type Decision } from '../types/Policy.js';
|
|
31
|
+
import type { OwnedAuditResource } from '../security/ownedRecords.js';
|
|
31
32
|
/**
|
|
32
33
|
* Thrown by ApiContext guard methods (requirePermission, actingUsername).
|
|
33
34
|
* Route handlers should catch this and forward `status` to `res.status()`.
|
|
@@ -201,8 +202,14 @@ export declare class ApiContext extends BaseContext {
|
|
|
201
202
|
* - an allow: a programming error. A refusal never silently passes.
|
|
202
203
|
*
|
|
203
204
|
* @example
|
|
204
|
-
* const d = await pdp.
|
|
205
|
-
* if (!d.permit) await ctx.enforceDeny('
|
|
205
|
+
* const d = await pdp.decideOwnedRecord(ctx.subject, { action: 'phr-read', resource: { type: 'phr-record', id, attributes: { owner } } });
|
|
206
|
+
* if (!d.permit) await ctx.enforceDeny('phr-read', d, { type: 'phr-record', id, owner, ownerUser });
|
|
206
207
|
*/
|
|
207
|
-
enforceDeny(permission: string, decision: Decision): Promise<never>;
|
|
208
|
+
enforceDeny(permission: string, decision: Decision, resource?: OwnedAuditResource): Promise<never>;
|
|
209
|
+
/**
|
|
210
|
+
* The refusal of one add-on record (#1823), recorded before the error is
|
|
211
|
+
* thrown. `authorization-deny` is continue-on-failure, so a lost record does
|
|
212
|
+
* not change the refusal. `owner` is the stored identity @id.
|
|
213
|
+
*/
|
|
214
|
+
private recordOwnedDenial;
|
|
208
215
|
}
|
|
@@ -6,6 +6,9 @@ import { BaseContext } from './BaseContext.js';
|
|
|
6
6
|
import { answeredWithPage } from '../managers/AuthManager.js';
|
|
7
7
|
import { STEP_UP_REASON } from '../types/Policy.js';
|
|
8
8
|
import { CONSENT_PATH } from '../types/Consent.js';
|
|
9
|
+
import { actorOf } from './ActorContext.js';
|
|
10
|
+
import { AUDIT_EVENT } from '../utils/auditEventNames.js';
|
|
11
|
+
import { recordAuditEvent } from '../utils/auditEvents.js';
|
|
9
12
|
// ── ApiError ────────────────────────────────────────────────────────────────
|
|
10
13
|
/**
|
|
11
14
|
* Thrown by ApiContext guard methods (requirePermission, actingUsername).
|
|
@@ -253,7 +256,7 @@ export class ApiContext extends BaseContext {
|
|
|
253
256
|
// one is asked as #1745 always has: holds the permission, may not act now —
|
|
254
257
|
// exactly the condition under which the PDP writes STEP_UP_REASON.
|
|
255
258
|
const stepUp = Boolean(this.request) && await this.holdsPermission(permission);
|
|
256
|
-
await this.enforceDeny(permission, { permit: false, applicable: true, reason: stepUp ? STEP_UP_REASON : 'policy_deny' });
|
|
259
|
+
await this.enforceDeny(permission, { permit: false, applicable: true, reason: stepUp ? STEP_UP_REASON : 'policy_deny', policyIds: [] });
|
|
257
260
|
}
|
|
258
261
|
/**
|
|
259
262
|
* Turn a deny into the refusal (#1431) — the one implementation of refusal
|
|
@@ -269,13 +272,15 @@ export class ApiContext extends BaseContext {
|
|
|
269
272
|
* - an allow: a programming error. A refusal never silently passes.
|
|
270
273
|
*
|
|
271
274
|
* @example
|
|
272
|
-
* const d = await pdp.
|
|
273
|
-
* if (!d.permit) await ctx.enforceDeny('
|
|
275
|
+
* const d = await pdp.decideOwnedRecord(ctx.subject, { action: 'phr-read', resource: { type: 'phr-record', id, attributes: { owner } } });
|
|
276
|
+
* if (!d.permit) await ctx.enforceDeny('phr-read', d, { type: 'phr-record', id, owner, ownerUser });
|
|
274
277
|
*/
|
|
275
|
-
async enforceDeny(permission, decision) {
|
|
278
|
+
async enforceDeny(permission, decision, resource) {
|
|
276
279
|
if (decision.permit) {
|
|
277
280
|
throw new Error(`ApiContext.enforceDeny('${permission}') was handed an allow decision: it enforces only a deny`);
|
|
278
281
|
}
|
|
282
|
+
if (resource)
|
|
283
|
+
await this.recordOwnedDenial(permission, decision, resource);
|
|
279
284
|
if (decision.reason === STEP_UP_REASON && this.request) {
|
|
280
285
|
// #1758: step-up is AuthManager's — the answer and its reauth-prompt record.
|
|
281
286
|
const authManager = this.engine.getManager('AuthManager');
|
|
@@ -288,4 +293,32 @@ export class ApiContext extends BaseContext {
|
|
|
288
293
|
? new ApiError(403, 'Forbidden')
|
|
289
294
|
: new ApiError(401, 'Authentication required');
|
|
290
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* The refusal of one add-on record (#1823), recorded before the error is
|
|
298
|
+
* thrown. `authorization-deny` is continue-on-failure, so a lost record does
|
|
299
|
+
* not change the refusal. `owner` is the stored identity @id.
|
|
300
|
+
*/
|
|
301
|
+
async recordOwnedDenial(permission, decision, resource) {
|
|
302
|
+
const who = this.subject
|
|
303
|
+
? actorOf(this.subject)
|
|
304
|
+
: { user: 'anonymous', ipAddress: this.ipAddress, metadata: { origin: 'request' } };
|
|
305
|
+
const sink = this.engine.getManager?.('AuditManager');
|
|
306
|
+
await recordAuditEvent(sink, {
|
|
307
|
+
eventType: AUDIT_EVENT.AUTHORIZATION_DENY,
|
|
308
|
+
...who,
|
|
309
|
+
action: permission,
|
|
310
|
+
resource: resource.id ?? '',
|
|
311
|
+
resourceType: resource.type,
|
|
312
|
+
result: 'deny',
|
|
313
|
+
reason: decision.reason || 'not permitted',
|
|
314
|
+
severity: 'medium',
|
|
315
|
+
metadata: {
|
|
316
|
+
...who.metadata,
|
|
317
|
+
...(resource.owner ? { owner: resource.owner } : {}),
|
|
318
|
+
...(resource.ownerUser ? { ownerUser: resource.ownerUser } : {}),
|
|
319
|
+
...(decision.policyIds?.length ? { policyIds: [...decision.policyIds] } : {}),
|
|
320
|
+
...(decision.conditionNotMet?.length ? { conditionNotMet: [...decision.conditionNotMet] } : {})
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
291
324
|
}
|
|
@@ -67,19 +67,13 @@ export declare abstract class BaseContext {
|
|
|
67
67
|
* asked of the PDP (#1431 step 14): the token and share ceilings, then the
|
|
68
68
|
* policies, with deny policies and role expansion.
|
|
69
69
|
*
|
|
70
|
-
* For a check against a PAGE, ask
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* resource type can decide (e.g. `page-public`: may this person make pages
|
|
76
|
-
* of this system-category public?). It is the policy question alone: unlike
|
|
77
|
-
* `canAccess` it walks none of one page's own tiers (private owner,
|
|
78
|
-
* frontmatter), so a page's owner gets no pass on it.
|
|
70
|
+
* For a check against a PAGE, ask {@link hasPermissionOn}: the PDP then
|
|
71
|
+
* reads the page's own attributes (its system-category, its owner) from the
|
|
72
|
+
* PIP (#2255). Asked here, a page write (`page-edit`, `-delete`, `-rename`)
|
|
73
|
+
* means "on some category", which is right for an affordance or a token's
|
|
74
|
+
* scope and wrong for a door.
|
|
79
75
|
*/
|
|
80
|
-
hasPermission(action: string
|
|
81
|
-
'system-category'?: unknown;
|
|
82
|
-
}): Promise<boolean>;
|
|
76
|
+
hasPermission(action: string): Promise<boolean>;
|
|
83
77
|
/**
|
|
84
78
|
* Does this request's subject HOLD the permission (#1635)? Policy alone,
|
|
85
79
|
* without the step-up freshness rule — for affordances only (show this
|
|
@@ -90,16 +84,22 @@ export declare abstract class BaseContext {
|
|
|
90
84
|
/** One answer per question for this request — `hasPermission` and `hasPermissionOn` share it. */
|
|
91
85
|
private cached;
|
|
92
86
|
/**
|
|
93
|
-
* A capability asked about the page it is for (#1539). For a page in
|
|
94
|
-
* vault: the container rule first — the owner, never a role, the same
|
|
95
|
-
* as Tier 0 of the page tiers — then the capability with the page's
|
|
96
|
-
* `vault`,
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* a named page asks this instead of
|
|
100
|
-
*
|
|
87
|
+
* A capability asked about the page it is for (#1539, #2255). For a page in
|
|
88
|
+
* a vault: the container rule first — the owner, never a role, the same
|
|
89
|
+
* rule as Tier 0 of the page tiers — then the capability with the page's
|
|
90
|
+
* `vault`, so a policy on the `vault` resource type (`vault-owner`)
|
|
91
|
+
* decides. For any other page, the PDP is asked about the page by name and
|
|
92
|
+
* reads its system-category and owner from the PIP, so the policies on its
|
|
93
|
+
* category decide. A route that acts on a named page asks this instead of
|
|
94
|
+
* the bare capability.
|
|
101
95
|
*/
|
|
102
96
|
hasPermissionOn(action: string, pageName: string): Promise<boolean>;
|
|
97
|
+
/**
|
|
98
|
+
* {@link hasPermissionOn} as an affordance (#2255): does the subject HOLD
|
|
99
|
+
* `action` on this page, policy alone, without the step-up rule? So the
|
|
100
|
+
* step-up fallback of a page-scoped check asks about the same page.
|
|
101
|
+
*/
|
|
102
|
+
holdsPermissionOn(action: string, pageName: string): Promise<boolean>;
|
|
103
103
|
/**
|
|
104
104
|
* The identifiers that match audience-style filters — every role, plus the
|
|
105
105
|
* username when there is one. Used where running the full evaluator per
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
import { parsePrivatePageName } from '../utils/privateStorePath.js';
|
|
3
|
-
import { permitsInVault } from '../utils/privateStoreAccess.js';
|
|
3
|
+
import { holdsInVault, permitsInVault } from '../utils/privateStoreAccess.js';
|
|
4
4
|
export class BaseContext {
|
|
5
5
|
/**
|
|
6
6
|
* The caller's subject as the middleware wrote it, or `null` for a context
|
|
@@ -57,25 +57,19 @@ export class BaseContext {
|
|
|
57
57
|
* asked of the PDP (#1431 step 14): the token and share ceilings, then the
|
|
58
58
|
* policies, with deny policies and role expansion.
|
|
59
59
|
*
|
|
60
|
-
* For a check against a PAGE, ask
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* resource type can decide (e.g. `page-public`: may this person make pages
|
|
66
|
-
* of this system-category public?). It is the policy question alone: unlike
|
|
67
|
-
* `canAccess` it walks none of one page's own tiers (private owner,
|
|
68
|
-
* frontmatter), so a page's owner gets no pass on it.
|
|
60
|
+
* For a check against a PAGE, ask {@link hasPermissionOn}: the PDP then
|
|
61
|
+
* reads the page's own attributes (its system-category, its owner) from the
|
|
62
|
+
* PIP (#2255). Asked here, a page write (`page-edit`, `-delete`, `-rename`)
|
|
63
|
+
* means "on some category", which is right for an affordance or a token's
|
|
64
|
+
* scope and wrong for a door.
|
|
69
65
|
*/
|
|
70
|
-
async hasPermission(action
|
|
66
|
+
async hasPermission(action) {
|
|
71
67
|
const pdp = this.engineRef.getManager('PolicyDecisionPoint');
|
|
72
68
|
if (!pdp)
|
|
73
69
|
return false;
|
|
74
70
|
// #637: hand over the already-resolved subject so the PDP need not
|
|
75
71
|
// resolve the account and its roles again.
|
|
76
|
-
return this.cached(
|
|
77
|
-
? pdp.decide(this.getActor(), { action, resource: { type: 'page', id: '*' }, attributes }).then((d) => d.permit)
|
|
78
|
-
: pdp.permits(this.getActor(), action)));
|
|
72
|
+
return this.cached(action, () => pdp.permits(this.getActor(), action));
|
|
79
73
|
}
|
|
80
74
|
/**
|
|
81
75
|
* Does this request's subject HOLD the permission (#1635)? Policy alone,
|
|
@@ -101,24 +95,47 @@ export class BaseContext {
|
|
|
101
95
|
return promise;
|
|
102
96
|
}
|
|
103
97
|
/**
|
|
104
|
-
* A capability asked about the page it is for (#1539). For a page in
|
|
105
|
-
* vault: the container rule first — the owner, never a role, the same
|
|
106
|
-
* as Tier 0 of the page tiers — then the capability with the page's
|
|
107
|
-
* `vault`,
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* a named page asks this instead of
|
|
111
|
-
*
|
|
98
|
+
* A capability asked about the page it is for (#1539, #2255). For a page in
|
|
99
|
+
* a vault: the container rule first — the owner, never a role, the same
|
|
100
|
+
* rule as Tier 0 of the page tiers — then the capability with the page's
|
|
101
|
+
* `vault`, so a policy on the `vault` resource type (`vault-owner`)
|
|
102
|
+
* decides. For any other page, the PDP is asked about the page by name and
|
|
103
|
+
* reads its system-category and owner from the PIP, so the policies on its
|
|
104
|
+
* category decide. A route that acts on a named page asks this instead of
|
|
105
|
+
* the bare capability.
|
|
112
106
|
*/
|
|
113
107
|
async hasPermissionOn(action, pageName) {
|
|
114
108
|
const name = parsePrivatePageName(pageName);
|
|
115
|
-
if (!name)
|
|
116
|
-
|
|
109
|
+
if (!name) {
|
|
110
|
+
const pdp = this.engineRef.getManager('PolicyDecisionPoint');
|
|
111
|
+
if (!pdp)
|
|
112
|
+
return false;
|
|
113
|
+
// A stand-in decider with no `decide` (test doubles) answers the capability, as `holdsPermission` does.
|
|
114
|
+
if (typeof pdp.decide !== 'function')
|
|
115
|
+
return this.hasPermission(action);
|
|
116
|
+
return this.cached(`${action}\u0000${pageName}`, () => pdp.decide(this.getActor(), { action, resource: { type: 'page', id: pageName } }).then((d) => d.permit));
|
|
117
|
+
}
|
|
117
118
|
if (!this.hasSubject())
|
|
118
119
|
return false;
|
|
119
120
|
const pdp = this.engineRef.getManager('PolicyDecisionPoint');
|
|
120
121
|
return this.cached(`${action}\u0000${pageName}`, () => permitsInVault(pdp, this.getActor(), action, { owner: name.owner, vault: name.store, resource: pageName }));
|
|
121
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* {@link hasPermissionOn} as an affordance (#2255): does the subject HOLD
|
|
125
|
+
* `action` on this page, policy alone, without the step-up rule? So the
|
|
126
|
+
* step-up fallback of a page-scoped check asks about the same page.
|
|
127
|
+
*/
|
|
128
|
+
async holdsPermissionOn(action, pageName) {
|
|
129
|
+
const pdp = this.engineRef.getManager('PolicyDecisionPoint');
|
|
130
|
+
if (!pdp || typeof pdp.holds !== 'function')
|
|
131
|
+
return this.hasPermissionOn(action, pageName);
|
|
132
|
+
const name = parsePrivatePageName(pageName);
|
|
133
|
+
if (!name)
|
|
134
|
+
return this.cached(`holds\u0000${action}\u0000${pageName}`, () => pdp.holds(this.getActor(), action, { resource: { type: 'page', id: pageName } }));
|
|
135
|
+
if (!this.hasSubject())
|
|
136
|
+
return false;
|
|
137
|
+
return this.cached(`holds\u0000${action}\u0000${pageName}`, () => holdsInVault(pdp, this.getActor(), action, { owner: name.owner, vault: name.store, resource: pageName }));
|
|
138
|
+
}
|
|
122
139
|
/**
|
|
123
140
|
* The identifiers that match audience-style filters — every role, plus the
|
|
124
141
|
* username when there is one. Used where running the full evaluator per
|
|
@@ -67,6 +67,15 @@ export interface JobContext {
|
|
|
67
67
|
viaProxy?: ProxyClaim;
|
|
68
68
|
/** The address the requesting call came from (#1179), when there was one. Provenance for the record. */
|
|
69
69
|
ipAddress?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The assurance level (1 to 3) of the requester's session when the work was
|
|
72
|
+
* requested (#2261), which is the job's `subject:aal` whenever it runs. Taken
|
|
73
|
+
* from the requester's own session or token, never from an argument, so it
|
|
74
|
+
* cannot exceed it. Absent for work no session asked for (boot, a schedule,
|
|
75
|
+
* the command line) and for a job requested before it was stored: its
|
|
76
|
+
* `subject:aal` is missing and a condition on it fails closed.
|
|
77
|
+
*/
|
|
78
|
+
aal?: 1 | 2 | 3;
|
|
70
79
|
/** ISO 8601. When the work was requested, not when it ran. */
|
|
71
80
|
requestedAt: string;
|
|
72
81
|
/** Why, for origins with no person to ask. Free text, for the audit record. */
|
|
@@ -84,10 +93,23 @@ export interface JobContext {
|
|
|
84
93
|
/** The shape a request-bound context exposes; both WikiContext and ApiContext satisfy it. */
|
|
85
94
|
export interface RequestIdentity {
|
|
86
95
|
username?: string | null;
|
|
96
|
+
/** With `signIn`, `aalCap` and `viaToken`, what the level the job stores is read from (#2261). */
|
|
97
|
+
isAuthenticated?: boolean;
|
|
98
|
+
signIn?: {
|
|
99
|
+
aal?: unknown;
|
|
100
|
+
} | null;
|
|
101
|
+
aalCap?: number;
|
|
87
102
|
viaToken?: AgentTokenGrant;
|
|
88
103
|
viaShare?: ShareGrant;
|
|
89
104
|
viaProxy?: ProxyClaim;
|
|
90
105
|
ipAddress?: string;
|
|
106
|
+
/** An `ApiContext` holds the request's subject here, a `WikiContext` as `userContext`: the level is read from that, not from the wrapper (#2287). */
|
|
107
|
+
subject?: object | null;
|
|
108
|
+
userContext?: object | null;
|
|
109
|
+
/** A `JobContext` handed back in: it already holds the level its requester's session had. */
|
|
110
|
+
origin?: unknown;
|
|
111
|
+
requestedAt?: unknown;
|
|
112
|
+
aal?: unknown;
|
|
91
113
|
}
|
|
92
114
|
/**
|
|
93
115
|
* The username a request-origin job carries when the request had no identity.
|
|
@@ -150,6 +172,15 @@ export declare function jobContextFromOperator(username: string, reason: string,
|
|
|
150
172
|
* a context to pass, including code that runs with no request at all.
|
|
151
173
|
*/
|
|
152
174
|
export declare function jobContextFromSystem(systemPrincipal: string, reason: string, now?: Date): JobContext;
|
|
175
|
+
/**
|
|
176
|
+
* Boot work done in one user's own vaults, on that user's behalf (#1815) —
|
|
177
|
+
* the start-up vault upgrades. The job carries the owner's username, so the
|
|
178
|
+
* vault container rule admits it as it admits any job carrying who asked, and
|
|
179
|
+
* what it may do there is the owner's own rights, resolved by policy when
|
|
180
|
+
* each write is decided. Never the system principal, which no vault admits.
|
|
181
|
+
* Like every job it carries no key, so an encrypted vault stays closed to it.
|
|
182
|
+
*/
|
|
183
|
+
export declare function jobContextForOwner(owner: string, reason: string, now?: Date): JobContext;
|
|
153
184
|
/** As {@link jobContextFromSystem}, for work started by a timer rather than at boot. */
|
|
154
185
|
export declare function jobContextFromSchedule(systemPrincipal: string, reason: string, now?: Date): JobContext;
|
|
155
186
|
/**
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* enqueued by a token-bearing request escapes the agent-token ceiling simply by
|
|
31
31
|
* becoming asynchronous.
|
|
32
32
|
*/
|
|
33
|
+
import { effectiveAal, toAal } from '../utils/policyAttributes.js';
|
|
33
34
|
/**
|
|
34
35
|
* The username a request-origin job carries when the request had no identity.
|
|
35
36
|
*
|
|
@@ -52,6 +53,7 @@ export function jobContextFromRequest(identity, now = new Date()) {
|
|
|
52
53
|
return {
|
|
53
54
|
username: identity?.username ?? ANONYMOUS_USERNAME,
|
|
54
55
|
origin: 'request',
|
|
56
|
+
...aalOf(identity),
|
|
55
57
|
...(identity?.viaToken ? { viaToken: identity.viaToken } : {}),
|
|
56
58
|
...(identity?.viaShare ? { viaShare: identity.viaShare } : {}),
|
|
57
59
|
...(identity?.viaProxy ? { viaProxy: proxyClaimForJob(identity.viaProxy) } : {}),
|
|
@@ -59,6 +61,24 @@ export function jobContextFromRequest(identity, now = new Date()) {
|
|
|
59
61
|
requestedAt: now.toISOString()
|
|
60
62
|
};
|
|
61
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The level the requester's session or token has right now (#2261), as a field
|
|
66
|
+
* to store with the job; nothing when it has none. Read from the granting
|
|
67
|
+
* session's own subject whatever the caller passed (#2287): a request's subject
|
|
68
|
+
* as is, an `ApiContext`'s `subject`, a `WikiContext`'s `userContext`, or a
|
|
69
|
+
* `JobContext` handed back in, whose stored level is kept as it is.
|
|
70
|
+
*/
|
|
71
|
+
function aalOf(identity) {
|
|
72
|
+
if (!identity)
|
|
73
|
+
return {};
|
|
74
|
+
if (typeof identity.origin === 'string' && typeof identity.requestedAt === 'string') {
|
|
75
|
+
const kept = toAal(identity.aal);
|
|
76
|
+
return kept ? { aal: kept } : {};
|
|
77
|
+
}
|
|
78
|
+
const wrapped = identity.subject ?? identity.userContext;
|
|
79
|
+
const aal = effectiveAal(wrapped && typeof wrapped === 'object' ? (wrapped) : identity);
|
|
80
|
+
return aal ? { aal } : {};
|
|
81
|
+
}
|
|
62
82
|
/** The proxy claim without its key handle (#2047): the actor, grant and scopes stay for the ceiling and the record. */
|
|
63
83
|
function proxyClaimForJob(claim) {
|
|
64
84
|
const { keyHandle: _dropped, ...kept } = claim;
|
|
@@ -119,6 +139,17 @@ export function jobContextFromSystem(systemPrincipal, reason, now = new Date())
|
|
|
119
139
|
reason
|
|
120
140
|
};
|
|
121
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Boot work done in one user's own vaults, on that user's behalf (#1815) —
|
|
144
|
+
* the start-up vault upgrades. The job carries the owner's username, so the
|
|
145
|
+
* vault container rule admits it as it admits any job carrying who asked, and
|
|
146
|
+
* what it may do there is the owner's own rights, resolved by policy when
|
|
147
|
+
* each write is decided. Never the system principal, which no vault admits.
|
|
148
|
+
* Like every job it carries no key, so an encrypted vault stays closed to it.
|
|
149
|
+
*/
|
|
150
|
+
export function jobContextForOwner(owner, reason, now = new Date()) {
|
|
151
|
+
return { username: owner, origin: 'boot', requestedAt: now.toISOString(), reason };
|
|
152
|
+
}
|
|
122
153
|
/** As {@link jobContextFromSystem}, for work started by a timer rather than at boot. */
|
|
123
154
|
export function jobContextFromSchedule(systemPrincipal, reason, now = new Date()) {
|
|
124
155
|
return { ...jobContextFromSystem(systemPrincipal, reason, now), origin: 'schedule' };
|
|
@@ -149,7 +180,9 @@ export function toPermissionSubject(ctx) {
|
|
|
149
180
|
...(ctx.viaShare ? { viaShare: ctx.viaShare } : {}),
|
|
150
181
|
...(ctx.viaProxy ? { viaProxy: ctx.viaProxy } : {}),
|
|
151
182
|
// #1999: the job a scheduled run acts as, for the PIP to resolve.
|
|
152
|
-
...(ctx.job ? { job: ctx.job } : {})
|
|
183
|
+
...(ctx.job ? { job: ctx.job } : {}),
|
|
184
|
+
// #2261: the level stored when it was requested, for `subject:aal`.
|
|
185
|
+
...(ctx.aal ? { grantedAal: ctx.aal } : {})
|
|
153
186
|
};
|
|
154
187
|
}
|
|
155
188
|
/** One line for a log or an audit record: who, from where, and how. */
|
|
@@ -31,11 +31,16 @@ export declare function exportSize(items: readonly IceItem[]): number;
|
|
|
31
31
|
export declare function checkExportSize(items: readonly IceItem[], maxBytes: number): void;
|
|
32
32
|
/**
|
|
33
33
|
* Run the source's items through the chain, each shim's output the next
|
|
34
|
-
* one's input, and return the
|
|
34
|
+
* one's input, and return the file the last shim made. What came before
|
|
35
35
|
* travels with each item while a shim keeps the count: earlier metadata
|
|
36
36
|
* under later, as an import's chain does.
|
|
37
37
|
*
|
|
38
|
+
* Items that are already `file`s skip the chain and join what it makes. One
|
|
39
|
+
* file is returned as it is; several, or any when `zipName` is given, are
|
|
40
|
+
* packed into `<zipName>.zip` (`export.zip` by default), each under its own
|
|
41
|
+
* name, a repeated name made unique. The zip is held to `maxBytes` too.
|
|
42
|
+
*
|
|
38
43
|
* @throws IceExportFailed when a step holds more than `maxBytes`, a shim
|
|
39
|
-
* makes something other than what it declares, or the end is
|
|
44
|
+
* makes something other than what it declares, or the end is no file
|
|
40
45
|
*/
|
|
41
|
-
export declare function runExportChain(items: IceItem[], chain: readonly RegisteredShim[], tools: (shim: RegisteredShim) => ExportShimTools, settings: IceExportSettings): Promise<IceItem>;
|
|
46
|
+
export declare function runExportChain(items: IceItem[], chain: readonly RegisteredShim[], tools: (shim: RegisteredShim) => ExportShimTools, settings: IceExportSettings, zipName?: string): Promise<IceItem>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
2
|
+
import { type IceExportSource, type IceItem } from './types.js';
|
|
3
|
+
/** Core's search export source. */
|
|
4
|
+
export declare const SEARCH_EXPORT_SOURCE = "core.search";
|
|
5
|
+
/** A search, as the page search takes it: the fields `/search` and `/api/assets/search` read. */
|
|
6
|
+
export interface SearchExportParams {
|
|
7
|
+
q?: string;
|
|
8
|
+
types?: string;
|
|
9
|
+
mimeCategory?: string;
|
|
10
|
+
year?: number;
|
|
11
|
+
category?: string[];
|
|
12
|
+
keywords?: string[];
|
|
13
|
+
systemKeywords?: string[];
|
|
14
|
+
status?: string[];
|
|
15
|
+
searchIn?: string[];
|
|
16
|
+
since?: string;
|
|
17
|
+
until?: string;
|
|
18
|
+
date?: string;
|
|
19
|
+
}
|
|
20
|
+
/** The search's parameters from a query string or a body: known fields only, empty ones left out. */
|
|
21
|
+
export declare function searchExportParams(src: Record<string, unknown> | null | undefined): SearchExportParams;
|
|
22
|
+
/** What `read` and `mayExport` take as `ref`. */
|
|
23
|
+
export declare const searchExportRef: (src: Record<string, unknown> | null | undefined) => string;
|
|
24
|
+
/** A page as an NCM item: its text and what a document made from it shows. */
|
|
25
|
+
export declare function pageItem(name: string, content: string, metadata: Record<string, unknown>): IceItem;
|
|
26
|
+
interface Manager {
|
|
27
|
+
getManager<M>(name: string): M | null | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface SearchExportDeps {
|
|
30
|
+
engine: Manager;
|
|
31
|
+
/** May this user export this page alone (`page-export` for it, in its private store or shared folder)? */
|
|
32
|
+
mayExportPage(ctx: ActorContext, pageName: string): Promise<boolean>;
|
|
33
|
+
limits(): {
|
|
34
|
+
maxItems: number;
|
|
35
|
+
maxBytes: number;
|
|
36
|
+
};
|
|
37
|
+
/** Is the Export action for a search offered to this user (page-export held)? */
|
|
38
|
+
offered(ctx: ActorContext): Promise<boolean>;
|
|
39
|
+
/** Does this user hold `page-export` now? Throws the step-up answer when it needs a fresh sign-in. */
|
|
40
|
+
allowed(ctx: ActorContext): Promise<boolean>;
|
|
41
|
+
permits(ctx: ActorContext, permission: string): Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
export declare function searchExportSource(deps: SearchExportDeps): IceExportSource;
|
|
44
|
+
export {};
|
package/lib/src/ice/types.d.ts
CHANGED
|
@@ -641,6 +641,8 @@ export declare const EXPORT_FILE_KIND = "file";
|
|
|
641
641
|
export interface ExportReadInfo {
|
|
642
642
|
/** The shim that will make the file (`core.pdf`). */
|
|
643
643
|
format: string;
|
|
644
|
+
/** Tell the user something about what was read (what was left out and why); shown with the finished export. */
|
|
645
|
+
note(text: string): void;
|
|
644
646
|
}
|
|
645
647
|
/**
|
|
646
648
|
* The owner's door for reading the thing being exported (#2120, D5): a page
|
|
@@ -654,6 +656,8 @@ export interface IceExportSource {
|
|
|
654
656
|
label?: string;
|
|
655
657
|
/** The kind of the items it yields, e.g. `ncm` for a page. */
|
|
656
658
|
yields: IceKind;
|
|
659
|
+
/** Make the export a zip with this name (`search-results`) even when it holds one file. */
|
|
660
|
+
zipAs?: string;
|
|
657
661
|
/**
|
|
658
662
|
* Would this context be allowed to export `ref`? Asked only to decide
|
|
659
663
|
* whether to offer the Export action: an affordance, which records nothing.
|
|
@@ -724,6 +728,8 @@ export interface IceExportReport {
|
|
|
724
728
|
startedAt?: string;
|
|
725
729
|
finishedAt?: string;
|
|
726
730
|
error?: string;
|
|
731
|
+
/** What the source says about what it read, such as how many items were left out. */
|
|
732
|
+
notes?: string[];
|
|
727
733
|
file?: IceExportFile;
|
|
728
734
|
/** When the file is let go (`ngdpbase.export.engine.keep-minutes`). */
|
|
729
735
|
expiresAt?: string;
|
|
@@ -604,34 +604,6 @@ declare class AddonsManager extends BaseManager {
|
|
|
604
604
|
* @param addonName Name of the add-on (for logging)
|
|
605
605
|
* @param addonPath Filesystem path to the add-on directory
|
|
606
606
|
*/
|
|
607
|
-
/**
|
|
608
|
-
* Default edit-protection for a seeded addon page (#971, addons.md §3).
|
|
609
|
-
*
|
|
610
|
-
* §3 settled the mechanism: the seeder stamps frontmatter `access`, which
|
|
611
|
-
* Tier 1 of the evaluator honours directly, needs no `PolicyEvaluator`
|
|
612
|
-
* change, and is hash-neutral (`pageSourceHash` covers the body only, so
|
|
613
|
-
* adding metadata cannot disturb the #920 reseed comparison).
|
|
614
|
-
*
|
|
615
|
-
* §9 settled *who owns what*, and the two must be read together. A blanket
|
|
616
|
-
* admin lock would contradict the operator's decision that domain content
|
|
617
|
-
* pages "should be normal pages, purely seeded by the addon" — the addon
|
|
618
|
-
* provides a starting corpus and then lets go. So the default follows §9's
|
|
619
|
-
* ownership column rather than locking everything:
|
|
620
|
-
*
|
|
621
|
-
* | Category | Purpose | Default |
|
|
622
|
-
* |---|---|---|
|
|
623
|
-
* | `system` | Feature UI, site chrome — infrastructure | admin-only edit |
|
|
624
|
-
* | `documentation` | Help / docs — addon-owned | admin-only edit |
|
|
625
|
-
* | `general` | Domain content, demo — **instance-owned** | no stamp |
|
|
626
|
-
* | anything else | unclassified, treated as addon-owned | admin-only edit |
|
|
627
|
-
*
|
|
628
|
-
* An addon can always override by declaring its own `access`, which is the
|
|
629
|
-
* documented escape hatch in §3 and the reason the default is a default.
|
|
630
|
-
*
|
|
631
|
-
* @param category - The page's resolved `system-category`
|
|
632
|
-
* @returns The access object to stamp, or undefined to leave the page unprotected
|
|
633
|
-
*/
|
|
634
|
-
private defaultAddonPageAccess;
|
|
635
607
|
/**
|
|
636
608
|
* Surface a page-level seed failure beyond the boot log (#951).
|
|
637
609
|
*
|
|
@@ -82,6 +82,14 @@ export interface AgentTokenRecord {
|
|
|
82
82
|
approvedBy?: string;
|
|
83
83
|
/** The proxy grant it was minted under. Ending that grant ends this token; it works only while the grant is in force. */
|
|
84
84
|
proxyGrantId?: string;
|
|
85
|
+
/**
|
|
86
|
+
* #2261: the assurance level (1 to 3) of the minting session when the token
|
|
87
|
+
* was made, which is `subject:aal` for every request the token makes. Taken
|
|
88
|
+
* from the session, never from the request, so it cannot exceed it. Absent
|
|
89
|
+
* on a token made before this was stored: its `subject:aal` is missing and a
|
|
90
|
+
* condition on it fails closed.
|
|
91
|
+
*/
|
|
92
|
+
aal?: 1 | 2 | 3;
|
|
85
93
|
}
|
|
86
94
|
/** A record safe to return over the API — no hash. */
|
|
87
95
|
export type AgentTokenPublic = Omit<AgentTokenRecord, 'hash'>;
|
|
@@ -500,6 +500,17 @@ declare class AttachmentManager extends BaseManager implements CatalogSource {
|
|
|
500
500
|
buffer: Buffer;
|
|
501
501
|
metadata: AttachmentMetadata;
|
|
502
502
|
} | null>;
|
|
503
|
+
/**
|
|
504
|
+
* May `ctx` read this file, by the rules `GET /attachments/:id` serves it
|
|
505
|
+
* under: a file in the requester's own store, else `asset-read`, and a
|
|
506
|
+
* private file only to its owner or their delegate (PIP). No bytes are read.
|
|
507
|
+
*/
|
|
508
|
+
mayReadAttachment(attachmentId: string, ctx: ActorContext): Promise<boolean>;
|
|
509
|
+
/** A file's original bytes and metadata, when {@link mayReadAttachment} lets `ctx` read it; else null. */
|
|
510
|
+
readAttachmentFor(attachmentId: string, ctx: ActorContext): Promise<{
|
|
511
|
+
buffer: Buffer;
|
|
512
|
+
metadata: AttachmentMetadata;
|
|
513
|
+
} | null>;
|
|
503
514
|
/**
|
|
504
515
|
* The requester's own private stores, as provider locations (#1460).
|
|
505
516
|
*
|
|
@@ -60,6 +60,15 @@ export interface FactorEntry {
|
|
|
60
60
|
*/
|
|
61
61
|
preference?: number;
|
|
62
62
|
}
|
|
63
|
+
/** What an account can add or use toward a stronger sign-in (#2356); see `strongerSignInWays`. */
|
|
64
|
+
export interface StrongerSignInWays {
|
|
65
|
+
addPasskey: boolean;
|
|
66
|
+
addTotp: boolean;
|
|
67
|
+
usePasskey: boolean;
|
|
68
|
+
useTotp: boolean;
|
|
69
|
+
password: boolean;
|
|
70
|
+
outside: string[];
|
|
71
|
+
}
|
|
63
72
|
export interface Factor {
|
|
64
73
|
provider: string;
|
|
65
74
|
primary: boolean;
|
|
@@ -797,6 +806,13 @@ declare class AuthManager extends BaseManager {
|
|
|
797
806
|
* the request — so a passkey made here works only on that hostname.
|
|
798
807
|
*/
|
|
799
808
|
private registerPasskeys;
|
|
809
|
+
/**
|
|
810
|
+
* #2317: the setting says passkeys are on, but the site's address cannot
|
|
811
|
+
* support them (base-url not set explicitly, or neither https nor localhost),
|
|
812
|
+
* so none are offered. The same rule registerPasskeys applies. Admin →
|
|
813
|
+
* Configuration says so on that setting's row and offers to turn it off.
|
|
814
|
+
*/
|
|
815
|
+
passkeysCannotWork(): boolean;
|
|
800
816
|
/** TOTP enrolment (#421), available whenever the credentials store is open. */
|
|
801
817
|
private openTotp;
|
|
802
818
|
/** Whether an authenticator app can be enrolled here (#421). */
|
|
@@ -826,6 +842,8 @@ declare class AuthManager extends BaseManager {
|
|
|
826
842
|
private passkeyProvider;
|
|
827
843
|
/** The host passkeys are tied to, or null when passkeys are off (#448). */
|
|
828
844
|
passkeyRelyingParty(): PasskeyRelyingParty | null;
|
|
845
|
+
/** Can a passkey be added or used here at all (#2345)? False when passkeys are off or the address can't host them. */
|
|
846
|
+
passkeysAvailable(): boolean;
|
|
829
847
|
/** Options for `username` to enrol a passkey (#448); the caller keeps `challenge` for verify. */
|
|
830
848
|
/**
|
|
831
849
|
* May this account sign in with `providerId`? Its `allowedAuthMethods`, when
|
|
@@ -1212,6 +1230,26 @@ declare class AuthManager extends BaseManager {
|
|
|
1212
1230
|
now: Aal;
|
|
1213
1231
|
ifEnrolled: Aal;
|
|
1214
1232
|
}>;
|
|
1233
|
+
/**
|
|
1234
|
+
* The site's factors this account may use (`offered`: narrowed to its
|
|
1235
|
+
* `allowedAuthMethods`), and of those the ones it can use now (`usable`: a
|
|
1236
|
+
* factor that must be enrolled counts only once it is). One reading for
|
|
1237
|
+
* the level the account can reach and for what it is told to add (#2356).
|
|
1238
|
+
*/
|
|
1239
|
+
private accountFactors;
|
|
1240
|
+
/**
|
|
1241
|
+
* What this account can add or use here toward a stronger sign-in (#2356),
|
|
1242
|
+
* for the sign-in banners and the confirm page's wording. Never a decision:
|
|
1243
|
+
* who steps down and what step-up needs stay with `rolesAtSignIn` and
|
|
1244
|
+
* `stepUpNeeded`. "Add" is a passkey or an authenticator app the site
|
|
1245
|
+
* offers, the account is allowed and has not set up; "use" is one it has,
|
|
1246
|
+
* is allowed, and the site offers (the app's code as step-up offers it).
|
|
1247
|
+
* `password` is a password it has and is allowed, which the app's code
|
|
1248
|
+
* needs to reach AAL2. `outside` names any other allowed sign-in method
|
|
1249
|
+
* that reaches AAL2 by itself, such as an outside provider's.
|
|
1250
|
+
* Computed per call; never cached across accounts.
|
|
1251
|
+
*/
|
|
1252
|
+
strongerSignInWays(username: string): Promise<StrongerSignInWays>;
|
|
1215
1253
|
/**
|
|
1216
1254
|
* The roles an account holds at a lower level than they require only
|
|
1217
1255
|
* because it has not enrolled a factor it is allowed (#1690): it acts with
|