@openpolicy/sdk 0.0.19 → 0.0.20
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/README.md +10 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/skills/annotate-data-collection/SKILL.md +240 -0
- package/skills/annotate-third-parties/SKILL.md +237 -0
- package/skills/annotate-third-parties/references/known-packages.md +37 -0
- package/skills/define-config/SKILL.md +286 -0
- package/skills/define-config/references/privacy-config.md +153 -0
- package/skills/define-config/references/terms-config.md +130 -0
- package/skills/getting-started/SKILL.md +251 -0
- package/skills/migrate/SKILL.md +360 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: getting-started
|
|
3
|
+
description: >
|
|
4
|
+
End-to-end setup for OpenPolicy: install @openpolicy/sdk, @openpolicy/react, and
|
|
5
|
+
@openpolicy/vite-auto-collect; create openpolicy.ts with defineConfig(); wire autoCollect()
|
|
6
|
+
into vite.config.ts; wrap the React app with <OpenPolicy>; render <PrivacyPolicy>.
|
|
7
|
+
type: lifecycle
|
|
8
|
+
library: openpolicy
|
|
9
|
+
library_version: "0.0.19"
|
|
10
|
+
sources:
|
|
11
|
+
- jamiedavenport/openpolicy:packages/sdk/README.md
|
|
12
|
+
- jamiedavenport/openpolicy:packages/react/src/context.tsx
|
|
13
|
+
- jamiedavenport/openpolicy:packages/vite-auto-collect/src/index.ts
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
Install packages:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bun add @openpolicy/sdk @openpolicy/react @openpolicy/vite-auto-collect
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Create `openpolicy.ts` at the project root:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { defineConfig, dataCollected, thirdParties } from "@openpolicy/sdk";
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
company: {
|
|
31
|
+
name: "Acme",
|
|
32
|
+
legalName: "Acme, Inc.",
|
|
33
|
+
address: "123 Main St, San Francisco, CA 94105",
|
|
34
|
+
contact: "privacy@acme.com",
|
|
35
|
+
},
|
|
36
|
+
privacy: {
|
|
37
|
+
effectiveDate: "2026-01-01",
|
|
38
|
+
dataCollected: {
|
|
39
|
+
...dataCollected,
|
|
40
|
+
"Account Information": ["Email address", "Display name"],
|
|
41
|
+
},
|
|
42
|
+
thirdParties: [...thirdParties],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Add `autoCollect()` to `vite.config.ts` — it must appear before any React plugin:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { defineConfig } from "vite";
|
|
51
|
+
import react from "@vitejs/plugin-react";
|
|
52
|
+
import { autoCollect } from "@openpolicy/vite-auto-collect";
|
|
53
|
+
|
|
54
|
+
export default defineConfig({
|
|
55
|
+
plugins: [
|
|
56
|
+
autoCollect({ thirdParties: { usePackageJson: true } }),
|
|
57
|
+
react(),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Wrap the application root with `<OpenPolicy>`:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// main.tsx or _app.tsx or layout.tsx
|
|
66
|
+
import { OpenPolicy } from "@openpolicy/react";
|
|
67
|
+
import config from "./openpolicy";
|
|
68
|
+
|
|
69
|
+
export function App({ children }: { children: React.ReactNode }) {
|
|
70
|
+
return <OpenPolicy config={config}>{children}</OpenPolicy>;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Render a policy page:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { PrivacyPolicy } from "@openpolicy/react";
|
|
78
|
+
import "@openpolicy/react/styles.css";
|
|
79
|
+
|
|
80
|
+
export default function PrivacyPage() {
|
|
81
|
+
return <PrivacyPolicy />;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Core Patterns
|
|
86
|
+
|
|
87
|
+
### Mark data collection inline with `collecting()`
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { collecting } from "@openpolicy/sdk";
|
|
91
|
+
|
|
92
|
+
// Call next to the point of collection; autoCollect() scans for these at build time
|
|
93
|
+
export async function createUser(name: string, email: string) {
|
|
94
|
+
const user = collecting(
|
|
95
|
+
"Account Information",
|
|
96
|
+
{ name, email },
|
|
97
|
+
{ name: "Display name", email: "Email address" },
|
|
98
|
+
);
|
|
99
|
+
return db.users.create(user);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The category and label arguments must be string literals — dynamic variables are silently skipped by the static scanner.
|
|
104
|
+
|
|
105
|
+
### Mark third-party integrations with `thirdParty()`
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { thirdParty } from "@openpolicy/sdk";
|
|
109
|
+
|
|
110
|
+
// Place next to the integration's initialisation
|
|
111
|
+
thirdParty("Stripe", "Payment processing", "https://stripe.com/privacy");
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The `autoCollect({ thirdParties: { usePackageJson: true } })` option also auto-detects ~30 known packages (Stripe, Sentry, PostHog, etc.) from `package.json`.
|
|
115
|
+
|
|
116
|
+
### Spread both sentinels in `openpolicy.ts`
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { defineConfig, dataCollected, thirdParties } from "@openpolicy/sdk";
|
|
120
|
+
|
|
121
|
+
export default defineConfig({
|
|
122
|
+
company: {
|
|
123
|
+
name: "Acme",
|
|
124
|
+
legalName: "Acme, Inc.",
|
|
125
|
+
address: "123 Main St, San Francisco, CA 94105",
|
|
126
|
+
contact: "privacy@acme.com",
|
|
127
|
+
},
|
|
128
|
+
privacy: {
|
|
129
|
+
effectiveDate: "2026-01-01",
|
|
130
|
+
dataCollected: {
|
|
131
|
+
...dataCollected, // populated by autoCollect() at build time
|
|
132
|
+
"Manual Category": ["Manually added field"], // additional hand-declared entries
|
|
133
|
+
},
|
|
134
|
+
thirdParties: [...thirdParties], // populated by autoCollect() at build time
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Both `dataCollected` and `thirdParties` are placeholder objects in `@openpolicy/sdk`; `autoCollect()` replaces them via virtual module injection during the Vite build.
|
|
140
|
+
|
|
141
|
+
## Common Mistakes
|
|
142
|
+
|
|
143
|
+
### CRITICAL: Using `openPolicy()` from `@openpolicy/vite` instead of `autoCollect()`
|
|
144
|
+
|
|
145
|
+
Wrong:
|
|
146
|
+
```ts
|
|
147
|
+
// vite.config.ts
|
|
148
|
+
import { openPolicy } from "@openpolicy/vite";
|
|
149
|
+
|
|
150
|
+
export default defineConfig({
|
|
151
|
+
plugins: [openPolicy({ formats: ["markdown"], outDir: "public/policies" })],
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Correct:
|
|
156
|
+
```ts
|
|
157
|
+
// vite.config.ts
|
|
158
|
+
import { autoCollect } from "@openpolicy/vite-auto-collect";
|
|
159
|
+
|
|
160
|
+
export default defineConfig({
|
|
161
|
+
plugins: [autoCollect()],
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`openPolicy()` generates static files at build time that React components never read; `autoCollect()` populates the `dataCollected` and `thirdParties` sentinels that feed the React runtime rendering path.
|
|
166
|
+
|
|
167
|
+
Source: maintainer interview
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### HIGH: Rendering policy components without `<OpenPolicy>` provider
|
|
172
|
+
|
|
173
|
+
Wrong:
|
|
174
|
+
```tsx
|
|
175
|
+
// privacy-page.tsx
|
|
176
|
+
import { PrivacyPolicy } from "@openpolicy/react";
|
|
177
|
+
|
|
178
|
+
export default function PrivacyPage() {
|
|
179
|
+
return <PrivacyPolicy />;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Correct:
|
|
184
|
+
```tsx
|
|
185
|
+
// layout.tsx — wrap at the root
|
|
186
|
+
import { OpenPolicy } from "@openpolicy/react";
|
|
187
|
+
import config from "./openpolicy";
|
|
188
|
+
|
|
189
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
190
|
+
return <OpenPolicy config={config}>{children}</OpenPolicy>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// privacy-page.tsx — component reads from context
|
|
194
|
+
import { PrivacyPolicy } from "@openpolicy/react";
|
|
195
|
+
|
|
196
|
+
export default function PrivacyPage() {
|
|
197
|
+
return <PrivacyPolicy />;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`PrivacyPolicy`, `TermsOfService`, and `CookiePolicy` read config from React context; without the provider they silently render `null` with no visible error.
|
|
202
|
+
|
|
203
|
+
Source: packages/react/src/context.tsx
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### HIGH: Not spreading `dataCollected` and `thirdParties` sentinels into config
|
|
208
|
+
|
|
209
|
+
Wrong:
|
|
210
|
+
```ts
|
|
211
|
+
// openpolicy.ts
|
|
212
|
+
import { defineConfig } from "@openpolicy/sdk";
|
|
213
|
+
|
|
214
|
+
export default defineConfig({
|
|
215
|
+
company: {
|
|
216
|
+
name: "Acme",
|
|
217
|
+
legalName: "Acme, Inc.",
|
|
218
|
+
address: "123 Main St, San Francisco, CA 94105",
|
|
219
|
+
contact: "privacy@acme.com",
|
|
220
|
+
},
|
|
221
|
+
privacy: {
|
|
222
|
+
effectiveDate: "2026-01-01",
|
|
223
|
+
dataCollected: { "Account Information": ["Email address"] },
|
|
224
|
+
thirdParties: [],
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Correct:
|
|
230
|
+
```ts
|
|
231
|
+
// openpolicy.ts
|
|
232
|
+
import { defineConfig, dataCollected, thirdParties } from "@openpolicy/sdk";
|
|
233
|
+
|
|
234
|
+
export default defineConfig({
|
|
235
|
+
company: {
|
|
236
|
+
name: "Acme",
|
|
237
|
+
legalName: "Acme, Inc.",
|
|
238
|
+
address: "123 Main St, San Francisco, CA 94105",
|
|
239
|
+
contact: "privacy@acme.com",
|
|
240
|
+
},
|
|
241
|
+
privacy: {
|
|
242
|
+
effectiveDate: "2026-01-01",
|
|
243
|
+
dataCollected: { ...dataCollected, "Account Information": ["Email address"] },
|
|
244
|
+
thirdParties: [...thirdParties],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Without spreading the sentinels, `autoCollect()` plugin output is discarded and the policy compiles with only the hand-declared entries — all `collecting()` and `thirdParty()` call annotations are silently ignored.
|
|
250
|
+
|
|
251
|
+
Source: packages/sdk/src/auto-collected.ts
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: migrate
|
|
3
|
+
description: >
|
|
4
|
+
Converting existing hand-written privacy policies or terms of service documents into OpenPolicy defineConfig() configs — mapping prose sections to structured TypeScript fields without passing raw text as values.
|
|
5
|
+
type: lifecycle
|
|
6
|
+
library: openpolicy
|
|
7
|
+
library_version: "0.0.19"
|
|
8
|
+
requires:
|
|
9
|
+
- openpolicy/define-config
|
|
10
|
+
sources:
|
|
11
|
+
- jamiedavenport/openpolicy:packages/core/src/types.ts
|
|
12
|
+
- jamiedavenport/openpolicy:packages/sdk/src/constants.ts
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# openpolicy/migrate
|
|
16
|
+
|
|
17
|
+
This skill builds on openpolicy/define-config. Read it first.
|
|
18
|
+
|
|
19
|
+
OpenPolicy generates all prose from structured fields. The job of a migration is to extract structure from an existing document — not to transcribe its sentences. Every field value must be a short label, boolean, enum string, or object; never a paragraph.
|
|
20
|
+
|
|
21
|
+
## Setup: Before and After
|
|
22
|
+
|
|
23
|
+
### Existing privacy policy (excerpt)
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Privacy Policy — Effective January 1, 2026
|
|
27
|
+
|
|
28
|
+
Acme, Inc. ("Acme") operates the Acme platform. This policy describes how we collect
|
|
29
|
+
and use personal data in compliance with the GDPR and the California Consumer Privacy Act.
|
|
30
|
+
|
|
31
|
+
Data We Collect
|
|
32
|
+
We collect the following categories of personal data:
|
|
33
|
+
- Account information: your name, email address, and password when you register.
|
|
34
|
+
- Payment details: the last 4 digits of your card, your billing name, and billing address.
|
|
35
|
+
- Usage data: pages you visit, features you use, and time spent in the app.
|
|
36
|
+
- Device information: your device type, operating system, and browser version.
|
|
37
|
+
|
|
38
|
+
We use Google Analytics for product analytics and Stripe for payment processing.
|
|
39
|
+
|
|
40
|
+
Legal Basis (GDPR)
|
|
41
|
+
We process your data on the basis of legitimate interests and, where required, consent.
|
|
42
|
+
|
|
43
|
+
Data Retention
|
|
44
|
+
Account information is held until you delete your account. Usage data is retained for
|
|
45
|
+
90 days. Payment records are kept for 3 years as required by applicable law.
|
|
46
|
+
|
|
47
|
+
Your Rights
|
|
48
|
+
Under the GDPR you have the right to access, correct, erase, port, restrict, and object
|
|
49
|
+
to processing of your data. California residents may opt out of the sale of personal
|
|
50
|
+
information and are protected from discrimination for exercising their rights.
|
|
51
|
+
|
|
52
|
+
Contact: privacy@acme.com | Acme, Inc., 123 Main St, San Francisco, CA 94105
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Migrated config
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// openpolicy.ts
|
|
59
|
+
import {
|
|
60
|
+
defineConfig,
|
|
61
|
+
Compliance,
|
|
62
|
+
DataCategories,
|
|
63
|
+
Retention,
|
|
64
|
+
Providers,
|
|
65
|
+
dataCollected,
|
|
66
|
+
thirdParties,
|
|
67
|
+
} from "@openpolicy/sdk";
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
company: {
|
|
71
|
+
name: "Acme",
|
|
72
|
+
legalName: "Acme, Inc.",
|
|
73
|
+
address: "123 Main St, San Francisco, CA 94105",
|
|
74
|
+
contact: "privacy@acme.com",
|
|
75
|
+
},
|
|
76
|
+
privacy: {
|
|
77
|
+
effectiveDate: "2026-01-01",
|
|
78
|
+
// GDPR + CCPA: spread both presets, then union the array fields
|
|
79
|
+
...Compliance.GDPR,
|
|
80
|
+
jurisdictions: [
|
|
81
|
+
...Compliance.GDPR.jurisdictions,
|
|
82
|
+
...Compliance.CCPA.jurisdictions,
|
|
83
|
+
],
|
|
84
|
+
userRights: [
|
|
85
|
+
...Compliance.GDPR.userRights,
|
|
86
|
+
...Compliance.CCPA.userRights,
|
|
87
|
+
],
|
|
88
|
+
dataCollected: {
|
|
89
|
+
...dataCollected,
|
|
90
|
+
...DataCategories.AccountInfo,
|
|
91
|
+
...DataCategories.PaymentInfo,
|
|
92
|
+
...DataCategories.UsageData,
|
|
93
|
+
...DataCategories.DeviceInfo,
|
|
94
|
+
},
|
|
95
|
+
retention: {
|
|
96
|
+
"Account Information": Retention.UntilAccountDeletion,
|
|
97
|
+
"Usage Data": Retention.NinetyDays,
|
|
98
|
+
"Payment Information": Retention.ThreeYears,
|
|
99
|
+
},
|
|
100
|
+
cookies: { essential: true, analytics: true, marketing: false },
|
|
101
|
+
thirdParties: [
|
|
102
|
+
...thirdParties,
|
|
103
|
+
Providers.GoogleAnalytics,
|
|
104
|
+
Providers.Stripe,
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Core Patterns
|
|
111
|
+
|
|
112
|
+
### 1. Mapping data collection sections
|
|
113
|
+
|
|
114
|
+
Read each "data we collect" section and identify the category name and the specific fields listed. Map each to a short label — never copy sentences.
|
|
115
|
+
|
|
116
|
+
`DataCategories` presets cover the most common categories. Check if the existing policy's categories match before reaching for custom keys:
|
|
117
|
+
|
|
118
|
+
| Preset | Generated key | Fields |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `DataCategories.AccountInfo` | `"Account Information"` | Name, Email address |
|
|
121
|
+
| `DataCategories.SessionData` | `"Session Data"` | IP address, User agent, Browser type |
|
|
122
|
+
| `DataCategories.PaymentInfo` | `"Payment Information"` | Card last 4 digits, Billing name, Billing address |
|
|
123
|
+
| `DataCategories.UsageData` | `"Usage Data"` | Pages visited, Features used, Time spent |
|
|
124
|
+
| `DataCategories.DeviceInfo` | `"Device Information"` | Device type, Operating system, Browser version |
|
|
125
|
+
| `DataCategories.LocationData` | `"Location Data"` | Country, City, Timezone |
|
|
126
|
+
| `DataCategories.Communications` | `"Communications"` | Email content, Support tickets |
|
|
127
|
+
|
|
128
|
+
For categories not covered by a preset, add a custom key with short field labels:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
dataCollected: {
|
|
132
|
+
...dataCollected,
|
|
133
|
+
...DataCategories.AccountInfo,
|
|
134
|
+
"Health Data": ["Blood glucose readings", "Heart rate"],
|
|
135
|
+
},
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Always spread `dataCollected` first so the autoCollect plugin's output is included alongside the explicit entries.
|
|
139
|
+
|
|
140
|
+
### 2. Mapping jurisdiction and legal basis from GDPR/CCPA language
|
|
141
|
+
|
|
142
|
+
Scan the existing policy for jurisdiction signals:
|
|
143
|
+
|
|
144
|
+
| Prose signal | Maps to |
|
|
145
|
+
|---|---|
|
|
146
|
+
| "GDPR", "EU", "EEA", "European" | `jurisdictions: ["eu"]` |
|
|
147
|
+
| "CCPA", "California", "California residents" | `jurisdictions: ["ca"]` |
|
|
148
|
+
| "Australian Privacy Act" | `jurisdictions: ["au"]` |
|
|
149
|
+
| No specific regulation cited | `jurisdictions: ["us"]` |
|
|
150
|
+
|
|
151
|
+
For legal basis (GDPR policies only), map the stated basis:
|
|
152
|
+
|
|
153
|
+
| Prose | `legalBasis` value |
|
|
154
|
+
|---|---|
|
|
155
|
+
| "legitimate interests" | `"legitimate_interests"` |
|
|
156
|
+
| "your consent" / "you have agreed" | `"consent"` |
|
|
157
|
+
| "to perform a contract" / "to provide the service" | `"contract"` |
|
|
158
|
+
| "legal obligation" / "required by law" | `"legal_obligation"` |
|
|
159
|
+
|
|
160
|
+
When the policy states more than one basis, use an array:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
legalBasis: ["legitimate_interests", "consent"],
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Use `Compliance.GDPR` or `Compliance.CCPA` as a starting point when the existing policy explicitly targets those regulations. Merge the array fields when both apply:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
...Compliance.GDPR,
|
|
170
|
+
jurisdictions: [...Compliance.GDPR.jurisdictions, ...Compliance.CCPA.jurisdictions],
|
|
171
|
+
userRights: [...Compliance.GDPR.userRights, ...Compliance.CCPA.userRights],
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`Compliance.CCPA` does not include `legalBasis` — only add it when the existing policy states an EU legal basis.
|
|
175
|
+
|
|
176
|
+
### 3. Mapping TermsOfService optional sections
|
|
177
|
+
|
|
178
|
+
`TermsOfServiceConfig` has three required fields (`effectiveDate`, `acceptance`, `governingLaw`) and many optional ones. The rule for migration: if the existing document has a section covering a concept, include the corresponding config field.
|
|
179
|
+
|
|
180
|
+
Read each section heading and match it:
|
|
181
|
+
|
|
182
|
+
| Existing section | Config field |
|
|
183
|
+
|---|---|
|
|
184
|
+
| Age / eligibility requirements | `eligibility` |
|
|
185
|
+
| Account creation, credentials | `accounts` |
|
|
186
|
+
| Prohibited conduct / acceptable use | `prohibitedUses` |
|
|
187
|
+
| User-generated content, uploads | `userContent` |
|
|
188
|
+
| Intellectual property, trademarks | `intellectualProperty` |
|
|
189
|
+
| Pricing, subscriptions, refunds | `payments` |
|
|
190
|
+
| Uptime, maintenance | `availability` |
|
|
191
|
+
| Account suspension / cancellation | `termination` |
|
|
192
|
+
| Disclaimer of warranties | `disclaimers` |
|
|
193
|
+
| Limitation of liability | `limitationOfLiability` |
|
|
194
|
+
| Indemnification | `indemnification` |
|
|
195
|
+
| Third-party services / links | `thirdPartyServices` |
|
|
196
|
+
| Dispute resolution, arbitration | `disputeResolution` |
|
|
197
|
+
| How changes are communicated | `changesPolicy` |
|
|
198
|
+
| Link to privacy policy | `privacyPolicyUrl` |
|
|
199
|
+
|
|
200
|
+
A full migration for a SaaS product with payments, user content, and arbitration:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
terms: {
|
|
204
|
+
effectiveDate: "2026-01-01",
|
|
205
|
+
acceptance: { methods: ["creating an account", "using the service"] },
|
|
206
|
+
governingLaw: { jurisdiction: "Delaware, USA" },
|
|
207
|
+
eligibility: { minimumAge: 18 },
|
|
208
|
+
accounts: {
|
|
209
|
+
registrationRequired: true,
|
|
210
|
+
userResponsibleForCredentials: true,
|
|
211
|
+
companyCanTerminate: true,
|
|
212
|
+
},
|
|
213
|
+
prohibitedUses: [
|
|
214
|
+
"Reverse engineering the service",
|
|
215
|
+
"Automated scraping without prior written consent",
|
|
216
|
+
"Using the service to violate applicable law",
|
|
217
|
+
],
|
|
218
|
+
userContent: {
|
|
219
|
+
usersOwnContent: true,
|
|
220
|
+
licenseGrantedToCompany: true,
|
|
221
|
+
licenseDescription: "A worldwide, royalty-free license to host and display your content.",
|
|
222
|
+
companyCanRemoveContent: true,
|
|
223
|
+
},
|
|
224
|
+
payments: {
|
|
225
|
+
hasPaidFeatures: true,
|
|
226
|
+
refundPolicy: "30-day money-back guarantee on annual plans.",
|
|
227
|
+
priceChangesNotice: "30 days advance notice via email.",
|
|
228
|
+
},
|
|
229
|
+
termination: {
|
|
230
|
+
companyCanTerminate: true,
|
|
231
|
+
userCanTerminate: true,
|
|
232
|
+
effectOfTermination: "All licenses granted to the user terminate immediately.",
|
|
233
|
+
},
|
|
234
|
+
disclaimers: { serviceProvidedAsIs: true, noWarranties: true },
|
|
235
|
+
limitationOfLiability: {
|
|
236
|
+
excludesIndirectDamages: true,
|
|
237
|
+
liabilityCap: "Fees paid in the prior 12 months.",
|
|
238
|
+
},
|
|
239
|
+
disputeResolution: {
|
|
240
|
+
method: "arbitration",
|
|
241
|
+
venue: "San Francisco, CA",
|
|
242
|
+
classActionWaiver: true,
|
|
243
|
+
},
|
|
244
|
+
changesPolicy: {
|
|
245
|
+
noticeMethod: "Email notification",
|
|
246
|
+
noticePeriodDays: 30,
|
|
247
|
+
},
|
|
248
|
+
privacyPolicyUrl: "https://acme.com/privacy",
|
|
249
|
+
},
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 4. Using presets to standardize values
|
|
253
|
+
|
|
254
|
+
Prefer preset constants over raw strings wherever the meaning matches exactly. This reduces typo risk and keeps the config readable.
|
|
255
|
+
|
|
256
|
+
**Retention periods** — match common prose to preset keys:
|
|
257
|
+
|
|
258
|
+
| Prose | Preset |
|
|
259
|
+
|---|---|
|
|
260
|
+
| "until you delete your account" | `Retention.UntilAccountDeletion` |
|
|
261
|
+
| "until your session ends" | `Retention.UntilSessionExpiry` |
|
|
262
|
+
| "30 days" | `Retention.ThirtyDays` |
|
|
263
|
+
| "90 days" | `Retention.NinetyDays` |
|
|
264
|
+
| "1 year" | `Retention.OneYear` |
|
|
265
|
+
| "3 years" | `Retention.ThreeYears` |
|
|
266
|
+
| "as required by law" | `Retention.AsRequiredByLaw` |
|
|
267
|
+
|
|
268
|
+
For a period not in the preset list, use a plain string:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
retention: {
|
|
272
|
+
"Audit Logs": "7 years",
|
|
273
|
+
},
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**User rights** — `UserRight` enum values and their prose equivalents:
|
|
277
|
+
|
|
278
|
+
| Prose | Value |
|
|
279
|
+
|---|---|
|
|
280
|
+
| right of access / to view your data | `"access"` |
|
|
281
|
+
| right to correct / rectify | `"rectification"` |
|
|
282
|
+
| right to delete / erasure / "right to be forgotten" | `"erasure"` |
|
|
283
|
+
| right to data portability | `"portability"` |
|
|
284
|
+
| right to restrict processing | `"restriction"` |
|
|
285
|
+
| right to object | `"objection"` |
|
|
286
|
+
| right to opt out of sale | `"opt_out_sale"` |
|
|
287
|
+
| right to non-discrimination | `"non_discrimination"` |
|
|
288
|
+
|
|
289
|
+
## Common Mistakes
|
|
290
|
+
|
|
291
|
+
### HIGH — Passing prose text as field values instead of mapping to structured fields
|
|
292
|
+
|
|
293
|
+
OpenPolicy generates all human-readable sentences from the config structure. Passing paragraph text into fields produces malformed or legally duplicated output.
|
|
294
|
+
|
|
295
|
+
Wrong:
|
|
296
|
+
```ts
|
|
297
|
+
privacy: {
|
|
298
|
+
dataCollected: {
|
|
299
|
+
// WRONG: prose sentence passed as a field label
|
|
300
|
+
"Data": ["We collect information you provide when you register for an account, including your name and email address."],
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Correct:
|
|
306
|
+
```ts
|
|
307
|
+
privacy: {
|
|
308
|
+
dataCollected: {
|
|
309
|
+
"Account Information": ["Name", "Email address"],
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The same principle applies to `prohibitedUses` (keep each entry to a short clause, not a full paragraph), `acceptance.methods` (short action phrases, not sentences), and `payments.refundPolicy` (one concise sentence is acceptable; not a multi-paragraph refund terms block).
|
|
315
|
+
|
|
316
|
+
Source: `packages/core/src/types.ts`
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### MEDIUM — Omitting optional TermsOfService sections that cover existing policy content
|
|
321
|
+
|
|
322
|
+
Skipping optional sections when the existing document has matching content produces a terms document that is legally less complete than what the team approved.
|
|
323
|
+
|
|
324
|
+
Wrong:
|
|
325
|
+
```ts
|
|
326
|
+
// Existing policy has payment terms, user content policy, and account termination section —
|
|
327
|
+
// but all three are omitted from the config.
|
|
328
|
+
terms: {
|
|
329
|
+
effectiveDate: "2026-01-01",
|
|
330
|
+
acceptance: { methods: ["using the service"] },
|
|
331
|
+
governingLaw: { jurisdiction: "Delaware, USA" },
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Correct:
|
|
336
|
+
```ts
|
|
337
|
+
terms: {
|
|
338
|
+
effectiveDate: "2026-01-01",
|
|
339
|
+
acceptance: { methods: ["using the service"] },
|
|
340
|
+
governingLaw: { jurisdiction: "Delaware, USA" },
|
|
341
|
+
payments: {
|
|
342
|
+
hasPaidFeatures: true,
|
|
343
|
+
refundPolicy: "30-day money-back guarantee.",
|
|
344
|
+
priceChangesNotice: "30 days advance notice.",
|
|
345
|
+
},
|
|
346
|
+
userContent: {
|
|
347
|
+
usersOwnContent: true,
|
|
348
|
+
licenseGrantedToCompany: true,
|
|
349
|
+
companyCanRemoveContent: true,
|
|
350
|
+
},
|
|
351
|
+
termination: {
|
|
352
|
+
companyCanTerminate: true,
|
|
353
|
+
userCanTerminate: true,
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Before finalizing the config, re-read each section of the original document and confirm a corresponding field is present in the output config. Optional sections that are absent from the config are entirely omitted from the compiled document — there is no fallback prose.
|
|
359
|
+
|
|
360
|
+
Source: `packages/core/src/types.ts`
|