@clioplaylists/clio 0.1.7 → 0.1.8

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.
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import AppContext from '../context';
3
+ export declare const createRouter: (ctx: AppContext) => Router;
@@ -0,0 +1,31 @@
1
+ import { Router } from 'express';
2
+ export const createRouter = (ctx) => {
3
+ const router = Router();
4
+ const did = ctx.cfg.serverDid;
5
+ if (did.startsWith('did:web:')) {
6
+ const hostname = did.slice('did:web:'.length);
7
+ const serviceEndpoint = `https://${hostname}`;
8
+ router.get('/.well-known/did.json', (_req, res) => {
9
+ res.json({
10
+ '@context': ['https://www.w3.org/ns/did/v1'],
11
+ id: did,
12
+ verificationMethod: [
13
+ {
14
+ id: `${did}#atproto`,
15
+ type: 'Multikey',
16
+ controller: did,
17
+ publicKeyMultibase: ctx.signingKey.did().replace('did:key:', ''),
18
+ },
19
+ ],
20
+ service: [
21
+ {
22
+ id: '#clio_appview',
23
+ type: 'ClioAppView',
24
+ serviceEndpoint,
25
+ }
26
+ ],
27
+ });
28
+ });
29
+ }
30
+ return router;
31
+ };
package/dist/config.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface ServerConfigValues {
5
5
  serverDid: string;
6
6
  didPlcUrl: string;
7
7
  handleResolverNameservers?: string[];
8
+ dataplaneUrls: string[];
8
9
  }
9
10
  export declare class ServerConfig {
10
11
  private cfg;
@@ -18,4 +19,5 @@ export declare class ServerConfig {
18
19
  get localUrl(): string;
19
20
  get serverDid(): string;
20
21
  get didPlcUrl(): string;
22
+ get dataplaneUrls(): string[];
21
23
  }
package/dist/config.js CHANGED
@@ -10,6 +10,7 @@ export class ServerConfig {
10
10
  const debugMode = overrides?.debugMode || process.env.NODE_ENV !== 'production';
11
11
  const serverDid = overrides?.serverDid || process.env.CLIO_SERVER_DID || 'did:example:test';
12
12
  const didPlcUrl = overrides?.didPlcUrl || process.env.DID_PLC_URL || 'http://localhost:2582';
13
+ const dataplaneUrls = overrides?.dataplaneUrls ?? envList(process.env.CLIO_DATAPLANE_URLS);
13
14
  // const handleResolverNameservers = process.env.BSKY_HANDLE_RESOLVER_NAMESERVERS
14
15
  // ? process.env.BSKY_HANDLE_RESOLVER_NAMESERVERS.split(',')
15
16
  // : []
@@ -21,6 +22,7 @@ export class ServerConfig {
21
22
  port,
22
23
  serverDid,
23
24
  didPlcUrl,
25
+ dataplaneUrls,
24
26
  });
25
27
  }
26
28
  assignPort(port) {
@@ -46,4 +48,12 @@ export class ServerConfig {
46
48
  get didPlcUrl() {
47
49
  return this.cfg.didPlcUrl;
48
50
  }
51
+ get dataplaneUrls() {
52
+ return this.cfg.dataplaneUrls;
53
+ }
54
+ }
55
+ function envList(str) {
56
+ if (str === undefined || str.length === 0)
57
+ return [];
58
+ return str.split(',');
49
59
  }
package/dist/context.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Keypair } from '@atproto/crypto';
1
2
  import { DataPlaneClient } from './client';
2
3
  import { ServerConfig } from './config';
3
4
  export default class AppContext {
@@ -5,7 +6,9 @@ export default class AppContext {
5
6
  constructor(opts: {
6
7
  cfg: ServerConfig;
7
8
  dataplane: DataPlaneClient;
9
+ signingKey: Keypair;
8
10
  });
9
11
  get cfg(): ServerConfig;
10
12
  get dataplane(): DataPlaneClient;
13
+ get signingKey(): Keypair;
11
14
  }
package/dist/context.js CHANGED
@@ -9,4 +9,7 @@ export default class AppContext {
9
9
  get dataplane() {
10
10
  return this.opts.dataplane;
11
11
  }
12
+ get signingKey() {
13
+ return this.opts.signingKey;
14
+ }
12
15
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Interface for a reactive list of hosts, i.e. for use with the dataplane client.
3
+ */
4
+ export interface HostList {
5
+ get: () => Iterable<string>;
6
+ onUpdate(handler: HostListHandler): void;
7
+ }
8
+ type HostListHandler = (hosts: Iterable<string>) => void;
9
+ /**
10
+ * Maintains a reactive HostList based on a simple setter.
11
+ */
12
+ export declare class BasicHostList implements HostList {
13
+ private hosts;
14
+ private handlers;
15
+ constructor(hosts: Iterable<string>);
16
+ get(): Iterable<string>;
17
+ set(hosts: Iterable<string>): void;
18
+ private update;
19
+ onUpdate(handler: HostListHandler): void;
20
+ }
21
+ export {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Maintains a reactive HostList based on a simple setter.
3
+ */
4
+ export class BasicHostList {
5
+ hosts;
6
+ handlers = [];
7
+ constructor(hosts) {
8
+ this.hosts = hosts;
9
+ }
10
+ get() {
11
+ return this.hosts;
12
+ }
13
+ set(hosts) {
14
+ this.hosts = hosts;
15
+ this.update();
16
+ }
17
+ update() {
18
+ for (const handler of this.handlers) {
19
+ handler(this.hosts);
20
+ }
21
+ }
22
+ onUpdate(handler) {
23
+ this.handlers.push(handler);
24
+ }
25
+ }
@@ -0,0 +1,11 @@
1
+ import { Client, Code, ConnectError } from '@connectrpc/connect';
2
+ import { ClioService } from '../../rpc/clio_connect';
3
+ import { HostList } from './hosts';
4
+ export type DataPlaneClient = Client<typeof ClioService>;
5
+ type HttpVersion = '1.1' | '2';
6
+ export declare const createDataPlaneClient: (hostList: HostList, opts: {
7
+ httpVersion?: HttpVersion;
8
+ rejectUnauthorized?: boolean;
9
+ }) => DataPlaneClient;
10
+ export { Code };
11
+ export declare const isDataplaneError: (err: unknown, code?: Code) => err is ConnectError;
@@ -0,0 +1,97 @@
1
+ import { Code, ConnectError, createClient, makeAnyClient, } from '@connectrpc/connect';
2
+ import { createGrpcTransport } from '@connectrpc/connect-node';
3
+ import assert from 'node:assert';
4
+ import { randomInt } from 'node:crypto';
5
+ import { ClioService } from '../../rpc/clio_connect';
6
+ const MAX_RETRIES = 3;
7
+ export const createDataPlaneClient = (hostList, opts) => {
8
+ const clients = new DataPlaneClients(hostList, opts);
9
+ return makeAnyClient(ClioService, (method) => {
10
+ return async (...args) => {
11
+ let tries = 0;
12
+ let error;
13
+ let remainingClients = clients.get();
14
+ while (tries < MAX_RETRIES) {
15
+ const client = randomElement(remainingClients);
16
+ assert(client, 'no clients available');
17
+ try {
18
+ return await client[method.localName](...args);
19
+ }
20
+ catch (err) {
21
+ if (err instanceof ConnectError &&
22
+ (err.code === Code.Unavailable || err.code === Code.Aborted)) {
23
+ tries++;
24
+ error = err;
25
+ remainingClients = getRemainingClients(remainingClients, client);
26
+ }
27
+ else {
28
+ throw err;
29
+ }
30
+ }
31
+ }
32
+ assert(error);
33
+ throw error;
34
+ };
35
+ });
36
+ };
37
+ export { Code };
38
+ /**
39
+ * Uses a reactive HostList in order to maintain a pool of DataPlaneClients.
40
+ * Each DataPlaneClient is cached per host so that it maintains connections
41
+ * and other internal state when the underlying HostList is updated.
42
+ */
43
+ class DataPlaneClients {
44
+ hostList;
45
+ clientOpts;
46
+ clients = [];
47
+ clientsByHost = new Map();
48
+ constructor(hostList, clientOpts) {
49
+ this.hostList = hostList;
50
+ this.clientOpts = clientOpts;
51
+ this.refresh();
52
+ this.hostList.onUpdate(() => this.refresh());
53
+ }
54
+ get() {
55
+ return this.clients;
56
+ }
57
+ refresh() {
58
+ this.clients = [];
59
+ for (const host of this.hostList.get()) {
60
+ let client = this.clientsByHost.get(host);
61
+ if (!client) {
62
+ client = this.createClient(host);
63
+ this.clientsByHost.set(host, client);
64
+ }
65
+ this.clients.push(client);
66
+ }
67
+ }
68
+ createClient(host) {
69
+ return createBaseClient(host, this.clientOpts);
70
+ }
71
+ }
72
+ export const isDataplaneError = (err, code) => {
73
+ if (err instanceof ConnectError) {
74
+ return !code || err.code === code;
75
+ }
76
+ return false;
77
+ };
78
+ const createBaseClient = (baseUrl, opts) => {
79
+ const { httpVersion = '2', rejectUnauthorized = true } = opts;
80
+ const transport = createGrpcTransport({
81
+ baseUrl,
82
+ httpVersion,
83
+ acceptCompression: [],
84
+ nodeOptions: { rejectUnauthorized },
85
+ });
86
+ return createClient(ClioService, transport);
87
+ };
88
+ const getRemainingClients = (clients, lastClient) => {
89
+ if (clients.length < 2)
90
+ return clients; // no clients to choose from
91
+ return clients.filter((c) => c !== lastClient);
92
+ };
93
+ const randomElement = (arr) => {
94
+ if (arr.length === 0)
95
+ return;
96
+ return arr[randomInt(arr.length)];
97
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Keypair } from '@atproto/crypto';
1
2
  import express from 'express';
2
3
  import { Server } from 'http';
3
4
  import { ServerConfig } from './config';
@@ -13,6 +14,7 @@ export declare class ClioAppView {
13
14
  });
14
15
  static create(opts: {
15
16
  config: ServerConfig;
17
+ signingKey: Keypair;
16
18
  }): ClioAppView;
17
19
  start(): Promise<Server>;
18
20
  destroy(): Promise<void>;
package/dist/index.js CHANGED
@@ -5,8 +5,9 @@ import events from 'events';
5
5
  import express from 'express';
6
6
  import { createHttpTerminator } from 'http-terminator';
7
7
  import API, { health } from './api';
8
- import { createBaseClient } from './client';
9
8
  import AppContext from './context';
9
+ import { createDataPlaneClient } from './dataplane';
10
+ import { BasicHostList } from './dataplane/client/hosts';
10
11
  import * as error from './error';
11
12
  import { createServer } from './lexicons';
12
13
  import { loggerMiddleware } from './logger';
@@ -21,15 +22,20 @@ export class ClioAppView {
21
22
  this.app = opts.app;
22
23
  }
23
24
  static create(opts) {
24
- const { config } = opts;
25
+ const { config, signingKey } = opts;
25
26
  const app = express();
26
27
  app.use(cors({ maxAge: DAY / SECOND }));
27
28
  app.use(loggerMiddleware);
28
- const dataplane = createBaseClient('http://localhost:4000', {});
29
+ const dataplaneHostList = new BasicHostList(config.dataplaneUrls);
30
+ const dataplane = createDataPlaneClient(dataplaneHostList, {
31
+ httpVersion: '2',
32
+ rejectUnauthorized: false,
33
+ });
29
34
  const server = createServer();
30
35
  const ctx = new AppContext({
31
36
  cfg: config,
32
37
  dataplane,
38
+ signingKey,
33
39
  });
34
40
  const api = API(server, ctx);
35
41
  app.use(health.createRouter(ctx));
package/dist/start.js CHANGED
@@ -1,13 +1,21 @@
1
1
  'use strict';
2
+ const assert = require('node:assert');
2
3
  const { ClioAppView } = require('./index');
3
4
  const { ServerConfig } = require('./config');
5
+ const { Secp256k1Keypair } = require('@atproto/crypto');
4
6
  const main = async () => {
7
+ const env = getEnv();
5
8
  const config = ServerConfig.readEnv();
6
- const clio = ClioAppView.create({ config });
9
+ assert(env.serviceSigningKey, 'must set BSKY_SERVICE_SIGNING_KEY');
10
+ const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey);
11
+ const clio = ClioAppView.create({ config, signingKey });
7
12
  await clio.start();
8
13
  const shutdown = async () => {
9
14
  await clio.destroy();
10
15
  };
11
16
  process.on('SIGTERM', shutdown);
12
17
  };
18
+ const getEnv = () => ({
19
+ serviceSigningKey: process.env.BSKY_SERVICE_SIGNING_KEY || undefined,
20
+ });
13
21
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clioplaylists/clio",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Appview server for the Clio application",
5
5
  "main": "dist/start.js",
6
6
  "types": "dist/index.d.ts",