@haruhimemoe/next-kit 0.9.0 → 0.11.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 +13 -1
- package/README.md +3 -2
- package/dist/check/standards.d.ts +4 -4
- package/dist/check/standards.js +6 -6
- package/dist/legal/blocks.d.ts +3 -1
- package/dist/legal/blocks.js +8 -7
- package/dist/legal/copy.d.ts +66 -0
- package/dist/legal/copy.js +92 -0
- package/dist/legal/index.d.ts +3 -1
- package/dist/legal/index.js +3 -1
- package/dist/legal/markdown.d.ts +23 -0
- package/dist/legal/markdown.js +115 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.11.0] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `legalMarkdownTransform(site)` in `legal`: a `mdxToMarkdown` `transforms` entry that turns each self-closing legal block tag (`<LegalContact />`, `<DataWeKeep />`, `<Processors />`, `<YourRights />`, `<DmcaNotice />`, `<NoWarranty />`, `<Changes />` and `<Changes date="YYYY-MM-DD" />`) into Markdown carrying the same words as its React block. Without it, an app that drops the legal blocks straight into its `content/legal/*.mdx` loses that text from its `.md` mirrors and llms-full.txt, since `mdxToMarkdown` strips unknown capitalized JSX. The seven blocks' sentences and list items now live in a new `legal/copy.ts`, shared by `blocks.tsx` and `legalMarkdownTransform` so the two outputs can't drift apart; `blocks.tsx`'s rendered output is unchanged.
|
|
13
|
+
|
|
14
|
+
## [0.10.0] - 2026-10-05
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **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).
|
|
18
|
+
|
|
9
19
|
## [0.9.0] - 2026-10-05
|
|
10
20
|
|
|
11
21
|
### Added
|
|
@@ -104,7 +114,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
104
114
|
- `@haruhimemoe/next-kit/auth-react`: `createSignedInMarker`, `createAccountStore`, `useAccount`, `createAccount`, `RestoreSignedIn` and `osuSignIn`.
|
|
105
115
|
- `@haruhimemoe/next-kit/testing`: `startMemoryMongo`, `setupTestDb`, `setupMsw` and the fake osu! app env.
|
|
106
116
|
|
|
107
|
-
[unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.
|
|
117
|
+
[unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.10.0...HEAD
|
|
118
|
+
[0.10.0]: https://github.com/haruhimemoe/next-kit/compare/v0.9.0...v0.10.0
|
|
119
|
+
[0.9.0]: https://github.com/haruhimemoe/next-kit/compare/v0.8.0...v0.9.0
|
|
108
120
|
[0.8.0]: https://github.com/haruhimemoe/next-kit/compare/v0.7.0...v0.8.0
|
|
109
121
|
[0.6.2]: https://github.com/haruhimemoe/next-kit/compare/v0.6.1...v0.6.2
|
|
110
122
|
[0.6.1]: https://github.com/haruhimemoe/next-kit/compare/v0.6.0...v0.6.1
|
package/README.md
CHANGED
|
@@ -13,7 +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,
|
|
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, `legalEntries` for the app's content registry, and `legalMarkdownTransform` so those blocks survive `mdxToMarkdown`'s `.md` mirrors and llms-full.txt instead of being dropped as unknown JSX.
|
|
17
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.
|
|
18
18
|
|
|
19
19
|
Every name, path, limit and message comes from the caller. There is no root entry point; import a subpath.
|
|
@@ -228,7 +228,7 @@ Every app is checked against:
|
|
|
228
228
|
|
|
229
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`).
|
|
230
230
|
- **brand** (always): `brand/page.tsx`.
|
|
231
|
-
- **legal** (always): `legal/page.tsx`, `legal/[x]/page.tsx`, `legal/[x]/md/route.ts` (any dynamic segment name), and
|
|
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`.
|
|
232
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`.
|
|
233
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.
|
|
234
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`.
|
|
@@ -404,6 +404,7 @@ No runtime imports.
|
|
|
404
404
|
| `LegalDataStore`, `LegalProcessor` | One kind of data kept (`what`, `why`), and one third party that processes it (`name`, `purpose`, `link?`). |
|
|
405
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
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
|
+
| `legalMarkdownTransform(site)` | A `transforms` entry for `mdxToMarkdown` (see `docs`): replaces each self-closing legal block tag (`<LegalContact />`, ..., `<Changes />` or `<Changes date="YYYY-MM-DD" />`) with Markdown carrying the same words as its React block, so an app's `.md` mirrors and llms-full.txt don't silently lose the legal text. |
|
|
407
408
|
|
|
408
409
|
### vcs
|
|
409
410
|
|
|
@@ -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
|
|
6
|
-
*
|
|
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
|
|
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 = {
|
package/dist/check/standards.js
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
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
|
|
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(
|
|
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");
|
package/dist/legal/blocks.d.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* @desc The seven legal blocks: plain server-safe React (no hooks) an app drops into its own
|
|
4
4
|
* legal MDX pages, rendered from a `LegalSite` config instead of hand-written boilerplate.
|
|
5
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.
|
|
6
|
+
* No `@haruhimemoe/ui` dependency: these are plain text blocks, not UI components. The
|
|
7
|
+
* sentences and list items come from ./copy.js, shared with markdown.ts's
|
|
8
|
+
* `legalMarkdownTransform` so the two outputs can't drift apart.
|
|
7
9
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
10
|
* @created Mon Oct 5, 2026
|
|
9
11
|
* @modified Mon Oct 5, 2026
|
package/dist/legal/blocks.js
CHANGED
|
@@ -1,43 +1,44 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CCPA_HEADING, CCPA_RIGHTS, CHANGES_HEADING, CONTACT_HEADING, COOKIES_HEADING, COUNTER_NOTICE_HEADING, COUNTER_NOTICE_ITEMS_BEFORE_JURISDICTION, COUNTER_NOTICE_SIGNATURE, changesParagraph, contactIntro, counterNoticeJurisdiction, DATA_HEADING, DMCA_AGENT_INTRO, DMCA_HEADING, GDPR_HEADING, GDPR_RIGHTS, LIABILITY_HEADING, liabilityParagraph, PROCESSORS_HEADING, RIGHTS_CLOSING_INTRO, TAKEDOWN_HEADING, TAKEDOWN_ITEMS, WARRANTY_HEADING, warrantyParagraph, } from "./copy.js";
|
|
2
3
|
/**
|
|
3
4
|
* @function LegalContact
|
|
4
5
|
* @param props {LegalBlockProps} the site config
|
|
5
6
|
* @returns {ReactElement} who runs the site and the address for legal questions
|
|
6
7
|
*/
|
|
7
|
-
export const LegalContact = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children:
|
|
8
|
+
export const LegalContact = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: CONTACT_HEADING }), _jsxs("p", { children: [contactIntro(site), _jsx("a", { href: `mailto:${site.contactEmail}`, children: site.contactEmail }), "."] })] }));
|
|
8
9
|
/**
|
|
9
10
|
* @function DataWeKeep
|
|
10
11
|
* @param props {LegalBlockProps} the site config
|
|
11
12
|
* @returns {ReactElement} what the app stores and why, plus the cookies it sets
|
|
12
13
|
*/
|
|
13
|
-
export const DataWeKeep = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children:
|
|
14
|
+
export const DataWeKeep = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: DATA_HEADING }), _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_HEADING }), _jsx("ul", { children: site.cookies.map((cookie) => (_jsx("li", { children: cookie }, cookie))) })] }))] }));
|
|
14
15
|
/**
|
|
15
16
|
* @function Processors
|
|
16
17
|
* @param props {LegalBlockProps} the site config
|
|
17
18
|
* @returns {ReactElement} the third parties that process data on the app's behalf
|
|
18
19
|
*/
|
|
19
|
-
export const Processors = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children:
|
|
20
|
+
export const Processors = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: PROCESSORS_HEADING }), _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
|
/**
|
|
21
22
|
* @function YourRights
|
|
22
23
|
* @param props {LegalBlockProps} the site config
|
|
23
24
|
* @returns {ReactElement} the GDPR and CCPA rights every visitor has, and how to use them
|
|
24
25
|
*/
|
|
25
|
-
export const YourRights = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children:
|
|
26
|
+
export const YourRights = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: GDPR_HEADING }), _jsx("ul", { children: GDPR_RIGHTS.map((right) => (_jsxs("li", { children: [_jsx("strong", { children: right.term }), " ", right.desc] }, right.term))) }), _jsx("h2", { children: CCPA_HEADING }), _jsx("ul", { children: CCPA_RIGHTS.map((right) => (_jsxs("li", { children: [_jsx("strong", { children: right.term }), " ", right.desc] }, right.term))) }), _jsxs("p", { children: [RIGHTS_CLOSING_INTRO, _jsx("a", { href: `mailto:${site.contactEmail}`, children: site.contactEmail }), "."] })] }));
|
|
26
27
|
/**
|
|
27
28
|
* @function DmcaNotice
|
|
28
29
|
* @param props {LegalBlockProps} the site config
|
|
29
30
|
* @returns {ReactElement} the DMCA agent, and the notice and counter-notice steps
|
|
30
31
|
*/
|
|
31
|
-
export const DmcaNotice = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children:
|
|
32
|
+
export const DmcaNotice = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: DMCA_HEADING }), site.hosting ? _jsx("p", { children: site.hosting }) : null, _jsxs("p", { children: [DMCA_AGENT_INTRO, _jsx("a", { href: `mailto:${site.contactEmail}`, children: site.contactEmail }), "."] }), _jsx("h3", { children: TAKEDOWN_HEADING }), _jsx("ol", { children: TAKEDOWN_ITEMS.map((item) => (_jsx("li", { children: item }, item))) }), _jsx("h3", { children: COUNTER_NOTICE_HEADING }), _jsxs("ol", { children: [COUNTER_NOTICE_ITEMS_BEFORE_JURISDICTION.map((item) => (_jsx("li", { children: item }, item))), _jsx("li", { children: counterNoticeJurisdiction(site) }), _jsx("li", { children: COUNTER_NOTICE_SIGNATURE })] })] }));
|
|
32
33
|
/**
|
|
33
34
|
* @function NoWarranty
|
|
34
35
|
* @param props {LegalBlockProps} the site config
|
|
35
36
|
* @returns {ReactElement} the "as is" warranty disclaimer and limitation of liability
|
|
36
37
|
*/
|
|
37
|
-
export const NoWarranty = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children:
|
|
38
|
+
export const NoWarranty = ({ site }) => (_jsxs("section", { children: [_jsx("h2", { children: WARRANTY_HEADING }), _jsx("p", { children: warrantyParagraph(site) }), _jsx("h2", { children: LIABILITY_HEADING }), _jsx("p", { children: liabilityParagraph(site) })] }));
|
|
38
39
|
/**
|
|
39
40
|
* @function Changes
|
|
40
41
|
* @param props {ChangesProps} the site config and an optional per-page date
|
|
41
42
|
* @returns {ReactElement} the standard "we may update this page" line with its effective date
|
|
42
43
|
*/
|
|
43
|
-
export const Changes = ({ site, date }) => (_jsxs("section", { children: [_jsx("h2", { children:
|
|
44
|
+
export const Changes = ({ site, date }) => (_jsxs("section", { children: [_jsx("h2", { children: CHANGES_HEADING }), _jsx("p", { children: changesParagraph(date ?? site.effectiveDate) })] }));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/legal/copy.ts
|
|
3
|
+
* @desc The fixed sentences and list items behind the seven legal blocks (blocks.tsx) and their
|
|
4
|
+
* Markdown mirror (markdown.ts's `legalMarkdownTransform`), kept in one place so the two
|
|
5
|
+
* outputs can't drift apart. Pure data and template functions: no JSX, no Markdown syntax,
|
|
6
|
+
* just the words. Anything that includes a link (the contact email, the DMCA agent email,
|
|
7
|
+
* the rights-request email) is split into the text before the link so each renderer can
|
|
8
|
+
* append its own link syntax.
|
|
9
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
10
|
+
* @created Mon Oct 5, 2026
|
|
11
|
+
* @modified Mon Oct 5, 2026
|
|
12
|
+
*/
|
|
13
|
+
import type { LegalSite } from "./types.js";
|
|
14
|
+
/** `LegalContact`'s h2 heading. */
|
|
15
|
+
export declare const CONTACT_HEADING = "Contact";
|
|
16
|
+
/** `LegalContact`'s sentence, up to (not including) the contact email link. */
|
|
17
|
+
export declare const contactIntro: (site: LegalSite) => string;
|
|
18
|
+
/** `DataWeKeep`'s h2 heading. */
|
|
19
|
+
export declare const DATA_HEADING = "What we store";
|
|
20
|
+
/** `DataWeKeep`'s h3 heading over the cookie list, shown only when `site.cookies` isn't empty. */
|
|
21
|
+
export declare const COOKIES_HEADING = "Cookies";
|
|
22
|
+
/** `Processors`'s h2 heading. */
|
|
23
|
+
export declare const PROCESSORS_HEADING = "Service providers";
|
|
24
|
+
/** One GDPR or CCPA right: a bold term and its plain-text description. */
|
|
25
|
+
export type LegalRight = {
|
|
26
|
+
term: string;
|
|
27
|
+
desc: string;
|
|
28
|
+
};
|
|
29
|
+
/** `YourRights`'s GDPR section heading. */
|
|
30
|
+
export declare const GDPR_HEADING = "Your rights under the GDPR";
|
|
31
|
+
/** `YourRights`'s six GDPR rights, in list order. */
|
|
32
|
+
export declare const GDPR_RIGHTS: readonly LegalRight[];
|
|
33
|
+
/** `YourRights`'s CCPA section heading. */
|
|
34
|
+
export declare const CCPA_HEADING = "Your rights under the CCPA";
|
|
35
|
+
/** `YourRights`'s three CCPA rights, in list order. */
|
|
36
|
+
export declare const CCPA_RIGHTS: readonly LegalRight[];
|
|
37
|
+
/** `YourRights`'s closing sentence, up to (not including) the contact email link. */
|
|
38
|
+
export declare const RIGHTS_CLOSING_INTRO = "To use any of these rights, email ";
|
|
39
|
+
/** `DmcaNotice`'s h2 heading. */
|
|
40
|
+
export declare const DMCA_HEADING = "Copyright and DMCA";
|
|
41
|
+
/** `DmcaNotice`'s agent sentence, up to (not including) the contact email link. */
|
|
42
|
+
export declare const DMCA_AGENT_INTRO = "Our designated agent for copyright notices is ";
|
|
43
|
+
/** `DmcaNotice`'s h3 heading over the takedown-notice list. */
|
|
44
|
+
export declare const TAKEDOWN_HEADING = "A takedown notice should include";
|
|
45
|
+
/** `DmcaNotice`'s takedown-notice steps, in list order. */
|
|
46
|
+
export declare const TAKEDOWN_ITEMS: readonly string[];
|
|
47
|
+
/** `DmcaNotice`'s h3 heading over the counter-notice list. */
|
|
48
|
+
export declare const COUNTER_NOTICE_HEADING = "A counter-notice should include";
|
|
49
|
+
/** `DmcaNotice`'s counter-notice steps that come before the operator-specific jurisdiction step. */
|
|
50
|
+
export declare const COUNTER_NOTICE_ITEMS_BEFORE_JURISDICTION: readonly string[];
|
|
51
|
+
/** `DmcaNotice`'s jurisdiction step, templated with the site's operator. */
|
|
52
|
+
export declare const counterNoticeJurisdiction: (site: LegalSite) => string;
|
|
53
|
+
/** `DmcaNotice`'s final counter-notice step. */
|
|
54
|
+
export declare const COUNTER_NOTICE_SIGNATURE = "your physical or electronic signature.";
|
|
55
|
+
/** `NoWarranty`'s "as is" heading. */
|
|
56
|
+
export declare const WARRANTY_HEADING = "Disclaimer of warranties";
|
|
57
|
+
/** `NoWarranty`'s "as is" paragraph, templated with the site's name. */
|
|
58
|
+
export declare const warrantyParagraph: (site: LegalSite) => string;
|
|
59
|
+
/** `NoWarranty`'s limitation-of-liability heading. */
|
|
60
|
+
export declare const LIABILITY_HEADING = "Limitation of liability";
|
|
61
|
+
/** `NoWarranty`'s limitation-of-liability paragraph, templated with the operator and site name. */
|
|
62
|
+
export declare const liabilityParagraph: (site: LegalSite) => string;
|
|
63
|
+
/** `Changes`'s h2 heading. */
|
|
64
|
+
export declare const CHANGES_HEADING = "Changes";
|
|
65
|
+
/** `Changes`'s full sentence, templated with the page's effective date. */
|
|
66
|
+
export declare const changesParagraph: (date: string) => string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/legal/copy.ts
|
|
3
|
+
* @desc The fixed sentences and list items behind the seven legal blocks (blocks.tsx) and their
|
|
4
|
+
* Markdown mirror (markdown.ts's `legalMarkdownTransform`), kept in one place so the two
|
|
5
|
+
* outputs can't drift apart. Pure data and template functions: no JSX, no Markdown syntax,
|
|
6
|
+
* just the words. Anything that includes a link (the contact email, the DMCA agent email,
|
|
7
|
+
* the rights-request email) is split into the text before the link so each renderer can
|
|
8
|
+
* append its own link syntax.
|
|
9
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
10
|
+
* @created Mon Oct 5, 2026
|
|
11
|
+
* @modified Mon Oct 5, 2026
|
|
12
|
+
*/
|
|
13
|
+
/** `LegalContact`'s h2 heading. */
|
|
14
|
+
export const CONTACT_HEADING = "Contact";
|
|
15
|
+
/** `LegalContact`'s sentence, up to (not including) the contact email link. */
|
|
16
|
+
export const contactIntro = (site) => `${site.operator} runs ${site.siteName}. For anything on this page, write to `;
|
|
17
|
+
/** `DataWeKeep`'s h2 heading. */
|
|
18
|
+
export const DATA_HEADING = "What we store";
|
|
19
|
+
/** `DataWeKeep`'s h3 heading over the cookie list, shown only when `site.cookies` isn't empty. */
|
|
20
|
+
export const COOKIES_HEADING = "Cookies";
|
|
21
|
+
/** `Processors`'s h2 heading. */
|
|
22
|
+
export const PROCESSORS_HEADING = "Service providers";
|
|
23
|
+
/** `YourRights`'s GDPR section heading. */
|
|
24
|
+
export const GDPR_HEADING = "Your rights under the GDPR";
|
|
25
|
+
/** `YourRights`'s six GDPR rights, in list order. */
|
|
26
|
+
export const GDPR_RIGHTS = [
|
|
27
|
+
{ term: "Access and portability.", desc: "Get a copy of your data in a machine-readable file." },
|
|
28
|
+
{ term: "Rectification.", desc: "Have wrong data corrected." },
|
|
29
|
+
{ term: "Erasure.", desc: "Have your data deleted." },
|
|
30
|
+
{
|
|
31
|
+
term: "Restriction.",
|
|
32
|
+
desc: "Ask us to pause using your data while a question about it is sorted out.",
|
|
33
|
+
},
|
|
34
|
+
{ term: "Objection.", desc: "Object to anything we do on the basis of legitimate interest." },
|
|
35
|
+
{ term: "Complaint.", desc: "Complain to your data protection supervisory authority." },
|
|
36
|
+
];
|
|
37
|
+
/** `YourRights`'s CCPA section heading. */
|
|
38
|
+
export const CCPA_HEADING = "Your rights under the CCPA";
|
|
39
|
+
/** `YourRights`'s three CCPA rights, in list order. */
|
|
40
|
+
export const CCPA_RIGHTS = [
|
|
41
|
+
{
|
|
42
|
+
term: "Know, delete and correct.",
|
|
43
|
+
desc: "Ask what we hold about you, ask us to delete it, and ask us to correct it.",
|
|
44
|
+
},
|
|
45
|
+
{ term: "Selling and sharing.", desc: "We don't sell or share personal information." },
|
|
46
|
+
{ term: "Opting out.", desc: "We honor Global Privacy Control signals." },
|
|
47
|
+
];
|
|
48
|
+
/** `YourRights`'s closing sentence, up to (not including) the contact email link. */
|
|
49
|
+
export const RIGHTS_CLOSING_INTRO = "To use any of these rights, email ";
|
|
50
|
+
/** `DmcaNotice`'s h2 heading. */
|
|
51
|
+
export const DMCA_HEADING = "Copyright and DMCA";
|
|
52
|
+
/** `DmcaNotice`'s agent sentence, up to (not including) the contact email link. */
|
|
53
|
+
export const DMCA_AGENT_INTRO = "Our designated agent for copyright notices is ";
|
|
54
|
+
/** `DmcaNotice`'s h3 heading over the takedown-notice list. */
|
|
55
|
+
export const TAKEDOWN_HEADING = "A takedown notice should include";
|
|
56
|
+
/** `DmcaNotice`'s takedown-notice steps, in list order. */
|
|
57
|
+
export const TAKEDOWN_ITEMS = [
|
|
58
|
+
"your contact information;",
|
|
59
|
+
"the copyrighted work you believe is infringed;",
|
|
60
|
+
"the URL or page the material appears on;",
|
|
61
|
+
"a good-faith statement that the use is not authorized;",
|
|
62
|
+
"a sworn statement that you're the rights holder, or authorized to act for them;",
|
|
63
|
+
"your physical or electronic signature.",
|
|
64
|
+
];
|
|
65
|
+
/** `DmcaNotice`'s h3 heading over the counter-notice list. */
|
|
66
|
+
export const COUNTER_NOTICE_HEADING = "A counter-notice should include";
|
|
67
|
+
/** `DmcaNotice`'s counter-notice steps that come before the operator-specific jurisdiction step. */
|
|
68
|
+
export const COUNTER_NOTICE_ITEMS_BEFORE_JURISDICTION = [
|
|
69
|
+
"your contact information;",
|
|
70
|
+
"the material removed and where it appeared;",
|
|
71
|
+
"a sworn, good-faith statement that it was removed by mistake or misidentification;",
|
|
72
|
+
];
|
|
73
|
+
/** `DmcaNotice`'s jurisdiction step, templated with the site's operator. */
|
|
74
|
+
export const counterNoticeJurisdiction = (site) => `your consent to the jurisdiction of your local courts, or ${site.operator}'s;`;
|
|
75
|
+
/** `DmcaNotice`'s final counter-notice step. */
|
|
76
|
+
export const COUNTER_NOTICE_SIGNATURE = "your physical or electronic signature.";
|
|
77
|
+
/** `NoWarranty`'s "as is" heading. */
|
|
78
|
+
export const WARRANTY_HEADING = "Disclaimer of warranties";
|
|
79
|
+
/** `NoWarranty`'s "as is" paragraph, templated with the site's name. */
|
|
80
|
+
export const warrantyParagraph = (site) => `${site.siteName} is provided "as is" and "as available", without warranties of any kind, ` +
|
|
81
|
+
`express or implied, including merchantability, fitness for a particular purpose and ` +
|
|
82
|
+
`non-infringement.`;
|
|
83
|
+
/** `NoWarranty`'s limitation-of-liability heading. */
|
|
84
|
+
export const LIABILITY_HEADING = "Limitation of liability";
|
|
85
|
+
/** `NoWarranty`'s limitation-of-liability paragraph, templated with the operator and site name. */
|
|
86
|
+
export const liabilityParagraph = (site) => `To the fullest extent the law allows, ${site.operator} is not liable for any indirect, ` +
|
|
87
|
+
`incidental, special, consequential or punitive damages, or for any loss of data or accounts, ` +
|
|
88
|
+
`arising from your use of ${site.siteName}.`;
|
|
89
|
+
/** `Changes`'s h2 heading. */
|
|
90
|
+
export const CHANGES_HEADING = "Changes";
|
|
91
|
+
/** `Changes`'s full sentence, templated with the page's effective date. */
|
|
92
|
+
export const changesParagraph = (date) => `We may update this page. It was last updated on ${date}.`;
|
package/dist/legal/index.d.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* @file src/legal/index.ts
|
|
3
3
|
* @desc @haruhimemoe/next-kit/legal: the five-page legal convention (terms, privacy,
|
|
4
4
|
* your-privacy-rights, copyright, disclaimers). `LegalSite` config, seven MDX blocks
|
|
5
|
-
* rendered from it,
|
|
5
|
+
* rendered from it, `legalEntries` for the app's content registry, and
|
|
6
|
+
* `legalMarkdownTransform` for the .md mirrors and llms-full.txt.
|
|
6
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
8
|
* @created Mon Oct 5, 2026
|
|
8
9
|
* @modified Mon Oct 5, 2026
|
|
9
10
|
*/
|
|
10
11
|
export { Changes, type ChangesProps, DataWeKeep, DmcaNotice, type LegalBlockProps, LegalContact, NoWarranty, Processors, YourRights, } from "./blocks.js";
|
|
11
12
|
export { type LegalPageOverrides, legalEntries } from "./entries.js";
|
|
13
|
+
export { legalMarkdownTransform } from "./markdown.js";
|
|
12
14
|
export { LEGAL_SLUGS, type LegalDataStore, type LegalProcessor, type LegalSite, type LegalSlug, } from "./types.js";
|
package/dist/legal/index.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* @file src/legal/index.ts
|
|
3
3
|
* @desc @haruhimemoe/next-kit/legal: the five-page legal convention (terms, privacy,
|
|
4
4
|
* your-privacy-rights, copyright, disclaimers). `LegalSite` config, seven MDX blocks
|
|
5
|
-
* rendered from it,
|
|
5
|
+
* rendered from it, `legalEntries` for the app's content registry, and
|
|
6
|
+
* `legalMarkdownTransform` for the .md mirrors and llms-full.txt.
|
|
6
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
8
|
* @created Mon Oct 5, 2026
|
|
8
9
|
* @modified Mon Oct 5, 2026
|
|
9
10
|
*/
|
|
10
11
|
export { Changes, DataWeKeep, DmcaNotice, LegalContact, NoWarranty, Processors, YourRights, } from "./blocks.js";
|
|
11
12
|
export { legalEntries } from "./entries.js";
|
|
13
|
+
export { legalMarkdownTransform } from "./markdown.js";
|
|
12
14
|
export { LEGAL_SLUGS, } from "./types.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/legal/markdown.ts
|
|
3
|
+
* @desc `legalMarkdownTransform`: a `mdxToMarkdown` `transforms` entry (see ../docs/markdown.ts)
|
|
4
|
+
* that turns each self-closing legal block tag (`<LegalContact />`, `<DataWeKeep />`,
|
|
5
|
+
* `<Processors />`, `<YourRights />`, `<DmcaNotice />`, `<NoWarranty />`, `<Changes />`)
|
|
6
|
+
* into Markdown carrying the same words as the matching React block in blocks.tsx. Without
|
|
7
|
+
* this, mdxToMarkdown's rule 4 drops the unknown capitalized JSX and an app's .md mirrors
|
|
8
|
+
* and llms-full.txt silently lose the legal text. The wording comes from ./copy.js, the
|
|
9
|
+
* same module blocks.tsx renders from, so the two outputs can't drift apart.
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Mon Oct 5, 2026
|
|
12
|
+
* @modified Mon Oct 5, 2026
|
|
13
|
+
*/
|
|
14
|
+
import type { LegalSite } from "./types.js";
|
|
15
|
+
/**
|
|
16
|
+
* @function legalMarkdownTransform
|
|
17
|
+
* @param site {LegalSite} the site config every block renders from, same as the React blocks
|
|
18
|
+
* @returns {(source: string) => string} a next-kit `mdxToMarkdown` transform: replaces every
|
|
19
|
+
* self-closing legal block tag with Markdown carrying the same words as its React block.
|
|
20
|
+
* `<Changes date="YYYY-MM-DD" />` uses the given date, same as the `Changes` component's `date`
|
|
21
|
+
* prop; `<Changes />` falls back to `site.effectiveDate`. Every other block takes no attributes.
|
|
22
|
+
*/
|
|
23
|
+
export declare const legalMarkdownTransform: (site: LegalSite) => (source: string) => string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/legal/markdown.ts
|
|
3
|
+
* @desc `legalMarkdownTransform`: a `mdxToMarkdown` `transforms` entry (see ../docs/markdown.ts)
|
|
4
|
+
* that turns each self-closing legal block tag (`<LegalContact />`, `<DataWeKeep />`,
|
|
5
|
+
* `<Processors />`, `<YourRights />`, `<DmcaNotice />`, `<NoWarranty />`, `<Changes />`)
|
|
6
|
+
* into Markdown carrying the same words as the matching React block in blocks.tsx. Without
|
|
7
|
+
* this, mdxToMarkdown's rule 4 drops the unknown capitalized JSX and an app's .md mirrors
|
|
8
|
+
* and llms-full.txt silently lose the legal text. The wording comes from ./copy.js, the
|
|
9
|
+
* same module blocks.tsx renders from, so the two outputs can't drift apart.
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Mon Oct 5, 2026
|
|
12
|
+
* @modified Mon Oct 5, 2026
|
|
13
|
+
*/
|
|
14
|
+
import { CCPA_HEADING, CCPA_RIGHTS, CHANGES_HEADING, CONTACT_HEADING, COOKIES_HEADING, COUNTER_NOTICE_HEADING, COUNTER_NOTICE_ITEMS_BEFORE_JURISDICTION, COUNTER_NOTICE_SIGNATURE, changesParagraph, contactIntro, counterNoticeJurisdiction, DATA_HEADING, DMCA_AGENT_INTRO, DMCA_HEADING, GDPR_HEADING, GDPR_RIGHTS, LIABILITY_HEADING, liabilityParagraph, PROCESSORS_HEADING, RIGHTS_CLOSING_INTRO, TAKEDOWN_HEADING, TAKEDOWN_ITEMS, WARRANTY_HEADING, warrantyParagraph, } from "./copy.js";
|
|
15
|
+
/** One attribute on a self-closing tag: `name="value"`, `name='value'`, `name={"value"}` or
|
|
16
|
+
* `name={'value'}`. Only string-literal values are read; a JS expression prop is left alone. */
|
|
17
|
+
const ATTR = /([\w-]+)=(?:"([^"]*)"|'([^']*)'|\{"([^"]*)"\}|\{'([^']*)'\})/g;
|
|
18
|
+
/** Matches `<Name ...attrs.../>`, tolerating any run of whitespace and any attributes
|
|
19
|
+
* (quoted, single-quoted, braced, or valueless). */
|
|
20
|
+
const tagPattern = (name) => new RegExp(`<${name}\\b((?:\\s+[\\w-]+(?:=(?:"[^"]*"|'[^']*'|\\{[^{}]*\\}))?)*)\\s*/>`, "g");
|
|
21
|
+
/** Parses a matched tag's attribute string into a name -> value map. */
|
|
22
|
+
const attributes = (body) => {
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const match of body.matchAll(ATTR)) {
|
|
25
|
+
out[match[1]] = match[2] ?? match[3] ?? match[4] ?? match[5] ?? "";
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
};
|
|
29
|
+
/** `${intro}[email](mailto:email).`: an intro sentence plus a Markdown mailto link. */
|
|
30
|
+
const mailtoLine = (intro, email) => `${intro}[${email}](mailto:${email}).`;
|
|
31
|
+
/** One bullet per right, bold term then its plain-text description. */
|
|
32
|
+
const rightsList = (rights) => rights.map((right) => `- **${right.term}** ${right.desc}`).join("\n");
|
|
33
|
+
/** A numbered list, in source order. */
|
|
34
|
+
const orderedList = (items) => items.map((item, index) => `${index + 1}. ${item}`).join("\n");
|
|
35
|
+
const legalContactMarkdown = (site) => `## ${CONTACT_HEADING}\n\n${mailtoLine(contactIntro(site), site.contactEmail)}`;
|
|
36
|
+
const dataWeKeepMarkdown = (site) => {
|
|
37
|
+
const stores = site.stores.map((store) => `- **${store.what}.** ${store.why}`).join("\n");
|
|
38
|
+
const cookies = site.cookies.length > 0
|
|
39
|
+
? `\n\n### ${COOKIES_HEADING}\n\n${site.cookies.map((cookie) => `- ${cookie}`).join("\n")}`
|
|
40
|
+
: "";
|
|
41
|
+
return `## ${DATA_HEADING}\n\n${stores}${cookies}`;
|
|
42
|
+
};
|
|
43
|
+
const processorsMarkdown = (site) => {
|
|
44
|
+
const list = site.processors
|
|
45
|
+
.map((processor) => {
|
|
46
|
+
const name = processor.link ? `[${processor.name}](${processor.link})` : processor.name;
|
|
47
|
+
return `- **${name}** ${processor.purpose}`;
|
|
48
|
+
})
|
|
49
|
+
.join("\n");
|
|
50
|
+
return `## ${PROCESSORS_HEADING}\n\n${list}`;
|
|
51
|
+
};
|
|
52
|
+
const yourRightsMarkdown = (site) => [
|
|
53
|
+
`## ${GDPR_HEADING}`,
|
|
54
|
+
"",
|
|
55
|
+
rightsList(GDPR_RIGHTS),
|
|
56
|
+
"",
|
|
57
|
+
`## ${CCPA_HEADING}`,
|
|
58
|
+
"",
|
|
59
|
+
rightsList(CCPA_RIGHTS),
|
|
60
|
+
"",
|
|
61
|
+
mailtoLine(RIGHTS_CLOSING_INTRO, site.contactEmail),
|
|
62
|
+
].join("\n");
|
|
63
|
+
const dmcaNoticeMarkdown = (site) => [
|
|
64
|
+
`## ${DMCA_HEADING}`,
|
|
65
|
+
"",
|
|
66
|
+
...(site.hosting ? [site.hosting, ""] : []),
|
|
67
|
+
mailtoLine(DMCA_AGENT_INTRO, site.contactEmail),
|
|
68
|
+
"",
|
|
69
|
+
`### ${TAKEDOWN_HEADING}`,
|
|
70
|
+
"",
|
|
71
|
+
orderedList(TAKEDOWN_ITEMS),
|
|
72
|
+
"",
|
|
73
|
+
`### ${COUNTER_NOTICE_HEADING}`,
|
|
74
|
+
"",
|
|
75
|
+
orderedList([
|
|
76
|
+
...COUNTER_NOTICE_ITEMS_BEFORE_JURISDICTION,
|
|
77
|
+
counterNoticeJurisdiction(site),
|
|
78
|
+
COUNTER_NOTICE_SIGNATURE,
|
|
79
|
+
]),
|
|
80
|
+
].join("\n");
|
|
81
|
+
const noWarrantyMarkdown = (site) => [
|
|
82
|
+
`## ${WARRANTY_HEADING}`,
|
|
83
|
+
"",
|
|
84
|
+
warrantyParagraph(site),
|
|
85
|
+
"",
|
|
86
|
+
`## ${LIABILITY_HEADING}`,
|
|
87
|
+
"",
|
|
88
|
+
liabilityParagraph(site),
|
|
89
|
+
].join("\n");
|
|
90
|
+
const changesMarkdown = (date) => `## ${CHANGES_HEADING}\n\n${changesParagraph(date)}`;
|
|
91
|
+
/** Every block's tag name and Markdown renderer, keyed in the order they're tried. `Changes` is
|
|
92
|
+
* handled separately below so it can read its optional `date` attribute. */
|
|
93
|
+
const SIMPLE_BLOCKS = [
|
|
94
|
+
["LegalContact", legalContactMarkdown],
|
|
95
|
+
["DataWeKeep", dataWeKeepMarkdown],
|
|
96
|
+
["Processors", processorsMarkdown],
|
|
97
|
+
["YourRights", yourRightsMarkdown],
|
|
98
|
+
["DmcaNotice", dmcaNoticeMarkdown],
|
|
99
|
+
["NoWarranty", noWarrantyMarkdown],
|
|
100
|
+
];
|
|
101
|
+
/**
|
|
102
|
+
* @function legalMarkdownTransform
|
|
103
|
+
* @param site {LegalSite} the site config every block renders from, same as the React blocks
|
|
104
|
+
* @returns {(source: string) => string} a next-kit `mdxToMarkdown` transform: replaces every
|
|
105
|
+
* self-closing legal block tag with Markdown carrying the same words as its React block.
|
|
106
|
+
* `<Changes date="YYYY-MM-DD" />` uses the given date, same as the `Changes` component's `date`
|
|
107
|
+
* prop; `<Changes />` falls back to `site.effectiveDate`. Every other block takes no attributes.
|
|
108
|
+
*/
|
|
109
|
+
export const legalMarkdownTransform = (site) => (source) => {
|
|
110
|
+
let text = source.replace(tagPattern("Changes"), (_whole, body) => changesMarkdown(attributes(body).date ?? site.effectiveDate));
|
|
111
|
+
for (const [name, render] of SIMPLE_BLOCKS) {
|
|
112
|
+
text = text.replace(tagPattern(name), () => render(site));
|
|
113
|
+
}
|
|
114
|
+
return text;
|
|
115
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haruhimemoe/next-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|