@jwilleke/ngdpbase-types 0.0.0 → 5.19.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.
Files changed (304) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +16 -2
  3. package/lib/packages/ngdpbase-types/index.d.ts +32 -0
  4. package/lib/packages/ngdpbase-types/testing.d.ts +43 -0
  5. package/lib/packages/ngdpbase-types/testing.js +53 -0
  6. package/lib/src/cache/CacheDependencies.d.ts +50 -0
  7. package/lib/src/cache/CacheDependencies.js +50 -0
  8. package/lib/src/cache/ICacheAdapter.d.ts +86 -0
  9. package/lib/src/cache/RegionCache.d.ts +171 -0
  10. package/lib/src/context/ActorContext.d.ts +48 -0
  11. package/lib/src/context/ActorContext.js +33 -0
  12. package/lib/src/context/ApiContext.d.ts +194 -0
  13. package/lib/src/context/ApiContext.js +273 -0
  14. package/lib/src/context/BaseContext.d.ts +110 -0
  15. package/lib/src/context/BaseContext.js +143 -0
  16. package/lib/src/context/JobContext.d.ts +153 -0
  17. package/lib/src/context/JobContext.js +144 -0
  18. package/lib/src/context/WikiContext.d.ts +404 -0
  19. package/lib/src/context/bootActions.d.ts +64 -0
  20. package/lib/src/context/bootActions.js +111 -0
  21. package/lib/src/converters/HtmlConverter.d.ts +70 -0
  22. package/lib/src/converters/IContentConverter.d.ts +94 -0
  23. package/lib/src/converters/JSPWikiConverter.d.ts +117 -0
  24. package/lib/src/converters/conversionWarning.d.ts +15 -0
  25. package/lib/src/converters/ncm/bridge.d.ts +22 -0
  26. package/lib/src/converters/ncm/existing.d.ts +23 -0
  27. package/lib/src/converters/ncm/fix/blocks.d.ts +89 -0
  28. package/lib/src/converters/ncm/fix/bulletMarkers.d.ts +21 -0
  29. package/lib/src/converters/ncm/fix/index.d.ts +36 -0
  30. package/lib/src/converters/ncm/fix/jspwikiBullets.d.ts +20 -0
  31. package/lib/src/converters/ncm/fix/jspwikiCodeMarkers.d.ts +38 -0
  32. package/lib/src/converters/ncm/fix/jspwikiHeadings.d.ts +25 -0
  33. package/lib/src/converters/ncm/fix/jspwikiIndent.d.ts +24 -0
  34. package/lib/src/converters/ncm/fix/jspwikiInsert.d.ts +21 -0
  35. package/lib/src/converters/ncm/fix/jspwikiItalic.d.ts +29 -0
  36. package/lib/src/converters/ncm/fix/jspwikiLinkLeftovers.d.ts +27 -0
  37. package/lib/src/converters/ncm/fix/moreInformationFooter.d.ts +25 -0
  38. package/lib/src/converters/ncm/fix/styleClosers.d.ts +25 -0
  39. package/lib/src/converters/ncm/fix/tightenLists.d.ts +30 -0
  40. package/lib/src/converters/ncm/fix/types.d.ts +33 -0
  41. package/lib/src/converters/ncm/footnotes.d.ts +39 -0
  42. package/lib/src/converters/ncm/images.d.ts +49 -0
  43. package/lib/src/converters/ncm/index.d.ts +20 -0
  44. package/lib/src/converters/ncm/links.d.ts +21 -0
  45. package/lib/src/converters/ncm/normalize.d.ts +39 -0
  46. package/lib/src/converters/ncm/placeholder.d.ts +25 -0
  47. package/lib/src/converters/ncm/tables.d.ts +36 -0
  48. package/lib/src/converters/ncm/types.d.ts +42 -0
  49. package/lib/src/http/egressPolicy.d.ts +52 -0
  50. package/lib/src/http/guardedFetch.d.ts +89 -0
  51. package/lib/src/http/guardedFetch.js +113 -0
  52. package/lib/src/http/guardedLookup.d.ts +48 -0
  53. package/lib/src/http/guardedLookup.js +73 -0
  54. package/lib/src/http/ssrf.d.ts +83 -0
  55. package/lib/src/http/ssrf.js +311 -0
  56. package/lib/src/mail/MailProvider.d.ts +36 -0
  57. package/lib/src/mail/NodemailerMailProvider.d.ts +26 -0
  58. package/lib/src/managers/AddonsManager.d.ts +691 -0
  59. package/lib/src/managers/AgentTokenManager.d.ts +269 -0
  60. package/lib/src/managers/AssetManager.d.ts +116 -0
  61. package/lib/src/managers/AttachmentManager.d.ts +1032 -0
  62. package/lib/src/managers/AuthManager.d.ts +1257 -0
  63. package/lib/src/managers/BackgroundJobManager.d.ts +434 -0
  64. package/lib/src/managers/BaseManager.d.ts +427 -0
  65. package/lib/src/managers/BaseManager.js +415 -0
  66. package/lib/src/managers/CacheManager.d.ts +159 -0
  67. package/lib/src/managers/CatalogManager.d.ts +235 -0
  68. package/lib/src/managers/CommentManager.d.ts +75 -0
  69. package/lib/src/managers/ConfigurationManager.d.ts +747 -0
  70. package/lib/src/managers/DatabaseManager.d.ts +188 -0
  71. package/lib/src/managers/DatabaseManager.js +590 -0
  72. package/lib/src/managers/EmailManager.d.ts +47 -0
  73. package/lib/src/managers/ExportManager.d.ts +129 -0
  74. package/lib/src/managers/ExportManager.js +420 -0
  75. package/lib/src/managers/FootnoteManager.d.ts +118 -0
  76. package/lib/src/managers/MediaManager.d.ts +279 -0
  77. package/lib/src/managers/MetricsManager.d.ts +89 -0
  78. package/lib/src/managers/NotificationManager.d.ts +165 -0
  79. package/lib/src/managers/OidcManager.d.ts +210 -0
  80. package/lib/src/managers/OrganizationManager.d.ts +107 -0
  81. package/lib/src/managers/PageManager.d.ts +1645 -0
  82. package/lib/src/managers/PersonManager.d.ts +48 -0
  83. package/lib/src/managers/PluginManager.d.ts +179 -0
  84. package/lib/src/managers/PluginManager.js +393 -0
  85. package/lib/src/managers/PolicyEvaluator.d.ts +213 -0
  86. package/lib/src/managers/RenderingManager.d.ts +358 -0
  87. package/lib/src/managers/RoleManager.d.ts +134 -0
  88. package/lib/src/managers/SearchManager.d.ts +402 -0
  89. package/lib/src/managers/ShareManager.d.ts +251 -0
  90. package/lib/src/managers/TemplateManager.d.ts +176 -0
  91. package/lib/src/managers/ThemeManager.d.ts +56 -0
  92. package/lib/src/managers/UserManager.d.ts +480 -0
  93. package/lib/src/managers/ValidationManager.d.ts +426 -0
  94. package/lib/src/managers/VariableManager.d.ts +145 -0
  95. package/lib/src/middleware/csrf.d.ts +3 -0
  96. package/lib/src/parsers/LinkParser.d.ts +457 -0
  97. package/lib/src/parsers/MarkupParser.d.ts +769 -0
  98. package/lib/src/parsers/context/ParseContext.d.ts +314 -0
  99. package/lib/src/parsers/data/emoji-map.d.ts +18 -0
  100. package/lib/src/parsers/dom/DOMBuilder.d.ts +210 -0
  101. package/lib/src/parsers/dom/DOMParser.d.ts +252 -0
  102. package/lib/src/parsers/dom/Tokenizer.d.ts +358 -0
  103. package/lib/src/parsers/dom/WikiDocument.d.ts +272 -0
  104. package/lib/src/parsers/dom/handlers/DOMLinkHandler.d.ts +260 -0
  105. package/lib/src/parsers/dom/handlers/DOMPluginHandler.d.ts +212 -0
  106. package/lib/src/parsers/dom/handlers/DOMVariableHandler.d.ts +171 -0
  107. package/lib/src/parsers/filters/BaseFilter.d.ts +269 -0
  108. package/lib/src/parsers/filters/FilterChain.d.ts +320 -0
  109. package/lib/src/parsers/handlers/BaseSyntaxHandler.d.ts +331 -0
  110. package/lib/src/parsers/handlers/HandlerRegistry.d.ts +225 -0
  111. package/lib/src/parsers/handlers/JSPWikiPreprocessor.d.ts +89 -0
  112. package/lib/src/parsers/handlers/LinkParserHandler.d.ts +80 -0
  113. package/lib/src/parsers/handlers/PluginSyntaxHandler.d.ts +78 -0
  114. package/lib/src/parsers/handlers/WikiFormHandler.d.ts +149 -0
  115. package/lib/src/parsers/handlers/WikiTagHandler.d.ts +125 -0
  116. package/lib/src/parsers/jspwikiTableRow.d.ts +39 -0
  117. package/lib/src/parsers/styleBlockSyntax.d.ts +14 -0
  118. package/lib/src/plugins/types.d.ts +79 -0
  119. package/lib/src/providers/AgentTokenAuthProvider.d.ts +40 -0
  120. package/lib/src/providers/AuthentikBearerAuthProvider.d.ts +75 -0
  121. package/lib/src/providers/BaseAttachmentProvider.d.ts +197 -0
  122. package/lib/src/providers/BaseAuthProvider.d.ts +220 -0
  123. package/lib/src/providers/BaseCacheProvider.d.ts +140 -0
  124. package/lib/src/providers/BaseCredentialsProvider.d.ts +84 -0
  125. package/lib/src/providers/BaseDatabaseProvider.d.ts +22 -0
  126. package/lib/src/providers/BaseDatabaseProvider.js +12 -0
  127. package/lib/src/providers/BaseLoggingProvider.d.ts +68 -0
  128. package/lib/src/providers/BaseLoggingProvider.js +37 -0
  129. package/lib/src/providers/BaseMediaProvider.d.ts +308 -0
  130. package/lib/src/providers/BasePageProvider.d.ts +337 -0
  131. package/lib/src/providers/BaseProvider.d.ts +98 -0
  132. package/lib/src/providers/BaseProvider.js +74 -0
  133. package/lib/src/providers/BaseSearchProvider.d.ts +362 -0
  134. package/lib/src/providers/BasicAttachmentProvider.d.ts +599 -0
  135. package/lib/src/providers/CloudflareAccessAuthProvider.d.ts +72 -0
  136. package/lib/src/providers/FileCredentialsProvider.d.ts +33 -0
  137. package/lib/src/providers/FileJobStateProvider.d.ts +58 -0
  138. package/lib/src/providers/FileLoggingProvider.d.ts +43 -0
  139. package/lib/src/providers/FileLoggingProvider.js +120 -0
  140. package/lib/src/providers/FileOidcAdapter.d.ts +25 -0
  141. package/lib/src/providers/FileSystemMediaProvider.d.ts +403 -0
  142. package/lib/src/providers/LunrSearchProvider.d.ts +280 -0
  143. package/lib/src/providers/MagicLinkAuthProvider.d.ts +125 -0
  144. package/lib/src/providers/NullCacheProvider.d.ts +72 -0
  145. package/lib/src/providers/OidcAuthProvider.d.ts +180 -0
  146. package/lib/src/providers/OidcBearerAuthProvider.d.ts +30 -0
  147. package/lib/src/providers/PasskeyAuthProvider.d.ts +58 -0
  148. package/lib/src/providers/PasswordAuthProvider.d.ts +20 -0
  149. package/lib/src/providers/SqliteDatabaseProvider.d.ts +55 -0
  150. package/lib/src/providers/SqliteDatabaseProvider.js +90 -0
  151. package/lib/src/providers/SqliteJobStateProvider.d.ts +41 -0
  152. package/lib/src/providers/databaseMigrations.d.ts +8 -0
  153. package/lib/src/providers/databaseMigrations.js +47 -0
  154. package/lib/src/providers/jobState.d.ts +99 -0
  155. package/lib/src/providers/lunrQueryToMiniSearch.d.ts +32 -0
  156. package/lib/src/providers/mediaFilenameDate.d.ts +22 -0
  157. package/lib/src/providers/sqliteBackup.d.ts +128 -0
  158. package/lib/src/providers/sqliteBackup.js +365 -0
  159. package/lib/src/providers/sqliteLocation.d.ts +17 -0
  160. package/lib/src/providers/sqliteLocation.js +55 -0
  161. package/lib/src/providers/sqliteMigrations.d.ts +48 -0
  162. package/lib/src/providers/sqliteMigrations.js +72 -0
  163. package/lib/src/rendering/githubAlerts.d.ts +22 -0
  164. package/lib/src/rendering/htmlPolicy.d.ts +76 -0
  165. package/lib/src/rendering/markdownConverter.d.ts +73 -0
  166. package/lib/src/security/PolicyDecisionPoint.d.ts +245 -0
  167. package/lib/src/security/PolicyInformationPoint.d.ts +448 -0
  168. package/lib/src/security/pageActions.d.ts +40 -0
  169. package/lib/src/security/permissions.generated.d.ts +78 -0
  170. package/lib/src/types/Asset.d.ts +414 -0
  171. package/lib/src/types/Catalog.d.ts +61 -0
  172. package/lib/src/types/Comment.d.ts +11 -0
  173. package/lib/src/types/Config.d.ts +388 -0
  174. package/lib/src/types/Organization.d.ts +78 -0
  175. package/lib/src/types/OrganizationProvider.d.ts +38 -0
  176. package/lib/src/types/Page.d.ts +220 -0
  177. package/lib/src/types/Person.d.ts +56 -0
  178. package/lib/src/types/PersonProvider.d.ts +25 -0
  179. package/lib/src/types/Policy.d.ts +139 -0
  180. package/lib/src/types/Policy.js +17 -0
  181. package/lib/src/types/Provider.d.ts +810 -0
  182. package/lib/src/types/Role.d.ts +84 -0
  183. package/lib/src/types/RoleProvider.d.ts +35 -0
  184. package/lib/src/types/Schema.d.ts +425 -0
  185. package/lib/src/types/Share.d.ts +172 -0
  186. package/lib/src/types/Share.js +81 -0
  187. package/lib/src/types/User.d.ts +304 -0
  188. package/lib/src/types/Version.d.ts +189 -0
  189. package/lib/src/types/WikiEngine.d.ts +128 -0
  190. package/lib/src/types/express.d.ts +120 -0
  191. package/lib/src/types/express.js +1 -0
  192. package/lib/src/types/guards.d.ts +78 -0
  193. package/lib/src/types/heic-decode.d.ts +16 -0
  194. package/lib/src/types/index.d.ts +19 -0
  195. package/lib/src/types/linkedom.d.ts +77 -0
  196. package/lib/src/types/markdown-it-plugins.d.ts +21 -0
  197. package/lib/src/utils/DeltaStorage.d.ts +118 -0
  198. package/lib/src/utils/LocaleUtils.d.ts +105 -0
  199. package/lib/src/utils/LocaleUtils.js +294 -0
  200. package/lib/src/utils/PageNameMatcher.d.ts +98 -0
  201. package/lib/src/utils/PathPreflight.d.ts +38 -0
  202. package/lib/src/utils/PathPreflight.js +50 -0
  203. package/lib/src/utils/SectionUtils.d.ts +40 -0
  204. package/lib/src/utils/addonConfigLayer.d.ts +144 -0
  205. package/lib/src/utils/addonConfigLayer.js +382 -0
  206. package/lib/src/utils/addonDatabaseRules.d.ts +25 -0
  207. package/lib/src/utils/addonDatabaseRules.js +57 -0
  208. package/lib/src/utils/addonHost.d.ts +51 -0
  209. package/lib/src/utils/addonHost.js +72 -0
  210. package/lib/src/utils/addonInstaller.d.ts +109 -0
  211. package/lib/src/utils/addonLock.d.ts +37 -0
  212. package/lib/src/utils/addonLock.js +48 -0
  213. package/lib/src/utils/addonPageSync.d.ts +45 -0
  214. package/lib/src/utils/addonProvenance.d.ts +48 -0
  215. package/lib/src/utils/addonTarball.d.ts +41 -0
  216. package/lib/src/utils/addonsPathResolver.d.ts +76 -0
  217. package/lib/src/utils/addonsPathResolver.js +129 -0
  218. package/lib/src/utils/assetMetadataTags.d.ts +66 -0
  219. package/lib/src/utils/atomicWrite.d.ts +28 -0
  220. package/lib/src/utils/auditConfigChange.d.ts +57 -0
  221. package/lib/src/utils/auditEventNames.d.ts +50 -0
  222. package/lib/src/utils/auditEventNames.generated.d.ts +169 -0
  223. package/lib/src/utils/auditEventNames.generated.js +174 -0
  224. package/lib/src/utils/auditEventNames.js +49 -0
  225. package/lib/src/utils/auditEvents.d.ts +265 -0
  226. package/lib/src/utils/auditEvents.js +309 -0
  227. package/lib/src/utils/auditRegistry.d.ts +102 -0
  228. package/lib/src/utils/auditRegistry.js +168 -0
  229. package/lib/src/utils/bip39English.d.ts +2 -0
  230. package/lib/src/utils/configEnvKeys.d.ts +68 -0
  231. package/lib/src/utils/configFiles.d.ts +74 -0
  232. package/lib/src/utils/configFiles.js +150 -0
  233. package/lib/src/utils/delegation.d.ts +48 -0
  234. package/lib/src/utils/deriveUsername.d.ts +21 -0
  235. package/lib/src/utils/explainMediaPath.d.ts +123 -0
  236. package/lib/src/utils/formerTitles.d.ts +69 -0
  237. package/lib/src/utils/frontmatterAccess.d.ts +54 -0
  238. package/lib/src/utils/headlessAdminPassword.d.ts +55 -0
  239. package/lib/src/utils/imageTransform.d.ts +52 -0
  240. package/lib/src/utils/instanceEnvSecret.d.ts +51 -0
  241. package/lib/src/utils/jobSlots.d.ts +43 -0
  242. package/lib/src/utils/keywordNormalizer.d.ts +127 -0
  243. package/lib/src/utils/logger.d.ts +70 -0
  244. package/lib/src/utils/logger.js +106 -0
  245. package/lib/src/utils/magicLinkDeviceState.d.ts +95 -0
  246. package/lib/src/utils/managerUtils.d.ts +40 -0
  247. package/lib/src/utils/migrateLegacyPrivateAttachments.d.ts +15 -0
  248. package/lib/src/utils/orgFilename.d.ts +22 -0
  249. package/lib/src/utils/pageFeature.d.ts +27 -0
  250. package/lib/src/utils/pageFrontmatter.d.ts +21 -0
  251. package/lib/src/utils/pageNotes.d.ts +150 -0
  252. package/lib/src/utils/pageTitleRule.d.ts +37 -0
  253. package/lib/src/utils/pageToArticle.d.ts +59 -0
  254. package/lib/src/utils/pageUrl.d.ts +18 -0
  255. package/lib/src/utils/passwordChange.d.ts +12 -0
  256. package/lib/src/utils/passwordHash.d.ts +60 -0
  257. package/lib/src/utils/permissionCatalog.d.ts +52 -0
  258. package/lib/src/utils/pluginFormatters.d.ts +452 -0
  259. package/lib/src/utils/pluginFormatters.js +714 -0
  260. package/lib/src/utils/privateLinkRewrite.d.ts +42 -0
  261. package/lib/src/utils/privateStoreAccess.d.ts +57 -0
  262. package/lib/src/utils/privateStoreAccess.js +65 -0
  263. package/lib/src/utils/privateStoreCatalogs.d.ts +40 -0
  264. package/lib/src/utils/privateStoreCrypto.d.ts +127 -0
  265. package/lib/src/utils/privateStoreDoor.d.ts +181 -0
  266. package/lib/src/utils/privateStoreExport.d.ts +72 -0
  267. package/lib/src/utils/privateStoreFiles.d.ts +120 -0
  268. package/lib/src/utils/privateStoreMeta.d.ts +64 -0
  269. package/lib/src/utils/privateStoreMigrations.d.ts +37 -0
  270. package/lib/src/utils/privateStorePath.d.ts +259 -0
  271. package/lib/src/utils/privateStorePath.js +436 -0
  272. package/lib/src/utils/privateStoreTakeout.d.ts +76 -0
  273. package/lib/src/utils/privateStoreUnlock.d.ts +175 -0
  274. package/lib/src/utils/proxyClaim.d.ts +59 -0
  275. package/lib/src/utils/proxyClaim.js +30 -0
  276. package/lib/src/utils/redactPrivateNames.d.ts +21 -0
  277. package/lib/src/utils/redactPrivateNames.js +89 -0
  278. package/lib/src/utils/redactSecrets.d.ts +58 -0
  279. package/lib/src/utils/redactSecrets.js +171 -0
  280. package/lib/src/utils/renameLinkRewrite.d.ts +112 -0
  281. package/lib/src/utils/roleGrants.d.ts +35 -0
  282. package/lib/src/utils/safeRedirect.d.ts +32 -0
  283. package/lib/src/utils/schedule.d.ts +54 -0
  284. package/lib/src/utils/secretFileMode.d.ts +17 -0
  285. package/lib/src/utils/seededShippedPages.d.ts +45 -0
  286. package/lib/src/utils/sensitiveValues.d.ts +59 -0
  287. package/lib/src/utils/sensitiveValues.js +106 -0
  288. package/lib/src/utils/sessionEnd.d.ts +12 -0
  289. package/lib/src/utils/sessionGeneration.d.ts +24 -0
  290. package/lib/src/utils/sessionIdle.d.ts +41 -0
  291. package/lib/src/utils/sessionLimits.d.ts +68 -0
  292. package/lib/src/utils/shareLockbox.d.ts +64 -0
  293. package/lib/src/utils/sigstoreTrustedRoot.d.ts +33 -0
  294. package/lib/src/utils/sniffMime.d.ts +21 -0
  295. package/lib/src/utils/storeSearchIndex.d.ts +75 -0
  296. package/lib/src/utils/subjectMayDo.d.ts +21 -0
  297. package/lib/src/utils/systemCategoryRules.d.ts +30 -0
  298. package/lib/src/utils/systemCategoryRules.js +41 -0
  299. package/lib/src/utils/userCreateError.d.ts +62 -0
  300. package/lib/src/utils/username.d.ts +53 -0
  301. package/lib/src/utils/vaultUrlRewrite.d.ts +27 -0
  302. package/lib/src/utils/version.d.ts +84 -0
  303. package/lib/src/utils/videoFrame.d.ts +21 -0
  304. package/package.json +54 -3
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Encrypted, consistent copies of SQLite databases, and the staged restore
3
+ * that puts them back (#1723). Ported from yourPHR (`sqlite-backup.ts`,
4
+ * yourphr#461, #602, #608, #787, #866, #914), with these changes:
5
+ *
6
+ * - __One encrypted file per database__, not one file holding every
7
+ * database's tables. A restore needs no table-name split, and two databases
8
+ * may hold tables of the same name.
9
+ * - __Every database's snapshot is taken before any copying starts.__ Each
10
+ * source gets its own connection with its backup file attached; a read
11
+ * transaction is opened on every one of them, back to back, and only then
12
+ * are they copied. SQLite allows ten attached databases per connection, so
13
+ * one connection for all of them would cap a site at five databases; this
14
+ * has no cap. The live files are in WAL mode, so these readers never block
15
+ * the site's writers, and a write committed after the snapshot is in none of
16
+ * the copies.
17
+ * - `sqlite_sequence` (AUTOINCREMENT counters) and `user_version` are copied,
18
+ * so a restored table never hands out an id it handed out before.
19
+ * - Staging writes `<live>.staged` beside each live file, the path applying
20
+ * reads (yourphr#866: they were different directories), and is all or
21
+ * nothing: on any failure every staged file is removed.
22
+ * - Applying moves the live file's `-wal` and `-shm` aside with it
23
+ * (yourphr#866), so a leftover WAL can never replay onto the restored file.
24
+ *
25
+ * Every function here is synchronous SQLite and can take minutes on a large
26
+ * database: the managers call them through `runOffThread`, never on the thread
27
+ * that answers requests (yourphr#787, #914). Keys travel in `workerData`
28
+ * (memory), never argv or the environment.
29
+ */
30
+ import { chmodSync, copyFileSync, existsSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, readFileSync } from 'node:fs';
31
+ import { createRequire } from 'node:module';
32
+ import path from 'node:path';
33
+ import { Worker } from 'node:worker_threads';
34
+ /** The staged file for a live database: beside it, so staging and applying name the same path. */
35
+ export const stagedPath = (live) => `${live}.staged`;
36
+ /** SQLCipher key pragma escaping, as SqliteDatabaseProvider does it. */
37
+ function quoteKey(key) {
38
+ return `'${key.replace(/'/g, "''")}'`;
39
+ }
40
+ /** Open a file the way SqliteDatabaseProvider does: SQLCipher, keyed when a key is given. */
41
+ function openKeyed(Database, file, key) {
42
+ // Checked here, not with `fileMustExist`: SQLite opens an ATTACHed file with
43
+ // the main file's flags, and without CREATE the backup or staged file could
44
+ // not be made.
45
+ if (!existsSync(file))
46
+ throw new Error(`${file} does not exist`);
47
+ const db = new Database(file);
48
+ try {
49
+ db.pragma("cipher='sqlcipher'");
50
+ if (key)
51
+ db.pragma(`key=${quoteKey(key)}`);
52
+ // The first read: a wrong key fails here.
53
+ db.prepare('SELECT COUNT(*) AS n FROM sqlite_master').get();
54
+ return db;
55
+ }
56
+ catch (err) {
57
+ db.close();
58
+ throw err;
59
+ }
60
+ }
61
+ /**
62
+ * Copies every table (with its rows), index, trigger and view from one schema
63
+ * of the connection into another, inside the caller's transaction. This is
64
+ * sqlcipher_export(), spelled out: SQLite3MultipleCiphers does not ship it.
65
+ * Tables are created and filled before any index or trigger exists, so the
66
+ * copy fires no trigger.
67
+ */
68
+ function copyObjects(db, from, to) {
69
+ const all = db
70
+ .prepare(`SELECT type, name, sql FROM ${from}.sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 WHEN 'trigger' THEN 2 ELSE 3 END`)
71
+ .all();
72
+ // A virtual table (FTS5) owns shadow tables, <table>_data, _idx, _content,
73
+ // _docsize, _config: recreating it recreates them, and creating them by hand
74
+ // is refused. Their rows come back through the INSERT into the virtual table.
75
+ const virtual = all.filter((o) => /^CREATE VIRTUAL TABLE/i.test(o.sql)).map((o) => o.name);
76
+ const shadow = (name) => virtual.some((v) => name.startsWith(`${v}_`));
77
+ let tables = 0;
78
+ for (const object of all) {
79
+ if (shadow(object.name))
80
+ continue;
81
+ // CREATE TABLE x -> CREATE TABLE to.x: the one rewrite sqlcipher_export
82
+ // makes. sqlite_master SQL never carries a schema prefix.
83
+ const ddl = object.sql.replace(/^(CREATE (?:TABLE|INDEX|UNIQUE INDEX|TRIGGER|VIEW|VIRTUAL TABLE))\s+(?:IF NOT EXISTS\s+)?("[^"]+"|\[[^\]]+\]|`[^`]+`|\S+)/i, (_m, head, name) => `${head} ${to}.${name}`);
84
+ db.exec(ddl);
85
+ if (object.type !== 'table')
86
+ continue;
87
+ tables++;
88
+ const quoted = `"${object.name.replace(/"/g, '""')}"`;
89
+ if (virtual.includes(object.name)) {
90
+ // A virtual table's rowids are carried explicitly: SELECT * leaves them
91
+ // out, and the copy would be renumbered.
92
+ const cols = db.prepare(`SELECT name FROM ${from}.pragma_table_info(?)`).all(object.name)
93
+ .map((c) => `"${c.name.replace(/"/g, '""')}"`).join(', ');
94
+ db.exec(`INSERT INTO ${to}.${quoted} (rowid, ${cols}) SELECT rowid, ${cols} FROM ${from}.${quoted}`);
95
+ }
96
+ else {
97
+ db.exec(`INSERT INTO ${to}.${quoted} SELECT * FROM ${from}.${quoted}`);
98
+ }
99
+ }
100
+ // AUTOINCREMENT counters: without them a restored table could hand out an
101
+ // id it had already used for a deleted row.
102
+ const hasSequence = db.prepare(`SELECT 1 FROM ${from}.sqlite_master WHERE name = 'sqlite_sequence'`).get();
103
+ if (hasSequence && db.prepare(`SELECT 1 FROM ${to}.sqlite_master WHERE name = 'sqlite_sequence'`).get()) {
104
+ db.exec(`DELETE FROM ${to}.sqlite_sequence`);
105
+ db.exec(`INSERT INTO ${to}.sqlite_sequence (name, seq) SELECT name, seq FROM ${from}.sqlite_sequence`);
106
+ }
107
+ const userVersion = db.pragma(`${from}.user_version`, { simple: true });
108
+ if (userVersion)
109
+ db.pragma(`${to}.user_version = ${Number(userVersion)}`);
110
+ return tables;
111
+ }
112
+ /** SQLite's full integrity check of an open database; throws naming what it found. */
113
+ function requireIntegrity(db, what) {
114
+ const result = String(db.pragma('integrity_check')[0]?.integrity_check ?? 'no result');
115
+ if (result !== 'ok')
116
+ throw new Error(`${what} failed its integrity check: ${result}`);
117
+ }
118
+ /**
119
+ * Write an encrypted copy of each database under `backupKey`. A database that
120
+ * cannot be opened or copied is named in `failed`, its partial file removed;
121
+ * the others are still written. A target that already exists is never
122
+ * overwritten.
123
+ */
124
+ export function exportDatabasesSync(Database, exports, backupKey) {
125
+ if (!backupKey.trim()) {
126
+ throw new Error('a backup key is required: database backups are always encrypted');
127
+ }
128
+ const outcome = { written: [], failed: [] };
129
+ const open = [];
130
+ const fail = (job, err, db) => {
131
+ try {
132
+ db?.close();
133
+ }
134
+ catch { /* already failing */ }
135
+ rmSync(job.target, { force: true });
136
+ outcome.failed.push({ name: job.name, error: err.message });
137
+ };
138
+ // 1. A connection per database, its backup file attached under the backup key.
139
+ for (const job of exports) {
140
+ if (existsSync(job.target)) {
141
+ outcome.failed.push({ name: job.name, error: `${path.basename(job.target)} already exists; a backup never overwrites one` });
142
+ continue;
143
+ }
144
+ let db;
145
+ try {
146
+ db = openKeyed(Database, job.source, job.sourceKey);
147
+ db.prepare(`ATTACH DATABASE ? AS backup KEY ${quoteKey(backupKey)}`).run(job.target);
148
+ open.push({ job, db });
149
+ }
150
+ catch (err) {
151
+ fail(job, err, db);
152
+ }
153
+ }
154
+ // 2. Every snapshot, back to back, before any copying: one point in time.
155
+ const reading = [];
156
+ for (const entry of open) {
157
+ try {
158
+ entry.db.exec('BEGIN');
159
+ entry.db.prepare('SELECT COUNT(*) AS n FROM main.sqlite_master').get();
160
+ reading.push(entry);
161
+ }
162
+ catch (err) {
163
+ fail(entry.job, err, entry.db);
164
+ }
165
+ }
166
+ // 3. Copy each, then close and prove the copy opens under the backup key.
167
+ for (const { job, db } of reading) {
168
+ try {
169
+ const tables = copyObjects(db, 'main', 'backup');
170
+ db.exec('COMMIT');
171
+ db.close();
172
+ const check = openKeyed(Database, job.target, backupKey);
173
+ try {
174
+ requireIntegrity(check, `the backup of ${job.name}`);
175
+ }
176
+ finally {
177
+ check.close();
178
+ }
179
+ chmodSync(job.target, 0o600);
180
+ outcome.written.push({ name: job.name, target: job.target, sizeBytes: statSync(job.target).size, tables });
181
+ }
182
+ catch (err) {
183
+ try {
184
+ if (db.inTransaction)
185
+ db.exec('ROLLBACK');
186
+ }
187
+ catch { /* closing anyway */ }
188
+ fail(job, err, db.open ? db : undefined);
189
+ }
190
+ }
191
+ return outcome;
192
+ }
193
+ /**
194
+ * Stage a restore: each backup file is opened under the backup key,
195
+ * integrity-checked, and copied into `<live>.staged` under the target
196
+ * database's key, which is then opened and checked too. All or nothing: if
197
+ * any database fails, every staged file is removed and the error names it.
198
+ * Live files are never touched.
199
+ */
200
+ export function stageDatabasesSync(Database, stages, backupKey) {
201
+ if (!backupKey.trim()) {
202
+ throw new Error('a backup key is required to read a database backup');
203
+ }
204
+ const outcome = { staged: [] };
205
+ try {
206
+ for (const stage of stages) {
207
+ const staged = stagedPath(stage.live);
208
+ rmSync(staged, { force: true });
209
+ let db;
210
+ try {
211
+ db = openKeyed(Database, stage.backupFile, backupKey);
212
+ }
213
+ catch (err) {
214
+ throw new Error(`the backup of ${stage.name} cannot be read — wrong backup key, or not a backup: ${err.message}`, { cause: err });
215
+ }
216
+ let tables;
217
+ try {
218
+ requireIntegrity(db, `the backup of ${stage.name}`);
219
+ db.prepare(`ATTACH DATABASE ? AS staged KEY ${quoteKey(stage.targetKey)}`).run(staged);
220
+ db.exec('BEGIN');
221
+ try {
222
+ tables = copyObjects(db, 'main', 'staged');
223
+ db.exec('COMMIT');
224
+ }
225
+ catch (err) {
226
+ db.exec('ROLLBACK');
227
+ throw err;
228
+ }
229
+ }
230
+ finally {
231
+ db.close();
232
+ }
233
+ const check = openKeyed(Database, staged, stage.targetKey);
234
+ try {
235
+ requireIntegrity(check, `the staged copy of ${stage.name}`);
236
+ }
237
+ finally {
238
+ check.close();
239
+ }
240
+ chmodSync(staged, 0o600);
241
+ outcome.staged.push({ name: stage.name, staged, live: stage.live, tables });
242
+ }
243
+ return outcome;
244
+ }
245
+ catch (err) {
246
+ for (const stage of stages)
247
+ rmSync(stagedPath(stage.live), { force: true });
248
+ throw err;
249
+ }
250
+ }
251
+ export function runOffThread(job, timeoutMs) {
252
+ // Compiled, the worker is the .js beside this file. From source (vitest,
253
+ // tsx) it is the .ts, which needs tsx's loader.
254
+ const fromSource = import.meta.url.endsWith('.ts');
255
+ const entry = new URL(fromSource ? './sqliteBackupWorker.ts' : './sqliteBackupWorker.js', import.meta.url);
256
+ return new Promise((resolve, reject) => {
257
+ const driver = createRequire(import.meta.url).resolve('better-sqlite3-multiple-ciphers');
258
+ const workerData = { ...job, driver };
259
+ const worker = new Worker(entry, { workerData, ...(fromSource ? { execArgv: ['--import', 'tsx'] } : {}) });
260
+ let settled = false;
261
+ const settle = (fn) => {
262
+ if (settled)
263
+ return;
264
+ settled = true;
265
+ clearTimeout(timer);
266
+ fn();
267
+ };
268
+ const timer = setTimeout(() => {
269
+ settle(() => reject(new Error(`the database ${job.kind === 'export' ? 'backup' : 'restore'} did not finish within ${timeoutMs} ms (ngdpbase.database.backup.worker-timeout-ms) and was stopped`)));
270
+ void worker.terminate();
271
+ }, timeoutMs);
272
+ worker.once('message', (m) => {
273
+ settle(() => (m.ok ? resolve(m.result) : reject(new Error(m.error))));
274
+ });
275
+ worker.once('error', (err) => settle(() => reject(err)));
276
+ worker.once('exit', (code) => settle(() => reject(new Error(`the database backup worker exited (code ${code}) without a result`))));
277
+ });
278
+ }
279
+ /** Where the manifest of a staged restore lives: beside the application database. */
280
+ export const manifestPath = (applicationFile) => `${applicationFile}.restore.json`;
281
+ export function writeManifest(file, manifest) {
282
+ const tmp = `${file}.tmp`;
283
+ writeFileSync(tmp, JSON.stringify(manifest, null, 2) + '\n', { mode: 0o600 });
284
+ renameSync(tmp, file);
285
+ }
286
+ /**
287
+ * Apply a staged restore at start, before any database opens. Each live file,
288
+ * with its `-wal` and `-shm`, steps aside as `*.pre-restore` (`-wal`/`-shm`
289
+ * beside it, so the kept copy still opens as it was), and the staged file
290
+ * takes its name. A rename, never a write into a live database.
291
+ *
292
+ * All or nothing: if any database the manifest names is not staged, nothing
293
+ * is applied; what is staged is renamed `*.incomplete`, so it is neither
294
+ * applied nor retried. A staged file with no manifest (staging was
295
+ * interrupted) is set aside the same way. `allowed` says which live paths may
296
+ * be replaced, so a tampered manifest cannot rename an arbitrary file.
297
+ */
298
+ export function applyStagedRestore(applicationFile, allowed) {
299
+ const manifestFile = manifestPath(applicationFile);
300
+ const dir = path.dirname(applicationFile);
301
+ const stray = () => (existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.db.staged')).map((f) => path.join(dir, f)) : []);
302
+ if (!existsSync(manifestFile)) {
303
+ const leftovers = stray();
304
+ if (leftovers.length === 0)
305
+ return null;
306
+ for (const f of leftovers)
307
+ move(f, `${f}.incomplete`);
308
+ return { applied: [], refused: `staged database files with no restore manifest (an interrupted staging): ${leftovers.map((f) => path.basename(f)).join(', ')} — set aside as *.incomplete` };
309
+ }
310
+ const setAside = (reason) => {
311
+ for (const f of stray())
312
+ move(f, `${f}.incomplete`);
313
+ move(manifestFile, `${manifestFile}.incomplete`);
314
+ return { applied: [], refused: reason };
315
+ };
316
+ let manifest;
317
+ try {
318
+ manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
319
+ }
320
+ catch (err) {
321
+ return setAside(`the restore manifest cannot be read: ${err.message}`);
322
+ }
323
+ const entries = Array.isArray(manifest?.databases) ? manifest.databases : [];
324
+ const bad = entries.find((e) => !allowed(e.live) || e.staged !== stagedPath(e.live));
325
+ if (entries.length === 0 || bad) {
326
+ return setAside(bad ? `the restore manifest names ${bad.live}, which is not a database of this site` : 'the restore manifest names no database');
327
+ }
328
+ const missing = entries.filter((e) => !existsSync(e.staged));
329
+ if (missing.length > 0) {
330
+ return setAside(`only ${entries.length - missing.length} of ${entries.length} databases were staged (missing: ${missing.map((e) => e.name).join(', ')}), and they belong together`);
331
+ }
332
+ const applied = [];
333
+ for (const entry of entries) {
334
+ const kept = `${entry.live}.pre-restore`;
335
+ // A previous restore's kept WAL must not pair with this restore's kept file.
336
+ for (const sibling of [`${kept}-wal`, `${kept}-shm`])
337
+ rmSync(sibling, { force: true });
338
+ if (existsSync(entry.live)) {
339
+ move(entry.live, kept);
340
+ for (const suffix of ['-wal', '-shm']) {
341
+ if (existsSync(`${entry.live}${suffix}`))
342
+ move(`${entry.live}${suffix}`, `${kept}${suffix}`);
343
+ }
344
+ }
345
+ // Never a WAL beside the restored file that is not its own.
346
+ for (const suffix of ['-wal', '-shm'])
347
+ rmSync(`${entry.live}${suffix}`, { force: true });
348
+ move(entry.staged, entry.live);
349
+ applied.push({ name: entry.name, live: entry.live, kept });
350
+ }
351
+ unlinkSync(manifestFile);
352
+ return { applied };
353
+ }
354
+ /** A rename; a copy and unlink when the two paths are on different devices. */
355
+ function move(from, to) {
356
+ try {
357
+ renameSync(from, to);
358
+ }
359
+ catch (err) {
360
+ if (err.code !== 'EXDEV')
361
+ throw err;
362
+ copyFileSync(from, to);
363
+ unlinkSync(from);
364
+ }
365
+ }
@@ -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 `&lt;`), 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[];