diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml deleted file mode 100644 index 07896bf..0000000 --- a/.github/workflows/gh-pages.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Sample workflow for building and deploying a Jekyll site to GitHub Pages -name: GitHub Pages deploy - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Build job - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v5 - - name: Install - run: npm install - - name: Build - run: npm run build-example - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: public/ - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 0732d62..65e8a6b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # link ![tests](https://github.com/bicycle-codes/link/actions/workflows/nodejs.yml/badge.svg) [![types](https://img.shields.io/npm/types/@bicycle-codes/link?style=flat-square)](README.md) -[![module](https://img.shields.io/badge/module-ESM-blue?style=flat-square)](README.md) +[![module](https://img.shields.io/badge/module-ESM%2FCJS-blue?style=flat-square)](README.md) [![semantic versioning](https://img.shields.io/badge/semver-2.0.0-blue?logo=semver&style=flat-square)](https://semver.org/) [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) Link multiple devices via websocket. -This depends on each device having a [keystore](https://github.com/fission-codes/keystore-idb) that holds its private keys. +This depends on each device having a [keystore](https://github.com/fission-codes/keystore-idb) which stores the private keys. ## install @@ -16,3 +16,175 @@ npm i -S @bicycle-codes/link ``` ## use + +## example +Connect two devices, a phone and computer, for example. They must both know `code`, which by default is a 6 digit numberic code that should be transmitted out of band. + +```js +import { program as Program } from '@oddjs/odd' +import { create as createID } from '@bicycle-codes/identity' + +const program = await Program({ + namespace: { + name: 'link-example', + creator: 'bicycle-computing' + } +}) +const { crypto } = program.components + +const myId = await createID(crypto, { + humanName: 'alice', + humanReadableDeviceName: 'phone' +}) + +/** + * 'phone' is the parent device. The parent should connect first. + * The resolved promise is for a new `Identity`, which is a new ID, including + * the child device + */ +const newIdentity = await Parent(myId, crypto, { + host: 'localhost:1999', + code: '1234' +}) +``` + +...On a different machine... + +```js +const program = await Program({ + namespace: { + name: 'link-example', + creator: 'bicycle-computing' + } +}) +const { crypto } = program.components + +const { identity, certificate } = await Child(crypto, { + host: PARTY_URL, + code: '1234', + humanReadableDeviceName: 'computer' +}) +``` + +Both machines now have an ID that looks like this: + +![Screenshot of identity](image.png) + +### serverside +This depends on a websocket server existing. We provide the export +`server` to help with this. + +This should be ergonomic to use with [partykit](https://www.partykit.io/). + +#### server example + +```js +import type * as Party from 'partykit/server' +import { onConnect, onMessage } from '@bicycle-codes/link/server' + +export default class Server implements Party.Server { + existingDevice:string|undefined + + constructor (readonly room: Party.Room) { + this.room = room + } + + /** + * Parent device must connect first + */ + onConnect (conn:Party.Connection) { + onConnect(this, conn) + } + + onMessage (message:string, sender:Party.Connection) { + onMessage(this, message, sender) + } +} + +Server satisfies Party.Worker +``` + +## API + +### Parent +Call this from the "parent" device. It returns a promise that will resolve with a new identity, that includes the child devices. + +```ts +import type { Crypto, Identity } from '@bicycle-codes/identity' + +async function Parent (identity:Identity, oddCrypto:Crypto, { + host, + code, + query +}:{ + host:string; + code:string; + query?:string; +}):Promise +``` + +### Child +Call this from the "child" device. It returns a promise that will resolve with +`{ identity, certificate }`, where `certificate` is a signed message from the +parent device, serving as proof that the child is authorized. + +```ts +import type { Crypto, Identity } from '@bicycle-codes/identity' + +async function Child (oddCrypto:Crypto, { + host, + code, + query, + humanReadableDeviceName +}:{ + host:string; + code:string; + query?:string; + humanReadableDeviceName:string; +}):Promise<{ identity:Identity, certificate:Certificate }> +``` + +### Code +Need to create a code before connecting the parent device. The code should be transmitted out-of-band; it serves as verification that the two devices want to connect. + +By default this will create a random 6 digit numeric code; see the source code +for how to use a different alphabet. + +```ts +function Code (alphabet?:string, length?:number):string { + return customAlphabet(alphabet || numbers, length ?? 6)() +} +``` + +#### `Code` example +```js +import { Code } from '@bicycle-codes/link' +const code = Code() +// => 942814 +``` + +## types + +### Certificate + +The certificate is a signed message from the "parent" device, +saying that the new device is authorized. + +```ts +import { create as createMessage } from '@bicycle-codes/message' + +type Certificate = Awaited< + ReturnType> +> +``` + +The certificate will also have keys `author` and `signature`, via the +[message module](https://github.com/bicycle-codes/message), with the DID and +signature for this data. diff --git a/example/index.ts b/example/index.ts index f6dc3f0..6d5b68d 100644 --- a/example/index.ts +++ b/example/index.ts @@ -7,9 +7,7 @@ import { program as Program } from '@oddjs/odd' import { Button } from '@nichoth/components/htm/button' import Debug from '@nichoth/debug' import { TextInput } from '@nichoth/components/htm/text-input' -import { customAlphabet } from '@nichoth/nanoid' -import { numbers } from '@nichoth/nanoid-dictionary' -import { Parent, Child, Certificate } from '../src/index.js' +import { Parent, Child, Certificate, Code } from '../src/index.js' import '@nichoth/components/button-outline.css' import '@nichoth/components/text-input.css' import '@nichoth/components/button.css' @@ -46,7 +44,7 @@ const Example:FunctionComponent = function Example () { ev.preventDefault() debug('add a child device', ev) // the parent needs to create a random ID for websocket connection - const code = customAlphabet(numbers, 6)() + const code = Code() batch(() => { state.linkState.value = 'parent' diff --git a/image.png b/image.png new file mode 100644 index 0000000..405108d Binary files /dev/null and b/image.png differ diff --git a/package.json b/package.json index 69c8865..9d405f2 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { - "private": true, + "name": "@bicycle-codes/link", + "description": "Link multiple machiines via websocket.", "type": "module", "version": "0.0.0", "main": "dist/index.js", @@ -20,6 +21,30 @@ "postversion": "git push --follow-tags && npm publish", "prepublishOnly": "npm run build" }, + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./server": { + "import": "./dist/server.js", + "require": "./dist/server.cjs" + }, + "./*": { + "import": [ + "./dist/*.js", + "./dist/*" + ], + "require": [ + "./dist/*.cjs", + "./dist/*" + ] + } + }, + "directories": { + "example": "example", + "test": "test" + }, "dependencies": { "@bicycle-codes/identity": "^0.7.2", "@nichoth/nanoid": "^5.0.8", @@ -51,22 +76,22 @@ "typescript": "^5.4.4", "vite": "^5.2.10" }, - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, - "./*": { - "import": [ - "./dist/*.js", - "./dist/*" - ], - "require": [ - "./dist/*.cjs", - "./dist/*" - ] - } + "types": "./dist/index.d.ts", + "bugs": { + "url": "https://github.com/bicycle-codes/link/issues" }, + "homepage": "https://github.com/bicycle-codes/link#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/bicycle-codes/link.git" + }, + "keywords": [ + "link", + "multiple", + "devices", + "websocket", + "crypto" + ], "author": "nichoth (https://nichoth.com)", "license": "MIT" } diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index da7aebe..0000000 --- a/src/util.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { DID, Crypto } from '@bicycle-codes/identity' -import { concat, toString } from 'uint8arrays' - -const BASE58_DID_PREFIX = 'did:key:z' - -export async function writeKeyToDid ( - crypto:Crypto -):Promise { - const [pubKey, ksAlg] = await Promise.all([ - crypto.keystore.publicWriteKey(), - crypto.keystore.getAlgorithm() - ]) - - return publicKeyToDid(crypto, pubKey, ksAlg) -} - -export function publicKeyToDid ( - crypto:Crypto, - publicKey:Uint8Array, - keyType:string -):DID { - // Prefix public-write key - const prefix = crypto.did.keyTypes[keyType]?.magicBytes - if (prefix === null) { - throw new Error(`Key type '${keyType}' not supported, ` + - `available types: ${Object.keys(crypto.did.keyTypes).join(', ')}`) - } - - const prefixedBuf = concat([prefix, publicKey]) - - // Encode prefixed - return (BASE58_DID_PREFIX + toString(prefixedBuf, 'base58btc')) as DID -}