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.
- package/README.md +23 -17
- package/bin.js +87 -30
- package/cli.js +15 -0
- package/config.js +102 -16
- package/extractDate.js +70 -6
- package/index.js +14 -5
- package/package.json +42 -7
- package/plugins/assets/index.js +65 -0
- package/plugins/atproto/index.js +132 -0
- package/plugins/atproto/password.js +71 -0
- package/plugins/atproto/publish.js +406 -0
- package/plugins/atproto/record.js +144 -0
- package/plugins/atproto/rkey.js +54 -0
- package/plugins/deploy-cloudflare/index.js +92 -0
- package/plugins/fonts/index.js +18 -21
- package/plugins/html/assets/bundle-BeK1Hptv.css +21 -0
- package/plugins/html/bundle/assets/index-BeK1Hptv.css +21 -0
- package/plugins/html/bundle/index.js +21145 -0
- package/plugins/html/editor/EditButton.svelte +152 -0
- package/plugins/html/editor/Editor.svelte +51 -0
- package/plugins/html/editor/Overlays.svelte +6 -0
- package/plugins/html/editor/SettingsPanel.svelte +397 -0
- package/plugins/html/editor/Toolbar.svelte +644 -0
- package/plugins/html/editor/create-session.js +250 -0
- package/plugins/html/editor/directives.js +128 -0
- package/plugins/html/editor/frontmatter.js +97 -0
- package/plugins/html/editor/ingest.js +339 -0
- package/plugins/html/editor/main.js +94 -0
- package/plugins/html/editor/nodes/Alert.svelte +21 -0
- package/plugins/html/editor/nodes/Blockquote.svelte +9 -0
- package/plugins/html/editor/nodes/CodeBlock.svelte +19 -0
- package/plugins/html/editor/nodes/Embed.svelte +19 -0
- package/plugins/html/editor/nodes/Emphasis.svelte +11 -0
- package/plugins/html/editor/nodes/Heading.svelte +15 -0
- package/plugins/html/editor/nodes/Highlight.svelte +11 -0
- package/plugins/html/editor/nodes/Image.svelte +18 -0
- package/plugins/html/editor/nodes/InlineCode.svelte +11 -0
- package/plugins/html/editor/nodes/Link.svelte +17 -0
- package/plugins/html/editor/nodes/List.svelte +15 -0
- package/plugins/html/editor/nodes/ListItem.svelte +9 -0
- package/plugins/html/editor/nodes/Page.svelte +21 -0
- package/plugins/html/editor/nodes/Paragraph.svelte +9 -0
- package/plugins/html/editor/nodes/Strikethrough.svelte +11 -0
- package/plugins/html/editor/nodes/Strong.svelte +11 -0
- package/plugins/html/editor/nodes/ThematicBreak.svelte +9 -0
- package/plugins/html/editor/nodes/index.js +18 -0
- package/plugins/html/editor/rollup.config.js +35 -0
- package/plugins/html/editor/serialize.js +157 -0
- package/plugins/html/editor/settings-file.js +83 -0
- package/plugins/html/editor/socket.js +202 -0
- package/plugins/html/editor/source.js +43 -0
- package/plugins/html/image.js +104 -0
- package/plugins/html/index.js +760 -0
- package/plugins/html/partials.js +299 -0
- package/plugins/html/socialLinks.js +67 -0
- package/plugins/html/writeRules.js +377 -0
- package/plugins/icons/index.js +2 -6
- package/plugins/images/index.js +78 -55
- package/plugins/markdown/brandColors.js +10 -0
- package/plugins/markdown/colorScheme.js +64 -0
- package/plugins/markdown/emoji.js +30 -0
- package/plugins/markdown/frontmatter.js +253 -0
- package/plugins/markdown/index.js +287 -1274
- package/plugins/markdown/links.js +97 -0
- package/plugins/markdown/metadata.js +308 -0
- package/plugins/markdown/readRules.js +143 -0
- package/plugins/markdown/typography.js +109 -0
- package/plugins/markdown/visitor.js +143 -0
- package/plugins/publish/index.js +34 -0
- package/plugins/robots/agents.js +79 -0
- package/plugins/robots/index.js +30 -10
- package/plugins/robots/robots.js +48 -0
- package/plugins/styles/index.js +111 -26
- package/plugins/styles/theme.js +116 -0
- package/plugins/urls/index.js +78 -0
- package/plugins/urls/urlMetadata.js +246 -0
- package/plugins/vectors/index.js +5 -9
- package/plugins/xml/entryContent.js +103 -0
- package/plugins/xml/index.js +123 -50
- package/secretPaths.js +170 -0
- package/stylesheets/DefaultStyles.css +367 -437
- package/stylesheets/ResetStyles.css +2 -38
- package/stylesheets/SyntaxHighlightingStyles.css +269 -0
- package/stylesheets/TypographyStyles.css +386 -128
- package/systemPaths.js +48 -0
- package/utils.js +56 -5
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -73
- package/.github/ISSUE_TEMPLATE/config.yml +0 -7
- package/docs-source/.cache.json +0 -9
- package/docs-source/.obsidian/app.json +0 -3
- package/docs-source/.obsidian/appearance.json +0 -3
- package/docs-source/.obsidian/core-plugins-migration.json +0 -30
- package/docs-source/.obsidian/core-plugins.json +0 -20
- package/docs-source/.obsidian/workspace.json +0 -168
- package/docs-source/.stackblitzrc +0 -3
- package/docs-source/.votive.db +0 -0
- package/docs-source/about.md +0 -3
- package/docs-source/blog/home.md +0 -5
- package/docs-source/blog/url-ui.md +0 -21
- package/docs-source/blog.md +0 -5
- package/docs-source/docs/.votive.db +0 -0
- package/docs-source/docs/deploy.md +0 -67
- package/docs-source/docs/file-structure.md +0 -31
- package/docs-source/docs/folder-settings.md +0 -23
- package/docs-source/docs/home.md +0 -55
- package/docs-source/docs/images.md +0 -10
- package/docs-source/docs/items.md +0 -13
- package/docs-source/docs/pages.md +0 -141
- package/docs-source/docs/settings.md +0 -4
- package/docs-source/docs/styling.md +0 -34
- package/docs-source/docs/taxonomies.md +0 -37
- package/docs-source/features/cards.md +0 -7
- package/docs-source/features/editing.md +0 -7
- package/docs-source/features/emoji.md +0 -7
- package/docs-source/features/frontmatter.md +0 -7
- package/docs-source/features/lists.md +0 -7
- package/docs-source/features/navigation.md +0 -7
- package/docs-source/features/rich-previews.md +0 -7
- package/docs-source/features/robots.md +0 -7
- package/docs-source/features/rss.md +0 -7
- package/docs-source/features/sitemap.md +0 -7
- package/docs-source/features/speed.md +0 -7
- package/docs-source/features/static.md +0 -7
- package/docs-source/features/taxonomies.md +0 -7
- package/docs-source/home.md +0 -42
- package/docs-source/output/about.html +0 -1
- package/docs-source/output/blog/url-ui.html +0 -8
- package/docs-source/output/blog.html +0 -1
- package/docs-source/output/default.css +0 -1
- package/docs-source/output/docs/deploy.html +0 -34
- package/docs-source/output/docs/file-structure.html +0 -24
- package/docs-source/output/docs/folder-settings.html +0 -20
- package/docs-source/output/docs/images.html +0 -2
- package/docs-source/output/docs/items.html +0 -6
- package/docs-source/output/docs/pages.html +0 -101
- package/docs-source/output/docs/styling.html +0 -18
- package/docs-source/output/docs/taxonomies.html +0 -20
- package/docs-source/output/docs.html +0 -28
- package/docs-source/output/features/cards.html +0 -1
- package/docs-source/output/features/editing.html +0 -1
- package/docs-source/output/features/emoji.html +0 -1
- package/docs-source/output/features/frontmatter.html +0 -1
- package/docs-source/output/features/lists.html +0 -1
- package/docs-source/output/features/navigation.html +0 -1
- package/docs-source/output/features/rich-previews.html +0 -1
- package/docs-source/output/features/robots.html +0 -1
- package/docs-source/output/features/rss.html +0 -1
- package/docs-source/output/features/sitemap.html +0 -1
- package/docs-source/output/features/speed.html +0 -1
- package/docs-source/output/features/static.html +0 -1
- package/docs-source/output/features/taxonomies.html +0 -1
- package/docs-source/output/features.html +0 -1
- package/docs-source/output/feed.xml +0 -1
- package/docs-source/output/index.html +0 -21
- package/docs-source/output/reset.css +0 -1
- package/docs-source/output/roadmap.html +0 -87
- package/docs-source/output/robots.txt +0 -14
- package/docs-source/output/sitemap.xml +0 -16
- package/docs-source/output/typography.css +0 -1
- package/docs-source/roadmap.md +0 -98
- package/docs-source/settings.md +0 -13
- package/getMetadata.js +0 -41
- package/jsconfig.json +0 -10
- package/regex.js +0 -36
- 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.
|
|
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.
|
|
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
|
-
"
|
|
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 }
|