@ampless/admin 0.2.0-alpha.2 → 0.2.0-alpha.21

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.
Files changed (49) hide show
  1. package/README.ja.md +73 -0
  2. package/README.md +3 -0
  3. package/dist/api/index.d.ts +1 -1
  4. package/dist/chunk-2ITWLRYF.js +38 -0
  5. package/dist/chunk-5JKOPRCO.js +41 -0
  6. package/dist/chunk-5Q6KVRZ2.js +250 -0
  7. package/dist/chunk-7IR4F7GA.js +6 -0
  8. package/dist/chunk-A3SWBQA6.js +71 -0
  9. package/dist/chunk-BC4B6DLO.js +21 -0
  10. package/dist/chunk-BWFCQNPU.js +1264 -0
  11. package/dist/chunk-CQY55RDG.js +48 -0
  12. package/dist/chunk-CVJCMTYB.js +1197 -0
  13. package/dist/chunk-JOASK4AM.js +360 -0
  14. package/dist/{chunk-TJR3ALRJ.js → chunk-OSUTPPAU.js} +171 -68
  15. package/dist/chunk-QXJIIBUQ.js +21 -0
  16. package/dist/chunk-S66L5CDS.js +335 -0
  17. package/dist/chunk-SRNH2IVA.js +149 -0
  18. package/dist/chunk-TZWSXAHD.js +32 -0
  19. package/dist/chunk-VXEVLHGL.js +10 -0
  20. package/dist/chunk-W6BXESPW.js +198 -0
  21. package/dist/chunk-XY4JWSMS.js +33 -0
  22. package/dist/components/admin-dashboard.d.ts +10 -0
  23. package/dist/components/admin-dashboard.js +9 -0
  24. package/dist/components/edit-post-view.d.ts +9 -0
  25. package/dist/components/edit-post-view.js +14 -0
  26. package/dist/components/index.d.ts +40 -21
  27. package/dist/components/index.js +32 -15
  28. package/dist/components/login-view.d.ts +5 -0
  29. package/dist/components/login-view.js +9 -0
  30. package/dist/components/mcp-tokens-view.d.ts +22 -0
  31. package/dist/components/mcp-tokens-view.js +9 -0
  32. package/dist/components/media-view.d.ts +5 -0
  33. package/dist/components/media-view.js +12 -0
  34. package/dist/components/new-post-view.d.ts +5 -0
  35. package/dist/components/new-post-view.js +14 -0
  36. package/dist/components/posts-list-view.d.ts +5 -0
  37. package/dist/components/posts-list-view.js +11 -0
  38. package/dist/components/users-list-view.d.ts +7 -0
  39. package/dist/components/users-list-view.js +9 -0
  40. package/dist/{i18n-ByHM_Bho.d.ts → i18n-MWvAMHzn.d.ts} +253 -2
  41. package/dist/index.d.ts +14 -9
  42. package/dist/index.js +18 -10
  43. package/dist/lib/theme-actions.d.ts +17 -0
  44. package/dist/lib/theme-actions.js +7 -0
  45. package/dist/metafile-esm.json +1 -1
  46. package/dist/pages/index.d.ts +67 -15
  47. package/dist/pages/index.js +147 -659
  48. package/package.json +12 -9
  49. package/dist/chunk-T2RSMFOI.js +0 -2074
@@ -0,0 +1,360 @@
1
+ 'use client';
2
+ import {
3
+ useT
4
+ } from "./chunk-XY4JWSMS.js";
5
+
6
+ // src/components/mcp-tokens-view.tsx
7
+ import { useEffect, useState } from "react";
8
+ import {
9
+ Button,
10
+ Table,
11
+ TableBody,
12
+ TableCell,
13
+ TableHead,
14
+ TableHeader,
15
+ TableRow
16
+ } from "@ampless/runtime/ui";
17
+
18
+ // src/lib/mcp-token-format.ts
19
+ import { randomBytes, createHash } from "crypto";
20
+ function generateToken() {
21
+ const random = toBase64Url(randomBytes(32));
22
+ const plain = `amk_${random}`;
23
+ return {
24
+ plain,
25
+ hash: hashToken(plain),
26
+ prefix: plain.slice(0, 8)
27
+ };
28
+ }
29
+ function hashToken(plain) {
30
+ return createHash("sha256").update(plain).digest("hex");
31
+ }
32
+ function toBase64Url(bytes) {
33
+ return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
+ }
35
+
36
+ // src/lib/mcp-token-storage.ts
37
+ import { getKvStore } from "ampless";
38
+ var TOKENS_PK = "mcp-tokens";
39
+ async function listTokens() {
40
+ const items = await getKvStore().query(TOKENS_PK);
41
+ return items.map((item) => item.value);
42
+ }
43
+ async function findByHash(hash) {
44
+ return await getKvStore().get(TOKENS_PK, hash);
45
+ }
46
+ async function createToken(meta) {
47
+ const full = {
48
+ ...meta,
49
+ lastUsedAt: null,
50
+ revokedAt: null
51
+ };
52
+ await getKvStore().put(TOKENS_PK, meta.hash, full);
53
+ return full;
54
+ }
55
+ async function revokeToken(hash) {
56
+ const existing = await findByHash(hash);
57
+ if (!existing) return;
58
+ if (existing.revokedAt) return;
59
+ await getKvStore().put(TOKENS_PK, hash, {
60
+ ...existing,
61
+ revokedAt: (/* @__PURE__ */ new Date()).toISOString()
62
+ });
63
+ }
64
+
65
+ // src/components/mcp-tokens-view.tsx
66
+ import { jsx, jsxs } from "react/jsx-runtime";
67
+ function tokenStatus(tok) {
68
+ if (tok.revokedAt) return "revoked";
69
+ if (tok.expiresAt && new Date(tok.expiresAt) < /* @__PURE__ */ new Date()) return "expired";
70
+ return "active";
71
+ }
72
+ function relativeTime(iso) {
73
+ if (!iso) return "";
74
+ const ms = Date.now() - new Date(iso).getTime();
75
+ const s = Math.floor(ms / 1e3);
76
+ if (s < 60) return `${s}s ago`;
77
+ const m = Math.floor(s / 60);
78
+ if (m < 60) return `${m}m ago`;
79
+ const h = Math.floor(m / 60);
80
+ if (h < 24) return `${h}h ago`;
81
+ const d = Math.floor(h / 24);
82
+ return `${d}d ago`;
83
+ }
84
+ function McpTokensView({ currentUserId, currentUserEmail, sites, mcpEndpoint }) {
85
+ const t = useT();
86
+ const [endpointCopied, setEndpointCopied] = useState(false);
87
+ async function copyEndpoint() {
88
+ if (!mcpEndpoint) return;
89
+ try {
90
+ await navigator.clipboard.writeText(mcpEndpoint);
91
+ setEndpointCopied(true);
92
+ setTimeout(() => setEndpointCopied(false), 2e3);
93
+ } catch (err) {
94
+ console.error("[mcp-tokens-view] copy endpoint failed", err);
95
+ }
96
+ }
97
+ const [tokens, setTokens] = useState(null);
98
+ const [loading, setLoading] = useState(true);
99
+ const [loadError, setLoadError] = useState(null);
100
+ const [showCreateModal, setShowCreateModal] = useState(false);
101
+ const [scopeSiteId, setScopeSiteId] = useState(null);
102
+ const [expPreset, setExpPreset] = useState("never");
103
+ const [customDate, setCustomDate] = useState("");
104
+ const [creating, setCreating] = useState(false);
105
+ const [createError, setCreateError] = useState(null);
106
+ const [revealedPlain, setRevealedPlain] = useState(null);
107
+ const [copied, setCopied] = useState(false);
108
+ async function loadTokens() {
109
+ setLoading(true);
110
+ setLoadError(null);
111
+ try {
112
+ const list = await listTokens();
113
+ setTokens(list);
114
+ } catch (err) {
115
+ console.error("[mcp-tokens-view] listTokens failed", err);
116
+ setLoadError(err instanceof Error ? err.message : String(err));
117
+ } finally {
118
+ setLoading(false);
119
+ }
120
+ }
121
+ useEffect(() => {
122
+ void loadTokens();
123
+ }, []);
124
+ function openCreateModal() {
125
+ setScopeSiteId(null);
126
+ setExpPreset("never");
127
+ setCustomDate("");
128
+ setCreateError(null);
129
+ setShowCreateModal(true);
130
+ }
131
+ function expiresAtFromPreset() {
132
+ if (expPreset === "never") return null;
133
+ if (expPreset === "30days") {
134
+ const d = /* @__PURE__ */ new Date();
135
+ d.setDate(d.getDate() + 30);
136
+ return d.toISOString();
137
+ }
138
+ if (expPreset === "90days") {
139
+ const d = /* @__PURE__ */ new Date();
140
+ d.setDate(d.getDate() + 90);
141
+ return d.toISOString();
142
+ }
143
+ if (expPreset === "custom" && customDate) {
144
+ return new Date(customDate).toISOString();
145
+ }
146
+ return null;
147
+ }
148
+ async function handleIssue() {
149
+ setCreating(true);
150
+ setCreateError(null);
151
+ try {
152
+ const { plain, hash, prefix } = generateToken();
153
+ const meta = await createToken({
154
+ hash,
155
+ prefix,
156
+ scope: { siteId: scopeSiteId },
157
+ createdBy: currentUserId,
158
+ createdByEmail: currentUserEmail,
159
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
160
+ expiresAt: expiresAtFromPreset()
161
+ });
162
+ void meta;
163
+ setShowCreateModal(false);
164
+ setRevealedPlain(plain);
165
+ void loadTokens();
166
+ } catch (err) {
167
+ console.error("[mcp-tokens-view] createToken failed", err);
168
+ setCreateError(err instanceof Error ? err.message : String(err));
169
+ } finally {
170
+ setCreating(false);
171
+ }
172
+ }
173
+ async function handleRevoke(hash) {
174
+ if (!confirm(t("mcpTokens.revokeConfirm"))) return;
175
+ try {
176
+ await revokeToken(hash);
177
+ void loadTokens();
178
+ } catch (err) {
179
+ console.error("[mcp-tokens-view] revokeToken failed", err);
180
+ alert(err instanceof Error ? err.message : String(err));
181
+ }
182
+ }
183
+ async function copyToClipboard() {
184
+ if (!revealedPlain) return;
185
+ try {
186
+ await navigator.clipboard.writeText(revealedPlain);
187
+ setCopied(true);
188
+ setTimeout(() => setCopied(false), 1500);
189
+ } catch (err) {
190
+ console.warn("[mcp-tokens-view] clipboard write failed", err);
191
+ }
192
+ }
193
+ function scopeLabel(siteId) {
194
+ if (!siteId) return t("mcpTokens.scopeAll");
195
+ const site = sites.find((s) => s.id === siteId);
196
+ return site?.name ?? siteId;
197
+ }
198
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl space-y-8 p-4 md:p-8", children: [
199
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
200
+ /* @__PURE__ */ jsxs("div", { children: [
201
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold md:text-3xl", children: t("mcpTokens.title") }),
202
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: t("mcpTokens.description") })
203
+ ] }),
204
+ /* @__PURE__ */ jsx(Button, { type: "button", onClick: openCreateModal, children: t("mcpTokens.createButton") })
205
+ ] }),
206
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card px-4 py-3 text-sm", children: [
207
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: t("mcpTokens.endpointTitle") }),
208
+ mcpEndpoint ? /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-2", children: [
209
+ /* @__PURE__ */ jsx("code", { className: "flex-1 overflow-x-auto rounded border bg-muted px-2 py-1 font-mono text-xs", children: mcpEndpoint }),
210
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: copyEndpoint, children: endpointCopied ? t("mcpTokens.endpointCopied") : t("mcpTokens.endpointCopy") })
211
+ ] }) : /* @__PURE__ */ jsx("p", { className: "mt-1 text-muted-foreground", children: t("mcpTokens.endpointMissing") })
212
+ ] }),
213
+ /* @__PURE__ */ jsx("div", { className: "rounded-md border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800 dark:border-yellow-700 dark:bg-yellow-950 dark:text-yellow-200", children: t("mcpTokens.inertBanner") }),
214
+ /* @__PURE__ */ jsx("section", { className: "space-y-3", children: loading ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("mcpTokens.loading") }) : loadError ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
215
+ t("mcpTokens.error"),
216
+ ": ",
217
+ loadError
218
+ ] }) : !tokens || tokens.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("mcpTokens.listEmpty") }) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs(Table, { children: [
219
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
220
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnPrefix") }),
221
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnScope") }),
222
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnCreated") }),
223
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnLastUsed") }),
224
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnStatus") }),
225
+ /* @__PURE__ */ jsx(TableHead, { children: t("common.actions") })
226
+ ] }) }),
227
+ /* @__PURE__ */ jsx(TableBody, { children: tokens.map((tok) => {
228
+ const status = tokenStatus(tok);
229
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
230
+ /* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs", children: tok.prefix }),
231
+ /* @__PURE__ */ jsx(TableCell, { className: "text-sm", children: scopeLabel(tok.scope.siteId) }),
232
+ /* @__PURE__ */ jsxs(TableCell, { className: "text-xs text-muted-foreground", children: [
233
+ /* @__PURE__ */ jsx("span", { title: tok.createdAt, children: relativeTime(tok.createdAt) }),
234
+ tok.createdByEmail && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-muted-foreground/70", children: [
235
+ "by ",
236
+ tok.createdByEmail
237
+ ] })
238
+ ] }),
239
+ /* @__PURE__ */ jsx(TableCell, { className: "text-xs text-muted-foreground", children: tok.lastUsedAt ? /* @__PURE__ */ jsx("span", { title: tok.lastUsedAt, children: relativeTime(tok.lastUsedAt) }) : t("mcpTokens.lastUsedNever") }),
240
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
241
+ "span",
242
+ {
243
+ className: status === "active" ? "text-sm font-medium text-green-700 dark:text-green-400" : "text-sm text-muted-foreground",
244
+ children: status === "active" ? t("mcpTokens.statusActive") : status === "revoked" ? t("mcpTokens.statusRevoked") : t("mcpTokens.statusExpired")
245
+ }
246
+ ) }),
247
+ /* @__PURE__ */ jsx(TableCell, { children: status === "active" && /* @__PURE__ */ jsx(
248
+ Button,
249
+ {
250
+ type: "button",
251
+ variant: "outline",
252
+ size: "sm",
253
+ onClick: () => handleRevoke(tok.hash),
254
+ children: t("mcpTokens.revoke")
255
+ }
256
+ ) })
257
+ ] }, tok.hash);
258
+ }) })
259
+ ] }) }) }),
260
+ showCreateModal && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-md rounded-md border bg-card p-6 shadow-lg", children: [
261
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("mcpTokens.createModalTitle") }),
262
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-4", children: [
263
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
264
+ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", htmlFor: "mcp-scope", children: t("mcpTokens.scopeLabel") }),
265
+ /* @__PURE__ */ jsxs(
266
+ "select",
267
+ {
268
+ id: "mcp-scope",
269
+ className: "w-full rounded-md border bg-background px-2 py-1.5 text-sm",
270
+ value: scopeSiteId ?? "",
271
+ onChange: (e) => setScopeSiteId(e.target.value === "" ? null : e.target.value),
272
+ children: [
273
+ /* @__PURE__ */ jsx("option", { value: "", children: t("mcpTokens.scopeAll") }),
274
+ sites.map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.name }, s.id))
275
+ ]
276
+ }
277
+ )
278
+ ] }),
279
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
280
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("mcpTokens.expirationLabel") }),
281
+ ["never", "30days", "90days", "custom"].map((preset) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
282
+ /* @__PURE__ */ jsx(
283
+ "input",
284
+ {
285
+ type: "radio",
286
+ name: "mcp-expiration",
287
+ value: preset,
288
+ checked: expPreset === preset,
289
+ onChange: () => setExpPreset(preset)
290
+ }
291
+ ),
292
+ t(`mcpTokens.expiration${preset.charAt(0).toUpperCase() + preset.slice(1)}`)
293
+ ] }, preset)),
294
+ expPreset === "custom" && /* @__PURE__ */ jsx(
295
+ "input",
296
+ {
297
+ type: "date",
298
+ className: "mt-1 block rounded-md border bg-background px-2 py-1 text-sm",
299
+ value: customDate,
300
+ min: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
301
+ onChange: (e) => setCustomDate(e.target.value)
302
+ }
303
+ )
304
+ ] }),
305
+ createError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: createError })
306
+ ] }),
307
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-2", children: [
308
+ /* @__PURE__ */ jsx(
309
+ Button,
310
+ {
311
+ type: "button",
312
+ variant: "outline",
313
+ onClick: () => setShowCreateModal(false),
314
+ children: t("common.cancel")
315
+ }
316
+ ),
317
+ /* @__PURE__ */ jsx(
318
+ Button,
319
+ {
320
+ type: "button",
321
+ disabled: creating || expPreset === "custom" && !customDate,
322
+ onClick: () => void handleIssue(),
323
+ children: creating ? t("mcpTokens.issuing") : t("mcpTokens.issueButton")
324
+ }
325
+ )
326
+ ] })
327
+ ] }) }),
328
+ revealedPlain && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-2xl rounded-md border bg-card p-6 shadow-lg", children: [
329
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("mcpTokens.revealTitle") }),
330
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: t("mcpTokens.revealHint") }),
331
+ /* @__PURE__ */ jsx("div", { className: "mt-4 rounded-md border bg-muted/50 p-3", children: /* @__PURE__ */ jsx("code", { className: "block select-all break-all font-mono text-xs", children: revealedPlain }) }),
332
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
333
+ /* @__PURE__ */ jsx(
334
+ Button,
335
+ {
336
+ type: "button",
337
+ variant: "outline",
338
+ onClick: () => void copyToClipboard(),
339
+ children: copied ? t("mcpTokens.copied") : t("mcpTokens.copy")
340
+ }
341
+ ),
342
+ /* @__PURE__ */ jsx(
343
+ Button,
344
+ {
345
+ type: "button",
346
+ onClick: () => {
347
+ setRevealedPlain(null);
348
+ setCopied(false);
349
+ },
350
+ children: t("mcpTokens.done")
351
+ }
352
+ )
353
+ ] })
354
+ ] }) })
355
+ ] });
356
+ }
357
+
358
+ export {
359
+ McpTokensView
360
+ };
@@ -34,9 +34,13 @@ var en_default = {
34
34
  posts: "Posts",
35
35
  media: "Media",
36
36
  sites: "Sites",
37
+ users: "Users",
38
+ mcpTokens: "MCP tokens",
37
39
  viewSite: "View site",
38
40
  signOut: "Sign out",
39
- site: "Site"
41
+ site: "Site",
42
+ openMenu: "Open menu",
43
+ closeMenu: "Close menu"
40
44
  },
41
45
  dashboard: {
42
46
  title: "Dashboard",
@@ -76,6 +80,20 @@ var en_default = {
76
80
  tagsPlaceholder: "comma, separated, tags",
77
81
  tagsHint: "Used to group posts on tag pages (e.g. /tag/tech).",
78
82
  status: "Status",
83
+ noLayout: "No layout",
84
+ noLayoutHint: "Serve this post as bare HTML \u2014 no theme header, footer, or root layout. The body is returned as the entire response. Best paired with format: HTML when the body contains a full <!DOCTYPE html>\u2026</html> document.",
85
+ formatStaticLabel: "Static (zip / file bundle)",
86
+ static: {
87
+ pick: "Pick a .zip or one or more files",
88
+ pickHint: "Bundle assets must reference each other with relative paths only (no leading /). The zip's contents are extracted into a directory under your post slug and served verbatim \u2014 no theme chrome.",
89
+ currentBundle: "Current bundle: {count} files (entry: {entrypoint})",
90
+ pendingBundle: "Pending upload: {count} files, {size}",
91
+ issuesTitle: "{count} validation issue(s)",
92
+ issuesHint: "Fix the listed paths and re-upload. Absolute (/foo) and protocol-relative (//cdn.example/foo) references break under the bundle's URL prefix.",
93
+ emptyBundle: "No usable files found in the selection.",
94
+ noBundle: "Pick a bundle before saving.",
95
+ previewHint: "Static bundles render as the served HTML at /<slug>/. Preview by saving and visiting the public URL."
96
+ },
79
97
  saveChanges: "Save changes",
80
98
  createPost: "Create post",
81
99
  delete: "Delete",
@@ -147,6 +165,25 @@ var en_default = {
147
165
  label: "Site"
148
166
  }
149
167
  },
168
+ users: {
169
+ list: {
170
+ title: "Users",
171
+ description: "Promote signed-up users to admin or editor. Sign-ups always start with no role and must be granted access here.",
172
+ columnEmail: "Email",
173
+ columnRole: "Role",
174
+ columnActions: "Actions",
175
+ save: "Save",
176
+ saving: "Saving...",
177
+ saved: "Saved.",
178
+ cannotEditSelf: "You cannot change your own role.",
179
+ empty: "No users yet.",
180
+ loading: "Loading users...",
181
+ error: "Failed to load users",
182
+ roleAdmin: "Admin",
183
+ roleEditor: "Editor",
184
+ roleNone: "None"
185
+ }
186
+ },
150
187
  theme: {
151
188
  title: "Theme",
152
189
  activeLabel: "Active theme",
@@ -164,7 +201,14 @@ var en_default = {
164
201
  customizationHeading: "{theme} customization",
165
202
  customizationHint: "Empty input resets to the manifest default.",
166
203
  lengthHelp: "CSS length, e.g. 0.5rem, 4px.",
167
- imagePlaceholder: "https://\u2026 or /media/\u2026"
204
+ imagePlaceholder: "https://\u2026 or /media/\u2026",
205
+ colorScheme: {
206
+ label: "Color scheme",
207
+ hint: "How the public site picks between the theme's light and dark token palettes.",
208
+ auto: "Auto (follow visitor's system)",
209
+ light: "Light only",
210
+ dark: "Dark only"
211
+ }
168
212
  },
169
213
  editor: {
170
214
  linkPrompt: "URL",
@@ -225,6 +269,45 @@ var en_default = {
225
269
  userExists: "An account with this email already exists."
226
270
  }
227
271
  },
272
+ mcpTokens: {
273
+ title: "MCP tokens",
274
+ description: "Access tokens for the HTTP MCP endpoint. Use them in Claude Desktop / Cursor / any MCP-aware client to read and write this CMS over the wire.",
275
+ endpointTitle: "MCP endpoint",
276
+ endpointCopy: "Copy",
277
+ endpointCopied: "Copied",
278
+ endpointMissing: "Not deployed yet. Run `npm run sandbox` or push to Amplify Hosting to provision the endpoint URL.",
279
+ inertBanner: "Heads up: the endpoint validates tokens, but tool dispatch (list_posts / create_post / etc.) lands in v0.2 Phase 4. Valid tokens currently get a stub 200 response.",
280
+ createButton: "Create token",
281
+ createModalTitle: "Create token",
282
+ scopeLabel: "Scope",
283
+ scopeAll: "All sites",
284
+ expirationLabel: "Expiration",
285
+ expirationNever: "Never",
286
+ expiration30days: "30 days",
287
+ expiration90days: "90 days",
288
+ expirationCustom: "Custom date",
289
+ issueButton: "Issue token",
290
+ issuing: "Issuing...",
291
+ revealTitle: "Token issued",
292
+ revealHint: "This is the only time you will see this token. Copy it now \u2014 closing this dialog discards it permanently.",
293
+ copy: "Copy",
294
+ copied: "Copied!",
295
+ done: "Done",
296
+ loading: "Loading tokens...",
297
+ error: "Failed to load tokens",
298
+ listEmpty: "No tokens yet. Click 'Create token' to issue your first one.",
299
+ columnPrefix: "Prefix",
300
+ columnScope: "Scope",
301
+ columnCreated: "Created",
302
+ columnLastUsed: "Last used",
303
+ columnStatus: "Status",
304
+ lastUsedNever: "Never used",
305
+ statusActive: "Active",
306
+ statusRevoked: "Revoked",
307
+ statusExpired: "Expired",
308
+ revoke: "Revoke",
309
+ revokeConfirm: "Revoke this token? This cannot be undone."
310
+ },
228
311
  public: {
229
312
  back: "\u2190 Back",
230
313
  home: "\u2190 Home",
@@ -269,9 +352,13 @@ var ja_default = {
269
352
  posts: "\u8A18\u4E8B",
270
353
  media: "\u30E1\u30C7\u30A3\u30A2",
271
354
  sites: "\u30B5\u30A4\u30C8",
355
+ users: "\u30E6\u30FC\u30B6\u30FC",
356
+ mcpTokens: "MCP \u30C8\u30FC\u30AF\u30F3",
272
357
  viewSite: "\u30B5\u30A4\u30C8\u3092\u8868\u793A",
273
358
  signOut: "\u30B5\u30A4\u30F3\u30A2\u30A6\u30C8",
274
- site: "\u30B5\u30A4\u30C8"
359
+ site: "\u30B5\u30A4\u30C8",
360
+ openMenu: "\u30E1\u30CB\u30E5\u30FC\u3092\u958B\u304F",
361
+ closeMenu: "\u30E1\u30CB\u30E5\u30FC\u3092\u9589\u3058\u308B"
275
362
  },
276
363
  dashboard: {
277
364
  title: "\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9",
@@ -311,6 +398,20 @@ var ja_default = {
311
398
  tagsPlaceholder: "\u30AB\u30F3\u30DE, \u533A\u5207\u308A, \u30BF\u30B0",
312
399
  tagsHint: "\u30BF\u30B0\u30DA\u30FC\u30B8\u3067\u8A18\u4E8B\u3092\u30B0\u30EB\u30FC\u30D4\u30F3\u30B0 (\u4F8B: /tag/tech)\u3002",
313
400
  status: "\u30B9\u30C6\u30FC\u30BF\u30B9",
401
+ noLayout: "\u30EC\u30A4\u30A2\u30A6\u30C8\u3092\u4F7F\u308F\u306A\u3044",
402
+ noLayoutHint: "\u30C6\u30FC\u30DE\u306E\u30D8\u30C3\u30C0\u30FC / \u30D5\u30C3\u30BF\u30FC / root layout \u3092\u4ECB\u3055\u305A\u3001\u672C\u6587\u3092 HTTP \u30EC\u30B9\u30DD\u30F3\u30B9\u672C\u4F53\u3068\u3057\u3066\u305D\u306E\u307E\u307E\u8FD4\u3057\u307E\u3059\u3002\u672C\u6587\u306B <!DOCTYPE html>\u2026</html> \u304C\u542B\u307E\u308C\u308B\u5834\u5408\u306F\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8: HTML \u3068\u306E\u7D44\u307F\u5408\u308F\u305B\u304C\u6700\u9069\u3067\u3059\u3002",
403
+ formatStaticLabel: "Static (zip / \u30D5\u30A1\u30A4\u30EB\u4E00\u62EC)",
404
+ static: {
405
+ pick: ".zip \u307E\u305F\u306F\u8907\u6570\u30D5\u30A1\u30A4\u30EB\u3092\u9078\u629E",
406
+ pickHint: "\u30D0\u30F3\u30C9\u30EB\u5185\u306E\u30D5\u30A1\u30A4\u30EB\u9593\u53C2\u7167\u306F\u76F8\u5BFE\u30D1\u30B9\u306E\u307F (\u5148\u982D / \u306A\u3057)\u3002zip \u306E\u4E2D\u8EAB\u306F\u8A18\u4E8B slug \u306E\u914D\u4E0B\u306B\u5C55\u958B\u3055\u308C\u3001\u30C6\u30FC\u30DE\u88C5\u98FE\u3092\u4ECB\u3055\u305A\u305D\u306E\u307E\u307E\u914D\u4FE1\u3055\u308C\u307E\u3059\u3002",
407
+ currentBundle: "\u73FE\u5728\u306E\u30D0\u30F3\u30C9\u30EB: {count} \u30D5\u30A1\u30A4\u30EB (entry: {entrypoint})",
408
+ pendingBundle: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u5F85\u3061: {count} \u30D5\u30A1\u30A4\u30EB, {size}",
409
+ issuesTitle: "{count} \u4EF6\u306E\u691C\u8A3C\u30A8\u30E9\u30FC",
410
+ issuesHint: "\u8A72\u5F53\u30D1\u30B9\u3092\u4FEE\u6B63\u3057\u3066\u518D\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u7D76\u5BFE\u30D1\u30B9 (/foo) \u3084\u30D7\u30ED\u30C8\u30B3\u30EB\u7701\u7565 (//cdn.example/foo) \u306F\u30D0\u30F3\u30C9\u30EB\u306E URL prefix \u4E0B\u3067\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002",
411
+ emptyBundle: "\u9078\u629E\u30D5\u30A1\u30A4\u30EB\u304B\u3089\u53D6\u308A\u51FA\u305B\u308B\u4E2D\u8EAB\u304C\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
412
+ noBundle: "\u4FDD\u5B58\u524D\u306B\u30D0\u30F3\u30C9\u30EB\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
413
+ previewHint: "Static \u30D0\u30F3\u30C9\u30EB\u306F /<slug>/ \u304B\u3089\u5B9F HTML \u3068\u3057\u3066\u914D\u4FE1\u3055\u308C\u307E\u3059\u3002\u4FDD\u5B58\u5F8C\u306B\u516C\u958B URL \u3092\u958B\u3044\u3066\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
414
+ },
314
415
  saveChanges: "\u5909\u66F4\u3092\u4FDD\u5B58",
315
416
  createPost: "\u8A18\u4E8B\u3092\u4F5C\u6210",
316
417
  delete: "\u524A\u9664",
@@ -382,6 +483,25 @@ var ja_default = {
382
483
  label: "\u30B5\u30A4\u30C8"
383
484
  }
384
485
  },
486
+ users: {
487
+ list: {
488
+ title: "\u30E6\u30FC\u30B6\u30FC",
489
+ description: "\u30B5\u30A4\u30F3\u30A2\u30C3\u30D7\u6E08\u307F\u30E6\u30FC\u30B6\u30FC\u3092\u7BA1\u7406\u8005\u307E\u305F\u306F\u7DE8\u96C6\u8005\u306B\u6607\u683C\u3057\u307E\u3059\u3002\u30B5\u30A4\u30F3\u30A2\u30C3\u30D7\u76F4\u5F8C\u306F\u30ED\u30FC\u30EB\u7121\u3057\u306E\u305F\u3081\u3001\u3053\u3053\u3067\u30A2\u30AF\u30BB\u30B9\u3092\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
490
+ columnEmail: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9",
491
+ columnRole: "\u30ED\u30FC\u30EB",
492
+ columnActions: "\u64CD\u4F5C",
493
+ save: "\u4FDD\u5B58",
494
+ saving: "\u4FDD\u5B58\u4E2D...",
495
+ saved: "\u4FDD\u5B58\u3057\u307E\u3057\u305F\u3002",
496
+ cannotEditSelf: "\u81EA\u5206\u81EA\u8EAB\u306E\u30ED\u30FC\u30EB\u306F\u5909\u66F4\u3067\u304D\u307E\u305B\u3093\u3002",
497
+ empty: "\u30E6\u30FC\u30B6\u30FC\u304C\u3044\u307E\u305B\u3093\u3002",
498
+ loading: "\u30E6\u30FC\u30B6\u30FC\u3092\u8AAD\u307F\u8FBC\u307F\u4E2D...",
499
+ error: "\u30E6\u30FC\u30B6\u30FC\u4E00\u89A7\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
500
+ roleAdmin: "\u7BA1\u7406\u8005",
501
+ roleEditor: "\u7DE8\u96C6\u8005",
502
+ roleNone: "\u306A\u3057"
503
+ }
504
+ },
385
505
  theme: {
386
506
  title: "\u30C6\u30FC\u30DE",
387
507
  activeLabel: "\u30A2\u30AF\u30C6\u30A3\u30D6\u30C6\u30FC\u30DE",
@@ -399,7 +519,14 @@ var ja_default = {
399
519
  customizationHeading: "{theme}\u306E\u30AB\u30B9\u30BF\u30DE\u30A4\u30BA",
400
520
  customizationHint: "\u7A7A\u6B04\u306B\u3059\u308B\u3068\u30DE\u30CB\u30D5\u30A7\u30B9\u30C8\u306E\u30C7\u30D5\u30A9\u30EB\u30C8\u5024\u306B\u623B\u308A\u307E\u3059\u3002",
401
521
  lengthHelp: "CSS\u306E\u9577\u3055\u3002\u4F8B: 0.5rem, 4px\u3002",
402
- imagePlaceholder: "https://\u2026 \u307E\u305F\u306F /media/\u2026"
522
+ imagePlaceholder: "https://\u2026 \u307E\u305F\u306F /media/\u2026",
523
+ colorScheme: {
524
+ label: "\u30AB\u30E9\u30FC\u30B9\u30AD\u30FC\u30E0",
525
+ hint: "\u516C\u958B\u30B5\u30A4\u30C8\u3067\u30C6\u30FC\u30DE\u306E\u30E9\u30A4\u30C8/\u30C0\u30FC\u30AF\u4E21\u30D1\u30EC\u30C3\u30C8\u306E\u3069\u3061\u3089\u3092\u4F7F\u3046\u304B\u306E\u9078\u629E\u3002",
526
+ auto: "\u81EA\u52D5 (\u95B2\u89A7\u8005\u306EOS\u8A2D\u5B9A\u306B\u8FFD\u5F93)",
527
+ light: "\u30E9\u30A4\u30C8\u56FA\u5B9A",
528
+ dark: "\u30C0\u30FC\u30AF\u56FA\u5B9A"
529
+ }
403
530
  },
404
531
  editor: {
405
532
  linkPrompt: "URL",
@@ -460,6 +587,45 @@ var ja_default = {
460
587
  userExists: "\u3053\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u306E\u30A2\u30AB\u30A6\u30F3\u30C8\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059\u3002"
461
588
  }
462
589
  },
590
+ mcpTokens: {
591
+ title: "MCP \u30C8\u30FC\u30AF\u30F3",
592
+ description: "HTTP MCP \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u7528\u306E\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\u3002Claude Desktop / Cursor \u306A\u3069\u306E MCP \u5BFE\u5FDC\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u304B\u3089\u3001\u3053\u306E CMS \u306E\u8AAD\u307F\u66F8\u304D\u306B\u4F7F\u3048\u307E\u3059\u3002",
593
+ endpointTitle: "MCP \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8",
594
+ endpointCopy: "\u30B3\u30D4\u30FC",
595
+ endpointCopied: "\u30B3\u30D4\u30FC\u6E08\u307F",
596
+ endpointMissing: "\u307E\u3060\u30C7\u30D7\u30ED\u30A4\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`npm run sandbox` \u3092\u5B9F\u884C\u3059\u308B\u304B Amplify Hosting \u306B push \u3057\u3066\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8 URL \u3092\u30D7\u30ED\u30D3\u30B8\u30E7\u30CB\u30F3\u30B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
597
+ inertBanner: "\u6CE8\u610F: \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306F\u30C8\u30FC\u30AF\u30F3\u691C\u8A3C\u3092\u884C\u3044\u307E\u3059\u304C\u3001\u30C4\u30FC\u30EB\u30C7\u30A3\u30B9\u30D1\u30C3\u30C1 (list_posts / create_post \u306A\u3069) \u306F v0.2 Phase 4 \u3067\u5B9F\u88C5\u3055\u308C\u307E\u3059\u3002\u73FE\u5728\u306F\u6709\u52B9\u306A\u30C8\u30FC\u30AF\u30F3\u306B\u5BFE\u3057\u3066 stub \u306E 200 \u30EC\u30B9\u30DD\u30F3\u30B9\u3092\u8FD4\u3057\u307E\u3059\u3002",
598
+ createButton: "\u30C8\u30FC\u30AF\u30F3\u3092\u4F5C\u6210",
599
+ createModalTitle: "\u30C8\u30FC\u30AF\u30F3\u3092\u4F5C\u6210",
600
+ scopeLabel: "\u30B9\u30B3\u30FC\u30D7",
601
+ scopeAll: "\u3059\u3079\u3066\u306E\u30B5\u30A4\u30C8",
602
+ expirationLabel: "\u6709\u52B9\u671F\u9650",
603
+ expirationNever: "\u7121\u671F\u9650",
604
+ expiration30days: "30\u65E5",
605
+ expiration90days: "90\u65E5",
606
+ expirationCustom: "\u65E5\u4ED8\u3092\u6307\u5B9A",
607
+ issueButton: "\u30C8\u30FC\u30AF\u30F3\u3092\u767A\u884C",
608
+ issuing: "\u767A\u884C\u4E2D...",
609
+ revealTitle: "\u30C8\u30FC\u30AF\u30F3\u3092\u767A\u884C\u3057\u307E\u3057\u305F",
610
+ revealHint: "\u3053\u306E\u30C8\u30FC\u30AF\u30F3\u304C\u8868\u793A\u3055\u308C\u308B\u306E\u306F\u4ECA\u56DE\u9650\u308A\u3067\u3059\u3002\u4ECA\u3059\u3050\u30B3\u30D4\u30FC\u3057\u3066\u304F\u3060\u3055\u3044 \u2014 \u30C0\u30A4\u30A2\u30ED\u30B0\u3092\u9589\u3058\u308B\u3068\u6C38\u4E45\u306B\u5931\u308F\u308C\u307E\u3059\u3002",
611
+ copy: "\u30B3\u30D4\u30FC",
612
+ copied: "\u30B3\u30D4\u30FC\u3057\u307E\u3057\u305F",
613
+ done: "\u9589\u3058\u308B",
614
+ loading: "\u30C8\u30FC\u30AF\u30F3\u3092\u8AAD\u307F\u8FBC\u307F\u4E2D...",
615
+ error: "\u30C8\u30FC\u30AF\u30F3\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
616
+ listEmpty: "\u30C8\u30FC\u30AF\u30F3\u304C\u307E\u3060\u3042\u308A\u307E\u305B\u3093\u3002\u300C\u30C8\u30FC\u30AF\u30F3\u3092\u4F5C\u6210\u300D\u3092\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u6700\u521D\u306E\u30C8\u30FC\u30AF\u30F3\u3092\u767A\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
617
+ columnPrefix: "\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9",
618
+ columnScope: "\u30B9\u30B3\u30FC\u30D7",
619
+ columnCreated: "\u4F5C\u6210\u65E5\u6642",
620
+ columnLastUsed: "\u6700\u7D42\u4F7F\u7528",
621
+ columnStatus: "\u30B9\u30C6\u30FC\u30BF\u30B9",
622
+ lastUsedNever: "\u672A\u4F7F\u7528",
623
+ statusActive: "\u6709\u52B9",
624
+ statusRevoked: "\u5931\u52B9",
625
+ statusExpired: "\u671F\u9650\u5207\u308C",
626
+ revoke: "\u5931\u52B9",
627
+ revokeConfirm: "\u3053\u306E\u30C8\u30FC\u30AF\u30F3\u3092\u5931\u52B9\u3055\u305B\u307E\u3059\u304B\uFF1F\u3053\u306E\u64CD\u4F5C\u306F\u5143\u306B\u623B\u305B\u307E\u305B\u3093\u3002"
628
+ },
463
629
  public: {
464
630
  back: "\u2190 \u623B\u308B",
465
631
  home: "\u2190 \u30DB\u30FC\u30E0",
@@ -505,71 +671,8 @@ function translate(dict, key, vars) {
505
671
  });
506
672
  }
507
673
 
508
- // src/lib/media.ts
509
- var state = { outputs: null, cmsConfig: null };
510
- function setAdminMediaContext(outputs, cmsConfig2) {
511
- state.outputs = outputs;
512
- state.cmsConfig = cmsConfig2;
513
- }
514
- function publicMediaUrl(input) {
515
- if (/^https?:\/\//.test(input)) return input;
516
- let path = input.replace(/^\/+/, "");
517
- if (path.startsWith("public/")) path = path.slice("public/".length);
518
- const { outputs, cmsConfig: cmsConfig2 } = state;
519
- const delivery = cmsConfig2?.media?.delivery ?? "nextjs";
520
- if (delivery !== "s3-direct") return `/api/media/${path}`;
521
- const storage = outputs?.storage;
522
- if (!storage) return `/api/media/${path}`;
523
- return `https://${storage.bucket_name}.s3.${storage.aws_region}.amazonaws.com/public/${path}`;
524
- }
525
- function createMedia(outputs, cmsConfig2) {
526
- const storage = outputs.storage;
527
- function s3DirectUrl(path) {
528
- if (!storage) return `/api/media/${path}`;
529
- return `https://${storage.bucket_name}.s3.${storage.aws_region}.amazonaws.com/public/${path}`;
530
- }
531
- function urlFor(input) {
532
- if (/^https?:\/\//.test(input)) return input;
533
- let path = input.replace(/^\/+/, "");
534
- if (path.startsWith("public/")) path = path.slice("public/".length);
535
- const delivery = cmsConfig2.media?.delivery ?? "nextjs";
536
- return delivery === "s3-direct" ? s3DirectUrl(path) : `/api/media/${path}`;
537
- }
538
- return { publicMediaUrl: urlFor };
539
- }
540
-
541
- // src/lib/admin-site-client.ts
542
- import { DEFAULT_SITE_ID, isMultiSite } from "ampless";
543
- var ADMIN_SITE_COOKIE = "admin-site-id";
544
- var cmsConfig = null;
545
- function setAdminCmsConfig(config) {
546
- cmsConfig = config;
547
- }
548
- function readAdminSiteIdFromCookie() {
549
- if (!cmsConfig) return DEFAULT_SITE_ID;
550
- if (!isMultiSite(cmsConfig)) return DEFAULT_SITE_ID;
551
- const sites = cmsConfig.sites ?? {};
552
- if (typeof document !== "undefined") {
553
- const match = document.cookie.match(
554
- new RegExp(`(?:^|;\\s*)${ADMIN_SITE_COOKIE}=([^;]+)`)
555
- );
556
- if (match) {
557
- const v = decodeURIComponent(match[1]);
558
- if (sites[v]) return v;
559
- }
560
- }
561
- const first = Object.keys(sites)[0];
562
- return first ?? DEFAULT_SITE_ID;
563
- }
564
-
565
674
  export {
566
675
  resolveLocale,
567
676
  getDictionary,
568
- translate,
569
- setAdminMediaContext,
570
- publicMediaUrl,
571
- createMedia,
572
- ADMIN_SITE_COOKIE,
573
- setAdminCmsConfig,
574
- readAdminSiteIdFromCookie
677
+ translate
575
678
  };