@bkazemi/verily 0.6.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 +197 -91
- package/dist/browser/connect-dialog.js +3 -4
- package/dist/browser/evidence-dialog.d.ts +7 -2
- package/dist/browser/evidence-dialog.js +62 -26
- package/dist/browser/index.js +3 -0
- 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/core/index.d.ts +78 -2
- package/dist/core/index.js +26 -2
- 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/discord.js +3 -0
- package/dist/server/domain.js +37 -10
- package/dist/server/github-gist.js +16 -4
- package/dist/server/github.js +3 -0
- package/dist/server/index.js +20 -47
- package/dist/server/link.js +9 -4
- package/dist/server/render.d.ts +49 -0
- package/dist/server/render.js +104 -0
- package/dist/server/service.d.ts +44 -1
- package/dist/server/service.js +225 -19
- package/dist/server/style.d.ts +1 -1
- package/dist/server/style.js +132 -2
- package/dist/server/youtube.js +5 -0
- package/dist/verily.js +155 -124
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +30 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Store } from './store.js';
|
|
2
|
+
export declare class OwnerAuth {
|
|
3
|
+
private readonly storage;
|
|
4
|
+
private readonly ownerKey;
|
|
5
|
+
constructor(storage: Store, ownerKey: string);
|
|
6
|
+
private token;
|
|
7
|
+
authenticated(request: Request): Promise<boolean>;
|
|
8
|
+
/** Which owner session a request belongs to, by the hash its record is stored under. */
|
|
9
|
+
session(request: Request): Promise<string | undefined>;
|
|
10
|
+
login(key: string): Promise<string | undefined>;
|
|
11
|
+
logout(request: Request): Promise<string>;
|
|
12
|
+
allow(bucket: 'login', limit: number, windowMs: number): Promise<boolean>;
|
|
13
|
+
prune(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { expired } from '../core/index.js';
|
|
3
|
+
import { hash, secret } from '../server/service.js';
|
|
4
|
+
const sessionMs = 8 * 3600000;
|
|
5
|
+
// Use a normal host-only cookie name. Secure, HttpOnly, SameSite and Path=/
|
|
6
|
+
// provide the relevant protections while avoiding __Host-prefix rejection by
|
|
7
|
+
// browsers or privacy extensions on workers.dev.
|
|
8
|
+
const cookieName = 'verily_owner';
|
|
9
|
+
export class OwnerAuth {
|
|
10
|
+
storage;
|
|
11
|
+
ownerKey;
|
|
12
|
+
constructor(storage, ownerKey) {
|
|
13
|
+
this.storage = storage;
|
|
14
|
+
this.ownerKey = ownerKey;
|
|
15
|
+
if (!/^[A-Za-z0-9_-]{43,}$/.test(ownerKey))
|
|
16
|
+
throw new Error('OWNER_KEY must be a randomly generated base64url secret of at least 32 bytes');
|
|
17
|
+
}
|
|
18
|
+
token(request) {
|
|
19
|
+
return request.headers
|
|
20
|
+
.get('cookie')
|
|
21
|
+
?.split(';')
|
|
22
|
+
.map((part) => part.trim())
|
|
23
|
+
.find((part) => part.startsWith(`${cookieName}=`))
|
|
24
|
+
?.slice(cookieName.length + 1);
|
|
25
|
+
}
|
|
26
|
+
async authenticated(request) {
|
|
27
|
+
return (await this.session(request)) !== undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Which owner session a request belongs to, by the hash its record is stored under. */
|
|
30
|
+
async session(request) {
|
|
31
|
+
const token = this.token(request);
|
|
32
|
+
if (!token)
|
|
33
|
+
return undefined;
|
|
34
|
+
const session = await this.storage.get(`owner/session/${hash(token)}`);
|
|
35
|
+
return !!session && session.expiresAt > Date.now() && session.keyHash === hash(this.ownerKey)
|
|
36
|
+
? hash(token)
|
|
37
|
+
: undefined;
|
|
38
|
+
}
|
|
39
|
+
async login(key) {
|
|
40
|
+
if (!timingSafeEqual(Buffer.from(hash(key)), Buffer.from(hash(this.ownerKey))))
|
|
41
|
+
return;
|
|
42
|
+
const token = secret();
|
|
43
|
+
await this.storage.delete('owner/rate/v2/login');
|
|
44
|
+
await this.storage.put(`owner/session/${hash(token)}`, {
|
|
45
|
+
expiresAt: Date.now() + sessionMs,
|
|
46
|
+
keyHash: hash(this.ownerKey),
|
|
47
|
+
});
|
|
48
|
+
return `${cookieName}=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${sessionMs / 1000}`;
|
|
49
|
+
}
|
|
50
|
+
async logout(request) {
|
|
51
|
+
const token = this.token(request);
|
|
52
|
+
if (token)
|
|
53
|
+
await this.storage.delete(`owner/session/${hash(token)}`);
|
|
54
|
+
return `${cookieName}=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0`;
|
|
55
|
+
}
|
|
56
|
+
async allow(bucket, limit, windowMs) {
|
|
57
|
+
return this.storage.transaction(async (tx) => {
|
|
58
|
+
const key = `owner/rate/v2/${bucket}`;
|
|
59
|
+
const previous = await tx.get(key);
|
|
60
|
+
const next = previous && previous.until > Date.now()
|
|
61
|
+
? previous
|
|
62
|
+
: { count: 0, until: Date.now() + windowMs };
|
|
63
|
+
if (next.count >= limit)
|
|
64
|
+
return false;
|
|
65
|
+
next.count++;
|
|
66
|
+
await tx.put(key, next);
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async prune() {
|
|
71
|
+
// One counter for every POST, from before requests were limited per session and client.
|
|
72
|
+
await this.storage.delete('owner/rate/v2/mutation');
|
|
73
|
+
await this.storage.transaction(async (tx) => {
|
|
74
|
+
for (const [key, session] of await tx.list('owner/session/')) {
|
|
75
|
+
if (expired(session, Date.now()) || session.keyHash !== hash(this.ownerKey))
|
|
76
|
+
await tx.delete(key);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { serve } from './serve.js';
|
|
3
|
+
import { init } from './init.js';
|
|
4
|
+
const usage = `Usage:
|
|
5
|
+
verily serve Run an instance on Node, from the environment.
|
|
6
|
+
verily init cloudflare [dir] Write a Cloudflare project into dir.`;
|
|
7
|
+
/** Writes the starter, then says what to do with it. */
|
|
8
|
+
async function start(directory) {
|
|
9
|
+
await init(directory);
|
|
10
|
+
console.log(`Wrote a Cloudflare project to ${directory}
|
|
11
|
+
|
|
12
|
+
Next, in that directory:
|
|
13
|
+
1. npm install
|
|
14
|
+
2. Fill in account_id and the vars in wrangler.jsonc.
|
|
15
|
+
3. npx wrangler secret put OWNER_KEY
|
|
16
|
+
4. npm run deploy`);
|
|
17
|
+
}
|
|
18
|
+
const [command, target, directory] = process.argv.slice(2);
|
|
19
|
+
try {
|
|
20
|
+
if (command === 'serve') {
|
|
21
|
+
const { server } = await serve(process.env);
|
|
22
|
+
const { address, port } = server.address();
|
|
23
|
+
console.log(`Verily instance for ${process.env.PUBLIC_ORIGIN} listening on ${address}:${port}`);
|
|
24
|
+
}
|
|
25
|
+
else if (command === 'init' && target === 'cloudflare') {
|
|
26
|
+
await start(directory ?? '.');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.error(usage);
|
|
30
|
+
process.exit(command === undefined || command === '--help' || command === '-h' ? 0 : 2);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error(error instanceof Error ? error.message : error);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Storage } from '../core/index.js';
|
|
2
|
+
import { type Instance } from './index.js';
|
|
3
|
+
import { type InstanceEnv } from './env.js';
|
|
4
|
+
import { type Store } from './store.js';
|
|
5
|
+
/**
|
|
6
|
+
* The parts of Cloudflare's own types this file uses, written out here so the package
|
|
7
|
+
* needs none of Cloudflare's to be installed. A Durable Object's real storage, state and
|
|
8
|
+
* namespace each fit the one below.
|
|
9
|
+
*/
|
|
10
|
+
interface ObjectKeyValue {
|
|
11
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
12
|
+
put<T>(key: string, value: T): Promise<void>;
|
|
13
|
+
delete(key: string): Promise<boolean>;
|
|
14
|
+
list<T>(options: {
|
|
15
|
+
prefix: string;
|
|
16
|
+
}): Promise<Map<string, T>>;
|
|
17
|
+
}
|
|
18
|
+
export interface ObjectStorage extends ObjectKeyValue {
|
|
19
|
+
transaction<T>(work: (tx: ObjectKeyValue) => Promise<T>): Promise<T>;
|
|
20
|
+
getAlarm(): Promise<number | null>;
|
|
21
|
+
setAlarm(time: number): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface ObjectState {
|
|
24
|
+
storage: ObjectStorage;
|
|
25
|
+
blockConcurrencyWhile<T>(work: () => Promise<T>): Promise<T>;
|
|
26
|
+
}
|
|
27
|
+
interface ObjectNamespace {
|
|
28
|
+
idFromName(name: string): unknown;
|
|
29
|
+
get(id: never): {
|
|
30
|
+
fetch(url: string, init: RequestInit): Promise<Response>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The instance's settings, as vars and secrets, and the binding of the object that runs it. */
|
|
34
|
+
export interface Env extends InstanceEnv {
|
|
35
|
+
VERILY: ObjectNamespace;
|
|
36
|
+
}
|
|
37
|
+
/** A Durable Object's storage as an instance store. It is SQLite-backed, not Workers KV. */
|
|
38
|
+
export declare function cloudflareStore(storage: ObjectStorage): Store;
|
|
39
|
+
/** The library's record storage on a Durable Object, for a Worker that embeds the backend. */
|
|
40
|
+
export declare class CloudflareStorage implements Storage {
|
|
41
|
+
private readonly records;
|
|
42
|
+
constructor(storage: ObjectStorage);
|
|
43
|
+
transaction: Storage['transaction'];
|
|
44
|
+
}
|
|
45
|
+
/** The Worker: turns away what needs no storage, and passes the rest to the one object. */
|
|
46
|
+
declare const _default: {
|
|
47
|
+
fetch(request: Request, env: Env): Promise<Response>;
|
|
48
|
+
};
|
|
49
|
+
export default _default;
|
|
50
|
+
/** The instance on a Durable Object: its storage, an hourly alarm, and settings from `env`. */
|
|
51
|
+
export declare class VerilyStore {
|
|
52
|
+
private readonly ctx;
|
|
53
|
+
protected readonly instance: Instance;
|
|
54
|
+
constructor(ctx: ObjectState, env: Env);
|
|
55
|
+
alarm(): Promise<void>;
|
|
56
|
+
fetch(request: Request): Promise<Response>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { admit, createInstance } from './index.js';
|
|
2
|
+
import { fromEnv } from './env.js';
|
|
3
|
+
import { html } from './page.js';
|
|
4
|
+
import { recordStorage } from './store.js';
|
|
5
|
+
/** Values are copied on the way in and out, so a caller never shares one with storage. */
|
|
6
|
+
const keyValue = (storage) => ({
|
|
7
|
+
get: async (key) => structuredClone(await storage.get(key)),
|
|
8
|
+
put: (key, value) => storage.put(key, structuredClone(value)),
|
|
9
|
+
delete: async (key) => {
|
|
10
|
+
await storage.delete(key);
|
|
11
|
+
},
|
|
12
|
+
list: async (prefix) => [...(await storage.list({ prefix })).entries()].map(([key, value]) => [key, structuredClone(value)]),
|
|
13
|
+
});
|
|
14
|
+
/** A Durable Object's storage as an instance store. It is SQLite-backed, not Workers KV. */
|
|
15
|
+
export function cloudflareStore(storage) {
|
|
16
|
+
return {
|
|
17
|
+
...keyValue(storage),
|
|
18
|
+
transaction: (work) => storage.transaction((tx) => work(keyValue(tx))),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** The library's record storage on a Durable Object, for a Worker that embeds the backend. */
|
|
22
|
+
export class CloudflareStorage {
|
|
23
|
+
records;
|
|
24
|
+
constructor(storage) {
|
|
25
|
+
this.records = recordStorage(cloudflareStore(storage));
|
|
26
|
+
}
|
|
27
|
+
transaction = (work) => this.records.transaction(work);
|
|
28
|
+
}
|
|
29
|
+
const hour = 3600000;
|
|
30
|
+
/** The Worker: turns away what needs no storage, and passes the rest to the one object. */
|
|
31
|
+
export default {
|
|
32
|
+
async fetch(request, env) {
|
|
33
|
+
// Turned away here, a request never wakes the object.
|
|
34
|
+
const admitted = await admit(request, env.PUBLIC_ORIGIN);
|
|
35
|
+
if (admitted instanceof Response)
|
|
36
|
+
return admitted;
|
|
37
|
+
try {
|
|
38
|
+
// One installation, with an identity that survives deployments and hostname changes.
|
|
39
|
+
return await env.VERILY.get(env.VERILY.idFromName('site-owner')).fetch(admitted.url, {
|
|
40
|
+
method: admitted.method,
|
|
41
|
+
headers: Object.fromEntries(admitted.headers),
|
|
42
|
+
body: admitted.method === 'POST' ? await admitted.arrayBuffer() : undefined,
|
|
43
|
+
redirect: 'manual',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return html('Temporarily unavailable', '', 503);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
/** The instance on a Durable Object: its storage, an hourly alarm, and settings from `env`. */
|
|
52
|
+
export class VerilyStore {
|
|
53
|
+
ctx;
|
|
54
|
+
instance;
|
|
55
|
+
constructor(ctx, env) {
|
|
56
|
+
this.ctx = ctx;
|
|
57
|
+
this.instance = createInstance({ ...fromEnv(env), store: cloudflareStore(ctx.storage) });
|
|
58
|
+
ctx.blockConcurrencyWhile(async () => {
|
|
59
|
+
if ((await ctx.storage.getAlarm()) === null)
|
|
60
|
+
await ctx.storage.setAlarm(Date.now() + hour);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async alarm() {
|
|
64
|
+
// Schedule first so transient cleanup failures never permanently stop maintenance.
|
|
65
|
+
await this.ctx.storage.setAlarm(Date.now() + hour);
|
|
66
|
+
await this.instance.upkeep();
|
|
67
|
+
}
|
|
68
|
+
fetch(request) {
|
|
69
|
+
return this.instance.handle(request, request.headers.get('cf-connecting-ip'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LocalKind } from '../core/index.js';
|
|
2
|
+
import type { InstanceOptions } from './index.js';
|
|
3
|
+
/** The settings an instance reads, under the same names on every host. */
|
|
4
|
+
export interface InstanceEnv {
|
|
5
|
+
/** The origin the instance is served from. HTTPS, with no trailing slash. */
|
|
6
|
+
PUBLIC_ORIGIN: string;
|
|
7
|
+
SITE_NAME: string;
|
|
8
|
+
/** What the owner links: one of their accounts, a page, or the site. Defaults to account. */
|
|
9
|
+
OWNER_KIND?: LocalKind;
|
|
10
|
+
OWNER_LABEL: string;
|
|
11
|
+
OWNER_REFERENCE: string;
|
|
12
|
+
OWNER_PROFILE_URL: string;
|
|
13
|
+
/** The secret the owner signs in with: at least 32 random bytes, base64url. */
|
|
14
|
+
OWNER_KEY: string;
|
|
15
|
+
REPORT_URL: string;
|
|
16
|
+
/** GitHub sign-in is offered only when both are set. */
|
|
17
|
+
GITHUB_CLIENT_ID?: string;
|
|
18
|
+
GITHUB_CLIENT_SECRET?: string;
|
|
19
|
+
/** Discord sign-in is offered only when both are set. */
|
|
20
|
+
DISCORD_CLIENT_ID?: string;
|
|
21
|
+
DISCORD_CLIENT_SECRET?: string;
|
|
22
|
+
/** YouTube sign-in is offered only when both are set. */
|
|
23
|
+
GOOGLE_CLIENT_ID?: string;
|
|
24
|
+
GOOGLE_CLIENT_SECRET?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Email is offered only when both are set: a Resend API key, and the sender its codes
|
|
27
|
+
* come from, as `Name <address>` or an address on a domain verified with Resend.
|
|
28
|
+
*/
|
|
29
|
+
RESEND_API_KEY?: string;
|
|
30
|
+
EMAIL_FROM?: string;
|
|
31
|
+
/** How many codes may be mailed in a day, where the mail allowance is not the default. */
|
|
32
|
+
EMAIL_DAILY_LIMIT?: string | number;
|
|
33
|
+
/**
|
|
34
|
+
* Public records are signed unless this is `off`. The key is `SIGNING_KEY` where one is
|
|
35
|
+
* set, armored. Otherwise one is made on first use and kept in the store.
|
|
36
|
+
*/
|
|
37
|
+
SIGNING?: string;
|
|
38
|
+
SIGNING_KEY?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The sites that send their users here instead of hosting Verily, as a JSON list of
|
|
41
|
+
* `{ id, name, origin, authorizeUrl, returnUrl }`. Each needs its key in
|
|
42
|
+
* `SITE_<ID>_KEY`, such as `SITE_PARTNER_KEY`.
|
|
43
|
+
*/
|
|
44
|
+
SITES?: string | unknown[];
|
|
45
|
+
[key: `SITE_${string}_KEY`]: string | undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* An instance's options from its settings. Every host builds them here, so two hosts given
|
|
49
|
+
* the same settings offer the same methods and behave the same. The host adds the store.
|
|
50
|
+
*/
|
|
51
|
+
export declare function fromEnv(env: InstanceEnv): Omit<InstanceOptions, 'store'>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { discordProvider, dnsProvider, emailProvider, githubLinkProvider, githubProvider, pgpProvider, resendSender, youtubeProvider, } from '../server/index.js';
|
|
2
|
+
import { registry } from './sites.js';
|
|
3
|
+
/** The configured count of mailed messages a day, if it is a count at all. */
|
|
4
|
+
function dailyMail(configured) {
|
|
5
|
+
const count = Number(configured);
|
|
6
|
+
return Number.isSafeInteger(count) && count > 0 ? count : undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* An instance's options from its settings. Every host builds them here, so two hosts given
|
|
10
|
+
* the same settings offer the same methods and behave the same. The host adds the store.
|
|
11
|
+
*/
|
|
12
|
+
export function fromEnv(env) {
|
|
13
|
+
for (const name of [
|
|
14
|
+
'PUBLIC_ORIGIN',
|
|
15
|
+
'SITE_NAME',
|
|
16
|
+
'OWNER_LABEL',
|
|
17
|
+
'OWNER_REFERENCE',
|
|
18
|
+
'OWNER_PROFILE_URL',
|
|
19
|
+
'OWNER_KEY',
|
|
20
|
+
'REPORT_URL',
|
|
21
|
+
])
|
|
22
|
+
if (!env[name])
|
|
23
|
+
throw new Error(`${name} must be set`);
|
|
24
|
+
return {
|
|
25
|
+
origin: env.PUBLIC_ORIGIN,
|
|
26
|
+
siteName: env.SITE_NAME,
|
|
27
|
+
owner: {
|
|
28
|
+
// A site with no user accounts must not have its subject described as one.
|
|
29
|
+
kind: env.OWNER_KIND || undefined,
|
|
30
|
+
label: env.OWNER_LABEL,
|
|
31
|
+
reference: env.OWNER_REFERENCE,
|
|
32
|
+
profileUrl: env.OWNER_PROFILE_URL,
|
|
33
|
+
},
|
|
34
|
+
ownerKey: env.OWNER_KEY,
|
|
35
|
+
reportUrl: env.REPORT_URL,
|
|
36
|
+
// The same methods on every host, so only ones every host can run safely. The file
|
|
37
|
+
// method and a web key directory read a host the holder names, and a Cloudflare Worker
|
|
38
|
+
// cannot check where that resolves, so neither is offered.
|
|
39
|
+
providers: [
|
|
40
|
+
// Two ways of showing one GitHub account. Whichever is used first is the record's
|
|
41
|
+
// main method, and the other is listed beneath it once used.
|
|
42
|
+
...(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET
|
|
43
|
+
? [
|
|
44
|
+
githubProvider({
|
|
45
|
+
clientId: env.GITHUB_CLIENT_ID,
|
|
46
|
+
clientSecret: env.GITHUB_CLIENT_SECRET,
|
|
47
|
+
}),
|
|
48
|
+
]
|
|
49
|
+
: []),
|
|
50
|
+
githubLinkProvider(),
|
|
51
|
+
// A domain shown by a record in its DNS, which is one fetch to a fixed resolver.
|
|
52
|
+
dnsProvider(),
|
|
53
|
+
// A key needs no registration and no secret. An address on a key is shown only when
|
|
54
|
+
// the keyserver confirms it.
|
|
55
|
+
pgpProvider(),
|
|
56
|
+
...(env.DISCORD_CLIENT_ID && env.DISCORD_CLIENT_SECRET
|
|
57
|
+
? [
|
|
58
|
+
discordProvider({
|
|
59
|
+
clientId: env.DISCORD_CLIENT_ID,
|
|
60
|
+
clientSecret: env.DISCORD_CLIENT_SECRET,
|
|
61
|
+
}),
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
64
|
+
...(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
|
|
65
|
+
? [
|
|
66
|
+
youtubeProvider({
|
|
67
|
+
clientId: env.GOOGLE_CLIENT_ID,
|
|
68
|
+
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
69
|
+
}),
|
|
70
|
+
]
|
|
71
|
+
: []),
|
|
72
|
+
...(env.RESEND_API_KEY && env.EMAIL_FROM
|
|
73
|
+
? [
|
|
74
|
+
emailProvider({
|
|
75
|
+
send: resendSender({ apiKey: env.RESEND_API_KEY, from: env.EMAIL_FROM }),
|
|
76
|
+
}),
|
|
77
|
+
]
|
|
78
|
+
: []),
|
|
79
|
+
],
|
|
80
|
+
sites: registry(env.SITES, env),
|
|
81
|
+
signingKey: env.SIGNING === 'off' ? false : env.SIGNING_KEY || undefined,
|
|
82
|
+
mailPerDay: dailyMail(env.EMAIL_DAILY_LIMIT),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { type LocalAccount, type Provider } from '../core/index.js';
|
|
2
|
+
import { type Store } from './store.js';
|
|
3
|
+
import { type Site } from './sites.js';
|
|
4
|
+
export { fromEnv, type InstanceEnv } from './env.js';
|
|
5
|
+
export { registry, type Site } from './sites.js';
|
|
6
|
+
export { recordStorage, type KeyValue, type Store } from './store.js';
|
|
7
|
+
export interface InstanceOptions {
|
|
8
|
+
/** Everything the instance keeps: the records, and its own sessions, handoffs and counts. */
|
|
9
|
+
store: Store;
|
|
10
|
+
/** The origin the instance is served from. HTTPS, with no trailing slash. */
|
|
11
|
+
origin: string;
|
|
12
|
+
/** The owner's own site. A registered site's subjects carry their site's name instead. */
|
|
13
|
+
siteName: string;
|
|
14
|
+
/** What the owner links here: one of their accounts, a page, or the site. */
|
|
15
|
+
owner: Omit<LocalAccount, 'id' | 'siteName'> & {
|
|
16
|
+
profileUrl: string;
|
|
17
|
+
};
|
|
18
|
+
/** The secret the owner signs in with. */
|
|
19
|
+
ownerKey: string;
|
|
20
|
+
reportUrl: string;
|
|
21
|
+
/** The methods offered. A registered site may narrow these for its own holders. */
|
|
22
|
+
providers: Provider[];
|
|
23
|
+
/** The sites that send their users here instead of hosting Verily, as `registry()` reads them. */
|
|
24
|
+
sites?: Map<string, Site>;
|
|
25
|
+
/**
|
|
26
|
+
* The key public records are signed by, armored. Left out, a key is made on first use and
|
|
27
|
+
* kept in the store. `false` turns signing off.
|
|
28
|
+
*/
|
|
29
|
+
signingKey?: string | false;
|
|
30
|
+
/** How many codes may be mailed in a day, where the mail allowance is not the default. */
|
|
31
|
+
mailPerDay?: number;
|
|
32
|
+
/** How often each published proof is re-read. Six hours by default. */
|
|
33
|
+
recheckMs?: number;
|
|
34
|
+
/** How many proofs one `upkeep()` re-reads per provider. 10 by default. */
|
|
35
|
+
recheckBudget?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The checks that need nothing stored: the origin, the method, and a bound on the body.
|
|
39
|
+
* Gives back the response to send, or the request with its body read. `handle` runs this
|
|
40
|
+
* itself. A host that passes requests on to another process can run it first, so that
|
|
41
|
+
* process is not woken for a request this turns away.
|
|
42
|
+
*/
|
|
43
|
+
export declare function admit(request: Request, origin: string): Promise<Request | Response>;
|
|
44
|
+
/** A multi-site Verily server: a request handler, and the upkeep its host runs on a timer. */
|
|
45
|
+
export type Instance = ReturnType<typeof createInstance>;
|
|
46
|
+
export declare function createInstance(options: InstanceOptions): Server;
|
|
47
|
+
declare class Server {
|
|
48
|
+
private readonly options;
|
|
49
|
+
private readonly app;
|
|
50
|
+
private readonly store;
|
|
51
|
+
private readonly origin;
|
|
52
|
+
private readonly auth;
|
|
53
|
+
private readonly sites;
|
|
54
|
+
private readonly local;
|
|
55
|
+
constructor(options: InstanceOptions);
|
|
56
|
+
/** The library's service, for a host that needs more than requests and upkeep. */
|
|
57
|
+
get service(): import("../server/service.js").VerilyService;
|
|
58
|
+
/**
|
|
59
|
+
* What a host runs on a timer, about hourly: drops what has expired, and re-reads the
|
|
60
|
+
* published proofs that are due. The default of 10 per provider each hour stays inside
|
|
61
|
+
* the rate limit a provider gives an unauthenticated caller.
|
|
62
|
+
*/
|
|
63
|
+
upkeep(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Answers one request. `address` is the client's address as the host knows it, which
|
|
66
|
+
* rate limits count by. The instance reads no forwarding header itself, since which one
|
|
67
|
+
* can be trusted depends on what the host sits behind.
|
|
68
|
+
*/
|
|
69
|
+
handle(request: Request, address?: string | null): Promise<Response>;
|
|
70
|
+
private route;
|
|
71
|
+
/**
|
|
72
|
+
* One request from the connect dialog on a site's own page. The page holds the session
|
|
73
|
+
* and the flow's binding and sends them as headers, because this instance's cookies are
|
|
74
|
+
* another site's on that page and a browser may not send them. Here they are put where
|
|
75
|
+
* the library looks for them, and a binding the library sets comes back as a header. The
|
|
76
|
+
* session works only from its own site's origin, and only for the dialog's requests.
|
|
77
|
+
*/
|
|
78
|
+
private dialog;
|
|
79
|
+
/**
|
|
80
|
+
* Gives the sign-in window the binding of a flow a site's dialog started, as the cookie
|
|
81
|
+
* the provider's callback checks. `origin` is where the page that sent the binding is,
|
|
82
|
+
* as the window's own script saw it. It must be the site the flow's subject belongs to.
|
|
83
|
+
*/
|
|
84
|
+
private enter;
|
|
85
|
+
/** The site a subject belongs to, by its id's prefix. The owner's id has none. */
|
|
86
|
+
private siteOf;
|
|
87
|
+
/**
|
|
88
|
+
* A site's backend reading its own subjects' connections, unlisted ones included, which
|
|
89
|
+
* nobody else can read. This is what lets a site show a link to its own users without the
|
|
90
|
+
* link being public. A refused request counts against the caller's address, and a good
|
|
91
|
+
* one against the site's own allowance, apart from the one its holders' handoffs share.
|
|
92
|
+
*/
|
|
93
|
+
private read;
|
|
94
|
+
/** This installation's own signing key, made the first time a record is signed. */
|
|
95
|
+
private signingKey;
|
|
96
|
+
private site;
|
|
97
|
+
/**
|
|
98
|
+
* Counts a request that creates state: every POST, and the GETs that start a handoff, a
|
|
99
|
+
* session or a flow. A signed-in holder has a bucket of their own and anyone else is
|
|
100
|
+
* counted by address, so one visitor cannot lock everyone else out. A site's requests
|
|
101
|
+
* also share a ceiling, so one site cannot starve another.
|
|
102
|
+
*/
|
|
103
|
+
private allowed;
|
|
104
|
+
/**
|
|
105
|
+
* Removes a link from a site session and reports it back to the site. There is no flow to
|
|
106
|
+
* hold the result, so the session holds it, and success is reported only once the
|
|
107
|
+
* revocation has committed: pending first, then revoked, then complete. A retry resumes
|
|
108
|
+
* wherever the last attempt stopped, and one already complete reads what it stored.
|
|
109
|
+
*/
|
|
110
|
+
private disconnect;
|
|
111
|
+
/**
|
|
112
|
+
* Lists an account another way, or retires it, from a site session, and reports it back
|
|
113
|
+
* to the site as a disconnect is reported: pending on the session first, then changed,
|
|
114
|
+
* then complete. Asking again for what already holds changes nothing, so a retry is safe.
|
|
115
|
+
*/
|
|
116
|
+
private mark;
|
|
117
|
+
/**
|
|
118
|
+
* The settings page for whoever is signed in: the owner, or a holder a site sent here.
|
|
119
|
+
* The owner's page carries the embed for their own site; a site's holder has the site to
|
|
120
|
+
* show their badge, and removes a link through a route that tells the site so.
|
|
121
|
+
*/
|
|
122
|
+
private settings;
|
|
123
|
+
/**
|
|
124
|
+
* Every record the instance holds, for its operator: each site's subjects, public and
|
|
125
|
+
* unlisted alike, with what each is linked to and how it stands. Read-only. A public
|
|
126
|
+
* record links to its evidence; an unlisted one has no page that is the operator's to open.
|
|
127
|
+
*/
|
|
128
|
+
private records;
|
|
129
|
+
/**
|
|
130
|
+
* Asks before an account is retired or a connection is removed, since neither can be
|
|
131
|
+
* undone. The page's one button posts to the route that does it.
|
|
132
|
+
*/
|
|
133
|
+
private confirm;
|
|
134
|
+
/** One connection: the account's card, its state, and what can be done with it. */
|
|
135
|
+
private connection;
|
|
136
|
+
}
|