Repository navigation
feat(js-sdk): embedded identity verification (createSigningUrl + recipient modes) - #77
Merged
Merged
Conversation
2 of 3 tasks
nicolasiscoding
force-pushed
the
feature/turbosign-embedded-identity
branch
from
September 16, 2026 12:30
1ced510 to
b0c5402
Compare
3 tasks
| return recipients; | ||
| } | ||
|
|
||
| public List<Field> getFields() { |
nicolasiscoding
commented
Sep 18, 2026
| { | ||
| "compilerOptions": { | ||
| "target": "ES2020", | ||
| "module": "node16", |
…ipient modes Adds the TurboSign embedded identity-verification surface to the TypeScript SDK: - Recipient gains `phone`, `externalId`, and `identityVerification` — a discriminated union on `mode` (otp | external_idv | override) so the compiler narrows the required fields for the developer. - New `TurboSign.createSigningUrl(documentId, request)` mints a single-use embedded signing URL (DocuSign createRecipientView / BoldSign GetEmbeddedSignLink equivalent); typed request (recipientId | externalId, optional identityAssertion, https returnUrl) and response (url, expiresAt, mode, pendingChecks). - Fail-fast client-side validation with actionable messages (the server stays the source of truth): exactly one recipient selector, https returnUrl, override requires literal `true` + reason, external_idv requires a provider, otp+sms requires a phone. TDD: 10 new cases; full js-sdk suite green (335). tsc clean. Ports to py/php/go/java/ruby + the n8n node follow in a later commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
examples/turbosign-embedded-identity.ts — prepares an embedded recipient (otp/external_idv/ override), requests a single-use signing URL with createSigningUrl by externalId, and shows how to open it, with inline notes on the assertion, pending checks, single-use expiry, and the allowed- embedding-domains setting. Matches the existing example style. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…d example Identity verification is an optional layer: embedded signing works without it (a plain embedded recipient signs with no extra step). Add identityVerification only for flows that want an OTP challenge or have an external provider verify the signer (plus override for dev/testing). Baseline example now shows a recipient with no identityVerification. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ngSettings
createSigningUrl posted to a { data: { results } } endpoint but returned the raw
client result, which after the client strips the outer data is { results }, not the
flat response. Unwrap results (same convention as the quote and deliverable modules)
so url / pendingChecks / identityVerificationMode are populated. The flat-mocked unit
test hid this; the mocks now use the real { results } shape.
Add TurboSign.getEmbeddedSigningSettings(): a read-only view of the org-wide gates
(enabled, allowExternalIdv, allowIdentityOverride), the default channel, and the
allowed frame-ancestors, so an integrator can see what is permitted before minting
signing URLs. Writes stay in the settings UI and preferences API (audited).
Example: read the org settings up front, and correct the token vs single-use URL
note (no-verification and otp use the reusable token link; only external_idv and
override use the single-use sut link).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
…ethods
The existing identity tests mock HttpClient and return the already-unwrapped { results } shape, so
smartUnwrap + the handler's `.results` unwrap were never exercised — a backend envelope change would
break createSigningUrl / getEmbeddedSigningSettings while the tests stayed green. Add a companion
suite that uses the real HttpClient with a mocked fetch returning the actual { data: { results } }
server envelope, asserting each method returns the flat response (never the { results } wrapper).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
…d signing URL The signing URL from createSigningUrl is now the /e-signature/embed/... path, served with a per-tenant Content-Security-Policy: frame-ancestors. Clarify in the embedded-identity example that an origin not on the org's allow-list is hard-blocked from framing (not just warned), and that the non-embedded /sign/ links deny all framing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
A runnable example showing the INTEGRATOR side of embedded signing: a small "Northwind Mutual" web-app that embeds the TurboSign signing page in an iframe instead of emailing a link. - server.ts: a framework-free Node server that holds the API key and mints an embeddable signing URL with the SDK (TurboSign.createSigningUrl); the key never reaches the browser. - public/index.html: the host page that fetches the URL from its own backend, frames it, and advances on the `turbosign:completed` postMessage (no polling). - README: how to run + the key prerequisite — the org must allow-list the host origin (http://localhost:4000) in embedded-signing allowed origins, or the browser denies the frame (default-deny frame-ancestors). This is the same tenant change a real integrator (Baer's) makes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
….html gitignore) The web-app example's host page was skipped by the repo-wide *.html ignore; it is a source example file, not a build artifact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
…email OTP
Enhance the host web-app example so it demonstrates the full embedded ceremony end to end:
- The host page collects the signer's name + email in a form.
- The server creates the recipient with that email and identityVerification { mode: 'otp', channel:
'email' }, so the embedded signing page challenges an emailed 6-digit code before signing.
- Documents the completed-copy delivery: the signed copy is emailed to that recipient email (plus any
CC on the document, plus a sender completion notification).
- Adds a TURBODOCX_API_URL override so the example can point at a non-default backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
… is https createSigningUrl requires returnUrl to be an https URL. The example derives its origin from the port (http://localhost:4000 locally), which failed validation. Send returnUrl only when the host origin is https (production); locally over http, rely on the turbosign:completed postMessage for completion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
…index it Promote the embedded-signing host web-app from packages/js-sdk/examples/ to the repo-level examples/ folder, alongside partner-preferences (full-app showcases live here, distinct from the per-language snippets). Fix the ExampleAssets relative path for the new location, add examples/README.md indexing the showcases, and point the root README at examples/embedded-web-app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
Add proper environment-file handling to the embedded-web-app example so a user can copy it and run without inlining secrets on the command line. - Add .env.example documenting every variable (API key, org, sender, optional API URL, port) with placeholder values and comments. - Load config in server.ts via `import 'dotenv/config'` at the top; add dotenv as an exact-pinned devDependency (hoists to the workspace root). - Update the README Run section and the server.ts docstring to: copy .env.example to .env, fill it in, and run from the example directory (dotenv reads .env from the current working directory). The real .env is already covered by the root .gitignore and is never committed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
…gning Fold create-signature-request + per-recipient embed-URL minting into one developer-facing call, matching the DocuSeal / Dropbox Sign embedded-signing shape and scaling to multiple signers (one embed URL per recipient, returned in signing order). Pure SDK wrapper over sendSignature + createSigningUrl — no backend change. Maps ergonomic auth (emailOtp/sms) to identityVerification, expands a fields shorthand to Field objects (initials -> 'initial' type), defaults signingOrder to index+1 and sendEmail to false. Adds a wire-contract test and updates the embedded-web-app example. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
Add a new @turbodocx/embed workspace package with three pieces: a pure, unit-tested message handler (handleTurboSignMessage) that does origin pinning and dispatch on the turbosign:completed postMessage, a framework-agnostic <turbosign-form> web component, and a React <TurboSignForm> component (React is a peerDependency, exposed via the /react subpath so the web-component build never imports react). The package emits browser-loadable ESM. Embedded signing is now shown two ways: the existing examples/embedded-web-app is the raw iframe + hand-rolled message-listener approach, and the new examples/embedded-web-app-widget uses the <turbosign-form> web component in a plain HTML page (with a <TurboSignForm> React snippet in its README). Adds a test-embed CI job. 23 unit/DOM tests pass; tsc --noEmit clean. Closes #78 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo
…ners
With a real signing order the backend only mints an embed URL for the signer
whose turn it is. Instead of throwing the whole call away when a later signer
isn't in turn, each recipient result now carries a `status`:
- 'ready' — their turn; `embedUrl` is set, frame it now
- 'pending' — an earlier signer hasn't finished; `embedUrl` is null (mint it
later with createSigningUrl once earlier signers complete)
- 'completed' — already signed; `embedUrl` is null
`embedUrl` is now `string | null`. The mint loop catches the backend's
RecipientNotInTurn / NotSignersTurn (-> pending) and RecipientAlreadySigned
(-> completed) 409s; any other error still propagates.
Adds a sequential/kiosk example (examples/embedded-web-app-sequential) that
signs one document with two signers in order on the same device, minting each
next URL just-in-time. Narrows the now-nullable embedUrl at the single-signer
example boundaries.
Tests: 4 new wire tests (ready/pending/completed/rethrow); 346/346 green.
… paths)
Consolidate the embedded-signing examples into one Vite + React + shadcn app with
three clearly-labeled paths — Single signer, Sequential kiosk (turn-aware), and the
<TurboSignForm> widget — replacing the three separate example dirs.
Architecture: React -> its own server.ts (holds the API key, uses @turbodocx/sdk)
-> TurboDocx. The browser never imports the SDK or sees the key; the SPA calls
/api/single, /api/kiosk/start, /api/kiosk/next and frames the minted URLs. Vite
proxies /api to the server in dev. This is the pattern real integrations use in prod.
- server.ts + handlers.ts: three key-holding endpoints (single / kiosk start / next)
- kiosk mintNextWithRetry: rides out the brief window right after a signer completes,
where the backend hasn't advanced the signing turn yet (RecipientNotInTurn)
- Vitest: 10 tests (handlers with SDK mocked; frontend api client with fetch mocked,
incl. the turn-race retry)
- remove embedded-web-app-{sequential,widget} (folded into paths) + the old
static public/index.html; refresh both READMEs
Verified end-to-end live: all three paths sign to completion with real email OTP.
…dy a public token-auth'd API, not iframe-only
…ning example - .env.example: drop the stale VITE_* section (nothing reads it after the server-side rewrite — it taught the client-side-key anti-pattern the rewrite removed); consistent sender brand - vite proxy + server.ts now key off the same PORT var (was SERVER_PORT vs PORT, which silently broke the /api proxy on a port override) - Kiosk: thread documentId into the initial frame() call (the state set just above hasn't committed for that render, so a pending signer-1 would mint with an empty documentId) - SingleSigner/Kiosk/Widget: pin the turbosign:completed listener to the framed URL's origin instead of accepting any origin (was fail-open by default) - mintNextWithRetry: retry on the machine-readable RecipientNotInTurn code, not the human message (message stays as a fallback); server.ts forwards the code - handlers.ts: read the sample PDF once, not per request - server.ts: cap the request body in the BFF's readBody - build: plain `tsc --noEmit` (drop the composite project-reference config that didn't typecheck) - embed: TurboSignForm no longer annotates its return as ReactElement, which tripped React 18.3's stricter JSX types in a strict consumer Tests: example 11/11, embed 23/23; app tsc --noEmit clean.
…/java/ruby
Ports the js-sdk embedded-signing surface to the other five languages,
idiomatically and additively (no change to existing send/sign behavior):
- createSigningUrl(documentId, {recipientId XOR externalId, identityAssertion?,
returnUrl?}) — client-side XOR + https-only returnUrl guards before any HTTP;
unwraps the {data:{results}} double envelope to results (the bug the js-sdk
originally shipped is guarded against in every port).
- getEmbeddedSigningSettings() — read-only org gates.
- createEmbeddedSignature(...) — sendSignature + per-recipient createSigningUrl,
turn-aware by error CODE: RecipientNotInTurn/NotSignersTurn -> pending (null
url), RecipientAlreadySigned -> completed, success -> ready; other errors
rethrow. Recipient gains phone/externalId/identityVerification; sendEmail is
plumbed through the send path (was missing in several SDKs) gated on non-null
so an explicit false survives.
Each language ships unit tests mocking only the HTTP boundary (status mapping,
XOR validation, envelope unwrap, signing-order assembly). Green: go build+test,
py 348, php 380 (phpstan L8), java 333 (offline), ruby 340.
Also folds in the earlier js-sdk/embed JSDoc path fixes referencing
examples/embedded-web-app.
…embedded ports
Makes the five ports behave identically to each other and to the js reference:
- SMS-OTP fail-fast (PhoneRequiredForSmsOtp before any HTTP) added to py/ruby/java
(js/go/php already had it); py also fixed an empty sms:{} being silently dropped.
- Empty `fields` marshals to "[]" not "null" in Go (backend can iterate it).
- Empty-string returnUrl is treated as absent (not sent, no error) in php/ruby/java,
matching js/go/py.
- create_signing_url falls back to the flat body when the results envelope is
absent in py/ruby (php/go/java already guarded this).
- SMS shorthand falls back to the recipient top-level phone in Go (was overwriting
with the empty SMS number and wrongly firing the fail-fast); matches
js `auth?.sms?.phoneNumber ?? r.phone` and php/java/py/ruby.
Each language adds regression tests. Green: go build+test, py 351, php 381
(phpstan L8), java 335 (offline), ruby 343.
…a/ruby Each mirrors packages/js-sdk/examples/turbosign-embedded-identity.ts exactly in structure and narrative, idiomatic per language: read embedded settings -> send a document with an embedded recipient (identity modes shown commented, matching the js baseline) -> createSigningUrl -> print url/mode/pendingChecks/expiresAt. Same placeholder values across all six so they read identically. All compile/lint clean (go build, py_compile, php -l, ruby -c, javac).
…gning
Found by live e2e against the backend: req_val read only symbol/string of the exact
key, not the camelCase<->snake_case variant, so an idiomatic Ruby
`auth: { email_otp: true }` silently resolved to NO identity verification — the OTP
was dropped with no error (mode=null, empty pendingChecks), while Python accepted
the same snake_case. req_val now folds camelCase<->snake_case (both symbol and
string), matching its own "accepts both key styles throughout" contract, so
email_otp / phone_number resolve the same as emailOtp / phoneNumber. +1 spec.
…on Simulator example The SDK IdentityAssertion type gains optional method/methodDetail/assuranceLevel/verifiedName/evidenceUrl/overrideEmailMatching. The embedded-web-app External IdV path gains an Identity Verification Simulator dialog (collect verified name/email/method/assurance, run a simulated verification, pass the assertion back), with a differing email flipping overrideEmailMatching.
amitsharma-turbodocx
force-pushed
the
feature/turbosign-embedded-identity
branch
from
September 30, 2026 08:10
5dd9c8c to
6977c3d
Compare
…was already emailed The repo-wide *.html ignore (meant for generated output) silently dropped the embedded-web-app's index.html when the example moved to Vite, so a fresh clone could not start the app. Track that one file with an explicit exception. The code is only sent when the signer clicks "Send Code" in the signing panel, but the Widget, Single signer and Kiosk status lines said "We emailed a 6-digit code" before any code existed. The copy now lives in lib/statusCopy.ts (with tests) and points the signer at the Send Code button. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Identity assertions: the optional audit-trail context (method, methodDetail, assuranceLevel, verifiedName, evidenceUrl, overrideEmailMatching) is now settable in Go, Java and PHP, whose typed assertions only carried the four required keys; PHP's array form dropped them too. Unset keys stay off the wire. Python and Ruby already passed them through; now documented. - getEmbeddedSigningSettings exposes allowChannelOverride: whether a request may pick a channel other than defaultChannel (false = OtpOverrideNotAllowed). Nullable in the typed SDKs, so a response without it reads as unknown, not locked. - @turbodocx/embed passes the completion `event` (signing_complete | already_signed) and `scope` through to onCompleted / the DOM event, and its docs use the real /e-signature/embed/ path. - Docs corrected in every language: defaultChannel applies to API/SDK sends; an empty allowedFrameAncestors denies framing everywhere; sendEmail:false is honoured; expiresAt is the document's expiry for otp/no-verification links; createEmbeddedSignature's mode for pending signers is the requested one. - Per-language embedded examples now send sendEmail:false, explain the org default and print allowChannelOverride; the web-app example documents its fourth (External IdV) path. - openapi.yaml gains the signing-url and embedded-signing-settings endpoints; the parity rules list the three embedded operations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
amitsharma-turbodocx
marked this pull request as ready for review
October 1, 2026 14:32
added 9 commits
October 5, 2026 13:47
Replace the plain Identity Verification Simulator form with a guided, phone-style check: choose a method, capture (document or face frame with a scan line), watch the checks complete, then confirm the verified details. Still clearly simulated (no camera, no upload, no vendor call); the asserted values and the email-match override are unchanged.
…and blank externalId
- ReminderStatus gains skipped_requires_single_use_url: external_idv and override
recipients sign only through a single-use createSigningUrl link and are never emailed.
- resend and sendReminder document the 409 ConflictError with code
RecipientRequiresSingleUseUrl when only such recipients are named.
- identityVerification { mode: 'otp' } without a channel takes the org default channel,
or email when that default is none.
- A blank or whitespace-only externalId counts as absent.
… and blank externalId
- resend_email and send_reminder document the 409 ConflictError with code
RecipientRequiresSingleUseUrl, and the skipped_requires_single_use_url reminder status
for external_idv and override recipients, which are never emailed.
- identityVerification {"mode": "otp"} without a channel takes the org default channel,
or email when that default is none.
- A blank or whitespace-only externalId counts as absent.
… and blank externalId - ResendEmail and SendReminder document the 409 *ConflictError with Code RecipientRequiresSingleUseUrl, and the skipped_requires_single_use_url reminder status for external_idv and override recipients, which are never emailed. - IdentityVerification Mode "otp" with an empty Channel takes the org default channel, or email when that default is none. - A whitespace-only ExternalID counts as absent.
…t and blank externalId - resend and sendReminder document the 409 ConflictException with code RecipientRequiresSingleUseUrl, and the skipped_requires_single_use_url reminder status for external_idv and override recipients, which are never emailed. - IdentityVerification::otp() notes how the API resolves an otp request without a channel and the EmbeddedSigningNotEnabled 403. - A blank or whitespace-only externalId counts as absent.
…lt and blank externalId - resendEmail and sendReminder document the 409 ConflictException with code RecipientRequiresSingleUseUrl, and the skipped_requires_single_use_url reminder status for external_idv and override recipients, which are never emailed. - IdentityVerification.otp(null) takes the org default channel, or email when that default is none. - A blank or whitespace-only externalId counts as absent.
…lt and blank externalId
- resend_email and send_reminder document the 409 ConflictError with code
RecipientRequiresSingleUseUrl, and the skipped_requires_single_use_url reminder status
for external_idv and override recipients, which are never emailed.
- identityVerification { mode: "otp" } without a channel takes the org default channel,
or email when that default is none.
- A blank or whitespace-only externalId counts as absent.
…d and resend rule
Member
Author
|
Synced the SDKs with recent API behavior changes (docs in all six SDKs, one type addition in JS):
All six test suites pass. |
added 2 commits
October 5, 2026 21:39
…t least one recipient required)
…d title prop - An empty, null or undefined origin now rejects every message instead of accepting any origin. Local development can opt out explicitly with allowAnyOrigin (React prop, handler option) or allow-any-origin (attribute). The components log a one-time console.warn when messages are ignored for a missing origin. - Both components also require event.source to be their own iframe's contentWindow. handleTurboSignMessage stays pure and takes an optional expectedSource. - New title prop / attribute for the iframe's accessible name, defaulting to "TurboSign signing". - README: rewrite "Why origin pinning matters", document the new options and the 0.2.0 change, fix the example link and a dash. - Drop the placeholder lint script; bump to 0.2.0.
Member
Author
|
Pushed 3b81252 to
Tests: 54/54 passing ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related
Backend PR: nicolasiscoding/RapidDocxBackend#1801 (embedded signing + email/SMS OTP, extended with the identity-verification modes)
Design:
docs/TURBOSIGN_EMBEDDED_IDENTITY_VERIFICATION_DESIGN.mdin the backend repo.Description
Adds the embedded identity-verification surface to the TypeScript SDK (first of the six; ports to py/php/go/java/ruby + the n8n node follow).
phone,externalId, andidentityVerification— a discriminated union onmode(otp|external_idv|override) so the compiler narrows the required fields.TurboSign.createSigningUrl(documentId, request)mints a single-use embedded signing URL (DocuSigncreateRecipientView/ BoldSignGetEmbeddedSignLinkequivalent). Typed request (recipientId | externalId, optionalidentityAssertionfor external IDV, httpsreturnUrl) and response (url,expiresAt,identityVerificationMode,pendingChecks).returnUrl,overriderequires literaltrue+reason,external_idvrequires aprovider,otp+smsrequires aphone.Pre-Review Checklist
tsc --noEmitcleananytypes introduced (test-only casts for intentionally-invalid inputs)Deployment Notes
Requires the backend
POST /turbosign/documents/:documentId/signing-urlendpoint (backend PR #1801). No SDK version bump yet — this ships in lockstep across all six SDKs once the ports land.🤖 Generated with Claude Code
https://claude.ai/code/session_019qnwET4dBdHMHpyMgi6cHo