@bkazemi/verily 0.7.0 → 0.8.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/README.md +183 -94
- package/dist/browser/mark.d.ts +2 -6
- package/dist/browser/mark.js +6 -12
- package/dist/browser/provider-mark.d.ts +3 -3
- package/dist/browser/provider-mark.js +15 -90
- package/dist/instance/auth.d.ts +14 -0
- package/dist/instance/auth.js +80 -0
- package/dist/instance/bin.d.ts +2 -0
- package/dist/instance/bin.js +36 -0
- package/dist/instance/cloudflare.d.ts +57 -0
- package/dist/instance/cloudflare.js +71 -0
- package/dist/instance/env.d.ts +51 -0
- package/dist/instance/env.js +84 -0
- package/dist/instance/index.d.ts +136 -0
- package/dist/instance/index.js +777 -0
- package/dist/instance/init.d.ts +6 -0
- package/dist/instance/init.js +28 -0
- package/dist/instance/limits.d.ts +30 -0
- package/dist/instance/limits.js +45 -0
- package/dist/instance/node.d.ts +13 -0
- package/dist/instance/node.js +43 -0
- package/dist/instance/page.d.ts +18 -0
- package/dist/instance/page.js +30 -0
- package/dist/instance/postgres.d.ts +19 -0
- package/dist/instance/postgres.js +83 -0
- package/dist/instance/routes.d.ts +7 -0
- package/dist/instance/routes.js +22 -0
- package/dist/instance/serve.d.ts +31 -0
- package/dist/instance/serve.js +49 -0
- package/dist/instance/sites.d.ts +169 -0
- package/dist/instance/sites.js +413 -0
- package/dist/instance/starter.d.ts +6 -0
- package/dist/instance/starter.js +99 -0
- package/dist/instance/store.d.ts +20 -0
- package/dist/instance/store.js +11 -0
- package/dist/instance/undici.d.ts +8 -0
- package/dist/instance/undici.js +9 -0
- package/dist/marks.d.ts +44 -0
- package/dist/marks.js +98 -0
- package/dist/server/index.js +8 -56
- package/dist/server/render.d.ts +49 -0
- package/dist/server/render.js +104 -0
- package/dist/server/style.d.ts +1 -1
- package/dist/server/style.js +122 -1
- package/dist/verily.js +96 -104
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +30 -2
package/README.md
CHANGED
|
@@ -16,24 +16,35 @@
|
|
|
16
16
|
<img src="docs/images/badge.png" alt="A profile card with a Verily badge showing a GitHub account" width="480" />
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
Verily
|
|
19
|
+
Verily comes as a library you embed in your own backend, or as a ready-made server you run on Node or Cloudflare. A signed-in user of your site proves they control an external account, such as a GitHub account, a domain or an email address, by one of several [proof methods](#proof-methods). They approve the connection and choose who can read it. A public one is a record anyone can inspect, with a badge for their profile. An unlisted one is kept off public pages.
|
|
20
20
|
|
|
21
21
|
It doesn't sign anyone into your site, and it doesn't establish legal identity.
|
|
22
22
|
|
|
23
|
+
## Contents
|
|
24
|
+
|
|
25
|
+
- **Start here:** [How it works](#how-it-works) · [Ways to use it](#ways-to-use-it)
|
|
26
|
+
- **Embedding the backend:** [Install](#install) · [Embed the backend in your app](#embed-the-backend-in-your-app) · [Show the badge](#show-the-badge) · [Proof methods](#proof-methods) · [Operations](#operations)
|
|
27
|
+
- **Running an instance:** [Run an instance](#run-an-instance) · [A badge on a static site](#a-badge-on-a-static-site)
|
|
28
|
+
- **Using someone else's instance:** [Using a hosted instance](#using-a-hosted-instance)
|
|
29
|
+
- **Reference:** [Serving other sites](#serving-other-sites) · [Signed records](#signed-records) · [What DNS says of the verifier](#what-dns-says-of-the-verifier) · [Development](#development) · [License](#license)
|
|
30
|
+
|
|
23
31
|
## How it works
|
|
24
32
|
|
|
25
33
|
1. Your site tells Verily who is signed in. Verily never handles your logins.
|
|
26
|
-
2. The user opens
|
|
27
|
-
3. They approve the connection. Verily stores it and publishes the evidence at a public URL.
|
|
34
|
+
2. The user opens Verily's connect dialog on your site, picks a method, and proves they control the external account.
|
|
35
|
+
3. They approve the connection and choose who can read it. Verily stores it, and publishes the evidence of a public one at a public URL.
|
|
28
36
|
4. A `<verily-badge>` on their profile shows the external account. Clicking it opens the evidence.
|
|
29
37
|
5. Verily keeps the record honest: published proofs are re-read, records expire, and either side can revoke.
|
|
30
38
|
|
|
31
|
-
##
|
|
39
|
+
## Ways to use it
|
|
40
|
+
|
|
41
|
+
Verily's backend comes in two forms, and a site can also use one that someone else runs.
|
|
32
42
|
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
43
|
+
- **Embed it in your app.** `createVerily` runs inside your backend and links your app's own accounts. Your app says who is signed in and registers with each sign-in provider. See [Embed the backend in your app](#embed-the-backend-in-your-app).
|
|
44
|
+
- **Run an instance.** `createInstance` is a complete app with one owner, who signs in with a key. Other sites can send their users to it. A running one is called an instance. See [Run an instance](#run-an-instance).
|
|
45
|
+
- **Use an instance someone else runs.** It holds the provider apps and the records. Your backend makes two calls. See [Using a hosted instance](#using-a-hosted-instance).
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
All three give your backend the same records, so the code that shows them stays the same if you switch.
|
|
37
48
|
|
|
38
49
|
## Install
|
|
39
50
|
|
|
@@ -41,19 +52,24 @@ Both give your backend the same records in the same shape, so the code that show
|
|
|
41
52
|
npm install @bkazemi/verily
|
|
42
53
|
```
|
|
43
54
|
|
|
44
|
-
Requires Node.js 22.13+ or 24+. In Node, `@bkazemi/verily` exports the server. Browser bundlers get a browser-only entry with no server or database code. With TypeScript, use `moduleResolution: "NodeNext"` for the server and `"Bundler"` for the browser.
|
|
55
|
+
Requires Node.js 22.13+ or 24+. In Node, `@bkazemi/verily` exports the server. Browser bundlers get a browser-only entry with no server or database code. `@bkazemi/verily/instance` is the standalone backend, and `@bkazemi/verily/site` is the client for a site that uses someone else's instance. With TypeScript, use `moduleResolution: "NodeNext"` for the server and `"Bundler"` for the browser.
|
|
45
56
|
|
|
46
|
-
##
|
|
57
|
+
## Embed the backend in your app
|
|
47
58
|
|
|
48
59
|
### 1. Storage
|
|
49
60
|
|
|
50
61
|
```ts
|
|
51
62
|
import { Pool, PostgresStorage } from '@bkazemi/verily';
|
|
52
63
|
|
|
53
|
-
const storage = new PostgresStorage(
|
|
54
|
-
|
|
64
|
+
const storage = new PostgresStorage(
|
|
65
|
+
new Pool({ connectionString: process.env.DATABASE_URL }),
|
|
66
|
+
);
|
|
67
|
+
// creates its one table if it doesn't exist
|
|
68
|
+
await storage.migrate();
|
|
55
69
|
```
|
|
56
70
|
|
|
71
|
+
Postgres ships with the package. Anything implementing the `Storage` interface in [`src/core/index.ts`](src/core/index.ts) works too.
|
|
72
|
+
|
|
57
73
|
### 2. Create the handler
|
|
58
74
|
|
|
59
75
|
```ts
|
|
@@ -73,14 +89,18 @@ const verily = createVerily({
|
|
|
73
89
|
profileOrigins: ['https://community.example'],
|
|
74
90
|
reportUrl: 'mailto:reports@community.example',
|
|
75
91
|
authenticate: async (request) => {
|
|
76
|
-
|
|
92
|
+
// your own session lookup
|
|
93
|
+
const user = await getSignedInUser(request);
|
|
77
94
|
if (!user) return undefined;
|
|
78
95
|
|
|
79
96
|
return {
|
|
80
|
-
|
|
97
|
+
// private, stable, never reused
|
|
98
|
+
id: user.id,
|
|
81
99
|
label: user.displayName,
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
// public and durable; never an email
|
|
101
|
+
reference: user.username,
|
|
102
|
+
// optional
|
|
103
|
+
profileUrl: `https://community.example/users/${user.username}`,
|
|
84
104
|
};
|
|
85
105
|
},
|
|
86
106
|
});
|
|
@@ -96,11 +116,11 @@ const verily = createVerily({
|
|
|
96
116
|
| `reportUrl` | Where readers report a bad record (`https:` or `mailto:`). |
|
|
97
117
|
| `authenticate` | Returns the signed-in local account for a request, or `undefined` if nobody is signed in. |
|
|
98
118
|
|
|
99
|
-
Sign-in methods need an app registered with the provider
|
|
119
|
+
Sign-in methods need an app registered with the provider. The callback URL is always `<baseUrl>/callback`, and every place you run Verily, development included, needs its own registered.
|
|
100
120
|
|
|
101
121
|
- **GitHub:** [register an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) and set its callback URL. An OAuth app has only one, so development and production each need their own app.
|
|
102
122
|
- **Discord:** create an application in the [Discord developer portal](https://discord.com/developers/applications) and add the callback URL under **OAuth2 → Redirects**. One application can list several. Discord shows the client secret only when you reset it.
|
|
103
|
-
- **YouTube:** in the [Google Cloud console](https://console.cloud.google.com/), enable the YouTube Data API v3, create an OAuth client of type **Web application**, and add the callback URL as an authorized redirect URI. One client can list several. The `youtube.readonly` scope is sensitive: until Google verifies your app, users see
|
|
123
|
+
- **YouTube:** in the [Google Cloud console](https://console.cloud.google.com/), enable the YouTube Data API v3, create an OAuth client of type **Web application**, and add the callback URL as an authorized redirect URI. One client can list several. The `youtube.readonly` scope is sensitive: until Google verifies your app, users see a warning and only 100 accounts can ever use it, so apply for verification before offering it publicly.
|
|
104
124
|
|
|
105
125
|
Pass the app's client id and secret to `githubProvider()`, `discordProvider()` or `youtubeProvider()`, and keep them on the server. The other methods need no registration.
|
|
106
126
|
|
|
@@ -129,10 +149,12 @@ Put the connect pill on your settings page. Clicking it opens a dialog where the
|
|
|
129
149
|
<verily-connect backend-url="/api/verily"></verily-connect>
|
|
130
150
|
```
|
|
131
151
|
|
|
132
|
-
|
|
152
|
+
A provider's sign-in opens in a small window, and the dialog carries on when the user returns. When a connection is recorded, the element fires `verily-result`, whose `detail` holds the new `connectionId`. The backend must be on the page's origin.
|
|
133
153
|
|
|
134
154
|
The full-page flow at `<baseUrl>/verify` still works, for links and for browsers without JavaScript. `<baseUrl>/mine` returns the user's connections as JSON, for your settings page.
|
|
135
155
|
|
|
156
|
+
**Public or unlisted.** At approval the user chooses who can read the connection; unlisted is the default. A public connection has an evidence page anyone can open, which is what a badge with a `connection-id` reads. An unlisted one has none. Its holder can make one sharing link at a time, good for 7 days (`shareTtlMs`), with `client.issueShare(id)`, and end it with `client.revokeShare(id)`. The user changes visibility at `<baseUrl>/visibility/<id>`, and `service.mine(account)` returns a user's own records, unlisted included.
|
|
157
|
+
|
|
136
158
|
Then schedule the upkeep described under [Operations](#operations).
|
|
137
159
|
|
|
138
160
|
## Show the badge
|
|
@@ -140,37 +162,44 @@ Then schedule the upkeep described under [Operations](#operations).
|
|
|
140
162
|
With a bundler:
|
|
141
163
|
|
|
142
164
|
```js
|
|
143
|
-
|
|
165
|
+
// also registers <verily-badge>
|
|
166
|
+
import { init } from '@bkazemi/verily';
|
|
144
167
|
|
|
145
168
|
const client = init({ backendUrl: '/api/verily' });
|
|
146
169
|
```
|
|
147
170
|
|
|
148
|
-
Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.
|
|
171
|
+
Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.8.0/dist/verily.js`. The script defines a global `Verily`.
|
|
149
172
|
|
|
150
173
|
Then place the badge wherever the account appears:
|
|
151
174
|
|
|
152
175
|
```html
|
|
153
176
|
<script src="/assets/verily.js" defer></script>
|
|
154
|
-
<verily-badge
|
|
177
|
+
<verily-badge
|
|
178
|
+
backend-url="/api/verily"
|
|
179
|
+
connection-id="CONNECTION_ID"
|
|
180
|
+
></verily-badge>
|
|
155
181
|
```
|
|
156
182
|
|
|
157
183
|
Clicking the badge opens the verification details:
|
|
158
184
|
|
|
159
185
|
<p align="center">
|
|
160
|
-
<img src="docs/images/dialog.png" alt="The verification details dialog: the local account, the linked GitHub account, its status, and when it was approved
|
|
186
|
+
<img src="docs/images/dialog.png" alt="The verification details dialog: the local account, the linked GitHub account and when it joined, its status, how it was shown, and when it was approved and expires" width="448" />
|
|
161
187
|
</p>
|
|
162
188
|
|
|
163
189
|
A user with several accounts gets one badge for all of them. Give it every connection id, in any order:
|
|
164
190
|
|
|
165
191
|
```html
|
|
166
|
-
<verily-badge
|
|
192
|
+
<verily-badge
|
|
193
|
+
backend-url="/api/verily"
|
|
194
|
+
connection-ids="ID_ONE ID_TWO ID_THREE"
|
|
195
|
+
></verily-badge>
|
|
167
196
|
```
|
|
168
197
|
|
|
169
|
-
It shows the account
|
|
198
|
+
It shows the first account connected, then a count of the rest, such as `+2`. Clicking it opens the details with one card per account. Only accounts verified now are counted, and an account on more than one record is shown once. The ids must all belong to the same user.
|
|
170
199
|
|
|
171
|
-
A holder can change
|
|
200
|
+
A holder can change the order. An account marked preferred leads the badge, and ones marked unused come last but still count. A retired account is kept as history: never counted, grouped at the foot of the details, and shown with the date it was last verified. A badge with only retired accounts is a quiet pill; to show nothing, leave retired records out.
|
|
172
201
|
|
|
173
|
-
On a desktop,
|
|
202
|
+
On a desktop, hovering or focusing the badge opens a small panel naming up to eight of the accounts. It needs the Popover API. Add `peek="off"` to leave it out.
|
|
174
203
|
|
|
175
204
|
Add `stacked` to name the accounts in the badge itself, one to a row:
|
|
176
205
|
|
|
@@ -182,21 +211,27 @@ Add `stacked` to name the accounts in the badge itself, one to a row:
|
|
|
182
211
|
></verily-badge>
|
|
183
212
|
```
|
|
184
213
|
|
|
185
|
-
|
|
214
|
+
<img src="docs/images/stacked.png" alt="A stacked badge listing four accounts, one to a row, then +2 more" width="213" />
|
|
215
|
+
|
|
216
|
+
It lists up to four verified accounts, then counts the rest, such as `+2 more`. It is as tall as its rows, so give it a line of its own. `mountBadges` and `presentConnections` take the same choices as `{ stacked: true }` and `{ peek: false }`.
|
|
186
217
|
|
|
187
|
-
|
|
218
|
+
With several accounts, each card shows the name, status and verifier, with the method and dates behind a Details toggle. Add `details-open`, or pass `{ detailsOpen: true }`, to start every card open.
|
|
188
219
|
|
|
189
|
-
The badge refreshes every 30 seconds
|
|
220
|
+
The badge refreshes every 30 seconds and reads only public evidence. The client can also start and end connections, and draw a badge into your own element:
|
|
190
221
|
|
|
191
222
|
```js
|
|
192
|
-
|
|
193
|
-
await client.
|
|
194
|
-
|
|
223
|
+
// the connect dialog; call from a click
|
|
224
|
+
await client.openConnect();
|
|
225
|
+
// opens <baseUrl>/verify in a popup; call from a click
|
|
226
|
+
await client.connect({ provider: 'github' });
|
|
227
|
+
// draws once; call again to refresh
|
|
228
|
+
await client.mountBadge(element, { connectionId });
|
|
195
229
|
await client.disconnect(connectionId);
|
|
196
|
-
|
|
230
|
+
// or 'unused', 'current', 'retired'
|
|
231
|
+
await client.mark(connectionId, 'preferred');
|
|
197
232
|
```
|
|
198
233
|
|
|
199
|
-
`mark`
|
|
234
|
+
`mark` acts on the account, across every record of it the user has. Preferring one account takes the mark from another. Retiring freezes its records: they are never rechecked or read as verified, cannot change visibility, and are kept until removed. Only a renewal by a record's first method brings it back.
|
|
200
235
|
|
|
201
236
|
To match your site's look, set any of these CSS variables on an ancestor: `--verily-surface`, `--verily-text`, `--verily-border`, `--verily-muted`, `--verily-hover`, `--verily-font-family`, `--verily-font-size`.
|
|
202
237
|
|
|
@@ -213,6 +248,7 @@ import {
|
|
|
213
248
|
dnsProvider,
|
|
214
249
|
wellKnownProvider,
|
|
215
250
|
pgpProvider,
|
|
251
|
+
emailProvider,
|
|
216
252
|
} from '@bkazemi/verily';
|
|
217
253
|
```
|
|
218
254
|
|
|
@@ -229,9 +265,9 @@ import {
|
|
|
229
265
|
| `pgpProvider()` | signing a line Verily gives them with their OpenPGP key, then pasting it and the key. | None. |
|
|
230
266
|
| `emailProvider()` | pressing a button, or entering a code, that Verily mails to their address. | A function that sends one message. |
|
|
231
267
|
|
|
232
|
-
|
|
268
|
+
Apart from the sign-ins and the mailed code, the user publishes the proof in their own time and comes back. A proof that fails can be fixed and resubmitted up to five times. Gists, link-backs, DNS records and files can be taken down later, so Verily re-reads them on a schedule. A PGP signature is kept by Verily and published at `<baseUrl>/connections/<id>/proof`.
|
|
233
269
|
|
|
234
|
-
**How old an account is.**
|
|
270
|
+
**How old an account is.** Signing in with GitHub, Discord or YouTube also reads when the account was made. It is kept on the record as `external.createdAt`, in milliseconds, shown as "Joined" beside the account's name, and covered by a signed record. The other methods learn no date, so a record first shown by one of them has none until its holder also signs in. A provider of your own sets it by returning `createdAt` from `authenticate()`.
|
|
235
271
|
|
|
236
272
|
**Several at once.** List more than one, and `/verify` offers each method as its own button:
|
|
237
273
|
|
|
@@ -241,21 +277,21 @@ providers: [githubProvider({ clientId, clientSecret }), githubLinkProvider()],
|
|
|
241
277
|
|
|
242
278
|
Proving the same account a second way adds that proof to the existing record instead of creating another.
|
|
243
279
|
|
|
244
|
-
**Approval only where there is something to decide.** A new connection
|
|
280
|
+
**Approval only where there is something to decide.** A new connection ends with the user choosing who can read it and confirming. A renewal, or a second proof of an account already linked, is recorded as soon as the proof checks out. That holds for flows started from your own pages. A flow entered through a link to `GET <baseUrl>/sessions` still ends on an approval, because any site can send a browser down a link.
|
|
245
281
|
|
|
246
|
-
|
|
282
|
+
If you call `service.start()` yourself, pass `true` as its last argument only when the request provably came from the user's own page, as a same-origin POST does. Without it the flow always ends on an approval.
|
|
247
283
|
|
|
248
|
-
**
|
|
284
|
+
**A link-back found at sign-in.** With `githubProvider()` and `githubLinkProvider()` both listed, signing in with GitHub also reads the profile that signed in. If its website field already links to the user's `profileUrl`, the approval step says so, and confirming records the link-back beside the sign-in. A link with `http://` in place of `https://` counts; a different host, port or path does not.
|
|
249
285
|
|
|
250
|
-
**
|
|
286
|
+
**Link-backs.** GitHub and Mastodon already mark profile links `rel="me"`. By default the page may be on any public host, and the account is named by the page's address. That needs Node, because Verily checks each connection so the address cannot point inside your network. Pass `hosts` to read only certain hosts (required on Cloudflare Workers), and `profile` to name the account by handle. Your own `fetch` replaces that network check, so it must enforce the same rule. Only `<a>` and `<link>` elements in HTML, or a `Link:` header, count; [`src/server/link.ts`](src/server/link.ts) has the exact rules.
|
|
251
287
|
|
|
252
|
-
**
|
|
288
|
+
**Domains.** `dnsProvider()` proves a domain, including one that serves no page. The user adds a TXT record named `_verily` whose value is their `profileUrl`, and enters the domain. Verily looks it up through `https://dns.google/resolve`, so it runs on Cloudflare Workers, and the evidence links to that lookup. `wellKnownProvider()` reads `https://<domain>/.well-known/verily.txt` instead. It fetches a host the user names, so it follows the link-back's rules: Node, or `hosts`. Both name the exact domain entered and say nothing about its subdomains. Where the resolver validated the answer with DNSSEC, the evidence says so. A local account with no `profileUrl` is given a random value, `verily-proof=…`, to publish instead, and a renewal asks for the same one. List both providers and a domain proved one way picks up the other if it is already there.
|
|
253
289
|
|
|
254
|
-
|
|
290
|
+
**Your own method.** A provider is an object, and [`src/core/index.ts`](src/core/index.ts) documents each shape. An artifact provider may also set `field` and `input` to name what it asks for, `resolve` to turn something short such as a username into the address to read, and `known` to say where to look for an account already on record. A sign-in provider's `authorizationUrl` is passed `account` on a renewal or visibility change, so it can ask for that account by name.
|
|
255
291
|
|
|
256
292
|
**OpenPGP.** The key's fingerprint is the identity. An email address is shown only when the key signed it **and** either keys.openpgp.org has confirmed it or the address's domain publishes the key in its [web key directory](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/). The key must be valid when the proof is checked (not expired or revoked), SHA-1 signatures are refused, and a signing subkey must be properly bound to its key.
|
|
257
293
|
|
|
258
|
-
**Email.** Verily sends no mail itself. Give `emailProvider()` a `send` function and it hands
|
|
294
|
+
**Email.** Verily sends no mail itself. Give `emailProvider()` a `send` function and it hands it one message per flow, as HTML and plain text:
|
|
259
295
|
|
|
260
296
|
```ts
|
|
261
297
|
emailProvider({
|
|
@@ -266,7 +302,8 @@ emailProvider({
|
|
|
266
302
|
subject,
|
|
267
303
|
text,
|
|
268
304
|
html,
|
|
269
|
-
// The logo travels in the message. Attach each image inline under
|
|
305
|
+
// The logo travels in the message. Attach each image inline under
|
|
306
|
+
// its content id.
|
|
270
307
|
attachments: images.map((image) => ({
|
|
271
308
|
filename: image.filename,
|
|
272
309
|
content: Buffer.from(image.content, 'base64'),
|
|
@@ -277,44 +314,76 @@ emailProvider({
|
|
|
277
314
|
}),
|
|
278
315
|
```
|
|
279
316
|
|
|
280
|
-
The HTML loads nothing
|
|
317
|
+
The HTML loads nothing remote. Its one image, the Verily logo, is attached; a sender that drops `images` still sends a complete message.
|
|
281
318
|
|
|
282
|
-
The user types an address and Verily mails it a button
|
|
319
|
+
The user types an address and Verily mails it a button, with an eight-character code beneath as the other way in. In the browser that started the flow, opening the link proves the mailbox. In any other browser the page asks for one press of **Confirm**, so a mail scanner that follows links confirms nothing. The link and the code last the flow's ten minutes, and the code gets five tries. The address is the account's name on the record, so a public link shows it to everyone; the user is told before the code is sent. Like a sign-in, it proves the mailbox could be read that day, and nothing is re-read later.
|
|
283
320
|
|
|
284
|
-
A visitor chooses where the message goes, so Verily
|
|
321
|
+
A visitor chooses where the message goes, so Verily sends at most 100 messages in twenty-four hours and 5 to any one address. Set `sendLimits: { day, address }` to change them, or `Infinity` to lift one. The day's count is shared, so also rate limit `POST <baseUrl>/sessions` and `POST <baseUrl>/flows/*/submit`.
|
|
285
322
|
|
|
286
323
|
`resendSender({ apiKey, from })` is a ready-made `send` for [Resend](https://resend.com), using only `fetch`:
|
|
287
324
|
|
|
288
325
|
```ts
|
|
289
326
|
emailProvider({
|
|
290
|
-
send: resendSender({
|
|
327
|
+
send: resendSender({
|
|
328
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
329
|
+
from: 'Verily <verify@community.example>',
|
|
330
|
+
}),
|
|
291
331
|
}),
|
|
292
332
|
```
|
|
293
333
|
|
|
294
334
|
**Matching accounts.** Two methods agree on an account when the provider's account ids match. A link-back only knows an address, so it's matched by profile URL instead, and only in flows the user starts themselves. Removing a connection from the external side always needs a matching provider account id.
|
|
295
335
|
|
|
336
|
+
## Run an instance
|
|
337
|
+
|
|
338
|
+
An instance runs on Cloudflare or on Node from the same settings. `PUBLIC_ORIGIN`, `SITE_NAME`, `OWNER_LABEL`, `OWNER_REFERENCE`, `OWNER_PROFILE_URL`, `REPORT_URL` and `OWNER_KEY` are required. `OWNER_KEY` is the secret you sign in with: at least 32 random bytes, base64url.
|
|
339
|
+
|
|
340
|
+
```sh
|
|
341
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
A link back from a GitHub profile, a DNS record and an OpenPGP signature are always offered. GitHub, Discord and YouTube sign-in and email are each offered once their secrets are set. Each sign-in app needs `<PUBLIC_ORIGIN>/callback` as a redirect. [`src/instance/env.ts`](src/instance/env.ts) lists every setting.
|
|
345
|
+
|
|
346
|
+
**On Cloudflare.**
|
|
347
|
+
|
|
348
|
+
```sh
|
|
349
|
+
npx @bkazemi/verily init cloudflare my-instance
|
|
350
|
+
cd my-instance && npm install
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Fill in `wrangler.jsonc`, run `npx wrangler secret put OWNER_KEY`, then `npm run deploy`. To take a new release, update the dependency and deploy again.
|
|
354
|
+
|
|
355
|
+
**On Node.** Install the package, set the settings and `DATABASE_URL` in the environment, and run `npx verily serve`. It keeps everything in one Postgres table, which it creates, and listens on `127.0.0.1:3000` for a proxy that terminates TLS. `HOST` and `PORT` change where. Set `CLIENT_ADDRESS_HEADER` to the header your proxy sets to the client's address, such as `X-Real-IP`. Without it every visitor is counted as the proxy and shares one rate limit.
|
|
356
|
+
|
|
357
|
+
**In your own server.** `createInstance` from `@bkazemi/verily/instance` takes the same options and a store, and returns a request handler and an `upkeep()` to run hourly.
|
|
358
|
+
|
|
359
|
+
Cloudflare and `verily serve` both run the hourly upkeep themselves.
|
|
360
|
+
|
|
361
|
+
**Serving other sites.** List each site in `SITES`, as JSON, with `id`, `name`, `origin`, `authorizeUrl` and `returnUrl`, and put its key in `SITE_<ID>_KEY`. Give the site its id and key, and it uses the client under [Using a hosted instance](#using-a-hosted-instance). A site may also carry `providers` and `visibility`, to narrow what its users are offered.
|
|
362
|
+
|
|
296
363
|
## A badge on a static site
|
|
297
364
|
|
|
298
|
-
A static site only needs the script and the badge markup; the backend runs somewhere else over HTTPS. The quickest backend is
|
|
365
|
+
A static site only needs the script and the badge markup; the backend runs somewhere else over HTTPS. The quickest backend is an instance of your own on Cloudflare:
|
|
299
366
|
|
|
300
|
-
1.
|
|
301
|
-
2.
|
|
302
|
-
3.
|
|
303
|
-
4. Add the script and the badge to your site:
|
|
367
|
+
1. Set one up and deploy it, as [Run an instance](#run-an-instance) describes.
|
|
368
|
+
2. On the instance, sign in with `OWNER_KEY`, verify, and approve a **public** connection. Its id is on your settings page there, in the markup to embed.
|
|
369
|
+
3. Add the script and the badge to your site:
|
|
304
370
|
|
|
305
371
|
```html
|
|
306
|
-
<script
|
|
372
|
+
<script
|
|
373
|
+
src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.8.0/dist/verily.js"
|
|
374
|
+
defer
|
|
375
|
+
></script>
|
|
307
376
|
<verily-badge
|
|
308
|
-
backend-url="https://your-
|
|
377
|
+
backend-url="https://verily.your-site.example"
|
|
309
378
|
connection-id="CONNECTION_ID"
|
|
310
379
|
></verily-badge>
|
|
311
380
|
```
|
|
312
381
|
|
|
313
|
-
Run the
|
|
382
|
+
Run the instance on a domain readers can connect to your site, because `PUBLIC_ORIGIN` is shown as the verifier. If your site sets a Content Security Policy, allow the instance in `connect-src`.
|
|
314
383
|
|
|
315
384
|
## Using a hosted instance
|
|
316
385
|
|
|
317
|
-
|
|
386
|
+
The instance's operator registers your site and gives you its id and a key. You register with no sign-in provider, store no records and run no backend. Your backend sends a signed-in user to the instance and reads their connections back.
|
|
318
387
|
|
|
319
388
|
You are trusting that operator. They can read every record made through their instance, unlisted ones included, and for sign-in and email methods a reader has only the operator's word that the account was verified. Link-back, gist and PGP proofs can be rechecked by anyone. A site that does not want to rely on an operator runs its own instance.
|
|
320
389
|
|
|
@@ -339,13 +408,17 @@ const handle = verily.handler({
|
|
|
339
408
|
if (!user) return undefined;
|
|
340
409
|
|
|
341
410
|
return {
|
|
342
|
-
|
|
411
|
+
// private and stable; the instance never shows it
|
|
412
|
+
id: user.id,
|
|
343
413
|
label: user.displayName,
|
|
344
|
-
|
|
414
|
+
// durable and safe to show; never an email
|
|
415
|
+
reference: user.handle,
|
|
345
416
|
};
|
|
346
417
|
},
|
|
347
|
-
|
|
348
|
-
|
|
418
|
+
// where a signed-out user is sent
|
|
419
|
+
signInUrl: '/login',
|
|
420
|
+
// where a user lands when they come back
|
|
421
|
+
returnUrl: '/settings',
|
|
349
422
|
});
|
|
350
423
|
|
|
351
424
|
// GET and POST /api/verily/*
|
|
@@ -376,7 +449,7 @@ Once a user has a link, show their badge in place of the pill, and give it the s
|
|
|
376
449
|
></verily-badge>
|
|
377
450
|
```
|
|
378
451
|
|
|
379
|
-
The badge's dialog then has Renew and Remove under each account, the controls that mark
|
|
452
|
+
The badge's dialog then has Renew and Remove under each account, the controls that mark or retire it, and Add account. Renew extends the record under the same id, so embeds keep working. It fires `verily-result` on each change. `verily.beginUrl('manage')` opens a user's links on the instance, for a site that would rather link there.
|
|
380
453
|
|
|
381
454
|
**3. Read connections where you show them.**
|
|
382
455
|
|
|
@@ -385,7 +458,7 @@ const { [user.id]: connections } = await verily.connections([user.id]);
|
|
|
385
458
|
const verified = connections.filter((c) => c.status === 'verified');
|
|
386
459
|
```
|
|
387
460
|
|
|
388
|
-
Show only `verified` records as verified. A `retired` record
|
|
461
|
+
Show only `verified` records as verified. A `retired` record carries `retiredAt` and can be shown as history, never as a current account. `mark` is `preferred` or `unused` where the user set one; it is their ordering, not something checked.
|
|
389
462
|
|
|
390
463
|
To show them, hand the records to the badge. It draws them as given and fetches nothing, so it works for unlisted links, which a browser cannot read for itself:
|
|
391
464
|
|
|
@@ -395,23 +468,28 @@ To show them, hand the records to the badge. It draws them as given and fetches
|
|
|
395
468
|
|
|
396
469
|
From script, set the property instead: `badge.connections = records`. Send a page only the records its reader may see: whatever is in the page, the reader has. Several records become one badge, as under [Show the badge](#show-the-badge), and an unlisted one opens its details without linking anywhere.
|
|
397
470
|
|
|
398
|
-
|
|
471
|
+
Unlisted records are included, and only your site can read them. `connections()` takes any number of ids, so read a whole page of users in one call. Every call reads the instance, so a removed or expired link stops showing on the next page load. `cacheMs` reuses an answer for that long, during which a revoked connection still shows as verified, so leave it unset or keep it to a few seconds. `{ fresh: true }` makes one call read anyway.
|
|
399
472
|
|
|
400
473
|
If you would rather your user ids never left your site, give the client a secret:
|
|
401
474
|
|
|
402
475
|
```ts
|
|
403
|
-
const verily = createSiteClient({
|
|
476
|
+
const verily = createSiteClient({
|
|
477
|
+
instance,
|
|
478
|
+
site,
|
|
479
|
+
key,
|
|
480
|
+
idSecret: process.env.VERILY_ID_SECRET,
|
|
481
|
+
});
|
|
404
482
|
```
|
|
405
483
|
|
|
406
|
-
|
|
484
|
+
The instance is then sent a stand-in for each id, and for a subject with no `reference`. The same secret and user always give the same stand-in, so nothing is stored. The secret must never change, so keep one for this alone.
|
|
407
485
|
|
|
408
486
|
A site that would rather write its own endpoints can: `verily.authorize(state, subject)` signs the redirect handoff, `verily.handoff(subject)` the pill's, and `verily.result(token)` checks what a user returns with.
|
|
409
487
|
|
|
410
|
-
**Moving
|
|
488
|
+
**Moving off someone else's instance.** To [run an instance](#run-an-instance) of your own, register your site with it and change the client's `instance`, `site` and `key`; the code above stays. To embed the backend, replace `verily.connections([id])` with `service.mine(account)`, which returns the same records, and the three steps above with the handler from [Embed the backend in your app](#embed-the-backend-in-your-app). Either way records are not moved, so users verify again.
|
|
411
489
|
|
|
412
490
|
## Serving other sites
|
|
413
491
|
|
|
414
|
-
One backend can serve sites that don't run Verily themselves, so none of them registers anything with a sign-in provider. A subject from such a site names it in `LocalAccount.siteName`, and every page, proof line and record describing that subject uses the name in place of the `siteName` option. The backend stays the verifier.
|
|
492
|
+
One backend can serve sites that don't run Verily themselves, so none of them registers anything with a sign-in provider. An instance does this already, from its `SITES` setting. This section is for an embedded backend that does it itself. A subject from such a site names it in `LocalAccount.siteName`, and every page, proof line and record describing that subject uses the name in place of the `siteName` option. The backend stays the verifier.
|
|
415
493
|
|
|
416
494
|
Three options tie a flow to the site that sent the holder:
|
|
417
495
|
|
|
@@ -423,17 +501,17 @@ Three options tie a flow to the site that sent the holder:
|
|
|
423
501
|
|
|
424
502
|
Two more narrow what a subject is offered, for a backend whose sites don't all want the same. `providersFor(local)` returns the provider ids a subject may use: it is offered those alone, in that order, and a flow for any other is refused. `visibilityFor(local)` returns the visibilities it may choose: with one, the approval page states it and offers no choice.
|
|
425
503
|
|
|
426
|
-
`result` is recorded when the flow ends and never changes: its kind, how it ended, the connection and its visibility at that moment, and `finishedAt`.
|
|
504
|
+
`result` is recorded when the flow ends and never changes: its kind, how it ended, the connection and its visibility at that moment, and `finishedAt`. An instance is built on these hooks, and [`src/instance/sites.ts`](src/instance/sites.ts) is a worked example of them.
|
|
427
505
|
|
|
428
506
|
## Signed records
|
|
429
507
|
|
|
430
|
-
Evidence is normally believed because the verifier's own origin serves it. Set a signing key and each public record can also be downloaded as a file signed with OpenPGP, which anyone can check later with `gpg`, without the
|
|
508
|
+
Evidence is normally believed because the verifier's own origin serves it. Set a signing key and each public record can also be downloaded as a file signed with OpenPGP, which anyone can check later with `gpg`, without the verifier being up.
|
|
431
509
|
|
|
432
510
|
```ts
|
|
433
511
|
import { createVerily, generateSigningKey } from '@bkazemi/verily';
|
|
434
512
|
|
|
435
|
-
// Make the key once and keep it with your other secrets. It is an armored
|
|
436
|
-
// private key, so it spans several lines.
|
|
513
|
+
// Make the key once and keep it with your other secrets. It is an armored
|
|
514
|
+
// OpenPGP private key, so it spans several lines.
|
|
437
515
|
console.log(await generateSigningKey('verifier.example'));
|
|
438
516
|
|
|
439
517
|
createVerily({
|
|
@@ -445,9 +523,9 @@ createVerily({
|
|
|
445
523
|
With a key set:
|
|
446
524
|
|
|
447
525
|
- **`GET <baseUrl>/connections/<id>?format=signed`** returns the signed file for a public record that is currently verified: the record as JSON under an OpenPGP cleartext signature. It is signed when asked for, so it says the record stood at that moment. An unlisted, revoked or expired record has no signed form. A public retired record has one, signed as `version: 2` with `status: "retired"` and `retiredAt`, so a reader that knows only version 1 refuses it and never takes it for a record that stood.
|
|
448
|
-
- **`GET <baseUrl>/keys.asc`** is the public key as `gpg --import` reads it. **`GET <baseUrl>/keys`** lists the same keys as JSON, each with its fingerprint.
|
|
526
|
+
- **`GET <baseUrl>/keys.asc`** is the public key as `gpg --import` reads it. **`GET <baseUrl>/keys`** lists the same keys as JSON, each with its fingerprint. An instance also serves that list at `/.well-known/verily-keys.json`.
|
|
449
527
|
- **`<baseUrl>/check`** is a page where a saved file can be pasted and read back.
|
|
450
|
-
- The evidence page
|
|
528
|
+
- The evidence page links to the signed record. The badge's dialog checks the signature in the browser and shows a `signed` pill after the verifier. A record whose signature fails is shown as unconfirmed, with `bad signature` in the pill's place.
|
|
451
529
|
- The approval step tells the holder that a saved signed record outlives removal.
|
|
452
530
|
|
|
453
531
|
To check a file yourself:
|
|
@@ -462,15 +540,18 @@ Or in code, which runs in a browser too:
|
|
|
462
540
|
```ts
|
|
463
541
|
import { verifySigned } from '@bkazemi/verily';
|
|
464
542
|
|
|
465
|
-
const { keys } = await (
|
|
466
|
-
|
|
543
|
+
const { keys } = await (
|
|
544
|
+
await fetch('https://verifier.example/api/verily/keys')
|
|
545
|
+
).json();
|
|
546
|
+
// undefined if no key signed it
|
|
547
|
+
const record = await verifySigned(file, keys);
|
|
467
548
|
```
|
|
468
549
|
|
|
469
550
|
A signed record proves what the verifier claimed and when (`issuedAt`). It does not prove the claim was true, and it does not say whether the record still stands: ask `evidenceUrl` for that. Save the keys with the file if it has to outlive the verifier's domain. When you change keys, list the old key from `/keys` in `retiredKeys` so what it signed still checks.
|
|
470
551
|
|
|
471
552
|
`generateSigningKey()` makes an Ed25519 key in the OpenPGP form GnuPG reads, with no subkeys. Any OpenPGP key that signs with its primary key works as `signingKey`; one that is revoked, expired or passphrase-protected is refused. Checking is done by OpenPGP.js under the format's own rules: a key is judged as it stood when the record was signed, so an expired key, or one revoked as retired or superseded, still answers for records it signed before then, and a key revoked as compromised signs nothing at all.
|
|
472
553
|
|
|
473
|
-
|
|
554
|
+
An instance signs by default, with a key it makes and stores itself. Set `SIGNING_KEY` to supply your own, or `SIGNING` to `off` to sign nothing.
|
|
474
555
|
|
|
475
556
|
## What DNS says of the verifier
|
|
476
557
|
|
|
@@ -486,43 +567,48 @@ Two TXT records let a reader check the verifier against DNS instead of taking it
|
|
|
486
567
|
```ts
|
|
487
568
|
import { dnsKeys, signedBy, verifySigned } from '@bkazemi/verily';
|
|
488
569
|
|
|
489
|
-
|
|
570
|
+
// keys saved with the file, or from /keys
|
|
571
|
+
const record = await verifySigned(file, keys);
|
|
490
572
|
const named = await dnsKeys(new URL(record.evidenceUrl).hostname);
|
|
491
573
|
|
|
492
|
-
|
|
493
|
-
named.
|
|
574
|
+
// the verifier's domain names the key that signed it
|
|
575
|
+
named.ids.includes(await signedBy(file));
|
|
576
|
+
// whether the resolver validated that answer
|
|
577
|
+
named.dnssec;
|
|
494
578
|
```
|
|
495
579
|
|
|
496
580
|
**The site's verifier.** A record's local side is the verifier's statement of what the site told it. On the site's own pages that is enough, since the site put the badge there. On a shared evidence link or a saved signed record, nothing shows the site chose this verifier. A site whose verifier is on another domain publishes `verily-verifier=<host>` on the domain of its `profileUrl`, with the port if `baseUrl` has one. `dnsVerifiers(domain)` reads it back. A verifier on the site's own domain, or a subdomain of it, needs no record and none is looked for.
|
|
497
581
|
|
|
498
|
-
Set `dns: true` and the
|
|
582
|
+
Set `dns: true` and the backend reads both through `https://dns.google/resolve`, the resolver the domain method asks:
|
|
499
583
|
|
|
500
584
|
```ts
|
|
501
585
|
createVerily({
|
|
502
586
|
// ...
|
|
503
|
-
|
|
587
|
+
// or { fetch, timeoutMs }
|
|
588
|
+
dns: true,
|
|
504
589
|
});
|
|
505
590
|
```
|
|
506
591
|
|
|
507
592
|
- The evidence page and `<baseUrl>/check` say the signing key is named in the verifier's DNS, with a link to the lookup, once it is. The answer is kept for ten minutes.
|
|
508
593
|
- When a link is approved, the site's domain is looked up. If it names this verifier, the record's local side carries the lookup as `artifactUrl` and the record as `expect`, the evidence reads "Stated by … whose DNS names this verifier" and links to the lookup, and the signed record includes it. `recheck()` reads each site again every `recheckMs`, one lookup for all of a site's records. A record loses the proof when the site stops naming the verifier, or when the lookup has gone unread for `freshnessMs`; its status is unaffected either way.
|
|
509
594
|
|
|
510
|
-
|
|
595
|
+
An instance sets `dns: true`.
|
|
511
596
|
|
|
512
597
|
## Operations
|
|
513
598
|
|
|
514
|
-
DNS setup is saved for seven days from its first attempt. If propagation takes longer than a flow’s ten minutes, sign in and choose DNS again to restore the same TXT value and the last domain entered (or restart the same renewal). Each return creates a fresh flow and reads the proof again; the saved setup grants no verification on its own. Only the most recent domain entered is remembered for that operation. Keep the TXT record after verification for ongoing checks. A proof covers the exact domain entered; verify parent domains and subdomains separately.
|
|
515
|
-
|
|
516
599
|
Verification lasts 30 days, flows 10 minutes, and sharing links 7 days. Change them with `validityMs`, `flowTtlMs` and `shareTtlMs`. Evidence is never cached.
|
|
517
600
|
|
|
518
|
-
|
|
601
|
+
A user's DNS setup is saved for seven days. If propagation outlasts a flow's ten minutes, they choose DNS again and get the same TXT value and the last domain entered. Each return reads the proof afresh. The TXT record has to stay after verification, for the ongoing checks.
|
|
602
|
+
|
|
603
|
+
Run the upkeep on a schedule, for example hourly. An instance does this itself.
|
|
519
604
|
|
|
520
605
|
```ts
|
|
521
606
|
setInterval(
|
|
522
607
|
async () => {
|
|
523
608
|
try {
|
|
524
609
|
await verily.service.prune();
|
|
525
|
-
|
|
610
|
+
// only needed if you use a method other than signing in
|
|
611
|
+
await verily.service.recheck();
|
|
526
612
|
} catch (error) {
|
|
527
613
|
console.error('Verily upkeep failed', error);
|
|
528
614
|
}
|
|
@@ -532,7 +618,7 @@ setInterval(
|
|
|
532
618
|
```
|
|
533
619
|
|
|
534
620
|
- **`prune()`** deletes expired flows and DNS drafts, and deletes expired or revoked records after 90 days. A retired record is kept until it is removed.
|
|
535
|
-
- **`recheck()`** re-reads gists, link-backs, DNS records and files that are due, up to 5 per provider per run (its `budget` argument)
|
|
621
|
+
- **`recheck()`** re-reads gists, link-backs, DNS records and files that are due, up to 5 per provider per run (its `budget` argument), each every 24 hours (`recheckMs`). A read that finds the proof gone makes the connection unconfirmed at once. A read that fails, such as a timeout, changes nothing, and the proof stays current for 7 days after its last successful read (`freshnessMs`). Either way it is read again after a quarter of `recheckMs`, and the connection is verified again as soon as a read finds the proof. A provider of your own says a proof is gone by throwing `Absent` from `verify`. For PGP, it asks the keyserver whether the key has been revoked, and revokes the connection if so.
|
|
536
622
|
|
|
537
623
|
- **`onChange`** is an option, not a call. Set it to hear when a record's status, visibility, expiry or mark changes, so your site can update its own copy without polling:
|
|
538
624
|
|
|
@@ -545,9 +631,9 @@ setInterval(
|
|
|
545
631
|
});
|
|
546
632
|
```
|
|
547
633
|
|
|
548
|
-
`evidence` is the record
|
|
634
|
+
`evidence` is the record now, `local` is your subject with its private id, `previous` is what was last delivered, and `revision` counts that record's deliveries. Ignore a change whose `revision` is not higher than the last you applied, since a slow call can finish after a newer one. A change made by a request is delivered before the request returns, so keep the call quick. Changes that come with time alone, such as expiry, are delivered by `recheck()`, or by `service.announce()` if you do not recheck. Delivery is at least once: a call that throws is retried on the next run, and turning the option on reports every existing record once, 50 per run. A record removed by `prune()` is not reported.
|
|
549
635
|
|
|
550
|
-
**If you never call `recheck()`, set `freshnessMs: Infinity`.** Otherwise connections proved by gist, link-back or PGP lapse after a week.
|
|
636
|
+
**If you never call `recheck()`, set `freshnessMs: Infinity`.** Otherwise connections proved by gist, link-back, DNS record, file or PGP lapse after a week.
|
|
551
637
|
|
|
552
638
|
## Development
|
|
553
639
|
|
|
@@ -558,7 +644,8 @@ The full example also needs Postgres and a GitHub OAuth app, a Discord applicati
|
|
|
558
644
|
```sh
|
|
559
645
|
npm run build
|
|
560
646
|
docker compose up -d --wait
|
|
561
|
-
|
|
647
|
+
# add credentials for at least one sign-in, and a long EXAMPLE_PASSWORD
|
|
648
|
+
cp example/.env.example .env
|
|
562
649
|
node --env-file=.env --import tsx example/server.ts
|
|
563
650
|
```
|
|
564
651
|
|
|
@@ -567,14 +654,16 @@ Checks and tests:
|
|
|
567
654
|
```sh
|
|
568
655
|
npm run build
|
|
569
656
|
npm run check
|
|
570
|
-
|
|
657
|
+
# npm run format fixes formatting
|
|
658
|
+
npm run format:check
|
|
571
659
|
npm test
|
|
572
660
|
TEST_DATABASE_URL=postgres://verily:verily@localhost:5432/verily npm test
|
|
573
661
|
npm run test:consumer
|
|
662
|
+
npm run starter:check
|
|
574
663
|
```
|
|
575
664
|
|
|
576
|
-
The Postgres and example tests only run when `TEST_DATABASE_URL` is set. The other tests use a fake provider and in-memory storage, so a live GitHub sign-in still has to be tried by hand. `test:consumer` installs the packed package into a separate project and checks its Node and browser imports. CI runs all of it, Postgres included, on Node 22.13, 24 and 26.
|
|
665
|
+
The Postgres and example tests only run when `TEST_DATABASE_URL` is set. The other tests use a fake provider and in-memory storage, so a live GitHub sign-in still has to be tried by hand. `test:consumer` installs the packed package into a separate project and checks its Node and browser imports. `starter:check` writes the project `verily init cloudflare` gives a new instance and bundles it as a deploy would, without deploying. CI runs all of it, Postgres included, on Node 22.13, 24 and 26.
|
|
577
666
|
|
|
578
667
|
## License
|
|
579
668
|
|
|
580
|
-
MIT. Signed records and `pgpProvider()` use [OpenPGP.js](https://github.com/openpgpjs/openpgpjs), which is LGPL-3.0-or-later. The badge script `verily.js` includes it, unmodified, to check signatures; its
|
|
669
|
+
MIT. Signed records and `pgpProvider()` use [OpenPGP.js](https://github.com/openpgpjs/openpgpjs), which is LGPL-3.0-or-later. The badge script `verily.js` includes it, unmodified, to check signatures; its license is in [`docs/licenses/openpgp.txt`](docs/licenses/openpgp.txt).
|