@tollwyn/baukasten 0.0.2 → 1.2.3
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 +254 -1
- package/dist/bin/check.d.ts +2 -0
- package/dist/bin/check.js +38 -0
- package/dist/bin/migrate.d.ts +2 -0
- package/dist/bin/migrate.js +111 -0
- package/dist/bin/stubs.d.ts +2 -0
- package/dist/bin/stubs.js +38 -0
- package/dist/bin/test-accounts.d.ts +2 -0
- package/dist/bin/test-accounts.js +62 -0
- package/dist/check.d.ts +20 -0
- package/dist/check.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9 -0
- package/dist/product.d.ts +161 -0
- package/dist/product.js +152 -0
- package/dist/server/accounts.d.ts +128 -0
- package/dist/server/accounts.js +282 -0
- package/dist/server/adapter/db.d.ts +5 -0
- package/dist/server/adapter/db.js +63 -0
- package/dist/server/adapter/log.d.ts +3 -0
- package/dist/server/adapter/log.js +12 -0
- package/dist/server/adapter/mailer.d.ts +4 -0
- package/dist/server/adapter/mailer.js +56 -0
- package/dist/server/adapter/migrate.d.ts +21 -0
- package/dist/server/adapter/migrate.js +113 -0
- package/dist/server/adapter/secrets.d.ts +2 -0
- package/dist/server/adapter/secrets.js +40 -0
- package/dist/server/adapter/stripe.d.ts +11 -0
- package/dist/server/adapter/stripe.js +333 -0
- package/dist/server/api.d.ts +25 -0
- package/dist/server/api.js +98 -0
- package/dist/server/baukasten-migrations.d.ts +4 -0
- package/dist/server/baukasten-migrations.js +126 -0
- package/dist/server/billing.d.ts +94 -0
- package/dist/server/billing.js +349 -0
- package/dist/server/catalog.d.ts +39 -0
- package/dist/server/catalog.js +193 -0
- package/dist/server/config.d.ts +108 -0
- package/dist/server/config.js +244 -0
- package/dist/server/endpoints.d.ts +44 -0
- package/dist/server/endpoints.js +159 -0
- package/dist/server/fields.d.ts +7 -0
- package/dist/server/fields.js +18 -0
- package/dist/server/headers.d.ts +1 -0
- package/dist/server/headers.js +9 -0
- package/dist/server/health.d.ts +34 -0
- package/dist/server/health.js +116 -0
- package/dist/server/hook.d.ts +92 -0
- package/dist/server/hook.js +293 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.js +11 -0
- package/dist/server/legal/de.d.ts +2 -0
- package/dist/server/legal/de.js +251 -0
- package/dist/server/legal/document.d.ts +37 -0
- package/dist/server/legal/document.js +30 -0
- package/dist/server/legal/en.d.ts +2 -0
- package/dist/server/legal/en.js +251 -0
- package/dist/server/legal/fleet.d.ts +114 -0
- package/dist/server/legal/fleet.js +89 -0
- package/dist/server/legal/index.d.ts +3 -0
- package/dist/server/legal/index.js +11 -0
- package/dist/server/limits.d.ts +58 -0
- package/dist/server/limits.js +76 -0
- package/dist/server/log.d.ts +26 -0
- package/dist/server/log.js +61 -0
- package/dist/server/machine.d.ts +4 -0
- package/dist/server/machine.js +57 -0
- package/dist/server/mail.d.ts +20 -0
- package/dist/server/mail.js +29 -0
- package/dist/server/messages.d.ts +105 -0
- package/dist/server/messages.js +232 -0
- package/dist/server/migrations.d.ts +28 -0
- package/dist/server/migrations.js +59 -0
- package/dist/server/openapi.d.ts +24 -0
- package/dist/server/openapi.js +173 -0
- package/dist/server/pages.d.ts +158 -0
- package/dist/server/pages.js +491 -0
- package/dist/server/ports.d.ts +273 -0
- package/dist/server/ports.js +1 -0
- package/dist/server/runs.d.ts +30 -0
- package/dist/server/runs.js +55 -0
- package/dist/server/sales.d.ts +38 -0
- package/dist/server/sales.js +132 -0
- package/dist/server/services.d.ts +57 -0
- package/dist/server/services.js +44 -0
- package/dist/server/start.d.ts +14 -0
- package/dist/server/start.js +65 -0
- package/dist/server/stripe.d.ts +33 -0
- package/dist/server/stripe.js +29 -0
- package/dist/server/terms.d.ts +7 -0
- package/dist/server/terms.js +9 -0
- package/dist/server/test-accounts.d.ts +28 -0
- package/dist/server/test-accounts.js +62 -0
- package/dist/server/via.d.ts +9 -0
- package/dist/server/via.js +15 -0
- package/dist/server/web.d.ts +48 -0
- package/dist/server/web.js +105 -0
- package/dist/shopify/adapter/shopify.d.ts +8 -0
- package/dist/shopify/adapter/shopify.js +232 -0
- package/dist/shopify/admin.d.ts +33 -0
- package/dist/shopify/admin.js +47 -0
- package/dist/shopify/app.d.ts +60 -0
- package/dist/shopify/app.js +67 -0
- package/dist/shopify/config.d.ts +53 -0
- package/dist/shopify/config.js +133 -0
- package/dist/shopify/constants.d.ts +27 -0
- package/dist/shopify/constants.js +27 -0
- package/dist/shopify/headers.d.ts +1 -0
- package/dist/shopify/headers.js +8 -0
- package/dist/shopify/hmac.d.ts +1 -0
- package/dist/shopify/hmac.js +9 -0
- package/dist/shopify/hook.d.ts +60 -0
- package/dist/shopify/hook.js +508 -0
- package/dist/shopify/id-token.d.ts +12 -0
- package/dist/shopify/id-token.js +66 -0
- package/dist/shopify/index.d.ts +19 -0
- package/dist/shopify/index.js +15 -0
- package/dist/shopify/legal/de.d.ts +3 -0
- package/dist/shopify/legal/de.js +123 -0
- package/dist/shopify/legal/en.d.ts +3 -0
- package/dist/shopify/legal/en.js +121 -0
- package/dist/shopify/legal/index.d.ts +19 -0
- package/dist/shopify/legal/index.js +8 -0
- package/dist/shopify/legal/shopify.d.ts +16 -0
- package/dist/shopify/legal/shopify.js +21 -0
- package/dist/shopify/migrations.d.ts +4 -0
- package/dist/shopify/migrations.js +67 -0
- package/dist/shopify/pages.d.ts +45 -0
- package/dist/shopify/pages.js +146 -0
- package/dist/shopify/ports.d.ts +77 -0
- package/dist/shopify/ports.js +1 -0
- package/dist/shopify/sales.d.ts +4 -0
- package/dist/shopify/sales.js +27 -0
- package/dist/shopify/scopes.d.ts +2 -0
- package/dist/shopify/scopes.js +13 -0
- package/dist/shopify/seal.d.ts +12 -0
- package/dist/shopify/seal.js +60 -0
- package/dist/shopify/shop.d.ts +4 -0
- package/dist/shopify/shop.js +15 -0
- package/dist/shopify/shops.d.ts +76 -0
- package/dist/shopify/shops.js +256 -0
- package/dist/shopify/sveltekit.d.ts +19 -0
- package/dist/shopify/sveltekit.js +18 -0
- package/dist/shopify/texts.d.ts +28 -0
- package/dist/shopify/texts.js +49 -0
- package/dist/shopify/toml.d.ts +22 -0
- package/dist/shopify/toml.js +125 -0
- package/dist/shopify/webhooks.d.ts +65 -0
- package/dist/shopify/webhooks.js +220 -0
- package/dist/stubs.d.ts +15 -0
- package/dist/stubs.js +287 -0
- package/dist/sveltekit.d.ts +20 -0
- package/dist/sveltekit.js +18 -0
- package/dist/testing/app.d.ts +36 -0
- package/dist/testing/app.js +155 -0
- package/dist/testing/browser.d.ts +21 -0
- package/dist/testing/browser.js +21 -0
- package/dist/testing/connection.d.ts +7 -0
- package/dist/testing/connection.js +303 -0
- package/dist/testing/database.d.ts +21 -0
- package/dist/testing/database.js +54 -0
- package/dist/testing/env-example.d.ts +4 -0
- package/dist/testing/env-example.js +33 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/shopify-connection.d.ts +8 -0
- package/dist/testing/shopify-connection.js +313 -0
- package/dist/testing/shopify.d.ts +71 -0
- package/dist/testing/shopify.js +278 -0
- package/dist/testing/smtp.d.ts +38 -0
- package/dist/testing/smtp.js +165 -0
- package/dist/testing/stripe.d.ts +128 -0
- package/dist/testing/stripe.js +511 -0
- package/dist/texts.d.ts +139 -0
- package/dist/texts.js +265 -0
- package/dist/ui/AccountPage.svelte +143 -0
- package/dist/ui/AccountPage.svelte.d.ts +10 -0
- package/dist/ui/ConfirmEmailPage.svelte +21 -0
- package/dist/ui/ConfirmEmailPage.svelte.d.ts +10 -0
- package/dist/ui/ErrorPage.svelte +14 -0
- package/dist/ui/ErrorPage.svelte.d.ts +3 -0
- package/dist/ui/Field.svelte +12 -0
- package/dist/ui/Field.svelte.d.ts +10 -0
- package/dist/ui/ForgotPasswordPage.svelte +32 -0
- package/dist/ui/ForgotPasswordPage.svelte.d.ts +7 -0
- package/dist/ui/Form.svelte +30 -0
- package/dist/ui/Form.svelte.d.ts +11 -0
- package/dist/ui/Frame.svelte +84 -0
- package/dist/ui/Frame.svelte.d.ts +12 -0
- package/dist/ui/LegalPage.svelte +38 -0
- package/dist/ui/LegalPage.svelte.d.ts +9 -0
- package/dist/ui/Linked.svelte +5 -0
- package/dist/ui/Linked.svelte.d.ts +8 -0
- package/dist/ui/LoginPage.svelte +41 -0
- package/dist/ui/LoginPage.svelte.d.ts +10 -0
- package/dist/ui/LogoutPage.svelte +19 -0
- package/dist/ui/LogoutPage.svelte.d.ts +9 -0
- package/dist/ui/ResetPasswordPage.svelte +30 -0
- package/dist/ui/ResetPasswordPage.svelte.d.ts +10 -0
- package/dist/ui/SignupPage.svelte +52 -0
- package/dist/ui/SignupPage.svelte.d.ts +10 -0
- package/dist/ui/VerifyEmailPage.svelte +50 -0
- package/dist/ui/VerifyEmailPage.svelte.d.ts +10 -0
- package/dist/ui/base.css +287 -0
- package/dist/ui/data.d.ts +167 -0
- package/dist/ui/data.js +1 -0
- package/dist/ui/frame.d.ts +17 -0
- package/dist/ui/frame.js +27 -0
- package/dist/ui/index.d.ts +15 -0
- package/dist/ui/index.js +15 -0
- package/dist/variables.d.ts +20 -0
- package/dist/variables.js +62 -0
- package/package.json +88 -4
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { SESSION_DAYS } from '../accounts.js';
|
|
2
|
+
import { code, heading, lettered, link, list, longDate, mail, note, numbered, paragraph, strong } from './document.js';
|
|
3
|
+
import { FLEET, FLEET_PROCESSORS } from './fleet.js';
|
|
4
|
+
const { cloudflare, hoster, neon, stripe, webServer } = FLEET;
|
|
5
|
+
const BASIS = {
|
|
6
|
+
contract: 'performance of the contract, Art. 6(1)(b) GDPR',
|
|
7
|
+
legitimateInterest: 'legitimate interest, Art. 6(1)(f) GDPR'
|
|
8
|
+
};
|
|
9
|
+
const TRANSFER = {
|
|
10
|
+
adequate: 'a country with adequate protection',
|
|
11
|
+
dpf: 'certified under the Data Privacy Framework',
|
|
12
|
+
scc: 'under the EU standard contractual clauses, as recognised by the FDPIC'
|
|
13
|
+
};
|
|
14
|
+
function register(entry) {
|
|
15
|
+
switch (entry.kind) {
|
|
16
|
+
case 'none':
|
|
17
|
+
return 'Sole proprietorship, not entered in the Swiss commercial register.';
|
|
18
|
+
case 'entered':
|
|
19
|
+
return `Commercial register: ${entry.uid}.`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const VAT = {
|
|
23
|
+
none: {
|
|
24
|
+
imprint: 'Not registered for Swiss VAT; there is no VAT number.',
|
|
25
|
+
terms: 'We are not registered for Swiss VAT and charge no VAT; you account for any tax due in your country.'
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const BROWSER = {
|
|
29
|
+
networkErrorLogging: `${cloudflare.name} also has your browser store an instruction to report failed connections (Network Error Logging). Clearing cookies and site data in your browser removes this instruction too.`
|
|
30
|
+
};
|
|
31
|
+
const EDGE = {
|
|
32
|
+
worldwideUnderScc: 'in the data centre nearest to you, which can be outside Switzerland and the EU; there the standard contractual clauses of its data processing addendum apply'
|
|
33
|
+
};
|
|
34
|
+
const ACCESS_LOG = {
|
|
35
|
+
none: 'Our web server keeps no access log'
|
|
36
|
+
};
|
|
37
|
+
const ERROR_LOG = {
|
|
38
|
+
requestWithoutAddress: 'only a request it cannot pass on to the application it writes to its error log, without your IP address and without parameters or headers'
|
|
39
|
+
};
|
|
40
|
+
const NEW_VERSION = {
|
|
41
|
+
removed: 'it is also removed whenever a new version starts',
|
|
42
|
+
stays: 'it stays when a new version starts'
|
|
43
|
+
};
|
|
44
|
+
const STRIPE_ROLE = {
|
|
45
|
+
controller: 'Stripe also processes data as a controller of its own, for example to prevent fraud and to meet its legal duties; see '
|
|
46
|
+
};
|
|
47
|
+
/** The section of the terms on processing on behalf, which the privacy policy points to. */
|
|
48
|
+
const ON_BEHALF = 'Data we process on your behalf';
|
|
49
|
+
const who = (f) => `${f.operator.name}, ${f.operator.address}`;
|
|
50
|
+
const version = (f) => note(`Version of ${longDate(f.version, 'en')}.`);
|
|
51
|
+
const from = (data) => data.source === undefined ? '' : `, from ${data.source}`;
|
|
52
|
+
export const en = {
|
|
53
|
+
imprint: (f) => ({
|
|
54
|
+
title: 'Imprint',
|
|
55
|
+
blocks: [
|
|
56
|
+
paragraph(`${f.name} is operated by`),
|
|
57
|
+
paragraph(strong(f.operator.name)),
|
|
58
|
+
paragraph(f.operator.address),
|
|
59
|
+
paragraph(`${register(f.operator.register)} ${VAT[f.operator.vat].imprint}`),
|
|
60
|
+
paragraph('Email: ', mail(FLEET.mailbox), '. Email is the only support channel; there is no telephone hotline.'),
|
|
61
|
+
paragraph('The operator named above is responsible for the content of this website.')
|
|
62
|
+
]
|
|
63
|
+
}),
|
|
64
|
+
privacy: (f) => {
|
|
65
|
+
const own = f.legal.data;
|
|
66
|
+
const processor = f.legal.processor;
|
|
67
|
+
return {
|
|
68
|
+
title: 'Privacy policy',
|
|
69
|
+
blocks: [
|
|
70
|
+
version(f),
|
|
71
|
+
paragraph(`This policy explains which personal data ${f.name} processes, why, where and for how long. The controller is ${who(f)}, reachable at `, mail(FLEET.mailbox), '. It follows the Swiss Federal Act on Data Protection (FADP) and, where it applies, the EU General Data Protection Regulation (GDPR).'),
|
|
72
|
+
heading('What we process, why, and on what basis'),
|
|
73
|
+
list([
|
|
74
|
+
strong('Account'),
|
|
75
|
+
`: your email address, a salted hash of your password, when you signed up and which version of the terms you accepted, when you confirmed your address, the links we send you by email (with a new address you ask for), your login sessions and hashes of your API keys — to log you in, to answer the requests of your API keys, to send you account emails such as the confirmation of your address and the password reset, and to know which terms apply (${BASIS.contract}). You need an email address to use ${f.name}.`
|
|
76
|
+
], ...own.map((data) => [
|
|
77
|
+
strong(data.name),
|
|
78
|
+
`: ${data.what}${from(data)} — ${data.purpose} (${BASIS[data.basis]}).`
|
|
79
|
+
]), [
|
|
80
|
+
strong('Payment'),
|
|
81
|
+
`: Stripe processes your email address, payment details and billing address, and a tax ID if you give one; we keep your Stripe customer and subscription numbers, the state of your subscription and the amounts and dates of your payments (${BASIS.contract}; for keeping them afterwards, our legitimate interest in meeting Swiss accounting law, Art. 6(1)(f) GDPR).`
|
|
82
|
+
], [
|
|
83
|
+
strong('Partner links'),
|
|
84
|
+
`: when you come to ${f.name} through a link with `,
|
|
85
|
+
code('?via=…'),
|
|
86
|
+
', the links of the site carry that partner name on, and when you sign up we store it with your account; software that signs up over the API may send one in the field ',
|
|
87
|
+
code('via'),
|
|
88
|
+
`. It holds nothing but the partner name, and we use it to know which partners bring customers (${BASIS.legitimateInterest}). We set no cookie for this and store nothing in your browser.`
|
|
89
|
+
], [
|
|
90
|
+
strong('Security and operations'),
|
|
91
|
+
`: our application logs each request (method, path without parameters, status, duration) and events of the service with internal numbers such as that of your account, without IP addresses, email addresses, passwords or keys. ${ACCESS_LOG[webServer.accessLog]}; ${ERROR_LOG[webServer.errorLog.content]}. Our mail relay logs the recipient address and the delivery status of each email we send and keeps them for at most ${FLEET.relayLogDays} days. To limit sign-ups, logins and other attempts, we count them in the memory of the application, never on disk: per IP address, per account, per API key and per email address, this one only as a hash; a count applies for at most one hour and is forgotten at the latest when the application restarts (legitimate interest in a secure and working service, Art. 6(1)(f) GDPR).`
|
|
92
|
+
], [
|
|
93
|
+
strong('Emails to us'),
|
|
94
|
+
': when you write to us, we keep your message and address to answer you and to keep a record of the exchange (legitimate interest; for customers, performance of the contract).'
|
|
95
|
+
]),
|
|
96
|
+
...(processor.kind === 'yes'
|
|
97
|
+
? [
|
|
98
|
+
paragraph(`For personal data ${processor.data}, which concern ${processor.subjects}, you are the controller, and we process them on your behalf (see “${ON_BEHALF}” in the `, link('terms', '/terms'), ').')
|
|
99
|
+
]
|
|
100
|
+
: []),
|
|
101
|
+
paragraph(strong('Cookies'), ': we set one cookie, only for this site and HttpOnly, Secure and SameSite=Lax: ', code('__Host-session'), ` keeps you logged in, for ${SESSION_DAYS} days or until you log out, and is needed for the web app. You can block cookies in your browser; you can then not log in to the web app, while the API works with keys. ${BROWSER[cloudflare.browser]} We use no advertising or analytics trackers, sell no data, and make no decisions about you by automated means.`),
|
|
102
|
+
heading('Who receives data, and where'),
|
|
103
|
+
list([
|
|
104
|
+
`${cloudflare.name} (${cloudflare.country.en}; ${TRANSFER[cloudflare.transfer]}) runs in front of ${f.name}: it terminates the encrypted connection, passes requests on and filters attacks. It therefore sees every request, including your IP address, ${EDGE[cloudflare.edge]}. It also stores the encrypted backups of our database (Cloudflare R2, ${cloudflare.backups.where.en}), which it cannot read.`
|
|
105
|
+
], [
|
|
106
|
+
`Application server: ${hoster.name} (${hoster.country.en}), in its data centres in ${hoster.dataCentres.en}; the server is rented and administered by the operator.`
|
|
107
|
+
], [
|
|
108
|
+
`Database: ${neon.name} (a ${neon.owner} company, ${neon.country.en}; ${TRANSFER[neon.transfer]}), stored in ${neon.cloud} region ${neon.region}, ${neon.place.en}; access from the provider’s country is possible.`
|
|
109
|
+
], [
|
|
110
|
+
`Payments: ${stripe.name} (${stripe.country.en}), with transfers to ${stripe.parent.name} (${stripe.parent.country.en}; ${TRANSFER[stripe.parent.transfer]}). ${STRIPE_ROLE[stripe.role]}`,
|
|
111
|
+
link(stripe.privacy, `https://${stripe.privacy}`),
|
|
112
|
+
'.'
|
|
113
|
+
], [
|
|
114
|
+
`Email: ${FLEET.mail.name} (${FLEET.mail.country.en}) sends our emails and keeps our mailbox.`
|
|
115
|
+
], ...f.legal.recipients.map((recipient) => [
|
|
116
|
+
`${recipient.name} (${recipient.place}; ${TRANSFER[recipient.transfer]}): ${recipient.purpose}.`
|
|
117
|
+
])),
|
|
118
|
+
paragraph(`Data may therefore be processed in the countries named above and, for ${cloudflare.name}, in the country of the data centre that answers you. Each recipient outside Switzerland and the EU is named above with the safeguard that covers it.`),
|
|
119
|
+
heading('How long'),
|
|
120
|
+
list([
|
|
121
|
+
'Your account with its sessions, links and keys stays until you delete it under Account; then it is deleted at once.'
|
|
122
|
+
], ...own.map((data) => [strong(data.name), `: ${data.kept}.`]), [
|
|
123
|
+
`Our database provider keeps a history for restoring the database to an earlier point in time for at most ${neon.historyDays} days, and our encrypted backups of it are deleted after about ${cloudflare.backups.days} days; deleted data leave both within that time.`
|
|
124
|
+
], [
|
|
125
|
+
'After deletion only a pseudonymous record of the account remains, without your email address: the partner name, the dates of the account (such as sign-up) and the version of the terms accepted, the Stripe customer and subscription numbers with the last state of the subscription, and the amounts and dates of your payments. We keep them for ten years, as Swiss accounting law requires (Art. 958f CO). Stripe keeps its own records, including your invoices.'
|
|
126
|
+
], [
|
|
127
|
+
`The application log rotates by size, in ${FLEET.appLog.files} files of ${FLEET.appLog.megabytes} MB, and is overwritten as new entries come in; ${NEW_VERSION[FLEET.appLog.newVersion]}. The error log of the web server rotates by size as well, in ${webServer.errorLog.files} files of ${webServer.errorLog.megabytes} MB, and is overwritten as new entries come in; ${NEW_VERSION[webServer.errorLog.newVersion]}. The mail relay log is kept for at most ${FLEET.relayLogDays} days.`
|
|
128
|
+
], [
|
|
129
|
+
`The session lasts ${SESSION_DAYS} days or until you log out. The counts per IP address, account, API key and email address apply for at most one hour, are kept in memory only and are forgotten at the latest when the application restarts.`
|
|
130
|
+
], [
|
|
131
|
+
'Emails to us: as long as the exchange needs it and, for customers, as long as the contract and accounting law require.'
|
|
132
|
+
]),
|
|
133
|
+
heading('Your rights'),
|
|
134
|
+
paragraph('You may ask what we hold about you and receive it in a common electronic format, have it corrected or deleted, and have its processing restricted. You may object at any time to processing based on our legitimate interest. You can change your email address and delete your account yourself under Account; for everything else write to ', mail(FLEET.mailbox), '. We answer within 30 days. You may complain to the Federal Data Protection and Information Commissioner (FDPIC) or, in the EU, to the data protection authority of your country.'),
|
|
135
|
+
heading('Changes'),
|
|
136
|
+
paragraph('The version published here applies. We tell customers about important changes by email.')
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
terms: (f) => {
|
|
141
|
+
const processor = f.legal.processor;
|
|
142
|
+
const processors = [
|
|
143
|
+
...FLEET_PROCESSORS,
|
|
144
|
+
...f.legal.recipients
|
|
145
|
+
.filter((recipient) => recipient.role === 'processor')
|
|
146
|
+
.map((recipient) => recipient.name)
|
|
147
|
+
];
|
|
148
|
+
const sections = [
|
|
149
|
+
{
|
|
150
|
+
title: 'For businesses',
|
|
151
|
+
blocks: [
|
|
152
|
+
paragraph(`${f.name} is offered only to businesses, public bodies and the self-employed for their professional activity, not to consumers.`)
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
title: 'The service',
|
|
157
|
+
blocks: [
|
|
158
|
+
paragraph(`${f.legal.service} We may develop ${f.name} further, but we do not remove its main functions during a paid period.`)
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: 'What we do not guarantee',
|
|
163
|
+
blocks: [
|
|
164
|
+
paragraph(`${f.legal.notGuaranteed} We do not guarantee that ${f.name} is always available: maintenance and outages of our hosting and network providers or of Stripe can delay or interrupt the service.`)
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
title: 'Account and trial',
|
|
169
|
+
blocks: [
|
|
170
|
+
paragraph(`You need an account with a valid email address. If ${f.name} offers a free trial, its duration and limits are those shown on its pricing page; it ends by itself and costs nothing unless you pay. Keep your password and API keys secret; you are responsible for what is done with them.`)
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
title: 'Prices and payment',
|
|
175
|
+
blocks: [
|
|
176
|
+
paragraph(`Prices are those shown on the pricing page, in the currency shown there, paid in advance through Stripe. ${VAT[f.operator.vat].terms} A subscription renews for its period until you cancel it in the customer portal; it then ends at the end of the paid period. If you change your plan in the customer portal, Stripe charges or credits the difference pro rata, as shown there; otherwise paid periods and purchases are not refunded, also not when you delete your account, which ends a subscription at once. Unused prepaid credit lapses without refund when you delete your account. If a payment fails, we may suspend what you pay for until it is made. We may change prices with 30 days’ notice by email; you may cancel before the change applies.`)
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
title: 'Use',
|
|
181
|
+
blocks: [
|
|
182
|
+
paragraph(`${f.legal.use} You may not use ${f.name} to overload or disrupt our service, nor against the law.`)
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
title: 'Your data',
|
|
187
|
+
blocks: [
|
|
188
|
+
paragraph(`What you put into ${f.name} remains yours. When you delete your account, it is deleted as described in the privacy policy.`)
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
...(processor.kind === 'yes'
|
|
192
|
+
? [
|
|
193
|
+
{
|
|
194
|
+
title: ON_BEHALF,
|
|
195
|
+
blocks: [
|
|
196
|
+
paragraph(`For personal data ${processor.data}, which concern ${processor.subjects}, you are the controller and we are your processor (Art. 9 FADP, Art. 28 GDPR). We process these data for as long as your account exists and only to provide ${f.name} to you. We:`),
|
|
197
|
+
lettered([
|
|
198
|
+
'process them only on your documented instructions, which are these terms and your settings, and tell you if we think an instruction breaks data protection law;'
|
|
199
|
+
], ['bind everyone who has access to them to confidentiality;'], [
|
|
200
|
+
'protect them with appropriate technical and organisational measures, among them encrypted connections, hashed passwords and keys, and access limited to the operator;'
|
|
201
|
+
], [
|
|
202
|
+
`use as sub-processors only those named in the privacy policy (${processors.join('; ')}), which you approve, and bind them to data protection duties equivalent to these; we announce a new one by email 30 days in advance, and you may object by cancelling before it starts;`
|
|
203
|
+
], [
|
|
204
|
+
'help you, as far as we can, to answer requests of data subjects and to meet your duties on security, breach notification and impact assessments;'
|
|
205
|
+
], [
|
|
206
|
+
'tell you without undue delay of a breach of security that concerns these data;'
|
|
207
|
+
], [
|
|
208
|
+
`delete them when you delete your account, unless the law requires us to keep them; before that you can read or export them through ${processor.retrieve}; copies in the history of the database and in our backups end as the privacy policy describes;`
|
|
209
|
+
], [
|
|
210
|
+
'give you the information you need to show that these duties are met, and allow audits by you or an auditor bound to confidentiality, with reasonable notice and at your cost.'
|
|
211
|
+
]),
|
|
212
|
+
paragraph('Transfers to the countries named in the privacy policy are made under the safeguards named there.')
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
: []),
|
|
217
|
+
{
|
|
218
|
+
title: 'Liability',
|
|
219
|
+
blocks: [
|
|
220
|
+
paragraph('We are liable without limit for intent and gross negligence. For slight negligence, as far as the law permits, our liability is limited to the fees you paid in the twelve months before the damage, and we are not liable for indirect or consequential damage, lost profit or lost data. During a free trial we are liable only for intent and gross negligence. These limits also apply to the liability for our auxiliaries and subcontractors.')
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
title: 'Ending',
|
|
225
|
+
blocks: [
|
|
226
|
+
paragraph('You may delete your account at any time under Account. We may end your account with 30 days’ notice by email and then refund fees paid for the time after the end and unused prepaid credit. We may end it at once if you seriously break these terms.')
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
title: 'Changes to these terms',
|
|
231
|
+
blocks: [
|
|
232
|
+
paragraph('We may change these terms with 30 days’ notice by email. If you do not cancel before the change applies, the new version applies from then on.')
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
title: 'Law and jurisdiction',
|
|
237
|
+
blocks: [
|
|
238
|
+
paragraph(`Swiss law applies, excluding the UN Convention on Contracts for the International Sale of Goods. The exclusive place of jurisdiction is ${f.operator.jurisdiction}, Switzerland; we may also sue you at your seat.`)
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
];
|
|
242
|
+
return {
|
|
243
|
+
title: 'Terms of service',
|
|
244
|
+
blocks: [
|
|
245
|
+
version(f),
|
|
246
|
+
paragraph(`These terms apply between ${who(f)} (“we”) and the business that uses ${f.name} at ${f.origin}, in the web app or through the API (“you”). You accept them when you sign up, on the website or over the API, and again when you pay in Stripe Checkout. Whoever signs up confirms that they act for a business and may bind it.`),
|
|
247
|
+
...numbered(sections)
|
|
248
|
+
]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export declare const FLEET: {
|
|
2
|
+
/** The shared mailbox: the sender of every product (MAIL_FROM) and its contact. */
|
|
3
|
+
readonly mailbox: "hello@tollwyn.com";
|
|
4
|
+
/** In front of every product: TLS, passing requests on, filtering attacks; and R2. */
|
|
5
|
+
readonly cloudflare: {
|
|
6
|
+
readonly name: "Cloudflare, Inc.";
|
|
7
|
+
readonly country: Readonly<Record<"en" | "de", string>>;
|
|
8
|
+
readonly transfer: "dpf";
|
|
9
|
+
readonly role: "processor";
|
|
10
|
+
/**
|
|
11
|
+
* What it has the browser keep: no cookie (seen from outside: no Set-Cookie on pages, forms
|
|
12
|
+
* and the API of a product), but the headers Nel and Report-To, an instruction to report
|
|
13
|
+
* failed connections. Its bot protection would set __cf_bm, which would then be named here.
|
|
14
|
+
*/
|
|
15
|
+
readonly browser: "networkErrorLogging";
|
|
16
|
+
/**
|
|
17
|
+
* Where it answers a visitor: in its data centre nearest to them, anywhere; outside Switzerland
|
|
18
|
+
* and the EU under the standard contractual clauses of its data processing addendum.
|
|
19
|
+
*/
|
|
20
|
+
readonly edge: "worldwideUnderScc";
|
|
21
|
+
/** The encrypted dumps of each database, in a bucket of R2 with a lifecycle rule. */
|
|
22
|
+
readonly backups: {
|
|
23
|
+
readonly where: {
|
|
24
|
+
readonly en: "in the EU";
|
|
25
|
+
readonly de: "in der EU";
|
|
26
|
+
};
|
|
27
|
+
readonly days: 30;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/** The server of the products: the seat of the company, and where its data centres are. */
|
|
31
|
+
readonly hoster: {
|
|
32
|
+
readonly name: "Hetzner Online GmbH";
|
|
33
|
+
readonly country: {
|
|
34
|
+
readonly en: "Germany";
|
|
35
|
+
readonly de: "Deutschland";
|
|
36
|
+
};
|
|
37
|
+
readonly dataCentres: {
|
|
38
|
+
readonly en: "Germany";
|
|
39
|
+
readonly de: "Deutschland";
|
|
40
|
+
};
|
|
41
|
+
readonly role: "processor";
|
|
42
|
+
};
|
|
43
|
+
/** Each product's database: a project of its own. */
|
|
44
|
+
readonly neon: {
|
|
45
|
+
readonly name: "Neon, LLC";
|
|
46
|
+
readonly owner: "Databricks";
|
|
47
|
+
readonly country: Readonly<Record<"en" | "de", string>>;
|
|
48
|
+
/** Certified under the Data Privacy Framework through Databricks. */
|
|
49
|
+
readonly transfer: "dpf";
|
|
50
|
+
readonly role: "processor";
|
|
51
|
+
readonly cloud: "Amazon Web Services";
|
|
52
|
+
/** The region of the cloud, which Neon names aws-eu-central-1. */
|
|
53
|
+
readonly region: "eu-central-1";
|
|
54
|
+
readonly place: {
|
|
55
|
+
readonly en: "Frankfurt, Germany";
|
|
56
|
+
readonly de: "Frankfurt, Deutschland";
|
|
57
|
+
};
|
|
58
|
+
/** The history to restore a database to an earlier point, at most this long. */
|
|
59
|
+
readonly historyDays: 30;
|
|
60
|
+
};
|
|
61
|
+
/** Each product's own Stripe account; Stripe is a controller of its own too. */
|
|
62
|
+
readonly stripe: {
|
|
63
|
+
readonly name: "Stripe Payments Europe, Limited";
|
|
64
|
+
readonly country: {
|
|
65
|
+
readonly en: "Ireland";
|
|
66
|
+
readonly de: "Irland";
|
|
67
|
+
};
|
|
68
|
+
readonly role: "controller";
|
|
69
|
+
readonly parent: {
|
|
70
|
+
readonly name: "Stripe, Inc.";
|
|
71
|
+
readonly country: Readonly<Record<"en" | "de", string>>;
|
|
72
|
+
readonly transfer: "dpf";
|
|
73
|
+
};
|
|
74
|
+
readonly privacy: "stripe.com/privacy";
|
|
75
|
+
};
|
|
76
|
+
/** Sends the mails from the relay on and keeps the mailbox. */
|
|
77
|
+
readonly mail: {
|
|
78
|
+
readonly name: "Infomaniak Network SA";
|
|
79
|
+
readonly country: {
|
|
80
|
+
readonly en: "Switzerland";
|
|
81
|
+
readonly de: "Schweiz";
|
|
82
|
+
};
|
|
83
|
+
readonly role: "processor";
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The log of a product's container rotates by size in this many files of this size, and goes with
|
|
87
|
+
* the container when a new version starts.
|
|
88
|
+
*/
|
|
89
|
+
readonly appLog: {
|
|
90
|
+
readonly files: 3;
|
|
91
|
+
readonly megabytes: 10;
|
|
92
|
+
readonly newVersion: "removed";
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Caddy keeps no access log. What fails, a request it cannot pass on among it, its default logger
|
|
96
|
+
* writes into the log of its container, filtered: without headers (the address of the visitor
|
|
97
|
+
* stood in CF-Connecting-IP and X-Forwarded-For), without the query, the address of Cloudflare
|
|
98
|
+
* shortened. Docker rotates that log by size; it belongs to the web server, not to a product, and
|
|
99
|
+
* stays when a product starts a new version.
|
|
100
|
+
*/
|
|
101
|
+
readonly webServer: {
|
|
102
|
+
readonly accessLog: "none";
|
|
103
|
+
readonly errorLog: {
|
|
104
|
+
readonly content: "requestWithoutAddress";
|
|
105
|
+
readonly files: 3;
|
|
106
|
+
readonly megabytes: 20;
|
|
107
|
+
readonly newVersion: "stays";
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
/** The mail relay logs each recipient and the state of each delivery; journald keeps it this long. */
|
|
111
|
+
readonly relayLogDays: 30;
|
|
112
|
+
};
|
|
113
|
+
/** The providers of the fleet that process data for the operator, which the terms name. */
|
|
114
|
+
export declare const FLEET_PROCESSORS: ("Cloudflare, Inc." | "Hetzner Online GmbH" | "Neon, LLC" | "Infomaniak Network SA")[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const USA = { en: 'USA', de: 'USA' };
|
|
2
|
+
export const FLEET = {
|
|
3
|
+
/** The shared mailbox: the sender of every product (MAIL_FROM) and its contact. */
|
|
4
|
+
mailbox: 'hello@tollwyn.com',
|
|
5
|
+
/** In front of every product: TLS, passing requests on, filtering attacks; and R2. */
|
|
6
|
+
cloudflare: {
|
|
7
|
+
name: 'Cloudflare, Inc.',
|
|
8
|
+
country: USA,
|
|
9
|
+
transfer: 'dpf',
|
|
10
|
+
role: 'processor',
|
|
11
|
+
/**
|
|
12
|
+
* What it has the browser keep: no cookie (seen from outside: no Set-Cookie on pages, forms
|
|
13
|
+
* and the API of a product), but the headers Nel and Report-To, an instruction to report
|
|
14
|
+
* failed connections. Its bot protection would set __cf_bm, which would then be named here.
|
|
15
|
+
*/
|
|
16
|
+
browser: 'networkErrorLogging',
|
|
17
|
+
/**
|
|
18
|
+
* Where it answers a visitor: in its data centre nearest to them, anywhere; outside Switzerland
|
|
19
|
+
* and the EU under the standard contractual clauses of its data processing addendum.
|
|
20
|
+
*/
|
|
21
|
+
edge: 'worldwideUnderScc',
|
|
22
|
+
/** The encrypted dumps of each database, in a bucket of R2 with a lifecycle rule. */
|
|
23
|
+
backups: { where: { en: 'in the EU', de: 'in der EU' }, days: 30 }
|
|
24
|
+
},
|
|
25
|
+
/** The server of the products: the seat of the company, and where its data centres are. */
|
|
26
|
+
hoster: {
|
|
27
|
+
name: 'Hetzner Online GmbH',
|
|
28
|
+
country: { en: 'Germany', de: 'Deutschland' },
|
|
29
|
+
dataCentres: { en: 'Germany', de: 'Deutschland' },
|
|
30
|
+
role: 'processor'
|
|
31
|
+
},
|
|
32
|
+
/** Each product's database: a project of its own. */
|
|
33
|
+
neon: {
|
|
34
|
+
name: 'Neon, LLC',
|
|
35
|
+
owner: 'Databricks',
|
|
36
|
+
country: USA,
|
|
37
|
+
/** Certified under the Data Privacy Framework through Databricks. */
|
|
38
|
+
transfer: 'dpf',
|
|
39
|
+
role: 'processor',
|
|
40
|
+
cloud: 'Amazon Web Services',
|
|
41
|
+
/** The region of the cloud, which Neon names aws-eu-central-1. */
|
|
42
|
+
region: 'eu-central-1',
|
|
43
|
+
place: { en: 'Frankfurt, Germany', de: 'Frankfurt, Deutschland' },
|
|
44
|
+
/** The history to restore a database to an earlier point, at most this long. */
|
|
45
|
+
historyDays: 30
|
|
46
|
+
},
|
|
47
|
+
/** Each product's own Stripe account; Stripe is a controller of its own too. */
|
|
48
|
+
stripe: {
|
|
49
|
+
name: 'Stripe Payments Europe, Limited',
|
|
50
|
+
country: { en: 'Ireland', de: 'Irland' },
|
|
51
|
+
role: 'controller',
|
|
52
|
+
parent: { name: 'Stripe, Inc.', country: USA, transfer: 'dpf' },
|
|
53
|
+
privacy: 'stripe.com/privacy'
|
|
54
|
+
},
|
|
55
|
+
/** Sends the mails from the relay on and keeps the mailbox. */
|
|
56
|
+
mail: {
|
|
57
|
+
name: 'Infomaniak Network SA',
|
|
58
|
+
country: { en: 'Switzerland', de: 'Schweiz' },
|
|
59
|
+
role: 'processor'
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* The log of a product's container rotates by size in this many files of this size, and goes with
|
|
63
|
+
* the container when a new version starts.
|
|
64
|
+
*/
|
|
65
|
+
appLog: { files: 3, megabytes: 10, newVersion: 'removed' },
|
|
66
|
+
/**
|
|
67
|
+
* Caddy keeps no access log. What fails, a request it cannot pass on among it, its default logger
|
|
68
|
+
* writes into the log of its container, filtered: without headers (the address of the visitor
|
|
69
|
+
* stood in CF-Connecting-IP and X-Forwarded-For), without the query, the address of Cloudflare
|
|
70
|
+
* shortened. Docker rotates that log by size; it belongs to the web server, not to a product, and
|
|
71
|
+
* stays when a product starts a new version.
|
|
72
|
+
*/
|
|
73
|
+
webServer: {
|
|
74
|
+
accessLog: 'none',
|
|
75
|
+
errorLog: { content: 'requestWithoutAddress', files: 3, megabytes: 20, newVersion: 'stays' }
|
|
76
|
+
},
|
|
77
|
+
/** The mail relay logs each recipient and the state of each delivery; journald keeps it this long. */
|
|
78
|
+
relayLogDays: 30
|
|
79
|
+
};
|
|
80
|
+
/** The providers of the fleet that process data for the operator, which the terms name. */
|
|
81
|
+
export const FLEET_PROCESSORS = [
|
|
82
|
+
FLEET.cloudflare,
|
|
83
|
+
FLEET.hoster,
|
|
84
|
+
FLEET.neon,
|
|
85
|
+
FLEET.stripe,
|
|
86
|
+
FLEET.mail
|
|
87
|
+
]
|
|
88
|
+
.filter((provider) => provider.role === 'processor')
|
|
89
|
+
.map((provider) => provider.name);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { de } from './de.js';
|
|
2
|
+
import { en } from './en.js';
|
|
3
|
+
const TEXTS = { en, de };
|
|
4
|
+
export function legalPages(facts) {
|
|
5
|
+
const texts = TEXTS[facts.language];
|
|
6
|
+
return {
|
|
7
|
+
imprint: texts.imprint(facts),
|
|
8
|
+
privacy: texts.privacy(facts),
|
|
9
|
+
terms: texts.terms(facts)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A window counts for an hour at most: the privacy policy of every product says that a count per IP
|
|
3
|
+
* address applies for at most one hour, and a product counts its own routes with these counters too.
|
|
4
|
+
*/
|
|
5
|
+
export declare const LONGEST_WINDOW_MS: number;
|
|
6
|
+
/** At most `limit` tries per `windowMs`, which is at most LONGEST_WINDOW_MS. */
|
|
7
|
+
export interface Rule {
|
|
8
|
+
limit: number;
|
|
9
|
+
windowMs: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Limits {
|
|
12
|
+
/** Counts one try of `key` under `rule`: 0 when it may go on, else the seconds to wait. */
|
|
13
|
+
take(key: string, rule: Rule): number;
|
|
14
|
+
/** Forgets `key`, as after a good login. */
|
|
15
|
+
reset(key: string): void;
|
|
16
|
+
size(): number;
|
|
17
|
+
}
|
|
18
|
+
export declare const LIMITS: {
|
|
19
|
+
/** Sign-ups from one address, on the web and over the API. */
|
|
20
|
+
readonly signup: {
|
|
21
|
+
readonly limit: 10;
|
|
22
|
+
readonly windowMs: number;
|
|
23
|
+
};
|
|
24
|
+
/** Logins from one address with one email address; checks of the password of one account. */
|
|
25
|
+
readonly login: {
|
|
26
|
+
readonly limit: 10;
|
|
27
|
+
readonly windowMs: number;
|
|
28
|
+
};
|
|
29
|
+
/** Logins from one address, whatever the email address: no spraying of one password. */
|
|
30
|
+
readonly loginAddress: {
|
|
31
|
+
readonly limit: 30;
|
|
32
|
+
readonly windowMs: number;
|
|
33
|
+
};
|
|
34
|
+
/** Requests for a new password from one address. */
|
|
35
|
+
readonly forgotPassword: {
|
|
36
|
+
readonly limit: 5;
|
|
37
|
+
readonly windowMs: number;
|
|
38
|
+
};
|
|
39
|
+
/** Mails of the accounts to one recipient, whoever asks for them. */
|
|
40
|
+
readonly mailsTo: {
|
|
41
|
+
readonly limit: 5;
|
|
42
|
+
readonly windowMs: number;
|
|
43
|
+
};
|
|
44
|
+
/** Changes of the address of one account. */
|
|
45
|
+
readonly emailChange: {
|
|
46
|
+
readonly limit: 3;
|
|
47
|
+
readonly windowMs: number;
|
|
48
|
+
};
|
|
49
|
+
/** Requests with one API key. */
|
|
50
|
+
readonly api: {
|
|
51
|
+
readonly limit: 120;
|
|
52
|
+
readonly windowMs: 60000;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export declare function createLimits(options: {
|
|
56
|
+
now: () => number;
|
|
57
|
+
capacity?: number;
|
|
58
|
+
}): Limits;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Counters in memory, one fixed window per key: one process serves a product, so memory is the right
|
|
2
|
+
// place, and a restart forgets them, which only ever errs towards letting someone in.
|
|
3
|
+
/**
|
|
4
|
+
* A window counts for an hour at most: the privacy policy of every product says that a count per IP
|
|
5
|
+
* address applies for at most one hour, and a product counts its own routes with these counters too.
|
|
6
|
+
*/
|
|
7
|
+
export const LONGEST_WINDOW_MS = 60 * 60_000;
|
|
8
|
+
export const LIMITS = {
|
|
9
|
+
/** Sign-ups from one address, on the web and over the API. */
|
|
10
|
+
signup: { limit: 10, windowMs: 60 * 60_000 },
|
|
11
|
+
/** Logins from one address with one email address; checks of the password of one account. */
|
|
12
|
+
login: { limit: 10, windowMs: 15 * 60_000 },
|
|
13
|
+
/** Logins from one address, whatever the email address: no spraying of one password. */
|
|
14
|
+
loginAddress: { limit: 30, windowMs: 15 * 60_000 },
|
|
15
|
+
/** Requests for a new password from one address. */
|
|
16
|
+
forgotPassword: { limit: 5, windowMs: 60 * 60_000 },
|
|
17
|
+
/** Mails of the accounts to one recipient, whoever asks for them. */
|
|
18
|
+
mailsTo: { limit: 5, windowMs: 60 * 60_000 },
|
|
19
|
+
/** Changes of the address of one account. */
|
|
20
|
+
emailChange: { limit: 3, windowMs: 60 * 60_000 },
|
|
21
|
+
/** Requests with one API key. */
|
|
22
|
+
api: { limit: 120, windowMs: 60_000 }
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Past this many keys, ended windows go first, then the oldest that refuse nothing. A window that
|
|
26
|
+
* refuses stays, even past it: whoever floods the counters with keys of their own cannot reset the
|
|
27
|
+
* one that stops them, and every such window cost its `limit` tries and ends with its time.
|
|
28
|
+
*/
|
|
29
|
+
const CAPACITY = 50_000;
|
|
30
|
+
export function createLimits(options) {
|
|
31
|
+
const { now } = options;
|
|
32
|
+
const capacity = options.capacity ?? CAPACITY;
|
|
33
|
+
const windows = new Map();
|
|
34
|
+
function current(key, at) {
|
|
35
|
+
const window = windows.get(key);
|
|
36
|
+
if (window !== undefined && window.endsAt <= at) {
|
|
37
|
+
windows.delete(key);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return window;
|
|
41
|
+
}
|
|
42
|
+
function makeRoom(at) {
|
|
43
|
+
for (const [key, window] of windows)
|
|
44
|
+
if (window.endsAt <= at)
|
|
45
|
+
windows.delete(key);
|
|
46
|
+
for (const [key, window] of windows) {
|
|
47
|
+
if (windows.size < capacity)
|
|
48
|
+
break;
|
|
49
|
+
if (window.count < window.limit)
|
|
50
|
+
windows.delete(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
take(key, rule) {
|
|
55
|
+
if (rule.windowMs > LONGEST_WINDOW_MS) {
|
|
56
|
+
throw new Error(`a limit counts for one hour at most (the privacy policy says so): this one counts for ${rule.windowMs} ms`);
|
|
57
|
+
}
|
|
58
|
+
const at = now();
|
|
59
|
+
const window = current(key, at);
|
|
60
|
+
if (window === undefined) {
|
|
61
|
+
if (windows.size >= capacity)
|
|
62
|
+
makeRoom(at);
|
|
63
|
+
windows.set(key, { count: 1, limit: rule.limit, endsAt: at + rule.windowMs });
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
if (window.count >= rule.limit)
|
|
67
|
+
return Math.ceil((window.endsAt - at) / 1_000);
|
|
68
|
+
window.count += 1;
|
|
69
|
+
return 0;
|
|
70
|
+
},
|
|
71
|
+
reset(key) {
|
|
72
|
+
windows.delete(key);
|
|
73
|
+
},
|
|
74
|
+
size: () => windows.size
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The first three keys of every line belong to the log. */
|
|
2
|
+
export type Fields = Record<string, unknown> & {
|
|
3
|
+
time?: never;
|
|
4
|
+
level?: never;
|
|
5
|
+
msg?: never;
|
|
6
|
+
};
|
|
7
|
+
export interface Log {
|
|
8
|
+
debug: (msg: string, fields?: Fields) => void;
|
|
9
|
+
info: (msg: string, fields?: Fields) => void;
|
|
10
|
+
warn: (msg: string, fields?: Fields) => void;
|
|
11
|
+
error: (msg: string, fields?: Fields) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface DescribedError {
|
|
14
|
+
name: string;
|
|
15
|
+
message: string;
|
|
16
|
+
code?: string | number;
|
|
17
|
+
errors?: string[];
|
|
18
|
+
}
|
|
19
|
+
/** An error as the log shows it; an AggregateError (pg tries every address) lists its messages. */
|
|
20
|
+
export declare function describeError(error: unknown): DescribedError;
|
|
21
|
+
export declare function createLog(options: {
|
|
22
|
+
/** LOG_LEVEL; without it the log writes from info on. */
|
|
23
|
+
level: string | undefined;
|
|
24
|
+
write: (line: string) => void;
|
|
25
|
+
now: () => Date;
|
|
26
|
+
}): Log;
|