import type { SecretDeclaration } from "../manifest.js"; /** Validate public package metadata without reading or printing secret values. */ export function parseSecretDeclarations(value: unknown): SecretDeclaration[] { if (value === undefined) return []; if (!Array.isArray(value)) throw new Error("notion.secrets must be an array"); const names = new Set(); return value.map((entry: unknown, index) => { const field = `notion.secrets[${index}]`; if ( typeof entry !== "object" || entry === null || Object.keys(entry).some( (key) => !["name", "title", "description", "required"].includes(key), ) || !("name" in entry) || typeof entry.name !== "string" || !("title" in entry) || typeof entry.title !== "string" || !("description" in entry) || typeof entry.description !== "string" ) throw new Error( `${field} must contain name, title, description, and optionally required with their expected types and no other fields`, ); const { name, title, description } = entry; const required = "required" in entry ? entry.required : true; if (typeof required !== "boolean") { throw new Error(`${field}.required must be a boolean when provided`); } if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name)) { throw new Error( `${field}.name must be 1–64 ASCII characters, starting with a letter and containing only letters, numbers, and underscores`, ); } if ( name.toUpperCase().startsWith("NOTION_") && !["NOTION_API_TOKEN", "NOTION_API_BASE_URL"].includes(name.toUpperCase()) ) { throw new Error(`${field}.name must not use the reserved NOTION_ prefix`); } if (names.has(name)) throw new Error(`${field}.name must be unique`); names.add(name); if ( Array.from(title).some((character) => { const code = character.charCodeAt(0); return code < 32 || code === 127 || code === 0x2028 || code === 0x2029; }) ) { throw new Error(`${field}.title must be a single line without control characters`); } if ( Array.from(description).some((character) => { const code = character.charCodeAt(0); return (code < 32 && code !== 9 && code !== 10 && code !== 13) || code === 127; }) ) { throw new Error( `${field}.description must not contain control characters other than tabs and line breaks`, ); } if (title.trim().length < 1 || title.trim().length > 80) { throw new Error(`${field}.title must contain 1–80 UTF-16 code units after trimming`); } if (description.trim().length < 1 || description.trim().length > 512) { throw new Error( `${field}.description must contain 1–512 UTF-16 code units after trimming`, ); } return { name, title: title.trim(), description: description.trim(), required }; }); } /** Read only the declaration list from an app's package.json. */ export function packageSecretDeclarations(packageJson: unknown): SecretDeclaration[] { if (typeof packageJson !== "object" || packageJson === null || !("notion" in packageJson)) return []; const { notion } = packageJson; if (typeof notion !== "object" || notion === null || !("secrets" in notion)) return []; return parseSecretDeclarations(notion.secrets); }