vowel 0.6.3 → 0.7.0-beta.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 (165) hide show
  1. package/README.md +23 -17
  2. package/bin.js +87 -30
  3. package/cli.js +15 -0
  4. package/config.js +102 -16
  5. package/extractDate.js +70 -6
  6. package/index.js +14 -5
  7. package/package.json +42 -7
  8. package/plugins/assets/index.js +65 -0
  9. package/plugins/atproto/index.js +132 -0
  10. package/plugins/atproto/password.js +71 -0
  11. package/plugins/atproto/publish.js +406 -0
  12. package/plugins/atproto/record.js +144 -0
  13. package/plugins/atproto/rkey.js +54 -0
  14. package/plugins/deploy-cloudflare/index.js +92 -0
  15. package/plugins/fonts/index.js +18 -21
  16. package/plugins/html/assets/bundle-BeK1Hptv.css +21 -0
  17. package/plugins/html/bundle/assets/index-BeK1Hptv.css +21 -0
  18. package/plugins/html/bundle/index.js +21145 -0
  19. package/plugins/html/editor/EditButton.svelte +152 -0
  20. package/plugins/html/editor/Editor.svelte +51 -0
  21. package/plugins/html/editor/Overlays.svelte +6 -0
  22. package/plugins/html/editor/SettingsPanel.svelte +397 -0
  23. package/plugins/html/editor/Toolbar.svelte +644 -0
  24. package/plugins/html/editor/create-session.js +250 -0
  25. package/plugins/html/editor/directives.js +128 -0
  26. package/plugins/html/editor/frontmatter.js +97 -0
  27. package/plugins/html/editor/ingest.js +339 -0
  28. package/plugins/html/editor/main.js +94 -0
  29. package/plugins/html/editor/nodes/Alert.svelte +21 -0
  30. package/plugins/html/editor/nodes/Blockquote.svelte +9 -0
  31. package/plugins/html/editor/nodes/CodeBlock.svelte +19 -0
  32. package/plugins/html/editor/nodes/Embed.svelte +19 -0
  33. package/plugins/html/editor/nodes/Emphasis.svelte +11 -0
  34. package/plugins/html/editor/nodes/Heading.svelte +15 -0
  35. package/plugins/html/editor/nodes/Highlight.svelte +11 -0
  36. package/plugins/html/editor/nodes/Image.svelte +18 -0
  37. package/plugins/html/editor/nodes/InlineCode.svelte +11 -0
  38. package/plugins/html/editor/nodes/Link.svelte +17 -0
  39. package/plugins/html/editor/nodes/List.svelte +15 -0
  40. package/plugins/html/editor/nodes/ListItem.svelte +9 -0
  41. package/plugins/html/editor/nodes/Page.svelte +21 -0
  42. package/plugins/html/editor/nodes/Paragraph.svelte +9 -0
  43. package/plugins/html/editor/nodes/Strikethrough.svelte +11 -0
  44. package/plugins/html/editor/nodes/Strong.svelte +11 -0
  45. package/plugins/html/editor/nodes/ThematicBreak.svelte +9 -0
  46. package/plugins/html/editor/nodes/index.js +18 -0
  47. package/plugins/html/editor/rollup.config.js +35 -0
  48. package/plugins/html/editor/serialize.js +157 -0
  49. package/plugins/html/editor/settings-file.js +83 -0
  50. package/plugins/html/editor/socket.js +202 -0
  51. package/plugins/html/editor/source.js +43 -0
  52. package/plugins/html/image.js +104 -0
  53. package/plugins/html/index.js +760 -0
  54. package/plugins/html/partials.js +299 -0
  55. package/plugins/html/socialLinks.js +67 -0
  56. package/plugins/html/writeRules.js +377 -0
  57. package/plugins/icons/index.js +2 -6
  58. package/plugins/images/index.js +78 -55
  59. package/plugins/markdown/brandColors.js +10 -0
  60. package/plugins/markdown/colorScheme.js +64 -0
  61. package/plugins/markdown/emoji.js +30 -0
  62. package/plugins/markdown/frontmatter.js +253 -0
  63. package/plugins/markdown/index.js +287 -1274
  64. package/plugins/markdown/links.js +97 -0
  65. package/plugins/markdown/metadata.js +308 -0
  66. package/plugins/markdown/readRules.js +143 -0
  67. package/plugins/markdown/typography.js +109 -0
  68. package/plugins/markdown/visitor.js +143 -0
  69. package/plugins/publish/index.js +34 -0
  70. package/plugins/robots/agents.js +79 -0
  71. package/plugins/robots/index.js +30 -10
  72. package/plugins/robots/robots.js +48 -0
  73. package/plugins/styles/index.js +111 -26
  74. package/plugins/styles/theme.js +116 -0
  75. package/plugins/urls/index.js +78 -0
  76. package/plugins/urls/urlMetadata.js +246 -0
  77. package/plugins/vectors/index.js +5 -9
  78. package/plugins/xml/entryContent.js +103 -0
  79. package/plugins/xml/index.js +123 -50
  80. package/secretPaths.js +170 -0
  81. package/stylesheets/DefaultStyles.css +367 -437
  82. package/stylesheets/ResetStyles.css +2 -38
  83. package/stylesheets/SyntaxHighlightingStyles.css +269 -0
  84. package/stylesheets/TypographyStyles.css +386 -128
  85. package/systemPaths.js +48 -0
  86. package/utils.js +56 -5
  87. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -73
  88. package/.github/ISSUE_TEMPLATE/config.yml +0 -7
  89. package/docs-source/.cache.json +0 -9
  90. package/docs-source/.obsidian/app.json +0 -3
  91. package/docs-source/.obsidian/appearance.json +0 -3
  92. package/docs-source/.obsidian/core-plugins-migration.json +0 -30
  93. package/docs-source/.obsidian/core-plugins.json +0 -20
  94. package/docs-source/.obsidian/workspace.json +0 -168
  95. package/docs-source/.stackblitzrc +0 -3
  96. package/docs-source/.votive.db +0 -0
  97. package/docs-source/about.md +0 -3
  98. package/docs-source/blog/home.md +0 -5
  99. package/docs-source/blog/url-ui.md +0 -21
  100. package/docs-source/blog.md +0 -5
  101. package/docs-source/docs/.votive.db +0 -0
  102. package/docs-source/docs/deploy.md +0 -67
  103. package/docs-source/docs/file-structure.md +0 -31
  104. package/docs-source/docs/folder-settings.md +0 -23
  105. package/docs-source/docs/home.md +0 -55
  106. package/docs-source/docs/images.md +0 -10
  107. package/docs-source/docs/items.md +0 -13
  108. package/docs-source/docs/pages.md +0 -141
  109. package/docs-source/docs/settings.md +0 -4
  110. package/docs-source/docs/styling.md +0 -34
  111. package/docs-source/docs/taxonomies.md +0 -37
  112. package/docs-source/features/cards.md +0 -7
  113. package/docs-source/features/editing.md +0 -7
  114. package/docs-source/features/emoji.md +0 -7
  115. package/docs-source/features/frontmatter.md +0 -7
  116. package/docs-source/features/lists.md +0 -7
  117. package/docs-source/features/navigation.md +0 -7
  118. package/docs-source/features/rich-previews.md +0 -7
  119. package/docs-source/features/robots.md +0 -7
  120. package/docs-source/features/rss.md +0 -7
  121. package/docs-source/features/sitemap.md +0 -7
  122. package/docs-source/features/speed.md +0 -7
  123. package/docs-source/features/static.md +0 -7
  124. package/docs-source/features/taxonomies.md +0 -7
  125. package/docs-source/home.md +0 -42
  126. package/docs-source/output/about.html +0 -1
  127. package/docs-source/output/blog/url-ui.html +0 -8
  128. package/docs-source/output/blog.html +0 -1
  129. package/docs-source/output/default.css +0 -1
  130. package/docs-source/output/docs/deploy.html +0 -34
  131. package/docs-source/output/docs/file-structure.html +0 -24
  132. package/docs-source/output/docs/folder-settings.html +0 -20
  133. package/docs-source/output/docs/images.html +0 -2
  134. package/docs-source/output/docs/items.html +0 -6
  135. package/docs-source/output/docs/pages.html +0 -101
  136. package/docs-source/output/docs/styling.html +0 -18
  137. package/docs-source/output/docs/taxonomies.html +0 -20
  138. package/docs-source/output/docs.html +0 -28
  139. package/docs-source/output/features/cards.html +0 -1
  140. package/docs-source/output/features/editing.html +0 -1
  141. package/docs-source/output/features/emoji.html +0 -1
  142. package/docs-source/output/features/frontmatter.html +0 -1
  143. package/docs-source/output/features/lists.html +0 -1
  144. package/docs-source/output/features/navigation.html +0 -1
  145. package/docs-source/output/features/rich-previews.html +0 -1
  146. package/docs-source/output/features/robots.html +0 -1
  147. package/docs-source/output/features/rss.html +0 -1
  148. package/docs-source/output/features/sitemap.html +0 -1
  149. package/docs-source/output/features/speed.html +0 -1
  150. package/docs-source/output/features/static.html +0 -1
  151. package/docs-source/output/features/taxonomies.html +0 -1
  152. package/docs-source/output/features.html +0 -1
  153. package/docs-source/output/feed.xml +0 -1
  154. package/docs-source/output/index.html +0 -21
  155. package/docs-source/output/reset.css +0 -1
  156. package/docs-source/output/roadmap.html +0 -87
  157. package/docs-source/output/robots.txt +0 -14
  158. package/docs-source/output/sitemap.xml +0 -16
  159. package/docs-source/output/typography.css +0 -1
  160. package/docs-source/roadmap.md +0 -98
  161. package/docs-source/settings.md +0 -13
  162. package/getMetadata.js +0 -41
  163. package/jsconfig.json +0 -10
  164. package/regex.js +0 -36
  165. package/stylesheets/brand-colors.css +0 -119
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "vowel",
3
3
  "type": "module",
4
- "version": "0.6.3",
4
+ "version": "0.7.0-beta.0",
5
+ "engines": {
6
+ "node": ">=26"
7
+ },
5
8
  "bin": "bin.js",
6
9
  "main": "index.js",
7
10
  "scripts": {
8
- "test": "node --test tests/*.js"
11
+ "test": "node --test tests/*.js",
12
+ "build:editor": "cd plugins/html/editor && rollup -c"
9
13
  },
10
14
  "keywords": [
11
15
  "markdown",
@@ -15,21 +19,35 @@
15
19
  "author": "Sam Littlefair",
16
20
  "license": "MIT",
17
21
  "description": "Markdown websites",
22
+ "devDependencies": {
23
+ "@rollup/plugin-node-resolve": "^16.0.3",
24
+ "linkedom": "^0.18.13",
25
+ "rollup": "^4.0.0",
26
+ "rollup-plugin-cleanup": "^3.2.1",
27
+ "rollup-plugin-svelte": "^7.2.3",
28
+ "svelte": "^5.56.9"
29
+ },
18
30
  "dependencies": {
19
31
  "@bomb.sh/args": "^0.3.1",
20
32
  "@bomb.sh/tab": "^0.0.15",
21
33
  "@clack/prompts": "^1.4.0",
22
34
  "@jsdevtools/rehype-toc": "^3.0.2",
23
35
  "autoprefixer": "^10.4.27",
36
+ "colorhorse": "^0.3.1",
24
37
  "cssnano": "^7.1.4",
38
+ "emojilib": "^4.0.3",
25
39
  "encoding-sniffer": "^1.0.2",
26
40
  "fast-xml-parser": "^5.3.6",
27
41
  "faye-websocket": "^0.11.4",
42
+ "github-slugger": "^2.0.0",
28
43
  "hast-util-from-html": "^2.0.3",
29
44
  "hast-util-to-string": "^3.0.1",
45
+ "hast-util-to-text": "^4.0.2",
30
46
  "hastscript": "^9.0.1",
47
+ "highlight.js": "^11.12.0",
31
48
  "js-yaml": "^4.1.0",
32
- "lightningcss": "^1.32.0",
49
+ "lightningcss": "^1.33.0",
50
+ "lowlight": "^3.3.0",
33
51
  "mdast": "^2.3.2",
34
52
  "mdast-normalize-headings": "^4.0.0",
35
53
  "mdast-util-from-markdown": "^2.0.2",
@@ -37,6 +55,7 @@
37
55
  "mdast-util-gfm-strikethrough": "^2.0.0",
38
56
  "mdast-util-gfm-table": "^2.0.0",
39
57
  "mdast-util-gfm-task-list-item": "^2.0.0",
58
+ "mdast-util-highlight-mark": "^1.2.2",
40
59
  "mdast-util-to-hast": "^13.2.1",
41
60
  "mdast-util-to-markdown": "^2.1.2",
42
61
  "mdast-util-to-string": "^4.0.0",
@@ -44,6 +63,7 @@
44
63
  "micromark-extension-gfm-footnote": "^2.1.0",
45
64
  "micromark-extension-gfm-table": "^2.1.1",
46
65
  "micromark-extension-gfm-task-list-item": "^2.1.0",
66
+ "micromark-extension-highlight-mark": "^1.2.0",
47
67
  "postcss": "^8.5.9",
48
68
  "rehype": "^13.0.2",
49
69
  "rehype-document": "^7.0.3",
@@ -58,7 +78,9 @@
58
78
  "remark-html": "^16.0.1",
59
79
  "remark-parse": "^11.0.0",
60
80
  "remark-rehype": "^11.1.2",
81
+ "rollup-plugin-import-css": "^4.2.1",
61
82
  "sharp": "^0.35.3",
83
+ "svedit": "^0.14.0",
62
84
  "unified": "^11.0.5",
63
85
  "unist-util-find": "^3.0.0",
64
86
  "unist-util-is": "^6.0.0",
@@ -69,12 +91,25 @@
69
91
  "unist-util-visit-parents": "^6.0.0",
70
92
  "unpic": "^4.2.2",
71
93
  "url-metadata": "^5.4.1",
72
- "voot": "^0.1.0",
73
- "votive": "^0.4.0",
94
+ "votive": "0.5.0-beta.0",
74
95
  "xast-util-sitemap": "^2.0.0",
75
96
  "xast-util-to-xml": "^4.0.0",
76
97
  "xml": "^1.0.1",
77
98
  "yaml": "^2.8.2",
78
99
  "yamljs": "^0.3.0"
79
- }
80
- }
100
+ },
101
+ "files": [
102
+ "bin.js",
103
+ "cli.js",
104
+ "config.js",
105
+ "extractDate.js",
106
+ "index.js",
107
+ "secretPaths.js",
108
+ "systemPaths.js",
109
+ "utils.js",
110
+ "plugins",
111
+ "stylesheets",
112
+ "!plugins/html/bundle.js",
113
+ "!plugins/html/editorClient.js"
114
+ ]
115
+ }
@@ -0,0 +1,65 @@
1
+ import { statSync } from "node:fs"
2
+ import path from "node:path"
3
+
4
+ /** @import * as Votive from "votive" */
5
+
6
+ /**
7
+ * Files the site publishes as they are: video, data and documents an
8
+ * author links to from a page. Copied through byte for byte - like a
9
+ * font - with one rule: **a file over the size limit is not published.**
10
+ * Cloudflare Pages refuses any single file over 25 MiB, and a deploy
11
+ * that fails on one video is worse than a build that says so up front.
12
+ * The read (deferred, like every buffer read) stats the source rather
13
+ * than reading it, logs one error naming the file and its size, and
14
+ * makes the target virtual, so nothing lands on disk and nothing links
15
+ * to it as though it were there. The limit is `config.assetSizeLimit`
16
+ * in bytes for a host that allows more.
17
+ *
18
+ * Per Sam (Sept 19): copy through mp4 and csv, with a reasonable file
19
+ * size limit. The list is what people link to from a page and expect
20
+ * to be there; adding an extension is adding it here.
21
+ */
22
+
23
+ const EXTENSIONS = [".mp4", ".webm", ".csv", ".json", ".pdf"]
24
+
25
+ /** 25 MiB: Cloudflare Pages' per-file limit. */
26
+ const DEFAULT_SIZE_LIMIT = 25 * 1024 * 1024
27
+
28
+ /** @param {number} bytes */
29
+ const megabytes = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)} MB`
30
+
31
+ /**
32
+ * @type {Votive.ProcessorRead}
33
+ */
34
+ function readAsset(source, { config }) {
35
+ const limit = Number(config.assetSizeLimit) || DEFAULT_SIZE_LIMIT
36
+ const { size } = statSync(path.join(config.sourceFolder, source.path))
37
+ if (size <= limit) return { metadata: { size } }
38
+
39
+ config.log?.("error", `${source.path} is ${megabytes(size)}; files over ${megabytes(limit)} are not published (Cloudflare Pages refuses them). Host it elsewhere and link to it.`)
40
+ return { metadata: { size, oversized: true }, write: false }
41
+ }
42
+
43
+ /** @type {Votive.ProcessorWrite} */
44
+ function writeAsset(target) {
45
+ if (target.metadata?.oversized) return undefined
46
+ return { data: target.buffer() }
47
+ }
48
+
49
+ /** @type {Votive.VotiveProcessor} */
50
+ const assetsProcessor = {
51
+ format: "buffer",
52
+ extensions: EXTENSIONS,
53
+ router: ({ name, dir, ext }) => ({ name, dir, ext }),
54
+ readFile: readAsset,
55
+ writeFile: writeAsset
56
+ }
57
+
58
+ /** @type {Votive.VotivePlugin} */
59
+ const vowelAssetsPlugin = {
60
+ name: "vowel-assets",
61
+ processors: [assetsProcessor]
62
+ }
63
+
64
+ export default vowelAssetsPlugin
65
+ export { EXTENSIONS, DEFAULT_SIZE_LIMIT }
@@ -0,0 +1,132 @@
1
+ import path from "node:path"
2
+ import { pageRecord, publicationURI, siteOrigin } from "./record.js"
3
+ import { publishToAtproto } from "./publish.js"
4
+ import { saveAppPassword } from "./password.js"
5
+
6
+ /** @import * as Votive from "votive" */
7
+
8
+ /**
9
+ * AT Protocol, phase one: the site's domain becomes the account's handle.
10
+ *
11
+ * An AT Protocol account is a DID (`did:plc:…`), and a domain becomes its
12
+ * handle when the domain proves it points at that DID - one of the two
13
+ * proofs is serving the DID as plain text at `/.well-known/atproto-did`.
14
+ * That is a file, so it is a stub: declared when settings.md says
15
+ *
16
+ * atproto_did: did:plc:abc123
17
+ *
18
+ * and gone, file and all, when it stops saying so. Deploying the site is
19
+ * then the verification. Nothing here talks to the network; the author
20
+ * pastes the DID from their account settings. Resolving it from a handle
21
+ * is a later phase, through readURL like any other fetch.
22
+ *
23
+ * Phase two: a page under an `atproto_lexicon:` gets an AT-URI and a record,
24
+ * computed by `transformFile` with no network (record.js), and a
25
+ * `<link rel="site.standard.document">` from the html plugin. Publishing
26
+ * is a command, `atproto-publish` (publish.js), which makes the account's
27
+ * records match the site. See tasks/2-in-progress/atproto-integration.md.
28
+ *
29
+ * The publication: with a `domain` too, the site is a
30
+ * `site.standard.publication`, every document's `site` names it, and
31
+ * `/.well-known/site.standard.publication` serves its AT-URI - the
32
+ * domain's claim to it. The record is the publish command's.
33
+ */
34
+
35
+ const WELL_KNOWN = path.join(".well-known", "atproto-did")
36
+ const WELL_KNOWN_PUBLICATION = path.join(".well-known", "site.standard.publication")
37
+
38
+ /** A DID is `did:<method>:<id>`; that is all this checks. */
39
+ function validDID(value) {
40
+ return typeof value === "string" && /^did:[a-z]+:[A-Za-z0-9._:%-]+$/.test(value)
41
+ }
42
+
43
+ /**
44
+ * The account, from the nearest settings.md that names one. The atproto
45
+ * settings are flat, `atproto_did` / `atproto_lexicon` / `atproto_pds`,
46
+ * so each cascades on its own: a folder that sets only a lexicon keeps
47
+ * the root's account. Not reserved, so each arrives as `fm_atproto_*`.
48
+ * @param {{ lastNonNull: (label: string) => unknown }} settings
49
+ */
50
+ function atprotoDID(settings) {
51
+ return settings.lastNonNull("fm_atproto_did")
52
+ }
53
+
54
+ /**
55
+ * The page's AT-URI and record, stored as metadata for the html plugin's
56
+ * link and the publish command. Runs once per read of the page; nothing
57
+ * here reaches the network. A page missing a property its lexicon
58
+ * requires is not a record (mentioned under `verbose`); an unsupported
59
+ * lexicon is warned about.
60
+ */
61
+ function transformFile(target, { settings, config }) {
62
+ if (target.write === false) return undefined
63
+ const did = atprotoDID(settings)
64
+ if (!validDID(did)) return undefined
65
+
66
+ const domain = settings.lastNonNull("fm_domain")
67
+ const result = pageRecord(target, { did, domain, settings })
68
+ if ("skip" in result) return undefined
69
+ if ("incomplete" in result) {
70
+ config.log("info", `${target.path}: ${result.incomplete}`)
71
+ return undefined
72
+ }
73
+ if ("warning" in result) {
74
+ config.log("warn", `${target.path}: ${result.warning}`)
75
+ return undefined
76
+ }
77
+ return { metadata: { atUri: result.atUri, atprotoRecord: result.record } }
78
+ }
79
+
80
+ /** @type {Votive.ProcessorStubs} */
81
+ function createStubs({ settings }) {
82
+ const did = atprotoDID(settings)
83
+ if (!did) return []
84
+ if (!validDID(did)) {
85
+ throw new Error(`settings.md: atproto_did must look like did:plc:… or did:web:…, not ${JSON.stringify(did)}`)
86
+ }
87
+ const origin = siteOrigin(settings.lastNonNull("fm_domain"))
88
+ const publication = origin ? [{ path: WELL_KNOWN_PUBLICATION, params: { text: publicationURI(did, origin) } }] : []
89
+ return [{ path: WELL_KNOWN, params: { text: did } }, ...publication]
90
+ }
91
+
92
+ /** @type {Votive.ProcessorExpand} */
93
+ function expandStubs({ params }) {
94
+ return { text: params.text }
95
+ }
96
+
97
+ /**
98
+ * Claims files with **no extension**, which is what a well-known file
99
+ * is, and `.publication`, which `site.standard.publication` looks like
100
+ * one to. The scan never hands this processor a real file - dot-folders
101
+ * are skipped - so only the stubs arrive; a file elsewhere in the project
102
+ * with either (a LICENSE) is read here and routed nowhere.
103
+ * @type {Votive.VotiveProcessor}
104
+ */
105
+ const wellKnownProcessor = {
106
+ extensions: ["", ".publication"],
107
+ format: "text",
108
+ router: ({ dir, name, ext }) => dir.includes(".well-known") ? { dir, name, ext } : false,
109
+ createStubs,
110
+ expandStubs,
111
+ readFile: (source) => ({ data: source.text, metadata: {} }),
112
+ writeFile: (target) => ({ data: target.data })
113
+ }
114
+
115
+ const recordProcessor = {
116
+ extensions: [".html"],
117
+ format: "text",
118
+ transformFile
119
+ }
120
+
121
+ /** @type {Votive.VotivePlugin} */
122
+ const vowelAtprotoPlugin = {
123
+ name: "vowel-atproto",
124
+ processors: [wellKnownProcessor, recordProcessor],
125
+ commands: {
126
+ "atproto-publish": publishToAtproto,
127
+ "atproto-password": saveAppPassword
128
+ }
129
+ }
130
+
131
+ export default vowelAtprotoPlugin
132
+ export { validDID, atprotoDID }
@@ -0,0 +1,71 @@
1
+ import { mkdir, writeFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import { password as promptPassword, isCancel } from "@clack/prompts"
4
+ import votive from "votive"
5
+ import { passwordFile, resolvePDS, xrpc } from "./publish.js"
6
+
7
+ /**
8
+ * `vowel --command atproto-password`: saves the account's app password
9
+ * where `atproto-publish` looks for it (publish.js's `passwordFile`).
10
+ *
11
+ * Asked for with a masked prompt, so it never lands in shell history. With
12
+ * no terminal to ask in - a Publish button, CI - it comes from the
13
+ * payload, `{"password": "…"}`.
14
+ *
15
+ * With an `atproto_did` in the root settings.md, the password is checked
16
+ * by logging in, and a password for another account, or none, is refused
17
+ * before anything is written. Without one it is saved unchecked, and
18
+ * said so. The file is readable by its owner only.
19
+ *
20
+ * @param {{ password?: string } | undefined} payload
21
+ * @param {{ config: import("votive").VotiveConfig, notify: (message: object) => void }} context
22
+ */
23
+ async function saveAppPassword(payload, { config, notify }) {
24
+ const given = typeof payload?.password === "string" ? payload.password.trim() : ""
25
+ const password = given || await askForPassword()
26
+
27
+ const { did, pds: configuredPDS } = await account(config)
28
+ const verified = typeof did === "string" && did !== ""
29
+ if (verified) {
30
+ const pds = typeof configuredPDS === "string" && configuredPDS ? configuredPDS : await resolvePDS(did)
31
+ notify({ status: "progress", message: `Checking the password with ${pds}...` })
32
+ const session = await xrpc(pds, "com.atproto.server.createSession", { body: { identifier: did, password } })
33
+ if (session.did !== did) throw new Error(`atproto: that app password is for ${session.did}, not ${did}; nothing saved`)
34
+ }
35
+
36
+ const file = passwordFile(config.sourceFolder)
37
+ await mkdir(path.dirname(file), { recursive: true })
38
+ await writeFile(file, password, { mode: 0o600 })
39
+
40
+ const checked = verified ? `checked against ${did}` : "not checked: no atproto_did in settings.md"
41
+ notify({ status: "done", message: `Saved the app password to ${file} (${checked})` })
42
+ return { saved: file, verified }
43
+ }
44
+
45
+ /** The masked prompt, or an error saying what to pass instead. */
46
+ async function askForPassword() {
47
+ if (!process.stdin.isTTY) {
48
+ throw new Error("atproto: no terminal to ask in; pass the app password as {\"password\": \"…\"}")
49
+ }
50
+ const answer = await promptPassword({ message: "App password (from your Bluesky settings)" })
51
+ if (isCancel(answer) || !answer.trim()) throw new Error("atproto: no app password given; nothing saved")
52
+ return answer.trim()
53
+ }
54
+
55
+ /**
56
+ * The root's `atproto_did` and `atproto_pds`, from a build of the site -
57
+ * settings are the database's to resolve.
58
+ * @param {import("votive").VotiveConfig} config
59
+ */
60
+ async function account(config) {
61
+ const site = await votive({ ...config, verbose: false })
62
+ try {
63
+ await site.build({ defer: false })
64
+ const root = site.database.setting.getByFolder("")
65
+ return { did: root.lastNonNull("fm_atproto_did"), pds: root.lastNonNull("fm_atproto_pds") }
66
+ } finally {
67
+ await site.close()
68
+ }
69
+ }
70
+
71
+ export { saveAppPassword }