Compare commits

..

No commits in common. "ad0131c2d956ab699c3cd51b8a96175f0cb05ecc" and "780099172fae1cfee0de5dd9a7dae422f0dec160" have entirely different histories.

6 changed files with 24 additions and 103 deletions

View File

@ -1,51 +0,0 @@
# Adobe Sign → DocuSign Migrator — Environment Variables
# Copy this file to .env and fill in your values.
# Never commit .env to version control.
# ─── Adobe Sign ──────────────────────────────────────────────────────────────
# OAuth app credentials from the Adobe Sign developer console
ADOBE_CLIENT_ID=your-adobe-client-id
ADOBE_CLIENT_SECRET=your-adobe-client-secret
# Auto-written by src/adobe_auth.py after the one-time OAuth flow.
# Leave blank; they will be populated on first run.
ADOBE_ACCESS_TOKEN=
ADOBE_REFRESH_TOKEN=
ADOBE_SIGN_BASE_URL=https://api.eu2.adobesign.com/api/rest/v6
# ─── DocuSign ────────────────────────────────────────────────────────────────
# Integration key (client ID) from the DocuSign developer console
DOCUSIGN_CLIENT_ID=your-integration-key
# Client secret — only needed for the one-time Auth Code Grant consent flow
DOCUSIGN_CLIENT_SECRET=your-client-secret
# GUID of the DocuSign user to impersonate via JWT grant
# Found in the DocuSign admin UI under Users → user details
DOCUSIGN_USER_ID=your-docusign-user-guid
# Account ID of the target DocuSign account
# Found in the DocuSign admin UI under Settings → Account Profile
DOCUSIGN_ACCOUNT_ID=your-docusign-account-id
# Path to the RSA private key file used for JWT signing
# Generate a keypair in the DocuSign developer console and save the private key here
DOCUSIGN_PRIVATE_KEY_PATH=/path/to/private.key
# OAuth auth server — use account-d.docusign.com for sandbox, account.docusign.com for production
DOCUSIGN_AUTH_SERVER=account-d.docusign.com
# eSignature REST API base URL
# Sandbox: https://demo.docusign.net/restapi
# Production: https://na3.docusign.net/restapi (replace na3 with your shard)
DOCUSIGN_BASE_URL=https://demo.docusign.net/restapi
# Redirect URI registered in your DocuSign app (used only during one-time consent flow)
DOCUSIGN_REDIRECT_URI=http://localhost:8080/callback
# Auto-written by src/docusign_auth.py to cache the JWT access token.
# Leave blank; they will be populated automatically.
DOCUSIGN_ACCESS_TOKEN=
DOCUSIGN_TOKEN_EXPIRY=

View File

@ -52,7 +52,7 @@ For production replace with `account.docusign.com` and your account's base URL (
**3. Authenticate with Adobe Sign** (one-time):
```bash
python3 src/adobe_auth.py
python3 src/auth_adobe.py
```
Opens a browser. After authorizing, paste the redirect URL back into the terminal.
Tokens are saved to `.env` and auto-refreshed on subsequent runs.
@ -120,7 +120,7 @@ unexpected API behaviors, and the fixes applied.
```
src/
adobe_auth.py # One-time OAuth flow for Adobe Sign
auth_adobe.py # One-time OAuth flow for Adobe Sign
adobe_api.py # Adobe Sign API client (auto token refresh)
download_templates.py # List and download templates from Adobe Sign
compose_docusign_template.py # Core conversion: Adobe Sign → DocuSign JSON

View File

@ -23,7 +23,7 @@ Source: Adobe Sign UI "Change field type" dropdown (all 15 types) + API field da
| Image | INLINE_IMAGE | DATA | — | (skipped) | No DocuSign equivalent |
| Company | TEXT_FIELD | COMPANY or SIGNER_COMPANY | — | companyTabs | Auto-populated from signer profile. API returns `SIGNER_COMPANY` when set via UI. |
| Title | TEXT_FIELD | TITLE or SIGNER_TITLE | — | titleTabs | Auto-populated from signer profile. API returns `SIGNER_TITLE` when set via UI. |
| Stamp | STAMP | — | — | stampTabs | DocuSign stampTabs — signer uploads/selects a stamp image (hanko/seal). Requires stamp feature enabled on account. |
| Stamp | STAMP | — | — | (skipped) | No DocuSign equivalent |
| Signature block | BLOCK | SIGNATURE_BLOCK | — | signHereTabs | Composite block — mapped to sign-here |
## Role/Recipient Mapping
@ -76,7 +76,7 @@ Tab types that support merging (one tab emitted per location):
`emailAddressTabs`, `companyTabs`, `titleTabs`, `listTabs`, `checkboxTabs`
Tab types that do not merge (only first location used or handled specially):
`signHereTabs`, `initialHereTabs`, `stampTabs` — each location is an independent signing/stamping action
`signHereTabs`, `initialHereTabs` — each location is an independent signing action
`radioGroupTabs` — each location is one radio button within the group
`signerAttachmentTabs` — each location is an independent attachment request
@ -90,9 +90,6 @@ Tab types that do not merge (only first location used or handled specially):
best-effort via standard DocuSign validation types only.
- **Radio group flattening**: Adobe radios with `radioGroup` are merged into a single
DocuSign `radioGroupTabs` entry with per-location radio button coordinates.
- **Stamp tab account feature**: `stampTabs` requires the stamp/hanko feature to be
enabled on the DocuSign account. Verify before migrating templates that contain
Adobe Sign STAMP fields.
## To Do
- Add conditional logic/rule mapping table

View File

@ -5,8 +5,8 @@ from dotenv import load_dotenv, set_key
load_dotenv()
SHARD = "eu2"
TOKEN_URL = f"https://api.{SHARD}.adobesign.com/oauth/v2/token" # initial auth code exchange
REFRESH_URL = f"https://api.{SHARD}.adobesign.com/oauth/v2/refresh" # token refresh (non-standard separate endpoint)
TOKEN_URL = f"https://api.{SHARD}.adobesign.com/oauth/v2/token"
REDIRECT_URI = "https://localhost:8080/callback"
ENV_FILE = os.path.join(os.path.dirname(__file__), "..", ".env")
@ -16,22 +16,21 @@ def _refresh_access_token():
refresh_token = os.getenv("ADOBE_REFRESH_TOKEN")
if not all([client_id, client_secret, refresh_token]):
raise RuntimeError("Missing credentials for token refresh. Run src/adobe_auth.py first.")
raise RuntimeError("Missing credentials for token refresh. Run src/auth_adobe.py first.")
data = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": REDIRECT_URI,
}
resp = requests.post(REFRESH_URL, data=data)
if not resp.ok:
raise RuntimeError(
f"Adobe Sign refresh token is invalid or expired ({resp.status_code}: {resp.text}). "
"Run `python3 src/adobe_auth.py` to re-authenticate."
)
resp = requests.post(TOKEN_URL, data=data)
resp.raise_for_status()
new_token = resp.json()["access_token"]
set_key(os.path.abspath(ENV_FILE), "ADOBE_ACCESS_TOKEN", new_token)
abs_env = os.path.abspath(ENV_FILE)
set_key(abs_env, "ADOBE_ACCESS_TOKEN", new_token)
os.environ["ADOBE_ACCESS_TOKEN"] = new_token
return new_token

View File

@ -2,7 +2,7 @@
One-time Adobe Sign OAuth setup.
Run this script once to authorize the app and save tokens to .env:
python src/adobe_auth.py
python src/auth_adobe.py
Prerequisites:
- Set ADOBE_CLIENT_ID and ADOBE_CLIENT_SECRET in .env (or export them)
@ -58,34 +58,17 @@ def exchange_code_for_tokens(code, client_id, client_secret):
"client_secret": client_secret,
}
resp = requests.post(TOKEN_URL, data=data)
if not resp.ok:
print(f"ERROR: Token exchange failed ({resp.status_code}): {resp.text}")
resp.raise_for_status()
tokens = resp.json()
print(f"Token exchange response keys: {list(tokens.keys())}")
if "error" in tokens:
raise ValueError(f"Adobe Sign returned error: {tokens.get('error')}: {tokens.get('error_description')}")
return tokens
resp.raise_for_status()
return resp.json()
def save_tokens(tokens):
access_token = tokens.get("access_token", "")
refresh_token = tokens.get("refresh_token", "")
if not access_token:
raise ValueError("Adobe Sign did not return an access_token — not saving incomplete tokens")
if not refresh_token:
print("WARNING: Adobe Sign did not return a refresh_token — you will need to re-authenticate when the access token expires (~1 hour)")
abs_env = os.path.abspath(ENV_FILE)
set_key(abs_env, "ADOBE_ACCESS_TOKEN", access_token)
if refresh_token:
set_key(abs_env, "ADOBE_REFRESH_TOKEN", refresh_token)
set_key(abs_env, "ADOBE_ACCESS_TOKEN", tokens["access_token"])
if "refresh_token" in tokens:
set_key(abs_env, "ADOBE_REFRESH_TOKEN", tokens["refresh_token"])
set_key(abs_env, "ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
print(f"Access token saved (length={len(access_token)})")
if refresh_token:
print(f"Refresh token saved (length={len(refresh_token)})")
print(f"Tokens written to {abs_env}")
print(f"Tokens saved to {abs_env}")
def main():

View File

@ -19,10 +19,9 @@ Key rules applied:
- No top-level "status" field (belongs on envelope sends, not templates)
Field type coverage:
Mapped: TEXT_FIELD, SIGNATURE, CHECKBOX, DATE, DROP_DOWN, RADIO, BLOCK, STAMP
Mapped: TEXT_FIELD, SIGNATURE, CHECKBOX, DATE, DROP_DOWN, RADIO, BLOCK
Partial: FILE_CHOOSER signerAttachmentTabs (with warning)
STAMP stampTabs (requires stamp feature enabled on DocuSign account warning logged)
Skipped: INLINE_IMAGE, PARTICIPATION_STAMP (no DocuSign equivalent warning logged)
Skipped: INLINE_IMAGE (no DocuSign equivalent warning logged)
"""
import base64
@ -230,14 +229,8 @@ def build_tabs_for_field(field: dict, warnings: list) -> dict:
warnings.append(f"INLINE_IMAGE '{label}' → skipped (no DocuSign equivalent)")
return {}
elif input_type == "STAMP":
# DocuSign stampTabs — signer uploads or selects a hanko/seal stamp image.
# Requires the stamp feature to be enabled on the DocuSign account.
warnings.append(f"STAMP '{label}' → stampTabs (verify stamp feature is enabled on your DocuSign account)")
return {"stampTabs": [_make_base_tab(loc, label) for loc in locations]}
elif input_type == "PARTICIPATION_STAMP":
warnings.append(f"PARTICIPATION_STAMP '{label}' → skipped (no DocuSign equivalent)")
elif input_type in ("STAMP", "PARTICIPATION_STAMP"):
warnings.append(f"{input_type} '{label}' → skipped (no DocuSign equivalent)")
return {}
else: