Skip to content

Commit

Permalink
Docstrings + Lint (#8)
Browse files Browse the repository at this point in the history
* added docstrings + small lint fixes

* update doc strings to reflect base65url component
  • Loading branch information
skgbafa authored Apr 27, 2023
1 parent 99fe7d0 commit c2ddf51
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 5 deletions.
84 changes: 79 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@ export * from './utils';
export { AttObj, PlainJSON, CID };
const urnRecapPrefix = 'urn:recap:';

/**
* Recap class handles the creation, merging, and decoding of ReCap objects
*/
export class Recap {
#prf: Array<CID>;
#att: AttObj;

/**
* Constructs a Recap instance
*
* @param att - The input attenuation object (default is an empty object {})
* @param prf - The input proof array (default is an empty array [])
*/
constructor(att: AttObj = {}, prf: Array<CID> | Array<string> = []) {
checkAtt(att);
this.#att = att;
Expand All @@ -26,14 +35,29 @@ export class Recap {
);
}

/**
* Gets the proofs array of the Recap object
*
* @returns An Array of CID objects
*/
get proofs(): Array<CID> {
return this.#prf;
}

/**
* Gets the attenuation object of the Recap object
*
* @returns An attenuation object (AttObj)
*/
get attenuations(): AttObj {
return this.#att;
}

/**
* Calculates the statement field of a SIWE recap-transformed-statement
*
* @returns A string representing the statement constructed from the Recap object
*/
get statement(): string {
let statement =
'I further authorize the stated URI to perform the following actions on my behalf: ';
Expand Down Expand Up @@ -66,6 +90,11 @@ export class Recap {
return statement;
}

/**
* Adds a new proof to the proofs collection of the Recap object
*
* @param cid - A CID (Content Identifier) object or its string representation
*/
addProof(cid: string | CID) {
if (typeof cid === 'string') {
this.#prf.push(CID.parse(cid));
Expand All @@ -74,10 +103,18 @@ export class Recap {
}
}

/**
* Adds a new attenuation to the attenuations object of the Recap object
*
* @param resource - The resource URI
* @param namespace - The ability namespace (default is *)
* @param name - The ability name (default is *)
* @param restriction - A JSON object containing restrictions or requirements for the action (default is {})
*/
addAttenuation(
resource: string,
namespace: string = '*',
name: string = '*',
namespace = '*',
name = '*',
restriction: { [key: string]: PlainJSON } = {}
) {
if (!validString(namespace)) {
Expand All @@ -101,6 +138,11 @@ export class Recap {
}
}

/**
* Merges another Recap object with the current Recap object
*
* @param other - The other Recap object to be merged
*/
merge(other: Recap) {
this.#prf.push(...other.proofs.filter(cid => !this.#prf.includes(cid)));

Expand All @@ -124,6 +166,13 @@ export class Recap {
}
}

/**
* Decodes a Recap URI into a Recap object
*
* @param recap - The input Recap URI string
* @returns A Recap object decoded from the input Recap URI
* @throws Will throw an error if the input string is not a valid Recap URI
*/
static decode_urn(recap: string): Recap {
if (!recap.startsWith(urnRecapPrefix)) {
throw new Error('Invalid recap urn');
Expand All @@ -133,14 +182,28 @@ export class Recap {
return new Recap(att, prf);
}

/**
* Extracts the Recap object from a SiweMessage instance
*
* @param siwe - A SiweMessage instance
* @returns A Recap object extracted from the input SiweMessage
* @throws Will throw an error if the SiweMessage doesn't have any resources
*/
static extract(siwe: SiweMessage): Recap {
if (siwe.resources === undefined) {
throw new Error('No resources in SiweMessage');
}
let last_index = siwe.resources.length - 1;
const last_index = siwe.resources.length - 1;
return Recap.decode_urn(siwe.resources[last_index]);
}

/**
* Extracts and verifies a Recap object from a SiweMessage instance
*
* @param siwe - A SiweMessage instance
* @returns A verified Recap object extracted from the input SiweMessage
* @throws Will throw an error if the SiweMessage has an invalid statement
*/
static extract_and_verify(siwe: SiweMessage): Recap {
const recap = Recap.extract(siwe);
if (
Expand All @@ -152,6 +215,12 @@ export class Recap {
return recap;
}

/**
* Adds a Recap object to a SiweMessage
*
* @param siwe - The input SiweMessage instance to be modified
* @returns A modified SiweMessage instance with the Recap object added
*/
add_to_siwe_message(siwe: SiweMessage): SiweMessage {
try {
// try merge with existing recap
Expand All @@ -162,8 +231,8 @@ export class Recap {
) {
throw new Error('no recap');
}
let other = Recap.extract_and_verify(siwe);
let previousStatement = other.statement;
const other = Recap.extract_and_verify(siwe);
const previousStatement = other.statement;
other.merge(this);
siwe.statement =
siwe.statement.slice(0, -previousStatement.length) + other.statement;
Expand All @@ -183,6 +252,11 @@ export class Recap {
}
}

/**
* Encodes a Recap object into a Recap URI
*
* @returns A Recap URI string
*/
encode(): string {
return `${urnRecapPrefix}${encodeRecap(this.#att, this.#prf)}`;
}
Expand Down
54 changes: 54 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,56 @@ import { base64url } from 'multiformats/bases/base64';
import { CID } from 'multiformats/cid';
import serialize from 'canonicalize';

/**
* PlainJSON type definition.
*
* @typedef PlainJSON
* @type {boolean|number|string|Object.<string, PlainJSON>|Array.<PlainJSON>}
*/
export type PlainJSON =
| boolean
| number
| string
| { [key: string]: PlainJSON }
| Array<PlainJSON>;

/**
* Attenuation object type definition.
*
* @typedef AttObj
* @type {Object.<string, Object.<string, Array.<PlainJSON>>>}
*/
export type AttObj = { [key: string]: { [key: string]: Array<PlainJSON> } };

/** @private */
const stringRegex = /^[a-zA-Z0-9.*_+-]+$/g;
/** @private */
const abilityStringRegex = /^[a-zA-Z0-9.*_+-]+\/[a-zA-Z0-9.*_+-]+$/g;

/**
* Check if the input string is a valid string Regex.
*
* @param {string} str - The input string.
* @returns {boolean} - Returns true if the input string is valid, otherwise false.
*/
export const validString = (str: string) => str.match(stringRegex) !== null;

/**
* Check if the input string is a valid ability string Regex.
*
* @param {string} str - The input string.
* @returns {boolean} - Returns true if the input string is valid, otherwise false.
*/
export const validAbString = (str: string) =>
str.match(abilityStringRegex) !== null;

/**
* Encode recap details (an attenuation object and a list of CIDs) into a base64 representation to be append the the Recap URI.
*
* @param {AttObj} att - The attenuation object.
* @param {Array.<CID>} prf - An array of proof CIDs.
* @returns {string} - Returns the base64url encoded component of a ReCap URI.
*/
export const encodeRecap = (att: AttObj, prf: Array<CID>) =>
base64url.encoder.baseEncode(
new TextEncoder().encode(
Expand All @@ -29,6 +63,13 @@ export const encodeRecap = (att: AttObj, prf: Array<CID>) =>
)
);

/**
* Decode the base64 component of a ReCap URI into recap details (an attenuation object and a list of CIDs).
*
* @param {string} recap - The base64url encoded component ReCap URI.
* @returns {Object} - An object containing a decoded att property as an attenuation object, and a decoded prf property as an array of CID objects.
* @throws {Error} - Throws an error if the ReCap URI is invalid.
*/
export const decodeRecap = (
recap: string
): { att: AttObj; prf: Array<CID> } => {
Expand Down Expand Up @@ -57,6 +98,13 @@ export const decodeRecap = (
};
};

/**
* Check if the input attenuation object is valid.
*
* @param {AttObj} att - The input attenuation object.
* @returns {boolean} - Returns true if the input attenuation object is valid, otherwise false.
* @throws {Error} - Throws an error if the attenuation object contains invalid entries.
*/
export const checkAtt = (att: AttObj): att is AttObj => {
// TODO ensure the att keys are valid URIs
// because URIs are so broad, there's no easy/efficient way to do this
Expand All @@ -82,6 +130,12 @@ export const checkAtt = (att: AttObj): att is AttObj => {
return true;
};

/**
* Check if the input object is sorted.
*
* @param {PlainJSON} obj - The input object.
* @returns {boolean} - Returns true if the input object is sorted, otherwise false.
*/
export const isSorted = (obj: PlainJSON): boolean => {
if (Array.isArray(obj)) {
// its an array
Expand Down

0 comments on commit c2ddf51

Please sign in to comment.