@stelstone/server 0.30.1 → 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
|
@@ -231,7 +231,7 @@ export const apiRoutes = [
|
|
|
231
231
|
method: "PUT",
|
|
232
232
|
path: "/api/collections/:collection/:file",
|
|
233
233
|
auth: "any",
|
|
234
|
-
handler: async ({ adapters, params, body, header }) => {
|
|
234
|
+
handler: async ({ adapters, params, body, header, user }) => {
|
|
235
235
|
const bodyErr = pageBodyError(body);
|
|
236
236
|
if (bodyErr) return badRequest(bodyErr);
|
|
237
237
|
|
|
@@ -239,7 +239,7 @@ export const apiRoutes = [
|
|
|
239
239
|
const expectedVersion = ifMatch ? ifMatch.replace(/^W\//, "").replace(/"/g, "") : undefined;
|
|
240
240
|
|
|
241
241
|
try {
|
|
242
|
-
await adapters.content.writePage(params.collection, params.file, body, { expectedVersion });
|
|
242
|
+
await adapters.content.writePage(params.collection, params.file, body, { expectedVersion, actor: user });
|
|
243
243
|
} catch (err) {
|
|
244
244
|
if (err.status === 412) {
|
|
245
245
|
return {
|
|
@@ -585,11 +585,11 @@ export const apiRoutes = [
|
|
|
585
585
|
method: "POST",
|
|
586
586
|
path: "/api/publish",
|
|
587
587
|
auth: "admin",
|
|
588
|
-
handler: async ({ adapters, config, body }) => {
|
|
588
|
+
handler: async ({ adapters, config, body, user }) => {
|
|
589
589
|
const target = resolveTarget(config, body?.target);
|
|
590
590
|
if (target.error) return { status: 400, json: { ok: false, message: target.error } };
|
|
591
591
|
try {
|
|
592
|
-
return ok(await adapters.content.publish(null, { target: target.branch }));
|
|
592
|
+
return ok(await adapters.content.publish(null, { target: target.branch, actor: user }));
|
|
593
593
|
} catch (err) {
|
|
594
594
|
return { status: 500, json: { ok: false, message: err.message } };
|
|
595
595
|
}
|
|
@@ -603,7 +603,7 @@ export const apiRoutes = [
|
|
|
603
603
|
method: "POST",
|
|
604
604
|
path: "/api/collections/:collection/:file/publish",
|
|
605
605
|
auth: "admin",
|
|
606
|
-
handler: async ({ adapters, params, config, body }) => {
|
|
606
|
+
handler: async ({ adapters, params, config, body, user }) => {
|
|
607
607
|
const target = resolveTarget(config, body?.target);
|
|
608
608
|
if (target.error) return { status: 400, json: { ok: false, message: target.error } };
|
|
609
609
|
if (!adapters.content.capabilities?.perEntryPublish) {
|
|
@@ -617,6 +617,7 @@ export const apiRoutes = [
|
|
|
617
617
|
await adapters.content.publish(null, {
|
|
618
618
|
entries: [{ collection: params.collection, file: params.file }],
|
|
619
619
|
target: target.branch,
|
|
620
|
+
actor: user,
|
|
620
621
|
}),
|
|
621
622
|
);
|
|
622
623
|
} catch (err) {
|
package/src/version.mjs
CHANGED