Compare commits
4 Commits
780099172f
...
ad0131c2d9
| Author | SHA1 | Date |
|---|---|---|
|
|
ad0131c2d9 | |
|
|
e2e47f2662 | |
|
|
9c6c01d619 | |
|
|
766986a795 |
|
|
@ -0,0 +1,51 @@
|
||||||
|
# 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=
|
||||||
|
|
@ -52,7 +52,7 @@ For production replace with `account.docusign.com` and your account's base URL (
|
||||||
|
|
||||||
**3. Authenticate with Adobe Sign** (one-time):
|
**3. Authenticate with Adobe Sign** (one-time):
|
||||||
```bash
|
```bash
|
||||||
python3 src/auth_adobe.py
|
python3 src/adobe_auth.py
|
||||||
```
|
```
|
||||||
Opens a browser. After authorizing, paste the redirect URL back into the terminal.
|
Opens a browser. After authorizing, paste the redirect URL back into the terminal.
|
||||||
Tokens are saved to `.env` and auto-refreshed on subsequent runs.
|
Tokens are saved to `.env` and auto-refreshed on subsequent runs.
|
||||||
|
|
@ -120,7 +120,7 @@ unexpected API behaviors, and the fixes applied.
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
auth_adobe.py # One-time OAuth flow for Adobe Sign
|
adobe_auth.py # One-time OAuth flow for Adobe Sign
|
||||||
adobe_api.py # Adobe Sign API client (auto token refresh)
|
adobe_api.py # Adobe Sign API client (auto token refresh)
|
||||||
download_templates.py # List and download templates from Adobe Sign
|
download_templates.py # List and download templates from Adobe Sign
|
||||||
compose_docusign_template.py # Core conversion: Adobe Sign → DocuSign JSON
|
compose_docusign_template.py # Core conversion: Adobe Sign → DocuSign JSON
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| 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. |
|
| 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. |
|
| Title | TEXT_FIELD | TITLE or SIGNER_TITLE | — | titleTabs | Auto-populated from signer profile. API returns `SIGNER_TITLE` when set via UI. |
|
||||||
| Stamp | STAMP | — | — | (skipped) | No DocuSign equivalent |
|
| Stamp | STAMP | — | — | stampTabs | DocuSign stampTabs — signer uploads/selects a stamp image (hanko/seal). Requires stamp feature enabled on account. |
|
||||||
| Signature block | BLOCK | SIGNATURE_BLOCK | — | signHereTabs | Composite block — mapped to sign-here |
|
| Signature block | BLOCK | SIGNATURE_BLOCK | — | signHereTabs | Composite block — mapped to sign-here |
|
||||||
|
|
||||||
## Role/Recipient Mapping
|
## Role/Recipient Mapping
|
||||||
|
|
@ -76,7 +76,7 @@ Tab types that support merging (one tab emitted per location):
|
||||||
`emailAddressTabs`, `companyTabs`, `titleTabs`, `listTabs`, `checkboxTabs`
|
`emailAddressTabs`, `companyTabs`, `titleTabs`, `listTabs`, `checkboxTabs`
|
||||||
|
|
||||||
Tab types that do not merge (only first location used or handled specially):
|
Tab types that do not merge (only first location used or handled specially):
|
||||||
`signHereTabs`, `initialHereTabs` — each location is an independent signing action
|
`signHereTabs`, `initialHereTabs`, `stampTabs` — each location is an independent signing/stamping action
|
||||||
`radioGroupTabs` — each location is one radio button within the group
|
`radioGroupTabs` — each location is one radio button within the group
|
||||||
`signerAttachmentTabs` — each location is an independent attachment request
|
`signerAttachmentTabs` — each location is an independent attachment request
|
||||||
|
|
||||||
|
|
@ -90,6 +90,9 @@ Tab types that do not merge (only first location used or handled specially):
|
||||||
best-effort via standard DocuSign validation types only.
|
best-effort via standard DocuSign validation types only.
|
||||||
- **Radio group flattening**: Adobe radios with `radioGroup` are merged into a single
|
- **Radio group flattening**: Adobe radios with `radioGroup` are merged into a single
|
||||||
DocuSign `radioGroupTabs` entry with per-location radio button coordinates.
|
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
|
## To Do
|
||||||
- Add conditional logic/rule mapping table
|
- Add conditional logic/rule mapping table
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ from dotenv import load_dotenv, set_key
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
SHARD = "eu2"
|
SHARD = "eu2"
|
||||||
TOKEN_URL = f"https://api.{SHARD}.adobesign.com/oauth/v2/token"
|
TOKEN_URL = f"https://api.{SHARD}.adobesign.com/oauth/v2/token" # initial auth code exchange
|
||||||
REDIRECT_URI = "https://localhost:8080/callback"
|
REFRESH_URL = f"https://api.{SHARD}.adobesign.com/oauth/v2/refresh" # token refresh (non-standard separate endpoint)
|
||||||
ENV_FILE = os.path.join(os.path.dirname(__file__), "..", ".env")
|
ENV_FILE = os.path.join(os.path.dirname(__file__), "..", ".env")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,21 +16,22 @@ def _refresh_access_token():
|
||||||
refresh_token = os.getenv("ADOBE_REFRESH_TOKEN")
|
refresh_token = os.getenv("ADOBE_REFRESH_TOKEN")
|
||||||
|
|
||||||
if not all([client_id, client_secret, refresh_token]):
|
if not all([client_id, client_secret, refresh_token]):
|
||||||
raise RuntimeError("Missing credentials for token refresh. Run src/auth_adobe.py first.")
|
raise RuntimeError("Missing credentials for token refresh. Run src/adobe_auth.py first.")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"grant_type": "refresh_token",
|
"grant_type": "refresh_token",
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
"client_id": client_id,
|
"client_id": client_id,
|
||||||
"client_secret": client_secret,
|
"client_secret": client_secret,
|
||||||
"redirect_uri": REDIRECT_URI,
|
|
||||||
}
|
}
|
||||||
resp = requests.post(TOKEN_URL, data=data)
|
resp = requests.post(REFRESH_URL, data=data)
|
||||||
resp.raise_for_status()
|
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."
|
||||||
|
)
|
||||||
new_token = resp.json()["access_token"]
|
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
|
os.environ["ADOBE_ACCESS_TOKEN"] = new_token
|
||||||
return new_token
|
return new_token
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
One-time Adobe Sign OAuth setup.
|
One-time Adobe Sign OAuth setup.
|
||||||
|
|
||||||
Run this script once to authorize the app and save tokens to .env:
|
Run this script once to authorize the app and save tokens to .env:
|
||||||
python src/auth_adobe.py
|
python src/adobe_auth.py
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
- Set ADOBE_CLIENT_ID and ADOBE_CLIENT_SECRET in .env (or export them)
|
- Set ADOBE_CLIENT_ID and ADOBE_CLIENT_SECRET in .env (or export them)
|
||||||
|
|
@ -58,17 +58,34 @@ def exchange_code_for_tokens(code, client_id, client_secret):
|
||||||
"client_secret": client_secret,
|
"client_secret": client_secret,
|
||||||
}
|
}
|
||||||
resp = requests.post(TOKEN_URL, data=data)
|
resp = requests.post(TOKEN_URL, data=data)
|
||||||
resp.raise_for_status()
|
if not resp.ok:
|
||||||
return resp.json()
|
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
|
||||||
|
|
||||||
|
|
||||||
def save_tokens(tokens):
|
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)
|
abs_env = os.path.abspath(ENV_FILE)
|
||||||
set_key(abs_env, "ADOBE_ACCESS_TOKEN", tokens["access_token"])
|
set_key(abs_env, "ADOBE_ACCESS_TOKEN", access_token)
|
||||||
if "refresh_token" in tokens:
|
if refresh_token:
|
||||||
set_key(abs_env, "ADOBE_REFRESH_TOKEN", tokens["refresh_token"])
|
set_key(abs_env, "ADOBE_REFRESH_TOKEN", refresh_token)
|
||||||
set_key(abs_env, "ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
|
set_key(abs_env, "ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
|
||||||
print(f"Tokens saved to {abs_env}")
|
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}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
@ -19,9 +19,10 @@ Key rules applied:
|
||||||
- No top-level "status" field (belongs on envelope sends, not templates)
|
- No top-level "status" field (belongs on envelope sends, not templates)
|
||||||
|
|
||||||
Field type coverage:
|
Field type coverage:
|
||||||
Mapped: TEXT_FIELD, SIGNATURE, CHECKBOX, DATE, DROP_DOWN, RADIO, BLOCK
|
Mapped: TEXT_FIELD, SIGNATURE, CHECKBOX, DATE, DROP_DOWN, RADIO, BLOCK, STAMP
|
||||||
Partial: FILE_CHOOSER → signerAttachmentTabs (with warning)
|
Partial: FILE_CHOOSER → signerAttachmentTabs (with warning)
|
||||||
Skipped: INLINE_IMAGE (no DocuSign equivalent — warning logged)
|
STAMP → stampTabs (requires stamp feature enabled on DocuSign account — warning logged)
|
||||||
|
Skipped: INLINE_IMAGE, PARTICIPATION_STAMP (no DocuSign equivalent — warning logged)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
@ -229,8 +230,14 @@ def build_tabs_for_field(field: dict, warnings: list) -> dict:
|
||||||
warnings.append(f"INLINE_IMAGE '{label}' → skipped (no DocuSign equivalent)")
|
warnings.append(f"INLINE_IMAGE '{label}' → skipped (no DocuSign equivalent)")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
elif input_type in ("STAMP", "PARTICIPATION_STAMP"):
|
elif input_type == "STAMP":
|
||||||
warnings.append(f"{input_type} '{label}' → skipped (no DocuSign equivalent)")
|
# 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)")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue