Skip to content

feat(js-sdk): embedded identity verification (createSigningUrl + recipient modes) - #77

Merged
nicolasiscoding merged 37 commits into
mainfrom
feature/turbosign-embedded-identity
Oct 7, 2026
Merged

nicolasiscoding merged 37 commits into
mainfrom
feature/turbosign-embedded-identity

Conversation

@nicolasiscoding

Copy link
Copy Markdown
Member

Related

Backend PR: nicolasiscoding/RapidDocxBackend#1801 (embedded signing + email/SMS OTP, extended with the identity-verification modes)
Design: docs/TURBOSIGN_EMBEDDED_IDENTITY_VERIFICATION_DESIGN.md in 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).

  • Recipient gains phone, externalId, and identityVerification — a discriminated union on mode (otp | external_idv | override) so the compiler narrows the required fields.
  • TurboSign.createSigningUrl(documentId, request) mints a single-use embedded signing URL (DocuSign createRecipientView / BoldSign GetEmbeddedSignLink equivalent). Typed request (recipientId | externalId, optional identityAssertion for external IDV, https returnUrl) and response (url, expiresAt, identityVerificationMode, pendingChecks).
  • Fail-fast client validation with actionable messages (server stays source of truth): exactly one recipient selector, https returnUrl, override requires literal true + reason, external_idv requires a provider, otp+sms requires a phone.

Pre-Review Checklist

  • Tests pass (js-sdk suite green: 335)
  • tsc --noEmit clean
  • No any types introduced (test-only casts for intentionally-invalid inputs)
  • Ported to py/php/go/java/ruby + n8n (follow-up commit)

Deployment Notes

Requires the backend POST /turbosign/documents/:documentId/signing-url endpoint (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

return recipients;
}

public List<Field> getFields() {
Comment thread packages/embed/tsconfig.json Outdated
{
"compilerOptions": {
"target": "ES2020",
"module": "node16",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node20+

nicolasiscoding and others added 24 commits September 30, 2026 13:30
…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.
…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
amitsharma-turbodocx force-pushed the feature/turbosign-embedded-identity branch from 5dd9c8c to 6977c3d Compare September 30, 2026 08:10
amitsharma-turbodocx and others added 2 commits October 1, 2026 19:52
…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
amitsharma-turbodocx marked this pull request as ready for review October 1, 2026 14:32
Nicolas Fry 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.
@nicolasiscoding

Copy link
Copy Markdown
Member Author

Synced the SDKs with recent API behavior changes (docs in all six SDKs, one type addition in JS):

  • OTP channel default: identityVerification: { mode: "otp" } without a channel now takes the org's default channel, or email when that default is none. On an org with embedded signing off it is rejected with EmbeddedSigningNotEnabled (403).
  • Single-use-URL recipients: external_idv and override recipients sign only through a single-use createSigningUrl link and are never sent signing, reminder or resend emails. Resend skips them. Remind reports them as skipped_requires_single_use_url, which is now part of the JS ReminderStatus type. Naming only such recipients in either call returns 409 with code RecipientRequiresSingleUseUrl, which maps to each SDK's conflict error.
  • Blank externalId: an empty or whitespace-only externalId counts as absent.
  • Error mapping: 404 (DocumentNotFound, RecipientNotFound) and 409 already map to the not-found and conflict error classes in all six SDKs, and the API's error code is read first. No code change was needed there.

All six test suites pass.

Nicolas Fry added 2 commits October 5, 2026 21:39
…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.
@nicolasiscoding

Copy link
Copy Markdown
Member Author

Pushed 3b81252 to @turbodocx/embed, bumped to 0.2.0 because the default behavior changes:

  • Fails closed without an origin. An empty, null or undefined origin now ignores every message instead of accepting any origin. For local development only, opt out explicitly with allowAnyOrigin (React prop, handler option) or the allow-any-origin attribute. The components log a one-time console.warn when messages are being ignored for a missing origin.
  • Source check. <TurboSignForm> and <turbosign-form> now require event.source to be their own iframe's contentWindow, so another same-origin window cannot forge turbosign:completed. handleTurboSignMessage stays pure and takes an optional expectedSource.
  • title prop / attribute for the iframe's accessible name (default TurboSign signing).
  • README: rewrote "Why origin pinning matters" and documented the 0.2.0 change. Also fixed the example link and dropped the placeholder lint script.

Tests: 54/54 passing (packages/embed), typecheck clean, npm pack contains only dist, README and LICENSE.

@nicolasiscoding
nicolasiscoding merged commit 476707f into main Oct 7, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants