blogwright-pds 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ant Stanley
4
+
5
+ Portions of packages/cli/src/pds/rkey.ts are vendored from
6
+ https://github.com/mastrojs/atproto (MIT License, © 2025 Mastro).
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # blogwright-pds
2
+
3
+ standard.site (AT Protocol) publishing for [blogwright](https://github.com/antstanley/blogwright):
4
+ the atproto OAuth confidential client, the Secrets Manager-backed key/session store,
5
+ the `site.standard.publication` / `site.standard.document` record sync, and the
6
+ URL-derived rkey implementation (also exposed as the `blogwright/rkey` subpath of the CLI).
7
+
8
+ Consumed by the `blogwright` CLI, which owns dispatch and wiring; this package owns the
9
+ feature logic and depends only on `blogwright-core` (ports, config types, secrets client)
10
+ and `@atproto/oauth-client-node`. See the repository README for the `pds` command surface.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Single source of truth for the atproto OAuth confidential-client documents.
3
+ * The site serves these from /oauth/ (see `blogwright pds keygen`, which writes
4
+ * the committed public/oauth/*.json files); `pds login` verifies the deployed
5
+ * copies match before authorizing, so the CLI and the PDS always agree.
6
+ */
7
+ import type { OAuthClientMetadataInput } from '@atproto/oauth-client-node';
8
+ import type { OpsConfig, PdsConfig } from 'blogwright-core';
9
+ /**
10
+ * Repo-relative paths of the committed OAuth client documents. The segments
11
+ * under the public dir are protocol-fixed — they must mirror the URL paths in
12
+ * the client metadata — so only the public dir itself is configurable.
13
+ */
14
+ export declare function clientDocumentPaths(cfg: Pick<OpsConfig, 'paths'>): {
15
+ clientMetadata: string;
16
+ jwks: string;
17
+ };
18
+ /** Per the atproto OAuth spec the client_id IS the metadata document URL. */
19
+ export declare function clientMetadataUrl(domain: string): string;
20
+ export declare function jwksUrl(domain: string): string;
21
+ /** Static page (src/pages/oauth/callback.astro) that displays the redirect params. */
22
+ export declare function callbackUrl(domain: string): string;
23
+ /** The client metadata document served at clientMetadataUrl(domain). */
24
+ export declare function clientMetadata(domain: string, pds: PdsConfig): OAuthClientMetadataInput;
25
+ /** The JWKS document served at jwksUrl(domain) — public key material only. */
26
+ export declare function jwksDocument(publicJwk: Record<string, unknown>): {
27
+ keys: [typeof publicJwk];
28
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Single source of truth for the atproto OAuth confidential-client documents.
3
+ * The site serves these from /oauth/ (see `blogwright pds keygen`, which writes
4
+ * the committed public/oauth/*.json files); `pds login` verifies the deployed
5
+ * copies match before authorizing, so the CLI and the PDS always agree.
6
+ */
7
+ /**
8
+ * Repo-relative paths of the committed OAuth client documents. The segments
9
+ * under the public dir are protocol-fixed — they must mirror the URL paths in
10
+ * the client metadata — so only the public dir itself is configurable.
11
+ */
12
+ export function clientDocumentPaths(cfg) {
13
+ return {
14
+ clientMetadata: `${cfg.paths.publicDir}/oauth/client-metadata.json`,
15
+ jwks: `${cfg.paths.publicDir}/oauth/jwks.json`,
16
+ };
17
+ }
18
+ /** Per the atproto OAuth spec the client_id IS the metadata document URL. */
19
+ export function clientMetadataUrl(domain) {
20
+ return `https://${domain}/oauth/client-metadata.json`;
21
+ }
22
+ export function jwksUrl(domain) {
23
+ return `https://${domain}/oauth/jwks.json`;
24
+ }
25
+ /** Static page (src/pages/oauth/callback.astro) that displays the redirect params. */
26
+ export function callbackUrl(domain) {
27
+ return `https://${domain}/oauth/callback`;
28
+ }
29
+ /** The client metadata document served at clientMetadataUrl(domain). */
30
+ export function clientMetadata(domain, pds) {
31
+ return {
32
+ client_id: clientMetadataUrl(domain),
33
+ client_name: pds.name,
34
+ client_uri: `https://${domain}`,
35
+ application_type: 'web',
36
+ grant_types: ['authorization_code', 'refresh_token'],
37
+ response_types: ['code'],
38
+ redirect_uris: [callbackUrl(domain)],
39
+ scope: 'atproto transition:generic',
40
+ token_endpoint_auth_method: 'private_key_jwt',
41
+ token_endpoint_auth_signing_alg: 'ES256',
42
+ dpop_bound_access_tokens: true,
43
+ jwks_uri: jwksUrl(domain),
44
+ };
45
+ }
46
+ /** The JWKS document served at jwksUrl(domain) — public key material only. */
47
+ export function jwksDocument(publicJwk) {
48
+ if (publicJwk.d !== undefined)
49
+ throw new Error('jwksDocument received a private JWK (has "d")');
50
+ return { keys: [publicJwk] };
51
+ }
52
+ //# sourceMappingURL=client-metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-metadata.js","sourceRoot":"","sources":["../src/client-metadata.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAA6B;IAI/D,OAAO;QACL,cAAc,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,6BAA6B;QACnE,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,kBAAkB;KAC/C,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,OAAO,WAAW,MAAM,6BAA6B,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,MAAc;IACpC,OAAO,WAAW,MAAM,kBAAkB,CAAC;AAC7C,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,OAAO,WAAW,MAAM,iBAAiB,CAAC;AAC5C,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,GAAc;IAC3D,OAAO;QACL,SAAS,EAAE,iBAAiB,CAAC,MAAM,CAAC;QACpC,WAAW,EAAE,GAAG,CAAC,IAAI;QACrB,UAAU,EAAE,WAAW,MAAM,EAAE;QAC/B,gBAAgB,EAAE,KAAK;QACvB,WAAW,EAAE,CAAC,oBAAoB,EAAE,eAAe,CAAC;QACpD,cAAc,EAAE,CAAC,MAAM,CAAC;QACxB,aAAa,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACpC,KAAK,EAAE,4BAA4B;QACnC,0BAA0B,EAAE,iBAAiB;QAC7C,+BAA+B,EAAE,OAAO;QACxC,wBAAwB,EAAE,IAAI;QAC9B,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC;KAC1B,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAC,SAAkC;IAC7D,IAAI,SAAS,CAAC,CAAC,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IAChG,OAAO,EAAE,IAAI,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { PdsContext } from './context.js';
2
+ import { generateClientKey, login as oauthLogin, openPdsRepo, verifyClientAssets } from './oauth.js';
3
+ import { type OpenRepo, type SyncSummary } from './sync.js';
4
+ /**
5
+ * Generate the OAuth confidential-client key: private JWK into the secret
6
+ * (clearing any session — client auth is bound to the key), public half into
7
+ * the two committed /oauth/ documents the site serves.
8
+ */
9
+ export declare function keygen(ctx: PdsContext, repoRoot?: string, generateKey?: typeof generateClientKey): Promise<void>;
10
+ /** Interactive OAuth bootstrap; see oauth.ts#login for the flow. */
11
+ export declare function login(ctx: PdsContext, opts: {
12
+ identifier?: string | undefined;
13
+ }, runLogin?: typeof oauthLogin): Promise<void>;
14
+ /** Show whether the secret exists and which parts it holds. Never prints values. */
15
+ export declare function secretStatus(ctx: PdsContext): Promise<void>;
16
+ /** Delete the secret (immediate, no recovery window). */
17
+ export declare function secretDelete(ctx: PdsContext, opts: {
18
+ yes: boolean;
19
+ }): Promise<void>;
20
+ /**
21
+ * One-time (idempotent) publication setup: create the site.standard.publication
22
+ * record — or update it when the committed well-known file already names one — and
23
+ * write the two site files the user commits.
24
+ */
25
+ export declare function init(ctx: PdsContext, repoRoot?: string, openRepo?: typeof openPdsRepo, verifyAssets?: typeof verifyClientAssets): Promise<void>;
26
+ /** Reconcile PDS records against local content. Production only. */
27
+ export declare function sync(ctx: PdsContext, repoRoot?: string, openRepo?: OpenRepo): Promise<void>;
28
+ /**
29
+ * Post-deploy hook: reconcile the PDS after a successful production deploy.
30
+ * Never fatal — a PDS outage must not fail a good site deploy; the next deploy
31
+ * re-reconciles. No-op unless production, configured, and initialised.
32
+ */
33
+ export declare function syncAfterDeploy(ctx: PdsContext, repoRoot?: string, doSync?: (ctx: PdsContext, repoRoot: string) => Promise<SyncSummary>): Promise<void>;
@@ -0,0 +1,159 @@
1
+ import { join } from 'node:path';
2
+ import { colors, findRepoRoot } from 'blogwright-core';
3
+ import { clientDocumentPaths, clientMetadata, jwksDocument } from './client-metadata.js';
4
+ import { generateClientKey, login as oauthLogin, openPdsRepo, publicClientJwk, verifyClientAssets, } from './oauth.js';
5
+ import { loadPdsSecret, updatePdsSecret } from './secret.js';
6
+ import { PUBLICATION_COLLECTION, publicationRecord, readWellKnownUri, requirePdsConfig, syncPds, wellKnownPath, } from './sync.js';
7
+ import { rkeyFromUri } from './xrpc.js';
8
+ /**
9
+ * Generate the OAuth confidential-client key: private JWK into the secret
10
+ * (clearing any session — client auth is bound to the key), public half into
11
+ * the two committed /oauth/ documents the site serves.
12
+ */
13
+ export async function keygen(ctx, repoRoot, generateKey = generateClientKey) {
14
+ const pds = requirePdsConfig(ctx);
15
+ if (!ctx.domain)
16
+ throw new Error('pds keygen requires a configured domain');
17
+ const root = repoRoot ?? (await findRepoRoot(ctx.ports.fs));
18
+ const kid = `${ctx.config.siteName}-oauth-${new Date().toISOString().slice(0, 10)}`;
19
+ const clientKey = await generateKey(kid);
20
+ await updatePdsSecret(ctx.clients.secrets, pds.secretName, (secret) => ({ ...secret, clientKey, session: undefined }),
21
+ // keygen is the migration entry point — a legacy app-password value is replaced
22
+ { replaceLegacy: true });
23
+ ctx.logger.ok(`stored private key "${kid}" in secret "${pds.secretName}"`);
24
+ const docPaths = clientDocumentPaths(ctx.config);
25
+ const documents = [
26
+ [docPaths.clientMetadata, clientMetadata(ctx.domain, pds)],
27
+ [docPaths.jwks, jwksDocument(await publicClientJwk(clientKey))],
28
+ ];
29
+ for (const [path, body] of documents) {
30
+ await ctx.ports.fs.writeText(join(root, path), `${JSON.stringify(body, null, 2)}\n`);
31
+ ctx.logger.info(` wrote ${path}`);
32
+ }
33
+ ctx.logger.info(colors.bold(`Commit ${ctx.config.paths.publicDir}/oauth/* and release — then run \`blogwright pds login\`.`));
34
+ }
35
+ /** Interactive OAuth bootstrap; see oauth.ts#login for the flow. */
36
+ export async function login(ctx, opts, runLogin = oauthLogin) {
37
+ if (!opts.identifier) {
38
+ throw new Error('pds login requires --identifier <handle-or-did>');
39
+ }
40
+ await runLogin(ctx, opts.identifier, {
41
+ promptLine: (question) => ctx.ports.terminal.question(question),
42
+ });
43
+ }
44
+ /** Show whether the secret exists and which parts it holds. Never prints values. */
45
+ export async function secretStatus(ctx) {
46
+ const pds = requirePdsConfig(ctx);
47
+ const meta = await ctx.clients.secrets.describeSecret(pds.secretName);
48
+ if (!meta) {
49
+ ctx.logger.info(`no secret at "${pds.secretName}" — create it with \`blogwright pds keygen\``);
50
+ return;
51
+ }
52
+ ctx.logger.info(` name ${meta.name}`);
53
+ ctx.logger.info(` arn ${meta.arn}`);
54
+ if (meta.lastChangedDate !== undefined) {
55
+ ctx.logger.info(` last changed ${new Date(meta.lastChangedDate * 1000).toISOString()}`);
56
+ }
57
+ try {
58
+ const secret = await loadPdsSecret(ctx);
59
+ const kid = secret.clientKey?.kid;
60
+ ctx.logger.info(` client key ${secret.clientKey ? `yes (${kid ?? 'no kid'})` : 'no'}`);
61
+ ctx.logger.info(` did ${secret.did ?? 'no'}`);
62
+ ctx.logger.info(` session ${secret.session ? 'yes' : 'no'}`);
63
+ }
64
+ catch (err) {
65
+ ctx.logger.warn(err.message);
66
+ }
67
+ }
68
+ /** Delete the secret (immediate, no recovery window). */
69
+ export async function secretDelete(ctx, opts) {
70
+ const pds = requirePdsConfig(ctx);
71
+ if (!opts.yes)
72
+ throw new Error(`refusing to delete secret "${pds.secretName}" without --yes`);
73
+ await ctx.clients.secrets.deleteSecret(pds.secretName);
74
+ ctx.logger.ok(`deleted secret "${pds.secretName}"`);
75
+ }
76
+ /**
77
+ * One-time (idempotent) publication setup: create the site.standard.publication
78
+ * record — or update it when the committed well-known file already names one — and
79
+ * write the two site files the user commits.
80
+ */
81
+ export async function init(ctx, repoRoot, openRepo = openPdsRepo, verifyAssets = verifyClientAssets) {
82
+ const pds = requirePdsConfig(ctx);
83
+ if (!ctx.domain)
84
+ throw new Error('pds init requires a configured domain');
85
+ const root = repoRoot ?? (await findRepoRoot(ctx.ports.fs));
86
+ await verifyAssets(ctx);
87
+ const { did, repo } = await openRepo(ctx);
88
+ const record = publicationRecord(pds, `https://${ctx.domain}`);
89
+ const existingUri = await readWellKnownUri(ctx.ports.fs, root, ctx.config);
90
+ if (existingUri && !existingUri.startsWith(`at://${did}/`)) {
91
+ // A committed well-known from a fork or an account migration: adopting it
92
+ // would publish document records that point at a publication this account
93
+ // does not own, silently breaking standard.site verification.
94
+ throw new Error(`${wellKnownPath(ctx.config)} points at ${existingUri}, which belongs to a different ` +
95
+ `account than ${did} — delete the file to create a fresh publication, or log in ` +
96
+ `with the owning account`);
97
+ }
98
+ let publicationUri;
99
+ if (existingUri) {
100
+ await repo.putRecord(PUBLICATION_COLLECTION, rkeyFromUri(existingUri), record);
101
+ publicationUri = existingUri;
102
+ ctx.logger.ok(`updated publication ${publicationUri}`);
103
+ }
104
+ else {
105
+ const created = await repo.createRecord(PUBLICATION_COLLECTION, record);
106
+ publicationUri = created.uri;
107
+ ctx.logger.ok(`created publication ${publicationUri}`);
108
+ }
109
+ const wellKnown = wellKnownPath(ctx.config);
110
+ await ctx.ports.fs.writeText(join(root, wellKnown), `${publicationUri}\n`);
111
+ await ctx.ports.fs.writeText(join(root, ctx.config.paths.atprotoJson), `${JSON.stringify({ did, publicationUri }, null, 2)}\n`);
112
+ ctx.logger.info(` wrote ${wellKnown}`);
113
+ ctx.logger.info(` wrote ${ctx.config.paths.atprotoJson}`);
114
+ ctx.logger.info(colors.bold('Commit both files and deploy — they verify the publication and the post link tags.'));
115
+ }
116
+ /** Reconcile PDS records against local content. Production only. */
117
+ export async function sync(ctx, repoRoot, openRepo = openPdsRepo) {
118
+ if (ctx.env !== 'production') {
119
+ throw new Error(`pds sync publishes canonical production URLs and refuses to run for "${ctx.env}"`);
120
+ }
121
+ const root = repoRoot ?? (await findRepoRoot(ctx.ports.fs));
122
+ const summary = await syncPds(ctx, root, openRepo);
123
+ logSummary(ctx, summary);
124
+ }
125
+ function logSummary(ctx, s) {
126
+ ctx.logger.ok(`publication ${s.publication}; documents: ${s.created.length} created, ` +
127
+ `${s.updated.length} updated, ${s.unchanged} unchanged`);
128
+ for (const slug of s.created)
129
+ ctx.logger.info(` created ${slug}`);
130
+ for (const slug of s.updated)
131
+ ctx.logger.info(` updated ${slug}`);
132
+ if (s.orphans.length > 0) {
133
+ ctx.logger.warn(`${s.orphans.length} PDS record(s) have no local post (rkeys: ${s.orphans.join(', ')}) — ` +
134
+ 'not deleted; remove them manually if intended');
135
+ }
136
+ }
137
+ /**
138
+ * Post-deploy hook: reconcile the PDS after a successful production deploy.
139
+ * Never fatal — a PDS outage must not fail a good site deploy; the next deploy
140
+ * re-reconciles. No-op unless production, configured, and initialised.
141
+ */
142
+ export async function syncAfterDeploy(ctx, repoRoot, doSync = (c, r) => syncPds(c, r, openPdsRepo)) {
143
+ if (ctx.env !== 'production' || !ctx.config.pds)
144
+ return;
145
+ const root = repoRoot ?? (await findRepoRoot(ctx.ports.fs));
146
+ const initialised = await readWellKnownUri(ctx.ports.fs, root, ctx.config).catch(() => undefined);
147
+ if (!initialised) {
148
+ ctx.logger.info('standard.site publishing not initialised (`blogwright pds init`) — skipping');
149
+ return;
150
+ }
151
+ try {
152
+ ctx.logger.step('syncing standard.site records to the PDS');
153
+ logSummary(ctx, await doSync(ctx, root));
154
+ }
155
+ catch (err) {
156
+ ctx.logger.warn(`pds sync failed (deploy unaffected): ${err.message}`);
157
+ }
158
+ }
159
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { FileSystem } from 'blogwright-core';
2
+ /** A publishable blog post, as enumerated from the Astro content collection. */
3
+ export interface PostMeta {
4
+ /**
5
+ * Content-collection id: frontmatter `slug` when set, else the file path
6
+ * under the content dir minus its `.md`/`.mdx` extension and any trailing
7
+ * `/index` segment — matching the id Astro's glob loader produces.
8
+ */
9
+ slug: string;
10
+ title: string;
11
+ description: string;
12
+ pubDate: Date;
13
+ }
14
+ /**
15
+ * Parse the flat frontmatter subset used by src/content.config.ts: scalar
16
+ * `key: value` lines where value is a bare token, a quoted string, a date, or a
17
+ * boolean. Deliberately not a YAML parser — the schema is flat by construction.
18
+ */
19
+ export declare function parseFrontmatter(source: string, file: string): Record<string, string>;
20
+ /**
21
+ * Enumerate publishable posts from the content collection, mirroring the Astro
22
+ * glob-loader ids: `.md`/`.mdx` files, a frontmatter `slug` wins outright, a
23
+ * trailing `/index` segment is dropped, drafts excluded. (Astro additionally
24
+ * github-slugifies unusual path segments — keep file names lowercase-kebab, as
25
+ * the rkey derived here must match the id the site builds its link tags from.)
26
+ */
27
+ export declare function listPublishablePosts(fs: FileSystem, repoRoot: string, contentDir?: string): Promise<PostMeta[]>;
@@ -0,0 +1,69 @@
1
+ import { join } from 'node:path';
2
+ /** Default content-collection directory; overridable via `config.paths.content`. */
3
+ const DEFAULT_CONTENT_DIR = 'src/content/blog';
4
+ const REQUIRED_FIELDS = ['title', 'description', 'pubDate'];
5
+ /**
6
+ * Parse the flat frontmatter subset used by src/content.config.ts: scalar
7
+ * `key: value` lines where value is a bare token, a quoted string, a date, or a
8
+ * boolean. Deliberately not a YAML parser — the schema is flat by construction.
9
+ */
10
+ export function parseFrontmatter(source, file) {
11
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
12
+ if (!match?.[1])
13
+ throw new Error(`${file}: no frontmatter block`);
14
+ const fields = {};
15
+ for (const line of match[1].split(/\r?\n/)) {
16
+ if (!line.trim() || line.trimStart().startsWith('#'))
17
+ continue;
18
+ const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
19
+ if (!kv)
20
+ throw new Error(`${file}: unsupported frontmatter line "${line}"`);
21
+ const key = kv[1];
22
+ let value = (kv[2] ?? '').trim();
23
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
24
+ value = value.slice(1, -1).replaceAll("''", "'");
25
+ }
26
+ else if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
27
+ value = value.slice(1, -1).replaceAll('\\"', '"');
28
+ }
29
+ fields[key] = value;
30
+ }
31
+ return fields;
32
+ }
33
+ /**
34
+ * Enumerate publishable posts from the content collection, mirroring the Astro
35
+ * glob-loader ids: `.md`/`.mdx` files, a frontmatter `slug` wins outright, a
36
+ * trailing `/index` segment is dropped, drafts excluded. (Astro additionally
37
+ * github-slugifies unusual path segments — keep file names lowercase-kebab, as
38
+ * the rkey derived here must match the id the site builds its link tags from.)
39
+ */
40
+ export async function listPublishablePosts(fs, repoRoot, contentDir = DEFAULT_CONTENT_DIR) {
41
+ const dir = join(repoRoot, contentDir);
42
+ const posts = [];
43
+ for (const file of await fs.listFiles(dir)) {
44
+ const rel = file.replaceAll('\\', '/');
45
+ if (!/\.mdx?$/.test(rel))
46
+ continue;
47
+ const base = rel.replace(/\.mdx?$/, '');
48
+ const pathId = base.endsWith('/index') ? base.slice(0, -'/index'.length) : base;
49
+ const fields = parseFrontmatter(await fs.readText(join(dir, file)), rel);
50
+ if (fields.draft === 'true')
51
+ continue;
52
+ for (const key of REQUIRED_FIELDS) {
53
+ if (!fields[key])
54
+ throw new Error(`${rel}: frontmatter is missing "${key}"`);
55
+ }
56
+ const pubDate = new Date(fields.pubDate);
57
+ if (Number.isNaN(pubDate.getTime())) {
58
+ throw new Error(`${rel}: invalid pubDate "${fields.pubDate}"`);
59
+ }
60
+ posts.push({
61
+ slug: fields.slug || pathId,
62
+ title: fields.title,
63
+ description: fields.description,
64
+ pubDate,
65
+ });
66
+ }
67
+ return posts.sort((a, b) => a.slug.localeCompare(b.slug));
68
+ }
69
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The narrow slice of the host CLI's context this feature package depends on.
3
+ * The CLI's OpsContext satisfies it structurally, so the dispatch boundary is
4
+ * a plain assignment — this package never imports CLI types.
5
+ */
6
+ import type { FileSystem, OpsConfig, SecretsManagerClient, Terminal } from 'blogwright-core';
7
+ /** Leveled logger surface the pds commands report through. */
8
+ export interface PdsLogger {
9
+ info(msg: string): void;
10
+ step(msg: string): void;
11
+ ok(msg: string): void;
12
+ warn(msg: string): void;
13
+ error(msg: string): void;
14
+ }
15
+ /** The ports the pds feature crosses: repo files and the operator's terminal. */
16
+ export interface PdsPorts {
17
+ fs: FileSystem;
18
+ terminal: Terminal;
19
+ }
20
+ /** Everything a pds command needs from its host — config, secrets, ports, logging. */
21
+ export interface PdsContext {
22
+ env: string;
23
+ domain: string | undefined;
24
+ config: OpsConfig;
25
+ clients: {
26
+ secrets: SecretsManagerClient;
27
+ };
28
+ ports: PdsPorts;
29
+ logger: PdsLogger;
30
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The narrow slice of the host CLI's context this feature package depends on.
3
+ * The CLI's OpsContext satisfies it structurally, so the dispatch boundary is
4
+ * a plain assignment — this package never imports CLI types.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public surface of blogwright-pds: the pds command entry points the CLI
3
+ * dispatches to, plus the context types that name the dependency boundary.
4
+ * The rkey implementation is the separate `blogwright-pds/rkey` subpath.
5
+ */
6
+ export * from './commands.js';
7
+ export type { PdsContext, PdsLogger, PdsPorts } from './context.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public surface of blogwright-pds: the pds command entry points the CLI
3
+ * dispatches to, plus the context types that name the dependency boundary.
4
+ * The rkey implementation is the separate `blogwright-pds/rkey` subpath.
5
+ */
6
+ export * from './commands.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,eAAe,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * atproto OAuth (confidential client) for standard.site publishing — the only
3
+ * module that touches @atproto/oauth-client-node. The blog itself is the OAuth
4
+ * client: its metadata + JWKS are static files on the site (client-metadata.ts),
5
+ * the private key and session live in the Secrets Manager secret (secret.ts),
6
+ * and every token refresh persists the rotated refresh token back there.
7
+ */
8
+ import { type Jwk } from '@atproto/oauth-client-node';
9
+ import type { PdsContext } from './context.js';
10
+ import { PdsClient } from './xrpc.js';
11
+ /** A fresh ES256 private JWK for private_key_jwt client authentication. */
12
+ export declare function generateClientKey(kid: string): Promise<Jwk>;
13
+ /** Public half of the client key, for the served JWKS. */
14
+ export declare function publicClientJwk(clientKey: Jwk): Promise<Record<string, unknown>>;
15
+ /**
16
+ * Check that the deployed /oauth/ documents match what this CLI would send the
17
+ * authorization server. Run before login/init — a stale or missing deployment
18
+ * would otherwise fail deep inside the OAuth flow.
19
+ */
20
+ export declare function verifyClientAssets(ctx: PdsContext, fetchImpl?: typeof fetch): Promise<void>;
21
+ /** The authorize/callback surface of NodeOAuthClient — injectable for tests. */
22
+ interface OauthFlow {
23
+ authorize(input: string): Promise<URL>;
24
+ callback(params: URLSearchParams): Promise<{
25
+ session: {
26
+ did: string;
27
+ };
28
+ }>;
29
+ }
30
+ export interface LoginDeps {
31
+ /** Ask the operator one question and return their answer. */
32
+ promptLine: (question: string) => Promise<string>;
33
+ flow?: OauthFlow | undefined;
34
+ verifyAssets?: typeof verifyClientAssets | undefined;
35
+ }
36
+ /**
37
+ * One-time interactive bootstrap: authorize in a browser, land on the site's
38
+ * /oauth/callback page, paste the redirect URL back. The client persists the
39
+ * session (and DID) into the secret through the session store.
40
+ */
41
+ export declare function login(ctx: PdsContext, identifier: string, deps: LoginDeps): Promise<string>;
42
+ /**
43
+ * Restore the stored OAuth session (transparently refreshing — the rotated
44
+ * refresh token lands back in the secret) and wrap it as a PdsClient. The
45
+ * session's fetchHandler adds DPoP + auth headers and handles nonce retries.
46
+ */
47
+ export declare function openPdsRepo(ctx: PdsContext): Promise<{
48
+ did: string;
49
+ repo: PdsClient;
50
+ }>;
51
+ export {};