signify-ts
Version:
Signing at the edge for KERI, ACDC, and KERIA
523 lines (471 loc) • 16 kB
text/typescript
import { Tier } from '../core/salter';
import { Algos } from '../core/manager';
import { incept, interact, reply, rotate } from '../core/eventing';
import { b, Ilks, Serials, Versionage } from '../core/core';
import { Tholder } from '../core/tholder';
import { MtrDex } from '../core/matter';
import { Serder } from '../core/serder';
import { parseRangeHeaders } from '../core/httping';
import { KeyManager } from '../core/keeping';
import { HabState } from '../core/state';
/** Arguments required to create an identfier */
export interface CreateIdentiferArgs {
transferable?: boolean;
isith?: string | number | string[];
nsith?: string | number | string[];
wits?: string[];
toad?: number;
proxy?: string;
delpre?: string;
dcode?: string;
data?: any;
algo?: Algos;
pre?: string;
states?: any[];
rstates?: any[];
prxs?: any[];
nxts?: any[];
mhab?: HabState;
keys?: string[];
ndigs?: string[];
bran?: string;
count?: number;
ncount?: number;
tier?: Tier;
extern_type?: string;
extern?: any;
}
/** Arguments required to rotate an identfier */
export interface RotateIdentifierArgs {
transferable?: boolean;
nsith?: string | number | string[];
toad?: number;
cuts?: string[];
adds?: string[];
data?: Array<object>;
ncode?: string;
ncount?: number;
ncodes?: string[];
states?: any[];
rstates?: any[];
}
/**
* Reducing the SignifyClient dependencies used by Identifier class
*/
export interface IdentifierDeps {
fetch(
pathname: string,
method: string,
body: unknown,
headers?: Headers
): Promise<Response>;
pidx: number;
manager: KeyManager | null;
}
/**
* Updatable information for a managed identifier
*/
export interface IdentifierInfo {
name: string;
}
/** Identifier */
export class Identifier {
public client: IdentifierDeps;
/**
* Identifier
* @param {IdentifierDeps} client
*/
constructor(client: IdentifierDeps) {
this.client = client;
}
/**
* List managed identifiers
* @async
* @param {number} [start=0] Start index of list of notifications, defaults to 0
* @param {number} [end=24] End index of list of notifications, defaults to 24
* @returns {Promise<any>} A promise to the list of managed identifiers
*/
async list(start: number = 0, end: number = 24): Promise<any> {
const extraHeaders = new Headers();
extraHeaders.append('Range', `aids=${start}-${end}`);
const path = `/identifiers`;
const data = null;
const method = 'GET';
const res = await this.client.fetch(path, method, data, extraHeaders);
const cr = res.headers.get('content-range');
const range = parseRangeHeaders(cr, 'aids');
const aids = await res.json();
return {
start: range.start,
end: range.end,
total: range.total,
aids: aids,
};
}
/**
* Get information for a managed identifier
* @async
* @param {string} name Prefix or alias of the identifier
* @returns {Promise<HabState>} A promise to the identifier information
*/
async get(name: string): Promise<HabState> {
const path = `/identifiers/${encodeURIComponent(name)}`;
const data = null;
const method = 'GET';
const res = await this.client.fetch(path, method, data);
return await res.json();
}
/**
* Update managed identifier
* @async
* @param {string} name Prefix or alias of the identifier
* @param {IdentifierInfo} info Information to update for the given identifier
* @returns {Promise<HabState>} A promise to the identifier information after updating
*/
async update(name: string, info: IdentifierInfo): Promise<HabState> {
const path = `/identifiers/${name}`;
const method = 'PUT';
const res = await this.client.fetch(path, method, info);
return await res.json();
}
/**
* Create a managed identifier
* @async
* @param {string} name Name or alias of the identifier
* @param {CreateIdentiferArgs} [kargs] Optional parameters to create the identifier
* @returns {EventResult} The inception result
*/
async create(
name: string,
kargs: CreateIdentiferArgs = {}
): Promise<EventResult> {
const algo = kargs.algo == undefined ? Algos.salty : kargs.algo;
const transferable = kargs.transferable ?? true;
const isith = kargs.isith ?? '1';
let nsith = kargs.nsith ?? '1';
let wits = kargs.wits ?? [];
const toad = kargs.toad ?? 0;
let dcode = kargs.dcode ?? MtrDex.Blake3_256;
const proxy = kargs.proxy;
const delpre = kargs.delpre;
const data = kargs.data != undefined ? [kargs.data] : [];
const pre = kargs.pre;
const states = kargs.states;
const rstates = kargs.rstates;
const prxs = kargs.prxs;
const nxts = kargs.nxts;
const mhab = kargs.mhab;
const _keys = kargs.keys;
const _ndigs = kargs.ndigs;
const bran = kargs.bran;
const count = kargs.count;
let ncount = kargs.ncount;
const tier = kargs.tier;
const extern_type = kargs.extern_type;
const extern = kargs.extern;
if (!transferable) {
ncount = 0;
nsith = 0;
dcode = MtrDex.Ed25519N;
}
const xargs = {
transferable: transferable,
isith: isith,
nsith: nsith,
wits: wits,
toad: toad,
proxy: proxy,
delpre: delpre,
dcode: dcode,
data: data,
algo: algo,
pre: pre,
prxs: prxs,
nxts: nxts,
mhab: mhab,
states: states,
rstates: rstates,
keys: _keys,
ndigs: _ndigs,
bran: bran,
count: count,
ncount: ncount,
tier: tier,
extern_type: extern_type,
extern: extern,
};
const keeper = this.client.manager!.new(algo, this.client.pidx, xargs);
const [keys, ndigs] = await keeper!.incept(transferable);
wits = wits !== undefined ? wits : [];
let serder: Serder | undefined = undefined;
if (delpre == undefined) {
serder = incept({
keys: keys!,
isith: isith,
ndigs: ndigs,
nsith: nsith,
toad: toad,
wits: wits,
cnfg: [],
data: data,
version: Versionage,
kind: Serials.JSON,
code: dcode,
intive: false,
});
} else {
serder = incept({
keys: keys!,
isith: isith,
ndigs: ndigs,
nsith: nsith,
toad: toad,
wits: wits,
cnfg: [],
data: data,
version: Versionage,
kind: Serials.JSON,
code: dcode,
intive: false,
delpre: delpre,
});
}
const sigs = await keeper!.sign(b(serder.raw));
const jsondata: any = {
name: name,
icp: serder.ked,
sigs: sigs,
proxy: proxy,
smids:
states != undefined
? states.map((state) => state.i)
: undefined,
rmids:
rstates != undefined
? rstates.map((state) => state.i)
: undefined,
};
jsondata[algo] = keeper.params();
this.client.pidx = this.client.pidx + 1;
const res = await this.client.fetch('/identifiers', 'POST', jsondata);
return new EventResult(serder, sigs, res);
}
/**
* Generate an interaction event in a managed identifier
* @async
* @param {string} name Prefix or alias of the identifier
* @param {any} [data] Option data to be anchored in the interaction event
* @returns {Promise<EventResult>} A promise to the interaction event result
*/
async interact(name: string, data?: any): Promise<EventResult> {
let { serder, sigs, jsondata } = await this.createInteract(name, data);
const res = await this.client.fetch(
'/identifiers/' + name + '/events',
'POST',
jsondata
);
return new EventResult(serder, sigs, res);
}
async createInteract(
name: string,
data?: any
): Promise<{ serder: any; sigs: any; jsondata: any }> {
const hab = await this.get(name);
const pre: string = hab.prefix;
const state = hab.state;
const sn = parseInt(state.s, 16);
const dig = state.d;
data = Array.isArray(data) ? data : [data];
const serder = interact({
pre: pre,
sn: sn + 1,
data: data,
dig: dig,
version: undefined,
kind: undefined,
});
const keeper = this.client!.manager!.get(hab);
const sigs = await keeper.sign(b(serder.raw));
const jsondata: any = {
ixn: serder.ked,
sigs: sigs,
};
jsondata[keeper.algo] = keeper.params();
return { serder, sigs, jsondata };
}
/**
* Generate a rotation event in a managed identifier
* @param {string} name Name or alias of the identifier
* @param {RotateIdentifierArgs} [kargs] Optional parameters requiered to generate the rotation event
* @returns {Promise<EventResult>} A promise to the rotation event result
*/
async rotate(
name: string,
kargs: RotateIdentifierArgs = {}
): Promise<EventResult> {
const transferable = kargs.transferable ?? true;
const ncode = kargs.ncode ?? MtrDex.Ed25519_Seed;
const ncount = kargs.ncount ?? 1;
const hab = await this.get(name);
const pre = hab.prefix;
const delegated = hab.state.di !== '';
const state = hab.state;
const count = state.k.length;
const dig = state.d;
const ridx = parseInt(state.s, 16) + 1;
const wits = state.b;
let isith = state.nt;
let nsith = kargs.nsith ?? isith;
// if isith is None: # compute default from newly rotated verfers above
if (isith == undefined)
isith = `${Math.max(1, Math.ceil(count / 2)).toString(16)}`;
// if nsith is None: # compute default from newly rotated digers above
if (nsith == undefined)
nsith = `${Math.max(1, Math.ceil(ncount / 2)).toString(16)}`;
const cst = new Tholder({ sith: isith }).sith; // current signing threshold
const nst = new Tholder({ sith: nsith }).sith; // next signing threshold
// Regenerate next keys to sign rotation event
const keeper = this.client.manager!.get(hab);
// Create new keys for next digests
const ncodes = kargs.ncodes ?? new Array(ncount).fill(ncode);
const states = kargs.states == undefined ? [] : kargs.states;
const rstates = kargs.rstates == undefined ? [] : kargs.rstates;
const [keys, ndigs] = await keeper!.rotate(
ncodes,
transferable,
states,
rstates
);
const cuts = kargs.cuts ?? [];
const adds = kargs.adds ?? [];
const data = kargs.data != undefined ? [kargs.data] : [];
const toad = kargs.toad;
const ilk = delegated ? Ilks.drt : Ilks.rot;
const serder = rotate({
pre: pre,
ilk: ilk,
keys: keys,
dig: dig,
sn: ridx,
isith: cst,
nsith: nst,
ndigs: ndigs,
toad: toad,
wits: wits,
cuts: cuts,
adds: adds,
data: data,
});
const sigs = await keeper.sign(b(serder.raw));
const jsondata: any = {
rot: serder.ked,
sigs: sigs,
smids:
states != undefined
? states.map((state) => state.i)
: undefined,
rmids:
rstates != undefined
? rstates.map((state) => state.i)
: undefined,
};
jsondata[keeper.algo] = keeper.params();
const res = await this.client.fetch(
'/identifiers/' + name + '/events',
'POST',
jsondata
);
return new EventResult(serder, sigs, res);
}
/**
* Authorize an endpoint provider in a given role for a managed identifier
* @remarks
* Typically used to authorize the agent to be the endpoint provider for the identifier in the role of `agent`
* @async
* @param {string} name Name or alias of the identifier
* @param {string} role Authorized role for eid
* @param {string} [eid] Optional qb64 of endpoint provider to be authorized
* @param {string} [stamp=now] Optional date-time-stamp RFC-3339 profile of iso8601 datetime. Now is the default if not provided
* @returns {Promise<EventResult>} A promise to the result of the authorization
*/
async addEndRole(
name: string,
role: string,
eid?: string,
stamp?: string
): Promise<EventResult> {
const hab = await this.get(name);
const pre = hab.prefix;
const rpy = this.makeEndRole(pre, role, eid, stamp);
const keeper = this.client.manager!.get(hab);
const sigs = await keeper.sign(b(rpy.raw));
const jsondata = {
rpy: rpy.ked,
sigs: sigs,
};
const res = await this.client.fetch(
'/identifiers/' + name + '/endroles',
'POST',
jsondata
);
return new EventResult(rpy, sigs, res);
}
/**
* Generate an /end/role/add reply message
* @param {string} pre Prefix of the identifier
* @param {string} role Authorized role for eid
* @param {string} [eid] Optional qb64 of endpoint provider to be authorized
* @param {string} [stamp=now] Optional date-time-stamp RFC-3339 profile of iso8601 datetime. Now is the default if not provided
* @returns {Serder} The reply message
*/
private makeEndRole(
pre: string,
role: string,
eid?: string,
stamp?: string
): Serder {
const data: any = {
cid: pre,
role: role,
};
if (eid != undefined) {
data.eid = eid;
}
const route = '/end/role/add';
return reply(route, data, stamp, undefined, Serials.JSON);
}
/**
* Get the members of a group identifier
* @async
* @param {string} name - Name or alias of the identifier
* @returns {Promise<any>} - A promise to the list of members
*/
async members(name: string): Promise<any> {
const res = await this.client.fetch(
'/identifiers/' + name + '/members',
'GET',
undefined
);
return await res.json();
}
}
/** Event Result */
export class EventResult {
private readonly _serder: Serder;
private readonly _sigs: string[];
private readonly response: Response;
constructor(serder: Serder, sigs: string[], response: Response) {
this._serder = serder;
this._sigs = sigs;
this.response = response;
}
get serder() {
return this._serder;
}
get sigs() {
return this._sigs;
}
async op(): Promise<any> {
return await this.response.json();
}
}