@haruhimemoe/next-kit 0.8.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.10.0] - 2026-10-05
10
+
11
+ ### Changed
12
+ - **Breaking.** `next-kit check`'s legal standard requires all five pages of the legal convention: `content/legal/{terms,privacy,your-privacy-rights,copyright,disclaimers}.mdx` (was terms and privacy only).
13
+
14
+ ## [0.9.0] - 2026-10-05
15
+
16
+ ### Added
17
+ - `legal` entry point: the five-page legal convention every service ships (terms, privacy, your-privacy-rights, copyright, disclaimers). A `LegalSite` config (site name, operator, contact email, effective date, data stores, processors, cookies), seven plain server-safe blocks an app drops into its own legal MDX (`LegalContact`, `DataWeKeep`, `Processors`, `YourRights`, `DmcaNotice`, `NoWarranty`, `Changes`; no hooks, no `@haruhimemoe/ui`), and `legalEntries(site, pages?)` for the five standard `ContentEntry` records so apps stop hand-writing the same titles and descriptions. next-kit's first `.tsx` entry point. `LegalSite.hosting` adds an optional line to `DmcaNotice`; `Changes` takes an optional per-page `date`.
18
+
19
+ ### Changed
20
+ - The `@haruhimemoe/ui` peer range also allows `^0.18.0`.
21
+
9
22
  ## [0.8.0] - 2026-10-05
10
23
 
11
24
  ### Changed
@@ -96,7 +109,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
96
109
  - `@haruhimemoe/next-kit/auth-react`: `createSignedInMarker`, `createAccountStore`, `useAccount`, `createAccount`, `RestoreSignedIn` and `osuSignIn`.
97
110
  - `@haruhimemoe/next-kit/testing`: `startMemoryMongo`, `setupTestDb`, `setupMsw` and the fake osu! app env.
98
111
 
99
- [unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.8.0...HEAD
112
+ [unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.10.0...HEAD
113
+ [0.10.0]: https://github.com/haruhimemoe/next-kit/compare/v0.9.0...v0.10.0
114
+ [0.9.0]: https://github.com/haruhimemoe/next-kit/compare/v0.8.0...v0.9.0
100
115
  [0.8.0]: https://github.com/haruhimemoe/next-kit/compare/v0.7.0...v0.8.0
101
116
  [0.6.2]: https://github.com/haruhimemoe/next-kit/compare/v0.6.1...v0.6.2
102
117
  [0.6.1]: https://github.com/haruhimemoe/next-kit/compare/v0.6.0...v0.6.1
package/README.md CHANGED
@@ -13,6 +13,7 @@ The Next.js server plumbing the haruhime.moe tools share. [packs.haruhime.moe](h
13
13
  - **`/testing`:** Vitest helpers: one in-memory MongoDB per run, an msw server that refuses unhandled requests, and a fake env.
14
14
  - **`/api-keys`:** the shared key format (an app prefix like `hpk_` plus 32 random bytes), a key store over `api_keys`, and the `/api/v1` guard with the standard limits.
15
15
  - **`/docs`:** a content registry for an app's docs, guides and legal pages: sections, entries, app-made extra entries (like bb's tag pages), the path helpers a dynamic route needs, and `mdxToMarkdown` to turn bb-flavored MDX into plain Markdown. No runtime imports. **`/docs/files`:** reads the markdown files a registry's entries point at and reports drift between the registry and disk (node:fs).
16
+ - **`/legal`:** the five-page legal convention (terms, privacy, your-privacy-rights, copyright, disclaimers). A `LegalSite` config, seven plain server-safe blocks (`LegalContact`, `DataWeKeep`, `Processors`, `YourRights`, `DmcaNotice`, `NoWarranty`, `Changes`) an app drops into its own legal MDX, and `legalEntries` for the app's content registry.
16
17
  - **`/vcs`:** document history in MongoDB on top of `@haruhimemoe/vcs`: one line of revisions per document, saves merged onto whatever landed since their base, revert, diffs, and autosave pruning.
17
18
 
18
19
  Every name, path, limit and message comes from the caller. There is no root entry point; import a subpath.
@@ -38,6 +39,7 @@ bun add @haruhimemoe/next-kit zod
38
39
  | `api-keys` | `mongodb` ^7.6.0 |
39
40
  | `docs` | nothing else |
40
41
  | `docs/files` | nothing else (`node:fs` is built in) |
42
+ | `legal` | `react` ^19.3.0 (types only, for JSX) |
41
43
  | `vcs` | `mongodb` ^7.6.0, `@haruhimemoe/vcs` ^0.1.0 |
42
44
 
43
45
  ## Use
@@ -226,7 +228,7 @@ Every app is checked against:
226
228
 
227
229
  - **crawl** (always): `robots.ts`, `sitemap.ts`, `llms.txt/route.ts`, `llms-full.txt/route.ts`, and `.well-known/security.txt/route.ts` (each also accepted as a route handler, like `robots.txt/route.ts`).
228
230
  - **brand** (always): `brand/page.tsx`.
229
- - **legal** (always): `legal/page.tsx`, `legal/[x]/page.tsx`, `legal/[x]/md/route.ts` (any dynamic segment name), and `content/legal/terms.mdx` plus `content/legal/privacy.mdx`.
231
+ - **legal** (always): `legal/page.tsx`, `legal/[x]/page.tsx`, `legal/[x]/md/route.ts` (any dynamic segment name), and the five legal convention pages: `content/legal/{terms,privacy,your-privacy-rights,copyright,disclaimers}.mdx`.
230
232
  - **docs**, once `src/app/api/v1` exists or any `content/docs` file does: `docs/page.tsx`, `docs/[x]/page.tsx`, `docs/[x]/md/route.ts`.
231
233
  - **guides**, only once a `content/guides` file exists: the same three files under `guides/`. An app with no guides is never asked for them.
232
234
  - **api**, once `src/app/api/v1` exists: `api/v1/me/route.ts`, `api/v1/openapi.json/route.ts`, `api/me/api-key/route.ts`, and `content/docs/api.mdx`.
@@ -393,6 +395,16 @@ No runtime imports.
393
395
  | `readContentMarkdown(content, section, slug, { root?, siteUrl, transforms? })` | Reads a registered entry's markdown file and converts it with `mdxToMarkdown` (using the entry's `title`). `root` defaults to `process.cwd()`. Returns null for an unregistered slug; rejects (ENOENT) when the slug is registered but its file is missing. |
394
396
  | `contentFileDrift(content, { root? })` | `{ missingFiles, unregistered }`: `missingFiles` lists registered entries with no file on disk (like `"guides/x.mdx"`); `unregistered` lists `.mdx` files on disk with no registry entry. `root` defaults to `process.cwd()`. |
395
397
 
398
+ ### legal
399
+
400
+ | Export | What it does |
401
+ | --- | --- |
402
+ | `LEGAL_SLUGS`, `LegalSlug` | The five standard legal page slugs, in order: `"terms"`, `"privacy"`, `"your-privacy-rights"`, `"copyright"`, `"disclaimers"`. |
403
+ | `LegalSite` | The config every block and `legalEntries` renders from: `siteName`, `operator`, `contactEmail`, `effectiveDate` (`YYYY-MM-DD`), `stores` (`LegalDataStore[]`), `processors` (`LegalProcessor[]`), `cookies` (`string[]`), optional `hosting` (one sentence on what users can post, shown by `DmcaNotice`). |
404
+ | `LegalDataStore`, `LegalProcessor` | One kind of data kept (`what`, `why`), and one third party that processes it (`name`, `purpose`, `link?`). |
405
+ | `LegalContact`, `DataWeKeep`, `Processors`, `YourRights`, `DmcaNotice`, `NoWarranty`, `Changes` | The seven blocks. Each takes `{ site: LegalSite }` and renders plain semantic HTML (no `@haruhimemoe/ui`), so it inherits the app's MDX prose styling. `DataWeKeep` skips the cookies list when `site.cookies` is empty. `Processors` links a processor that has a `link`, and plain-texts one that doesn't. `DmcaNotice` prints `site.hosting` only when set. `Changes` also takes an optional `date` for a page updated on its own day. |
406
+ | `legalEntries(site, pages?)` | The five `ContentEntry` records (`docs`'s registry shape) for the legal convention, with the default title and description (`site.siteName` filled in) and `lastUpdated` set to `site.effectiveDate`. `pages` overrides any field per slug; everything else keeps the default. |
407
+
396
408
  ### vcs
397
409
 
398
410
  | Export | What it does |
@@ -2,14 +2,14 @@
2
2
  * @file src/check/standards.ts
3
3
  * @desc The routes and content files every haruhime app serves, checked against an app's route
4
4
  * file list (paths under src/app) and content file list (paths under content/). Crawl and
5
- * brand are always checked; legal always checks its three routes plus its two content
6
- * files; docs joins in once api/v1 exists or a content/docs file does; guides joins in
7
- * only once a content/guides file does; the API standard, once api/v1 exists, checks its
5
+ * brand are always checked; legal always checks its three routes plus the five pages of
6
+ * the legal convention (LEGAL_SLUGS); docs joins in once api/v1 exists or a content/docs
7
+ * file does; guides joins in only once a content/guides file does; the API standard, once api/v1 exists, checks its
8
8
  * three routes plus content/docs/api.mdx. Checks files only: the content registry itself
9
9
  * is never parsed.
10
10
  * @author David @dvhsh (https://dvh.sh)
11
11
  * @created Sat Oct 3, 2026
12
- * @modified Sun Oct 4, 2026
12
+ * @modified Mon Oct 5, 2026
13
13
  */
14
14
  /** One standard's outcome. `missing` entries carry their own prefix (src/app/... or content/...). */
15
15
  export type StandardResult = {
@@ -2,15 +2,16 @@
2
2
  * @file src/check/standards.ts
3
3
  * @desc The routes and content files every haruhime app serves, checked against an app's route
4
4
  * file list (paths under src/app) and content file list (paths under content/). Crawl and
5
- * brand are always checked; legal always checks its three routes plus its two content
6
- * files; docs joins in once api/v1 exists or a content/docs file does; guides joins in
7
- * only once a content/guides file does; the API standard, once api/v1 exists, checks its
5
+ * brand are always checked; legal always checks its three routes plus the five pages of
6
+ * the legal convention (LEGAL_SLUGS); docs joins in once api/v1 exists or a content/docs
7
+ * file does; guides joins in only once a content/guides file does; the API standard, once api/v1 exists, checks its
8
8
  * three routes plus content/docs/api.mdx. Checks files only: the content registry itself
9
9
  * is never parsed.
10
10
  * @author David @dvhsh (https://dvh.sh)
11
11
  * @created Sat Oct 3, 2026
12
- * @modified Sun Oct 4, 2026
12
+ * @modified Mon Oct 5, 2026
13
13
  */
14
+ import { LEGAL_SLUGS } from "../legal/types.js";
14
15
  /** A route under src/app, matched against any of `patterns`. */
15
16
  const route = (name, ...patterns) => ({
16
17
  missing: `src/app/${name}`,
@@ -37,8 +38,7 @@ const sectionRoutes = (section) => [
37
38
  ];
38
39
  const LEGAL = [
39
40
  ...sectionRoutes("legal"),
40
- content("legal/terms.mdx"),
41
- content("legal/privacy.mdx"),
41
+ ...LEGAL_SLUGS.map((slug) => content(`legal/${slug}.mdx`)),
42
42
  ];
43
43
  const DOCS = sectionRoutes("docs");
44
44
  const GUIDES = sectionRoutes("guides");
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @file src/legal/blocks.tsx
3
+ * @desc The seven legal blocks: plain server-safe React (no hooks) an app drops into its own
4
+ * legal MDX pages, rendered from a `LegalSite` config instead of hand-written boilerplate.
5
+ * Semantic HTML only (section, h2, p, ul, a), so it inherits the app's MDX prose styling.
6
+ * No `@haruhimemoe/ui` dependency: these are plain text blocks, not UI components.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Oct 5, 2026
9
+ * @modified Mon Oct 5, 2026
10
+ */
11
+ import type { ReactElement } from "react";
12
+ import type { LegalSite } from "./types.js";
13
+ /** Every block's props: the site config it renders from. */
14
+ export type LegalBlockProps = {
15
+ site: LegalSite;
16
+ };
17
+ /** `Changes` props: the site config, plus this page's own date when it differs from the site's. */
18
+ export type ChangesProps = LegalBlockProps & {
19
+ date?: string | undefined;
20
+ };
21
+ /**
22
+ * @function LegalContact
23
+ * @param props {LegalBlockProps} the site config
24
+ * @returns {ReactElement} who runs the site and the address for legal questions
25
+ */
26
+ export declare const LegalContact: ({ site }: LegalBlockProps) => ReactElement;
27
+ /**
28
+ * @function DataWeKeep
29
+ * @param props {LegalBlockProps} the site config
30
+ * @returns {ReactElement} what the app stores and why, plus the cookies it sets
31
+ */
32
+ export declare const DataWeKeep: ({ site }: LegalBlockProps) => ReactElement;
33
+ /**
34
+ * @function Processors
35
+ * @param props {LegalBlockProps} the site config
36
+ * @returns {ReactElement} the third parties that process data on the app's behalf
37
+ */
38
+ export declare const Processors: ({ site }: LegalBlockProps) => ReactElement;
39
+ /**
40
+ * @function YourRights
41
+ * @param props {LegalBlockProps} the site config
42
+ * @returns {ReactElement} the GDPR and CCPA rights every visitor has, and how to use them
43
+ */
44
+ export declare const YourRights: ({ site }: LegalBlockProps) => ReactElement;
45
+ /**
46
+ * @function DmcaNotice
47
+ * @param props {LegalBlockProps} the site config
48
+ * @returns {ReactElement} the DMCA agent, and the notice and counter-notice steps
49
+ */
50
+ export declare const DmcaNotice: ({ site }: LegalBlockProps) => ReactElement;
51
+ /**
52
+ * @function NoWarranty
53
+ * @param props {LegalBlockProps} the site config
54
+ * @returns {ReactElement} the "as is" warranty disclaimer and limitation of liability
55
+ */
56
+ export declare const NoWarranty: ({ site }: LegalBlockProps) => ReactElement;
57
+ /**
58
+ * @function Changes
59
+ * @param props {ChangesProps} the site config and an optional per-page date
60
+ * @returns {ReactElement} the standard "we may update this page" line with its effective date
61
+ */
62
+ export declare const Changes: ({ site, date }: ChangesProps) => ReactElement;
@@ -0,0 +1,43 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @function LegalContact
4
+ * @param props {LegalBlockProps} the site config
5
+ * @returns {ReactElement} who runs the site and the address for legal questions
6
+ */
7
+ export const LegalContact = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: "Contact" }), _jsxs("p", { children: [site.operator, " runs ", site.siteName, ". For anything on this page, write to", " ", _jsx("a", { href: `mailto:${site.contactEmail}`, children: site.contactEmail }), "."] })] }));
8
+ /**
9
+ * @function DataWeKeep
10
+ * @param props {LegalBlockProps} the site config
11
+ * @returns {ReactElement} what the app stores and why, plus the cookies it sets
12
+ */
13
+ export const DataWeKeep = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: "What we store" }), _jsx("ul", { children: site.stores.map((store) => (_jsxs("li", { children: [_jsxs("strong", { children: [store.what, "."] }), " ", store.why] }, store.what))) }), site.cookies.length > 0 && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "Cookies" }), _jsx("ul", { children: site.cookies.map((cookie) => (_jsx("li", { children: cookie }, cookie))) })] }))] }));
14
+ /**
15
+ * @function Processors
16
+ * @param props {LegalBlockProps} the site config
17
+ * @returns {ReactElement} the third parties that process data on the app's behalf
18
+ */
19
+ export const Processors = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: "Service providers" }), _jsx("ul", { children: site.processors.map((processor) => (_jsxs("li", { children: [_jsx("strong", { children: processor.link ? _jsx("a", { href: processor.link, children: processor.name }) : processor.name }), " ", processor.purpose] }, processor.name))) })] }));
20
+ /**
21
+ * @function YourRights
22
+ * @param props {LegalBlockProps} the site config
23
+ * @returns {ReactElement} the GDPR and CCPA rights every visitor has, and how to use them
24
+ */
25
+ export const YourRights = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: "Your rights under the GDPR" }), _jsxs("ul", { children: [_jsxs("li", { children: [_jsx("strong", { children: "Access and portability." }), " Get a copy of your data in a machine-readable file."] }), _jsxs("li", { children: [_jsx("strong", { children: "Rectification." }), " Have wrong data corrected."] }), _jsxs("li", { children: [_jsx("strong", { children: "Erasure." }), " Have your data deleted."] }), _jsxs("li", { children: [_jsx("strong", { children: "Restriction." }), " Ask us to pause using your data while a question about it is sorted out."] }), _jsxs("li", { children: [_jsx("strong", { children: "Objection." }), " Object to anything we do on the basis of legitimate interest."] }), _jsxs("li", { children: [_jsx("strong", { children: "Complaint." }), " Complain to your data protection supervisory authority."] })] }), _jsx("h2", { children: "Your rights under the CCPA" }), _jsxs("ul", { children: [_jsxs("li", { children: [_jsx("strong", { children: "Know, delete and correct." }), " Ask what we hold about you, ask us to delete it, and ask us to correct it."] }), _jsxs("li", { children: [_jsx("strong", { children: "Selling and sharing." }), " We don't sell or share personal information."] }), _jsxs("li", { children: [_jsx("strong", { children: "Opting out." }), " We honor Global Privacy Control signals."] })] }), _jsxs("p", { children: ["To use any of these rights, email", " ", _jsx("a", { href: `mailto:${site.contactEmail}`, children: site.contactEmail }), "."] })] }));
26
+ /**
27
+ * @function DmcaNotice
28
+ * @param props {LegalBlockProps} the site config
29
+ * @returns {ReactElement} the DMCA agent, and the notice and counter-notice steps
30
+ */
31
+ export const DmcaNotice = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: "Copyright and DMCA" }), site.hosting ? _jsx("p", { children: site.hosting }) : null, _jsxs("p", { children: ["Our designated agent for copyright notices is", " ", _jsx("a", { href: `mailto:${site.contactEmail}`, children: site.contactEmail }), "."] }), _jsx("h3", { children: "A takedown notice should include" }), _jsxs("ol", { children: [_jsx("li", { children: "your contact information;" }), _jsx("li", { children: "the copyrighted work you believe is infringed;" }), _jsx("li", { children: "the URL or page the material appears on;" }), _jsx("li", { children: "a good-faith statement that the use is not authorized;" }), _jsx("li", { children: "a sworn statement that you're the rights holder, or authorized to act for them;" }), _jsx("li", { children: "your physical or electronic signature." })] }), _jsx("h3", { children: "A counter-notice should include" }), _jsxs("ol", { children: [_jsx("li", { children: "your contact information;" }), _jsx("li", { children: "the material removed and where it appeared;" }), _jsx("li", { children: "a sworn, good-faith statement that it was removed by mistake or misidentification;" }), _jsxs("li", { children: ["your consent to the jurisdiction of your local courts, or ", site.operator, "'s;"] }), _jsx("li", { children: "your physical or electronic signature." })] })] }));
32
+ /**
33
+ * @function NoWarranty
34
+ * @param props {LegalBlockProps} the site config
35
+ * @returns {ReactElement} the "as is" warranty disclaimer and limitation of liability
36
+ */
37
+ export const NoWarranty = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: "Disclaimer of warranties" }), _jsxs("p", { children: [site.siteName, " is provided \"as is\" and \"as available\", without warranties of any kind, express or implied, including merchantability, fitness for a particular purpose and non-infringement."] }), _jsx("h2", { children: "Limitation of liability" }), _jsxs("p", { children: ["To the fullest extent the law allows, ", site.operator, " is not liable for any indirect, incidental, special, consequential or punitive damages, or for any loss of data or accounts, arising from your use of ", site.siteName, "."] })] }));
38
+ /**
39
+ * @function Changes
40
+ * @param props {ChangesProps} the site config and an optional per-page date
41
+ * @returns {ReactElement} the standard "we may update this page" line with its effective date
42
+ */
43
+ export const Changes = ({ site, date }) => (_jsxs("section", { children: [_jsx("h2", { children: "Changes" }), _jsxs("p", { children: ["We may update this page. It was last updated on ", date ?? site.effectiveDate, "."] })] }));
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @file src/legal/entries.ts
3
+ * @desc legalEntries: the five standard `ContentEntry` records (`docs`'s registry shape) for the
4
+ * five-page legal convention, so apps stop hand-writing the same titles and descriptions.
5
+ * An app overrides any field per slug through `pages`; unlisted fields keep the default.
6
+ * @author David @dvhsh (https://dvh.sh)
7
+ * @created Mon Oct 5, 2026
8
+ * @modified Mon Oct 5, 2026
9
+ */
10
+ import type { ContentEntry } from "../docs/registry.js";
11
+ import { type LegalSite, type LegalSlug } from "./types.js";
12
+ /** Per-slug overrides for any `ContentEntry` field, keyed by `LegalSlug`. */
13
+ export type LegalPageOverrides = Partial<Record<LegalSlug, Partial<ContentEntry>>>;
14
+ /**
15
+ * @function legalEntries
16
+ * @param site {LegalSite} the site config; `siteName` fills the default descriptions
17
+ * @param pages {LegalPageOverrides} [pages] per-slug overrides (a different description, a
18
+ * `navTitle`, a page-specific `lastUpdated`); omitted fields keep the default
19
+ * @returns {ContentEntry[]} the five entries, in `LEGAL_SLUGS` order, ready for the `legal` array
20
+ * an app's content registry (`defineContent`) takes
21
+ */
22
+ export declare const legalEntries: (site: LegalSite, pages?: LegalPageOverrides) => ContentEntry[];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @file src/legal/entries.ts
3
+ * @desc legalEntries: the five standard `ContentEntry` records (`docs`'s registry shape) for the
4
+ * five-page legal convention, so apps stop hand-writing the same titles and descriptions.
5
+ * An app overrides any field per slug through `pages`; unlisted fields keep the default.
6
+ * @author David @dvhsh (https://dvh.sh)
7
+ * @created Mon Oct 5, 2026
8
+ * @modified Mon Oct 5, 2026
9
+ */
10
+ import { LEGAL_SLUGS } from "./types.js";
11
+ /** The default title and description for each of the five legal slugs. */
12
+ const DEFAULTS = {
13
+ terms: { title: "Terms of Service", description: "The rules for using {site}." },
14
+ privacy: { title: "Privacy Policy", description: "What {site} stores and why." },
15
+ "your-privacy-rights": {
16
+ title: "GDPR & CCPA",
17
+ description: "Your rights over your data under the GDPR and the CCPA, and how to use them.",
18
+ },
19
+ copyright: {
20
+ title: "Copyright & Takedown",
21
+ description: "How to report a copyright concern, and where to send a DMCA notice.",
22
+ },
23
+ disclaimers: {
24
+ title: "Disclaimers",
25
+ description: "Who {site} isn't affiliated with, and what it doesn't promise.",
26
+ },
27
+ };
28
+ /**
29
+ * @function legalEntries
30
+ * @param site {LegalSite} the site config; `siteName` fills the default descriptions
31
+ * @param pages {LegalPageOverrides} [pages] per-slug overrides (a different description, a
32
+ * `navTitle`, a page-specific `lastUpdated`); omitted fields keep the default
33
+ * @returns {ContentEntry[]} the five entries, in `LEGAL_SLUGS` order, ready for the `legal` array
34
+ * an app's content registry (`defineContent`) takes
35
+ */
36
+ export const legalEntries = (site, pages) => LEGAL_SLUGS.map((slug) => {
37
+ const fallback = DEFAULTS[slug];
38
+ return {
39
+ slug,
40
+ title: fallback.title,
41
+ description: fallback.description.replace("{site}", site.siteName),
42
+ lastUpdated: site.effectiveDate,
43
+ ...pages?.[slug],
44
+ };
45
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @file src/legal/index.ts
3
+ * @desc @haruhimemoe/next-kit/legal: the five-page legal convention (terms, privacy,
4
+ * your-privacy-rights, copyright, disclaimers). `LegalSite` config, seven MDX blocks
5
+ * rendered from it, and `legalEntries` for the app's content registry.
6
+ * @author David @dvhsh (https://dvh.sh)
7
+ * @created Mon Oct 5, 2026
8
+ * @modified Mon Oct 5, 2026
9
+ */
10
+ export { Changes, type ChangesProps, DataWeKeep, DmcaNotice, type LegalBlockProps, LegalContact, NoWarranty, Processors, YourRights, } from "./blocks.js";
11
+ export { type LegalPageOverrides, legalEntries } from "./entries.js";
12
+ export { LEGAL_SLUGS, type LegalDataStore, type LegalProcessor, type LegalSite, type LegalSlug, } from "./types.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @file src/legal/index.ts
3
+ * @desc @haruhimemoe/next-kit/legal: the five-page legal convention (terms, privacy,
4
+ * your-privacy-rights, copyright, disclaimers). `LegalSite` config, seven MDX blocks
5
+ * rendered from it, and `legalEntries` for the app's content registry.
6
+ * @author David @dvhsh (https://dvh.sh)
7
+ * @created Mon Oct 5, 2026
8
+ * @modified Mon Oct 5, 2026
9
+ */
10
+ export { Changes, DataWeKeep, DmcaNotice, LegalContact, NoWarranty, Processors, YourRights, } from "./blocks.js";
11
+ export { legalEntries } from "./entries.js";
12
+ export { LEGAL_SLUGS, } from "./types.js";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file src/legal/types.ts
3
+ * @desc LegalSite: the per-app config the legal blocks and legalEntries render from. No site
4
+ * name, URL, email or data list is hardcoded here; every app passes its own.
5
+ * @author David @dvhsh (https://dvh.sh)
6
+ * @created Mon Oct 5, 2026
7
+ * @modified Mon Oct 5, 2026
8
+ */
9
+ /** A third party that processes data on the app's behalf, like Vercel or MongoDB Atlas. */
10
+ export type LegalProcessor = {
11
+ name: string;
12
+ purpose: string;
13
+ link?: string;
14
+ };
15
+ /** One kind of data the app stores, and why. */
16
+ export type LegalDataStore = {
17
+ what: string;
18
+ why: string;
19
+ };
20
+ /** The five standard legal page slugs every service ships, in the order they're listed. */
21
+ export declare const LEGAL_SLUGS: readonly ["terms", "privacy", "your-privacy-rights", "copyright", "disclaimers"];
22
+ /** One of `LEGAL_SLUGS`. */
23
+ export type LegalSlug = (typeof LEGAL_SLUGS)[number];
24
+ /** The config every legal block and `legalEntries` renders from. */
25
+ export type LegalSite = {
26
+ /** The site's display name, like "packs.haruhime.moe". */
27
+ siteName: string;
28
+ /** Who runs the site, for the contact and "who's responsible" lines. */
29
+ operator: string;
30
+ /** Where a legal request, question or notice goes. */
31
+ contactEmail: string;
32
+ /** YYYY-MM-DD. Shown as this page's "last updated" date by `Changes`. */
33
+ effectiveDate: string;
34
+ /** What the app keeps, in `DataWeKeep`'s order. */
35
+ stores: readonly LegalDataStore[];
36
+ /** Who processes data on the app's behalf, in `Processors`'s order. */
37
+ processors: readonly LegalProcessor[];
38
+ /** The cookies the app sets, one line each, in `DataWeKeep`'s cookie list. */
39
+ cookies: readonly string[];
40
+ /** One sentence on what users can post or upload, shown by `DmcaNotice`. Omitted: no line. */
41
+ hosting?: string | undefined;
42
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @file src/legal/types.ts
3
+ * @desc LegalSite: the per-app config the legal blocks and legalEntries render from. No site
4
+ * name, URL, email or data list is hardcoded here; every app passes its own.
5
+ * @author David @dvhsh (https://dvh.sh)
6
+ * @created Mon Oct 5, 2026
7
+ * @modified Mon Oct 5, 2026
8
+ */
9
+ /** The five standard legal page slugs every service ships, in the order they're listed. */
10
+ export const LEGAL_SLUGS = [
11
+ "terms",
12
+ "privacy",
13
+ "your-privacy-rights",
14
+ "copyright",
15
+ "disclaimers",
16
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haruhimemoe/next-kit",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "The Next.js plumbing the haruhime.moe tools share: JSON route helpers, rate limits and budgets in MongoDB, bearer machine auth, zod env parsing, a connect-once MongoDB client with safe index builds, better-auth with osu! sign-in, the signed-in marker and account store for the browser, SEO: metadata, robots.txt, sitemaps, JSON-LD and llms.txt, and per-app API keys and the /api/v1 guard.",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -70,6 +70,10 @@
70
70
  "types": "./dist/docs/files/index.d.ts",
71
71
  "default": "./dist/docs/files/index.js"
72
72
  },
73
+ "./legal": {
74
+ "types": "./dist/legal/index.d.ts",
75
+ "default": "./dist/legal/index.js"
76
+ },
73
77
  "./vcs": {
74
78
  "types": "./dist/vcs/index.d.ts",
75
79
  "default": "./dist/vcs/index.js"
@@ -104,7 +108,7 @@
104
108
  },
105
109
  "peerDependencies": {
106
110
  "@haruhimemoe/osu": "^0.2.0 || ^0.3.0 || ^0.4.0",
107
- "@haruhimemoe/ui": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0",
111
+ "@haruhimemoe/ui": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0 || ^0.18.0",
108
112
  "@haruhimemoe/vcs": "^0.1.0",
109
113
  "better-auth": "^1.7.5",
110
114
  "mongodb": "^7.6.0",