import type { ApiClient, C2FResponse, CubeSignerResponse, EditPolicy, Empty, InvokeC2FResponse, JsonValue, KeyPolicy, KeyPolicyRule, MfaReceipts, PolicyAttachedToId, PolicyType, RolePolicy, RolePolicyRule, UpdatePolicyRequest, WasmRule, AceAttribute, PolicyAction, Ace, PolicyInfo, } from "./index.ts"; import { loadSubtleCrypto, encodeToHex, decodeFromHex } from "./index.ts"; /** * Named policy rule type. */ export type PolicyRule = KeyPolicyRule | RolePolicyRule | WasmRule; /** * A helper type for {@link PolicyInfo} with a more detailed `acl` type. */ type NamedPolicyInfo = PolicyInfo & { acl?: PolicyAcl; }; /** * The policy info for a named key policy. */ export type KeyPolicyInfo = NamedPolicyInfo & { policy_type: "Key"; }; /** * The policy info for a named role policy. */ export type RolePolicyInfo = NamedPolicyInfo & { policy_type: "Role"; }; /** * The policy info for a named wasm policy. */ export type WasmPolicyInfo = NamedPolicyInfo & { policy_type: "Wasm"; }; /** * The policy info for a Confidential Cloud Function. */ export type C2FInfo = WasmPolicyInfo; /** * A helper type for valid named policy version strings. */ export type Version = `v${number}` | `latest`; /** A policy access control entry. */ export type PolicyAce = Ace; /** A policy access control list. */ export type PolicyAcl = PolicyAce[]; /** Additional contexts when using policies. */ export type PolicyCtx = { /** * The resources (keys, roles, and key-in-roles) that the access control entry * applies to. */ resources?: AceAttribute; }; /** A resource a policy is invoked with or attached to. */ export type PolicyResource = /** A key or role id. */ | string /** Keys attached to roles. */ | { key_ids: "*" | string[]; role_ids: "*" | string[] }; /** * Upload the given Wasm Confidential Cloud Function. * * @param apiClient The API client to use. * @param policy The Wasm function. * @returns The Wasm function object hash to use for creating/updating C2F policies. * @throws if uploading the policy fails. * @internal */ export async function uploadWasmFunction( apiClient: ApiClient, policy: Uint8Array, ): Promise { // get the SHA-256 hash of the function to get the upload url. const subtle = await loadSubtleCrypto(); const hashBytes = await subtle.digest("SHA-256", policy); const hash = encodeToHex(new Uint8Array(hashBytes)); // get the upload URL const { signed_url } = await apiClient.wasmPolicyUpload({ hash }); // upload the wasm object const resp = await fetch(signed_url, { method: "PUT", body: policy, }); if (!resp.ok) { throw new Error(`Failed to upload function with status: ${resp.status}: ${resp.statusText}`); } return hash; } /** * Upload the given Wasm policy. * * @param apiClient The API client to use. * @param policy The Wasm function. * @returns The Wasm function object hash to use for creating/updating C2F policies. * @throws if uploading the policy fails. * @internal */ export const uploadWasmPolicy = uploadWasmFunction; /** * Abstract class for shared methods between key, role and Wasm policies. */ export abstract class NamedPolicy { protected readonly apiClient: ApiClient; protected data: NamedPolicyInfo; /** * Helper method for creating a named policy from a policy info. * * @param apiClient The api client to use. * @param info The policy info. * @returns The named policy object for the given info. */ static fromInfo(apiClient: ApiClient, info: PolicyInfo): NamedPolicy { switch (info.policy_type) { case "Key": return new NamedKeyPolicy(apiClient, info as KeyPolicyInfo); case "Role": return new NamedRolePolicy(apiClient, info as RolePolicyInfo); case "Wasm": return new C2FFunction(apiClient, info as C2FInfo); } } /** @returns The policy id */ get id(): string { return this.data.policy_id; } /** @returns The policy type */ get policyType(): PolicyType { return this.data.policy_type; } /** * Get a specific version of the policy. * * @param version The policy version to get. * @returns The specific version of the policy. */ async version(version: Version): Promise { let versionInfo; if (version == `v${this.data.version}`) { versionInfo = this.data; } else { versionInfo = (await this.apiClient.policyGet(this.id, version)) as NamedPolicyInfo; } return new NamedPolicyRules(this.apiClient, versionInfo); } /** * Get the latest version of the policy. * * @returns The latest version of the policy. */ async latest(): Promise { const data = await this.fetch("latest"); return new NamedPolicyRules(this.apiClient, data); } /** * Fetch and return the current name of the policy. * * @returns The policy name. */ async name(): Promise { const data = await this.fetch(); return data.name; } /** * Set a new name for the policy. * * @param name The new policy name. * @param mfaReceipt Optional MFA receipt(s). * @throws if MFA is required and no receipts are provided */ async setName(name: string, mfaReceipt?: MfaReceipts) { await this.update({ name }, mfaReceipt); } /** * Fetch and return the current owner of the policy. * * @returns The user id of the policy owner. * @example User#c3b9379c-4e8c-4216-bd0a-65ace53cf98f */ async owner(): Promise { const data = await this.fetch(); return data.owner; } /** * Set a new owner for the policy. * * @param owner The new owner of the policy. * @param mfaReceipt Optional MFA receipt(s). * @throws if MFA is required and no receipts are provided */ async setOwner(owner: string, mfaReceipt?: MfaReceipts) { await this.update({ owner }, mfaReceipt); } /** * Fetch and return the metadata value for the policy. * * @returns The policy metadata. */ async metadata(): Promise { const data = await this.fetch(); return data.metadata as JsonValue; } /** * Sets a new metadata value for the named policy (overwriting the existing value). * * @param metadata The new metadata for the named policy. * @param mfaReceipt Optional MFA receipt(s). * @throws if MFA is required and no receipts are provided */ async setMetadata(metadata: JsonValue, mfaReceipt?: MfaReceipts) { await this.update({ metadata }, mfaReceipt); } /** * Fetch and return the edit policy for the named policy. * * @returns The edit policy for this named policy. */ async editPolicy(): Promise { const data = await this.fetch(); return data.edit_policy; } /** * Set a new edit policy for the named policy. * * @param editPolicy The new edit policy. * @param mfaReceipt Optional MFA receipt(s). * @throws if MFA is required and no receipts are provided */ async setEditPolicy(editPolicy: EditPolicy, mfaReceipt?: MfaReceipts) { await this.update({ edit_policy: editPolicy }, mfaReceipt); } /** * Fetch and return the access control entries for the named policy. * * @returns The access control entries for this named policy. */ async acl(): Promise { const data = await this.fetch(); return data.acl; } /** * Sets new access control entries for the named policy (overwriting the existing entries). * * @param acl The access control entries to set. * @param mfaReceipt Optional MFA receipt(s). * @throws if MFA is required and no receipts are provided */ async setAcl(acl: PolicyAcl, mfaReceipt?: MfaReceipts) { await this.update({ acl }, mfaReceipt); } /** * @returns a list of all keys, roles, and key-in-roles that all versions of this policy * are attached to. */ async allAttached(): Promise { // there is no single-call way to achieve this. So instead, we // 1. Get the latest version of the policy // 2. For all versions `v0` to `latest`, fetch the policy info // 3. Join all policy `attached_to` arrays const data = await this.fetch("latest"); const latest = data.version; const versions = Array.from(Array(latest + 1).keys()); const batchSize = 10; let allAttached: PolicyAttachedToId[] = []; for (let batch = 0; batch < versions.length; batch += batchSize) { const rs = await Promise.all( versions.slice(batch, batch + batchSize).map((version) => { return this.apiClient.policyGet(this.id, `v${version}`); }), ); allAttached = allAttached.concat(rs.flatMap((policy) => policy.attached_to)); } return allAttached.concat(data.attached_to); } /** * Delete this policy. * * This can fail if the policy is still attached to any key, role, or key in role. * * @param mfaReceipt Optional MFA receipt(s). * @returns A response which can be used to approve MFA if needed * @throws if MFA is required and no receipts are provided */ async delete(mfaReceipt?: MfaReceipts): Promise> { return await this.apiClient.policyDelete(this.id, mfaReceipt); } // -------------------------------------------------------------------------- // -- INTERNAL -------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Constructor. * * @param apiClient The API client to use. * @param data The JSON response from the API server. * @internal */ protected constructor(apiClient: ApiClient, data: NamedPolicyInfo) { this.apiClient = apiClient; this.data = data; } /** * Update the policy. * * @param request The JSON request to send to the API server. * @param mfaReceipt Optional MFA receipt(s). * @returns The updated policy information. * @throws if MFA is required and no receipts are provided * @internal */ protected async update( request: UpdatePolicyRequest, mfaReceipt?: MfaReceipts, ): Promise { const resp = await this.apiClient.policyUpdate(this.id, request, mfaReceipt); this.data = resp.data() as NamedPolicyInfo; return this.data; } /** * Fetch the policy information. * * @param version The version of the policy to fetch. Defaults to "latest". * @returns The policy information. * @internal */ protected async fetch(version: Version = "latest"): Promise { this.data = (await this.apiClient.policyGet(this.id, version)) as NamedPolicyInfo; return this.data; } } /** * A representation of a named key policy. */ export class NamedKeyPolicy extends NamedPolicy { override data: KeyPolicyInfo; /** * Update the policy with new rules. * * @param rules The new rules to update the policy with. * @param mfaReceipt Optional MFA receipt(s). * @throws if MFA is required and no receipts are provided */ async setRules(rules: KeyPolicy, mfaReceipt?: MfaReceipts) { await this.update({ rules }, mfaReceipt); } // -------------------------------------------------------------------------- // -- INTERNAL -------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Constructor. * * @param apiClient The API client to use. * @param data The JSON response from the API server. * @internal */ constructor(apiClient: ApiClient, data: KeyPolicyInfo) { super(apiClient, data); this.data = data; } } /** * A representation of a named role policy. */ export class NamedRolePolicy extends NamedPolicy { override data: RolePolicyInfo; /** * Update the policy with new rules. * * @param rules The new rules to update the policy with. * @param mfaReceipt Optional MFA receipt(s). * @throws if MFA is required and no receipts are provided */ async setRules(rules: RolePolicy, mfaReceipt?: MfaReceipts) { await this.update({ rules }, mfaReceipt); } // -------------------------------------------------------------------------- // -- INTERNAL -------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Constructor. * * @param apiClient The API client to use. * @param data The JSON response from the API server. * @internal */ constructor(apiClient: ApiClient, data: RolePolicyInfo) { super(apiClient, data); this.data = data; } } /** * A representation of a Confidential Cloud Function (C2F). * * This class extends NamedPolicy because C2F functions can be attached * to keys and roles like a named policy. */ export class C2FFunction extends NamedPolicy { override data: C2FInfo; /** * Update this C2F function with a new Wasm function. * * @param policy The new Wasm function. * @param mfaReceipt Optional MFA receipt(s). * @throws if uploading the function fails. * @throws if MFA is required and no receipts are provided. */ async setWasmFunction(policy: Uint8Array, mfaReceipt?: MfaReceipts) { // upload the policy object const hash = await uploadWasmFunction(this.apiClient, policy); // update this policy with the new policy version. const body: UpdatePolicyRequest = { rules: [{ hash }] }; this.data = (await this.update(body, mfaReceipt)) as C2FInfo; } /** * Invoke this Confidential Cloud Function. * * @param keyId The optional key id that the function will be invoked with. * @param version The version of the function to invoke. Defaults to "latest". * @param request The optional sign request body that will be sent to the function. * @param roleId The optional role id that the function will be invoked by. * If `undefined`, the policy will be invoked by the user session. * @returns The result of invoking the function. */ async invoke( keyId?: string, version: Version = "latest", request?: JsonValue, roleId?: string, ): Promise { // TODO Ideally, `version` should be the first parameter. But for backwards // compatibility, we keep `keyId` as the first parameter for now. const resp = await this.apiClient.policyInvoke(this.id, version, { key_id: keyId, request, role_id: roleId, }); return new PolicyInvocation(resp); } // Backwards compability with Named Wasm Policy names /** * Update the policy with the new Wasm policy. * * @param policy The new Wasm policy object. * @param mfaReceipt Optional MFA receipt(s). * @throws if uploading the policy object fails. * @throws if MFA is required and no receipts are provided. */ setWasmPolicy = this.setWasmFunction; // -------------------------------------------------------------------------- // -- INTERNAL -------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Constructor. * * @param apiClient The API client to use. * @param data The JSON response from the API server. * @internal */ constructor(apiClient: ApiClient, data: C2FInfo) { super(apiClient, data); this.data = data; } } /** * A specific version of a named policy. */ export class NamedPolicyRules { /** The CubeSigner instance that this policy is associated with */ readonly #apiClient: ApiClient; #data: NamedPolicyInfo; /** * @returns The ID of the policy. * * @example NamedPolicy#a4a45cc2-0642-4c98-b6bd-0da347d608a4 */ get id(): string { return this.#data.policy_id; } /** * @returns The version of the policy this object contains. */ get version(): Version { return `v${this.#data.version}`; } /** * @returns The policy rules. * * @example [ "AssertErc20Tx" ] */ get rules(): PolicyRule[] { return this.#data.rules as PolicyRule[]; } /** * @returns a list of all keys, roles, and key-in-roles this version of the policy * is attached to. */ async allAttached(): Promise { const data = await this.fetch(); return data.attached_to; } // -------------------------------------------------------------------------- // -- INTERNAL -------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Constructor. * * @param apiClient The API client to use. * @param data The JSON response from the API server. * @internal */ constructor(apiClient: ApiClient, data: NamedPolicyInfo) { this.#apiClient = apiClient; this.#data = data; } /** * Fetch the policy information. * * @returns The policy information. * @internal */ private async fetch(): Promise { this.#data = (await this.#apiClient.policyGet(this.id, this.version)) as NamedPolicyInfo; return this.#data; } } /** * The result of invoking a Confidential Cloud Function. */ export class C2FInvocation { readonly #data: InvokeC2FResponse; /** @returns The policy response itself. */ get response(): C2FResponse { return this.#data.response; } /** * The standard output stream as raw bytes. Usually a UTF-8 encoded string, * use {@link TextDecoder} to decode. * * @returns The standard output stream. */ get stdoutBytes(): Uint8Array { return decodeFromHex(this.#data.stdout); } /** * The standard error stream as raw bytes. Usually a UTF-8 encoded string, use * {@link TextDecoder} to decode. * * @returns The standard error stream. */ get stderrBytes(): Uint8Array { return decodeFromHex(this.#data.stderr); } /** * The standard output stream as a Buffer. Usually a UTF-8 encoded string. * * @returns The standard output stream. * @deprecated Use {@link stdoutBytes} instead for browser compatibility. */ get stdout(): Buffer { // eslint-disable-next-line no-restricted-globals -- Buffer return type preserved for backwards compatibility return Buffer.from(this.stdoutBytes); } /** * The standard error stream as a Buffer. Usually a UTF-8 encoded string. * * @returns The standard error stream. * @deprecated Use {@link stderrBytes} instead for browser compatibility. */ get stderr(): Buffer { // eslint-disable-next-line no-restricted-globals -- Buffer return type preserved for backwards compatibility return Buffer.from(this.stderrBytes); } /** * Constructor. * * @param data The JSON response from the API server. * @internal */ constructor(data: InvokeC2FResponse) { this.#data = data; } } // Backwards compability with Named Wasm Policy names /** A representation of a Wasm policy. */ export type NamedWasmPolicy = C2FFunction; /** A representation of a Wasm policy. */ export const NamedWasmPolicy = C2FFunction; /** The result of invoking a named WASM policy. */ export type PolicyInvocation = C2FInvocation; /** The result of invoking a named WASM policy. */ export const PolicyInvocation = C2FInvocation;