@realiizlabs/admin 0.15.9 → 0.17.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.
@@ -1,7 +1,7 @@
1
1
  import { c as ImagePayload } from '../validate-C84Ll1cD.cjs';
2
2
  export { I as IMAGE_ERRORS, a as IMAGE_LIMITS, b as ImageLimits, V as ValidatedImages, d as decodedSize, v as validateImages } from '../validate-C84Ll1cD.cjs';
3
3
  import { a as IdentityUser, f as SiteRole, C as CookieStore, d as SiteMember, I as IdentityClient, c as SiteBranding } from '../members-nhbRSYyr.cjs';
4
- import { a as PullRequestStatus } from '../types-BP5G1myE.cjs';
4
+ import { a as PullRequestStatus } from '../types-Cy9UDz--.cjs';
5
5
  import { C as ContentTypeEntry } from '../types-DeHdasdP.cjs';
6
6
  import * as zod from 'zod';
7
7
  import * as zod_v4_core from 'zod/v4/core';
@@ -15,10 +15,15 @@ import '@supabase/supabase-js';
15
15
  * arrives here, lazily, so `next build` with no env set still succeeds.
16
16
  */
17
17
 
18
+ /** The site's repo and how Studio reaches it — the GitHub App (preferred) or a personal access token. */
18
19
  interface RepoConfig {
19
20
  owner: string;
20
21
  repo: string;
21
- token: string;
22
+ app?: {
23
+ appId: string;
24
+ privateKey: string;
25
+ } | null;
26
+ token?: string | null;
22
27
  }
23
28
  interface IdentityConfig {
24
29
  url: string;
@@ -151,6 +156,12 @@ interface PublishInput {
151
156
  } | null;
152
157
  /** Pictures chosen in this session, already shrunk in the browser; committed with the MDX. */
153
158
  images?: ImagePayload[];
159
+ /**
160
+ * The slug this item was opened as (edits only). When the new frontmatter produces a
161
+ * different file name — a date-prefixed event whose date moved — the old file is removed
162
+ * in the same commit, so an edit never leaves a stale copy behind.
163
+ */
164
+ replaces?: string | null;
154
165
  }
155
166
  type PublishResult = {
156
167
  ok: false;
@@ -249,6 +260,7 @@ declare function createStudio(config: StudioConfig): {
249
260
  pendingFor: (path: string) => Promise<PendingChange | null>;
250
261
  pathOf: (typeId: string, slug: string) => Promise<string | null>;
251
262
  listItems: (typeId: string) => Promise<ListItem[]>;
263
+ listSlugs: (typeId: string) => Promise<string[]>;
252
264
  countItems: (typeId: string) => Promise<number>;
253
265
  readItem: (typeId: string, slug: string, opts?: {
254
266
  ref?: string;
@@ -1,7 +1,7 @@
1
1
  import { c as ImagePayload } from '../validate-C84Ll1cD.js';
2
2
  export { I as IMAGE_ERRORS, a as IMAGE_LIMITS, b as ImageLimits, V as ValidatedImages, d as decodedSize, v as validateImages } from '../validate-C84Ll1cD.js';
3
3
  import { a as IdentityUser, f as SiteRole, C as CookieStore, d as SiteMember, I as IdentityClient, c as SiteBranding } from '../members-nhbRSYyr.js';
4
- import { a as PullRequestStatus } from '../types-BP5G1myE.js';
4
+ import { a as PullRequestStatus } from '../types-Cy9UDz--.js';
5
5
  import { C as ContentTypeEntry } from '../types-DeHdasdP.js';
6
6
  import * as zod from 'zod';
7
7
  import * as zod_v4_core from 'zod/v4/core';
@@ -15,10 +15,15 @@ import '@supabase/supabase-js';
15
15
  * arrives here, lazily, so `next build` with no env set still succeeds.
16
16
  */
17
17
 
18
+ /** The site's repo and how Studio reaches it — the GitHub App (preferred) or a personal access token. */
18
19
  interface RepoConfig {
19
20
  owner: string;
20
21
  repo: string;
21
- token: string;
22
+ app?: {
23
+ appId: string;
24
+ privateKey: string;
25
+ } | null;
26
+ token?: string | null;
22
27
  }
23
28
  interface IdentityConfig {
24
29
  url: string;
@@ -151,6 +156,12 @@ interface PublishInput {
151
156
  } | null;
152
157
  /** Pictures chosen in this session, already shrunk in the browser; committed with the MDX. */
153
158
  images?: ImagePayload[];
159
+ /**
160
+ * The slug this item was opened as (edits only). When the new frontmatter produces a
161
+ * different file name — a date-prefixed event whose date moved — the old file is removed
162
+ * in the same commit, so an edit never leaves a stale copy behind.
163
+ */
164
+ replaces?: string | null;
154
165
  }
155
166
  type PublishResult = {
156
167
  ok: false;
@@ -249,6 +260,7 @@ declare function createStudio(config: StudioConfig): {
249
260
  pendingFor: (path: string) => Promise<PendingChange | null>;
250
261
  pathOf: (typeId: string, slug: string) => Promise<string | null>;
251
262
  listItems: (typeId: string) => Promise<ListItem[]>;
263
+ listSlugs: (typeId: string) => Promise<string[]>;
252
264
  countItems: (typeId: string) => Promise<number>;
253
265
  readItem: (typeId: string, slug: string, opts?: {
254
266
  ref?: string;
@@ -1,6 +1,7 @@
1
1
  import 'server-only';
2
2
  import { redirect } from 'next/navigation';
3
3
  import { createServerClient } from '@supabase/ssr';
4
+ import { createSign } from 'crypto';
4
5
  import matter from 'gray-matter';
5
6
  import { unified } from 'unified';
6
7
  import remarkParse from 'remark-parse';
@@ -171,15 +172,15 @@ var ChecksFailedError = class extends Error {
171
172
  }
172
173
  };
173
174
 
174
- // src/git/client.ts
175
- var API = "https://api.github.com";
175
+ // src/git/http.ts
176
+ var GITHUB_API = "https://api.github.com";
176
177
  var GITHUB_TIMEOUT_MS = 1e4;
177
- async function ghFetch(ctx, method, path, body) {
178
- const res = await fetch(API + path, {
178
+ async function githubRequest(method, path, authorization, body, fetchImpl = fetch) {
179
+ const res = await fetchImpl(GITHUB_API + path, {
179
180
  method,
180
181
  signal: AbortSignal.timeout(GITHUB_TIMEOUT_MS),
181
182
  headers: {
182
- Authorization: `Bearer ${ctx.token}`,
183
+ Authorization: authorization,
183
184
  Accept: "application/vnd.github+json",
184
185
  "X-GitHub-Api-Version": "2022-11-28",
185
186
  "User-Agent": "realiizlabs-admin",
@@ -198,6 +199,65 @@ async function ghFetch(ctx, method, path, body) {
198
199
  }
199
200
  return { ok: res.ok, status: res.status, json };
200
201
  }
202
+
203
+ // src/git/app-auth.ts
204
+ var JWT_TTL_S = 9 * 60;
205
+ var REFRESH_BEFORE_MS = 5 * 6e4;
206
+ var cache = /* @__PURE__ */ new Map();
207
+ function b64url(input) {
208
+ return Buffer.from(input).toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
209
+ }
210
+ function appJwt(creds, now = Math.floor(Date.now() / 1e3)) {
211
+ const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
212
+ const payload = b64url(JSON.stringify({ iat: now - 60, exp: now + JWT_TTL_S, iss: creds.appId }));
213
+ const signer = createSign("RSA-SHA256");
214
+ signer.update(`${header}.${payload}`);
215
+ const signature = signer.sign(creds.privateKey.replace(/\\n/g, "\n"));
216
+ return `${header}.${payload}.${b64url(signature)}`;
217
+ }
218
+ function forgetInstallationToken(creds, owner, repo) {
219
+ cache.delete(`${creds.appId}:${owner}/${repo}`);
220
+ }
221
+ async function installationToken(creds, owner, repo, fetchImpl = fetch) {
222
+ const key = `${creds.appId}:${owner}/${repo}`;
223
+ const hit = cache.get(key);
224
+ if (hit && hit.expiresAt - Date.now() > REFRESH_BEFORE_MS) return hit.token;
225
+ const bearer = `Bearer ${appJwt(creds)}`;
226
+ const repoPath2 = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
227
+ const found = await githubRequest("GET", `${repoPath2}/installation`, bearer, void 0, fetchImpl);
228
+ if (found.status === 404) {
229
+ throw new GitHubError(
230
+ `The Realiiz Studio GitHub App isn't installed on ${owner}/${repo}. The repository's owner installs it once \u2014 ask your web team for the link.`,
231
+ { status: 404, path: `${repoPath2}/installation`, body: found.json }
232
+ );
233
+ }
234
+ if (!found.ok) throw new GitHubError(`GitHub ${found.status} looking up the app installation`, { status: found.status, path: `${repoPath2}/installation`, body: found.json });
235
+ const minted = await githubRequest(
236
+ "POST",
237
+ `/app/installations/${found.json.id}/access_tokens`,
238
+ bearer,
239
+ void 0,
240
+ fetchImpl
241
+ );
242
+ if (!minted.ok) throw new GitHubError(`GitHub ${minted.status} minting an installation token`, { status: minted.status, path: "/app/installations/\u2026/access_tokens", body: minted.json });
243
+ cache.set(key, { token: minted.json.token, expiresAt: Date.parse(minted.json.expires_at) });
244
+ return minted.json.token;
245
+ }
246
+
247
+ // src/git/client.ts
248
+ async function credentialFor(ctx) {
249
+ if (ctx.app) return installationToken(ctx.app, ctx.owner, ctx.repo);
250
+ if (ctx.token) return ctx.token;
251
+ throw new GitHubError(`No GitHub credential for ${ctx.owner}/${ctx.repo}: set GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY, or GITHUB_CONTENT_TOKEN.`, { status: 0, path: "", body: null });
252
+ }
253
+ async function ghFetch(ctx, method, path, body) {
254
+ const res = await githubRequest(method, path, `Bearer ${await credentialFor(ctx)}`, body);
255
+ if (res.status === 401 && ctx.app) {
256
+ forgetInstallationToken(ctx.app, ctx.owner, ctx.repo);
257
+ return githubRequest(method, path, `Bearer ${await credentialFor(ctx)}`, body);
258
+ }
259
+ return res;
260
+ }
201
261
  async function ghFetchOrThrow(ctx, method, path, body) {
202
262
  const res = await ghFetch(ctx, method, path, body);
203
263
  if (!res.ok) throw toError(res, path);
@@ -404,6 +464,10 @@ function makeReaders(config, registry, pending) {
404
464
  const entries = await listDirectory(repo(), e.folder);
405
465
  return entries.find((x) => registry.slugOf(e, x.name) === slug)?.path ?? null;
406
466
  };
467
+ const listSlugs = async (typeId) => {
468
+ const items = await listItems(typeId);
469
+ return items.filter((i) => !i.isNew).map((i) => i.slug).sort();
470
+ };
407
471
  const listItems = async (typeId) => {
408
472
  const e = need(typeId);
409
473
  const ctx = repo();
@@ -479,7 +543,7 @@ function makeReaders(config, registry, pending) {
479
543
  }
480
544
  };
481
545
  const prStatus = (number) => getPullRequestStatus(repo(), number);
482
- return { pathOf, listItems, countItems, readItem, lastAuthor, getPullRequest: getPullRequest2, prStatus };
546
+ return { pathOf, listItems, listSlugs, countItems, readItem, lastAuthor, getPullRequest: getPullRequest2, prStatus };
483
547
  }
484
548
 
485
549
  // src/git/branch.ts
@@ -601,18 +665,22 @@ ${input.body.trim()}
601
665
  if (!pictures.ok) return { ok: false, error: pictures.error };
602
666
  const files = [{ path, content }, ...pictures.files];
603
667
  const repo = config.env().repo;
668
+ const oldPath = input.replaces ? await readers.pathOf(entry.id, input.replaces).catch(() => null) : null;
669
+ const remove = oldPath && oldPath !== path ? [oldPath] : [];
670
+ const renamed = remove.length ? `
671
+ Renamed from \`${oldPath}\` to \`${path}\`` : "";
604
672
  if (input.existing && await stillOpen(input.existing.number)) {
605
- await writeFiles(repo, input.existing.branch, files, `Update: ${title}`);
673
+ await writeFiles(repo, input.existing.branch, files, `Update: ${title}`, { remove });
606
674
  redirect(`/admin/pr/${input.existing.number}`);
607
675
  }
608
676
  const branch = `admin/${entry.id}/${fileName.replace(/\.mdx$/, "")}-${stamp()}`;
609
677
  await createBranch(repo, branch);
610
- await writeFiles(repo, branch, files, `Publish: ${title}`);
678
+ await writeFiles(repo, branch, files, `Publish: ${title}`, { remove });
611
679
  const pr = await openPullRequest(repo, branch, {
612
680
  title: `Publish: ${title}`,
613
681
  body: `Published from the dashboard by ${user.email ?? user.id}.
614
682
 
615
- File: \`${path}\``
683
+ File: \`${path}\`${renamed}`
616
684
  });
617
685
  redirect(`/admin/pr/${pr.number}`);
618
686
  };
@@ -1103,7 +1171,7 @@ function makeHandlers(ctx) {
1103
1171
  }
1104
1172
 
1105
1173
  // src/version.ts
1106
- var ADMIN_VERSION = "0.15.9" ;
1174
+ var ADMIN_VERSION = "0.17.0" ;
1107
1175
 
1108
1176
  // src/studio/update.ts
1109
1177
  var PACKAGE_NAME = "@realiizlabs/admin";