Skip to content

Commit

Permalink
tos -> terms of use
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai committed Jan 3, 2024
1 parent a3076ad commit ef774fd
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 127 deletions.
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,18 +277,18 @@ cloudflared tunnel --url http://localhost:3000

Configuration can be set using environment variables

| Env Var | Description | Default |
| -------------------------- | ----------------------------------------------------------------------------------------- | ---------------------- |
| `DS_PORT` | Port that the server listens on | `3000` |
| `DS_MAX_RECORD_DATA_SIZE` | maximum size for `RecordsWrite` data. use `b`, `kb`, `mb`, `gb` for value | `1gb` |
| `DS_WEBSOCKET_SERVER` | whether to enable listening over `ws:`. values: `on`,`off` | `on` |
| `DWN_REGISTRATION_POW` | require new users to complete a proof-of-work challenge | `false` |
| `DWN_REGISTRATION_TOS` | require users to agree to a terms of service. Value is path to the terms of service file. | unset |
| `DWN_STORAGE` | URL to use for storage by default. See [Storage Options](#storage-options) for details | `level://data` |
| `DWN_STORAGE_MESSAGES` | URL to use for storage of messages. | value of `DWN_STORAGE` |
| `DWN_STORAGE_DATA` | URL to use for data storage | value of `DWN_STORAGE` |
| `DWN_STORAGE_EVENTS` | URL to use for event storage | value of `DWN_STORAGE` |
| `DWN_STORAGE_REGISTRATION` | URL to use for storage of registered DIDs | `sqlite://data/dwn.db` |
| Env Var | Description | Default |
| -------------------------------- | --------------------------------------------------------------------------------------- | ---------------------- |
| `DS_PORT` | Port that the server listens on | `3000` |
| `DS_MAX_RECORD_DATA_SIZE` | Maximum size for `RecordsWrite` data. use `b`, `kb`, `mb`, `gb` for value | `1gb` |
| `DS_WEBSOCKET_SERVER` | Whether to enable listening over `ws:`. values: `on`,`off` | `on` |
| `DWN_REGISTRATION_POW` | Require new users to complete a proof-of-work challenge | `false` |
| `DWN_TERMS_OF_SERVICE_FILE_PATH` | Required terms of service agreement if set. Value is path to the terms of service file. | unset |
| `DWN_STORAGE` | URL to use for storage by default. See [Storage Options](#storage-options) for details | `level://data` |
| `DWN_STORAGE_MESSAGES` | URL to use for storage of messages. | value of `DWN_STORAGE` |
| `DWN_STORAGE_DATA` | URL to use for data storage | value of `DWN_STORAGE` |
| `DWN_STORAGE_EVENTS` | URL to use for event storage | value of `DWN_STORAGE` |
| `DWN_STORAGE_REGISTRATION` | URL to use for storage of registered DIDs | `sqlite://data/dwn.db` |

### Storage Options

Expand All @@ -308,7 +308,7 @@ requirements are enabled before they are allowed to use the server. Tenants that
requirements are available at the `/info.json` endpoint.

- **Proof of Work** (`DWN_REGISTRATION_POW=true`) - new tenants must GET `/register/pow` for a challenge, then generate a nonce that produces a string that has a sha256 hex sum starting with the specified (`complexity`) number of zeros (`0`) when added to the end of the challenge (`sha256(challenge + nonce)`). This nonce should be POSTed to `/register/pow` with a JSON body including the `challenge`, the nonce in field `response` and `did`. Challenges expire after 5 minutes, and complexity will increase based on the number of successful proof-of-work registrations that have been completed within the last hour. This registration requirement is listed in `/info.json` as `proof-of-work-sha256-v0`.
- **Terms of Service** (`DWN_REGISTRATION_TOS=/path/to/tos.txt`) - new tenants must GET `/register/tos` to fetch the terms. These terms must be displayed to the human end-user, who must actively accept them. When the user accepts the terms, send the sha256 hash of the accepted terms and the user's did via POST `/register/tos`. The JSON body should have fields `tosHash` and `did`. To change the terms, update the file and restart the server. Users that accepted the old terms will be blocked until they accept the new terms. This registration requirement is listed in `/info.json` as `terms-of-service`.
- **Terms of Service** (`DWN_TERMS_OF_SERVICE_FILE_PATH=/path/to/terms-of-service.txt`) - new tenants must GET `/register/terms-of-service` to fetch the terms. These terms must be displayed to the human end-user, who must actively accept them. When the user accepts the terms, send the sha256 hash of the accepted terms and the user's did via POST `/register/terms-of-service`. The JSON body should have fields `termsOfServiceHash` and `did`. To change the terms, update the file and restart the server. Users that accepted the old terms will be blocked until they accept the new terms. This registration requirement is listed in `/info.json` as `terms-of-service`.

## Server info

Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const config = {
process.env.DWN_STORAGE ||
'sqlite://data/dwn.db',

registrationRequirementTos: process.env.DWN_REGISTRATION_TOS,
termsOfServiceFilePath: process.env.DWN_TERMS_OF_SERVICE_FILE_PATH,

// log level - trace/debug/info/warn/error
logLevel: process.env.DWN_SERVER_LOG_LEVEL || 'INFO',
Expand Down
20 changes: 11 additions & 9 deletions src/dwn-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { HttpServerShutdownHandler } from './lib/http-server-shutdown-handler.js
import { type Config, config as defaultConfig } from './config.js';
import { HttpApi } from './http-api.js';
import { setProcessHandlers } from './process-handlers.js';
import { RegisteredTenantGate } from './registered-tenant-gate.js';
import { getDWNConfig, getDialectFromURI } from './storage.js';
import { TenantGate } from './tenant-gate.js';
import { WsApi } from './ws-api.js';

export type DwnServerOptions = {
Expand Down Expand Up @@ -48,20 +48,22 @@ export class DwnServer {
* The DWN creation is secondary and only happens if it hasn't already been done.
*/
async #setupServer(): Promise<void> {
let tenantGate: TenantGate;
let tenantGate: RegisteredTenantGate;
if (!this.dwn) {
const tenantGateDB = getDialectFromURI(
new URL(this.config.tenantRegistrationStore),
);
const tos =
this.config.registrationRequirementTos !== undefined
? readFileSync(this.config.registrationRequirementTos).toString()
: null;
tenantGate = new TenantGate(

// Load terms of service if given the path.
const termsOfService =
this.config.termsOfServiceFilePath !== undefined
? readFileSync(this.config.termsOfServiceFilePath).toString()
: undefined;

tenantGate = new RegisteredTenantGate(
tenantGateDB,
this.config.registrationRequirementPow,
this.config.registrationRequirementTos !== undefined,
tos,
termsOfService,
);

this.dwn = await Dwn.create(getDWNConfig(this.config, tenantGate));
Expand Down
14 changes: 7 additions & 7 deletions src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ import {
import { config } from './config.js';
import { jsonRpcApi } from './json-rpc-api.js';
import { requestCounter, responseHistogram } from './metrics.js';
import type { TenantGate } from './tenant-gate.js';
import type { RegisteredTenantGate } from './registered-tenant-gate.js';

const packagejson = process.env.npm_package_json
const packageJson = process.env.npm_package_json
? JSON.parse(readFileSync(process.env.npm_package_json).toString())
: {};

export class HttpApi {
#api: Express;
#server: http.Server;
tenantGate: TenantGate;
tenantGate: RegisteredTenantGate;
dwn: Dwn;

constructor(dwn: Dwn, tenantGate: TenantGate) {
constructor(dwn: Dwn, tenantGate: RegisteredTenantGate) {
this.#api = express();
this.#server = http.createServer(this.#api);
this.dwn = dwn;
Expand Down Expand Up @@ -202,16 +202,16 @@ export class HttpApi {
if (config.registrationRequirementPow) {
registrationRequirements.push('proof-of-work-sha256-v0');
}
if (config.registrationRequirementTos) {
if (config.termsOfServiceFilePath !== undefined) {
registrationRequirements.push('terms-of-service');
}

res.json({
server: process.env.npm_package_name,
maxFileSize: config.maxRecordDataSize,
registrationRequirements: registrationRequirements,
version: packagejson.version,
sdkVersion: packagejson.dependencies['@tbd54566975/dwn-sdk-js'],
version: packageJson.version,
sdkVersion: packageJson.dependencies['@tbd54566975/dwn-sdk-js'],
});
});
}
Expand Down
83 changes: 42 additions & 41 deletions src/tenant-gate.ts → src/registered-tenant-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,26 @@ const CHALLENGE_TIMEOUT = 5 * 60 * 1000; // challenges are valid this long after
const COMPLEXITY_LOOKBACK = 5 * 60 * 1000; // complexity is based on number of successful registrations in this timeframe
const COMPLEXITY_MINIMUM = 5;

export class TenantGate {
export class RegisteredTenantGate {
#db: Kysely<TenantRegistrationDatabase>;
#powRequired: boolean;
#tosRequired: boolean;
#tos?: string;
#tosHash?: string;
#logRejections: boolean;
#proofOfWorkRequired: boolean;
#termsOfService?: string;
#termsOfServiceHash?: string;

constructor(
dialect: Dialect,
powRequired: boolean,
tosRequired: boolean,
currentTOS?: string,
logRejections?: boolean,
proofOfWorkRequired: boolean,
termsOfService?: string,
) {
this.#db = new Kysely<TenantRegistrationDatabase>({ dialect: dialect });
this.#powRequired = powRequired;
this.#tosRequired = tosRequired;
if (tosRequired) {
this.#tos = currentTOS;
const tosHash = createHash('sha256');
tosHash.update(currentTOS);
this.#tosHash = tosHash.digest('hex');
this.#proofOfWorkRequired = proofOfWorkRequired;

if (termsOfService) {
const termsOfServiceHash = createHash('sha256');
termsOfServiceHash.update(termsOfService);
this.#termsOfServiceHash = termsOfServiceHash.digest('hex');
this.#termsOfService = termsOfService;
}
this.#logRejections = logRejections || false;
}

async initialize(): Promise<void> {
Expand All @@ -53,38 +48,38 @@ export class TenantGate {
.ifNotExists()
.addColumn('did', 'text', (column) => column.primaryKey())
.addColumn('powTime', 'timestamp')
.addColumn('tos', 'boolean')
.addColumn('termsOfServiceHash', 'boolean')
.execute();
}

setupRoutes(server: Express): void {
if (this.#powRequired) {
if (this.#proofOfWorkRequired) {
server.get('/register/pow', (req: Request, res: Response) =>
this.getProofOfWorkChallenge(req, res),
);
server.post('/register/pow', (req: Request, res: Response) =>
this.verifyProofOfWorkChallenge(req, res),
);
}
if (this.#tosRequired) {
server.get('/register/tos', (req: Request, res: Response) =>
res.send(this.#tos),
if (this.#termsOfService) {
server.get('/register/terms-of-service', (req: Request, res: Response) =>
res.send(this.#termsOfService),
);
server.post('/register/tos', (req: Request, res: Response) =>
this.acceptTOS(req, res),
server.post('/register/terms-of-service', (req: Request, res: Response) =>
this.acceptTermsOfService(req, res),
);
}
}

async isTenant(tenant: string): Promise<boolean> {
if (!this.#powRequired && !this.#tosRequired) {
if (!this.#proofOfWorkRequired && !this.#termsOfService) {
return true;
}

const result = await this.#db
.selectFrom('authorizedTenants')
.select('powTime')
.select('tos')
.select('termsOfServiceHash')
.where('did', '=', tenant)
.execute();

Expand All @@ -95,17 +90,20 @@ export class TenantGate {

const row = result[0];

if (this.#powRequired && row.powTime == undefined) {
if (this.#proofOfWorkRequired && row.powTime == undefined) {
console.log('rejecting tenant that has not completed the proof of work', {
tenant,
});
return false;
}

if (this.#tosRequired && row.tos != this.#tosHash) {
if (
this.#termsOfService &&
row.termsOfServiceHash != this.#termsOfServiceHash
) {
console.log(
'rejecting tenant that has not accepted the current terms of service',
{ row, tenant, expected: this.#tosHash },
{ row, tenant, expected: this.#termsOfServiceHash },
);
return false;
}
Expand Down Expand Up @@ -205,46 +203,49 @@ export class TenantGate {
return complexity;
}

private async acceptTOS(req: Request, res: Response): Promise<void> {
private async acceptTermsOfService(
req: Request,
res: Response,
): Promise<void> {
const body: {
did: string;
tosHash: string;
termsOfServiceHash: string;
} = req.body;

if (body.tosHash != this.#tosHash) {
if (body.termsOfServiceHash != this.#termsOfServiceHash) {
res.status(400).json({
success: false,
reason: 'incorrect TOS hash',
reason: 'incorrect terms of service hash',
});
}

console.log('accepting tos', body);
console.log('accepting terms of service', body);

await this.#db
.insertInto('authorizedTenants')
.values({
did: body.did,
tos: body.tosHash,
termsOfServiceHash: body.termsOfServiceHash,
})
.onConflict((oc) =>
oc.column('did').doUpdateSet((eb) => ({
tos: eb.ref('excluded.tos'),
termsOfServiceHash: eb.ref('excluded.termsOfServiceHash'),
})),
)
.executeTakeFirstOrThrow();
res.status(200).json({ success: true });
}

async authorizeTenantTOS(tenant: string): Promise<void> {
async authorizeTenantTermsOfService(tenant: string): Promise<void> {
await this.#db
.insertInto('authorizedTenants')
.values({
did: tenant,
tos: this.#tosHash,
termsOfServiceHash: this.#termsOfServiceHash,
})
.onConflict((oc) =>
oc.column('did').doUpdateSet((eb) => ({
tos: eb.ref('excluded.tos'),
termsOfServiceHash: eb.ref('excluded.termsOfServiceHash'),
})),
)
.executeTakeFirst();
Expand All @@ -253,7 +254,7 @@ export class TenantGate {

interface AuthorizedTenants {
did: string;
tos: string;
termsOfServiceHash: string;
powTime: number;
}

Expand Down
4 changes: 2 additions & 2 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import pg from 'pg';
import Cursor from 'pg-cursor';

import type { Config } from './config.js';
import type { TenantGate } from './tenant-gate.js';
import type { RegisteredTenantGate } from './registered-tenant-gate.js';

export enum EStoreType {
DataStore,
Expand All @@ -44,7 +44,7 @@ export type StoreType = DataStore | EventLog | MessageStore;

export function getDWNConfig(
config: Config,
tenantGate: TenantGate,
tenantGate: RegisteredTenantGate,
): DwnConfig {
const dataStore: DataStore = getStore(config.dataStore, EStoreType.DataStore);
const eventLog: EventLog = getStore(config.eventLog, EStoreType.EventLog);
Expand Down
File renamed without changes.
Loading

0 comments on commit ef774fd

Please sign in to comment.