Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,11 @@ Rules that the templates above don't imply.
- **Signatures**: the request page puts the PDF viewer (pdf.js, so every browser renders the same) beside the
signers, delivery and activity cards; the signer's box is blue and draggable, other pending signers' boxes are dashed.
Signature boxes use the light palette in both themes, because they sit on white paper. "Sign with CIE" opens the
PoliNetwork eSign's own page in a popup: the PIN is typed there, never in the dashboard.
PoliNetwork eSign's own page in a popup: the PIN is typed there, never in the dashboard. A person appears once
however many places they sign: their places are buttons under their name ("Signature · p. 2", "Mark · p. 1") whose
menu switches the kind, moves or removes the place and rings its box on the page; marks have a dashed border.
With several documents waiting, "Sign N together" opens a strip of the documents in the page bar's toolbar row: each
is checked by opening it (a check replaces its number), can be left out, and the primary action stays "Next
document" until all are checked, then becomes "Sign N documents" (one agent window, one PIN).
- **Account**: permissions are grouped by area (Dashboard, Telegram, WhatsApp, group labels, Microsoft 365, Web) with
the service icon.
9 changes: 9 additions & 0 deletions docs/esign/TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ Legend: ☐ todo · ◐ in progress · ☑ done
`/tmp/esign-review-r2b/`): round-1 fixes confirmed; new findings fixed — ByteRange gap must be exactly /Contents,
change policy for uploaded signatures (`pdf/diff.ts`, checked against a pyHanko signature), retry lease, per-signer
email size budget, bounded OCSP read, fresh popup per signature, delivery polling, scope switch.
- ☑ Several OneDrive/SharePoint drives (`ESIGN_DRIVES`, backend#74, admin#104).
- ☑ Several places per signer: each place a full signature (one PDF signature each) or a mark (the first signature's
mark, covered by it); the person appears once in the dashboard (backend#75, admin PR, sign-agent#6).
- ☑ Sign several documents with one PIN: "Sign N together" in To sign opens a document strip to check each, then one
agent window signs them all (agent protocol 2; older agents are asked to update).
- ☐ Real-card check that one PIN can make several signatures in a session (`cie-mw`, `opencie`, built-in `cie`).
- ☐ Docs: user guide (install agent, pair CIE), ops (Graph permissions, IdP permissions, env).

## Manual steps for the maintainer (cannot be automated here)
Expand Down Expand Up @@ -151,6 +157,9 @@ Open `http://localhost:10431/dashboard/esign/requests` (use `localhost`, not `12

## Log

- 2026-10-17: several places per signer and batch signing with one PIN (backend#75, sign-agent#6, admin PR); merge
order: backend → bump admin → agent release v0.2.0 together with the backend deploy (protocol 2).

- 2026-10-17: backend#73 merged (released @polinetwork/backend 1.7.0), admin#102 merged; agent v0.1.1; code-signing
pipeline merged (esign #4).

Expand Down
35 changes: 19 additions & 16 deletions docs/esign/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ Dashboard Admin BFF Backend

Listens on `127.0.0.1:47321` only. Every request: `Host` must be `127.0.0.1:47321` or `localhost:47321`.

| Route | Caller | Notes |
| ----------------- | -------------------- | --------------------------------------------------------------------------------------- |
| `GET /` | anyone | "Agent is running" page |
| `GET /sign` | popup navigation | Query `ticket` (43 chars base64url), `origin` (must be in the allowlist). Serves the UI |
| `GET /api/job` | the agent's own page | Same-origin only (`Origin`/`Sec-Fetch-Site` checks). Job + card status |
| `POST /api/sign` | the agent's own page | `{ticket, origin, pin}`; runs the whole signature; returns the outcome |
| `GET /api/status` | the agent's own page | Providers, readers, card presence |
| Route | Caller | Notes |
| ----------------- | -------------------- | ------------------------------------------------------------------------------------ |
| `GET /` | anyone | "Agent is running" page |
| `GET /sign` | popup navigation | Query `ticket` repeated 1–10 times (43 chars base64url each), `origin` (allowlisted) |
| `GET /api/job` | the agent's own page | Same-origin only (`Origin`/`Sec-Fetch-Site` checks). Job + card status |
| `POST /api/sign` | the agent's own page | `{tickets, origin, pin}`: one card session signs every job; a result per ticket |
| `GET /api/status` | the agent's own page | Providers, readers, card presence |

Allowed origins: `https://admin.polinetwork.org` by default, `http://localhost:*` with `--dev`, or the config file.

Expand All @@ -55,12 +55,12 @@ Providers (tried in order, first with a card present; never falls back once a PI
Agent → backend (through the dashboard origin, `{origin}/api/esign/agent/…`, proxied by the BFF to the backend's
`/esign/agent/…`). Authentication is the ticket in the path; no cookies, no bearer tokens.

| Request | Body | Response |
| ---------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/esign/agent/jobs/{ticket}` | | `{job: {id, status, expiresAt, document: {title, fileName, pages}, signer: {name, role}, placement: {page}, requestedBy}}` |
| `POST /api/esign/agent/jobs/{ticket}/prepare` | `{certificate: b64 DER, chain?: b64[]}` | `{toBeSigned: b64 SHA-256, algorithm: "RSASSA-PKCS1-v1_5-SHA256"}` |
| `POST /api/esign/agent/jobs/{ticket}/complete` | `{signature: b64}` | `{status: "completed", request: {status, remaining}}` |
| `POST /api/esign/agent/jobs/{ticket}/fail` | `{code, message?}` | `{status}` — records cancellations / card errors for the dashboard |
| Request | Body | Response |
| ---------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/esign/agent/jobs/{ticket}` | | `{job: {id, status, expiresAt, document: {title, fileName, pages}, signer: {name, role}, placement: {page}, requestedBy, signatures, marks, pages}}` |
| `POST /api/esign/agent/jobs/{ticket}/prepare` | `{certificate: b64 DER, chain?: b64[], protocol: 2}` | `{toBeSigned: b64 SHA-256, algorithm: "RSASSA-PKCS1-v1_5-SHA256", step, steps}`; without `protocol: 2`, 426 `agent_outdated` |
| `POST /api/esign/agent/jobs/{ticket}/complete` | `{signature: b64}` | `{status: "next" \| "completed", request: {status, remaining}}`: on `next`, prepare the job's next place with the same session |
| `POST /api/esign/agent/jobs/{ticket}/fail` | `{code, message?}` | `{status}` — records cancellations / card errors for the dashboard |

Errors: `{error: {code, message}}` with codes `ticket_invalid` (404), `job_expired` (410), `job_done` (409),
`document_changed` (409), `certificate_invalid` (422), `signer_mismatch` (403), `signature_invalid` (422).
Expand All @@ -77,11 +77,14 @@ browse OneDrive). Requests are visible to their signers and creator.
Item IDs are only unique within their drive, so every drive call names one.
- `esign.documents.upload(FormData{file})` / `esign.documents.fromDrive({driveId, itemId})` → `{documentId, fileName, pages: [{width,height}], signatures, sha256}`.
- `esign.documents.content({documentId})` → `{base64}` (a staged or request document version).
- `esign.requests.create({title, documentId, signers: [{sub, role, placement}], ordered, output: {driveId, folderId, fileName}, email?: {to[], message?}})`.
- `esign.requests.create({title, documentId, signers: [{sub, role, places: [{...placement, kind}]}], ordered, output: {driveId, folderId, fileName}, email?: {to[], message?}})`.
`kind` is `signature` (its own PDF signature) or `mark` (the first signature's mark, drawn in that signature's
update); each signer has 1–20 places, at least one a signature, and appears once in the request.
- `esign.requests.list({scope: "inbox"|"mine"|"all", status?})`, `esign.requests.get({id})`.
- `esign.requests.cancel({id})`, `esign.requests.decline({id, reason})`, `esign.requests.retryDelivery({id})`.
- `esign.jobs.create({requestId, placement?})` → `{jobId, ticket, expiresAt, agentUrl}`; `esign.jobs.status({jobId})`;
`esign.jobs.cancel({jobId})`.
- `esign.jobs.create({items: [{requestId, places?}]})` (1–10, all or none) → `[{requestId, jobId, ticket, expiresAt}]`,
opened together in one agent window; `places` moves the caller's boxes (one position per place) before signing.
`esign.jobs.statuses({jobIds})`, `esign.jobs.cancel({jobIds})`.

Placement: `{page (0-based), x, y, width, height}` as fractions of the displayed page (crop box after rotation),
origin top-left.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dependencies": {
"@base-ui/react": "^1.6.0",
"@dnd-kit/react": "^0.4.0",
"@polinetwork/backend": "^1.8.0",
"@polinetwork/backend": "^1.9.0",
"@t3-oss/env-core": "^0.13.10",
"@tanstack/charts": "1.1.0",
"@tanstack/react-charts": "1.1.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/components/shell/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export type PageMatch =
| { kind: "esign-requests" }
| { kind: "esign-request"; id: string }
| { kind: "esign-new" }
| { kind: "esign-sign" }
| { kind: "esign-setup" }
| { kind: "reports-open" }
| { kind: "reports-closed" }
Expand Down Expand Up @@ -355,6 +356,7 @@ export function matchPath(pathname: string): PageMatch {
if (second === "inbox") return { kind: "esign-inbox" }
if (second === "requests") return { kind: "esign-requests" }
if (second === "new") return { kind: "esign-new" }
if (second === "sign") return { kind: "esign-sign" }
if (second === "setup") return { kind: "esign-setup" }
}
if (second === "requests" && third !== undefined && /^[0-9a-f-]{36}$/i.test(third)) {
Expand Down Expand Up @@ -400,6 +402,7 @@ export function serviceFor(match: PageMatch): Service {
case "esign-requests":
case "esign-request":
case "esign-new":
case "esign-sign":
case "esign-setup":
return esign
case "reports-open":
Expand Down Expand Up @@ -443,6 +446,7 @@ export function sectionFor(match: PageMatch): Section | null {
case "web-category":
return section(web, "categories")
case "esign-inbox":
case "esign-sign":
return section(esign, "inbox")
case "esign-requests":
case "esign-request":
Expand All @@ -464,7 +468,8 @@ export function isDeepPage(match: PageMatch): boolean {
match.kind === "web-category" ||
match.kind === "web-tag" ||
match.kind === "esign-request" ||
match.kind === "esign-new"
match.kind === "esign-new" ||
match.kind === "esign-sign"
)
}

Expand Down
10 changes: 7 additions & 3 deletions src/features/esign/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ export const AGENT_LAUNCH_URL = "polinetwork-esign://open"

export const AGENT_RELEASES_URL = "https://github.com/PoliNetworkOrg/esign/releases/latest"

/** The agent's signing page for a job. `origin` is this dashboard, which the agent checks against its allowlist. */
export function agentSignUrl(ticket: string, origin: string) {
const params = new URLSearchParams({ ticket, origin })
/**
* The agent's signing page for one or more jobs, signed with one PIN. `origin` is this dashboard, which the agent
* checks against its allowlist.
*/
export function agentSignUrl(tickets: string[], origin: string) {
const params = new URLSearchParams({ origin })
for (const ticket of tickets) params.append("ticket", ticket)
return `${AGENT_ORIGINS[0]}/sign?${params}`
}

Expand Down
Loading
Loading