# eimzo-sign-vue

> 🇺🇿 [O'zbekcha](https://unpkg.com/eimzo-sign-vue/README.uz.md)

> Ready-made **E-IMZO** signing dialog for Vue 2.7 / Vue 3.
> A drop-in modal for signing documents with Uzbekistan's electronic digital
> signature (E-IMZO).

With one line of code you show the user a signing dialog: they pick their
certificate, enter their password — you get back a finished signature (`pkcs7_64`).

```ts
import { useEimzoSign } from 'eimzo-sign-vue'

const { results } = await useEimzoSign().sign('Text to sign')
// results[0].pkcs7_64  ← the signature you send to your server
```

> The Options API's `this.$eimzoSign(...)` works the same way (see below) —
> but only inside a component method, since it relies on `this`. `<script setup>`
> has no `this`, so the `useEimzoSign()` above is always the safe choice.

- Looks like the **official E-IMZO dialog**
- Sign **multiple documents** with a single password
- **4 languages**: Uzbek (Latin/Cyrillic), Russian, English — driven by your app's locale
- No certificate is pre-selected when the modal first opens — the user always picks one explicitly
- Typed errors (`error.code`), never writes to `console`
- Vue 2.7 and Vue 3

---

## Table of contents

- [What is E-IMZO and what this package does](#what-is-e-imzo)
- [Quick start (5 minutes)](#quick-start)
- [Wiring it into a real project](#wiring-it-into-a-real-project)
- [Examples](#examples)
- [Full example projects (GitHub)](#full-example-projects)
- [`createEimzo` options](#createeimzo-options)
- [Result and errors](#result-and-errors)
- [Why isn't it working? (FAQ)](#why-isnt-it-working)
- [Vue 2.7](#vue-27)

---

## What is E-IMZO

**E-IMZO** is Uzbekistan's state electronic digital signature system. The
**E-IMZO app**, installed on the user's computer, talks to the browser over a
local connection and signs documents using a certificate (an ЭРИ key).

**This package** wires that signing flow into your Vue app with one call:

```
You: this.$eimzoSign(document)
        │
        ▼
  ┌───────────────────────────┐
  │  Sign with E-IMZO          │   ← the modal this package renders
  │  ─────────────────────────│
  │  ○ Full name  ...          │   ← the user's own certificates
  │  ○ "SAMPLE" LLC  ...       │
  │  [ Sign ] [ Cancel ]       │
  └───────────────────────────┘
        │  (the password prompt is a native OS/app dialog)
        ▼
  results[0].pkcs7_64  ← the signature. You send this to your server.
```

---

## Quick start

Try it locally right away (make sure the E-IMZO app is running):

**1. Install**

```sh
npm i eimzo-sign-vue
```

**2. Add to `main.ts` (or `main.js`)**

```ts
import { createApp } from 'vue'
import { createEimzo } from 'eimzo-sign-vue'
import App from './App.vue'

const eimzo = createEimzo({
  // A ready-made key for localhost (ships with the E-IMZO SDK). Test use only.
  apiKeys: [
    ['localhost', '96D0C1491615C82B9A54D9989779DF825B690748224C2B04F500F370D51827CE2644D8D4A82C18184D73AB8530BB8ED537269603F61DB0D03D2104ABF789970B'],
  ],
})

createApp(App).use(eimzo).mount('#app')
```

**3. In any component**

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { useEimzoSign } from 'eimzo-sign-vue'

const signature = ref('')

async function doSign() {
  try {
    const { results } = await useEimzoSign().sign('Hello world')
    signature.value = results[0].pkcs7_64
  } catch (e: any) {
    if (e.code !== 'E_CANCELLED') alert(e.message)
  }
}
</script>

<template>
  <button @click="doSign">Sign</button>
  <pre v-if="signature">{{ signature.slice(0, 80) }}…</pre>
</template>
```

Click the button → the modal opens → pick a certificate → "Sign". Done.

> Styles are injected automatically. `import 'eimzo-sign-vue/style.css'` is **not required**.

> **Using Vue 2.7?** Step 2 above (`createApp(...)`) only exists in Vue 3 —
> Vue 2.7 still bootstraps with `new Vue(...)`. A full, ready-to-use `main.js`
> example is in the [Vue 2.7](#vue-27) section. Step 3 (signing with
> `<script setup>`) is **identical** in both and works unchanged.

---

## Wiring it into a real project

### 0. Requirements

| | |
|---|---|
| App | Vue **2.7+** or Vue **3** |
| User's computer | The **E-IMZO app** installed and running |
| You need | An **API key** from E-IMZO for each domain |

### 1. Get an API key

E-IMZO issues a separate key for each domain (`app.bank.uz`, `hemis.uz`,
`localhost`, …). Get a key via [e-imzo.uz](https://e-imzo.uz) or your
organization. For local development, the `localhost` key above is enough.

### 2. `.env`

```ini
VITE_EIMZO_DOMAIN=app.bank.uz
VITE_EIMZO_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

> Don't hardcode the key in your source.

### 3. Configure the plugin

```ts
import { createEimzo } from 'eimzo-sign-vue'

const eimzo = createEimzo({
  apiKeys: [
    ['localhost', '96D0C1491615C82B9A54D9989779DF825B690748224C2B04F500F370D51827CE2644D8D4A82C18184D73AB8530BB8ED537269603F61DB0D03D2104ABF789970B'],
    [import.meta.env.VITE_EIMZO_DOMAIN, import.meta.env.VITE_EIMZO_KEY],
  ],

  // Optional:
  messages: () => appLang.value,          // locale (details below)
  onAudit: (e) => auditLog.push(e),       // audit log
})

app.use(eimzo)
```

### 4. Sign

```ts
import { useEimzoSign } from 'eimzo-sign-vue'

const { sign } = useEimzoSign()

async function submit(xml: string) {
  try {
    const { results } = await sign(xml)
    // results[0] = { id, pkcs7_64, signature_hex, signer_serial_number }
    await fetch('/api/sign', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ pkcs7: results[0].pkcs7_64 }),
    })
  } catch (e: any) {
    if (e.code === 'E_CANCELLED') return                 // user cancelled
    if (e.code === 'E_NOT_INSTALLED') alert('Please open the E-IMZO app')
    else alert('Signing failed: ' + e.message)
  }
}
```

In the Options API, `this.$eimzoSign(...)` works the same way.

### 5. Verify on the server — MANDATORY

This package only produces a signature (`pkcs7_64`); it does **not authenticate
anyone**. Your server must verify:

- the signature's validity and the certificate chain,
- that the certificate hasn't been revoked (OCSP/CRL),
- that the signer (`signer_serial_number` / TIN / PINFL) actually belongs to
  **the user who is logged in**.

In Uzbekistan this is typically done by an E-IMZO/DSKM backend.

### Checklist

- [ ] `npm i eimzo-sign-vue`
- [ ] `createEimzo({ apiKeys })` + `app.use(eimzo)`
- [ ] The correct API key for your domain
- [ ] `E_CANCELLED` and `E_NOT_INSTALLED` are handled
- [ ] `pkcs7_64` is sent to and verified on the server
- [ ] Tested with the E-IMZO app running

---

## Examples

### Signing multiple documents at once

The password is asked **once** — everything is signed with a single certificate.

```ts
const { results } = await sign([
  { id: 'p1', title: 'Payment order #1', content: xml1 },
  { id: 'p2', title: 'Payment order #2', content: xml2 },
  { id: 'p3', title: 'Contract appendix', content: xml3 },
])

for (const r of results) {
  await api.saveSignature(r.id, r.pkcs7_64)   // { id, pkcs7_64, ... }
}
```

### Showing the user what they're signing

If you provide `preview` and enable `show-documents`, the modal shows the
document details:

```ts
await sign([{
  id: 'p1',
  title: 'Payment order #42',
  content: xml,
  preview: [
    { label: 'Amount',      value: '1,500,000 UZS' },
    { label: 'Recipient',   value: 'ACME LLC' },
    { label: 'Account',     value: '2020 8000 9000 1234 5678' },
  ],
}])
```

```vue
<EimzoSignModal show-documents ... />
```

### Partially signed state

If the user signs several documents in a batch and cancels partway through,
don't lose what was already signed:

```ts
try {
  const { results } = await sign(docs)
  await api.saveAll(results)
} catch (e: any) {
  if (e.code === 'E_CANCELLED') {
    if (e.partial) await api.saveAll(e.partial)   // whatever was signed is here
    return
  }
  throw e
}
```

### A declarative component (instead of the imperative call)

```vue
<script setup>
import { ref } from 'vue'
import { EimzoSignModal, useEimzoSign } from 'eimzo-sign-vue'

const show = ref(false)
const eimzo = useEimzoSign()
const docs = [{ id: 'a', title: 'Application', content: '<xml/>' }]

function onSigned({ results }) {
  show.value = false
  // ... do something with results
}
</script>

<template>
  <button @click="show = true">Sign</button>

  <EimzoSignModal
    v-if="show"
    :client="eimzo.client"
    :docs="docs"
    messages="uz-latn"
    @signed="onSigned"
    @cancelled="show = false"
  />
</template>
```

### `<EimzoSignModal>` props and events

**Input (props):**

| Prop | Type | Required | Description |
|---|---|---|---|
| `client` | `EimzoClient` | ✅ | `useEimzoSign().client`, or an `EimzoClient` instance you created yourself |
| `docs` | `SignRequestDoc[]` | ✅ | The list of documents to sign (shape below) |
| `messages` | `string \| object \| function` | — | Locale ([see above](#locale)). Default: the browser's language |
| `auto-install` | `boolean` | — | Calls `client.install()` automatically. Default `true` |
| `show-documents` | `boolean` | — | Show document content (WYSIWYS, `preview`) in the modal. Default `false` |
| `idle-timeout-ms` | `number` | — | Auto-cancel after this many ms of inactivity. Default `120000`, `0` disables it |
| `theme` | `'light' \| 'dark'` | — | Force a theme. Default: the browser's theme |
| `inject-styles` | `boolean` | — | Set `false` to import `eimzo-sign-vue/style.css` yourself instead. Default `true` |

Each item in the `docs` array (`SignRequestDoc`):

| Field | Required | Description |
|---|---|---|
| `id` | ✅ | A stable identifier used to match the result back to the document |
| `title` | ✅ | User-visible title (e.g. "Payment order #123") |
| `content` | ✅ | The **actual** content being signed — see below |
| `preview` | — | `{ label, value }[]` — details shown when `show-documents` is on |

**What goes in `content`?**

Unlike `title`/`preview`, which are display-only, E-IMZO signs exactly this string:

- **Plain text, XML, or JSON** — the common case; pass it directly, with no
  pre-encoding: `content: xml` or `content: JSON.stringify(obj)`.
- **A base64 string** — if the document is a binary file (PDF, image, docx).
  A JS string can't hold raw binary, so convert the file to base64 **once,
  yourself** first and pass that string.

Either way, the library base64-encodes this string **one more time itself**
before sending it to E-IMZO (an SDK requirement) — so don't pre-encode plain
text/XML/JSON yourself, or it ends up double-encoded.

> When verifying on the server, this exact `content` (as given for signing)
> is compared again — the document sent to the modal and the one sent to the
> server must match, or the signature isn't considered valid.

**Output (events):**

| Event | Payload | When it fires |
|---|---|---|
| `@signed` | `{ results: SignResult[] }` — shape in [Result and errors](#result-and-errors) | After every document in the list has been signed successfully |
| `@cancelled` | `EimzoError` (`.code`, sometimes `.partial`) — codes in the table in that section | When the user cancels, or a fatal error occurs |

After either event you must close the modal yourself (e.g. set `v-if="show"`
to `false`) — the modal never closes itself.

### Locale

`messages` accepts a string, an alias, a function, or your own text:

```ts
// static
createEimzo({ apiKeys, messages: 'ru' })

// dynamic — the modal updates instantly when your app's language changes
createEimzo({ apiKeys, messages: () => i18n.global.locale.value })
```

| Value | Result |
|---|---|
| `'uz-latn'` `'uz-cyrl'` `'ru'` `'en'` | a built-in locale |
| alias: `la` `uz` `ru` `en` `cyrl` `lat` … | `la` → Latin, `uz` → Cyrillic, etc. |
| `() => localeCode` | dynamic (works with a reactive `locale`) |
| an `EimzoMessages` object | every string is your own / brand wording |
| omitted | detected from `<html lang>` → the browser's language |

`import { uzLatn, ru, en, EimzoMessages } from 'eimzo-sign-vue'` — you can
take one of the built-in objects and override individual fields.

### Without a modal — signing logic only

If you don't need a dialog (you have your own UI), use the lower-level package:

```sh
npm i eimzo-sign-core
```

```ts
import { EimzoClient } from 'eimzo-sign-core'

const eimzo = new EimzoClient({ apiKeys: [[domain, key]] })
await eimzo.install()
const certs = await eimzo.listCerts()             // render your own list
const res = await eimzo.sign(certs[0], xml)       // { pkcs7_64, ... }
```

Details: **[eimzo-sign-core](https://www.npmjs.com/package/eimzo-sign-core)**.

---

## Full example projects

If you want full, runnable (`npm install` + `npm run dev`) projects instead
of the snippets above — each comes with its own `README.md` covering setup,
errors, and troubleshooting:

- **eimzo-sign-vue3-example**
  — Vue 3, `<script setup>`, TypeScript, Vite. Uses the npm package directly
  with no extra configuration.
- **eimzo-sign-vue2-example**
  — Vue 2.7, Options API, Vite. Shows a WYSIWYS signing flow where the user
  picks a document from a table and signs it declaratively with
  `<EimzoSignModal :show-documents="true">`.

---

## `createEimzo` options

| Option | Description |
|---|---|
| `apiKeys` | **Required.** `[['domain', 'key'], …]` — issued by E-IMZO |
| `messages` | Locale (see above). Default: the browser's language |
| `onAudit` | `(event) => void` — an audit hook. The package never logs to `console` |
| `idleTimeoutMs` | Auto-close the modal after this many ms of inactivity. Default `120000` |
| `timeoutMs` | Wait time for each E-IMZO call. Default `30000` |
| `injectStyles` | Set `false` to import `eimzo-sign-vue/style.css` yourself |
| `theme` | `'light'` / `'dark'` — force a theme (default: the browser's theme) |

Some of these can be overridden per call:
`sign(docs, { messages: 'ru', theme: 'dark' })`.

Match your brand's colors with CSS variables:

```css
.eimzo-modal-root {
  --eimzo-primary: #0a7c3f;
  --eimzo-header-bg: #0a7c3f;
  --eimzo-radius: 6px;
}
```

---

## Result and errors

### `sign()` result

```ts
{
  results: [
    {
      id: 'p1',                     // the id you passed in (or 'single' for one document)
      pkcs7_64: 'MIIH…',            // the signature (base64) — send THIS to your server
      signature_hex: '3045…',
      signer_serial_number: '…',    // the serial number of the signing certificate
    },
  ]
}
```

### Errors — check `error.code` (never the message text)

| `code` | Meaning | What to do |
|---|---|---|
| `E_NOT_INSTALLED` | The E-IMZO app isn't running | Tell the user to open the app |
| `E_VERSION_LOW` | E-IMZO is out of date | "Please update" |
| `E_NO_CERTS` | No certificate found | "Plug in / configure your key" |
| `E_KEY_LOAD_CANCELLED` | The password prompt was closed | Return silently |
| `E_CANCELLED` | The user clicked "Cancel" | Return silently (may carry `.partial`) |
| `E_SERIAL_MISMATCH` | The signing certificate didn't match the one selected | Retry |
| `E_SIGN_FAILED` | Signing failed | Retry |
| `E_TIMEOUT` | E-IMZO didn't respond | Retry |
| `E_API_KEY_REJECTED` | The domain key is wrong | Check your configuration |

```ts
import { EimzoError } from 'eimzo-sign-vue'

if (EimzoError.is(e, 'E_NOT_INSTALLED')) {
  showBanner('Please start the E-IMZO app and refresh the page')
}
```

---

## Why isn't it working?

**I click the button and nothing happens**
Open the console. If it's `E_NOT_INSTALLED`, the E-IMZO app isn't open.
Check that `createEimzo` was called and `app.use(eimzo)` was run.

**`E_NOT_INSTALLED` even though the app is open**
E-IMZO may be an old version (`E_VERSION_LOW`). Or the browser is blocking
the local `wss://127.0.0.1:64443` connection (antivirus / corporate proxy).

**`E_API_KEY_REJECTED`, or no certificates show up**
The domain in `apiKeys` must exactly match the domain the site is opened on
(`localhost` ≠ `127.0.0.1`).

**pnpm can't find `eimzo-sign-core`**
If you import from it directly, also add `npm i eimzo-sign-core` (pnpm's
strict mode doesn't expose transitive packages).

**`renderList is not defined` / `openBlock` error on Vue 2.7**
`postinstall` didn't wire up the Vue 2 build — usually because the package
was installed with `--ignore-scripts`, or you're on **pnpm 9/10** (which
blocks build scripts by default) / **Yarn Berry (PnP)**. Quick fix:
`import { createEimzo } from 'eimzo-sign-vue/vue2'` — an explicit subpath
always picks the right build. The lasting pnpm fix: approve build scripts
for `eimzo-sign-vue` and `vue-demi` with `pnpm approve-builds`. Details —
[Vue 2.7](#vue-27).

**CSP error**
Add `ws://127.0.0.1:64646 wss://127.0.0.1:64443` to `connect-src`. If inline
styles are blocked: `createEimzo({ injectStyles: false })` +
`import 'eimzo-sign-vue/style.css'`.

**Error when importing during SSR (Nuxt)**
Load the modal client-side only (`<client-only>` / `onMounted`).

---

## Vue 2.7

`npm i eimzo-sign-vue` — **works with no extra configuration.** The only
difference is how you bootstrap the app in `main.js` (Vue 2.7 has no
`createApp`, it still uses `new Vue(...)`):

```js
// main.js
import Vue from 'vue'
import { createEimzo } from 'eimzo-sign-vue'
import App from './App.vue'

const eimzo = createEimzo({
  apiKeys: [
    ['localhost', '96D0C1491615C82B9A54D9989779DF825B690748224C2B04F500F370D51827CE2644D8D4A82C18184D73AB8530BB8ED537269603F61DB0D03D2104ABF789970B'],
  ],
})

Vue.use(eimzo)

new Vue({
  render: (h) => h(App),
}).$mount('#app')
```

Everything after that — signing (`useEimzoSign()`, `this.$eimzoSign(...)`,
`<script setup>`, `<EimzoSignModal>`) — is **identical** to Vue 3; all the
examples above work unchanged.

The package ships with **two separate builds** for Vue 2.7 and Vue 3
(`dist/vue2/`, `dist/vue3/`). At install time, the `postinstall` script
detects the installed Vue version and wires up the right build. The import
stays the same either way:

```ts
import { createEimzo, EimzoSignModal } from 'eimzo-sign-vue'
```

**When `postinstall` doesn't run** — if the package was installed with
`--ignore-scripts`, or you're on **pnpm 9/10** (which blocks build scripts by
default) / **Yarn Berry (PnP)** — auto-detection may not run, and the Vue 3
build gets wired up by default (no error, but it won't work in a Vue 2.7
project). In that case, import from the **explicit subpath** for Vue 2.7 —
this always works correctly:

```ts
import { createEimzo } from 'eimzo-sign-vue/vue2'   // or '/vue3'
```

> On pnpm 9/10: approve build scripts for `eimzo-sign-vue` and `vue-demi`
> with `pnpm approve-builds` (or add them to your project's
> `pnpm.onlyBuiltDependencies` list in `package.json`) — otherwise `vue-demi`
> itself may also misdetect the Vue version.

If you don't need a modal, just the state machine — `SignFlow` lives in
`eimzo-sign-core` (`npm i eimzo-sign-core`), which has no dependency on Vue at all.

> A full, runnable Vue 2.7 project (Options API, WYSIWYS signing with a
> document picked from a table) —
> **eimzo-sign-vue2-example**.
> The same example for Vue 3 —
> **eimzo-sign-vue3-example**.

---

## Security

- **Verify on your server** — the package produces a signature, it doesn't confirm identity.
- Don't hardcode `apiKeys` in your source — load them from `.env`.
- The package never writes to `console`; use `onAudit` for observability
  (only `serial` / `docId`, no personal data).
- The bundled E-IMZO SDK is pinned by its SHA-256 hash.

## License

MIT
