legal-terms 0.1.9

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/cli.mjs ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Print a configured Terms of Service + Privacy Policy as Markdown, HTML or
4
+ * plain text — for a repo's `TERMS.md`, a static page, or a diff of what a
5
+ * config change does to the policy text.
6
+ *
7
+ * @example
8
+ * ```sh
9
+ * npx legal-terms-privacy-policy --app-name Acme --variant full > TERMS.md
10
+ * npx legal-terms-privacy-policy --app-name Acme --format html --standalone \
11
+ * --no-part california --exclude social-features > terms.html
12
+ * ```
13
+ */
14
+ import { register } from 'node:module';
15
+
16
+ register('./ts-loader.mjs', import.meta.url);
17
+
18
+ const { renderMarkdown, renderHtml, renderText, resolveLegalDoc } = await import(
19
+ new URL('./src/index.ts', import.meta.url).href
20
+ );
21
+
22
+ const USAGE = `
23
+ legal-terms-privacy-policy — print a configurable Terms of Service + Privacy Policy
24
+
25
+ Usage:
26
+ legal-terms-privacy-policy [options]
27
+
28
+ Document:
29
+ --variant <summary|full> Which presentation to print (default: full)
30
+ --format <markdown|html|text|json>
31
+ Output format (default: markdown)
32
+ --standalone For --format html, emit a full page with styles
33
+
34
+ Values substituted into the text:
35
+ --app-name <name> Product name (default: Our Service)
36
+ --company-name <name> Legal entity (default: same as --app-name)
37
+ --contact-email <email> Address for legal and privacy requests
38
+ --home-url <url> Target of the "Back to Home" link
39
+ --last-revised <date> Revision date shown under the title
40
+ --effective-date <date> Effective date (default: same as --last-revised)
41
+ --jurisdiction <region> Region the Services are offered in
42
+ --minimum-age <n> Minimum age to hold an account
43
+ --children-age <n> COPPA age threshold
44
+ --deletion-days <n> Days to purge data after account deletion
45
+
46
+ Choosing what appears:
47
+ --no-part <id> Drop a named part; repeatable. One of:
48
+ ai, privacy, cookies, california, children,
49
+ security, thirdParty
50
+ --part <id> Force a part back on; repeatable
51
+ --include <id> Keep only these section ids; repeatable
52
+ --exclude <id> Drop these section ids; repeatable
53
+ --order <id,id,...> Put these sections first, in this order
54
+ --no-numbers Do not number the full-text sections
55
+ --title <text> Override the document title
56
+
57
+ --list-sections Print the section ids of the chosen variant
58
+ -h, --help Show this help
59
+ `.trim();
60
+
61
+ const PART_IDS = new Set([
62
+ 'ai',
63
+ 'privacy',
64
+ 'cookies',
65
+ 'california',
66
+ 'children',
67
+ 'security',
68
+ 'thirdParty',
69
+ ]);
70
+
71
+ function parseArgs(argv) {
72
+ const options = { parts: {}, include: [], exclude: [] };
73
+ let format = 'markdown';
74
+ let standalone = false;
75
+ let listSections = false;
76
+
77
+ /** Flags that take the next argv entry as their value. */
78
+ const valueFlags = {
79
+ '--variant': 'variant',
80
+ '--app-name': 'appName',
81
+ '--company-name': 'companyName',
82
+ '--contact-email': 'contactEmail',
83
+ '--home-url': 'homeUrl',
84
+ '--last-revised': 'lastRevisedDate',
85
+ '--effective-date': 'effectiveDate',
86
+ '--jurisdiction': 'jurisdiction',
87
+ '--minimum-age': 'minimumAge',
88
+ '--children-age': 'childrenAge',
89
+ '--deletion-days': 'dataDeletionDays',
90
+ '--title': 'title',
91
+ };
92
+
93
+ for (let i = 0; i < argv.length; i++) {
94
+ const arg = argv[i];
95
+ if (arg === '-h' || arg === '--help') {
96
+ console.log(USAGE);
97
+ process.exit(0);
98
+ } else if (arg === '--list-sections') {
99
+ listSections = true;
100
+ } else if (arg === '--standalone') {
101
+ standalone = true;
102
+ } else if (arg === '--no-numbers') {
103
+ options.features = { ...options.features, numbered: false };
104
+ } else if (arg === '--format') {
105
+ format = argv[++i];
106
+ } else if (arg === '--no-part' || arg === '--part') {
107
+ const id = argv[++i];
108
+ if (!PART_IDS.has(id)) {
109
+ console.error(`Unknown part "${id}". Expected one of: ${[...PART_IDS].join(', ')}`);
110
+ process.exit(1);
111
+ }
112
+ options.parts[id] = arg === '--part';
113
+ } else if (arg === '--include') {
114
+ options.include.push(argv[++i]);
115
+ } else if (arg === '--exclude') {
116
+ options.exclude.push(argv[++i]);
117
+ } else if (arg === '--order') {
118
+ options.order = argv[++i].split(',').map((s) => s.trim()).filter(Boolean);
119
+ } else if (valueFlags[arg]) {
120
+ options[valueFlags[arg]] = argv[++i];
121
+ } else {
122
+ console.error(`Unknown option "${arg}". Run with --help for usage.`);
123
+ process.exit(1);
124
+ }
125
+ }
126
+ return { options, format, standalone, listSections };
127
+ }
128
+
129
+ const { options, format, standalone, listSections } = parseArgs(process.argv.slice(2));
130
+
131
+ if (listSections) {
132
+ const doc = resolveLegalDoc(options);
133
+ for (const section of doc.sections) {
134
+ console.log(section.id);
135
+ for (const sub of section.subsections ?? []) console.log(` ${sub.id}`);
136
+ }
137
+ process.exit(0);
138
+ }
139
+
140
+ switch (format) {
141
+ case 'markdown':
142
+ case 'md':
143
+ process.stdout.write(renderMarkdown(options));
144
+ break;
145
+ case 'html':
146
+ process.stdout.write(renderHtml(options, { standalone }) + '\n');
147
+ break;
148
+ case 'text':
149
+ case 'txt':
150
+ process.stdout.write(renderText(options));
151
+ break;
152
+ case 'json':
153
+ process.stdout.write(JSON.stringify(resolveLegalDoc(options), null, 2) + '\n');
154
+ break;
155
+ default:
156
+ console.error(`Unknown format "${format}". Expected markdown, html, text or json.`);
157
+ process.exit(1);
158
+ }
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "legal-terms",
3
+ "version": "0.1.9",
4
+ "description": "Configurable Terms of Service and Privacy Policy — a scannable summary and the full legal text, with every section addable, removable and reorderable. React page, plus Markdown/HTML/text renderers and a CLI.",
5
+ "type": "module",
6
+ "author": "vtempest",
7
+ "license": "rights.institute/prosper",
8
+ "main": "./src/index.ts",
9
+ "types": "./src/index.ts",
10
+ "module": "./src/index.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "import": "./src/index.ts",
15
+ "require": "./src/index.ts"
16
+ },
17
+ "./react": {
18
+ "types": "./src/react/index.ts",
19
+ "import": "./src/react/index.ts"
20
+ },
21
+ "./content/full": "./src/content/full.ts",
22
+ "./content/summary": "./src/content/summary.ts",
23
+ "./package.json": "./package.json",
24
+ "./*": "./src/*"
25
+ },
26
+ "bin": {
27
+ "legal-terms-privacy-policy": "./cli.mjs"
28
+ },
29
+ "files": [
30
+ "src",
31
+ "cli.mjs",
32
+ "readme.md"
33
+ ],
34
+ "scripts": {
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "coverage": "vitest run --coverage",
38
+ "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=./junit.xml --coverage",
39
+ "typecheck": "tsc --noEmit",
40
+ "demo": "node cli.mjs --app-name 'Example App' --variant full --format markdown",
41
+ "pub": "npm version patch && npm publish"
42
+ },
43
+ "keywords": [
44
+ "terms-of-service",
45
+ "privacy-policy",
46
+ "legal",
47
+ "gdpr",
48
+ "ccpa",
49
+ "coppa",
50
+ "compliance",
51
+ "react",
52
+ "nextjs"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/OpenSourceAGI/dev-tools-starter-agent.git",
57
+ "directory": "packages/legal-terms-privacy-policy"
58
+ },
59
+ "homepage": "https://github.com/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/legal-terms-privacy-policy",
60
+ "peerDependencies": {
61
+ "lucide-react": ">=0.4.0",
62
+ "react": ">=18"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "lucide-react": {
66
+ "optional": true
67
+ },
68
+ "react": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "devDependencies": {
73
+ "@types/react": "^19.2.0",
74
+ "@types/react-dom": "^19.2.0",
75
+ "@vitest/coverage-v8": "^4.1.0",
76
+ "lucide-react": "^1.43.0",
77
+ "react": "^19.2.8",
78
+ "react-dom": "^19.2.8",
79
+ "typescript": "^5.9.3",
80
+ "vitest": "^4.1.0"
81
+ }
82
+ }
package/readme.md ADDED
@@ -0,0 +1,226 @@
1
+ <!-- template-git-repo:badges:start -->
2
+ <p align="center">
3
+ <a href="https://starterdocs.vtempest.workers.dev/docs/packages/legal-terms-privacy-policy"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
4
+ <a href="https://stackblitz.com/github/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/legal-terms-privacy-policy"><img height="20px" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" /></a>
5
+ <br />
6
+ <a href="https://www.npmjs.com/package/legal-terms-privacy-policy"><img src="https://img.shields.io/npm/dm/legal-terms-privacy-policy.svg" alt="NPM Monthly Downloads" /></a>
7
+ <a href="https://www.npmjs.com/package/legal-terms-privacy-policy"><img src="https://img.shields.io/npm/v/legal-terms-privacy-policy.svg" alt="npm version" /></a>
8
+ <a href="https://www.npmjs.com/package/legal-terms-privacy-policy"><img src="https://img.shields.io/npm/dt/legal-terms-privacy-policy.svg" alt="NPM Total Downloads" /></a>
9
+ <a href="https://www.npmjs.com/package/legal-terms-privacy-policy"><img src="https://img.shields.io/npm/types/legal-terms-privacy-policy" alt="TypeScript types" /></a>
10
+ <a href="https://packagephobia.com/result?p=legal-terms-privacy-policy"><img src="https://packagephobia.com/badge?p=legal-terms-privacy-policy" alt="Install size" /></a>
11
+ </p>
12
+ <!-- template-git-repo:badges:end -->
13
+
14
+ <!-- skills:install:start -->
15
+ **🤖 Agent skill** — `npx skills@latest add https://github.com/OpenSourceAGI/dev-tools-starter-agent --skill legal-terms-privacy-policy` ([what it covers](../../skills/legal-terms-privacy-policy/SKILL.md))
16
+ <!-- skills:install:end -->
17
+
18
+ # legal-terms-privacy-policy
19
+
20
+ One combined **Terms of Service + Privacy Policy**, in two presentations of the same policy, with every section addable, removable and reorderable from config.
21
+
22
+ - **Summary** — the scannable card-and-icon layout published at [rights.institute/terms-privacy](https://rights.institute/terms-privacy).
23
+ - **Full text** — the long-form legal document published by QwkSearch, Debate AI and AI Broker.
24
+
25
+ Ship both and let readers switch between them, or pin a page to one. Nothing in the package needs editing to adopt it: the product name, contact address, dates, which clauses appear and in what order all come from props.
26
+
27
+ > Not legal advice. This is boilerplate to start from — have a lawyer review the text you publish.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ npm install legal-terms-privacy-policy
33
+ ```
34
+
35
+ The package ships TypeScript source rather than a build output, matching the other packages in this monorepo. In a Next.js app, add it to `transpilePackages`:
36
+
37
+ ```js
38
+ // next.config.js
39
+ export default { transpilePackages: ['legal-terms-privacy-policy'] };
40
+ ```
41
+
42
+ `react` and `lucide-react` are optional peers — needed only for the React page, not for the Markdown/HTML/text renderers or the CLI.
43
+
44
+ ## React page
45
+
46
+ ```tsx
47
+ import { LegalTermsPrivacyPolicy } from 'legal-terms-privacy-policy/react';
48
+
49
+ export default function TermsPage() {
50
+ return (
51
+ <LegalTermsPrivacyPolicy
52
+ appName="QwkSearch"
53
+ contactEmail="legal@qwksearch.com"
54
+ lastRevisedDate="March 1, 2026"
55
+ defaultVariant="full"
56
+ />
57
+ );
58
+ }
59
+ ```
60
+
61
+ The component renders the whole page — back link, title, badges, revision date, the summary ⇄ full-text switch, section navigation and the body. It is Tailwind-styled and works in light and dark.
62
+
63
+ ### Controlling the variant
64
+
65
+ Uncontrolled, the switch keeps its own state starting from `defaultVariant`. Pass `variant` and `onVariantChange` to drive it from the URL instead:
66
+
67
+ ```tsx
68
+ 'use client';
69
+ const [variant, setVariant] = useState<Variant>('summary');
70
+ <LegalTermsPrivacyPolicy variant={variant} onVariantChange={setVariant} appName="Acme" />;
71
+ ```
72
+
73
+ To publish only one presentation, set `variant` and turn the switch off:
74
+
75
+ ```tsx
76
+ <LegalTermsPrivacyPolicy appName="Acme" variant="full" features={{ variantSwitch: false }} />
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ Every option below works the same way in the React component, the renderers and the CLI.
82
+
83
+ ### Values substituted into the text
84
+
85
+ The legal text carries `{{token}}` placeholders. Unknown tokens are left visible rather than blanked, so a typo shows up on the page instead of silently deleting a clause.
86
+
87
+ | Option | Default | Appears in |
88
+ | --- | --- | --- |
89
+ | `appName` | `"Our Service"` | Throughout |
90
+ | `companyName` | same as `appName` | Liability, contact, footer |
91
+ | `contactEmail` | `"legal@example.com"` | Accounts, retention, contact |
92
+ | `homeUrl` | `"/"` | "Back to Home" link |
93
+ | `lastRevisedDate` | `"January 1, 2025"` | Under the title |
94
+ | `effectiveDate` | same as `lastRevisedDate` | Under the title |
95
+ | `jurisdiction` | `"the United States"` | Introduction |
96
+ | `minimumAge` | `18` | Acceptance of Terms |
97
+ | `childrenAge` | `13` | Children's Privacy |
98
+ | `dataDeletionDays` | `30` | Data Security and Retention |
99
+ | `tokens` | `{}` | Extra placeholders for your own sections |
100
+
101
+ ### Adding and removing parts
102
+
103
+ `parts` switches named groups of clauses on or off in one go:
104
+
105
+ ```tsx
106
+ <LegalTermsPrivacyPolicy
107
+ appName="Grab URL"
108
+ parts={{ ai: false, california: false }} // no model in the loop, no CA notice
109
+ />
110
+ ```
111
+
112
+ | Part | Covers |
113
+ | --- | --- |
114
+ | `core` | Introduction, changes, accounts, use, materials, feedback, warranties, termination, contact. Always on — switching it off is ignored. |
115
+ | `ai` | The Artificial Intelligence Ethical Use Policy and AI-specific clauses |
116
+ | `privacy` | Collection, use, disclosure and retention of personal data |
117
+ | `cookies` | Cookies, tracking technologies and Do Not Track |
118
+ | `california` | The CCPA/CPRA resident notice |
119
+ | `children` | The COPPA under-13 notice |
120
+ | `security` | Security measures and data retention |
121
+ | `thirdParty` | Third-party links and social features |
122
+
123
+ For finer control, work by section id — `include` keeps only what you name, `exclude` drops it, and both reach subsections:
124
+
125
+ ```tsx
126
+ exclude={['social-features', 'california-selling']}
127
+ include={['introduction', 'privacy-policy', 'contact']}
128
+ ```
129
+
130
+ Naming a parent in `include` keeps its whole subtree (`['ai-ethics']` is the section and its four sub-policies); naming only a child keeps the parent as its heading (`['california-rights']` renders under "California Residents"). `exclude` still applies inside an included parent.
131
+
132
+ Run `npx legal-terms-privacy-policy --list-sections` (add `--variant summary`) to see every id.
133
+
134
+ ### Rewriting and adding sections
135
+
136
+ `replace` patches a section by id, merging over the built-in one — pass only the fields you are changing:
137
+
138
+ ```tsx
139
+ replace={{
140
+ contact: { blocks: [{ type: 'p', text: 'Write to {{companyName}}, 1 Main St.' }] },
141
+ 'california-selling': { title: 'We Do Not Sell Your Data' },
142
+ }}
143
+ ```
144
+
145
+ `add` inserts your own sections, anchored to an existing one:
146
+
147
+ ```tsx
148
+ add={[{
149
+ after: 'termination',
150
+ section: {
151
+ id: 'arbitration',
152
+ title: 'Arbitration and Governing Law',
153
+ icon: 'Scale',
154
+ accent: 'slate',
155
+ blocks: [
156
+ { type: 'p', text: 'Disputes with {{companyName}} are resolved by binding arbitration.' },
157
+ { type: 'ul', lead: 'Exceptions:', items: ['Small claims court', 'Injunctive relief'] },
158
+ ],
159
+ },
160
+ }]}
161
+ ```
162
+
163
+ Anchors are `after`, `before` or `at` (an index); with none of them, the section is appended. `order` puts named sections first, in the order given, and leaves the rest in place.
164
+
165
+ ### Block types
166
+
167
+ Sections hold `blocks`, so caller-authored content renders in every format:
168
+
169
+ | Block | Shape |
170
+ | --- | --- |
171
+ | `p` | `{ type: 'p', text, strong? }` |
172
+ | `ol` / `ul` | `{ type: 'ol', lead?, items }` — items are strings or `{ text, items }` for one nested level |
173
+ | `cards` | `{ type: 'cards', columns?: 1-4, center?, items: [{ title, text?, items?, icon? }] }` |
174
+ | `note` | `{ type: 'note', title?, text?, items?, icon? }` |
175
+
176
+ Card and note blocks render as tinted tiles in the summary variant and flatten to headings and lists in the full-text variant, so you only write them once. `icon` names a [lucide](https://lucide.dev) icon; unknown names fall back to a document glyph, or pass your own components through the `icons` prop.
177
+
178
+ ### Chrome
179
+
180
+ `features` toggles the page furniture: `backLink`, `sidebar`, `tableOfContents`, `copyButtons`, `badges`, `variantSwitch`, `footer`, `numbered`. `badges` (the array) sets the compliance pills — `['GDPR Compliant', 'CCPA Compliant', 'Cookie Policy']` by default; pass `[]` to drop them.
181
+
182
+ ## Rendering without React
183
+
184
+ ```ts
185
+ import { renderMarkdown, renderHtml, renderText, resolveLegalDoc } from 'legal-terms-privacy-policy';
186
+
187
+ const config = { appName: 'Acme', contactEmail: 'legal@acme.com', parts: { ai: false } };
188
+
189
+ renderMarkdown(config); // TERMS.md, MDX docs page
190
+ renderHtml(config, { standalone: true }); // complete styled page
191
+ renderText(config); // email, CLI, in-app agreement dialog
192
+ resolveLegalDoc(config).sections; // the resolved tree, to render yourself
193
+ ```
194
+
195
+ `LEGAL_CSS` is exported for pairing with the HTML fragment. Interpolated values are HTML-escaped, and emails and URLs in the text become links.
196
+
197
+ ## CLI
198
+
199
+ ```sh
200
+ npx legal-terms-privacy-policy --app-name Acme --variant full > TERMS.md
201
+
202
+ npx legal-terms-privacy-policy \
203
+ --app-name Acme --contact-email legal@acme.com \
204
+ --format html --standalone \
205
+ --no-part california --exclude social-features > terms.html
206
+
207
+ npx legal-terms-privacy-policy --list-sections --variant summary
208
+ ```
209
+
210
+ `--format` takes `markdown`, `html`, `text` or `json`. Run `--help` for the full list.
211
+
212
+ ## Keeping the two variants honest
213
+
214
+ The summary is a plain-language restatement; the full text is what binds. If you edit one, edit the other — `src/content/summary.ts` and `src/content/full.ts` are the two files to keep in step. Publishing a summary whose claims the full text does not support is the failure mode this package is shaped to avoid, which is why the switch is on by default.
215
+
216
+ ## Tests
217
+
218
+ ```sh
219
+ npm test
220
+ ```
221
+
222
+ Covers token substitution, part and id filtering, patching, insertion, ordering, and each renderer — including that neither variant ships an unsubstituted `{{token}}` and that interpolated values cannot inject markup.
223
+
224
+ ## License
225
+
226
+ [PROSPER 1.0.0](https://rights.institute/prosper)