@stelstone/server 0.30.0 → 0.31.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/package.json
CHANGED
|
@@ -86,7 +86,15 @@ export function createBasicAuth({
|
|
|
86
86
|
const p = decoded.slice(colon + 1);
|
|
87
87
|
const entry = findUser(u, p);
|
|
88
88
|
if (!entry) return null;
|
|
89
|
-
|
|
89
|
+
// `email` is optional and deliberately not derived from the username: a
|
|
90
|
+
// made-up address in a commit looks like a real one, and git history is
|
|
91
|
+
// read as evidence. Without it the commit falls back to the token owner.
|
|
92
|
+
return {
|
|
93
|
+
login: entry.user,
|
|
94
|
+
name: entry.name || entry.user,
|
|
95
|
+
email: entry.email || null,
|
|
96
|
+
role: entry.role || "editor",
|
|
97
|
+
};
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
return {
|
|
@@ -52,6 +52,20 @@ import { sanitize, safeFileName, sortPages, buildDuplicateData, listScheduledDue
|
|
|
52
52
|
* @param {Object} [opts.list] Listing strategy config (see above)
|
|
53
53
|
* @returns {import('./types.mjs').ContentAdapter}
|
|
54
54
|
*/
|
|
55
|
+
/**
|
|
56
|
+
* The `author` field for a git-data commit, or undefined.
|
|
57
|
+
*
|
|
58
|
+
* GitHub attributes a commit to the token owner when this is absent, which is
|
|
59
|
+
* what made every entry in the CMS's history read as the same person no
|
|
60
|
+
* matter who published it. A user with no configured email falls back to that
|
|
61
|
+
* rather than getting an invented address: history is read as evidence, and a
|
|
62
|
+
* plausible-looking address nobody owns is worse than an honest default.
|
|
63
|
+
*/
|
|
64
|
+
function commitAuthor(actor) {
|
|
65
|
+
if (!actor?.email) return undefined;
|
|
66
|
+
return { name: actor.name || actor.login, email: actor.email };
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
export function createGitHubContent({ token, owner, repo, branch, draftBranch, pagesDir, commitMessage, list = {}, collections = {} }) {
|
|
56
70
|
const listConfig = {
|
|
57
71
|
strategy: "index",
|
|
@@ -323,14 +337,19 @@ export function createGitHubContent({ token, owner, repo, branch, draftBranch, p
|
|
|
323
337
|
* write time — which is what this did before — discarded that
|
|
324
338
|
* guarantee and silently overwrote a concurrent edit.
|
|
325
339
|
*/
|
|
326
|
-
async writePage(collection, file, data, { expectedVersion } = {}) {
|
|
340
|
+
async writePage(collection, file, data, { expectedVersion, actor } = {}) {
|
|
327
341
|
await ensureDraftBranch();
|
|
328
342
|
const path = contentPath(collection, sanitize(file));
|
|
329
343
|
const sha = expectedVersion ?? (await getFileSha(path));
|
|
330
344
|
let result;
|
|
331
345
|
try {
|
|
346
|
+
// The history screen lists commits on the draft branch, so this write
|
|
347
|
+
// is the one that answers "who edited this" — attributing it matters
|
|
348
|
+
// more here than on the publish commit.
|
|
349
|
+
const author = commitAuthor(actor);
|
|
332
350
|
result = await apiPut(`/contents/${path}`, {
|
|
333
351
|
message: commitMsg(commitMessage),
|
|
352
|
+
...(author ? { author } : {}),
|
|
334
353
|
content: encodeContent(data),
|
|
335
354
|
branch: workBranch,
|
|
336
355
|
...(sha ? { sha } : {}),
|
|
@@ -447,7 +466,7 @@ export function createGitHubContent({ token, owner, repo, branch, draftBranch, p
|
|
|
447
466
|
* @param {string} [message]
|
|
448
467
|
* @param {{ entries?: {collection: string, file: string}[], target?: string }} [opts]
|
|
449
468
|
*/
|
|
450
|
-
async publish(message, { entries, target } = {}) {
|
|
469
|
+
async publish(message, { entries, target, actor } = {}) {
|
|
451
470
|
const toBranch = target || branch;
|
|
452
471
|
if (!draftMode) {
|
|
453
472
|
// Writes are committed instantly; trigger is external (Netlify webhook on push)
|
|
@@ -490,6 +509,7 @@ export function createGitHubContent({ token, owner, repo, branch, draftBranch, p
|
|
|
490
509
|
message: msg,
|
|
491
510
|
tree: newTree.sha,
|
|
492
511
|
parents: [baseCommitSha],
|
|
512
|
+
author: commitAuthor(actor),
|
|
493
513
|
});
|
|
494
514
|
await apiPatch(`/git/refs/heads/${toBranch}`, { sha: newCommit.sha });
|
|
495
515
|
const shortSha = newCommit.sha.slice(0, 7);
|
|
@@ -116,7 +116,7 @@ export function createGitHubOAuth({
|
|
|
116
116
|
if (!authHeader.startsWith("Bearer ")) return null;
|
|
117
117
|
const claims = verifyToken(authHeader.slice(7));
|
|
118
118
|
if (!claims || claims.type !== "session") return null;
|
|
119
|
-
return { login: claims.sub, role: getRole(claims.sub), name: claims.name };
|
|
119
|
+
return { login: claims.sub, role: getRole(claims.sub), name: claims.name, email: claims.email ?? null };
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
function issueSessionToken(login, name) {
|
|
@@ -156,6 +156,12 @@ function checkAuth(config, report, { getSecret }) {
|
|
|
156
156
|
`${entry.passEnv} is not set — user "${entry.user}" is DISABLED (check for a typo in the variable name)`,
|
|
157
157
|
);
|
|
158
158
|
}
|
|
159
|
+
// Optional, and only useful if it is real: it becomes the author of
|
|
160
|
+
// the commits this user makes. A malformed one would attribute their
|
|
161
|
+
// work to nobody, so say so rather than write it.
|
|
162
|
+
if (entry.email !== undefined && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(entry.email))) {
|
|
163
|
+
report.error(`auth.users[${i}].email`, "must be an email address — it becomes the git commit author");
|
|
164
|
+
}
|
|
159
165
|
});
|
|
160
166
|
} else if (!auth.passEnv && !auth.userEnv) {
|
|
161
167
|
report.warn("auth.users", "no users configured — the CMS will run without authentication");
|
package/src/routes.mjs
CHANGED
|
@@ -87,7 +87,12 @@ export const apiRoutes = [
|
|
|
87
87
|
handler: ({ env, runtime, adminUiVersion }) =>
|
|
88
88
|
ok({
|
|
89
89
|
serverVersion: SERVER_VERSION,
|
|
90
|
-
|
|
90
|
+
// The Node server reads this off the package it resolved. A Worker
|
|
91
|
+
// cannot: the SPA is served by the assets binding and its package.json
|
|
92
|
+
// is not in the bundle. The deployment composed that directory, so it
|
|
93
|
+
// can state the version the way it states the commit below — a stated
|
|
94
|
+
// version beats a null on the About screen.
|
|
95
|
+
adminUiVersion: adminUiVersion ?? env("CMS_ADMIN_UI_VERSION") ?? null,
|
|
91
96
|
// Declared by whoever constructed the handler, not sniffed: with
|
|
92
97
|
// nodejs_compat enabled a Worker also exposes process.versions.node,
|
|
93
98
|
// so feature detection reported "node" from inside a Worker.
|
|
@@ -226,7 +231,7 @@ export const apiRoutes = [
|
|
|
226
231
|
method: "PUT",
|
|
227
232
|
path: "/api/collections/:collection/:file",
|
|
228
233
|
auth: "any",
|
|
229
|
-
handler: async ({ adapters, params, body, header }) => {
|
|
234
|
+
handler: async ({ adapters, params, body, header, user }) => {
|
|
230
235
|
const bodyErr = pageBodyError(body);
|
|
231
236
|
if (bodyErr) return badRequest(bodyErr);
|
|
232
237
|
|
|
@@ -234,7 +239,7 @@ export const apiRoutes = [
|
|
|
234
239
|
const expectedVersion = ifMatch ? ifMatch.replace(/^W\//, "").replace(/"/g, "") : undefined;
|
|
235
240
|
|
|
236
241
|
try {
|
|
237
|
-
await adapters.content.writePage(params.collection, params.file, body, { expectedVersion });
|
|
242
|
+
await adapters.content.writePage(params.collection, params.file, body, { expectedVersion, actor: user });
|
|
238
243
|
} catch (err) {
|
|
239
244
|
if (err.status === 412) {
|
|
240
245
|
return {
|
|
@@ -580,11 +585,11 @@ export const apiRoutes = [
|
|
|
580
585
|
method: "POST",
|
|
581
586
|
path: "/api/publish",
|
|
582
587
|
auth: "admin",
|
|
583
|
-
handler: async ({ adapters, config, body }) => {
|
|
588
|
+
handler: async ({ adapters, config, body, user }) => {
|
|
584
589
|
const target = resolveTarget(config, body?.target);
|
|
585
590
|
if (target.error) return { status: 400, json: { ok: false, message: target.error } };
|
|
586
591
|
try {
|
|
587
|
-
return ok(await adapters.content.publish(null, { target: target.branch }));
|
|
592
|
+
return ok(await adapters.content.publish(null, { target: target.branch, actor: user }));
|
|
588
593
|
} catch (err) {
|
|
589
594
|
return { status: 500, json: { ok: false, message: err.message } };
|
|
590
595
|
}
|
|
@@ -598,7 +603,7 @@ export const apiRoutes = [
|
|
|
598
603
|
method: "POST",
|
|
599
604
|
path: "/api/collections/:collection/:file/publish",
|
|
600
605
|
auth: "admin",
|
|
601
|
-
handler: async ({ adapters, params, config, body }) => {
|
|
606
|
+
handler: async ({ adapters, params, config, body, user }) => {
|
|
602
607
|
const target = resolveTarget(config, body?.target);
|
|
603
608
|
if (target.error) return { status: 400, json: { ok: false, message: target.error } };
|
|
604
609
|
if (!adapters.content.capabilities?.perEntryPublish) {
|
|
@@ -612,6 +617,7 @@ export const apiRoutes = [
|
|
|
612
617
|
await adapters.content.publish(null, {
|
|
613
618
|
entries: [{ collection: params.collection, file: params.file }],
|
|
614
619
|
target: target.branch,
|
|
620
|
+
actor: user,
|
|
615
621
|
}),
|
|
616
622
|
);
|
|
617
623
|
} catch (err) {
|
|
@@ -643,11 +649,20 @@ export const apiRoutes = [
|
|
|
643
649
|
method: "GET",
|
|
644
650
|
path: "/api/deploy/status",
|
|
645
651
|
auth: "any",
|
|
646
|
-
handler: async ({ adapters, query }) => {
|
|
652
|
+
handler: async ({ adapters, config, query }) => {
|
|
647
653
|
if (!adapters.build.configured) return ok({ configured: false });
|
|
654
|
+
// Which branch's build to watch. After publishing to a target the new
|
|
655
|
+
// commit exists only on that target's branch, so without this the run
|
|
656
|
+
// is looked for on the deploy branch and never found — the pill then
|
|
657
|
+
// spins until it gives up, for a build that actually succeeded.
|
|
658
|
+
const target = resolveTarget(config, query.target);
|
|
659
|
+
if (target.error) return { status: 400, json: { error: target.error } };
|
|
648
660
|
try {
|
|
649
661
|
return ok(
|
|
650
|
-
await adapters.build.getDeployStatus({
|
|
662
|
+
await adapters.build.getDeployStatus({
|
|
663
|
+
branch: target.branch ?? query.branch,
|
|
664
|
+
sha: query.sha,
|
|
665
|
+
}),
|
|
651
666
|
);
|
|
652
667
|
} catch (err) {
|
|
653
668
|
if (err.upstreamStatus) {
|
package/src/version.mjs
CHANGED