@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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduled-job state in the application database (#1714). See `jobState.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Uses the one connection `DatabaseManager.getHandle()` hands out — never a
|
|
5
|
+
* database file of its own — so it is encrypted when the database is
|
|
6
|
+
* (SQLCipher, `NGDPBASE_DATABASE_KEY`). The tables come from the core
|
|
7
|
+
* migration ledger (`databaseMigrations.ts`, 20261008180000).
|
|
8
|
+
*
|
|
9
|
+
* A lock is a row in `job_leases`, taken by one conditional write: insert it,
|
|
10
|
+
* or overwrite it only when it has expired or already belongs to the caller.
|
|
11
|
+
* SQLite runs that statement as one unit, so of any servers racing for a slot
|
|
12
|
+
* exactly one sees its row change.
|
|
13
|
+
*
|
|
14
|
+
* @module providers/SqliteJobStateProvider
|
|
15
|
+
*/
|
|
16
|
+
import type Database from 'better-sqlite3-multiple-ciphers';
|
|
17
|
+
import { type JobRunRecord, type JobState, type JobStateLimits, type JobStateProvider } from './jobState.js';
|
|
18
|
+
type Handle = InstanceType<typeof Database>;
|
|
19
|
+
export interface SqliteJobStateOptions extends JobStateLimits {
|
|
20
|
+
/** For tests: the clock. */
|
|
21
|
+
now?: () => number;
|
|
22
|
+
}
|
|
23
|
+
declare class SqliteJobStateProvider implements JobStateProvider {
|
|
24
|
+
private readonly db;
|
|
25
|
+
readonly backend: "sqlite";
|
|
26
|
+
private readonly historyLimit;
|
|
27
|
+
private readonly checkpointMaxBytes;
|
|
28
|
+
private readonly now;
|
|
29
|
+
constructor(db: Handle, options?: SqliteJobStateOptions);
|
|
30
|
+
getState(jobId: string): Promise<JobState | null>;
|
|
31
|
+
putState(state: JobState): Promise<void>;
|
|
32
|
+
recordRun(jobId: string, run: JobRunRecord): Promise<void>;
|
|
33
|
+
listRuns(jobId: string): Promise<JobRunRecord[]>;
|
|
34
|
+
saveCheckpoint(jobId: string, runId: string, data: unknown): Promise<void>;
|
|
35
|
+
loadCheckpoint(jobId: string, runId: string): Promise<unknown>;
|
|
36
|
+
clearCheckpoint(jobId: string, runId: string): Promise<void>;
|
|
37
|
+
takeLock(jobId: string, slot: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
38
|
+
renewLock(jobId: string, slot: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
39
|
+
releaseLock(jobId: string, slot: string, owner: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export default SqliteJobStateProvider;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The application database's migration ledger (#1536), strictly ascending by
|
|
3
|
+
* id (YYYYMMDDHHMMSS). Each entry is a frozen snapshot of what it did on its
|
|
4
|
+
* date; see `sqliteMigrations.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import type { Migration } from './sqliteMigrations.js';
|
|
7
|
+
declare const DATABASE_MIGRATIONS: Migration[];
|
|
8
|
+
export default DATABASE_MIGRATIONS;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const DATABASE_MIGRATIONS = [
|
|
2
|
+
{
|
|
3
|
+
// #1714: scheduled-job state, history, checkpoints and slot locks.
|
|
4
|
+
id: '20261008180000',
|
|
5
|
+
description: 'scheduled jobs: job_state, job_runs, job_checkpoints, job_leases',
|
|
6
|
+
up: (db) => {
|
|
7
|
+
db.exec(`CREATE TABLE job_state (
|
|
8
|
+
job_id TEXT PRIMARY KEY,
|
|
9
|
+
last_slot_done TEXT,
|
|
10
|
+
next_slot TEXT,
|
|
11
|
+
current_run TEXT,
|
|
12
|
+
updated_at TEXT NOT NULL
|
|
13
|
+
)`);
|
|
14
|
+
db.exec(`CREATE TABLE job_runs (
|
|
15
|
+
run_id TEXT PRIMARY KEY,
|
|
16
|
+
job_id TEXT NOT NULL,
|
|
17
|
+
started_at TEXT NOT NULL,
|
|
18
|
+
record TEXT NOT NULL
|
|
19
|
+
)`);
|
|
20
|
+
db.exec('CREATE INDEX job_runs_by_job ON job_runs (job_id, started_at)');
|
|
21
|
+
db.exec(`CREATE TABLE job_checkpoints (
|
|
22
|
+
job_id TEXT NOT NULL,
|
|
23
|
+
run_id TEXT NOT NULL,
|
|
24
|
+
data TEXT NOT NULL,
|
|
25
|
+
updated_at TEXT NOT NULL,
|
|
26
|
+
PRIMARY KEY (job_id, run_id)
|
|
27
|
+
)`);
|
|
28
|
+
db.exec(`CREATE TABLE job_leases (
|
|
29
|
+
job_id TEXT NOT NULL,
|
|
30
|
+
slot TEXT NOT NULL,
|
|
31
|
+
owner TEXT NOT NULL,
|
|
32
|
+
expires_at INTEGER NOT NULL,
|
|
33
|
+
PRIMARY KEY (job_id, slot)
|
|
34
|
+
)`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
// #1715: the schedule a job's slots were computed from, so a rule changed
|
|
39
|
+
// while the server was down is recomputed rather than caught up.
|
|
40
|
+
id: '20261009140000',
|
|
41
|
+
description: 'scheduled jobs: job_state.rule',
|
|
42
|
+
up: (db) => {
|
|
43
|
+
db.exec('ALTER TABLE job_state ADD COLUMN rule TEXT');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
export default DATABASE_MIGRATIONS;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where scheduled jobs keep what must survive a restart (#1714, epic #1611):
|
|
3
|
+
* each job's last slot done, its next slot and its current run, a capped run
|
|
4
|
+
* history, a run's checkpoint, and the lock that lets exactly one server run a
|
|
5
|
+
* slot.
|
|
6
|
+
*
|
|
7
|
+
* Two backends, one contract (`__tests__/jobStateContract.ts` runs against
|
|
8
|
+
* both):
|
|
9
|
+
* - `file` — JSON in FAST_STORAGE, written atomically; a lock is a file
|
|
10
|
+
* created exclusively;
|
|
11
|
+
* - `sqlite` — tables in the application database (DatabaseManager), changed
|
|
12
|
+
* in transactions; a lock is a row taken by a conditional write.
|
|
13
|
+
*
|
|
14
|
+
* `selectJobStateBackend` reads `ngdpbase.jobs.state.provider`: `auto` picks
|
|
15
|
+
* `sqlite` when the application database is open, else `file`. Switching
|
|
16
|
+
* backends carries nothing across: the new one starts empty, next slots are
|
|
17
|
+
* recomputed and missed slots are not caught up.
|
|
18
|
+
*
|
|
19
|
+
* @module providers/jobState
|
|
20
|
+
*/
|
|
21
|
+
import type { JobContext } from '../context/JobContext.js';
|
|
22
|
+
/** One run of a job. Times are ISO 8601 instants. */
|
|
23
|
+
export interface JobRunRecord {
|
|
24
|
+
runId: string;
|
|
25
|
+
/** The slot this run is for; null for a run someone started by hand. */
|
|
26
|
+
slot: string | null;
|
|
27
|
+
startedAt: string;
|
|
28
|
+
/** 1 for the first try; a run resumed after a restart counts up. */
|
|
29
|
+
attempt: number;
|
|
30
|
+
/** `skipped`: a slot the scheduler passed over, by its catch-up or overlap rule (#1715). */
|
|
31
|
+
status: 'running' | 'completed' | 'failed' | 'interrupted' | 'skipped';
|
|
32
|
+
completedAt?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
/** A failed attempt with attempts left: when it is tried again (#1716). */
|
|
35
|
+
retryAt?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Who asked, for a run started by hand (#1746): the run is restarted as
|
|
38
|
+
* them after a restart, or reported to them. `null` for a scheduled slot,
|
|
39
|
+
* which runs as the system principal — said on purpose (#1399), never left
|
|
40
|
+
* out. A record written before this field existed reads as having none.
|
|
41
|
+
*/
|
|
42
|
+
requestedBy: JobContext | null;
|
|
43
|
+
}
|
|
44
|
+
export interface JobState {
|
|
45
|
+
jobId: string;
|
|
46
|
+
lastSlotDone: string | null;
|
|
47
|
+
/** The first slot not yet handled: run, skipped or failed (#1715). */
|
|
48
|
+
nextSlot: string | null;
|
|
49
|
+
current: JobRunRecord | null;
|
|
50
|
+
/**
|
|
51
|
+
* The schedule the slots were computed from (`rrule|dtstart|timeZone`), so a
|
|
52
|
+
* rule changed while the server was down is seen at the next start: slots
|
|
53
|
+
* are recomputed from then, and nothing is caught up for the old rule
|
|
54
|
+
* (#1715). Null for state written before the rule was kept.
|
|
55
|
+
*/
|
|
56
|
+
rule: string | null;
|
|
57
|
+
}
|
|
58
|
+
export interface JobStateLimits {
|
|
59
|
+
/** Runs kept per job (`ngdpbase.jobs.history-limit`). Default 50. */
|
|
60
|
+
historyLimit?: number;
|
|
61
|
+
/** Largest checkpoint accepted, in bytes of JSON. Default 256 KiB. */
|
|
62
|
+
checkpointMaxBytes?: number;
|
|
63
|
+
}
|
|
64
|
+
export declare const DEFAULT_HISTORY_LIMIT = 50;
|
|
65
|
+
export declare const DEFAULT_CHECKPOINT_MAX_BYTES: number;
|
|
66
|
+
export interface JobStateProvider {
|
|
67
|
+
readonly backend: 'file' | 'sqlite';
|
|
68
|
+
getState(jobId: string): Promise<JobState | null>;
|
|
69
|
+
putState(state: JobState): Promise<void>;
|
|
70
|
+
/** Add or replace a run in the job's history, keeping the newest `historyLimit`. */
|
|
71
|
+
recordRun(jobId: string, run: JobRunRecord): Promise<void>;
|
|
72
|
+
/** The job's runs, newest first. */
|
|
73
|
+
listRuns(jobId: string): Promise<JobRunRecord[]>;
|
|
74
|
+
/** Throws when the JSON is larger than `checkpointMaxBytes`. */
|
|
75
|
+
saveCheckpoint(jobId: string, runId: string, data: unknown): Promise<void>;
|
|
76
|
+
loadCheckpoint(jobId: string, runId: string): Promise<unknown>;
|
|
77
|
+
clearCheckpoint(jobId: string, runId: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Take the lock for one slot, or steal it when its holder let it expire.
|
|
80
|
+
* True for exactly one caller among any that contend.
|
|
81
|
+
*/
|
|
82
|
+
takeLock(jobId: string, slot: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
83
|
+
/** Extend a lock this owner holds. False when it no longer holds it. */
|
|
84
|
+
renewLock(jobId: string, slot: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
85
|
+
/** Give up a lock this owner holds; a lock held by another owner is left alone. */
|
|
86
|
+
releaseLock(jobId: string, slot: string, owner: string): Promise<void>;
|
|
87
|
+
close?(): void;
|
|
88
|
+
}
|
|
89
|
+
export declare function assertJobId(jobId: string): void;
|
|
90
|
+
/** A slot as a compact UTC stamp, safe in a file name: `20261008T060000Z`. */
|
|
91
|
+
export declare function slotKey(slot: string): string;
|
|
92
|
+
/** The JSON a checkpoint is stored as, refused when larger than the cap. */
|
|
93
|
+
export declare function checkpointJson(data: unknown, maxBytes: number): string;
|
|
94
|
+
/**
|
|
95
|
+
* Which backend `ngdpbase.jobs.state.provider` names. `auto` (the default)
|
|
96
|
+
* picks `sqlite` when the application database is open. `sqlite` with no
|
|
97
|
+
* database open is an error naming the key to set.
|
|
98
|
+
*/
|
|
99
|
+
export declare function selectJobStateBackend(configured: unknown, databaseEnabled: boolean): 'file' | 'sqlite';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lunr-style query strings, read as MiniSearch query trees (#1736).
|
|
3
|
+
*
|
|
4
|
+
* The search provider moved from Lunr to MiniSearch, but its callers still
|
|
5
|
+
* speak Lunr's query language: the field-scoped search builds `+title:word`,
|
|
6
|
+
* type-ahead appends `*` to the last term (`applyPrefixToLastTerm`), and a
|
|
7
|
+
* person may type any of it into the search box. This keeps that language,
|
|
8
|
+
* with the same meaning:
|
|
9
|
+
*
|
|
10
|
+
* word an optional term; with no required terms, any one matches
|
|
11
|
+
* +word required: every result contains it
|
|
12
|
+
* -word excluded: no result contains it
|
|
13
|
+
* field:word only that field (an unknown field is read as plain text, so
|
|
14
|
+
* a typed URL such as `https://x` is a search, not an error)
|
|
15
|
+
* word* prefix match
|
|
16
|
+
* word~1 fuzzy match, up to that edit distance
|
|
17
|
+
*
|
|
18
|
+
* A term's `^boost` is accepted and ignored. When required terms are present,
|
|
19
|
+
* optional terms no longer narrow the results (as in Lunr); they are not used
|
|
20
|
+
* for ranking either.
|
|
21
|
+
*
|
|
22
|
+
* @module providers/lunrQueryToMiniSearch
|
|
23
|
+
*/
|
|
24
|
+
import { type Query } from 'minisearch';
|
|
25
|
+
/**
|
|
26
|
+
* The MiniSearch query for a Lunr-style string, or null when the string holds
|
|
27
|
+
* nothing to search for.
|
|
28
|
+
*
|
|
29
|
+
* @param query - the query as a caller or a person wrote it
|
|
30
|
+
* @param fields - the indexed field names; a `field:` prefix naming another is plain text
|
|
31
|
+
*/
|
|
32
|
+
export declare function lunrQueryToMiniSearch(query: string, fields: readonly string[]): Query | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative filename → capture-date parser (#809, slice C of #807).
|
|
3
|
+
*
|
|
4
|
+
* Last-resort fallback used by media providers when a file carries no usable
|
|
5
|
+
* EXIF/QuickTime capture date. Many devices encode the capture time in the
|
|
6
|
+
* filename (e.g. `PXL_20260113_123414708.jpg`, `2026-04-03-IMG_4654.jpg`,
|
|
7
|
+
* `Feb_20_2026_19_25_04.jpg`); without this, those files index with
|
|
8
|
+
* `dateTimeOriginal: null` and sort only by file `mtime`.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints (from the issue):
|
|
11
|
+
* - **Conservative** — only a small set of high-confidence, anchored patterns
|
|
12
|
+
* are accepted. Anything ambiguous returns `null`, and the caller then
|
|
13
|
+
* falls back to `mtime` and surfaces the file to admins.
|
|
14
|
+
* - **Distinguishable provenance** — the caller records that the resulting
|
|
15
|
+
* date came from the filename (`captureDateSource: 'filename'`), so a
|
|
16
|
+
* parsed date never masquerades as authoritative EXIF metadata.
|
|
17
|
+
*
|
|
18
|
+
* Returns a `"YYYY-MM-DD HH:MM:SS"` string (the exact shape produced by the
|
|
19
|
+
* EXIF extractor, so it flows through sort/filter identically), or `null`.
|
|
20
|
+
* A date-only match yields a `00:00:00` time.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseDateFromFilename(filename: string): string | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Linux statfs f_type values for network filesystems (linux/magic.h). */
|
|
2
|
+
export declare const NETWORK_FILESYSTEMS: Readonly<Record<number, string>>;
|
|
3
|
+
export interface StatFsLike {
|
|
4
|
+
type: number;
|
|
5
|
+
}
|
|
6
|
+
/** The network filesystem `target` would live on, or null (#1714: shared by the SQLite and file stores). */
|
|
7
|
+
export declare function networkFilesystemAt(target: string, statfs?: (path: string) => StatFsLike, platform?: string): {
|
|
8
|
+
name: string;
|
|
9
|
+
probe: string;
|
|
10
|
+
type: number;
|
|
11
|
+
} | null;
|
|
12
|
+
/**
|
|
13
|
+
* Throws, naming the path and the filesystem, when `databaseFile` would be opened on a network
|
|
14
|
+
* filesystem. A file that does not exist yet is judged by the nearest directory that does, since
|
|
15
|
+
* that is where SQLite will create it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function refuseNetworkFilesystem(databaseFile: string, statfs?: (path: string) => StatFsLike, platform?: string): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite must not live on a network filesystem (ported from yourPHR, yourphr#628; ngdpbase #1536).
|
|
3
|
+
*
|
|
4
|
+
* Every database here runs in WAL mode, and WAL coordinates readers and writers through a `-shm`
|
|
5
|
+
* file each connection mmaps shared-writable. That does not work over NFS or SMB, and SQLite
|
|
6
|
+
* documents WAL there as unsupported: the database will not open, or opens and corrupts — on PHI,
|
|
7
|
+
* discovered at restore time. So the boot stats where each database will ACTUALLY live, whatever root
|
|
8
|
+
* it was composed from or however it was labelled, and refuses when that is a known network
|
|
9
|
+
* filesystem.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately narrow, as the issue decided:
|
|
12
|
+
* - fail-CLOSED on a known-bad type, fail-OPEN on anything unrecognised — the magic numbers are
|
|
13
|
+
* Linux's; a laptop's APFS (0x1a on macOS) must boot, not be refused;
|
|
14
|
+
* - boot-time only: a path that becomes a mount later is not re-checked;
|
|
15
|
+
* - SQLite files only. The backup destination SHOULD be on a NAS, and is never checked here.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, statfsSync } from 'node:fs';
|
|
18
|
+
import { dirname, resolve } from 'node:path';
|
|
19
|
+
/** Linux statfs f_type values for network filesystems (linux/magic.h). */
|
|
20
|
+
export const NETWORK_FILESYSTEMS = {
|
|
21
|
+
0x6969: 'NFS',
|
|
22
|
+
0x517b: 'SMB',
|
|
23
|
+
0xff534d42: 'CIFS',
|
|
24
|
+
0xfe534d42: 'SMB2'
|
|
25
|
+
};
|
|
26
|
+
/** The network filesystem `target` would live on, or null (#1714: shared by the SQLite and file stores). */
|
|
27
|
+
export function networkFilesystemAt(target, statfs = (p) => statfsSync(p), platform = process.platform) {
|
|
28
|
+
if (platform !== 'linux')
|
|
29
|
+
return null; // the numbers below are Linux's; elsewhere, unknown = allowed
|
|
30
|
+
let probe = resolve(target);
|
|
31
|
+
while (!existsSync(probe) && dirname(probe) !== probe)
|
|
32
|
+
probe = dirname(probe);
|
|
33
|
+
let type;
|
|
34
|
+
try {
|
|
35
|
+
type = statfs(probe).type;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null; // cannot tell: fail open, as for an unrecognised type
|
|
39
|
+
}
|
|
40
|
+
const name = NETWORK_FILESYSTEMS[type];
|
|
41
|
+
return name ? { name, probe, type } : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Throws, naming the path and the filesystem, when `databaseFile` would be opened on a network
|
|
45
|
+
* filesystem. A file that does not exist yet is judged by the nearest directory that does, since
|
|
46
|
+
* that is where SQLite will create it.
|
|
47
|
+
*/
|
|
48
|
+
export function refuseNetworkFilesystem(databaseFile, statfs = (p) => statfsSync(p), platform = process.platform) {
|
|
49
|
+
const found = networkFilesystemAt(databaseFile, statfs, platform);
|
|
50
|
+
if (found) {
|
|
51
|
+
throw new Error(`refusing to start: the SQLite database ${databaseFile} would live on ${found.name} (${found.probe}, filesystem type 0x${found.type.toString(16)}). ` +
|
|
52
|
+
'SQLite in WAL mode needs shared memory, which network filesystems do not provide — the database would fail to open or corrupt. ' +
|
|
53
|
+
'Put the data directory (FAST_STORAGE) on local disk; backups may still go to the NAS (#1536).');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema migrations — dated, run-once, recorded, transactional. Ported from yourPHR (yourphr#542),
|
|
3
|
+
* where it runs in production; ngdpbase #1536. A migration is a FROZEN SNAPSHOT of intent at its
|
|
4
|
+
* date — it never references live code, because the live code moves on and the migration must keep
|
|
5
|
+
* describing what it did the day it ran.
|
|
6
|
+
*
|
|
7
|
+
* Two rules yourPHR paid to learn, made structural here:
|
|
8
|
+
*
|
|
9
|
+
* - yourphr#528: ADD COLUMN leaves every EXISTING row NULL, not zero — and NULL + 1 is NULL, so
|
|
10
|
+
* a revocation counter "incremented" for months without moving, reporting success throughout.
|
|
11
|
+
* addColumnWithDefault() is the required spelling for counter-like columns: default for new
|
|
12
|
+
* rows, explicit backfill for existing ones, in the same migration.
|
|
13
|
+
* - Downgrade protection: a database that records a migration THIS build has never heard of is a
|
|
14
|
+
* database from the future, and opening it "best effort" is how schema corruption happens.
|
|
15
|
+
* Refuse to start instead.
|
|
16
|
+
*
|
|
17
|
+
* No down() — deliberately: the rollback story is a backup taken before anything migrates, not a
|
|
18
|
+
* reverse script that was never tested against real data.
|
|
19
|
+
*/
|
|
20
|
+
import type Database from 'better-sqlite3-multiple-ciphers';
|
|
21
|
+
export interface Migration {
|
|
22
|
+
/** Dated, sortable: YYYYMMDDHHMMSS. The registry must be strictly ascending. */
|
|
23
|
+
id: string;
|
|
24
|
+
description: string;
|
|
25
|
+
up: (db: InstanceType<typeof Database>) => void;
|
|
26
|
+
}
|
|
27
|
+
export interface MigrationReport {
|
|
28
|
+
applied: string[];
|
|
29
|
+
skipped: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* ADD COLUMN with a default AND a backfill of existing rows, in one statement pair — the yourphr#528
|
|
33
|
+
* spelling. SQLite's ADD COLUMN ... DEFAULT only applies the default to rows created afterwards
|
|
34
|
+
* on some historic versions and to reads via the schema on others; the explicit UPDATE removes
|
|
35
|
+
* the ambiguity for every row that already exists.
|
|
36
|
+
*/
|
|
37
|
+
export declare function addColumnWithDefault(db: InstanceType<typeof Database>, table: string, column: string, type: 'INTEGER' | 'TEXT' | 'REAL', defaultValue: number | string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Applies every pending migration, in order, each in its own transaction. Stops loudly at the
|
|
40
|
+
* first failure, leaving everything before it applied and everything from it untouched — a rerun
|
|
41
|
+
* after the fix picks up exactly there.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* `ledgerTable` names the table the applied ids are recorded in. A second database keeps its own
|
|
45
|
+
* name (yourphr#784): a backup holding several databases' tables in one file splits them back BY
|
|
46
|
+
* TABLE NAME on restore, and two tables called schema_migrations could not be told apart.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runMigrations(db: InstanceType<typeof Database>, registry: Migration[], ledgerTable?: string): MigrationReport;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const ID_SHAPE = /^\d{14}$/;
|
|
2
|
+
/**
|
|
3
|
+
* ADD COLUMN with a default AND a backfill of existing rows, in one statement pair — the yourphr#528
|
|
4
|
+
* spelling. SQLite's ADD COLUMN ... DEFAULT only applies the default to rows created afterwards
|
|
5
|
+
* on some historic versions and to reads via the schema on others; the explicit UPDATE removes
|
|
6
|
+
* the ambiguity for every row that already exists.
|
|
7
|
+
*/
|
|
8
|
+
export function addColumnWithDefault(db, table, column, type, defaultValue) {
|
|
9
|
+
const quoted = typeof defaultValue === 'string' ? `'${defaultValue.replace(/'/g, "''")}'` : String(defaultValue);
|
|
10
|
+
db.exec(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${type} NOT NULL DEFAULT ${quoted}`);
|
|
11
|
+
db.exec(`UPDATE "${table}" SET "${column}" = ${quoted} WHERE "${column}" IS NULL`);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Applies every pending migration, in order, each in its own transaction. Stops loudly at the
|
|
15
|
+
* first failure, leaving everything before it applied and everything from it untouched — a rerun
|
|
16
|
+
* after the fix picks up exactly there.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* `ledgerTable` names the table the applied ids are recorded in. A second database keeps its own
|
|
20
|
+
* name (yourphr#784): a backup holding several databases' tables in one file splits them back BY
|
|
21
|
+
* TABLE NAME on restore, and two tables called schema_migrations could not be told apart.
|
|
22
|
+
*/
|
|
23
|
+
export function runMigrations(db, registry, ledgerTable = 'schema_migrations') {
|
|
24
|
+
// Digits are allowed after the first letter: an add-on slug may carry them ('calendar2'), and its
|
|
25
|
+
// ledger is named after the slug (ngdp-accounting-addons#13).
|
|
26
|
+
if (!/^[a-z][a-z0-9_]*$/.test(ledgerTable))
|
|
27
|
+
throw new Error(`migration ledger table ${ledgerTable} is not a plain name`);
|
|
28
|
+
// Registry hygiene before anything touches the database.
|
|
29
|
+
let previous = '';
|
|
30
|
+
for (const m of registry) {
|
|
31
|
+
if (!ID_SHAPE.test(m.id)) {
|
|
32
|
+
throw new Error(`migration id ${m.id} is not YYYYMMDDHHMMSS`);
|
|
33
|
+
}
|
|
34
|
+
if (m.id <= previous) {
|
|
35
|
+
throw new Error(`migration ids must be strictly ascending: ${m.id} follows ${previous}`);
|
|
36
|
+
}
|
|
37
|
+
previous = m.id;
|
|
38
|
+
}
|
|
39
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${ledgerTable} (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
description TEXT NOT NULL,
|
|
42
|
+
applied_at TEXT NOT NULL
|
|
43
|
+
)`);
|
|
44
|
+
// Downgrade protection: an applied id the registry does not know means the database belongs to a
|
|
45
|
+
// NEWER build. Opening it anyway is how corruption happens — refuse, name the ids.
|
|
46
|
+
const appliedIds = new Set(db.prepare(`SELECT id FROM ${ledgerTable}`).all().map((r) => r.id));
|
|
47
|
+
const known = new Set(registry.map((m) => m.id));
|
|
48
|
+
const fromTheFuture = [...appliedIds].filter((id) => !known.has(id));
|
|
49
|
+
if (fromTheFuture.length > 0) {
|
|
50
|
+
throw new Error(`database schema is newer than this build — applied migrations this build does not know: ${fromTheFuture.sort().join(', ')}. Refusing to start; upgrade the app or restore the matching backup.`);
|
|
51
|
+
}
|
|
52
|
+
const report = { applied: [], skipped: 0 };
|
|
53
|
+
for (const migration of registry) {
|
|
54
|
+
if (appliedIds.has(migration.id)) {
|
|
55
|
+
report.skipped++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
db.exec('BEGIN');
|
|
59
|
+
try {
|
|
60
|
+
migration.up(db);
|
|
61
|
+
db.prepare(`INSERT INTO ${ledgerTable} (id, description, applied_at) VALUES (?, ?, ?)`).run(migration.id, migration.description, new Date().toISOString());
|
|
62
|
+
db.exec('COMMIT');
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
db.exec('ROLLBACK');
|
|
66
|
+
// The id in the error is what makes a 2am failure debuggable from the log line alone.
|
|
67
|
+
throw new Error(`migration ${migration.id} (${migration.description}) failed and was rolled back: ${err.message}`, { cause: err });
|
|
68
|
+
}
|
|
69
|
+
report.applied.push(migration.id);
|
|
70
|
+
}
|
|
71
|
+
return report;
|
|
72
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub alerts (#1493): a blockquote whose first line is exactly `[!NOTE]`,
|
|
3
|
+
* `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` renders in GitHub's
|
|
4
|
+
* own shape, stored as written:
|
|
5
|
+
*
|
|
6
|
+
* <div class="markdown-alert markdown-alert-note">
|
|
7
|
+
* <p class="markdown-alert-title"><svg …/>Note</p>
|
|
8
|
+
* …the rest of the quote…
|
|
9
|
+
* </div>
|
|
10
|
+
*
|
|
11
|
+
* Written here rather than taken from `markdown-it-github-alerts`: that plugin
|
|
12
|
+
* reads any text after the marker as a custom title and writes it into the
|
|
13
|
+
* page unescaped, which would let `> [!NOTE] <img onerror=…>` past the HTML
|
|
14
|
+
* policy (#1623). GitHub itself does not make an alert of a marker with text
|
|
15
|
+
* after it on the same line, and neither does this rule. Titles are fixed.
|
|
16
|
+
*
|
|
17
|
+
* Icons: GitHub's Octicons (info, light-bulb, report, alert, stop), MIT
|
|
18
|
+
* licensed, https://github.com/primer/octicons.
|
|
19
|
+
*/
|
|
20
|
+
import type MarkdownIt from 'markdown-it';
|
|
21
|
+
/** Turn qualifying blockquotes into GitHub alerts. */
|
|
22
|
+
export declare function githubAlerts(md: MarkdownIt): void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one HTML policy for page content (#1623).
|
|
3
|
+
*
|
|
4
|
+
* Raw HTML an author writes in a page is held to one allow-list, declared once
|
|
5
|
+
* in configuration (`ngdpbase.markup.html-policy`) and enforced here by one
|
|
6
|
+
* parser — sanitize-html, built on htmlparser2. Save and render ask the same
|
|
7
|
+
* question of the same code:
|
|
8
|
+
*
|
|
9
|
+
* - render: {@link filterAuthorHtml} rewrites each piece of author HTML,
|
|
10
|
+
* keeping what the policy allows and dropping the rest. It runs inside
|
|
11
|
+
* markdown-it's `html_block` / `html_inline` renderers
|
|
12
|
+
* (src/rendering/markdownConverter.ts), so it sees author HTML and nothing
|
|
13
|
+
* else: plugin, link and variable output is merged in after markdown-it,
|
|
14
|
+
* from placeholders, and is never seen by it.
|
|
15
|
+
* - save: {@link htmlPolicyViolations} names what that rewrite would drop,
|
|
16
|
+
* and SecurityFilter refuses the save with those names.
|
|
17
|
+
*
|
|
18
|
+
* Why per tag, not per fragment: markdown-it hands author HTML over in pieces —
|
|
19
|
+
* `<div>` and `</div>` arrive as separate tokens around the markdown between
|
|
20
|
+
* them, and inline HTML arrives one tag per token. A tree sanitiser given a
|
|
21
|
+
* piece balances it (`<div>` becomes `<div></div>`) and drops a close it never
|
|
22
|
+
* saw opened, which would break every page that wraps markdown in HTML. So each
|
|
23
|
+
* piece is split into tags and text with markdown-it's own HTML tag grammar,
|
|
24
|
+
* each open tag is rebuilt by sanitize-html from its parsed attributes, a close
|
|
25
|
+
* tag stays if its tag is allowed, and text is escaped.
|
|
26
|
+
*
|
|
27
|
+
* Why that is safe whatever the grammar's edge cases: everything not taken as
|
|
28
|
+
* a tag is escaped (`<` becomes `<`), so the browser cannot find a tag in it;
|
|
29
|
+
* everything taken as a tag is re-serialised by sanitize-html from what
|
|
30
|
+
* htmlparser2 parsed; comments, declarations and processing instructions are
|
|
31
|
+
* dropped. A disagreement between the grammar and a browser can only turn
|
|
32
|
+
* markup into visible text, never text into markup.
|
|
33
|
+
*
|
|
34
|
+
* Without a policy (no ConfigurationManager, or a malformed value) nothing an
|
|
35
|
+
* author wrote is allowed: the failure is closed. The pipeline's own
|
|
36
|
+
* constructs ({@link PIPELINE_HTML}) are always allowed.
|
|
37
|
+
*
|
|
38
|
+
* @module rendering/htmlPolicy
|
|
39
|
+
*/
|
|
40
|
+
/** The configuration key that declares the policy. */
|
|
41
|
+
export declare const HTML_POLICY_KEY = "ngdpbase.markup.html-policy";
|
|
42
|
+
/**
|
|
43
|
+
* The policy, read from configuration. There each list is a set kept as a map
|
|
44
|
+
* of `name: true` (#1612), so a layer adds or removes one entry without
|
|
45
|
+
* restating the rest; here it is the enabled names.
|
|
46
|
+
*/
|
|
47
|
+
export interface HtmlPolicy {
|
|
48
|
+
/** Tags an author may write. */
|
|
49
|
+
tags: string[];
|
|
50
|
+
/** Attributes allowed per tag; `*` applies to every allowed tag. */
|
|
51
|
+
attributes: Record<string, string[]>;
|
|
52
|
+
/** URL schemes allowed in URL attributes (`href`, `src`, …). Relative URLs are always allowed. */
|
|
53
|
+
schemes: string[];
|
|
54
|
+
/** A tag's own scheme set, used instead of `schemes` for that tag (`schemes-by-tag`). */
|
|
55
|
+
schemesByTag: Record<string, string[]>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The configured value of `ngdpbase.markup.html-policy` as a policy, or null
|
|
59
|
+
* when it is absent or not an object — which allows no author HTML at all.
|
|
60
|
+
*/
|
|
61
|
+
export declare function asHtmlPolicy(value: unknown): HtmlPolicy | null;
|
|
62
|
+
/**
|
|
63
|
+
* Author HTML as the policy allows it. `html` is one piece of author HTML —
|
|
64
|
+
* an html_block or html_inline token's content, or a raw block's text.
|
|
65
|
+
*/
|
|
66
|
+
export declare function filterAuthorHtml(html: string, policy: HtmlPolicy | null): string;
|
|
67
|
+
/** One thing the policy does not allow, at an offset into the fragment. */
|
|
68
|
+
export interface HtmlPolicyViolation {
|
|
69
|
+
message: string;
|
|
70
|
+
offset: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What {@link filterAuthorHtml} would drop from `html`, named for the author.
|
|
74
|
+
* Empty when the policy allows all of it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function htmlPolicyViolations(html: string, policy: HtmlPolicy | null): HtmlPolicyViolation[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that says how markdown becomes HTML (#1273, epic #1271).
|
|
3
|
+
*
|
|
4
|
+
* markdown-it replaces showdown, which has had no release since 2023 and whose
|
|
5
|
+
* mitigations (#599, #1064) were ours to keep correct on a dead parser; it was
|
|
6
|
+
* removed in #1274. Every option here was measured on the real page corpus by
|
|
7
|
+
* the #1272 harness (since retired), and each deliberate difference from
|
|
8
|
+
* showdown is a decision on the #1271 log (R2–R17), pinned by
|
|
9
|
+
* `__tests__/markdownConverter.test.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Three profiles, not one shared configuration (R3) — the call sites differed
|
|
12
|
+
* under showdown, and merging them would silently change their HTML:
|
|
13
|
+
*
|
|
14
|
+
* - `page`: page bodies, through MarkupParser. Single-newline breaks, heading
|
|
15
|
+
* ids from `SectionUtils.headingSlug`, sub/superscript, task lists.
|
|
16
|
+
* - `untrusted`: comments (`renderUntrustedInline`). Breaks, tables, fences
|
|
17
|
+
* and `<del>`; no heading ids, task lists or sub/superscript — the showdown
|
|
18
|
+
* converter it replaces had none of them.
|
|
19
|
+
* - `fallback`: degraded paths that run without the page pipeline. Plain
|
|
20
|
+
* CommonMark.
|
|
21
|
+
*
|
|
22
|
+
* The converter object keeps showdown's `makeHtml` shape, so a call site
|
|
23
|
+
* changes its constructor and nothing else.
|
|
24
|
+
*
|
|
25
|
+
* Author HTML (#1623): in the `page` and `fallback` profiles, every html_block
|
|
26
|
+
* and html_inline token markdown-it parses out of the source — the HTML an
|
|
27
|
+
* author wrote — is rewritten by the one HTML policy
|
|
28
|
+
* (src/rendering/htmlPolicy.ts) as it renders. Those two token types are the
|
|
29
|
+
* only way raw HTML leaves markdown-it, so nothing else needs guarding here. The policy is read at render time from whoever made the
|
|
30
|
+
* converter, so a change in configuration applies without a restart; with no
|
|
31
|
+
* reader, no author HTML is allowed. `untrusted` keeps its own sanitiser
|
|
32
|
+
* (src/utils/renderUntrustedInline.ts).
|
|
33
|
+
*
|
|
34
|
+
* @module rendering/markdownConverter
|
|
35
|
+
*/
|
|
36
|
+
import MarkdownIt from 'markdown-it';
|
|
37
|
+
export type MarkdownProfile = 'page' | 'untrusted' | 'fallback';
|
|
38
|
+
/** showdown's converter shape, kept so call sites swap without rewriting. */
|
|
39
|
+
export interface MarkdownConverter {
|
|
40
|
+
makeHtml(markdown: string): string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A markdown-it instance for one profile. Exported for the option tests; app
|
|
44
|
+
* code uses {@link createMarkdownConverter}.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildMarkdownIt(profile: MarkdownProfile): MarkdownIt;
|
|
47
|
+
/**
|
|
48
|
+
* Inline Markdown for one line of text that is not a paragraph: a table cell
|
|
49
|
+
* (#1351). The page profile's inline rules — `**bold**`, `*italic*`,
|
|
50
|
+
* `~~strike~~`, `~sub~`, `^sup^`, `` `code` ``, `[text](url)` — with HTML
|
|
51
|
+
* escaped rather than passed through, which is what a cell has always done.
|
|
52
|
+
* No block rules and no paragraph: `renderInline`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function renderInlineMarkdown(text: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* The converter for a profile. `readHtmlPolicy` returns the configured value of
|
|
57
|
+
* `ngdpbase.markup.html-policy` and is called on every render, so the policy in
|
|
58
|
+
* force is always the configured one; without it, no author HTML is allowed.
|
|
59
|
+
* One markdown-it instance per profile; renders are independent.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createMarkdownConverter(profile: MarkdownProfile, readHtmlPolicy?: () => unknown): MarkdownConverter;
|
|
62
|
+
/** A piece of author HTML in page source, and the 0-based line it starts on. */
|
|
63
|
+
export interface AuthorHtml {
|
|
64
|
+
html: string;
|
|
65
|
+
line: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The author HTML in page source, found exactly as the page profile finds it
|
|
69
|
+
* when rendering: the html_block and html_inline tokens. Code blocks and code
|
|
70
|
+
* spans are code to markdown-it, so they are not here. The save-time check
|
|
71
|
+
* (SecurityFilter) holds these to the policy the renderer applies (#1623).
|
|
72
|
+
*/
|
|
73
|
+
export declare function authorHtmlIn(markdown: string): AuthorHtml[];
|