1.1.1 • Published 2 months ago

@zcredjs/core v1.1.1

Weekly downloads
-
License
(Apache-2.0 OR MI...
Repository
github
Last release
2 months ago

zcredjs core

ZCred protocol core library. Contains basic interfaces, types, and implementations. For more details see ZCIPs

Http Credential

The HTTP-based zk-credential MUST contain a property named meta, which is an object including the field httpIssuerURL representing the URL of the issuer endpoint.

Credential type:

export type Attributes = {
  type: string;
  issuanceDate: string;
  validFrom: string;
  validUntil: string;
  subject: {
    id: Identifier
  }
}

export type HttpCredential<TAttr extends Attributes = Attributes> = {
 meta: {
    issuer: {
      type: MetaIssuerType;
      uri: string;
    }
  };
  attributes: TAttr,
  proofs: { [key: string]: Record<string, Proof> }
}

Proofs

Library supports signature proofs and ACI (attributes content identifier) proofs

Signature proof type:

export type SignatureProof = {
  /** Proof type */
  type: string;
  /** Issuer identifier according to ZCIP-2 */
  issuer: {
    id: Identifier;
  }
  /** Signature */
  signature: string;
  schema: {
    type: string[],
    issuer: {
      id: IdentifierSchema
    },
    signature: string[]
    attributes: AttributesSchema
  }
}

ACI (Attributes Content Identifier) proof type:

export type ACIProof = {
  /** Proof type */
  type: string;
  /** ACI */
  aci: string;
  schema: {
    attributes: AttributesSchema;
    type: string[];
    aci: string[];
  }
}

Http issuer

Issuer URI

Each http issuer MUST have a unique URI, e.g. "https://zcred.issuer/api/passport",

Info method

The Info method MUST have the URL <issuerURI>/info, for instance, "https://zcred.issuer/api/passport/info", and support the GET method. If the response status code is 200 (OK), the body MUST contain JSON according to the type.

export type Info = {
  /** Credential type */
  credentialType: string;
  /** If true Issuer MUST provide update proofs method */
  updatableProofs: boolean;
  /** ISO date when new proof types was added to credential */
  proofsUpdated: string;
  /** Proofs information */
  proofsInfo: {
    /** Proof type */
    type: string;
    /** References to proof */
    references: string[]
  }[]
}

Example of implementation

const resp = await fetch(new URL("./info", this.endpoint));
const body = await resp.json();
if (resp.ok) return body;
throw new Error(body);

Challenge method

The Challenge method MUST have the URL <issuerURI>/challenge, for example, "https://zcred.issuer/api/passport/challenge", and support the POST method. It should include HTTP headers such as content-type: application/json and authorization: Bearer <accessToken> (the authorization header is optional).

The request body MUST conform to the type ChallengeReq:

export type ChallengeOptions = {
  /** Chain id according CAIP-2 */
  chainId?: string;
  /** Redirect URL after issuing or authorization */
  redirectURL?: string;
}

export type ChallengeReq = {
  subject: {
    /** Credential subject identifier */
    id: Identifier;
  }
  /** Date when credential becomes valid */
  validFrom?: string;
  /** Date when credential becomes not valid */
  validUntil?: string;
  options?: ChallengeOptions
}

Response body MUST conform to the type:

export type Challenge = {
  /** Issuing session unique identifier */
  sessionId: string;
  /** Message for signing */
  message: string;
  /** Verification URL */
  verifyURL?: string;
}

Can issue method

The Challenge method MUST have the URL <issuerURI>/can-issue, for example, "https://zcred.issuer/api/passport/can-issue", and support the POST method. It should include HTTP headers such as content-type: application/json and authorization: Bearer <accessToken> (the authorization header is optional).

The request body MUST conform to the type CanIssueReq:

export type CanIssueReq = {
  /** Issuing session unique identifier */
  sessionId: string;
}

Response body MUST conform to the type:

export type CanIssue = {
  /** If true you can execute Issue Method */
  canIssue: boolean
}

Issue method

Issue method MUST has URL <issuerURI>/issue, e.g. "https://zcred.issuer/api/passport/issue", and supports POST method with http headers content-type: application/json & authorization: Bearer <accessToken> (authorization header is optional).

Request body MUST match type IssueReq:

export type IssueReq = {
  /** Issuing session unique identifier */
  sessionId: string;
  /** Signature from Challenge message */
  signature: string;
}

Response body MUST be zk-credential according ZCIP-2

Update proofs method

If info method result object updatableProofs value is true issuer MUST provides update proofs method, else update proofs method is optional.

Update method MUST has URL <issuerURI>/update-proofs, e.g. "https://zcred.issuer/api/passport/update-proofs", and supports POST method with http headers content-type: application/json & authorization: Bearer <accessToken> (authorization header is optional).

Update proofs method does not update zk-credential attributes value, it only MUST add new proof types and references to zk-credentialproofs property.

Request body MUST be zk-credential.

Response body MUST be zk-credential

Interface

/** Http issuer interface */
export interface IHttpIssuer {
  /** Issuer endpoint */
  uri: URL;
  /** Info method */
  getInfo(): Promise<Info>;
  /** Challenge method */
  getChallenge(challengeReq: ChallengeReq): Promise<Challenge>;
  /** Can issue method */
  canIssue(canIssueReq: CanIssueReq): Promise<CanIssue>;
  /** Issue method */
  issue<
    TCred extends HttpCredential = HttpCredential
  >(issueReq: IssueReq): Promise<TCred>;
  /** Update proofs method */
  updateProofs?<
    TCred extends HttpCredential = HttpCredential
  >(cred: TCred): Promise<TCred>;
  /** Change subject id method */
  browserIssue?<
    TCred extends HttpCredential = HttpCredential
  >(args: BrowserIssueParams): Promise<TCred>;
}

Implementation

import {
  BrowserIssueParams,
  CanIssue,
  CanIssueReq,
  Challenge,
  ChallengeReq,
  IHttpIssuer,
  Info,
  IssueReq,
} from "./types/issuer.js";
import { HttpCredential } from "./types/index.js";
import { repeatUtil } from "./utils/repeat.js";

export class HttpIssuer implements IHttpIssuer {
  readonly uri: URL;

  constructor(
    issuerURI: string,
    private readonly accessToken?: string
  ) {
    this.uri = new URL(issuerURI);
    const paths = this.uri.pathname;
    const type = paths[paths.length - 1];
    if (!type) {
      throw new Error(`Http issuer initialization error: issuer endpoint pathname is undefined, endpoint: ${issuerURI}`);
    }
  }

  static init(endpoint: string, accessToken?: string): IHttpIssuer {
    return new HttpIssuer(endpoint, accessToken);
  }

  async getInfo(): Promise<Info> {
    const resp = await fetch(new URL("./info", this.uri));
    const body = await resp.json();
    if (resp.ok) return body;
    throw new Error(body);
  }

  async getChallenge(challengeReq: ChallengeReq): Promise<Challenge> {
    const resp = await fetch(new URL("./challenge", this.uri), {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify(challengeReq)
    });
    const body = await resp.json();
    if (resp.ok) return body;
    throw new Error(body);
  }

  async canIssue(canIssueReq: CanIssueReq): Promise<CanIssue> {
    const resp = await fetch(new URL("./can-issue", this.uri), {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify(canIssueReq)
    });
    const body = await resp.json();
    if (resp.ok) return body;
    throw new Error(body);
  }

  async issue<
    TCred extends HttpCredential = HttpCredential
  >(issueReq: IssueReq): Promise<TCred> {
    const resp = await fetch(new URL("./issue", this.uri), {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify(issueReq)
    });
    const body = await resp.json();
    if (resp.ok) return body;
    throw new Error(body);
  }

  async updateProofs?<
    TCred extends HttpCredential = HttpCredential
  >(cred: TCred): Promise<TCred> {
    const resp = await fetch(new URL("./update-proofs", this.uri), {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify(cred)
    });
    const body = await resp.json();
    if (resp.ok) return body;
    throw new Error(body);
  }

  async browserIssue?<
    TCred extends HttpCredential = HttpCredential
  >({
    challengeReq,
    sign,
    windowOptions,
  }: BrowserIssueParams): Promise<TCred> {
    const challenge = await this.getChallenge(challengeReq);
    if (challenge.verifyURL) {
      const popup = window.open(
        challenge.verifyURL,
        windowOptions?.target,
        windowOptions?.feature
      );
      if (!popup) {
        throw new Error(`Can not open popup window to issue credential, popup URL: ${challenge.verifyURL}`);
      }
      const result = repeatUtil<boolean>(
        (r) => (r instanceof Error) ? true : r,
        1000,
        async () => {
          return (await this.canIssue({ sessionId: challenge.sessionId })).canIssue;
        }
      );
      if (result instanceof Error) throw result;
    }
    const signature = await sign({ message: challenge.message });
    return this.issue({
      sessionId: challenge.sessionId,
      signature: signature
    });
  }


  private get headers(): HeadersInit {
    if (this.accessToken) return {
      "Content-Type": "application/json",
      Authorization: `Bearer ${this.accessToken}`
    };
    return { "Content-Type": "application/json" };
  }
}

Wallet adapter

The wallet adapter facilitates adaptability between cryptocurrency wallets and the necessary zcred properties required for the issuing process.

Interface

export type SignFn = (args: { message: string }) => Promise<string>;

export interface IWalletAdapter {
  /** ZCIP-2 Identifier */
  getSubjectId(): Promise<{type: string; type: string}>;

  /** Blockchain address*/
  getAddress(): Promise<string>;

  /** CAIP-2 chain identifier */
  getChainId(): Promise<string>;

  /** Sign function */
  sign: SignFn;
}

Implementation

Auro wallet implementation example:

export class AuraWalletAdapter implements IWalletAdapter {
  constructor(private readonly provider: IAuroWallet) {
    this.getAddress = this.getAddress.bind(this);
    this.getSubjectId = this.getSubjectId.bind(this);
    this.getChainId = this.getChainId.bind(this);
    this.sign = this.sign.bind(this);
  }

  async getAddress(): Promise<string> {
    const result = (await this.provider.requestAccounts());
    const address = result[0];
    if (address) return address;
    throw new Error(`Mina address is not found`);
  }

  async getSubjectId(): Promise<Identifier> {
    const idType: IdType = "mina:publickey";
    return {
      type: idType,
      key: await this.getAddress()
    };
  }

  async getChainId(): Promise<MinaChainId> {
    const { chainId: chainName } = await this.provider.requestNetwork();
    if (isChainIdName(chainName)) {
      return `mina:${chainName}`;
    }
    await this.switchToMain();
    return "mina:mainnet";
  }

  private async switchToMain(): Promise<ChainName> {
    await this.provider.switchChain({ chainId: "mainnet" });
    return "mainnet";
  }

  async sign(args: { message: string }) {
    const {
      signature: {
        field,
        scalar
      }
    } = await this.provider.signMessage(args);
    return Signature.fromObject({
      r: Field.fromJSON(field),
      s: Scalar.fromJSON(scalar)
    }).toBase58();
  };
}

Credential Verifier

Verify zk-credentials proofs

Interface

export interface ICredentialVerifier {
  /** proof type which verifier provides */
  proofType: string;

  /**
   * Verify zk-credential
   * @param cred zk-credential
   * @param reference proof reference string
   */
  verify<
    TCred extends ZkCredential = ZkCredential
  >(cred: TCred, reference?: string): Promise<boolean>;
}
1.1.1

2 months ago

1.1.0

2 months ago

1.0.2

2 months ago

1.0.1

2 months ago

1.0.0

3 months ago

0.0.4

3 months ago

0.0.2

4 months ago

0.0.1

5 months ago