@jwilleke/ngdpbase-types 0.0.0 → 5.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +16 -2
- package/lib/packages/ngdpbase-types/index.d.ts +32 -0
- package/lib/packages/ngdpbase-types/testing.d.ts +43 -0
- package/lib/packages/ngdpbase-types/testing.js +53 -0
- package/lib/src/cache/CacheDependencies.d.ts +50 -0
- package/lib/src/cache/CacheDependencies.js +50 -0
- package/lib/src/cache/ICacheAdapter.d.ts +86 -0
- package/lib/src/cache/RegionCache.d.ts +171 -0
- package/lib/src/context/ActorContext.d.ts +48 -0
- package/lib/src/context/ActorContext.js +33 -0
- package/lib/src/context/ApiContext.d.ts +194 -0
- package/lib/src/context/ApiContext.js +273 -0
- package/lib/src/context/BaseContext.d.ts +110 -0
- package/lib/src/context/BaseContext.js +143 -0
- package/lib/src/context/JobContext.d.ts +153 -0
- package/lib/src/context/JobContext.js +144 -0
- package/lib/src/context/WikiContext.d.ts +404 -0
- package/lib/src/context/bootActions.d.ts +64 -0
- package/lib/src/context/bootActions.js +111 -0
- package/lib/src/converters/HtmlConverter.d.ts +70 -0
- package/lib/src/converters/IContentConverter.d.ts +94 -0
- package/lib/src/converters/JSPWikiConverter.d.ts +117 -0
- package/lib/src/converters/conversionWarning.d.ts +15 -0
- package/lib/src/converters/ncm/bridge.d.ts +22 -0
- package/lib/src/converters/ncm/existing.d.ts +23 -0
- package/lib/src/converters/ncm/fix/blocks.d.ts +89 -0
- package/lib/src/converters/ncm/fix/bulletMarkers.d.ts +21 -0
- package/lib/src/converters/ncm/fix/index.d.ts +36 -0
- package/lib/src/converters/ncm/fix/jspwikiBullets.d.ts +20 -0
- package/lib/src/converters/ncm/fix/jspwikiCodeMarkers.d.ts +38 -0
- package/lib/src/converters/ncm/fix/jspwikiHeadings.d.ts +25 -0
- package/lib/src/converters/ncm/fix/jspwikiIndent.d.ts +24 -0
- package/lib/src/converters/ncm/fix/jspwikiInsert.d.ts +21 -0
- package/lib/src/converters/ncm/fix/jspwikiItalic.d.ts +29 -0
- package/lib/src/converters/ncm/fix/jspwikiLinkLeftovers.d.ts +27 -0
- package/lib/src/converters/ncm/fix/moreInformationFooter.d.ts +25 -0
- package/lib/src/converters/ncm/fix/styleClosers.d.ts +25 -0
- package/lib/src/converters/ncm/fix/tightenLists.d.ts +30 -0
- package/lib/src/converters/ncm/fix/types.d.ts +33 -0
- package/lib/src/converters/ncm/footnotes.d.ts +39 -0
- package/lib/src/converters/ncm/images.d.ts +49 -0
- package/lib/src/converters/ncm/index.d.ts +20 -0
- package/lib/src/converters/ncm/links.d.ts +21 -0
- package/lib/src/converters/ncm/normalize.d.ts +39 -0
- package/lib/src/converters/ncm/placeholder.d.ts +25 -0
- package/lib/src/converters/ncm/tables.d.ts +36 -0
- package/lib/src/converters/ncm/types.d.ts +42 -0
- package/lib/src/http/egressPolicy.d.ts +52 -0
- package/lib/src/http/guardedFetch.d.ts +89 -0
- package/lib/src/http/guardedFetch.js +113 -0
- package/lib/src/http/guardedLookup.d.ts +48 -0
- package/lib/src/http/guardedLookup.js +73 -0
- package/lib/src/http/ssrf.d.ts +83 -0
- package/lib/src/http/ssrf.js +311 -0
- package/lib/src/mail/MailProvider.d.ts +36 -0
- package/lib/src/mail/NodemailerMailProvider.d.ts +26 -0
- package/lib/src/managers/AddonsManager.d.ts +691 -0
- package/lib/src/managers/AgentTokenManager.d.ts +269 -0
- package/lib/src/managers/AssetManager.d.ts +116 -0
- package/lib/src/managers/AttachmentManager.d.ts +1032 -0
- package/lib/src/managers/AuthManager.d.ts +1257 -0
- package/lib/src/managers/BackgroundJobManager.d.ts +434 -0
- package/lib/src/managers/BaseManager.d.ts +391 -0
- package/lib/src/managers/BaseManager.js +415 -0
- package/lib/src/managers/CacheManager.d.ts +159 -0
- package/lib/src/managers/CatalogManager.d.ts +235 -0
- package/lib/src/managers/CommentManager.d.ts +75 -0
- package/lib/src/managers/ConfigurationManager.d.ts +747 -0
- package/lib/src/managers/DatabaseManager.d.ts +89 -0
- package/lib/src/managers/DatabaseManager.js +236 -0
- package/lib/src/managers/EmailManager.d.ts +47 -0
- package/lib/src/managers/ExportManager.d.ts +129 -0
- package/lib/src/managers/ExportManager.js +420 -0
- package/lib/src/managers/FootnoteManager.d.ts +118 -0
- package/lib/src/managers/MediaManager.d.ts +279 -0
- package/lib/src/managers/MetricsManager.d.ts +89 -0
- package/lib/src/managers/NotificationManager.d.ts +165 -0
- package/lib/src/managers/OidcManager.d.ts +210 -0
- package/lib/src/managers/OrganizationManager.d.ts +107 -0
- package/lib/src/managers/PageManager.d.ts +1645 -0
- package/lib/src/managers/PersonManager.d.ts +48 -0
- package/lib/src/managers/PluginManager.d.ts +179 -0
- package/lib/src/managers/PluginManager.js +393 -0
- package/lib/src/managers/PolicyEvaluator.d.ts +213 -0
- package/lib/src/managers/RenderingManager.d.ts +358 -0
- package/lib/src/managers/RoleManager.d.ts +134 -0
- package/lib/src/managers/SearchManager.d.ts +402 -0
- package/lib/src/managers/ShareManager.d.ts +251 -0
- package/lib/src/managers/TemplateManager.d.ts +176 -0
- package/lib/src/managers/ThemeManager.d.ts +56 -0
- package/lib/src/managers/UserManager.d.ts +480 -0
- package/lib/src/managers/ValidationManager.d.ts +426 -0
- package/lib/src/managers/VariableManager.d.ts +145 -0
- package/lib/src/middleware/csrf.d.ts +3 -0
- package/lib/src/parsers/LinkParser.d.ts +457 -0
- package/lib/src/parsers/MarkupParser.d.ts +769 -0
- package/lib/src/parsers/context/ParseContext.d.ts +314 -0
- package/lib/src/parsers/data/emoji-map.d.ts +18 -0
- package/lib/src/parsers/dom/DOMBuilder.d.ts +210 -0
- package/lib/src/parsers/dom/DOMParser.d.ts +252 -0
- package/lib/src/parsers/dom/Tokenizer.d.ts +358 -0
- package/lib/src/parsers/dom/WikiDocument.d.ts +272 -0
- package/lib/src/parsers/dom/handlers/DOMLinkHandler.d.ts +260 -0
- package/lib/src/parsers/dom/handlers/DOMPluginHandler.d.ts +212 -0
- package/lib/src/parsers/dom/handlers/DOMVariableHandler.d.ts +171 -0
- package/lib/src/parsers/filters/BaseFilter.d.ts +269 -0
- package/lib/src/parsers/filters/FilterChain.d.ts +320 -0
- package/lib/src/parsers/handlers/BaseSyntaxHandler.d.ts +331 -0
- package/lib/src/parsers/handlers/HandlerRegistry.d.ts +225 -0
- package/lib/src/parsers/handlers/JSPWikiPreprocessor.d.ts +89 -0
- package/lib/src/parsers/handlers/LinkParserHandler.d.ts +80 -0
- package/lib/src/parsers/handlers/PluginSyntaxHandler.d.ts +78 -0
- package/lib/src/parsers/handlers/WikiFormHandler.d.ts +149 -0
- package/lib/src/parsers/handlers/WikiTagHandler.d.ts +125 -0
- package/lib/src/parsers/jspwikiTableRow.d.ts +39 -0
- package/lib/src/parsers/styleBlockSyntax.d.ts +14 -0
- package/lib/src/plugins/types.d.ts +79 -0
- package/lib/src/providers/AgentTokenAuthProvider.d.ts +40 -0
- package/lib/src/providers/AuthentikBearerAuthProvider.d.ts +75 -0
- package/lib/src/providers/BaseAttachmentProvider.d.ts +197 -0
- package/lib/src/providers/BaseAuthProvider.d.ts +220 -0
- package/lib/src/providers/BaseCacheProvider.d.ts +140 -0
- package/lib/src/providers/BaseCredentialsProvider.d.ts +84 -0
- package/lib/src/providers/BaseDatabaseProvider.d.ts +22 -0
- package/lib/src/providers/BaseDatabaseProvider.js +12 -0
- package/lib/src/providers/BaseLoggingProvider.d.ts +68 -0
- package/lib/src/providers/BaseLoggingProvider.js +37 -0
- package/lib/src/providers/BaseMediaProvider.d.ts +308 -0
- package/lib/src/providers/BasePageProvider.d.ts +337 -0
- package/lib/src/providers/BaseProvider.d.ts +98 -0
- package/lib/src/providers/BaseProvider.js +74 -0
- package/lib/src/providers/BaseSearchProvider.d.ts +362 -0
- package/lib/src/providers/BasicAttachmentProvider.d.ts +599 -0
- package/lib/src/providers/CloudflareAccessAuthProvider.d.ts +72 -0
- package/lib/src/providers/FileCredentialsProvider.d.ts +33 -0
- package/lib/src/providers/FileJobStateProvider.d.ts +58 -0
- package/lib/src/providers/FileLoggingProvider.d.ts +43 -0
- package/lib/src/providers/FileLoggingProvider.js +120 -0
- package/lib/src/providers/FileOidcAdapter.d.ts +25 -0
- package/lib/src/providers/FileSystemMediaProvider.d.ts +403 -0
- package/lib/src/providers/LunrSearchProvider.d.ts +280 -0
- package/lib/src/providers/MagicLinkAuthProvider.d.ts +125 -0
- package/lib/src/providers/NullCacheProvider.d.ts +72 -0
- package/lib/src/providers/OidcAuthProvider.d.ts +180 -0
- package/lib/src/providers/OidcBearerAuthProvider.d.ts +30 -0
- package/lib/src/providers/PasskeyAuthProvider.d.ts +58 -0
- package/lib/src/providers/PasswordAuthProvider.d.ts +20 -0
- package/lib/src/providers/SqliteDatabaseProvider.d.ts +55 -0
- package/lib/src/providers/SqliteDatabaseProvider.js +90 -0
- package/lib/src/providers/SqliteJobStateProvider.d.ts +41 -0
- package/lib/src/providers/databaseMigrations.d.ts +8 -0
- package/lib/src/providers/databaseMigrations.js +47 -0
- package/lib/src/providers/jobState.d.ts +99 -0
- package/lib/src/providers/lunrQueryToMiniSearch.d.ts +32 -0
- package/lib/src/providers/mediaFilenameDate.d.ts +22 -0
- package/lib/src/providers/sqliteLocation.d.ts +17 -0
- package/lib/src/providers/sqliteLocation.js +55 -0
- package/lib/src/providers/sqliteMigrations.d.ts +48 -0
- package/lib/src/providers/sqliteMigrations.js +72 -0
- package/lib/src/rendering/githubAlerts.d.ts +22 -0
- package/lib/src/rendering/htmlPolicy.d.ts +76 -0
- package/lib/src/rendering/markdownConverter.d.ts +73 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +245 -0
- package/lib/src/security/PolicyInformationPoint.d.ts +448 -0
- package/lib/src/security/pageActions.d.ts +40 -0
- package/lib/src/security/permissions.generated.d.ts +78 -0
- package/lib/src/types/Asset.d.ts +414 -0
- package/lib/src/types/Catalog.d.ts +61 -0
- package/lib/src/types/Comment.d.ts +11 -0
- package/lib/src/types/Config.d.ts +388 -0
- package/lib/src/types/Organization.d.ts +78 -0
- package/lib/src/types/OrganizationProvider.d.ts +38 -0
- package/lib/src/types/Page.d.ts +220 -0
- package/lib/src/types/Person.d.ts +56 -0
- package/lib/src/types/PersonProvider.d.ts +25 -0
- package/lib/src/types/Policy.d.ts +139 -0
- package/lib/src/types/Policy.js +17 -0
- package/lib/src/types/Provider.d.ts +810 -0
- package/lib/src/types/Role.d.ts +84 -0
- package/lib/src/types/RoleProvider.d.ts +35 -0
- package/lib/src/types/Schema.d.ts +425 -0
- package/lib/src/types/Share.d.ts +172 -0
- package/lib/src/types/Share.js +81 -0
- package/lib/src/types/User.d.ts +304 -0
- package/lib/src/types/Version.d.ts +189 -0
- package/lib/src/types/WikiEngine.d.ts +128 -0
- package/lib/src/types/express.d.ts +120 -0
- package/lib/src/types/express.js +1 -0
- package/lib/src/types/guards.d.ts +78 -0
- package/lib/src/types/heic-decode.d.ts +16 -0
- package/lib/src/types/index.d.ts +19 -0
- package/lib/src/types/linkedom.d.ts +77 -0
- package/lib/src/types/markdown-it-plugins.d.ts +21 -0
- package/lib/src/utils/DeltaStorage.d.ts +118 -0
- package/lib/src/utils/LocaleUtils.d.ts +105 -0
- package/lib/src/utils/LocaleUtils.js +294 -0
- package/lib/src/utils/PageNameMatcher.d.ts +98 -0
- package/lib/src/utils/PathPreflight.d.ts +38 -0
- package/lib/src/utils/PathPreflight.js +50 -0
- package/lib/src/utils/SectionUtils.d.ts +40 -0
- package/lib/src/utils/addonConfigLayer.d.ts +144 -0
- package/lib/src/utils/addonConfigLayer.js +382 -0
- package/lib/src/utils/addonDatabaseRules.d.ts +25 -0
- package/lib/src/utils/addonDatabaseRules.js +57 -0
- package/lib/src/utils/addonHost.d.ts +51 -0
- package/lib/src/utils/addonHost.js +72 -0
- package/lib/src/utils/addonInstaller.d.ts +109 -0
- package/lib/src/utils/addonLock.d.ts +37 -0
- package/lib/src/utils/addonLock.js +48 -0
- package/lib/src/utils/addonPageSync.d.ts +45 -0
- package/lib/src/utils/addonProvenance.d.ts +48 -0
- package/lib/src/utils/addonTarball.d.ts +41 -0
- package/lib/src/utils/addonsPathResolver.d.ts +76 -0
- package/lib/src/utils/addonsPathResolver.js +129 -0
- package/lib/src/utils/assetMetadataTags.d.ts +66 -0
- package/lib/src/utils/atomicWrite.d.ts +28 -0
- package/lib/src/utils/auditConfigChange.d.ts +57 -0
- package/lib/src/utils/auditEventNames.d.ts +50 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +163 -0
- package/lib/src/utils/auditEventNames.generated.js +168 -0
- package/lib/src/utils/auditEventNames.js +49 -0
- package/lib/src/utils/auditEvents.d.ts +265 -0
- package/lib/src/utils/auditEvents.js +309 -0
- package/lib/src/utils/auditRegistry.d.ts +102 -0
- package/lib/src/utils/auditRegistry.js +168 -0
- package/lib/src/utils/bip39English.d.ts +2 -0
- package/lib/src/utils/configEnvKeys.d.ts +68 -0
- package/lib/src/utils/configFiles.d.ts +74 -0
- package/lib/src/utils/configFiles.js +150 -0
- package/lib/src/utils/delegation.d.ts +48 -0
- package/lib/src/utils/deriveUsername.d.ts +21 -0
- package/lib/src/utils/explainMediaPath.d.ts +123 -0
- package/lib/src/utils/formerTitles.d.ts +69 -0
- package/lib/src/utils/frontmatterAccess.d.ts +54 -0
- package/lib/src/utils/headlessAdminPassword.d.ts +55 -0
- package/lib/src/utils/imageTransform.d.ts +52 -0
- package/lib/src/utils/instanceEnvSecret.d.ts +51 -0
- package/lib/src/utils/jobSlots.d.ts +43 -0
- package/lib/src/utils/keywordNormalizer.d.ts +127 -0
- package/lib/src/utils/logger.d.ts +70 -0
- package/lib/src/utils/logger.js +106 -0
- package/lib/src/utils/magicLinkDeviceState.d.ts +95 -0
- package/lib/src/utils/managerUtils.d.ts +40 -0
- package/lib/src/utils/migrateLegacyPrivateAttachments.d.ts +15 -0
- package/lib/src/utils/orgFilename.d.ts +22 -0
- package/lib/src/utils/pageFeature.d.ts +27 -0
- package/lib/src/utils/pageFrontmatter.d.ts +21 -0
- package/lib/src/utils/pageNotes.d.ts +150 -0
- package/lib/src/utils/pageTitleRule.d.ts +37 -0
- package/lib/src/utils/pageToArticle.d.ts +59 -0
- package/lib/src/utils/pageUrl.d.ts +18 -0
- package/lib/src/utils/passwordChange.d.ts +12 -0
- package/lib/src/utils/passwordHash.d.ts +60 -0
- package/lib/src/utils/permissionCatalog.d.ts +52 -0
- package/lib/src/utils/pluginFormatters.d.ts +452 -0
- package/lib/src/utils/pluginFormatters.js +714 -0
- package/lib/src/utils/privateLinkRewrite.d.ts +42 -0
- package/lib/src/utils/privateStoreAccess.d.ts +57 -0
- package/lib/src/utils/privateStoreAccess.js +65 -0
- package/lib/src/utils/privateStoreCatalogs.d.ts +40 -0
- package/lib/src/utils/privateStoreCrypto.d.ts +127 -0
- package/lib/src/utils/privateStoreDoor.d.ts +181 -0
- package/lib/src/utils/privateStoreExport.d.ts +72 -0
- package/lib/src/utils/privateStoreFiles.d.ts +120 -0
- package/lib/src/utils/privateStoreMeta.d.ts +64 -0
- package/lib/src/utils/privateStoreMigrations.d.ts +37 -0
- package/lib/src/utils/privateStorePath.d.ts +259 -0
- package/lib/src/utils/privateStorePath.js +436 -0
- package/lib/src/utils/privateStoreTakeout.d.ts +76 -0
- package/lib/src/utils/privateStoreUnlock.d.ts +175 -0
- package/lib/src/utils/proxyClaim.d.ts +59 -0
- package/lib/src/utils/proxyClaim.js +30 -0
- package/lib/src/utils/redactPrivateNames.d.ts +21 -0
- package/lib/src/utils/redactPrivateNames.js +89 -0
- package/lib/src/utils/redactSecrets.d.ts +58 -0
- package/lib/src/utils/redactSecrets.js +171 -0
- package/lib/src/utils/renameLinkRewrite.d.ts +112 -0
- package/lib/src/utils/roleGrants.d.ts +35 -0
- package/lib/src/utils/safeRedirect.d.ts +32 -0
- package/lib/src/utils/schedule.d.ts +54 -0
- package/lib/src/utils/secretFileMode.d.ts +17 -0
- package/lib/src/utils/seededShippedPages.d.ts +45 -0
- package/lib/src/utils/sensitiveValues.d.ts +59 -0
- package/lib/src/utils/sensitiveValues.js +106 -0
- package/lib/src/utils/sessionEnd.d.ts +12 -0
- package/lib/src/utils/sessionGeneration.d.ts +24 -0
- package/lib/src/utils/sessionIdle.d.ts +41 -0
- package/lib/src/utils/sessionLimits.d.ts +68 -0
- package/lib/src/utils/shareLockbox.d.ts +64 -0
- package/lib/src/utils/sigstoreTrustedRoot.d.ts +33 -0
- package/lib/src/utils/sniffMime.d.ts +21 -0
- package/lib/src/utils/storeSearchIndex.d.ts +75 -0
- package/lib/src/utils/subjectMayDo.d.ts +21 -0
- package/lib/src/utils/systemCategoryRules.d.ts +30 -0
- package/lib/src/utils/systemCategoryRules.js +41 -0
- package/lib/src/utils/userCreateError.d.ts +62 -0
- package/lib/src/utils/username.d.ts +53 -0
- package/lib/src/utils/vaultUrlRewrite.d.ts +27 -0
- package/lib/src/utils/version.d.ts +84 -0
- package/lib/src/utils/videoFrame.d.ts +21 -0
- package/package.json +54 -3
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit event builders for content mutations (#1080).
|
|
3
|
+
*
|
|
4
|
+
* `AuditManager` was already mature — file provider with rotation, 90-day
|
|
5
|
+
* retention, search, stats, export, and an admin UI — but almost nothing
|
|
6
|
+
* called it. Outside the manager the only `logAuditEvent` call sites were
|
|
7
|
+
* `page-delete`, admin raw-edit, two session operations, and share
|
|
8
|
+
* issue/revoke. Ordinary page create, edit, and rename, and every attachment
|
|
9
|
+
* upload and delete, produced no audit record at all.
|
|
10
|
+
*
|
|
11
|
+
* That left "who changed this page, when, and through what?" answerable only
|
|
12
|
+
* from a version manifest: per-page, not queryable across pages, and present
|
|
13
|
+
* only when the deployment runs `versioningfileprovider` (the shipped default
|
|
14
|
+
* is `filesystemprovider`, which keeps no versions). There was no way to ask
|
|
15
|
+
* "everything this agent token wrote last Tuesday" — which is exactly the
|
|
16
|
+
* question agent tokens (#946) make worth asking, since a token writes
|
|
17
|
+
* unattended and the version manifest records only a username.
|
|
18
|
+
*
|
|
19
|
+
* These are pure builders so the event *shape* can be tested without an
|
|
20
|
+
* engine, a request, or a manager. The shape is the contract: `#1082` reads
|
|
21
|
+
* `page-rename`'s `fromPageName`/`pageName` pair to resolve a link that
|
|
22
|
+
* points at a page's former title, so those two fields are load-bearing
|
|
23
|
+
* rather than descriptive.
|
|
24
|
+
*/
|
|
25
|
+
import logger from './logger.js';
|
|
26
|
+
import { auditEventDeclarations, AUDIT_EVENTS_KEY, isAuditEventEnabled, refusesOnFailure } from './auditRegistry.js';
|
|
27
|
+
import { AUDIT_EVENT, isCoreAuditEventName } from './auditEventNames.js';
|
|
28
|
+
import { proxyAttribution } from './proxyClaim.js';
|
|
29
|
+
/**
|
|
30
|
+
* The result vocabulary, as a runtime value (#1237).
|
|
31
|
+
*
|
|
32
|
+
* The type alone could not be offered as filter options, so the audit page
|
|
33
|
+
* hand-kept its own list — and it offered `allow`, `deny`, `error` while the
|
|
34
|
+
* log holds `success` (5,859), `deny` (896) and `failure` (79). Two of the
|
|
35
|
+
* three options matched zero records and the largest value was unfilterable.
|
|
36
|
+
* Exactly the #1115 failure, one field over.
|
|
37
|
+
*/
|
|
38
|
+
export const AUDIT_RESULTS = ['success', 'failure', 'allow', 'deny', 'error', 'logout'];
|
|
39
|
+
// #1201: the builders look the name up rather than interpolating it, so the
|
|
40
|
+
// compiler — not a grep — knows which names each family can produce.
|
|
41
|
+
const PAGE_EVENT = {
|
|
42
|
+
create: AUDIT_EVENT.PAGE_CREATE,
|
|
43
|
+
edit: AUDIT_EVENT.PAGE_EDIT,
|
|
44
|
+
rename: AUDIT_EVENT.PAGE_RENAME,
|
|
45
|
+
'link-rewrite': AUDIT_EVENT.PAGE_LINK_REWRITE
|
|
46
|
+
};
|
|
47
|
+
const ASSET_EVENT = {
|
|
48
|
+
upload: AUDIT_EVENT.ASSET_UPLOAD,
|
|
49
|
+
delete: AUDIT_EVENT.ASSET_DELETE
|
|
50
|
+
};
|
|
51
|
+
const TOKEN_EVENT = {
|
|
52
|
+
mint: AUDIT_EVENT.TOKEN_MINT,
|
|
53
|
+
revoke: AUDIT_EVENT.TOKEN_REVOKE,
|
|
54
|
+
extend: AUDIT_EVENT.TOKEN_EXTEND
|
|
55
|
+
};
|
|
56
|
+
/** A content row's "who" — `user`, `actor` and `ipAddress` — and the proxy grant's metadata. One place, read by every builder below. */
|
|
57
|
+
function whoOf(input) {
|
|
58
|
+
const { metadata, ...proxy } = proxyAttribution(input.viaProxy);
|
|
59
|
+
return { fields: { user: input.username ?? 'unknown', ...proxy, ipAddress: input.ipAddress }, metadata };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Token identity as metadata. Always present as explicit nulls for a human
|
|
63
|
+
* write so a query can filter on the field existing rather than having to
|
|
64
|
+
* treat "absent" and "not a token" as the same thing.
|
|
65
|
+
*/
|
|
66
|
+
function tokenMetadata(viaToken) {
|
|
67
|
+
return {
|
|
68
|
+
viaTokenId: viaToken?.id ?? null,
|
|
69
|
+
viaTokenName: viaToken?.name ?? null
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build the audit event for a page create, edit, rename, or link rewrite.
|
|
74
|
+
*
|
|
75
|
+
* `result` is always `success`: these are recorded *after* the write lands,
|
|
76
|
+
* unlike `page-delete`, which logs `attempted` before unlinking so that a
|
|
77
|
+
* crash mid-delete still leaves a trace. A create or edit has the page file
|
|
78
|
+
* and its version history as its own record, so recording the attempt buys
|
|
79
|
+
* nothing the file does not already say.
|
|
80
|
+
*/
|
|
81
|
+
export function buildPageMutationAuditEvent(input) {
|
|
82
|
+
const { op, pageName, uuid, fromPageName, rewriteOf, viaToken } = input;
|
|
83
|
+
const who = whoOf(input);
|
|
84
|
+
const metadata = {
|
|
85
|
+
pageName,
|
|
86
|
+
uuid: uuid ?? null,
|
|
87
|
+
...tokenMetadata(viaToken),
|
|
88
|
+
...who.metadata
|
|
89
|
+
};
|
|
90
|
+
// Only a rename has a previous title. Emitting `fromPageName: null` on
|
|
91
|
+
// every edit would make the field useless as a filter for #1082.
|
|
92
|
+
if (op === 'rename' && fromPageName) {
|
|
93
|
+
metadata.fromPageName = fromPageName;
|
|
94
|
+
}
|
|
95
|
+
// Which rename this rewrite belongs to. Emitted only for `link-rewrite`, for
|
|
96
|
+
// the same reason: a field present on every event is useless as a filter.
|
|
97
|
+
if (op === 'link-rewrite' && rewriteOf) {
|
|
98
|
+
metadata.rewriteFrom = rewriteOf.from;
|
|
99
|
+
metadata.rewriteTo = rewriteOf.to;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
eventType: PAGE_EVENT[op],
|
|
103
|
+
...who.fields,
|
|
104
|
+
action: `page-${op}`,
|
|
105
|
+
result: 'success',
|
|
106
|
+
// A token write is worth surfacing above ordinary editing traffic, but an
|
|
107
|
+
// edit is not a delete — `page-delete` reserves `high` for tokens.
|
|
108
|
+
severity: viaToken ? 'medium' : 'low',
|
|
109
|
+
metadata
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build the audit event for a page view (#1129).
|
|
114
|
+
*
|
|
115
|
+
* Emission is a deployment posture: `page-read` ships `enabled: false` in
|
|
116
|
+
* `ngdpbase.audit.events` (#1203), and `recordAuditEvent` honours the switch.
|
|
117
|
+
* Off, a general-purpose deployment does not drown its log in reads; on, a
|
|
118
|
+
* records-style deployment gets the access accounting — who looked at what —
|
|
119
|
+
* that read auditing exists for.
|
|
120
|
+
*/
|
|
121
|
+
export function buildPageViewAuditEvent(input) {
|
|
122
|
+
const { pageName, uuid, viaToken } = input;
|
|
123
|
+
const who = whoOf(input);
|
|
124
|
+
return {
|
|
125
|
+
eventType: AUDIT_EVENT.PAGE_READ,
|
|
126
|
+
...who.fields,
|
|
127
|
+
action: 'page-read',
|
|
128
|
+
result: 'success',
|
|
129
|
+
// Same convention as the mutations: an unattended (token-driven) read is
|
|
130
|
+
// the one a reviewer is scanning for.
|
|
131
|
+
severity: viaToken ? 'medium' : 'low',
|
|
132
|
+
metadata: {
|
|
133
|
+
pageName,
|
|
134
|
+
uuid: uuid ?? null,
|
|
135
|
+
...tokenMetadata(viaToken),
|
|
136
|
+
...who.metadata
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build the audit event for an attachment upload or delete.
|
|
142
|
+
*
|
|
143
|
+
* A delete outranks an upload because it is the one that loses data, and a
|
|
144
|
+
* token-driven delete matches `page-delete`'s `high` for the same reason:
|
|
145
|
+
* unattended destruction is the case someone reviewing the log is looking for.
|
|
146
|
+
*/
|
|
147
|
+
export function buildAttachmentAuditEvent(input) {
|
|
148
|
+
const { op, attachmentId, filename, pageName, sizeBytes, viaToken } = input;
|
|
149
|
+
const who = whoOf(input);
|
|
150
|
+
const metadata = {
|
|
151
|
+
attachmentId,
|
|
152
|
+
filename,
|
|
153
|
+
...tokenMetadata(viaToken),
|
|
154
|
+
...who.metadata
|
|
155
|
+
};
|
|
156
|
+
if (pageName)
|
|
157
|
+
metadata.pageName = pageName;
|
|
158
|
+
if (typeof sizeBytes === 'number')
|
|
159
|
+
metadata.sizeBytes = sizeBytes;
|
|
160
|
+
const severity = op === 'delete'
|
|
161
|
+
? (viaToken ? 'high' : 'medium')
|
|
162
|
+
: (viaToken ? 'medium' : 'low');
|
|
163
|
+
return {
|
|
164
|
+
eventType: ASSET_EVENT[op],
|
|
165
|
+
...who.fields,
|
|
166
|
+
action: `asset-${op}`,
|
|
167
|
+
result: 'success',
|
|
168
|
+
severity,
|
|
169
|
+
metadata
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const dropStats = { dropped: 0, lastEventType: null, lastError: null, lastAt: null };
|
|
173
|
+
/** Audit writes lost since boot. Read-only snapshot. */
|
|
174
|
+
export function getAuditDropStats() {
|
|
175
|
+
return { ...dropStats };
|
|
176
|
+
}
|
|
177
|
+
/** Reset the counter. For tests; nothing in the running app calls this. */
|
|
178
|
+
export function resetAuditDropStats() {
|
|
179
|
+
dropStats.dropped = 0;
|
|
180
|
+
dropStats.lastEventType = null;
|
|
181
|
+
dropStats.lastError = null;
|
|
182
|
+
dropStats.lastAt = null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Loud on the first loss, then at each power of ten.
|
|
186
|
+
*
|
|
187
|
+
* One line per dropped event would bury the signal in the noise of whatever
|
|
188
|
+
* outage caused it — and the first loss is the one that matters, because it is
|
|
189
|
+
* the moment the log stopped being complete.
|
|
190
|
+
*/
|
|
191
|
+
function shouldShout(count) {
|
|
192
|
+
if (count === 1)
|
|
193
|
+
return true;
|
|
194
|
+
let threshold = 10;
|
|
195
|
+
while (threshold <= count) {
|
|
196
|
+
if (threshold === count)
|
|
197
|
+
return true;
|
|
198
|
+
threshold *= 10;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
export async function recordAuditEvent(sink, event, onError) {
|
|
203
|
+
// #1638: an addon's name is checked here, against the merged registry, not
|
|
204
|
+
// by the compiler. One nobody declares — the addon is disabled, or its
|
|
205
|
+
// config/default-config.json never named it — is refused loudly rather than
|
|
206
|
+
// recorded under a name with no on-failure rule. Core names are the
|
|
207
|
+
// generated union and keep their existing path.
|
|
208
|
+
if (!isCoreAuditEventName(event.eventType) && !auditEventDeclarations()[event.eventType]) {
|
|
209
|
+
const message = `Audit event '${event.eventType}' is not declared (as a permission entry's audit block, or in ${AUDIT_EVENTS_KEY}). ` +
|
|
210
|
+
'An addon declares its events in its config/default-config.json, and they count only while the addon is enabled.';
|
|
211
|
+
logger.error(`[audit] ${message}`);
|
|
212
|
+
throw new Error(message);
|
|
213
|
+
}
|
|
214
|
+
// #1203: `enabled: false` on the event in `ngdpbase.audit.events` is a
|
|
215
|
+
// decision on the record. The emitter still exists and still calls this;
|
|
216
|
+
// the switch is honoured here, once, rather than at every call site.
|
|
217
|
+
if (!isAuditEventEnabled(event.eventType))
|
|
218
|
+
return 'not-enabled';
|
|
219
|
+
// An absent sink is a configuration state, not a failure. Counting it would
|
|
220
|
+
// make the number meaningless on any instance that never enabled auditing —
|
|
221
|
+
// and it must not turn every critical action into an error either, since
|
|
222
|
+
// auditing being off is already visible through #1118's posture.
|
|
223
|
+
if (!sink?.logAuditEvent)
|
|
224
|
+
return 'no-sink';
|
|
225
|
+
const critical = refusesOnFailure(event.eventType);
|
|
226
|
+
if (critical && !sink.flushAuditQueue) {
|
|
227
|
+
const message = `Audit sink cannot guarantee durability for ${event.eventType}, which is declared on-failure: refuse. ` +
|
|
228
|
+
'The action was refused rather than completed without a record.';
|
|
229
|
+
logger.error(`[audit] ${message}`);
|
|
230
|
+
throw new Error(message);
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await sink.logAuditEvent(event);
|
|
234
|
+
if (critical)
|
|
235
|
+
await sink.flushAuditQueue?.();
|
|
236
|
+
return 'recorded';
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
dropStats.dropped += 1;
|
|
240
|
+
dropStats.lastEventType = event.eventType;
|
|
241
|
+
dropStats.lastError = err instanceof Error ? err.message : String(err);
|
|
242
|
+
dropStats.lastAt = new Date().toISOString();
|
|
243
|
+
if (shouldShout(dropStats.dropped)) {
|
|
244
|
+
logger.error(`[audit] ${dropStats.dropped} audit event(s) lost since boot — latest ${event.eventType}: ${dropStats.lastError}. ` +
|
|
245
|
+
'The write itself succeeded; only its audit record was lost.');
|
|
246
|
+
}
|
|
247
|
+
onError?.(err);
|
|
248
|
+
// A critical event's failure is the caller's problem: it must be able to
|
|
249
|
+
// abandon the action rather than complete it unrecorded.
|
|
250
|
+
if (critical) {
|
|
251
|
+
throw new Error(`Audit write failed for ${event.eventType}, which is declared on-failure: refuse: ${dropStats.lastError}`, { cause: err });
|
|
252
|
+
}
|
|
253
|
+
return 'dropped';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Build the audit event for an agent token mint, extend or revoke (#1111, #1734).
|
|
258
|
+
*
|
|
259
|
+
* Before this, the token store was the only record that a credential had ever
|
|
260
|
+
* existed — and `purgeExpired()` claimed otherwise in a comment ("Audit is
|
|
261
|
+
* unaffected") while nothing emitted anything. That made `retention-days`
|
|
262
|
+
* load-bearing by accident: hash-bearing records were kept a month past
|
|
263
|
+
* usefulness purely so somebody could answer "what could this token do, and
|
|
264
|
+
* who stopped it?" With the lifecycle audited, the store can keep only what it
|
|
265
|
+
* needs to authenticate.
|
|
266
|
+
*
|
|
267
|
+
* Emitted from the manager rather than the route, unlike `page.*`. A page
|
|
268
|
+
* mutation logged at the HTTP layer misses an internal caller and that is
|
|
269
|
+
* survivable; an unaudited mint is a credential nobody knows exists.
|
|
270
|
+
*
|
|
271
|
+
* `scopes` appear on a mint only, `expiresAt` on a mint and an extend (with
|
|
272
|
+
* `previousExpiresAt` on an extend, #1734), and `revokedBy` on a revoke only —
|
|
273
|
+
* the same rule `page-rename` follows for `fromPageName`. A field present on
|
|
274
|
+
* every event is useless as a filter.
|
|
275
|
+
*/
|
|
276
|
+
export function buildTokenAuditEvent(input) {
|
|
277
|
+
const { op, id, owner, name, scopes, expiresAt, previousExpiresAt, revokedBy, viaToken } = input;
|
|
278
|
+
const who = whoOf(input);
|
|
279
|
+
const metadata = {
|
|
280
|
+
id,
|
|
281
|
+
owner,
|
|
282
|
+
name: name ?? null,
|
|
283
|
+
...tokenMetadata(viaToken),
|
|
284
|
+
...who.metadata
|
|
285
|
+
};
|
|
286
|
+
if (op === 'mint') {
|
|
287
|
+
metadata.scopes = [...(scopes ?? [])];
|
|
288
|
+
metadata.expiresAt = expiresAt;
|
|
289
|
+
}
|
|
290
|
+
if (op === 'extend') {
|
|
291
|
+
metadata.previousExpiresAt = previousExpiresAt;
|
|
292
|
+
metadata.expiresAt = expiresAt;
|
|
293
|
+
}
|
|
294
|
+
if (op === 'revoke' && revokedBy) {
|
|
295
|
+
metadata.revokedBy = revokedBy;
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
eventType: TOKEN_EVENT[op],
|
|
299
|
+
...who.fields,
|
|
300
|
+
action: `token-${op}`,
|
|
301
|
+
result: 'success',
|
|
302
|
+
// Every credential event outranks ordinary content traffic: a token acts
|
|
303
|
+
// unattended, so its creation and its revocation are what a reader of the
|
|
304
|
+
// log is looking for. A token minted BY a token is higher still — that is
|
|
305
|
+
// delegation widening on its own, and the case worth surfacing loudest.
|
|
306
|
+
severity: viaToken ? 'high' : 'medium',
|
|
307
|
+
metadata
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audit event registry, read from configuration (#1200, epic #1208).
|
|
3
|
+
*
|
|
4
|
+
* `ngdpbase.audit.events` in `config/app-default-config.json` names every
|
|
5
|
+
* recorded action, what happens when its record cannot be written, and whether it fires. This module is
|
|
6
|
+
* a reader over that map: nothing here declares an event.
|
|
7
|
+
*
|
|
8
|
+
* Until #1200 the declarations lived in this file, deliberately, on the
|
|
9
|
+
* argument that an operator who could edit them could narrow what the system
|
|
10
|
+
* claims to audit. The operator's decision on 2026-09-04 is that this is the
|
|
11
|
+
* point: configuration is authoritative, and narrowing is on the record —
|
|
12
|
+
* an admin UI edit emits `config-change`, and a disk edit is reported by
|
|
13
|
+
* `posture-recorded` at the next boot. See docs/audit-posture.md.
|
|
14
|
+
*
|
|
15
|
+
* #1638 (operator, 2026-10-05): a permission and its action are one thing. An
|
|
16
|
+
* event that records a permission's action is declared on the permission
|
|
17
|
+
* entry itself — `ngdpbase.permissions.definitions.<name>.audit` — and
|
|
18
|
+
* `ngdpbase.audit.events` holds only events that are not a permission's
|
|
19
|
+
* action. {@link auditDeclarationsFrom} reads both, and is the one reader:
|
|
20
|
+
* the runtime, the generator, the lint, the doc generator and the boot check
|
|
21
|
+
* all call it, so they cannot disagree.
|
|
22
|
+
*
|
|
23
|
+
* What stays in code is the emitters. `scripts/audit-coverage.ts` proves the
|
|
24
|
+
* map and the emitters agree.
|
|
25
|
+
*/
|
|
26
|
+
export declare const AUDIT_EVENTS_KEY = "ngdpbase.audit.events";
|
|
27
|
+
/** The permission registry; an entry's `audit` field declares its event (#1638). */
|
|
28
|
+
export declare const PERMISSION_DEFINITIONS_KEY = "ngdpbase.permissions.definitions";
|
|
29
|
+
/**
|
|
30
|
+
* What happens when the record cannot be written (#1121, #1158, #1218).
|
|
31
|
+
*
|
|
32
|
+
* - `refuse` — the action must not complete unless the record does; the
|
|
33
|
+
* write is flushed to the device before the action proceeds
|
|
34
|
+
* - `continue` — the action proceeds; the lost record is counted and surfaced
|
|
35
|
+
*
|
|
36
|
+
* This is failure handling, not importance. How important an event is lives
|
|
37
|
+
* on each record as `severity`. Until #1218 this field was called `tier` with
|
|
38
|
+
* values `critical` / `standard` / `volume`, which read as an importance
|
|
39
|
+
* scale that it never was; `volume` was `continue` plus `enabled: false`.
|
|
40
|
+
*/
|
|
41
|
+
export type AuditOnFailure = 'refuse' | 'continue';
|
|
42
|
+
/** One entry of `ngdpbase.audit.events`. */
|
|
43
|
+
export interface AuditEventDeclaration {
|
|
44
|
+
'on-failure': AuditOnFailure;
|
|
45
|
+
/** Whether the emitter fires. Omitted means true; `false` is a decision on the record. */
|
|
46
|
+
enabled?: boolean;
|
|
47
|
+
/** One line, shown in the admin filter and the documented table. */
|
|
48
|
+
description: string;
|
|
49
|
+
}
|
|
50
|
+
/** The shape of `ConfigurationManager.getProperty`, so this module needs no manager import. */
|
|
51
|
+
export type AuditEventsSource = (key: string, defaultValue?: unknown) => unknown;
|
|
52
|
+
/**
|
|
53
|
+
* Bind the live configuration. `AuditManager.initialize` calls this before it
|
|
54
|
+
* loads a provider, so every tier consulted from then on is the operator's.
|
|
55
|
+
*
|
|
56
|
+
* There is no fallback to the shipped file: reading
|
|
57
|
+
* `config/app-default-config.json` here would be a second reader over the
|
|
58
|
+
* configuration store, and a value could then come from the file when the live
|
|
59
|
+
* configuration says otherwise. Tests bind from the shipped file themselves
|
|
60
|
+
* (`vitest.setup.ts`), which is the one honest direct read.
|
|
61
|
+
*/
|
|
62
|
+
export declare function bindAuditEvents(source: AuditEventsSource | null): void;
|
|
63
|
+
/**
|
|
64
|
+
* Every declared event in a configuration (#1638): the entries of
|
|
65
|
+
* `ngdpbase.audit.events`, plus every permission entry carrying `audit`, under
|
|
66
|
+
* the permission's name. A permission's `audit.description` defaults to the
|
|
67
|
+
* permission's own description.
|
|
68
|
+
*
|
|
69
|
+
* A name in both places is a declaration made twice. The shipped file never
|
|
70
|
+
* does it (`npm run lint:audit` fails); an operator's custom file that still
|
|
71
|
+
* sets a moved event under `ngdpbase.audit.events` is honoured — its fields
|
|
72
|
+
* override the permission's — and said once, so it is never silent.
|
|
73
|
+
*/
|
|
74
|
+
export declare function auditDeclarationsFrom(read: AuditEventsSource): Record<string, AuditEventDeclaration>;
|
|
75
|
+
/** Every declared event, from the bound configuration. Empty, and said once, before binding. */
|
|
76
|
+
export declare function auditEventDeclarations(): Record<string, AuditEventDeclaration>;
|
|
77
|
+
/** The declaration for one event, or null when configuration does not name it. */
|
|
78
|
+
export declare function auditEventDeclaration(eventType: string): AuditEventDeclaration | null;
|
|
79
|
+
/** Every declared event type, sorted. */
|
|
80
|
+
export declare function auditEventTypes(): string[];
|
|
81
|
+
/** Every event type this system undertakes to emit: declared and not switched off. */
|
|
82
|
+
export declare function requiredEventTypes(): string[];
|
|
83
|
+
/** Is this event switched on? An undeclared event is not switched off. */
|
|
84
|
+
export declare function isAuditEventEnabled(eventType: string): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Does this event refuse the action when its record cannot be written
|
|
87
|
+
* (#1121, #1158)?
|
|
88
|
+
*
|
|
89
|
+
* Two layers need the same answer and must not be able to disagree:
|
|
90
|
+
* `recordAuditEvent` decides whether a failure rejects the action, and
|
|
91
|
+
* `FileAuditProvider.writeEvent` decides whether the record is fsynced before
|
|
92
|
+
* the write resolves. A rule that meant one thing to the caller and another to
|
|
93
|
+
* the writer would be the #1148 defect again.
|
|
94
|
+
*/
|
|
95
|
+
export declare function refusesOnFailure(eventType: string): boolean;
|
|
96
|
+
/** Every event type that refuses on failure, for reporting what the rule covers. */
|
|
97
|
+
export declare function refuseOnFailureEventTypes(): string[];
|
|
98
|
+
/** Events declared and switched off, with the reason. The honest half of the answer. */
|
|
99
|
+
export declare function disabledEventTypes(): Array<{
|
|
100
|
+
eventType: string;
|
|
101
|
+
description: string;
|
|
102
|
+
}>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audit event registry, read from configuration (#1200, epic #1208).
|
|
3
|
+
*
|
|
4
|
+
* `ngdpbase.audit.events` in `config/app-default-config.json` names every
|
|
5
|
+
* recorded action, what happens when its record cannot be written, and whether it fires. This module is
|
|
6
|
+
* a reader over that map: nothing here declares an event.
|
|
7
|
+
*
|
|
8
|
+
* Until #1200 the declarations lived in this file, deliberately, on the
|
|
9
|
+
* argument that an operator who could edit them could narrow what the system
|
|
10
|
+
* claims to audit. The operator's decision on 2026-09-04 is that this is the
|
|
11
|
+
* point: configuration is authoritative, and narrowing is on the record —
|
|
12
|
+
* an admin UI edit emits `config-change`, and a disk edit is reported by
|
|
13
|
+
* `posture-recorded` at the next boot. See docs/audit-posture.md.
|
|
14
|
+
*
|
|
15
|
+
* #1638 (operator, 2026-10-05): a permission and its action are one thing. An
|
|
16
|
+
* event that records a permission's action is declared on the permission
|
|
17
|
+
* entry itself — `ngdpbase.permissions.definitions.<name>.audit` — and
|
|
18
|
+
* `ngdpbase.audit.events` holds only events that are not a permission's
|
|
19
|
+
* action. {@link auditDeclarationsFrom} reads both, and is the one reader:
|
|
20
|
+
* the runtime, the generator, the lint, the doc generator and the boot check
|
|
21
|
+
* all call it, so they cannot disagree.
|
|
22
|
+
*
|
|
23
|
+
* What stays in code is the emitters. `scripts/audit-coverage.ts` proves the
|
|
24
|
+
* map and the emitters agree.
|
|
25
|
+
*/
|
|
26
|
+
import logger from './logger.js';
|
|
27
|
+
export const AUDIT_EVENTS_KEY = 'ngdpbase.audit.events';
|
|
28
|
+
/** The permission registry; an entry's `audit` field declares its event (#1638). */
|
|
29
|
+
export const PERMISSION_DEFINITIONS_KEY = 'ngdpbase.permissions.definitions';
|
|
30
|
+
let boundSource = null;
|
|
31
|
+
let warnedUnbound = false;
|
|
32
|
+
const warnedUndeclared = new Set();
|
|
33
|
+
/**
|
|
34
|
+
* Bind the live configuration. `AuditManager.initialize` calls this before it
|
|
35
|
+
* loads a provider, so every tier consulted from then on is the operator's.
|
|
36
|
+
*
|
|
37
|
+
* There is no fallback to the shipped file: reading
|
|
38
|
+
* `config/app-default-config.json` here would be a second reader over the
|
|
39
|
+
* configuration store, and a value could then come from the file when the live
|
|
40
|
+
* configuration says otherwise. Tests bind from the shipped file themselves
|
|
41
|
+
* (`vitest.setup.ts`), which is the one honest direct read.
|
|
42
|
+
*/
|
|
43
|
+
export function bindAuditEvents(source) {
|
|
44
|
+
boundSource = source;
|
|
45
|
+
warnedUnbound = false;
|
|
46
|
+
}
|
|
47
|
+
function asDeclarations(value) {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
49
|
+
return {};
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
52
|
+
// A custom config removes a shipped entry by setting it to null.
|
|
53
|
+
if (!entry || typeof entry !== 'object')
|
|
54
|
+
continue;
|
|
55
|
+
out[name] = entry;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
const warnedDoubled = new Set();
|
|
60
|
+
/**
|
|
61
|
+
* Every declared event in a configuration (#1638): the entries of
|
|
62
|
+
* `ngdpbase.audit.events`, plus every permission entry carrying `audit`, under
|
|
63
|
+
* the permission's name. A permission's `audit.description` defaults to the
|
|
64
|
+
* permission's own description.
|
|
65
|
+
*
|
|
66
|
+
* A name in both places is a declaration made twice. The shipped file never
|
|
67
|
+
* does it (`npm run lint:audit` fails); an operator's custom file that still
|
|
68
|
+
* sets a moved event under `ngdpbase.audit.events` is honoured — its fields
|
|
69
|
+
* override the permission's — and said once, so it is never silent.
|
|
70
|
+
*/
|
|
71
|
+
export function auditDeclarationsFrom(read) {
|
|
72
|
+
const events = asDeclarations(read(AUDIT_EVENTS_KEY, {}));
|
|
73
|
+
const permissions = read(PERMISSION_DEFINITIONS_KEY, {});
|
|
74
|
+
const out = {};
|
|
75
|
+
if (permissions && typeof permissions === 'object' && !Array.isArray(permissions)) {
|
|
76
|
+
for (const [name, entry] of Object.entries(permissions)) {
|
|
77
|
+
const audit = entry?.audit;
|
|
78
|
+
if (!audit || typeof audit !== 'object')
|
|
79
|
+
continue;
|
|
80
|
+
const description = entry.description;
|
|
81
|
+
out[name] = { description: typeof description === 'string' ? description : name, ...audit };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// A custom file written before #1638 removed an event with null in the
|
|
85
|
+
// events map; that still removes it, wherever it is now declared.
|
|
86
|
+
const rawEvents = read(AUDIT_EVENTS_KEY, {});
|
|
87
|
+
if (rawEvents && typeof rawEvents === 'object' && !Array.isArray(rawEvents)) {
|
|
88
|
+
for (const [name, entry] of Object.entries(rawEvents)) {
|
|
89
|
+
if (entry === null)
|
|
90
|
+
delete out[name];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const [name, d] of Object.entries(events)) {
|
|
94
|
+
if (out[name] && !warnedDoubled.has(name)) {
|
|
95
|
+
warnedDoubled.add(name);
|
|
96
|
+
logger.warn(`[audit] '${name}' is declared in ${AUDIT_EVENTS_KEY} and on its permission entry; the ${AUDIT_EVENTS_KEY} entry wins. Move it to ${PERMISSION_DEFINITIONS_KEY}.${name}.audit (#1638).`);
|
|
97
|
+
}
|
|
98
|
+
out[name] = { ...out[name], ...d };
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/** Every declared event, from the bound configuration. Empty, and said once, before binding. */
|
|
103
|
+
export function auditEventDeclarations() {
|
|
104
|
+
if (!boundSource) {
|
|
105
|
+
if (!warnedUnbound) {
|
|
106
|
+
warnedUnbound = true;
|
|
107
|
+
logger.warn(`[audit] ${AUDIT_EVENTS_KEY} is not bound yet; every event is treated as on-failure: continue until AuditManager initialises`);
|
|
108
|
+
}
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
return auditDeclarationsFrom(boundSource);
|
|
112
|
+
}
|
|
113
|
+
/** The declaration for one event, or null when configuration does not name it. */
|
|
114
|
+
export function auditEventDeclaration(eventType) {
|
|
115
|
+
const d = auditEventDeclarations()[eventType];
|
|
116
|
+
if (d)
|
|
117
|
+
return d;
|
|
118
|
+
// Never silent: an emitter producing a name configuration does not declare
|
|
119
|
+
// is treated as `standard` and said once, so the gap is visible in the log
|
|
120
|
+
// rather than in an assessor's report.
|
|
121
|
+
if (eventType && !warnedUndeclared.has(eventType)) {
|
|
122
|
+
warnedUndeclared.add(eventType);
|
|
123
|
+
logger.warn(`[audit] '${eventType}' is emitted but not declared in ${AUDIT_EVENTS_KEY}; treated as on-failure: continue`);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/** Every declared event type, sorted. */
|
|
128
|
+
export function auditEventTypes() {
|
|
129
|
+
return Object.keys(auditEventDeclarations()).sort();
|
|
130
|
+
}
|
|
131
|
+
/** Every event type this system undertakes to emit: declared and not switched off. */
|
|
132
|
+
export function requiredEventTypes() {
|
|
133
|
+
return Object.entries(auditEventDeclarations())
|
|
134
|
+
.filter(([, d]) => d.enabled !== false)
|
|
135
|
+
.map(([name]) => name)
|
|
136
|
+
.sort();
|
|
137
|
+
}
|
|
138
|
+
/** Is this event switched on? An undeclared event is not switched off. */
|
|
139
|
+
export function isAuditEventEnabled(eventType) {
|
|
140
|
+
return auditEventDeclaration(eventType)?.enabled !== false;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Does this event refuse the action when its record cannot be written
|
|
144
|
+
* (#1121, #1158)?
|
|
145
|
+
*
|
|
146
|
+
* Two layers need the same answer and must not be able to disagree:
|
|
147
|
+
* `recordAuditEvent` decides whether a failure rejects the action, and
|
|
148
|
+
* `FileAuditProvider.writeEvent` decides whether the record is fsynced before
|
|
149
|
+
* the write resolves. A rule that meant one thing to the caller and another to
|
|
150
|
+
* the writer would be the #1148 defect again.
|
|
151
|
+
*/
|
|
152
|
+
export function refusesOnFailure(eventType) {
|
|
153
|
+
return auditEventDeclaration(eventType)?.['on-failure'] === 'refuse';
|
|
154
|
+
}
|
|
155
|
+
/** Every event type that refuses on failure, for reporting what the rule covers. */
|
|
156
|
+
export function refuseOnFailureEventTypes() {
|
|
157
|
+
return Object.entries(auditEventDeclarations())
|
|
158
|
+
.filter(([, d]) => d['on-failure'] === 'refuse' && d.enabled !== false)
|
|
159
|
+
.map(([name]) => name)
|
|
160
|
+
.sort();
|
|
161
|
+
}
|
|
162
|
+
/** Events declared and switched off, with the reason. The honest half of the answer. */
|
|
163
|
+
export function disabledEventTypes() {
|
|
164
|
+
return Object.entries(auditEventDeclarations())
|
|
165
|
+
.filter(([, d]) => d.enabled === false)
|
|
166
|
+
.map(([eventType, d]) => ({ eventType, description: d.description }))
|
|
167
|
+
.sort((a, b) => a.eventType.localeCompare(b.eventType));
|
|
168
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-owned configuration keys (#1089).
|
|
3
|
+
*
|
|
4
|
+
* Six keys used to be owned by two layers at once: the environment, via a
|
|
5
|
+
* hardcoded map inside `ConfigurationManager.getProperty()` that nothing
|
|
6
|
+
* outside could see, and the admin configuration screen, which accepted and
|
|
7
|
+
* persisted edits to them that could never take effect — the override was
|
|
8
|
+
* checked *before* the merged config, so the write was inert immediately, not
|
|
9
|
+
* merely on next boot.
|
|
10
|
+
*
|
|
11
|
+
* The rule this restores: **a configuration key is owned by exactly one layer,
|
|
12
|
+
* never both.** Ownership is now declared in `ngdpbase.config.env-keys`,
|
|
13
|
+
* mirroring the `ngdpbase.config.sensitive-values` (its `secret` entries) precedent — a map rather than an
|
|
14
|
+
* array, because the UI needs the variable's name to tell an operator where the
|
|
15
|
+
* value actually lives.
|
|
16
|
+
*
|
|
17
|
+
* Two decisions worth keeping straight, both settled deliberately:
|
|
18
|
+
*
|
|
19
|
+
* - **Ownership is not conditional on the variable being set.** A key in the
|
|
20
|
+
* map is env-owned whether or not anything is currently exporting it. Making
|
|
21
|
+
* it conditional would hand the UI the source of truth whenever the variable
|
|
22
|
+
* is absent, which is precisely the ambiguity being removed.
|
|
23
|
+
* - **The shipped value is a boot fallback, not a setting.** It exists so a
|
|
24
|
+
* fresh install comes up. `describePropertySource` still reports it as the
|
|
25
|
+
* *effective* value when nothing overrides it, because the screen must show
|
|
26
|
+
* what is actually in force — but the field stays read-only either way.
|
|
27
|
+
*/
|
|
28
|
+
/** Config key holding the declared map of env-owned keys. */
|
|
29
|
+
export declare const ENV_KEYS_CONFIG_KEY = "ngdpbase.config.env-keys";
|
|
30
|
+
/** key → environment variable that supplies it. */
|
|
31
|
+
export type EnvKeyMap = Record<string, string>;
|
|
32
|
+
export interface PropertyDescription {
|
|
33
|
+
/** Whether this key is declared env-owned. Independent of whether the variable is set. */
|
|
34
|
+
envControlled: boolean;
|
|
35
|
+
/** The variable that owns it, or null when not env-owned. */
|
|
36
|
+
envVar: string | null;
|
|
37
|
+
/** The value actually in force right now. */
|
|
38
|
+
effective: unknown;
|
|
39
|
+
/** Where `effective` came from. */
|
|
40
|
+
source: 'env' | 'config';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Coerce an environment value to the type of the shipped default.
|
|
44
|
+
*
|
|
45
|
+
* Environment values are always strings, but the rest of the code expects the
|
|
46
|
+
* declared type — `ngdpbase.server.port` is `number` in `src/types/Config.ts`
|
|
47
|
+
* and ships as `3000`, so `NGDPBASE_PORT=3001` must not arrive as `"3001"` and
|
|
48
|
+
* quietly fail an arithmetic comparison somewhere downstream.
|
|
49
|
+
*
|
|
50
|
+
* Using the default's own type rather than a per-key table means there is one
|
|
51
|
+
* rule and nothing to keep in sync as keys are added.
|
|
52
|
+
*
|
|
53
|
+
* When the value cannot be coerced — a non-numeric string against a numeric
|
|
54
|
+
* default — the raw string is returned. Handing the caller something visibly
|
|
55
|
+
* wrong beats handing them a silent `NaN`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function coerceToTypeOf(value: string, defaultValue: unknown): unknown;
|
|
58
|
+
/**
|
|
59
|
+
* Describe where a config key's value comes from.
|
|
60
|
+
*
|
|
61
|
+
* @param key - The config key.
|
|
62
|
+
* @param envKeys - The declared map, or null when unavailable.
|
|
63
|
+
* @param env - Environment to read, normally `process.env`. Injected so this
|
|
64
|
+
* stays pure and testable.
|
|
65
|
+
* @param configValue - The merged config value, used both as the fallback and
|
|
66
|
+
* as the type to coerce an environment value to.
|
|
67
|
+
*/
|
|
68
|
+
export declare function describePropertySource(key: string, envKeys: EnvKeyMap | null | undefined, env: Record<string, string | undefined>, configValue: unknown): PropertyDescription;
|