blogwright-pds 0.3.2 → 0.4.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.
@@ -8,8 +8,8 @@ import type { OAuthClientMetadataInput } from '@atproto/oauth-client-node';
8
8
  import type { OpsConfig, PdsConfig } from 'blogwright-core';
9
9
  /**
10
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.
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
13
  */
14
14
  export declare function clientDocumentPaths(cfg: Pick<OpsConfig, 'paths'>): {
15
15
  clientMetadata: string;
@@ -22,7 +22,7 @@ export declare function jwksUrl(domain: string): string;
22
22
  export declare function callbackUrl(domain: string): string;
23
23
  /** The client metadata document served at clientMetadataUrl(domain). */
24
24
  export declare function clientMetadata(domain: string, pds: PdsConfig): OAuthClientMetadataInput;
25
- /** The JWKS document served at jwksUrl(domain) public key material only. */
25
+ /** The JWKS document served at jwksUrl(domain) - public key material only. */
26
26
  export declare function jwksDocument(publicJwk: Record<string, unknown>): {
27
27
  keys: [typeof publicJwk];
28
28
  };
@@ -6,8 +6,8 @@
6
6
  */
7
7
  /**
8
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.
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
11
  */
12
12
  export function clientDocumentPaths(cfg) {
13
13
  return {
@@ -43,10 +43,9 @@ export function clientMetadata(domain, pds) {
43
43
  jwks_uri: jwksUrl(domain),
44
44
  };
45
45
  }
46
- /** The JWKS document served at jwksUrl(domain) public key material only. */
46
+ /** The JWKS document served at jwksUrl(domain) - public key material only. */
47
47
  export function jwksDocument(publicJwk) {
48
48
  if (publicJwk.d !== undefined)
49
49
  throw new Error('jwksDocument received a private JWK (has "d")');
50
50
  return { keys: [publicJwk] };
51
51
  }
52
- //# sourceMappingURL=client-metadata.js.map
@@ -3,7 +3,7 @@ import { generateClientKey, login as oauthLogin, openPdsRepo, verifyClientAssets
3
3
  import { type OpenRepo, type SyncSummary } from './sync.js';
4
4
  /**
5
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
6
+ * (clearing any session - client auth is bound to the key), public half into
7
7
  * the two committed /oauth/ documents the site serves.
8
8
  */
9
9
  export declare function keygen(ctx: PdsContext, repoRoot?: string, generateKey?: typeof generateClientKey): Promise<void>;
@@ -19,7 +19,7 @@ export declare function secretDelete(ctx: PdsContext, opts: {
19
19
  }): Promise<void>;
20
20
  /**
21
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
22
+ * record - or update it when the committed well-known file already names one - and
23
23
  * write the two site files the user commits.
24
24
  */
25
25
  export declare function init(ctx: PdsContext, repoRoot?: string, openRepo?: typeof openPdsRepo, verifyAssets?: typeof verifyClientAssets): Promise<void>;
@@ -27,7 +27,7 @@ export declare function init(ctx: PdsContext, repoRoot?: string, openRepo?: type
27
27
  export declare function sync(ctx: PdsContext, repoRoot?: string, openRepo?: OpenRepo): Promise<void>;
28
28
  /**
29
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
30
+ * Never fatal - a PDS outage must not fail a good site deploy; the next deploy
31
31
  * re-reconciles. No-op unless production, configured, and initialised.
32
32
  */
33
33
  export declare function syncAfterDeploy(ctx: PdsContext, repoRoot?: string, doSync?: (ctx: PdsContext, repoRoot: string) => Promise<SyncSummary>): Promise<void>;
package/dist/commands.js CHANGED
@@ -7,7 +7,7 @@ import { PUBLICATION_COLLECTION, publicationRecord, readWellKnownUri, requirePds
7
7
  import { rkeyFromUri } from './xrpc.js';
8
8
  /**
9
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
10
+ * (clearing any session - client auth is bound to the key), public half into
11
11
  * the two committed /oauth/ documents the site serves.
12
12
  */
13
13
  export async function keygen(ctx, repoRoot, generateKey = generateClientKey) {
@@ -18,7 +18,7 @@ export async function keygen(ctx, repoRoot, generateKey = generateClientKey) {
18
18
  const kid = `${ctx.config.siteName}-oauth-${new Date().toISOString().slice(0, 10)}`;
19
19
  const clientKey = await generateKey(kid);
20
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
21
+ // keygen is the migration entry point - a legacy app-password value is replaced
22
22
  { replaceLegacy: true, tags: ctx.tags });
23
23
  ctx.logger.ok(`stored private key "${kid}" in secret "${pds.secretName}"`);
24
24
  const docPaths = clientDocumentPaths(ctx.config);
@@ -30,7 +30,7 @@ export async function keygen(ctx, repoRoot, generateKey = generateClientKey) {
30
30
  await ctx.ports.fs.writeText(join(root, path), `${JSON.stringify(body, null, 2)}\n`);
31
31
  ctx.logger.info(` wrote ${path}`);
32
32
  }
33
- ctx.logger.info(colors.bold(`Commit ${ctx.config.paths.publicDir}/oauth/* and release then run \`blogwright pds login\`.`));
33
+ ctx.logger.info(colors.bold(`Commit ${ctx.config.paths.publicDir}/oauth/* and release - then run \`blogwright pds login\`.`));
34
34
  }
35
35
  /** Interactive OAuth bootstrap; see oauth.ts#login for the flow. */
36
36
  export async function login(ctx, opts, runLogin = oauthLogin) {
@@ -46,7 +46,7 @@ export async function secretStatus(ctx) {
46
46
  const pds = requirePdsConfig(ctx);
47
47
  const meta = await ctx.clients.secrets.describeSecret(pds.secretName);
48
48
  if (!meta) {
49
- ctx.logger.info(`no secret at "${pds.secretName}" create it with \`blogwright pds keygen\``);
49
+ ctx.logger.info(`no secret at "${pds.secretName}" - create it with \`blogwright pds keygen\``);
50
50
  return;
51
51
  }
52
52
  ctx.logger.info(` name ${meta.name}`);
@@ -75,7 +75,7 @@ export async function secretDelete(ctx, opts) {
75
75
  }
76
76
  /**
77
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
78
+ * record - or update it when the committed well-known file already names one - and
79
79
  * write the two site files the user commits.
80
80
  */
81
81
  export async function init(ctx, repoRoot, openRepo = openPdsRepo, verifyAssets = verifyClientAssets) {
@@ -92,7 +92,7 @@ export async function init(ctx, repoRoot, openRepo = openPdsRepo, verifyAssets =
92
92
  // would publish document records that point at a publication this account
93
93
  // does not own, silently breaking standard.site verification.
94
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 ` +
95
+ `account than ${did} - delete the file to create a fresh publication, or log in ` +
96
96
  `with the owning account`);
97
97
  }
98
98
  let publicationUri;
@@ -111,7 +111,7 @@ export async function init(ctx, repoRoot, openRepo = openPdsRepo, verifyAssets =
111
111
  await ctx.ports.fs.writeText(join(root, ctx.config.paths.atprotoJson), `${JSON.stringify({ did, publicationUri }, null, 2)}\n`);
112
112
  ctx.logger.info(` wrote ${wellKnown}`);
113
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.'));
114
+ ctx.logger.info(colors.bold('Commit both files and deploy - they verify the publication and the post link tags.'));
115
115
  }
116
116
  /** Reconcile PDS records against local content. Production only. */
117
117
  export async function sync(ctx, repoRoot, openRepo = openPdsRepo) {
@@ -130,13 +130,13 @@ function logSummary(ctx, s) {
130
130
  for (const slug of s.updated)
131
131
  ctx.logger.info(` updated ${slug}`);
132
132
  if (s.orphans.length > 0) {
133
- ctx.logger.warn(`${s.orphans.length} PDS record(s) have no local post (rkeys: ${s.orphans.join(', ')}) ` +
133
+ ctx.logger.warn(`${s.orphans.length} PDS record(s) have no local post (rkeys: ${s.orphans.join(', ')}) - ` +
134
134
  'not deleted; remove them manually if intended');
135
135
  }
136
136
  }
137
137
  /**
138
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
139
+ * Never fatal - a PDS outage must not fail a good site deploy; the next deploy
140
140
  * re-reconciles. No-op unless production, configured, and initialised.
141
141
  */
142
142
  export async function syncAfterDeploy(ctx, repoRoot, doSync = (c, r) => syncPds(c, r, openPdsRepo)) {
@@ -145,7 +145,7 @@ export async function syncAfterDeploy(ctx, repoRoot, doSync = (c, r) => syncPds(
145
145
  const root = repoRoot ?? (await findRepoRoot(ctx.ports.fs));
146
146
  const initialised = await readWellKnownUri(ctx.ports.fs, root, ctx.config).catch(() => undefined);
147
147
  if (!initialised) {
148
- ctx.logger.info('standard.site publishing not initialised (`blogwright pds init`) skipping');
148
+ ctx.logger.info('standard.site publishing not initialised (`blogwright pds init`) - skipping');
149
149
  return;
150
150
  }
151
151
  try {
@@ -156,4 +156,3 @@ export async function syncAfterDeploy(ctx, repoRoot, doSync = (c, r) => syncPds(
156
156
  ctx.logger.warn(`pds sync failed (deploy unaffected): ${err.message}`);
157
157
  }
158
158
  }
159
- //# sourceMappingURL=commands.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * This package owns the `pds` config key end to end: validating the block
3
+ * core's `parseConfig` hands off unvalidated, and deriving the
4
+ * `<siteName>/atproto` default `secretName`.
5
+ *
6
+ * `validatePdsConfig` and `resolvePdsSecretName` were lifted verbatim from
7
+ * `packages/core/src/config.ts` (the `cfg.pds` branch of `validateConfig` and
8
+ * the `secretName` line of `mergeConfig`) - same checks, same message
9
+ * strings, same `new URL(...)` try/catch ordering, so a non-URL resolver
10
+ * still reports "must be a URL" before an `http://` one reports "must be
11
+ * https". Core's copies are now gone: these are the only ones.
12
+ *
13
+ * One case core never had to answer is answered here: an ABSENT block. Core
14
+ * gated its checks behind `if (cfg.pds)`, so nothing validated a document
15
+ * with no `pds` key; the host now calls a plugin's validator with
16
+ * `undefined` in exactly that case, and {@link NO_PDS_SECTION_MESSAGE} is
17
+ * what this package returns to an operator for it.
18
+ */
19
+ import type { PdsConfig } from 'blogwright-core';
20
+ /**
21
+ * A `pds` config block whose `secretName` has been resolved: never absent,
22
+ * where core's own `PdsConfig` declares it optional because core no longer
23
+ * defaults it. `requirePdsConfig` (`sync.ts`) returns this - built from
24
+ * {@link resolvePdsSecretName} - so every call site downstream keeps a total
25
+ * type on `secretName` with no cast and no `!`.
26
+ */
27
+ export interface ResolvedPdsConfig extends PdsConfig {
28
+ secretName: string;
29
+ }
30
+ /**
31
+ * What an ABSENT `pds` block reports. The same sentence `requirePdsConfig`
32
+ * (`sync.ts`) has always raised for a repo that has not written one, declared
33
+ * here and imported there so the two refusals cannot drift into two different
34
+ * sentences for one situation - whichever of them an operator happens to
35
+ * reach first.
36
+ */
37
+ export declare const NO_PDS_SECTION_MESSAGE = "config has no \"pds\" section - add it to config/production.jsonc";
38
+ /**
39
+ * Validate a raw `pds` config block, boundary-checked as `unknown` because it
40
+ * comes off `parseConfigDocument`'s `raw` half (a plugin's block has no type
41
+ * until its own package narrows it). Throws with the same message strings
42
+ * core's `validateConfig` raises today for these checks.
43
+ */
44
+ export declare function validatePdsConfig(raw: unknown): PdsConfig;
45
+ /**
46
+ * Resolve the secret name a `pds` block uses: the explicit `secretName` when
47
+ * present, otherwise `<siteName>/atproto`.
48
+ */
49
+ export declare function resolvePdsSecretName(pds: PdsConfig, siteName: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * This package owns the `pds` config key end to end: validating the block
3
+ * core's `parseConfig` hands off unvalidated, and deriving the
4
+ * `<siteName>/atproto` default `secretName`.
5
+ *
6
+ * `validatePdsConfig` and `resolvePdsSecretName` were lifted verbatim from
7
+ * `packages/core/src/config.ts` (the `cfg.pds` branch of `validateConfig` and
8
+ * the `secretName` line of `mergeConfig`) - same checks, same message
9
+ * strings, same `new URL(...)` try/catch ordering, so a non-URL resolver
10
+ * still reports "must be a URL" before an `http://` one reports "must be
11
+ * https". Core's copies are now gone: these are the only ones.
12
+ *
13
+ * One case core never had to answer is answered here: an ABSENT block. Core
14
+ * gated its checks behind `if (cfg.pds)`, so nothing validated a document
15
+ * with no `pds` key; the host now calls a plugin's validator with
16
+ * `undefined` in exactly that case, and {@link NO_PDS_SECTION_MESSAGE} is
17
+ * what this package returns to an operator for it.
18
+ */
19
+ /** Characters permitted in a Secrets Manager secret name. */
20
+ const SECRET_NAME_PATTERN = /^[\w/+=.@-]+$/;
21
+ /**
22
+ * What an ABSENT `pds` block reports. The same sentence `requirePdsConfig`
23
+ * (`sync.ts`) has always raised for a repo that has not written one, declared
24
+ * here and imported there so the two refusals cannot drift into two different
25
+ * sentences for one situation - whichever of them an operator happens to
26
+ * reach first.
27
+ */
28
+ export const NO_PDS_SECTION_MESSAGE = 'config has no "pds" section - add it to config/production.jsonc';
29
+ /** Template pds's default `secretName` is derived from - the one home for it in this package. */
30
+ function defaultSecretName(siteName) {
31
+ return `${siteName}/atproto`;
32
+ }
33
+ /**
34
+ * Validate a raw `pds` config block, boundary-checked as `unknown` because it
35
+ * comes off `parseConfigDocument`'s `raw` half (a plugin's block has no type
36
+ * until its own package narrows it). Throws with the same message strings
37
+ * core's `validateConfig` raises today for these checks.
38
+ */
39
+ export function validatePdsConfig(raw) {
40
+ // ABSENT is not MALFORMED, and the two want different sentences. The host
41
+ // calls this with `undefined` whenever the document carries no `pds` key
42
+ // (`resolvePluginConfig`, `packages/cli/src/plugins.ts`) - deliberately, so
43
+ // a plugin with derivable defaults can supply them. This plugin has none:
44
+ // `name` is an operator's choice and nothing can invent it, so an absent
45
+ // block is a refusal. But it is the ORDINARY first-run refusal, not a
46
+ // defect in a block that was written, so it names the missing section and
47
+ // how to create it rather than a key inside a block that does not exist.
48
+ // Without this guard the first statement below dereferences `undefined` and
49
+ // the operator gets a bare `TypeError` instead (the defect task 29's
50
+ // changeset named as a known issue).
51
+ //
52
+ // `null` joins `undefined` because that is the behaviour a `"pds": null`
53
+ // document has always had: core's own check was gated behind `if (cfg.pds)`
54
+ // and skipped it, and `requirePdsConfig`'s `if (!pds)` then reported it as
55
+ // an absent section. Every other present value stays MALFORMED and falls
56
+ // through to the key-naming checks below, which is where a string, a number
57
+ // or an array already reported `config.pds.name is required`.
58
+ if (raw === undefined || raw === null)
59
+ throw new Error(NO_PDS_SECTION_MESSAGE);
60
+ const cfg = raw;
61
+ if (!cfg.name?.trim())
62
+ throw new Error('config.pds.name is required');
63
+ if (cfg.handleResolver !== undefined) {
64
+ let resolver;
65
+ try {
66
+ resolver = new URL(cfg.handleResolver);
67
+ }
68
+ catch {
69
+ throw new Error(`config.pds.handleResolver must be a URL, got "${cfg.handleResolver}"`);
70
+ }
71
+ if (resolver.protocol !== 'https:') {
72
+ throw new Error(`config.pds.handleResolver must be https, got "${cfg.handleResolver}"`);
73
+ }
74
+ }
75
+ // `secretName` is optional on `PdsConfig` now that core no longer defaults
76
+ // it, so the character class applies only to a value that is actually
77
+ // present. An absent one is not malformed - it selects the
78
+ // `<siteName>/atproto` default `resolvePdsSecretName` supplies, which
79
+ // trivially satisfies the class. The guard is a narrowing, not a behaviour
80
+ // change: the previous unguarded call handed `undefined` to `RegExp.test`,
81
+ // which coerced it to the string "undefined" and matched, so an absent
82
+ // `secretName` passed this check before the guard existed too.
83
+ if (cfg.secretName !== undefined && !SECRET_NAME_PATTERN.test(cfg.secretName)) {
84
+ throw new Error(`config.pds.secretName has invalid characters: "${cfg.secretName}"`);
85
+ }
86
+ return cfg;
87
+ }
88
+ /**
89
+ * Resolve the secret name a `pds` block uses: the explicit `secretName` when
90
+ * present, otherwise `<siteName>/atproto`.
91
+ */
92
+ export function resolvePdsSecretName(pds, siteName) {
93
+ return pds.secretName ?? defaultSecretName(siteName);
94
+ }
package/dist/content.d.ts CHANGED
@@ -4,7 +4,7 @@ export interface PostMeta {
4
4
  /**
5
5
  * Content-collection id: frontmatter `slug` when set, else the file path
6
6
  * under the content dir minus its `.md`/`.mdx` extension and any trailing
7
- * `/index` segment matching the id Astro's glob loader produces.
7
+ * `/index` segment - matching the id Astro's glob loader produces.
8
8
  */
9
9
  slug: string;
10
10
  title: string;
@@ -14,14 +14,14 @@ export interface PostMeta {
14
14
  /**
15
15
  * Parse the flat frontmatter subset used by src/content.config.ts: scalar
16
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.
17
+ * boolean. Deliberately not a YAML parser - the schema is flat by construction.
18
18
  */
19
19
  export declare function parseFrontmatter(source: string, file: string): Record<string, string>;
20
20
  /**
21
21
  * Enumerate publishable posts from the content collection, mirroring the Astro
22
22
  * glob-loader ids: `.md`/`.mdx` files, a frontmatter `slug` wins outright, a
23
23
  * trailing `/index` segment is dropped, drafts excluded. (Astro additionally
24
- * github-slugifies unusual path segments keep file names lowercase-kebab, as
24
+ * github-slugifies unusual path segments - keep file names lowercase-kebab, as
25
25
  * the rkey derived here must match the id the site builds its link tags from.)
26
26
  */
27
27
  export declare function listPublishablePosts(fs: FileSystem, repoRoot: string, contentDir?: string): Promise<PostMeta[]>;
package/dist/content.js CHANGED
@@ -5,7 +5,7 @@ const REQUIRED_FIELDS = ['title', 'description', 'pubDate'];
5
5
  /**
6
6
  * Parse the flat frontmatter subset used by src/content.config.ts: scalar
7
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.
8
+ * boolean. Deliberately not a YAML parser - the schema is flat by construction.
9
9
  */
10
10
  export function parseFrontmatter(source, file) {
11
11
  const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
@@ -34,7 +34,7 @@ export function parseFrontmatter(source, file) {
34
34
  * Enumerate publishable posts from the content collection, mirroring the Astro
35
35
  * glob-loader ids: `.md`/`.mdx` files, a frontmatter `slug` wins outright, a
36
36
  * trailing `/index` segment is dropped, drafts excluded. (Astro additionally
37
- * github-slugifies unusual path segments keep file names lowercase-kebab, as
37
+ * github-slugifies unusual path segments - keep file names lowercase-kebab, as
38
38
  * the rkey derived here must match the id the site builds its link tags from.)
39
39
  */
40
40
  export async function listPublishablePosts(fs, repoRoot, contentDir = DEFAULT_CONTENT_DIR) {
@@ -64,6 +64,6 @@ export async function listPublishablePosts(fs, repoRoot, contentDir = DEFAULT_CO
64
64
  pubDate,
65
65
  });
66
66
  }
67
- return posts.sort((a, b) => a.slug.localeCompare(b.slug));
67
+ // Codepoint sort, not localeCompare: collation must not depend on host locale/ICU.
68
+ return posts.sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
68
69
  }
69
- //# sourceMappingURL=content.js.map
package/dist/context.d.ts CHANGED
@@ -1,32 +1,42 @@
1
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.
2
+ * The narrow slice of the host CLI's context this feature package depends on,
3
+ * expressed as a narrowing of core's `PluginContext`. The CLI's `OpsContext`
4
+ * satisfies it structurally, so the dispatch boundary is a plain assignment -
5
+ * this package never imports CLI types.
5
6
  */
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;
7
+ import type { PdsConfig, PluginContext, PluginLogger, PluginPorts, SecretsManagerClient } from 'blogwright-core';
8
+ /** Leveled logger surface the pds commands report through - core's `PluginLogger`. */
9
+ export type PdsLogger = PluginLogger;
10
+ /** The ports the pds feature crosses: repo files and the operator's terminal - core's `PluginPorts`. */
11
+ export type PdsPorts = PluginPorts;
12
+ /**
13
+ * Everything a pds command needs from its host - config, secrets, ports,
14
+ * logging. A `Pick` over core's `PluginContext<PdsConfig>` of exactly the
15
+ * members pds uses today - `env`, `domain`, `config`, `ports` (already
16
+ * exactly the `fs`/`terminal` pair `PluginPorts` names), `logger` and
17
+ * optional `tags` - plus `clients`, narrowed here to the one client pds
18
+ * calls rather than picked verbatim: `PluginContext.clients` is the full
19
+ * `AwsClients`, and `test-support.ts` builds a context supplying only
20
+ * `secrets`.
21
+ *
22
+ * Everything else on `PluginContext` is left out on purpose. `preview`,
23
+ * `names` and `accountId` are host surface pds has never needed.
24
+ * `pluginConfig`, `state`, `store`, `siteState`, `record()` and `save()` are
25
+ * all built at a dispatch boundary that no pds command function runs behind:
26
+ * `deploy` calls `syncAfterDeploy(ctx)` with a plain `OpsContext`
27
+ * (`packages/cli/src/commands.ts`), which has none of them, so picking any
28
+ * of the six would break that call. The plugin's one resource node
29
+ * (`nodes.ts`) takes a full `PluginContext<PdsConfig>` instead, because only
30
+ * the lifecycle verbs have a dispatch boundary to build one.
31
+ *
32
+ * `Pick`, never `Omit`: an `Omit` of those six dispatch-boundary members
33
+ * from `PluginContext`'s sixteen would drag `preview`, `names` and
34
+ * `accountId` back in, and `createTestContext`
35
+ * (`packages/pds/src/test-support.ts`) would have to fabricate a whole
36
+ * `Names` to keep compiling.
37
+ */
38
+ export interface PdsContext extends Pick<PluginContext<PdsConfig>, 'env' | 'domain' | 'config' | 'ports' | 'logger' | 'tags'> {
25
39
  clients: {
26
40
  secrets: SecretsManagerClient;
27
41
  };
28
- ports: PdsPorts;
29
- logger: PdsLogger;
30
- /** Resource tags the host applies to created AWS resources (the secret). */
31
- tags?: Record<string, string> | undefined;
32
42
  }
package/dist/context.js CHANGED
@@ -1,7 +1,7 @@
1
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.
2
+ * The narrow slice of the host CLI's context this feature package depends on,
3
+ * expressed as a narrowing of core's `PluginContext`. The CLI's `OpsContext`
4
+ * satisfies it structurally, so the dispatch boundary is a plain assignment -
5
+ * this package never imports CLI types.
5
6
  */
6
7
  export {};
7
- //# sourceMappingURL=context.js.map
package/dist/index.d.ts CHANGED
@@ -2,6 +2,21 @@
2
2
  * Public surface of blogwright-pds: the pds command entry points the CLI
3
3
  * dispatches to, plus the context types that name the dependency boundary.
4
4
  * The rkey implementation is the separate `blogwright-pds/rkey` subpath.
5
+ *
6
+ * The default export is the package's `Plugin` declaration (`plugin.ts`) -
7
+ * the whole SPI surface in one object. It sits BESIDE the named exports, not
8
+ * instead of them, though the split is now lopsided: since task 29 deleted
9
+ * the CLI's `runPds` branch, the six command functions reach `blogwright pds
10
+ * <action>` through the default export's declared commands, not by name, and
11
+ * `syncAfterDeploy` is the one named export the CLI's shipped code still
12
+ * imports at runtime - the post-deploy sync, which has no lifecycle hook in
13
+ * the SPI to carry it (see the import comment in the CLI's `commands.ts`).
14
+ * The type exports below are load-bearing too, not dead surface: the CLI's
15
+ * `context.test.ts` imports `PdsContext` to assert `OpsContext` still
16
+ * satisfies it by plain assignment. The command functions stay exported as
17
+ * this package's own public API; `plugin.ts` builds its declarations from
18
+ * them.
5
19
  */
6
20
  export * from './commands.js';
7
21
  export type { PdsContext, PdsLogger, PdsPorts } from './context.js';
22
+ export { default } from './plugin.js';
package/dist/index.js CHANGED
@@ -2,6 +2,20 @@
2
2
  * Public surface of blogwright-pds: the pds command entry points the CLI
3
3
  * dispatches to, plus the context types that name the dependency boundary.
4
4
  * The rkey implementation is the separate `blogwright-pds/rkey` subpath.
5
+ *
6
+ * The default export is the package's `Plugin` declaration (`plugin.ts`) -
7
+ * the whole SPI surface in one object. It sits BESIDE the named exports, not
8
+ * instead of them, though the split is now lopsided: since task 29 deleted
9
+ * the CLI's `runPds` branch, the six command functions reach `blogwright pds
10
+ * <action>` through the default export's declared commands, not by name, and
11
+ * `syncAfterDeploy` is the one named export the CLI's shipped code still
12
+ * imports at runtime - the post-deploy sync, which has no lifecycle hook in
13
+ * the SPI to carry it (see the import comment in the CLI's `commands.ts`).
14
+ * The type exports below are load-bearing too, not dead surface: the CLI's
15
+ * `context.test.ts` imports `PdsContext` to assert `OpsContext` still
16
+ * satisfies it by plain assignment. The command functions stay exported as
17
+ * this package's own public API; `plugin.ts` builds its declarations from
18
+ * them.
5
19
  */
6
20
  export * from './commands.js';
7
- //# sourceMappingURL=index.js.map
21
+ export { default } from './plugin.js';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The resource topography this plugin owns: one node attaching a
3
+ * `blogwright-pds`-named **inline policy** to the site's GitHub-OIDC deploy
4
+ * role, granting Secrets Manager access to the plugin's own secret.
5
+ *
6
+ * The grant lives here rather than in the site's resource graph because it is
7
+ * plugin topography: the site graph currently branches on `ctx.config.pds` and
8
+ * interpolates this plugin's secret name into an IAM ARN
9
+ * (`packages/cli/src/nodes.ts`), which is the CLI knowing something only this
10
+ * package should know. Moving it here is safe because IAM inline policies are
11
+ * *named*: the site's `<env>-deploy` document and this plugin's
12
+ * `blogwright-pds` document are independent objects on the same role, created,
13
+ * reconciled and removed with no read of each other. That is what makes the
14
+ * migration additive - both grants are live at once until the site's statement
15
+ * is removed a release later - and it is why `delete()` can safely remove this
16
+ * document without touching the role's own.
17
+ *
18
+ * The node's own state (the site's deploy role) is not something this plugin
19
+ * creates, so `read()` never reports the role - only whether the policy this
20
+ * plugin owns is attached to it.
21
+ */
22
+ import type { PdsConfig, PluginContext, ResourceNode } from 'blogwright-core';
23
+ /**
24
+ * A node in this plugin's own graph. Instantiated on the context the
25
+ * lifecycle verbs build (`PluginContext<PdsConfig>`), not on the SPI's bare
26
+ * `ResourceNode` default (`PluginContext<never>`), so a caller - the plugin's
27
+ * `nodes(ctx)` member, or a test - can invoke a node's methods with the very
28
+ * context it was handed. `ResourceNode`'s methods are method-declared and
29
+ * therefore bivariant in their parameter, so this is still assignable to the
30
+ * `ResourceNode[]` `Plugin.nodes` declares.
31
+ */
32
+ type PdsNode = ResourceNode<PluginContext<PdsConfig>>;
33
+ /**
34
+ * The nodes `blogwright-pds` contributes, for the plugin's `nodes(ctx)` member
35
+ * to return.
36
+ *
37
+ * Empty - a no-op `pds bootstrap`, not a failure - in three cases:
38
+ *
39
+ * 1. The site configures no `pds` block.
40
+ * 2. `config.githubRepo` is unset. A non-preview site graph only adds the
41
+ * deploy role when it is set (`packages/cli/src/nodes.ts`), so such a site
42
+ * is fully bootstrapped and simply has no role to attach to.
43
+ * 3. The target environment is the shared **preview** stack. That one is a
44
+ * privilege boundary, not an absence: the preview stack's role does exist
45
+ * (`buildNodes` adds `githubOidcRoleNode(true)` for it unconditionally),
46
+ * but its OIDC trust policy accepts the subject claim `repo:<owner>/<repo>:*`
47
+ * - **any ref** - where production accepts only the release-gated
48
+ * `repo:...:environment:production` (`oidcSubClaim`). The site's own graph
49
+ * withholds exactly this Secrets Manager statement from that role for that
50
+ * reason, and `resolvePdsSecretName` is environment-independent
51
+ * (`<siteName>/atproto`), so there is ONE PDS credential for the whole
52
+ * site: granting it here would let anyone who can push a branch read or
53
+ * rotate the site's ATProto session. `staging` is NOT a preview stack -
54
+ * the site already grants it this statement - so the skip is on the
55
+ * preview environment specifically, never on "not production".
56
+ *
57
+ * The preview check is `ctx.env === PREVIEW_ENV` and deliberately **not**
58
+ * `ctx.preview`, which would be dead code here: `runPlugin` builds its
59
+ * `ContextOptions` with no `preview` key at all
60
+ * (`packages/cli/src/plugin-commands.ts`), `createContext` defaults the flag to
61
+ * `false` (`context.ts`) and `toPluginContext` copies it, so `ctx.preview` is
62
+ * `false` for every plugin context the CLI builds - including the one
63
+ * `blogwright pds bootstrap --env preview` produces. `ctx.env` is what carries
64
+ * the stack's identity into a plugin.
65
+ *
66
+ * The `pds` block is read from `ctx.config`, not `ctx.pluginConfig`: core's
67
+ * `OpsConfig` still declares `pds` as a typed member (it keeps the type after
68
+ * the validation moves here), and the CLI does not populate `pluginConfig`
69
+ * from a plugin's own validator until later in this migration.
70
+ */
71
+ export declare function buildPdsNodes(ctx: PluginContext<PdsConfig>): PdsNode[];
72
+ export {};