@taprootio/site-authoring 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +239 -0
- package/bin/taproot-site.js +5 -0
- package/package.json +40 -0
- package/src/api.js +1507 -0
- package/src/appearance-contract.js +311 -0
- package/src/atomic-file.js +136 -0
- package/src/bounded-file.js +59 -0
- package/src/capabilities.js +29 -0
- package/src/cli.js +872 -0
- package/src/config.js +412 -0
- package/src/constants.js +173 -0
- package/src/content/components.js +870 -0
- package/src/content/free-form-section-registry.json +524 -0
- package/src/content/free-form-sections.js +582 -0
- package/src/content/index.js +29 -0
- package/src/content/inline-facts.js +97 -0
- package/src/content/markdown.js +1625 -0
- package/src/content/tiptap-prosemirror.ts +1266 -0
- package/src/content/validate-document.js +1079 -0
- package/src/content/vocabulary.js +397 -0
- package/src/credentials.js +544 -0
- package/src/errors.js +133 -0
- package/src/footer-contract.js +687 -0
- package/src/footer-draft-hash.js +203 -0
- package/src/footer-workspace.js +136 -0
- package/src/image-metadata.js +169 -0
- package/src/index.js +16 -0
- package/src/json-guard.js +111 -0
- package/src/output.js +157 -0
- package/src/presentation-reference.js +600 -0
- package/src/reference-help.js +927 -0
- package/src/session.js +316 -0
- package/src/settings-catalog.js +187 -0
- package/src/settings.js +157 -0
- package/src/theme-validation.js +344 -0
- package/src/transport.js +777 -0
- package/src/verbs/approve.js +178 -0
- package/src/verbs/deploy.js +327 -0
- package/src/verbs/env.js +89 -0
- package/src/verbs/footer-push.js +115 -0
- package/src/verbs/index.js +125 -0
- package/src/verbs/login.js +529 -0
- package/src/verbs/logout.js +39 -0
- package/src/verbs/media-upload.js +319 -0
- package/src/verbs/nav-push.js +333 -0
- package/src/verbs/pages-push.js +990 -0
- package/src/verbs/preview-page.js +107 -0
- package/src/verbs/preview-revoke.js +56 -0
- package/src/verbs/pull.js +727 -0
- package/src/verbs/sites.js +57 -0
- package/src/verbs/status.js +94 -0
- package/src/verbs/theme-push.js +197 -0
- package/src/verbs/use.js +129 -0
- package/src/verbs/validate.js +477 -0
- package/src/verbs/whoami.js +108 -0
- package/src/workspace.js +1085 -0
package/src/output.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { lstat, open } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CLI_BINARY_NAME,
|
|
7
|
+
CLI_NAME,
|
|
8
|
+
CLI_VERSION,
|
|
9
|
+
GITHUB_OUTPUT_KEY_PREFIX,
|
|
10
|
+
GITHUB_OUTPUT_RESULT_KEY,
|
|
11
|
+
LIMITS,
|
|
12
|
+
REFUSAL_UNCLASSIFIED,
|
|
13
|
+
RESULT_SCHEMA_VERSION,
|
|
14
|
+
} from "./constants.js";
|
|
15
|
+
import {
|
|
16
|
+
hasAsciiControl,
|
|
17
|
+
normalizePreviewRecovery,
|
|
18
|
+
sanitizeDiagnostic,
|
|
19
|
+
SiteAuthoringError,
|
|
20
|
+
} from "./errors.js";
|
|
21
|
+
|
|
22
|
+
const SAFE_SCALAR = /^[a-z][a-z0-9]*(?: [a-z][a-z0-9]*)?$/u;
|
|
23
|
+
const MAXIMUM_OUTPUT_PATH_LENGTH = 4_096;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The machine-readable failure. `refusal` is present only when the API
|
|
27
|
+
* classified the rejection, because "unclassified" tells a caller nothing it
|
|
28
|
+
* could not read from `status`.
|
|
29
|
+
*/
|
|
30
|
+
export function failureResult(error) {
|
|
31
|
+
const result = {
|
|
32
|
+
schemaVersion: RESULT_SCHEMA_VERSION,
|
|
33
|
+
ok: false,
|
|
34
|
+
cli: { name: CLI_NAME, version: CLI_VERSION },
|
|
35
|
+
error: { code: error.code },
|
|
36
|
+
};
|
|
37
|
+
if (error.field) result.error.field = error.field;
|
|
38
|
+
if (error.status) result.error.status = error.status;
|
|
39
|
+
if (Array.isArray(error.alternatives) && error.alternatives.length > 0) {
|
|
40
|
+
result.error.alternatives = error.alternatives;
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(error.completedWrites) && error.completedWrites.length > 0) {
|
|
43
|
+
result.error.completedWrites = error.completedWrites;
|
|
44
|
+
}
|
|
45
|
+
const previewRecovery = normalizePreviewRecovery(error.previewRecovery);
|
|
46
|
+
if (previewRecovery) result.error.preview = previewRecovery;
|
|
47
|
+
const refusal = typeof error.refusalKind === "function" ? error.refusalKind() : undefined;
|
|
48
|
+
if (typeof refusal === "string" && refusal !== REFUSAL_UNCLASSIFIED) result.error.refusal = refusal;
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requireBoundedJson(result) {
|
|
53
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
54
|
+
throw new SiteAuthoringError("output.result_invalid", "A verb returned something other than a result object.");
|
|
55
|
+
}
|
|
56
|
+
const json = JSON.stringify(result);
|
|
57
|
+
if (typeof json !== "string") {
|
|
58
|
+
throw new SiteAuthoringError("output.result_invalid", "A verb returned a result that cannot be serialized.");
|
|
59
|
+
}
|
|
60
|
+
if (Buffer.byteLength(json, "utf8") > LIMITS.githubOutputBytes) {
|
|
61
|
+
throw new SiteAuthoringError("output.too_large", "The machine-readable result exceeded its output bound.");
|
|
62
|
+
}
|
|
63
|
+
return json;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function githubDelimiter(json) {
|
|
67
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
68
|
+
const delimiter = `${GITHUB_OUTPUT_KEY_PREFIX}_${randomBytes(16).toString("hex")}`;
|
|
69
|
+
if (!json.includes(delimiter)) return delimiter;
|
|
70
|
+
}
|
|
71
|
+
throw new SiteAuthoringError(
|
|
72
|
+
"output.delimiter",
|
|
73
|
+
"Could not construct a collision-resistant GitHub Actions output delimiter.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function appendInspectedGithubOutput(outputPath, stats, contents) {
|
|
78
|
+
let handle;
|
|
79
|
+
try {
|
|
80
|
+
handle = await open(
|
|
81
|
+
outputPath,
|
|
82
|
+
fsConstants.O_WRONLY
|
|
83
|
+
| fsConstants.O_APPEND
|
|
84
|
+
| (fsConstants.O_NOFOLLOW ?? 0)
|
|
85
|
+
| (fsConstants.O_NONBLOCK ?? 0),
|
|
86
|
+
);
|
|
87
|
+
const openedStats = await handle.stat({ bigint: true });
|
|
88
|
+
if (
|
|
89
|
+
!openedStats.isFile()
|
|
90
|
+
|| openedStats.dev !== stats.dev
|
|
91
|
+
|| openedStats.ino !== stats.ino
|
|
92
|
+
) {
|
|
93
|
+
throw new SiteAuthoringError("output.github_changed", "GITHUB_OUTPUT changed while it was being opened.");
|
|
94
|
+
}
|
|
95
|
+
await handle.appendFile(contents, { encoding: "utf8" });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error instanceof SiteAuthoringError) throw error;
|
|
98
|
+
throw new SiteAuthoringError("output.github_write", "Could not append the machine-readable GitHub Actions output.");
|
|
99
|
+
} finally {
|
|
100
|
+
await handle?.close().catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function writeGithubActionsOutput(outputPath, result) {
|
|
105
|
+
if (
|
|
106
|
+
typeof outputPath !== "string"
|
|
107
|
+
|| outputPath.length === 0
|
|
108
|
+
|| outputPath.length > MAXIMUM_OUTPUT_PATH_LENGTH
|
|
109
|
+
|| hasAsciiControl(outputPath)
|
|
110
|
+
) {
|
|
111
|
+
throw new SiteAuthoringError("output.github_path", "GITHUB_OUTPUT is not a valid output-file path.");
|
|
112
|
+
}
|
|
113
|
+
let stats;
|
|
114
|
+
try {
|
|
115
|
+
stats = await lstat(outputPath, { bigint: true });
|
|
116
|
+
} catch {
|
|
117
|
+
throw new SiteAuthoringError(
|
|
118
|
+
"output.github_missing",
|
|
119
|
+
"GITHUB_OUTPUT does not name an existing Actions output file.",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
123
|
+
throw new SiteAuthoringError(
|
|
124
|
+
"output.github_invalid",
|
|
125
|
+
"GITHUB_OUTPUT must be a regular file, not a link or directory.",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const json = requireBoundedJson(result);
|
|
129
|
+
const delimiter = githubDelimiter(json);
|
|
130
|
+
const lines = [
|
|
131
|
+
`${GITHUB_OUTPUT_RESULT_KEY}<<${delimiter}`,
|
|
132
|
+
json,
|
|
133
|
+
delimiter,
|
|
134
|
+
];
|
|
135
|
+
// Convenience scalars stay deliberately thin. The verbs own the shape of a
|
|
136
|
+
// success payload, and a scalar whose value came from a response body could
|
|
137
|
+
// inject a line into the workflow's output file, so only values this package
|
|
138
|
+
// controls and re-validates are ever emitted as bare key=value pairs.
|
|
139
|
+
if (result.ok === true && typeof result.verb === "string" && SAFE_SCALAR.test(result.verb)) {
|
|
140
|
+
lines.push(`${GITHUB_OUTPUT_KEY_PREFIX}_verb=${result.verb}`);
|
|
141
|
+
}
|
|
142
|
+
await appendInspectedGithubOutput(outputPath, stats, `${lines.join("\n")}\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function humanFailure(error) {
|
|
146
|
+
const field = error.field ? ` field=${sanitizeDiagnostic(error.field, "unknown")}` : "";
|
|
147
|
+
const status = error.status ? ` status=${sanitizeDiagnostic(error.status, "unknown")}` : "";
|
|
148
|
+
const kind = typeof error.refusalKind === "function" ? error.refusalKind() : undefined;
|
|
149
|
+
const refusal = typeof kind === "string" && kind !== REFUSAL_UNCLASSIFIED
|
|
150
|
+
? ` refusal=${sanitizeDiagnostic(kind, "unknown")}`
|
|
151
|
+
: "";
|
|
152
|
+
return `${CLI_BINARY_NAME} failed [${error.code}]${field}${status}${refusal}: ${sanitizeDiagnostic(error.message)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function serializeResult(result) {
|
|
156
|
+
return requireBoundedJson(result);
|
|
157
|
+
}
|