signify-ts
Version:
Signing at the edge for KERI, ACDC, and KERIA
490 lines (442 loc) • 13.9 kB
text/typescript
import { Authenticater } from '../core/authing';
import { HEADER_SIG_TIME } from '../core/httping';
import { ExternalModule, KeyManager } from '../core/keeping';
import { Tier } from '../core/salter';
import { Identifier } from './aiding';
import { Contacts, Challenges } from './contacting';
import { Agent, Controller } from './controller';
import { Oobis, Operations, KeyEvents, KeyStates, Config } from './coring';
import { Credentials, Ipex, Registries, Schemas } from './credentialing';
import { Delegations } from './delegating';
import { Escrows } from './escrowing';
import { Exchanges } from './exchanging';
import { Groups } from './grouping';
import { Notifications } from './notifying';
const DEFAULT_BOOT_URL = 'http://localhost:3903';
class State {
agent: any | null;
controller: any | null;
ridx: number;
pidx: number;
constructor() {
this.agent = null;
this.controller = null;
this.pidx = 0;
this.ridx = 0;
}
}
/** SignifyClient */
export class SignifyClient {
public controller: Controller;
public url: string;
public bran: string;
public pidx: number;
public agent: Agent | null;
public authn: Authenticater | null;
public manager: KeyManager | null;
public tier: Tier;
public bootUrl: string;
public exteralModules: ExternalModule[];
/**
* SignifyClient constructor
* @param {string} url KERIA admin interface URL
* @param {string} bran Base64 21 char string that is used as base material for seed of the client AID
* @param {Tier} tier Security tier for generating keys of the client AID (high | mewdium | low)
* @param {string} bootUrl KERIA boot interface URL
* @param {ExternalModule[]} externalModules list of external modules to load
*/
constructor(
url: string,
bran: string,
tier: Tier = Tier.low,
bootUrl: string = DEFAULT_BOOT_URL,
externalModules: ExternalModule[] = []
) {
this.url = url;
if (bran.length < 21) {
throw Error('bran must be 21 characters');
}
this.bran = bran;
this.pidx = 0;
this.controller = new Controller(bran, tier);
this.authn = null;
this.agent = null;
this.manager = null;
this.tier = tier;
this.bootUrl = bootUrl;
this.exteralModules = externalModules;
}
get data() {
return [this.url, this.bran, this.pidx, this.authn];
}
/**
* Boot a KERIA agent
* @async
* @returns {Promise<Response>} A promise to the result of the boot
*/
async boot(): Promise<Response> {
const [evt, sign] = this.controller?.event ?? [];
const data = {
icp: evt.ked,
sig: sign.qb64,
stem: this.controller?.stem,
pidx: 1,
tier: this.controller?.tier,
};
return await fetch(this.bootUrl + '/boot', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Get state of the agent and the client
* @async
* @returns {Promise<Response>} A promise to the state
*/
async state(): Promise<State> {
const caid = this.controller?.pre;
const res = await fetch(this.url + `/agent/${caid}`);
if (res.status == 404) {
throw new Error(`agent does not exist for controller ${caid}`);
}
const data = await res.json();
const state = new State();
state.agent = data.agent ?? {};
state.controller = data.controller ?? {};
state.ridx = data.ridx ?? 0;
state.pidx = data.pidx ?? 0;
return state;
}
/** Connect to a KERIA agent
* @async
*/
async connect() {
const state = await this.state();
this.pidx = state.pidx;
//Create controller representing the local client AID
this.controller = new Controller(
this.bran,
this.tier,
0,
state.controller
);
this.controller.ridx = state.ridx !== undefined ? state.ridx : 0;
// Create agent representing the AID of KERIA cloud agent
this.agent = new Agent(state.agent);
if (this.agent.anchor != this.controller.pre) {
throw Error(
'commitment to controller AID missing in agent inception event'
);
}
if (this.controller.serder.ked.s == 0) {
await this.approveDelegation();
}
this.manager = new KeyManager(
this.controller.salter,
this.exteralModules
);
this.authn = new Authenticater(
this.controller.signer,
this.agent.verfer!
);
}
/**
* Fetch a resource from the KERIA agent
* @async
* @param {string} path Path to the resource
* @param {string} method HTTP method
* @param {any} data Data to be sent in the body of the resource
* @param {Headers} [extraHeaders] Optional extra headers to be sent with the request
* @returns {Promise<Response>} A promise to the result of the fetch
*/
async fetch(
path: string,
method: string,
data: any,
extraHeaders?: Headers
): Promise<Response> {
const headers = new Headers();
let signed_headers = new Headers();
const final_headers = new Headers();
headers.set('Signify-Resource', this.controller.pre);
headers.set(
HEADER_SIG_TIME,
new Date().toISOString().replace('Z', '000+00:00')
);
headers.set('Content-Type', 'application/json');
const _body = method == 'GET' ? null : JSON.stringify(data);
if (this.authn) {
signed_headers = this.authn.sign(
headers,
method,
path.split('?')[0]
);
} else {
throw new Error('client need to call connect first');
}
signed_headers.forEach((value, key) => {
final_headers.set(key, value);
});
if (extraHeaders !== undefined) {
extraHeaders.forEach((value, key) => {
final_headers.append(key, value);
});
}
const res = await fetch(this.url + path, {
method: method,
body: _body,
headers: final_headers,
});
if (!res.ok) {
const error = await res.text();
const message = `HTTP ${method} ${path} - ${res.status} ${res.statusText} - ${error}`;
throw new Error(message);
}
const isSameAgent =
this.agent?.pre === res.headers.get('signify-resource');
if (!isSameAgent) {
throw new Error('message from a different remote agent');
}
const verification = this.authn.verify(
res.headers,
method,
path.split('?')[0]
);
if (verification) {
return res;
} else {
throw new Error('response verification failed');
}
}
/**
* Create a Signed Request to fetch a resource from an external URL with headers signed by an AID
* @async
* @param {string} aidName Name or alias of the AID to be used for signing
* @param {string} url URL of the requested resource
* @param {RequestInit} req Request options should include:
* - method: HTTP method
* - data Data to be sent in the body of the resource.
* If the data is a CESR JSON string then you should also set contentType to 'application/json+cesr'
* If the data is a FormData object then you should not set the contentType and the browser will set it to 'multipart/form-data'
* If the data is an object then you should use JSON.stringify to convert it to a string and set the contentType to 'application/json'
* - contentType Content type of the request.
* @returns {Promise<Request>} A promise to the created Request
*/
async createSignedRequest(
aidName: string,
url: string,
req: RequestInit
): Promise<Request> {
const hab = await this.identifiers().get(aidName);
const keeper = this.manager!.get(hab);
const authenticator = new Authenticater(
keeper.signers[0],
keeper.signers[0].verfer
);
const headers = new Headers(req.headers);
headers.set('Signify-Resource', hab['prefix']);
headers.set(
HEADER_SIG_TIME,
new Date().toISOString().replace('Z', '000+00:00')
);
const signed_headers = authenticator.sign(
new Headers(headers),
req.method ?? 'GET',
new URL(url).pathname
);
req.headers = signed_headers;
return new Request(url, req);
}
/**
* Approve the delegation of the client AID to the KERIA agent
* @async
* @returns {Promise<Response>} A promise to the result of the approval
*/
async approveDelegation(): Promise<Response> {
const sigs = this.controller.approveDelegation(this.agent!);
const data = {
ixn: this.controller.serder.ked,
sigs: sigs,
};
return await fetch(
this.url + '/agent/' + this.controller.pre + '?type=ixn',
{
method: 'PUT',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
}
);
}
/**
* Save old client passcode in KERIA agent
* @async
* @param {string} passcode Passcode to be saved
* @returns {Promise<Response>} A promise to the result of the save
*/
async saveOldPasscode(passcode: string): Promise<Response> {
const caid = this.controller?.pre;
const body = { salt: passcode };
return await fetch(this.url + '/salt/' + caid, {
method: 'PUT',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Delete a saved passcode from KERIA agent
* @async
* @returns {Promise<Response>} A promise to the result of the deletion
*/
async deletePasscode(): Promise<Response> {
const caid = this.controller?.pre;
return await fetch(this.url + '/salt/' + caid, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Rotate the client AID
* @async
* @param {string} nbran Base64 21 char string that is used as base material for the new seed
* @param {Array<string>} aids List of managed AIDs to be rotated
* @returns {Promise<Response>} A promise to the result of the rotation
*/
async rotate(nbran: string, aids: string[]): Promise<Response> {
const data = this.controller.rotate(nbran, aids);
return await fetch(this.url + '/agent/' + this.controller.pre, {
method: 'PUT',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Get identifiers resource
* @returns {Identifier}
*/
identifiers(): Identifier {
return new Identifier(this);
}
/**
* Get OOBIs resource
* @returns {Oobis}
*/
oobis(): Oobis {
return new Oobis(this);
}
/**
* Get operations resource
* @returns {Operations}
*/
operations(): Operations {
return new Operations(this);
}
/**
* Get keyEvents resource
* @returns {KeyEvents}
*/
keyEvents(): KeyEvents {
return new KeyEvents(this);
}
/**
* Get keyStates resource
* @returns {KeyStates}
*/
keyStates(): KeyStates {
return new KeyStates(this);
}
/**
* Get credentials resource
* @returns {Credentials}
*/
credentials(): Credentials {
return new Credentials(this);
}
/**
* Get IPEX resource
* @returns {Ipex}
*/
ipex(): Ipex {
return new Ipex(this);
}
/**
* Get registries resource
* @returns {Registries}
*/
registries(): Registries {
return new Registries(this);
}
/**
* Get schemas resource
* @returns {Schemas}
*/
schemas(): Schemas {
return new Schemas(this);
}
/**
* Get challenges resource
* @returns {Challenges}
*/
challenges(): Challenges {
return new Challenges(this);
}
/**
* Get contacts resource
* @returns {Contacts}
*/
contacts(): Contacts {
return new Contacts(this);
}
/**
* Get notifications resource
* @returns {Notifications}
*/
notifications(): Notifications {
return new Notifications(this);
}
/**
* Get escrows resource
* @returns {Escrows}
*/
escrows(): Escrows {
return new Escrows(this);
}
/**
* Get groups resource
* @returns {Groups}
*/
groups(): Groups {
return new Groups(this);
}
/**
* Get exchange resource
* @returns {Exchanges}
*/
exchanges(): Exchanges {
return new Exchanges(this);
}
/**
* Get delegations resource
* @returns {Delegations}
*/
delegations(): Delegations {
return new Delegations(this);
}
/**
* Get agent config resource
* @returns {Config}
*/
config(): Config {
return new Config(this);
}
}