@valkyrianlabs/payload-markdown-docs 0.4.0 → 0.4.1

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 CHANGED
@@ -110,12 +110,20 @@ const docsLinks = await getPayloadMarkdownDocsLinks({ payload })
110
110
 
111
111
  ## Validate Locally
112
112
 
113
+ In an app or docs repository that has installed this package:
114
+
113
115
  ```bash
114
116
  pnpm exec payload-markdown-docs validate ./docs --source payload-markdown-docs
115
117
  pnpm exec payload-markdown-docs manifest ./docs --source payload-markdown-docs --pretty
116
118
  pnpm exec payload-markdown-docs plan ./docs --source payload-markdown-docs
117
119
  ```
118
120
 
121
+ From this package source checkout, use the local source CLI instead:
122
+
123
+ ```bash
124
+ pnpm cli validate ./docs --source payload-markdown-docs
125
+ ```
126
+
119
127
  In GitHub Actions, `--source` can be omitted when the docs set slug matches the
120
128
  repository name. The CLI infers it from `GITHUB_REPOSITORY`.
121
129
 
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { signDocsSyncRequest } from '../../security/index.js';
2
+ import { DocsSyncKeyError, signDocsSyncRequest } from '../../security/index.js';
3
3
  import { buildDocsManifest, sha256Hex, validateDocsManifest } from '../../sync/index.js';
4
4
  import { readDocsAiExportManifest, walkDocsFiles } from '../filesystem.js';
5
5
  import { formatIssues, formatPushSummary, printJson } from '../format.js';
@@ -258,12 +258,22 @@ export const runPushCommand = async (args, httpPost = postJson, httpGet = getJso
258
258
  }
259
259
  };
260
260
  } else {
261
- signedRequest = signDocsSyncRequest({
262
- body,
263
- endpoint: options.endpoint,
264
- keyId: options.keyId,
265
- privateKey: options.privateKey
266
- });
261
+ try {
262
+ signedRequest = signDocsSyncRequest({
263
+ body,
264
+ endpoint: options.endpoint,
265
+ keyId: options.keyId,
266
+ privateKey: options.privateKey
267
+ });
268
+ } catch (error) {
269
+ if (error instanceof DocsSyncKeyError) {
270
+ return {
271
+ exitCode: 1,
272
+ stderr: `${error.message}\n`
273
+ };
274
+ }
275
+ throw error;
276
+ }
267
277
  }
268
278
  const response = await httpPost({
269
279
  body: signedRequest.body,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/cli/commands/push.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\n\nimport type { DocsDeleteBehavior } from '../../sync/index.js'\nimport type {\n HttpGetJson,\n HttpPostJson,\n} from '../http.js'\nimport type {\n CliResult,\n ParsedCliArgs,\n PushCommandOptions,\n} from '../types.js'\n\nimport { signDocsSyncRequest } from '../../security/index.js'\nimport {\n buildDocsManifest,\n sha256Hex,\n validateDocsManifest,\n} from '../../sync/index.js'\nimport {\n readDocsAiExportManifest,\n walkDocsFiles,\n} from '../filesystem.js'\nimport { formatIssues, formatPushSummary, printJson } from '../format.js'\nimport {\n getJson,\n postJson,\n} from '../http.js'\nimport { getFlagBoolean, getFlagString } from '../parseArgs.js'\nimport { getDocsCommandOptions } from './validate.js'\n\nconst supportedPushDeleteBehaviors = new Set<DocsDeleteBehavior>([\n 'archive',\n 'delete',\n 'draft',\n 'ignore',\n])\n\ntype ServerPushResponse = {\n deleteBehavior?: string\n effectivePublishMode?: string\n error?: {\n code?: string\n message?: string\n }\n ok?: boolean\n publishRequested?: boolean\n summary?: {\n archive?: number\n create?: number\n delete?: number\n draft?: number\n unchanged?: number\n update?: number\n warnings?: number\n }\n syncRunId?: string\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst isServerPushResponse = (value: unknown): value is ServerPushResponse =>\n isRecord(value)\n\nconst validateEndpointUrl = (endpoint: string): CliResult | string => {\n try {\n const parsed = new URL(endpoint)\n\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return {\n exitCode: 1,\n stderr: '--endpoint must be a full http:// or https:// URL.\\n',\n }\n }\n\n return parsed.toString()\n } catch {\n return {\n exitCode: 1,\n stderr: '--endpoint must be a valid full http:// or https:// URL.\\n',\n }\n }\n}\n\nconst readPrivateKey = async (\n args: ParsedCliArgs,\n): Promise<CliResult | string> => {\n const privateKeyFile = getFlagString(args, 'private-key-file')\n const privateKeyEnv = getFlagString(args, 'private-key-env')\n\n if (privateKeyFile && privateKeyEnv) {\n return {\n exitCode: 1,\n stderr:\n 'Use either --private-key-file or --private-key-env, not both.\\n',\n }\n }\n\n if (!privateKeyFile && !privateKeyEnv) {\n return {\n exitCode: 1,\n stderr: 'Push requires --private-key-file or --private-key-env.\\n',\n }\n }\n\n if (privateKeyEnv) {\n const privateKey = process.env[privateKeyEnv]\n\n if (!privateKey) {\n return {\n exitCode: 1,\n stderr: `Environment variable \"${privateKeyEnv}\" is not set.\\n`,\n }\n }\n\n return privateKey\n }\n\n try {\n return await readFile(privateKeyFile ?? '', 'utf8')\n } catch (error) {\n return {\n exitCode: 1,\n stderr:\n error instanceof Error\n ? `Could not read private key file: ${error.message}\\n`\n : 'Could not read private key file.\\n',\n }\n }\n}\n\nconst getGithubOidcTokenRequestUrl = ({\n audience,\n requestUrl,\n}: {\n audience: string\n requestUrl: string\n}): CliResult | string => {\n try {\n const url = new URL(requestUrl)\n url.searchParams.set('audience', audience)\n\n return url.toString()\n } catch {\n return {\n exitCode: 1,\n stderr: 'ACTIONS_ID_TOKEN_REQUEST_URL is not a valid URL.\\n',\n }\n }\n}\n\nconst readGithubOidcToken = async ({\n args,\n audience,\n httpGet,\n}: {\n args: ParsedCliArgs\n audience: string\n httpGet: HttpGetJson\n}): Promise<CliResult | string> => {\n const tokenEnv = getFlagString(args, 'oidc-token-env')\n\n if (tokenEnv) {\n const token = process.env[tokenEnv]\n\n if (!token) {\n return {\n exitCode: 1,\n stderr: `Environment variable \"${tokenEnv}\" is not set.\\n`,\n }\n }\n\n return token\n }\n\n const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL\n const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN\n\n if (!requestUrl || !requestToken) {\n return {\n exitCode: 1,\n stderr:\n 'GitHub OIDC push requires ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN, or --oidc-token-env.\\n',\n }\n }\n\n const url = getGithubOidcTokenRequestUrl({\n audience,\n requestUrl,\n })\n\n if (typeof url !== 'string') {\n return url\n }\n\n const response = await httpGet({\n headers: {\n Authorization: `bearer ${requestToken}`,\n },\n url,\n })\n\n if (!response.ok || !isRecord(response.body) || typeof response.body.value !== 'string') {\n return {\n exitCode: 1,\n stderr: `Could not retrieve GitHub OIDC token. HTTP status ${response.status}.\\n`,\n }\n }\n\n return response.body.value\n}\n\nconst getPushCommandOptions = async (\n args: ParsedCliArgs,\n): Promise<CliResult | PushCommandOptions> => {\n const docsOptions = getDocsCommandOptions(args)\n\n if ('exitCode' in docsOptions) {\n return docsOptions\n }\n\n const endpointFlag = getFlagString(args, 'endpoint')\n\n if (!endpointFlag) {\n return {\n exitCode: 1,\n stderr: 'Push requires --endpoint <url>.\\n',\n }\n }\n\n const endpoint = validateEndpointUrl(endpointFlag)\n\n if (typeof endpoint !== 'string') {\n return endpoint\n }\n\n if (getFlagBoolean(args, 'dry-run') && getFlagBoolean(args, 'sync')) {\n return {\n exitCode: 1,\n stderr: 'Use either --dry-run or --sync, not both.\\n',\n }\n }\n\n const deleteBehaviorFlag = getFlagString(args, 'delete-behavior')\n\n if (\n deleteBehaviorFlag !== undefined &&\n !supportedPushDeleteBehaviors.has(deleteBehaviorFlag as DocsDeleteBehavior)\n ) {\n return {\n exitCode: 1,\n stderr: '--delete-behavior for push must be archive, delete, draft, or ignore.\\n',\n }\n }\n\n const mode: PushCommandOptions['mode'] = getFlagBoolean(args, 'sync')\n ? 'sync'\n : 'dry-run'\n const baseOptions = {\n ...docsOptions,\n deleteBehavior: deleteBehaviorFlag as DocsDeleteBehavior | undefined,\n endpoint,\n mode,\n publish: getFlagBoolean(args, 'publish'),\n }\n\n if (getFlagBoolean(args, 'github-oidc')) {\n if (getFlagString(args, 'key-id')) {\n return {\n exitCode: 1,\n stderr: 'Do not use --key-id with --github-oidc.\\n',\n }\n }\n\n if (getFlagString(args, 'private-key-file') || getFlagString(args, 'private-key-env')) {\n return {\n exitCode: 1,\n stderr: 'Do not use Ed25519 private key flags with --github-oidc.\\n',\n }\n }\n\n return {\n ...baseOptions,\n authMode: 'github-oidc',\n oidcTokenEnv: getFlagString(args, 'oidc-token-env'),\n }\n }\n\n const keyId = getFlagString(args, 'key-id')\n\n if (!keyId) {\n return {\n exitCode: 1,\n stderr: 'Push requires --key-id <id>.\\n',\n }\n }\n\n const privateKey = await readPrivateKey(args)\n\n if (typeof privateKey !== 'string') {\n return privateKey\n }\n\n return {\n ...baseOptions,\n authMode: 'ed25519',\n keyId,\n privateKey,\n }\n}\n\nconst formatServerFailure = ({\n body,\n status,\n}: {\n body: unknown\n status: number\n}): string => {\n if (isServerPushResponse(body) && body.error?.message) {\n return `${body.error.message}\\n`\n }\n\n return `Sync request failed with HTTP status ${status}.\\n`\n}\n\nexport const runPushCommand = async (\n args: ParsedCliArgs,\n httpPost: HttpPostJson = postJson,\n httpGet: HttpGetJson = getJson,\n): Promise<CliResult> => {\n const options = await getPushCommandOptions(args)\n\n if ('exitCode' in options) {\n return options\n }\n\n const files = await walkDocsFiles({\n root: options.docsRoot,\n })\n const aiExport = await readDocsAiExportManifest({\n root: options.docsRoot,\n })\n\n if (!aiExport.ok) {\n return {\n exitCode: 1,\n stderr: `AI export manifest is invalid.\\n\\nErrors:\\n${formatIssues(aiExport.issues)}\\n`,\n }\n }\n\n const manifest = buildDocsManifest({\n aiExport: aiExport.manifest,\n branch: options.branch,\n commit: options.commit,\n deleteBehavior: options.deleteBehavior ?? 'archive',\n files,\n mode: options.mode,\n publish: options.publish,\n repository: options.repository,\n sourceId: options.sourceId,\n })\n const validation = validateDocsManifest(manifest, {\n maxFileBytes: options.maxFileBytes,\n maxFiles: options.maxFiles,\n maxTotalBytes: options.maxTotalBytes,\n routeBase: `/${options.sourceId}`,\n })\n\n if (!validation.ok) {\n return {\n exitCode: 1,\n stderr: `Manifest is invalid.\\n\\nErrors:\\n${formatIssues(validation.issues)}\\n`,\n }\n }\n\n const body = JSON.stringify(manifest)\n let signedRequest:\n | {\n body: string\n headers: Record<string, string>\n }\n | ReturnType<typeof signDocsSyncRequest>\n\n if (options.authMode === 'github-oidc') {\n const oidcToken = await readGithubOidcToken({\n args,\n audience: options.sourceId,\n httpGet,\n })\n\n if (typeof oidcToken !== 'string') {\n return oidcToken\n }\n\n signedRequest = {\n body,\n headers: {\n Authorization: `Bearer ${oidcToken}`,\n 'Content-Type': 'application/json',\n 'X-VL-MD-DOCS-Body-SHA256': sha256Hex(body),\n },\n }\n } else {\n signedRequest = signDocsSyncRequest({\n body,\n endpoint: options.endpoint,\n keyId: options.keyId,\n privateKey: options.privateKey,\n })\n }\n\n const response = await httpPost({\n body: signedRequest.body,\n headers: signedRequest.headers,\n url: options.endpoint,\n })\n\n if (getFlagBoolean(args, 'json')) {\n return {\n exitCode:\n response.ok &&\n isServerPushResponse(response.body) &&\n response.body.ok === true\n ? 0\n : 1,\n stdout: printJson(\n {\n endpoint: options.endpoint,\n mode: options.mode,\n response: response.body,\n sourceId: options.sourceId,\n status: response.status,\n },\n getFlagBoolean(args, 'pretty'),\n ),\n }\n }\n\n if (\n !response.ok ||\n !isServerPushResponse(response.body) ||\n response.body.ok !== true\n ) {\n return {\n exitCode: 1,\n stderr: formatServerFailure({\n body: response.body,\n status: response.status,\n }),\n }\n }\n\n return {\n exitCode: 0,\n stdout: formatPushSummary({\n endpoint: options.endpoint,\n mode: options.mode,\n response: response.body,\n sourceId: options.sourceId,\n }),\n }\n}\n"],"names":["readFile","signDocsSyncRequest","buildDocsManifest","sha256Hex","validateDocsManifest","readDocsAiExportManifest","walkDocsFiles","formatIssues","formatPushSummary","printJson","getJson","postJson","getFlagBoolean","getFlagString","getDocsCommandOptions","supportedPushDeleteBehaviors","Set","isRecord","value","Array","isArray","isServerPushResponse","validateEndpointUrl","endpoint","parsed","URL","protocol","exitCode","stderr","toString","readPrivateKey","args","privateKeyFile","privateKeyEnv","privateKey","process","env","error","Error","message","getGithubOidcTokenRequestUrl","audience","requestUrl","url","searchParams","set","readGithubOidcToken","httpGet","tokenEnv","token","ACTIONS_ID_TOKEN_REQUEST_URL","requestToken","ACTIONS_ID_TOKEN_REQUEST_TOKEN","response","headers","Authorization","ok","body","status","getPushCommandOptions","docsOptions","endpointFlag","deleteBehaviorFlag","undefined","has","mode","baseOptions","deleteBehavior","publish","authMode","oidcTokenEnv","keyId","formatServerFailure","runPushCommand","httpPost","options","files","root","docsRoot","aiExport","issues","manifest","branch","commit","repository","sourceId","validation","maxFileBytes","maxFiles","maxTotalBytes","routeBase","JSON","stringify","signedRequest","oidcToken","stdout"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/cli/commands/push.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\n\nimport type { DocsDeleteBehavior } from '../../sync/index.js'\nimport type {\n HttpGetJson,\n HttpPostJson,\n} from '../http.js'\nimport type {\n CliResult,\n ParsedCliArgs,\n PushCommandOptions,\n} from '../types.js'\n\nimport {\n DocsSyncKeyError,\n signDocsSyncRequest,\n} from '../../security/index.js'\nimport {\n buildDocsManifest,\n sha256Hex,\n validateDocsManifest,\n} from '../../sync/index.js'\nimport {\n readDocsAiExportManifest,\n walkDocsFiles,\n} from '../filesystem.js'\nimport { formatIssues, formatPushSummary, printJson } from '../format.js'\nimport {\n getJson,\n postJson,\n} from '../http.js'\nimport { getFlagBoolean, getFlagString } from '../parseArgs.js'\nimport { getDocsCommandOptions } from './validate.js'\n\nconst supportedPushDeleteBehaviors = new Set<DocsDeleteBehavior>([\n 'archive',\n 'delete',\n 'draft',\n 'ignore',\n])\n\ntype ServerPushResponse = {\n deleteBehavior?: string\n effectivePublishMode?: string\n error?: {\n code?: string\n message?: string\n }\n ok?: boolean\n publishRequested?: boolean\n summary?: {\n archive?: number\n create?: number\n delete?: number\n draft?: number\n unchanged?: number\n update?: number\n warnings?: number\n }\n syncRunId?: string\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst isServerPushResponse = (value: unknown): value is ServerPushResponse =>\n isRecord(value)\n\nconst validateEndpointUrl = (endpoint: string): CliResult | string => {\n try {\n const parsed = new URL(endpoint)\n\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return {\n exitCode: 1,\n stderr: '--endpoint must be a full http:// or https:// URL.\\n',\n }\n }\n\n return parsed.toString()\n } catch {\n return {\n exitCode: 1,\n stderr: '--endpoint must be a valid full http:// or https:// URL.\\n',\n }\n }\n}\n\nconst readPrivateKey = async (\n args: ParsedCliArgs,\n): Promise<CliResult | string> => {\n const privateKeyFile = getFlagString(args, 'private-key-file')\n const privateKeyEnv = getFlagString(args, 'private-key-env')\n\n if (privateKeyFile && privateKeyEnv) {\n return {\n exitCode: 1,\n stderr:\n 'Use either --private-key-file or --private-key-env, not both.\\n',\n }\n }\n\n if (!privateKeyFile && !privateKeyEnv) {\n return {\n exitCode: 1,\n stderr: 'Push requires --private-key-file or --private-key-env.\\n',\n }\n }\n\n if (privateKeyEnv) {\n const privateKey = process.env[privateKeyEnv]\n\n if (!privateKey) {\n return {\n exitCode: 1,\n stderr: `Environment variable \"${privateKeyEnv}\" is not set.\\n`,\n }\n }\n\n return privateKey\n }\n\n try {\n return await readFile(privateKeyFile ?? '', 'utf8')\n } catch (error) {\n return {\n exitCode: 1,\n stderr:\n error instanceof Error\n ? `Could not read private key file: ${error.message}\\n`\n : 'Could not read private key file.\\n',\n }\n }\n}\n\nconst getGithubOidcTokenRequestUrl = ({\n audience,\n requestUrl,\n}: {\n audience: string\n requestUrl: string\n}): CliResult | string => {\n try {\n const url = new URL(requestUrl)\n url.searchParams.set('audience', audience)\n\n return url.toString()\n } catch {\n return {\n exitCode: 1,\n stderr: 'ACTIONS_ID_TOKEN_REQUEST_URL is not a valid URL.\\n',\n }\n }\n}\n\nconst readGithubOidcToken = async ({\n args,\n audience,\n httpGet,\n}: {\n args: ParsedCliArgs\n audience: string\n httpGet: HttpGetJson\n}): Promise<CliResult | string> => {\n const tokenEnv = getFlagString(args, 'oidc-token-env')\n\n if (tokenEnv) {\n const token = process.env[tokenEnv]\n\n if (!token) {\n return {\n exitCode: 1,\n stderr: `Environment variable \"${tokenEnv}\" is not set.\\n`,\n }\n }\n\n return token\n }\n\n const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL\n const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN\n\n if (!requestUrl || !requestToken) {\n return {\n exitCode: 1,\n stderr:\n 'GitHub OIDC push requires ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN, or --oidc-token-env.\\n',\n }\n }\n\n const url = getGithubOidcTokenRequestUrl({\n audience,\n requestUrl,\n })\n\n if (typeof url !== 'string') {\n return url\n }\n\n const response = await httpGet({\n headers: {\n Authorization: `bearer ${requestToken}`,\n },\n url,\n })\n\n if (!response.ok || !isRecord(response.body) || typeof response.body.value !== 'string') {\n return {\n exitCode: 1,\n stderr: `Could not retrieve GitHub OIDC token. HTTP status ${response.status}.\\n`,\n }\n }\n\n return response.body.value\n}\n\nconst getPushCommandOptions = async (\n args: ParsedCliArgs,\n): Promise<CliResult | PushCommandOptions> => {\n const docsOptions = getDocsCommandOptions(args)\n\n if ('exitCode' in docsOptions) {\n return docsOptions\n }\n\n const endpointFlag = getFlagString(args, 'endpoint')\n\n if (!endpointFlag) {\n return {\n exitCode: 1,\n stderr: 'Push requires --endpoint <url>.\\n',\n }\n }\n\n const endpoint = validateEndpointUrl(endpointFlag)\n\n if (typeof endpoint !== 'string') {\n return endpoint\n }\n\n if (getFlagBoolean(args, 'dry-run') && getFlagBoolean(args, 'sync')) {\n return {\n exitCode: 1,\n stderr: 'Use either --dry-run or --sync, not both.\\n',\n }\n }\n\n const deleteBehaviorFlag = getFlagString(args, 'delete-behavior')\n\n if (\n deleteBehaviorFlag !== undefined &&\n !supportedPushDeleteBehaviors.has(deleteBehaviorFlag as DocsDeleteBehavior)\n ) {\n return {\n exitCode: 1,\n stderr: '--delete-behavior for push must be archive, delete, draft, or ignore.\\n',\n }\n }\n\n const mode: PushCommandOptions['mode'] = getFlagBoolean(args, 'sync')\n ? 'sync'\n : 'dry-run'\n const baseOptions = {\n ...docsOptions,\n deleteBehavior: deleteBehaviorFlag as DocsDeleteBehavior | undefined,\n endpoint,\n mode,\n publish: getFlagBoolean(args, 'publish'),\n }\n\n if (getFlagBoolean(args, 'github-oidc')) {\n if (getFlagString(args, 'key-id')) {\n return {\n exitCode: 1,\n stderr: 'Do not use --key-id with --github-oidc.\\n',\n }\n }\n\n if (getFlagString(args, 'private-key-file') || getFlagString(args, 'private-key-env')) {\n return {\n exitCode: 1,\n stderr: 'Do not use Ed25519 private key flags with --github-oidc.\\n',\n }\n }\n\n return {\n ...baseOptions,\n authMode: 'github-oidc',\n oidcTokenEnv: getFlagString(args, 'oidc-token-env'),\n }\n }\n\n const keyId = getFlagString(args, 'key-id')\n\n if (!keyId) {\n return {\n exitCode: 1,\n stderr: 'Push requires --key-id <id>.\\n',\n }\n }\n\n const privateKey = await readPrivateKey(args)\n\n if (typeof privateKey !== 'string') {\n return privateKey\n }\n\n return {\n ...baseOptions,\n authMode: 'ed25519',\n keyId,\n privateKey,\n }\n}\n\nconst formatServerFailure = ({\n body,\n status,\n}: {\n body: unknown\n status: number\n}): string => {\n if (isServerPushResponse(body) && body.error?.message) {\n return `${body.error.message}\\n`\n }\n\n return `Sync request failed with HTTP status ${status}.\\n`\n}\n\nexport const runPushCommand = async (\n args: ParsedCliArgs,\n httpPost: HttpPostJson = postJson,\n httpGet: HttpGetJson = getJson,\n): Promise<CliResult> => {\n const options = await getPushCommandOptions(args)\n\n if ('exitCode' in options) {\n return options\n }\n\n const files = await walkDocsFiles({\n root: options.docsRoot,\n })\n const aiExport = await readDocsAiExportManifest({\n root: options.docsRoot,\n })\n\n if (!aiExport.ok) {\n return {\n exitCode: 1,\n stderr: `AI export manifest is invalid.\\n\\nErrors:\\n${formatIssues(aiExport.issues)}\\n`,\n }\n }\n\n const manifest = buildDocsManifest({\n aiExport: aiExport.manifest,\n branch: options.branch,\n commit: options.commit,\n deleteBehavior: options.deleteBehavior ?? 'archive',\n files,\n mode: options.mode,\n publish: options.publish,\n repository: options.repository,\n sourceId: options.sourceId,\n })\n const validation = validateDocsManifest(manifest, {\n maxFileBytes: options.maxFileBytes,\n maxFiles: options.maxFiles,\n maxTotalBytes: options.maxTotalBytes,\n routeBase: `/${options.sourceId}`,\n })\n\n if (!validation.ok) {\n return {\n exitCode: 1,\n stderr: `Manifest is invalid.\\n\\nErrors:\\n${formatIssues(validation.issues)}\\n`,\n }\n }\n\n const body = JSON.stringify(manifest)\n let signedRequest:\n | {\n body: string\n headers: Record<string, string>\n }\n | ReturnType<typeof signDocsSyncRequest>\n\n if (options.authMode === 'github-oidc') {\n const oidcToken = await readGithubOidcToken({\n args,\n audience: options.sourceId,\n httpGet,\n })\n\n if (typeof oidcToken !== 'string') {\n return oidcToken\n }\n\n signedRequest = {\n body,\n headers: {\n Authorization: `Bearer ${oidcToken}`,\n 'Content-Type': 'application/json',\n 'X-VL-MD-DOCS-Body-SHA256': sha256Hex(body),\n },\n }\n } else {\n try {\n signedRequest = signDocsSyncRequest({\n body,\n endpoint: options.endpoint,\n keyId: options.keyId,\n privateKey: options.privateKey,\n })\n } catch (error) {\n if (error instanceof DocsSyncKeyError) {\n return {\n exitCode: 1,\n stderr: `${error.message}\\n`,\n }\n }\n\n throw error\n }\n }\n\n const response = await httpPost({\n body: signedRequest.body,\n headers: signedRequest.headers,\n url: options.endpoint,\n })\n\n if (getFlagBoolean(args, 'json')) {\n return {\n exitCode:\n response.ok &&\n isServerPushResponse(response.body) &&\n response.body.ok === true\n ? 0\n : 1,\n stdout: printJson(\n {\n endpoint: options.endpoint,\n mode: options.mode,\n response: response.body,\n sourceId: options.sourceId,\n status: response.status,\n },\n getFlagBoolean(args, 'pretty'),\n ),\n }\n }\n\n if (\n !response.ok ||\n !isServerPushResponse(response.body) ||\n response.body.ok !== true\n ) {\n return {\n exitCode: 1,\n stderr: formatServerFailure({\n body: response.body,\n status: response.status,\n }),\n }\n }\n\n return {\n exitCode: 0,\n stdout: formatPushSummary({\n endpoint: options.endpoint,\n mode: options.mode,\n response: response.body,\n sourceId: options.sourceId,\n }),\n }\n}\n"],"names":["readFile","DocsSyncKeyError","signDocsSyncRequest","buildDocsManifest","sha256Hex","validateDocsManifest","readDocsAiExportManifest","walkDocsFiles","formatIssues","formatPushSummary","printJson","getJson","postJson","getFlagBoolean","getFlagString","getDocsCommandOptions","supportedPushDeleteBehaviors","Set","isRecord","value","Array","isArray","isServerPushResponse","validateEndpointUrl","endpoint","parsed","URL","protocol","exitCode","stderr","toString","readPrivateKey","args","privateKeyFile","privateKeyEnv","privateKey","process","env","error","Error","message","getGithubOidcTokenRequestUrl","audience","requestUrl","url","searchParams","set","readGithubOidcToken","httpGet","tokenEnv","token","ACTIONS_ID_TOKEN_REQUEST_URL","requestToken","ACTIONS_ID_TOKEN_REQUEST_TOKEN","response","headers","Authorization","ok","body","status","getPushCommandOptions","docsOptions","endpointFlag","deleteBehaviorFlag","undefined","has","mode","baseOptions","deleteBehavior","publish","authMode","oidcTokenEnv","keyId","formatServerFailure","runPushCommand","httpPost","options","files","root","docsRoot","aiExport","issues","manifest","branch","commit","repository","sourceId","validation","maxFileBytes","maxFiles","maxTotalBytes","routeBase","JSON","stringify","signedRequest","oidcToken","stdout"],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -87,7 +87,7 @@ Options:
87
87
  Options:
88
88
  --endpoint <url> Full Payload sync endpoint URL.
89
89
  --key-id <id> Server-configured Ed25519 key id.
90
- --private-key-file <path> PEM private key file from keygen.
90
+ --private-key-file <path> Private key file from keygen, or an unencrypted OpenSSH Ed25519 key.
91
91
  --private-key-env <name> Environment variable containing the private key.
92
92
  --github-oidc Use GitHub Actions OIDC bearer auth instead of Ed25519.
93
93
  --oidc-token-env <name> Environment variable containing an already-fetched OIDC token.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport type { CliCommandName, CliResult, ParsedCliArgs } from './types.js'\n\nimport { runInstallCommand } from './commands/install.js'\nimport { runKeygenCommand } from './commands/keygen.js'\nimport { runManifestCommand } from './commands/manifest.js'\nimport { runPlanCommand } from './commands/plan.js'\nimport { runPushCommand } from './commands/push.js'\nimport { runValidateCommand } from './commands/validate.js'\nimport { getFlagString, parseCliArgs } from './parseArgs.js'\n\nconst helpText = `payload-markdown-docs\n\nUsage:\n payload-markdown-docs validate <docs-root> [options]\n payload-markdown-docs manifest <docs-root> [options]\n payload-markdown-docs plan <docs-root> [options]\n payload-markdown-docs push <docs-root> [options]\n payload-markdown-docs keygen [options]\n payload-markdown-docs install skill --codex [options]\n\nCommands:\n validate Validate a local Markdown docs directory.\n manifest Print a JSON docs manifest for a local Markdown docs directory.\n plan Build a dry sync plan against optional existing docs records.\n push Sign and upload a docs manifest to a Payload sync endpoint.\n keygen Generate Ed25519 keys for signed sync.\n install Install local AI-agent guidance for docs maintenance.\n`\n\nconst commandHelp: Record<Exclude<CliCommandName, 'help'>, string> = {\n install: `payload-markdown-docs install skill --codex\n\nAliases:\n payload-markdown-docs install ai-skill --codex\n payload-markdown-docs install skill --agent codex\n\nOptions:\n --codex Install the Codex skill pack.\n --agent <codex> Agent target. Currently only codex.\n --out <path> Output directory. Defaults to .agents/skills/payload-markdown-docs.\n --docs-root <path> Docs root to mention in installed guidance. Defaults to ./docs.\n --package-manager <name> pnpm, npm, yarn, or bun. Auto-detected when omitted.\n --force Overwrite existing skill files.\n --dry-run Print planned files without writing.\n --help Show this help.\n\nInstalls local AI-agent guidance only. It does not sync docs, call Payload, or run package manager commands.\n`,\n keygen: `payload-markdown-docs keygen\n\nOptions:\n --format <pem|base64> Output key format. Defaults to pem.\n --out <dir> Write docs-sync-public.pem and docs-sync-private.pem.\n --force Overwrite existing key files when used with --out.\n --help Show this help.\n`,\n manifest: `payload-markdown-docs manifest <docs-root>\n\nOptions:\n --source <id> Docs set slug. Defaults to the GitHub repository name in GitHub Actions, otherwise local-docs.\n --repository <repo> Source repository metadata.\n --branch <branch> Source branch metadata.\n --commit <sha> Source commit metadata.\n --pretty Pretty-print JSON.\n --max-files <number> Maximum file count.\n --max-file-bytes <number> Maximum single file size.\n --max-total-bytes <number> Maximum total Markdown bytes.\n --help Show this help.\n`,\n plan: `payload-markdown-docs plan <docs-root>\n\nOptions:\n --existing <path> JSON array of existing docs records.\n --delete-behavior <value> archive, delete, draft, or ignore.\n --json Print full plan JSON.\n --pretty Pretty-print JSON output.\n --source <id> Docs set slug. Defaults to the GitHub repository name in GitHub Actions, otherwise local-docs.\n --repository <repo> Source repository metadata.\n --branch <branch> Source branch metadata.\n --commit <sha> Source commit metadata.\n --max-files <number> Maximum file count.\n --max-file-bytes <number> Maximum single file size.\n --max-total-bytes <number> Maximum total Markdown bytes.\n --help Show this help.\n`,\n push: `payload-markdown-docs push <docs-root>\n\nOptions:\n --endpoint <url> Full Payload sync endpoint URL.\n --key-id <id> Server-configured Ed25519 key id.\n --private-key-file <path> PEM private key file from keygen.\n --private-key-env <name> Environment variable containing the private key.\n --github-oidc Use GitHub Actions OIDC bearer auth instead of Ed25519.\n --oidc-token-env <name> Environment variable containing an already-fetched OIDC token.\n --dry-run Upload as dry-run mode. This is the default.\n --sync Upload as sync mode. Requires server sync.allowWrites.\n --publish Request published output. Server must allow publishing.\n --delete-behavior <value> archive, delete, draft, or ignore. Defaults to archive.\n --json Print structured JSON output.\n --pretty Pretty-print JSON output with --json.\n --source <id> Docs set slug. Defaults to the GitHub repository name in GitHub Actions, otherwise local-docs.\n --repository <repo> Source repository metadata.\n --branch <branch> Source branch metadata.\n --commit <sha> Source commit metadata.\n --max-files <number> Maximum file count.\n --max-file-bytes <number> Maximum single file size.\n --max-total-bytes <number> Maximum total Markdown bytes.\n --help Show this help.\n\nExamples:\n Ed25519:\n payload-markdown-docs push ./docs --endpoint \"$DOCS_SYNC_ENDPOINT\" --source main-docs --key-id github-actions-main --private-key-env DOCS_SYNC_PRIVATE_KEY --sync\n\n GitHub OIDC:\n payload-markdown-docs push ./docs --endpoint \"$DOCS_SYNC_ENDPOINT\" --github-oidc --sync\n\nGitHub OIDC requires workflow permissions: id-token: write and contents: read.\nHard delete requires explicit server sync.allowHardDelete. Existing collection and block targets are not supported yet.\n`,\n validate: `payload-markdown-docs validate <docs-root>\n\nOptions:\n --json Print validation JSON.\n --pretty Pretty-print JSON output.\n --source <id> Docs set slug. Defaults to the GitHub repository name in GitHub Actions, otherwise local-docs.\n --repository <repo> Source repository metadata.\n --branch <branch> Source branch metadata.\n --commit <sha> Source commit metadata.\n --max-files <number> Maximum file count.\n --max-file-bytes <number> Maximum single file size.\n --max-total-bytes <number> Maximum total Markdown bytes.\n --help Show this help.\n`,\n}\n\nconst getHelpForArgs = (args: ParsedCliArgs): string => {\n if (args.command !== 'help') {\n return commandHelp[args.command]\n }\n\n const topic = getFlagString(args, 'topic') ?? args.positionals[0]\n\n if (\n topic === 'keygen' ||\n topic === 'install' ||\n topic === 'manifest' ||\n topic === 'plan' ||\n topic === 'push' ||\n topic === 'validate'\n ) {\n return commandHelp[topic]\n }\n\n return helpText\n}\n\nexport const runCli = async (argv: string[]): Promise<CliResult> => {\n try {\n const parsed = parseCliArgs(argv)\n\n if (!parsed.ok) {\n return {\n exitCode: 1,\n stderr: `${parsed.error}\\n`,\n }\n }\n\n if (parsed.args.command === 'help' || parsed.args.flags.help === true) {\n return {\n exitCode: 0,\n stdout: getHelpForArgs(parsed.args),\n }\n }\n\n if (parsed.args.command === 'keygen') {\n return runKeygenCommand(parsed.args)\n }\n\n if (parsed.args.command === 'install') {\n return runInstallCommand(parsed.args)\n }\n\n if (parsed.args.command === 'manifest') {\n return runManifestCommand(parsed.args)\n }\n\n if (parsed.args.command === 'plan') {\n return runPlanCommand(parsed.args)\n }\n\n if (parsed.args.command === 'push') {\n return runPushCommand(parsed.args)\n }\n\n if (parsed.args.command === 'validate') {\n return runValidateCommand(parsed.args)\n }\n\n return {\n exitCode: 1,\n stderr: 'Unknown command.\\n',\n }\n } catch (error) {\n return {\n exitCode: 2,\n stderr: error instanceof Error ? `${error.message}\\n` : 'Unexpected internal error.\\n',\n }\n }\n}\n\nconst isCliEntrypoint = (): boolean => {\n if (!process.argv[1]) {\n return false\n }\n\n return fileURLToPath(import.meta.url) === path.resolve(process.argv[1])\n}\n\nif (isCliEntrypoint()) {\n const result = await runCli(process.argv.slice(2))\n\n if (result.stdout) {\n process.stdout.write(result.stdout)\n }\n\n if (result.stderr) {\n process.stderr.write(result.stderr)\n }\n\n process.exitCode = result.exitCode\n}\n"],"names":["path","fileURLToPath","runInstallCommand","runKeygenCommand","runManifestCommand","runPlanCommand","runPushCommand","runValidateCommand","getFlagString","parseCliArgs","helpText","commandHelp","install","keygen","manifest","plan","push","validate","getHelpForArgs","args","command","topic","positionals","runCli","argv","parsed","ok","exitCode","stderr","error","flags","help","stdout","Error","message","isCliEntrypoint","process","url","resolve","result","slice","write"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport type { CliCommandName, CliResult, ParsedCliArgs } from './types.js'\n\nimport { runInstallCommand } from './commands/install.js'\nimport { runKeygenCommand } from './commands/keygen.js'\nimport { runManifestCommand } from './commands/manifest.js'\nimport { runPlanCommand } from './commands/plan.js'\nimport { runPushCommand } from './commands/push.js'\nimport { runValidateCommand } from './commands/validate.js'\nimport { getFlagString, parseCliArgs } from './parseArgs.js'\n\nconst helpText = `payload-markdown-docs\n\nUsage:\n payload-markdown-docs validate <docs-root> [options]\n payload-markdown-docs manifest <docs-root> [options]\n payload-markdown-docs plan <docs-root> [options]\n payload-markdown-docs push <docs-root> [options]\n payload-markdown-docs keygen [options]\n payload-markdown-docs install skill --codex [options]\n\nCommands:\n validate Validate a local Markdown docs directory.\n manifest Print a JSON docs manifest for a local Markdown docs directory.\n plan Build a dry sync plan against optional existing docs records.\n push Sign and upload a docs manifest to a Payload sync endpoint.\n keygen Generate Ed25519 keys for signed sync.\n install Install local AI-agent guidance for docs maintenance.\n`\n\nconst commandHelp: Record<Exclude<CliCommandName, 'help'>, string> = {\n install: `payload-markdown-docs install skill --codex\n\nAliases:\n payload-markdown-docs install ai-skill --codex\n payload-markdown-docs install skill --agent codex\n\nOptions:\n --codex Install the Codex skill pack.\n --agent <codex> Agent target. Currently only codex.\n --out <path> Output directory. Defaults to .agents/skills/payload-markdown-docs.\n --docs-root <path> Docs root to mention in installed guidance. Defaults to ./docs.\n --package-manager <name> pnpm, npm, yarn, or bun. Auto-detected when omitted.\n --force Overwrite existing skill files.\n --dry-run Print planned files without writing.\n --help Show this help.\n\nInstalls local AI-agent guidance only. It does not sync docs, call Payload, or run package manager commands.\n`,\n keygen: `payload-markdown-docs keygen\n\nOptions:\n --format <pem|base64> Output key format. Defaults to pem.\n --out <dir> Write docs-sync-public.pem and docs-sync-private.pem.\n --force Overwrite existing key files when used with --out.\n --help Show this help.\n`,\n manifest: `payload-markdown-docs manifest <docs-root>\n\nOptions:\n --source <id> Docs set slug. Defaults to the GitHub repository name in GitHub Actions, otherwise local-docs.\n --repository <repo> Source repository metadata.\n --branch <branch> Source branch metadata.\n --commit <sha> Source commit metadata.\n --pretty Pretty-print JSON.\n --max-files <number> Maximum file count.\n --max-file-bytes <number> Maximum single file size.\n --max-total-bytes <number> Maximum total Markdown bytes.\n --help Show this help.\n`,\n plan: `payload-markdown-docs plan <docs-root>\n\nOptions:\n --existing <path> JSON array of existing docs records.\n --delete-behavior <value> archive, delete, draft, or ignore.\n --json Print full plan JSON.\n --pretty Pretty-print JSON output.\n --source <id> Docs set slug. Defaults to the GitHub repository name in GitHub Actions, otherwise local-docs.\n --repository <repo> Source repository metadata.\n --branch <branch> Source branch metadata.\n --commit <sha> Source commit metadata.\n --max-files <number> Maximum file count.\n --max-file-bytes <number> Maximum single file size.\n --max-total-bytes <number> Maximum total Markdown bytes.\n --help Show this help.\n`,\n push: `payload-markdown-docs push <docs-root>\n\nOptions:\n --endpoint <url> Full Payload sync endpoint URL.\n --key-id <id> Server-configured Ed25519 key id.\n --private-key-file <path> Private key file from keygen, or an unencrypted OpenSSH Ed25519 key.\n --private-key-env <name> Environment variable containing the private key.\n --github-oidc Use GitHub Actions OIDC bearer auth instead of Ed25519.\n --oidc-token-env <name> Environment variable containing an already-fetched OIDC token.\n --dry-run Upload as dry-run mode. This is the default.\n --sync Upload as sync mode. Requires server sync.allowWrites.\n --publish Request published output. Server must allow publishing.\n --delete-behavior <value> archive, delete, draft, or ignore. Defaults to archive.\n --json Print structured JSON output.\n --pretty Pretty-print JSON output with --json.\n --source <id> Docs set slug. Defaults to the GitHub repository name in GitHub Actions, otherwise local-docs.\n --repository <repo> Source repository metadata.\n --branch <branch> Source branch metadata.\n --commit <sha> Source commit metadata.\n --max-files <number> Maximum file count.\n --max-file-bytes <number> Maximum single file size.\n --max-total-bytes <number> Maximum total Markdown bytes.\n --help Show this help.\n\nExamples:\n Ed25519:\n payload-markdown-docs push ./docs --endpoint \"$DOCS_SYNC_ENDPOINT\" --source main-docs --key-id github-actions-main --private-key-env DOCS_SYNC_PRIVATE_KEY --sync\n\n GitHub OIDC:\n payload-markdown-docs push ./docs --endpoint \"$DOCS_SYNC_ENDPOINT\" --github-oidc --sync\n\nGitHub OIDC requires workflow permissions: id-token: write and contents: read.\nHard delete requires explicit server sync.allowHardDelete. Existing collection and block targets are not supported yet.\n`,\n validate: `payload-markdown-docs validate <docs-root>\n\nOptions:\n --json Print validation JSON.\n --pretty Pretty-print JSON output.\n --source <id> Docs set slug. Defaults to the GitHub repository name in GitHub Actions, otherwise local-docs.\n --repository <repo> Source repository metadata.\n --branch <branch> Source branch metadata.\n --commit <sha> Source commit metadata.\n --max-files <number> Maximum file count.\n --max-file-bytes <number> Maximum single file size.\n --max-total-bytes <number> Maximum total Markdown bytes.\n --help Show this help.\n`,\n}\n\nconst getHelpForArgs = (args: ParsedCliArgs): string => {\n if (args.command !== 'help') {\n return commandHelp[args.command]\n }\n\n const topic = getFlagString(args, 'topic') ?? args.positionals[0]\n\n if (\n topic === 'keygen' ||\n topic === 'install' ||\n topic === 'manifest' ||\n topic === 'plan' ||\n topic === 'push' ||\n topic === 'validate'\n ) {\n return commandHelp[topic]\n }\n\n return helpText\n}\n\nexport const runCli = async (argv: string[]): Promise<CliResult> => {\n try {\n const parsed = parseCliArgs(argv)\n\n if (!parsed.ok) {\n return {\n exitCode: 1,\n stderr: `${parsed.error}\\n`,\n }\n }\n\n if (parsed.args.command === 'help' || parsed.args.flags.help === true) {\n return {\n exitCode: 0,\n stdout: getHelpForArgs(parsed.args),\n }\n }\n\n if (parsed.args.command === 'keygen') {\n return runKeygenCommand(parsed.args)\n }\n\n if (parsed.args.command === 'install') {\n return runInstallCommand(parsed.args)\n }\n\n if (parsed.args.command === 'manifest') {\n return runManifestCommand(parsed.args)\n }\n\n if (parsed.args.command === 'plan') {\n return runPlanCommand(parsed.args)\n }\n\n if (parsed.args.command === 'push') {\n return runPushCommand(parsed.args)\n }\n\n if (parsed.args.command === 'validate') {\n return runValidateCommand(parsed.args)\n }\n\n return {\n exitCode: 1,\n stderr: 'Unknown command.\\n',\n }\n } catch (error) {\n return {\n exitCode: 2,\n stderr: error instanceof Error ? `${error.message}\\n` : 'Unexpected internal error.\\n',\n }\n }\n}\n\nconst isCliEntrypoint = (): boolean => {\n if (!process.argv[1]) {\n return false\n }\n\n return fileURLToPath(import.meta.url) === path.resolve(process.argv[1])\n}\n\nif (isCliEntrypoint()) {\n const result = await runCli(process.argv.slice(2))\n\n if (result.stdout) {\n process.stdout.write(result.stdout)\n }\n\n if (result.stderr) {\n process.stderr.write(result.stderr)\n }\n\n process.exitCode = result.exitCode\n}\n"],"names":["path","fileURLToPath","runInstallCommand","runKeygenCommand","runManifestCommand","runPlanCommand","runPushCommand","runValidateCommand","getFlagString","parseCliArgs","helpText","commandHelp","install","keygen","manifest","plan","push","validate","getHelpForArgs","args","command","topic","positionals","runCli","argv","parsed","ok","exitCode","stderr","error","flags","help","stdout","Error","message","isCliEntrypoint","process","url","resolve","result","slice","write"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export declare class DocsSyncKeyError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare const getEd25519PrivateKeyInput: (privateKey: string) => import("crypto").KeyObject;
5
+ export declare const getEd25519PublicKeyInput: (publicKey: string) => string | import("crypto").KeyObject;
6
+ export declare const buildOpenSshEd25519PublicKey: ({ comment, publicKey, }: {
7
+ comment?: string;
8
+ publicKey: Buffer;
9
+ }) => string;
@@ -0,0 +1,183 @@
1
+ import { createPrivateKey, createPublicKey } from 'node:crypto';
2
+ const opensshPrivateKeyBegin = '-----BEGIN OPENSSH PRIVATE KEY-----';
3
+ const opensshPrivateKeyEnd = '-----END OPENSSH PRIVATE KEY-----';
4
+ const pkcs8Ed25519PrivateKeyDerPrefix = Buffer.from('302e020100300506032b657004220420', 'hex');
5
+ const spkiEd25519PublicKeyDerPrefix = Buffer.from('302a300506032b6570032100', 'hex');
6
+ class BufferReader {
7
+ buffer;
8
+ offset = 0;
9
+ constructor(buffer){
10
+ this.buffer = buffer;
11
+ }
12
+ readBytes(length) {
13
+ if (length < 0 || this.remaining < length) {
14
+ throw new DocsSyncKeyError('OpenSSH key data is truncated.');
15
+ }
16
+ const value = this.buffer.subarray(this.offset, this.offset + length);
17
+ this.offset += length;
18
+ return value;
19
+ }
20
+ readString() {
21
+ const length = this.readUInt32();
22
+ return this.readBytes(length);
23
+ }
24
+ readUInt32() {
25
+ if (this.remaining < 4) {
26
+ throw new DocsSyncKeyError('OpenSSH key data is truncated.');
27
+ }
28
+ const value = this.buffer.readUInt32BE(this.offset);
29
+ this.offset += 4;
30
+ return value;
31
+ }
32
+ get remaining() {
33
+ return this.buffer.length - this.offset;
34
+ }
35
+ }
36
+ export class DocsSyncKeyError extends Error {
37
+ constructor(message){
38
+ super(message);
39
+ this.name = 'DocsSyncKeyError';
40
+ }
41
+ }
42
+ const packOpenSshString = (value)=>{
43
+ const buffer = typeof value === 'string' ? Buffer.from(value, 'utf8') : value;
44
+ const length = Buffer.alloc(4);
45
+ length.writeUInt32BE(buffer.length, 0);
46
+ return Buffer.concat([
47
+ length,
48
+ buffer
49
+ ]);
50
+ };
51
+ const normalizeBase64 = (value)=>value.replace(/\s+/g, '');
52
+ const createEd25519PrivateKeyFromSeed = (seed)=>{
53
+ if (seed.length !== 32) {
54
+ throw new DocsSyncKeyError('OpenSSH Ed25519 private key seed is invalid.');
55
+ }
56
+ return createPrivateKey({
57
+ type: 'pkcs8',
58
+ format: 'der',
59
+ key: Buffer.concat([
60
+ pkcs8Ed25519PrivateKeyDerPrefix,
61
+ seed
62
+ ])
63
+ });
64
+ };
65
+ const createEd25519PublicKeyFromRaw = (publicKey)=>{
66
+ if (publicKey.length !== 32) {
67
+ throw new DocsSyncKeyError('OpenSSH Ed25519 public key is invalid.');
68
+ }
69
+ return createPublicKey({
70
+ type: 'spki',
71
+ format: 'der',
72
+ key: Buffer.concat([
73
+ spkiEd25519PublicKeyDerPrefix,
74
+ publicKey
75
+ ])
76
+ });
77
+ };
78
+ const parseOpenSshPrivateKey = (privateKey)=>{
79
+ const begin = privateKey.indexOf(opensshPrivateKeyBegin);
80
+ const end = privateKey.indexOf(opensshPrivateKeyEnd);
81
+ if (begin < 0 || end < 0 || end <= begin) {
82
+ throw new DocsSyncKeyError('OpenSSH private key PEM is invalid.');
83
+ }
84
+ const base64Body = privateKey.slice(begin + opensshPrivateKeyBegin.length, end);
85
+ const data = Buffer.from(normalizeBase64(base64Body), 'base64');
86
+ const authMagic = Buffer.from('openssh-key-v1\0', 'utf8');
87
+ if (!data.subarray(0, authMagic.length).equals(authMagic)) {
88
+ throw new DocsSyncKeyError('OpenSSH private key magic header is invalid.');
89
+ }
90
+ const reader = new BufferReader(data.subarray(authMagic.length));
91
+ const cipherName = reader.readString().toString('utf8');
92
+ const kdfName = reader.readString().toString('utf8');
93
+ reader.readString();
94
+ if (cipherName !== 'none' || kdfName !== 'none') {
95
+ throw new DocsSyncKeyError('Encrypted OpenSSH private keys are not supported. Use `payload-markdown-docs keygen --out .docs-sync` or provide an unencrypted PKCS#8 PEM Ed25519 private key.');
96
+ }
97
+ const keyCount = reader.readUInt32();
98
+ if (keyCount !== 1) {
99
+ throw new DocsSyncKeyError('OpenSSH private key must contain exactly one key.');
100
+ }
101
+ reader.readString();
102
+ const privateBlob = reader.readString();
103
+ const privateReader = new BufferReader(privateBlob);
104
+ const checkInt = privateReader.readUInt32();
105
+ const repeatedCheckInt = privateReader.readUInt32();
106
+ if (checkInt !== repeatedCheckInt) {
107
+ throw new DocsSyncKeyError('OpenSSH private key check values do not match.');
108
+ }
109
+ const keyType = privateReader.readString().toString('utf8');
110
+ if (keyType !== 'ssh-ed25519') {
111
+ throw new DocsSyncKeyError('Only Ed25519 private keys are supported for docs sync signing.');
112
+ }
113
+ const publicKey = privateReader.readString();
114
+ const privateKeyBytes = privateReader.readString();
115
+ if (privateKeyBytes.length !== 64) {
116
+ throw new DocsSyncKeyError('OpenSSH Ed25519 private key payload is invalid.');
117
+ }
118
+ if (!privateKeyBytes.subarray(32).equals(publicKey)) {
119
+ throw new DocsSyncKeyError('OpenSSH Ed25519 private/public key data does not match.');
120
+ }
121
+ return createEd25519PrivateKeyFromSeed(privateKeyBytes.subarray(0, 32));
122
+ };
123
+ const parseOpenSshPublicKey = (publicKey)=>{
124
+ const [keyType, base64Key] = publicKey.trim().split(/\s+/, 3);
125
+ if (keyType !== 'ssh-ed25519' || !base64Key) {
126
+ throw new DocsSyncKeyError('Only Ed25519 public keys are supported for docs sync verification.');
127
+ }
128
+ const reader = new BufferReader(Buffer.from(base64Key, 'base64'));
129
+ const parsedKeyType = reader.readString().toString('utf8');
130
+ if (parsedKeyType !== 'ssh-ed25519') {
131
+ throw new DocsSyncKeyError('OpenSSH public key type does not match ssh-ed25519.');
132
+ }
133
+ return createEd25519PublicKeyFromRaw(reader.readString());
134
+ };
135
+ export const getEd25519PrivateKeyInput = (privateKey)=>{
136
+ const trimmed = privateKey.trim();
137
+ if (trimmed.includes('BEGIN OPENSSH PRIVATE KEY')) {
138
+ return parseOpenSshPrivateKey(trimmed);
139
+ }
140
+ if (trimmed.includes('BEGIN')) {
141
+ try {
142
+ return createPrivateKey(trimmed);
143
+ } catch {
144
+ throw new DocsSyncKeyError('Private key must be an Ed25519 PKCS#8 PEM key, base64 PKCS#8 DER key, or unencrypted OpenSSH Ed25519 private key.');
145
+ }
146
+ }
147
+ try {
148
+ return createPrivateKey({
149
+ type: 'pkcs8',
150
+ format: 'der',
151
+ key: Buffer.from(normalizeBase64(trimmed), 'base64')
152
+ });
153
+ } catch {
154
+ throw new DocsSyncKeyError('Private key must be an Ed25519 PKCS#8 PEM key, base64 PKCS#8 DER key, or unencrypted OpenSSH Ed25519 private key.');
155
+ }
156
+ };
157
+ export const getEd25519PublicKeyInput = (publicKey)=>{
158
+ const trimmed = publicKey.trim();
159
+ if (trimmed.startsWith('ssh-ed25519 ')) {
160
+ return parseOpenSshPublicKey(trimmed);
161
+ }
162
+ if (trimmed.includes('BEGIN PUBLIC KEY')) {
163
+ return trimmed;
164
+ }
165
+ return createPublicKey({
166
+ type: 'spki',
167
+ format: 'der',
168
+ key: Buffer.from(normalizeBase64(trimmed), 'base64')
169
+ });
170
+ };
171
+ export const buildOpenSshEd25519PublicKey = ({ comment, publicKey })=>{
172
+ const blob = Buffer.concat([
173
+ packOpenSshString('ssh-ed25519'),
174
+ packOpenSshString(publicKey)
175
+ ]);
176
+ return [
177
+ 'ssh-ed25519',
178
+ blob.toString('base64'),
179
+ comment
180
+ ].filter(Boolean).join(' ');
181
+ };
182
+
183
+ //# sourceMappingURL=ed25519Keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/security/ed25519Keys.ts"],"sourcesContent":["import {\n createPrivateKey,\n createPublicKey,\n} from 'node:crypto'\n\nconst opensshPrivateKeyBegin = '-----BEGIN OPENSSH PRIVATE KEY-----'\nconst opensshPrivateKeyEnd = '-----END OPENSSH PRIVATE KEY-----'\nconst pkcs8Ed25519PrivateKeyDerPrefix = Buffer.from(\n '302e020100300506032b657004220420',\n 'hex',\n)\nconst spkiEd25519PublicKeyDerPrefix = Buffer.from(\n '302a300506032b6570032100',\n 'hex',\n)\n\nclass BufferReader {\n private offset = 0\n\n constructor(private readonly buffer: Buffer) {}\n\n readBytes(length: number): Buffer {\n if (length < 0 || this.remaining < length) {\n throw new DocsSyncKeyError('OpenSSH key data is truncated.')\n }\n\n const value = this.buffer.subarray(this.offset, this.offset + length)\n this.offset += length\n\n return value\n }\n\n readString(): Buffer {\n const length = this.readUInt32()\n\n return this.readBytes(length)\n }\n\n readUInt32(): number {\n if (this.remaining < 4) {\n throw new DocsSyncKeyError('OpenSSH key data is truncated.')\n }\n\n const value = this.buffer.readUInt32BE(this.offset)\n this.offset += 4\n\n return value\n }\n\n get remaining(): number {\n return this.buffer.length - this.offset\n }\n}\n\nexport class DocsSyncKeyError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'DocsSyncKeyError'\n }\n}\n\nconst packOpenSshString = (value: Buffer | string): Buffer => {\n const buffer = typeof value === 'string' ? Buffer.from(value, 'utf8') : value\n const length = Buffer.alloc(4)\n length.writeUInt32BE(buffer.length, 0)\n\n return Buffer.concat([length, buffer])\n}\n\nconst normalizeBase64 = (value: string): string => value.replace(/\\s+/g, '')\n\nconst createEd25519PrivateKeyFromSeed = (seed: Buffer) => {\n if (seed.length !== 32) {\n throw new DocsSyncKeyError('OpenSSH Ed25519 private key seed is invalid.')\n }\n\n return createPrivateKey({\n type: 'pkcs8',\n format: 'der',\n key: Buffer.concat([pkcs8Ed25519PrivateKeyDerPrefix, seed]),\n })\n}\n\nconst createEd25519PublicKeyFromRaw = (publicKey: Buffer) => {\n if (publicKey.length !== 32) {\n throw new DocsSyncKeyError('OpenSSH Ed25519 public key is invalid.')\n }\n\n return createPublicKey({\n type: 'spki',\n format: 'der',\n key: Buffer.concat([spkiEd25519PublicKeyDerPrefix, publicKey]),\n })\n}\n\nconst parseOpenSshPrivateKey = (privateKey: string) => {\n const begin = privateKey.indexOf(opensshPrivateKeyBegin)\n const end = privateKey.indexOf(opensshPrivateKeyEnd)\n\n if (begin < 0 || end < 0 || end <= begin) {\n throw new DocsSyncKeyError('OpenSSH private key PEM is invalid.')\n }\n\n const base64Body = privateKey.slice(begin + opensshPrivateKeyBegin.length, end)\n const data = Buffer.from(normalizeBase64(base64Body), 'base64')\n const authMagic = Buffer.from('openssh-key-v1\\0', 'utf8')\n\n if (!data.subarray(0, authMagic.length).equals(authMagic)) {\n throw new DocsSyncKeyError('OpenSSH private key magic header is invalid.')\n }\n\n const reader = new BufferReader(data.subarray(authMagic.length))\n const cipherName = reader.readString().toString('utf8')\n const kdfName = reader.readString().toString('utf8')\n reader.readString()\n\n if (cipherName !== 'none' || kdfName !== 'none') {\n throw new DocsSyncKeyError(\n 'Encrypted OpenSSH private keys are not supported. Use `payload-markdown-docs keygen --out .docs-sync` or provide an unencrypted PKCS#8 PEM Ed25519 private key.',\n )\n }\n\n const keyCount = reader.readUInt32()\n\n if (keyCount !== 1) {\n throw new DocsSyncKeyError('OpenSSH private key must contain exactly one key.')\n }\n\n reader.readString()\n const privateBlob = reader.readString()\n const privateReader = new BufferReader(privateBlob)\n const checkInt = privateReader.readUInt32()\n const repeatedCheckInt = privateReader.readUInt32()\n\n if (checkInt !== repeatedCheckInt) {\n throw new DocsSyncKeyError('OpenSSH private key check values do not match.')\n }\n\n const keyType = privateReader.readString().toString('utf8')\n\n if (keyType !== 'ssh-ed25519') {\n throw new DocsSyncKeyError(\n 'Only Ed25519 private keys are supported for docs sync signing.',\n )\n }\n\n const publicKey = privateReader.readString()\n const privateKeyBytes = privateReader.readString()\n\n if (privateKeyBytes.length !== 64) {\n throw new DocsSyncKeyError('OpenSSH Ed25519 private key payload is invalid.')\n }\n\n if (!privateKeyBytes.subarray(32).equals(publicKey)) {\n throw new DocsSyncKeyError('OpenSSH Ed25519 private/public key data does not match.')\n }\n\n return createEd25519PrivateKeyFromSeed(privateKeyBytes.subarray(0, 32))\n}\n\nconst parseOpenSshPublicKey = (publicKey: string) => {\n const [keyType, base64Key] = publicKey.trim().split(/\\s+/, 3)\n\n if (keyType !== 'ssh-ed25519' || !base64Key) {\n throw new DocsSyncKeyError(\n 'Only Ed25519 public keys are supported for docs sync verification.',\n )\n }\n\n const reader = new BufferReader(Buffer.from(base64Key, 'base64'))\n const parsedKeyType = reader.readString().toString('utf8')\n\n if (parsedKeyType !== 'ssh-ed25519') {\n throw new DocsSyncKeyError('OpenSSH public key type does not match ssh-ed25519.')\n }\n\n return createEd25519PublicKeyFromRaw(reader.readString())\n}\n\nexport const getEd25519PrivateKeyInput = (privateKey: string) => {\n const trimmed = privateKey.trim()\n\n if (trimmed.includes('BEGIN OPENSSH PRIVATE KEY')) {\n return parseOpenSshPrivateKey(trimmed)\n }\n\n if (trimmed.includes('BEGIN')) {\n try {\n return createPrivateKey(trimmed)\n } catch {\n throw new DocsSyncKeyError(\n 'Private key must be an Ed25519 PKCS#8 PEM key, base64 PKCS#8 DER key, or unencrypted OpenSSH Ed25519 private key.',\n )\n }\n }\n\n try {\n return createPrivateKey({\n type: 'pkcs8',\n format: 'der',\n key: Buffer.from(normalizeBase64(trimmed), 'base64'),\n })\n } catch {\n throw new DocsSyncKeyError(\n 'Private key must be an Ed25519 PKCS#8 PEM key, base64 PKCS#8 DER key, or unencrypted OpenSSH Ed25519 private key.',\n )\n }\n}\n\nexport const getEd25519PublicKeyInput = (publicKey: string) => {\n const trimmed = publicKey.trim()\n\n if (trimmed.startsWith('ssh-ed25519 ')) {\n return parseOpenSshPublicKey(trimmed)\n }\n\n if (trimmed.includes('BEGIN PUBLIC KEY')) {\n return trimmed\n }\n\n return createPublicKey({\n type: 'spki',\n format: 'der',\n key: Buffer.from(normalizeBase64(trimmed), 'base64'),\n })\n}\n\nexport const buildOpenSshEd25519PublicKey = ({\n comment,\n publicKey,\n}: {\n comment?: string\n publicKey: Buffer\n}): string => {\n const blob = Buffer.concat([\n packOpenSshString('ssh-ed25519'),\n packOpenSshString(publicKey),\n ])\n\n return ['ssh-ed25519', blob.toString('base64'), comment].filter(Boolean).join(' ')\n}\n"],"names":["createPrivateKey","createPublicKey","opensshPrivateKeyBegin","opensshPrivateKeyEnd","pkcs8Ed25519PrivateKeyDerPrefix","Buffer","from","spkiEd25519PublicKeyDerPrefix","BufferReader","offset","buffer","readBytes","length","remaining","DocsSyncKeyError","value","subarray","readString","readUInt32","readUInt32BE","Error","message","name","packOpenSshString","alloc","writeUInt32BE","concat","normalizeBase64","replace","createEd25519PrivateKeyFromSeed","seed","type","format","key","createEd25519PublicKeyFromRaw","publicKey","parseOpenSshPrivateKey","privateKey","begin","indexOf","end","base64Body","slice","data","authMagic","equals","reader","cipherName","toString","kdfName","keyCount","privateBlob","privateReader","checkInt","repeatedCheckInt","keyType","privateKeyBytes","parseOpenSshPublicKey","base64Key","trim","split","parsedKeyType","getEd25519PrivateKeyInput","trimmed","includes","getEd25519PublicKeyInput","startsWith","buildOpenSshEd25519PublicKey","comment","blob","filter","Boolean","join"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  export { buildCanonicalSigningString, getCanonicalPathFromRequestUrl, } from './canonical.js';
2
2
  export type { CanonicalSigningStringInput } from './canonical.js';
3
+ export { buildOpenSshEd25519PublicKey, DocsSyncKeyError, getEd25519PrivateKeyInput, getEd25519PublicKeyInput, } from './ed25519Keys.js';
3
4
  export { verifyGitHubOidcToken } from './githubOidc.js';
4
5
  export type { GitHubOidcClaims, GitHubOidcErrorCode, GitHubOidcTrustedSource, GitHubOidcVerifyConfig, VerifiedGitHubOidcToken, VerifyGitHubOidcTokenResult, } from './githubOidc.js';
5
6
  export { extractSyncRequestHeaders, syncHeaderNames } from './headers.js';
@@ -1,4 +1,5 @@
1
1
  export { buildCanonicalSigningString, getCanonicalPathFromRequestUrl } from './canonical.js';
2
+ export { buildOpenSshEd25519PublicKey, DocsSyncKeyError, getEd25519PrivateKeyInput, getEd25519PublicKeyInput } from './ed25519Keys.js';
2
3
  export { verifyGitHubOidcToken } from './githubOidc.js';
3
4
  export { extractSyncRequestHeaders, syncHeaderNames } from './headers.js';
4
5
  export { decodeJwt, toBase64Url } from './jwt.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/security/index.ts"],"sourcesContent":["export {\n buildCanonicalSigningString,\n getCanonicalPathFromRequestUrl,\n} from './canonical.js'\nexport type { CanonicalSigningStringInput } from './canonical.js'\nexport { verifyGitHubOidcToken } from './githubOidc.js'\nexport type {\n GitHubOidcClaims,\n GitHubOidcErrorCode,\n GitHubOidcTrustedSource,\n GitHubOidcVerifyConfig,\n VerifiedGitHubOidcToken,\n VerifyGitHubOidcTokenResult,\n} from './githubOidc.js'\nexport { extractSyncRequestHeaders, syncHeaderNames } from './headers.js'\nexport type {\n ExtractSyncHeadersResult,\n SyncRequestHeaders,\n} from './headers.js'\nexport type { FetchJson } from './jwks.js'\nexport {\n decodeJwt,\n toBase64Url,\n} from './jwt.js'\nexport type { DecodedJwt } from './jwt.js'\nexport {\n assertNonceNotReplayed,\n storeAcceptedNonce,\n} from './nonce.js'\nexport type { NoncePayloadOperations } from './nonce.js'\nexport { signDocsSyncRequest } from './sign.js'\nexport type {\n SignDocsSyncRequestOptions,\n SignedDocsSyncRequest,\n} from './sign.js'\nexport {\n validateTimestampSkew,\n verifyBodySha256,\n verifyEd25519Signature,\n} from './verify.js'\nexport type {\n ValidateTimestampResult,\n VerifyBodyHashResult,\n} from './verify.js'\n"],"names":["buildCanonicalSigningString","getCanonicalPathFromRequestUrl","verifyGitHubOidcToken","extractSyncRequestHeaders","syncHeaderNames","decodeJwt","toBase64Url","assertNonceNotReplayed","storeAcceptedNonce","signDocsSyncRequest","validateTimestampSkew","verifyBodySha256","verifyEd25519Signature"],"mappings":"AAAA,SACEA,2BAA2B,EAC3BC,8BAA8B,QACzB,iBAAgB;AAEvB,SAASC,qBAAqB,QAAQ,kBAAiB;AASvD,SAASC,yBAAyB,EAAEC,eAAe,QAAQ,eAAc;AAMzE,SACEC,SAAS,EACTC,WAAW,QACN,WAAU;AAEjB,SACEC,sBAAsB,EACtBC,kBAAkB,QACb,aAAY;AAEnB,SAASC,mBAAmB,QAAQ,YAAW;AAK/C,SACEC,qBAAqB,EACrBC,gBAAgB,EAChBC,sBAAsB,QACjB,cAAa"}
1
+ {"version":3,"sources":["../../src/security/index.ts"],"sourcesContent":["export {\n buildCanonicalSigningString,\n getCanonicalPathFromRequestUrl,\n} from './canonical.js'\nexport type { CanonicalSigningStringInput } from './canonical.js'\nexport {\n buildOpenSshEd25519PublicKey,\n DocsSyncKeyError,\n getEd25519PrivateKeyInput,\n getEd25519PublicKeyInput,\n} from './ed25519Keys.js'\nexport { verifyGitHubOidcToken } from './githubOidc.js'\nexport type {\n GitHubOidcClaims,\n GitHubOidcErrorCode,\n GitHubOidcTrustedSource,\n GitHubOidcVerifyConfig,\n VerifiedGitHubOidcToken,\n VerifyGitHubOidcTokenResult,\n} from './githubOidc.js'\nexport { extractSyncRequestHeaders, syncHeaderNames } from './headers.js'\nexport type {\n ExtractSyncHeadersResult,\n SyncRequestHeaders,\n} from './headers.js'\nexport type { FetchJson } from './jwks.js'\nexport {\n decodeJwt,\n toBase64Url,\n} from './jwt.js'\nexport type { DecodedJwt } from './jwt.js'\nexport {\n assertNonceNotReplayed,\n storeAcceptedNonce,\n} from './nonce.js'\nexport type { NoncePayloadOperations } from './nonce.js'\nexport { signDocsSyncRequest } from './sign.js'\nexport type {\n SignDocsSyncRequestOptions,\n SignedDocsSyncRequest,\n} from './sign.js'\nexport {\n validateTimestampSkew,\n verifyBodySha256,\n verifyEd25519Signature,\n} from './verify.js'\nexport type {\n ValidateTimestampResult,\n VerifyBodyHashResult,\n} from './verify.js'\n"],"names":["buildCanonicalSigningString","getCanonicalPathFromRequestUrl","buildOpenSshEd25519PublicKey","DocsSyncKeyError","getEd25519PrivateKeyInput","getEd25519PublicKeyInput","verifyGitHubOidcToken","extractSyncRequestHeaders","syncHeaderNames","decodeJwt","toBase64Url","assertNonceNotReplayed","storeAcceptedNonce","signDocsSyncRequest","validateTimestampSkew","verifyBodySha256","verifyEd25519Signature"],"mappings":"AAAA,SACEA,2BAA2B,EAC3BC,8BAA8B,QACzB,iBAAgB;AAEvB,SACEC,4BAA4B,EAC5BC,gBAAgB,EAChBC,yBAAyB,EACzBC,wBAAwB,QACnB,mBAAkB;AACzB,SAASC,qBAAqB,QAAQ,kBAAiB;AASvD,SAASC,yBAAyB,EAAEC,eAAe,QAAQ,eAAc;AAMzE,SACEC,SAAS,EACTC,WAAW,QACN,WAAU;AAEjB,SACEC,sBAAsB,EACtBC,kBAAkB,QACb,aAAY;AAEnB,SAASC,mBAAmB,QAAQ,YAAW;AAK/C,SACEC,qBAAqB,EACrBC,gBAAgB,EAChBC,sBAAsB,QACjB,cAAa"}
@@ -1,16 +1,7 @@
1
- import { createPrivateKey, randomUUID, sign } from 'node:crypto';
1
+ import { randomUUID, sign } from 'node:crypto';
2
2
  import { sha256Hex } from '../sync/index.js';
3
3
  import { buildCanonicalSigningString } from './canonical.js';
4
- const getPrivateKeyInput = (privateKey)=>{
5
- if (privateKey.includes('BEGIN PRIVATE KEY')) {
6
- return privateKey;
7
- }
8
- return createPrivateKey({
9
- type: 'pkcs8',
10
- format: 'der',
11
- key: Buffer.from(privateKey, 'base64')
12
- });
13
- };
4
+ import { getEd25519PrivateKeyInput } from './ed25519Keys.js';
14
5
  const getEndpointPathname = (endpoint)=>new URL(endpoint).pathname;
15
6
  export const signDocsSyncRequest = ({ body, endpoint, keyId, nonce = randomUUID(), now = new Date(), privateKey })=>{
16
7
  const bodySha256 = sha256Hex(body);
@@ -22,7 +13,7 @@ export const signDocsSyncRequest = ({ body, endpoint, keyId, nonce = randomUUID(
22
13
  path: getEndpointPathname(endpoint),
23
14
  timestamp
24
15
  });
25
- const signature = sign(null, Buffer.from(canonicalString, 'utf8'), getPrivateKeyInput(privateKey)).toString('base64');
16
+ const signature = sign(null, Buffer.from(canonicalString, 'utf8'), getEd25519PrivateKeyInput(privateKey)).toString('base64');
26
17
  return {
27
18
  body,
28
19
  headers: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/security/sign.ts"],"sourcesContent":["import {\n createPrivateKey,\n randomUUID,\n sign,\n} from 'node:crypto'\n\nimport { sha256Hex } from '../sync/index.js'\nimport { buildCanonicalSigningString } from './canonical.js'\n\nexport type SignDocsSyncRequestOptions = {\n body: string\n endpoint: string\n keyId: string\n nonce?: string\n now?: Date\n privateKey: string\n}\n\nexport type SignedDocsSyncRequest = {\n body: string\n headers: Record<string, string>\n}\n\nconst getPrivateKeyInput = (privateKey: string) => {\n if (privateKey.includes('BEGIN PRIVATE KEY')) {\n return privateKey\n }\n\n return createPrivateKey({\n type: 'pkcs8',\n format: 'der',\n key: Buffer.from(privateKey, 'base64'),\n })\n}\n\nconst getEndpointPathname = (endpoint: string): string => new URL(endpoint).pathname\n\nexport const signDocsSyncRequest = ({\n body,\n endpoint,\n keyId,\n nonce = randomUUID(),\n now = new Date(),\n privateKey,\n}: SignDocsSyncRequestOptions): SignedDocsSyncRequest => {\n const bodySha256 = sha256Hex(body)\n const timestamp = now.toISOString()\n const canonicalString = buildCanonicalSigningString({\n bodySha256,\n method: 'POST',\n nonce,\n path: getEndpointPathname(endpoint),\n timestamp,\n })\n const signature = sign(\n null,\n Buffer.from(canonicalString, 'utf8'),\n getPrivateKeyInput(privateKey),\n ).toString('base64')\n\n return {\n body,\n headers: {\n 'Content-Type': 'application/json',\n 'X-VL-MD-DOCS-Body-SHA256': bodySha256,\n 'X-VL-MD-DOCS-Key-Id': keyId,\n 'X-VL-MD-DOCS-Nonce': nonce,\n 'X-VL-MD-DOCS-Signature': signature,\n 'X-VL-MD-DOCS-Timestamp': timestamp,\n },\n }\n}\n"],"names":["createPrivateKey","randomUUID","sign","sha256Hex","buildCanonicalSigningString","getPrivateKeyInput","privateKey","includes","type","format","key","Buffer","from","getEndpointPathname","endpoint","URL","pathname","signDocsSyncRequest","body","keyId","nonce","now","Date","bodySha256","timestamp","toISOString","canonicalString","method","path","signature","toString","headers"],"mappings":"AAAA,SACEA,gBAAgB,EAChBC,UAAU,EACVC,IAAI,QACC,cAAa;AAEpB,SAASC,SAAS,QAAQ,mBAAkB;AAC5C,SAASC,2BAA2B,QAAQ,iBAAgB;AAgB5D,MAAMC,qBAAqB,CAACC;IAC1B,IAAIA,WAAWC,QAAQ,CAAC,sBAAsB;QAC5C,OAAOD;IACT;IAEA,OAAON,iBAAiB;QACtBQ,MAAM;QACNC,QAAQ;QACRC,KAAKC,OAAOC,IAAI,CAACN,YAAY;IAC/B;AACF;AAEA,MAAMO,sBAAsB,CAACC,WAA6B,IAAIC,IAAID,UAAUE,QAAQ;AAEpF,OAAO,MAAMC,sBAAsB,CAAC,EAClCC,IAAI,EACJJ,QAAQ,EACRK,KAAK,EACLC,QAAQnB,YAAY,EACpBoB,MAAM,IAAIC,MAAM,EAChBhB,UAAU,EACiB;IAC3B,MAAMiB,aAAapB,UAAUe;IAC7B,MAAMM,YAAYH,IAAII,WAAW;IACjC,MAAMC,kBAAkBtB,4BAA4B;QAClDmB;QACAI,QAAQ;QACRP;QACAQ,MAAMf,oBAAoBC;QAC1BU;IACF;IACA,MAAMK,YAAY3B,KAChB,MACAS,OAAOC,IAAI,CAACc,iBAAiB,SAC7BrB,mBAAmBC,aACnBwB,QAAQ,CAAC;IAEX,OAAO;QACLZ;QACAa,SAAS;YACP,gBAAgB;YAChB,4BAA4BR;YAC5B,uBAAuBJ;YACvB,sBAAsBC;YACtB,0BAA0BS;YAC1B,0BAA0BL;QAC5B;IACF;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/security/sign.ts"],"sourcesContent":["import {\n randomUUID,\n sign,\n} from 'node:crypto'\n\nimport { sha256Hex } from '../sync/index.js'\nimport { buildCanonicalSigningString } from './canonical.js'\nimport { getEd25519PrivateKeyInput } from './ed25519Keys.js'\n\nexport type SignDocsSyncRequestOptions = {\n body: string\n endpoint: string\n keyId: string\n nonce?: string\n now?: Date\n privateKey: string\n}\n\nexport type SignedDocsSyncRequest = {\n body: string\n headers: Record<string, string>\n}\n\nconst getEndpointPathname = (endpoint: string): string => new URL(endpoint).pathname\n\nexport const signDocsSyncRequest = ({\n body,\n endpoint,\n keyId,\n nonce = randomUUID(),\n now = new Date(),\n privateKey,\n}: SignDocsSyncRequestOptions): SignedDocsSyncRequest => {\n const bodySha256 = sha256Hex(body)\n const timestamp = now.toISOString()\n const canonicalString = buildCanonicalSigningString({\n bodySha256,\n method: 'POST',\n nonce,\n path: getEndpointPathname(endpoint),\n timestamp,\n })\n const signature = sign(\n null,\n Buffer.from(canonicalString, 'utf8'),\n getEd25519PrivateKeyInput(privateKey),\n ).toString('base64')\n\n return {\n body,\n headers: {\n 'Content-Type': 'application/json',\n 'X-VL-MD-DOCS-Body-SHA256': bodySha256,\n 'X-VL-MD-DOCS-Key-Id': keyId,\n 'X-VL-MD-DOCS-Nonce': nonce,\n 'X-VL-MD-DOCS-Signature': signature,\n 'X-VL-MD-DOCS-Timestamp': timestamp,\n },\n }\n}\n"],"names":["randomUUID","sign","sha256Hex","buildCanonicalSigningString","getEd25519PrivateKeyInput","getEndpointPathname","endpoint","URL","pathname","signDocsSyncRequest","body","keyId","nonce","now","Date","privateKey","bodySha256","timestamp","toISOString","canonicalString","method","path","signature","Buffer","from","toString","headers"],"mappings":"AAAA,SACEA,UAAU,EACVC,IAAI,QACC,cAAa;AAEpB,SAASC,SAAS,QAAQ,mBAAkB;AAC5C,SAASC,2BAA2B,QAAQ,iBAAgB;AAC5D,SAASC,yBAAyB,QAAQ,mBAAkB;AAgB5D,MAAMC,sBAAsB,CAACC,WAA6B,IAAIC,IAAID,UAAUE,QAAQ;AAEpF,OAAO,MAAMC,sBAAsB,CAAC,EAClCC,IAAI,EACJJ,QAAQ,EACRK,KAAK,EACLC,QAAQZ,YAAY,EACpBa,MAAM,IAAIC,MAAM,EAChBC,UAAU,EACiB;IAC3B,MAAMC,aAAad,UAAUQ;IAC7B,MAAMO,YAAYJ,IAAIK,WAAW;IACjC,MAAMC,kBAAkBhB,4BAA4B;QAClDa;QACAI,QAAQ;QACRR;QACAS,MAAMhB,oBAAoBC;QAC1BW;IACF;IACA,MAAMK,YAAYrB,KAChB,MACAsB,OAAOC,IAAI,CAACL,iBAAiB,SAC7Bf,0BAA0BW,aAC1BU,QAAQ,CAAC;IAEX,OAAO;QACLf;QACAgB,SAAS;YACP,gBAAgB;YAChB,4BAA4BV;YAC5B,uBAAuBL;YACvB,sBAAsBC;YACtB,0BAA0BU;YAC1B,0BAA0BL;QAC5B;IACF;AACF,EAAC"}
@@ -1,5 +1,6 @@
1
- import { createPublicKey, verify } from 'node:crypto';
1
+ import { verify } from 'node:crypto';
2
2
  import { sha256Hex } from '../sync/index.js';
3
+ import { getEd25519PublicKeyInput } from './ed25519Keys.js';
3
4
  export const verifyBodySha256 = ({ body, expectedHash })=>{
4
5
  const computedHash = sha256Hex(body);
5
6
  if (!/^[a-f0-9]{64}$/i.test(expectedHash)) {
@@ -33,19 +34,9 @@ export const validateTimestampSkew = ({ maxSkewSeconds, now = new Date(), timest
33
34
  ok: true
34
35
  };
35
36
  };
36
- const getPublicKeyInput = (publicKey)=>{
37
- if (publicKey.includes('BEGIN PUBLIC KEY')) {
38
- return publicKey;
39
- }
40
- return createPublicKey({
41
- type: 'spki',
42
- format: 'der',
43
- key: Buffer.from(publicKey, 'base64')
44
- });
45
- };
46
37
  export const verifyEd25519Signature = ({ canonicalString, publicKey, signature })=>{
47
38
  try {
48
- return verify(null, Buffer.from(canonicalString, 'utf8'), getPublicKeyInput(publicKey), Buffer.from(signature, 'base64'));
39
+ return verify(null, Buffer.from(canonicalString, 'utf8'), getEd25519PublicKeyInput(publicKey), Buffer.from(signature, 'base64'));
49
40
  } catch {
50
41
  return false;
51
42
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/security/verify.ts"],"sourcesContent":["import { createPublicKey, verify } from 'node:crypto'\n\nimport { sha256Hex } from '../sync/index.js'\n\nexport type VerifyBodyHashResult =\n | {\n computedHash: string\n ok: false\n }\n | {\n computedHash: string\n ok: true\n }\n\nexport type ValidateTimestampResult =\n | {\n date: Date\n ok: true\n }\n | {\n message: string\n ok: false\n }\n\nexport const verifyBodySha256 = ({\n body,\n expectedHash,\n}: {\n body: string\n expectedHash: string\n}): VerifyBodyHashResult => {\n const computedHash = sha256Hex(body)\n\n if (!/^[a-f0-9]{64}$/i.test(expectedHash)) {\n return {\n computedHash,\n ok: false,\n }\n }\n\n return {\n computedHash,\n ok: computedHash === expectedHash.toLowerCase(),\n }\n}\n\nexport const validateTimestampSkew = ({\n maxSkewSeconds,\n now = new Date(),\n timestamp,\n}: {\n maxSkewSeconds: number\n now?: Date\n timestamp: string\n}): ValidateTimestampResult => {\n const date = new Date(timestamp)\n\n if (Number.isNaN(date.getTime())) {\n return {\n message: 'Sync request timestamp is invalid.',\n ok: false,\n }\n }\n\n const skewMs = Math.abs(now.getTime() - date.getTime())\n\n if (skewMs > maxSkewSeconds * 1000) {\n return {\n message: 'Sync request timestamp is outside the allowed skew.',\n ok: false,\n }\n }\n\n return {\n date,\n ok: true,\n }\n}\n\nconst getPublicKeyInput = (publicKey: string) => {\n if (publicKey.includes('BEGIN PUBLIC KEY')) {\n return publicKey\n }\n\n return createPublicKey({\n type: 'spki',\n format: 'der',\n key: Buffer.from(publicKey, 'base64'),\n })\n}\n\nexport const verifyEd25519Signature = ({\n canonicalString,\n publicKey,\n signature,\n}: {\n canonicalString: string\n publicKey: string\n signature: string\n}): boolean => {\n try {\n return verify(\n null,\n Buffer.from(canonicalString, 'utf8'),\n getPublicKeyInput(publicKey),\n Buffer.from(signature, 'base64'),\n )\n } catch {\n return false\n }\n}\n\n"],"names":["createPublicKey","verify","sha256Hex","verifyBodySha256","body","expectedHash","computedHash","test","ok","toLowerCase","validateTimestampSkew","maxSkewSeconds","now","Date","timestamp","date","Number","isNaN","getTime","message","skewMs","Math","abs","getPublicKeyInput","publicKey","includes","type","format","key","Buffer","from","verifyEd25519Signature","canonicalString","signature"],"mappings":"AAAA,SAASA,eAAe,EAAEC,MAAM,QAAQ,cAAa;AAErD,SAASC,SAAS,QAAQ,mBAAkB;AAsB5C,OAAO,MAAMC,mBAAmB,CAAC,EAC/BC,IAAI,EACJC,YAAY,EAIb;IACC,MAAMC,eAAeJ,UAAUE;IAE/B,IAAI,CAAC,kBAAkBG,IAAI,CAACF,eAAe;QACzC,OAAO;YACLC;YACAE,IAAI;QACN;IACF;IAEA,OAAO;QACLF;QACAE,IAAIF,iBAAiBD,aAAaI,WAAW;IAC/C;AACF,EAAC;AAED,OAAO,MAAMC,wBAAwB,CAAC,EACpCC,cAAc,EACdC,MAAM,IAAIC,MAAM,EAChBC,SAAS,EAKV;IACC,MAAMC,OAAO,IAAIF,KAAKC;IAEtB,IAAIE,OAAOC,KAAK,CAACF,KAAKG,OAAO,KAAK;QAChC,OAAO;YACLC,SAAS;YACTX,IAAI;QACN;IACF;IAEA,MAAMY,SAASC,KAAKC,GAAG,CAACV,IAAIM,OAAO,KAAKH,KAAKG,OAAO;IAEpD,IAAIE,SAAST,iBAAiB,MAAM;QAClC,OAAO;YACLQ,SAAS;YACTX,IAAI;QACN;IACF;IAEA,OAAO;QACLO;QACAP,IAAI;IACN;AACF,EAAC;AAED,MAAMe,oBAAoB,CAACC;IACzB,IAAIA,UAAUC,QAAQ,CAAC,qBAAqB;QAC1C,OAAOD;IACT;IAEA,OAAOxB,gBAAgB;QACrB0B,MAAM;QACNC,QAAQ;QACRC,KAAKC,OAAOC,IAAI,CAACN,WAAW;IAC9B;AACF;AAEA,OAAO,MAAMO,yBAAyB,CAAC,EACrCC,eAAe,EACfR,SAAS,EACTS,SAAS,EAKV;IACC,IAAI;QACF,OAAOhC,OACL,MACA4B,OAAOC,IAAI,CAACE,iBAAiB,SAC7BT,kBAAkBC,YAClBK,OAAOC,IAAI,CAACG,WAAW;IAE3B,EAAE,OAAM;QACN,OAAO;IACT;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/security/verify.ts"],"sourcesContent":["import { verify } from 'node:crypto'\n\nimport { sha256Hex } from '../sync/index.js'\nimport { getEd25519PublicKeyInput } from './ed25519Keys.js'\n\nexport type VerifyBodyHashResult =\n | {\n computedHash: string\n ok: false\n }\n | {\n computedHash: string\n ok: true\n }\n\nexport type ValidateTimestampResult =\n | {\n date: Date\n ok: true\n }\n | {\n message: string\n ok: false\n }\n\nexport const verifyBodySha256 = ({\n body,\n expectedHash,\n}: {\n body: string\n expectedHash: string\n}): VerifyBodyHashResult => {\n const computedHash = sha256Hex(body)\n\n if (!/^[a-f0-9]{64}$/i.test(expectedHash)) {\n return {\n computedHash,\n ok: false,\n }\n }\n\n return {\n computedHash,\n ok: computedHash === expectedHash.toLowerCase(),\n }\n}\n\nexport const validateTimestampSkew = ({\n maxSkewSeconds,\n now = new Date(),\n timestamp,\n}: {\n maxSkewSeconds: number\n now?: Date\n timestamp: string\n}): ValidateTimestampResult => {\n const date = new Date(timestamp)\n\n if (Number.isNaN(date.getTime())) {\n return {\n message: 'Sync request timestamp is invalid.',\n ok: false,\n }\n }\n\n const skewMs = Math.abs(now.getTime() - date.getTime())\n\n if (skewMs > maxSkewSeconds * 1000) {\n return {\n message: 'Sync request timestamp is outside the allowed skew.',\n ok: false,\n }\n }\n\n return {\n date,\n ok: true,\n }\n}\n\nexport const verifyEd25519Signature = ({\n canonicalString,\n publicKey,\n signature,\n}: {\n canonicalString: string\n publicKey: string\n signature: string\n}): boolean => {\n try {\n return verify(\n null,\n Buffer.from(canonicalString, 'utf8'),\n getEd25519PublicKeyInput(publicKey),\n Buffer.from(signature, 'base64'),\n )\n } catch {\n return false\n }\n}\n"],"names":["verify","sha256Hex","getEd25519PublicKeyInput","verifyBodySha256","body","expectedHash","computedHash","test","ok","toLowerCase","validateTimestampSkew","maxSkewSeconds","now","Date","timestamp","date","Number","isNaN","getTime","message","skewMs","Math","abs","verifyEd25519Signature","canonicalString","publicKey","signature","Buffer","from"],"mappings":"AAAA,SAASA,MAAM,QAAQ,cAAa;AAEpC,SAASC,SAAS,QAAQ,mBAAkB;AAC5C,SAASC,wBAAwB,QAAQ,mBAAkB;AAsB3D,OAAO,MAAMC,mBAAmB,CAAC,EAC/BC,IAAI,EACJC,YAAY,EAIb;IACC,MAAMC,eAAeL,UAAUG;IAE/B,IAAI,CAAC,kBAAkBG,IAAI,CAACF,eAAe;QACzC,OAAO;YACLC;YACAE,IAAI;QACN;IACF;IAEA,OAAO;QACLF;QACAE,IAAIF,iBAAiBD,aAAaI,WAAW;IAC/C;AACF,EAAC;AAED,OAAO,MAAMC,wBAAwB,CAAC,EACpCC,cAAc,EACdC,MAAM,IAAIC,MAAM,EAChBC,SAAS,EAKV;IACC,MAAMC,OAAO,IAAIF,KAAKC;IAEtB,IAAIE,OAAOC,KAAK,CAACF,KAAKG,OAAO,KAAK;QAChC,OAAO;YACLC,SAAS;YACTX,IAAI;QACN;IACF;IAEA,MAAMY,SAASC,KAAKC,GAAG,CAACV,IAAIM,OAAO,KAAKH,KAAKG,OAAO;IAEpD,IAAIE,SAAST,iBAAiB,MAAM;QAClC,OAAO;YACLQ,SAAS;YACTX,IAAI;QACN;IACF;IAEA,OAAO;QACLO;QACAP,IAAI;IACN;AACF,EAAC;AAED,OAAO,MAAMe,yBAAyB,CAAC,EACrCC,eAAe,EACfC,SAAS,EACTC,SAAS,EAKV;IACC,IAAI;QACF,OAAO1B,OACL,MACA2B,OAAOC,IAAI,CAACJ,iBAAiB,SAC7BtB,yBAAyBuB,YACzBE,OAAOC,IAAI,CAACF,WAAW;IAE3B,EAAE,OAAM;QACN,OAAO;IACT;AACF,EAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valkyrianlabs/payload-markdown-docs",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Git-backed Markdown documentation sync for Payload CMS, powered by payload-markdown.",
5
5
  "bin": {
6
6
  "payload-markdown-docs": "./dist/cli/index.js"
@@ -45,6 +45,8 @@
45
45
  "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths --ignore \"**/*.spec.ts\"",
46
46
  "build:types": "tsc -p tsconfig.build.json --outDir dist --rootDir ./src",
47
47
  "clean": "rimraf {dist,*.tsbuildinfo}",
48
+ "cli": "node --import @swc-node/register/esm-register ./src/cli/index.ts",
49
+ "cli:dist": "node ./dist/cli/index.js",
48
50
  "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json,md}\" dist/",
49
51
  "dev": "next dev dev --turbo",
50
52
  "dev:docs:keygen": "pnpm dev:payload run ./dev/scripts/create-docs-keypair.ts",