diff --git a/.changeset/recipient-links-rename.md b/.changeset/recipient-links-rename.md new file mode 100644 index 00000000..628cab3b --- /dev/null +++ b/.changeset/recipient-links-rename.md @@ -0,0 +1,5 @@ +--- +'@simplepdf/embed': patch +--- + +**Docs: a recipient link opens with `?link=`.** A SimplePDF documents URL carries its recipient link as `?link=`; the editor no longer reads `?prefill=`. diff --git a/copilot/README.md b/copilot/README.md index 79ba43a1..4a3e349d 100644 --- a/copilot/README.md +++ b/copilot/README.md @@ -144,13 +144,13 @@ See the privacy notes above for the per-route audio-egress disclosure. ### Load a specific document via `?url=` -To open a specific document instead of the bundled demo forms, append `?url=`. The value is passed as the `document` to ``; a SimplePDF documents URL (a `/documents/` link, optionally with a `?prefill=`) is navigated to directly by the embed core, so pass a valid one: +To open a specific document instead of the bundled demo forms, append `?url=`. The value is passed as the `document` to ``; a SimplePDF documents URL (a `/documents/` link, optionally with a recipient link's `?link=`) is navigated to directly by the embed core, so pass a valid one: ``` -http://localhost:3001/?url=https%3A%2F%2Fdemo.simplepdf.com%2Fdocuments%2Fc28f061b-1974-4251-ba7a-d08bedc3ef28%3Fprefill%3D35fdf39e-2e06-4712-bb9d-f62d2f88ce50 +http://localhost:3001/?url=https%3A%2F%2Fdemo.simplepdf.com%2Fdocuments%2Fc28f061b-1974-4251-ba7a-d08bedc3ef28%3Flink%3D35fdf39e-2e06-4712-bb9d-f62d2f88ce50 ``` -URL-encode the value whenever it carries its own query string (e.g. `?prefill=`), otherwise the nested params get parsed as part of the page URL and dropped. +URL-encode the value whenever it carries its own query string (e.g. `?link=`), otherwise the nested params get parsed as part of the page URL and dropped. The value must be an absolute `http(s)` URL on your configured base-domain family (`*.simplepdf.com` by default, or whatever host `VITE_SIMPLEPDF_BASE_DOMAIN` resolves to). Third-party origins are rejected on purpose: the iframe is granted clipboard access and is wired to the `postMessage` bridge, so framing an arbitrary site would hand it both. A malformed or off-domain `?url=` silently falls back to the default demo form. `?url=` combines with `?lang=`; when set, it wins for what the editor loads. diff --git a/copilot/src/routes/index.tsx b/copilot/src/routes/index.tsx index dfc0779e..a1a4567b 100644 --- a/copilot/src/routes/index.tsx +++ b/copilot/src/routes/index.tsx @@ -88,7 +88,7 @@ const isModelTab = (value: unknown): value is ModelTab => // its origin becomes the iframe origin AND the postMessage bridge target — it must // therefore be a valid document URL on that family (e.g. any `*.simplepdf.com` // tenant), such as -// https://demo.simplepdf.com/documents/c28f061b-1974-4251-ba7a-d08bedc3ef28?prefill=35fdf39e-2e06-4712-bb9d-f62d2f88ce50 +// https://demo.simplepdf.com/documents/c28f061b-1974-4251-ba7a-d08bedc3ef28?link=35fdf39e-2e06-4712-bb9d-f62d2f88ce50 // Rejecting everything else keeps a crafted `?url=javascript:...`, a relative // path resolving against our own origin, or a third-party origin (which would // be framed with our clipboard permissions and wired to the bridge) out of the diff --git a/embed/README.md b/embed/README.md index 462a3664..6e992a79 100644 --- a/embed/README.md +++ b/embed/README.md @@ -151,7 +151,7 @@ createEmbed({ target, companyIdentifier: 'acme', document: { dataUrl: 'data:appl createEmbed({ target, companyIdentifier: 'acme', document: { file: pdfFileOrBlob } }) ``` -- **`url`**: any `http(s)` URL. Fetched from your page first (50 MB cap); on CORS / size / network failure it falls back to the editor's `?open` loader, so CORS-restricted public URLs still load. `user:pass@` credentials are allowed (they route via `?open`). A **SimplePDF documents URL** on your base-domain family (e.g. `https://acme.simplepdf.com/documents/?prefill=`) is navigated to directly, so the editor loads + prefills the stored document itself (your `context` is carried through). +- **`url`**: any `http(s)` URL. Fetched from your page first (50 MB cap); on CORS / size / network failure it falls back to the editor's `?open` loader, so CORS-restricted public URLs still load. `user:pass@` credentials are allowed (they route via `?open`). A **SimplePDF documents URL** on your base-domain family (e.g. `https://acme.simplepdf.com/documents/?link=`) is navigated to directly, so the editor loads + prefills the stored document itself (your `context` is carried through). - **`file`**: a `File` (e.g. from ``) or any `Blob`. Converted for you, no `FileReader`. - **`dataUrl`**: a `data:` URL string. diff --git a/embed/embed-api.json b/embed/embed-api.json index 41bc6286..65df2c4f 100644 --- a/embed/embed-api.json +++ b/embed/embed-api.json @@ -1,5 +1,5 @@ { - "editor_version": "7845f7c3-20261014T100758Z", + "editor_version": "6f020890-20261114T210959Z", "description": "This is the SimplePDF editor interface contract. Drive the editor programmatically over window.postMessage with the editor iframe: post a JSON string { \"type\": {{operation request_type}}, \"request_id\": {{a request correlation id you generate}}, \"data\": {{input matching input_schema}} } to the iframe; the editor replies with { \"type\": \"REQUEST_RESULT\", \"data\": { \"request_id\": {{the same correlation id}}, \"result\": {{result}} } } (match each reply to its request by this id), where result is { \"success\": true, \"data\": {{a value matching the operation's output_schema; null for ops that return nothing}} } or { \"success\": false, \"error\": { code, message } }. Each operation lists the op-specific `error_codes` it can return; on top of those, any op may also fail with a gateway/permission code (origin/plan/signup gating, editor-not-ready, or an internal error). Every code is within `editor_error_schema` (the complete closed union to narrow against), where each code const carries a `description` of its meaning. Outbound events (see `events`) are pushed the same way. `operations` lists every operation the editor supports. Most require the embedding origin to be allowlisted (\"whitelisted\") for the tenant in the SimplePDF admin dashboard, and the tenant plan to permit them (LOAD_DOCUMENT is always available). A call the current setup does not permit returns the matching gateway code: forbidden:origin_not_whitelisted when the origin is not allowlisted, or bad_request:plan_upgrade_required when the plan excludes it. All JSON schemas use the json_schema_dialect declared at the root. We recommend the @simplepdf/embed package for a typed, ergonomic wrapper over this contract (https://github.com/SimplePDF/simplepdf-embed).", "json_schema_dialect": "https://json-schema.org/draft/2020-12/schema", "protocol": { @@ -1263,7 +1263,7 @@ { "type": "string", "const": "bad_request:failed_to_load_document", - "description": "The document LOAD_DOCUMENT was given could not be loaded (unreachable, not a PDF or image, corrupted, password-protected, superseded by another load, or the editor could not prepare it: the server never resolved it, or storing it failed); the message says which." + "description": "The document LOAD_DOCUMENT was given could not be loaded (unreachable, not a PDF or image, corrupted, password-protected, superseded by another load, or the editor could not prepare it: the server never resolved it, storing it failed, or it carries more form fields than a document can); the message says which." }, { "type": "string", diff --git a/embed/src/mount.ts b/embed/src/mount.ts index bf19eeb4..c396c4b8 100644 --- a/embed/src/mount.ts +++ b/embed/src/mount.ts @@ -286,7 +286,7 @@ const assertValidDocument = (document: unknown): void => { } // A SimplePDF "documents" URL is a stored document / share route -// (e.g. https://.simplepdf.com/documents/?prefill=), +// (e.g. https://.simplepdf.com/documents/?link=), // not a fetchable PDF. The path is /documents/, optionally locale-prefixed. const DOCUMENTS_PATH_PATTERN = /^\/(?:[a-z]{2}\/)?documents\/[^/]+\/?$/ @@ -631,7 +631,7 @@ const mountIntoContainer = ( ? ((): string => { // Navigate straight to the stored document; carry the context through // (the editor decodes ?context= on the documents route too) while - // preserving the URL's own query, e.g. ?prefill=. + // preserving the URL's own query, e.g. ?link=. const src = new URL(documentsUrl.url.href) if (encodedContext !== null) { src.searchParams.set('context', encodedContext) diff --git a/embed/test/mount.test.ts b/embed/test/mount.test.ts index 7c3cc9a5..33f76536 100644 --- a/embed/test/mount.test.ts +++ b/embed/test/mount.test.ts @@ -215,7 +215,7 @@ describe(createEmbed.name, () => { target: '#root', companyIdentifier: 'acme', context: { a: 1 }, - document: { url: 'https://demo.simplepdf.com/documents/abc-123?prefill=p-9' }, + document: { url: 'https://demo.simplepdf.com/documents/abc-123?link=p-9' }, }) const iframe = document.querySelector('#root iframe') if (!(iframe instanceof HTMLIFrameElement)) { @@ -225,7 +225,7 @@ describe(createEmbed.name, () => { // Bridges to the URL's own subdomain (demo), not the configured companyIdentifier (acme). expect(src.origin).toBe('https://demo.simplepdf.com') expect(src.pathname).toBe('/documents/abc-123') - expect(src.searchParams.get('prefill')).toBe('p-9') + expect(src.searchParams.get('link')).toBe('p-9') expect(src.searchParams.get('context')).not.toBeNull() // It is a direct navigation, not the host-fetch /editor path. expect(src.searchParams.get('loadingPlaceholder')).toBeNull() diff --git a/react/src/embed-pdf.test.tsx b/react/src/embed-pdf.test.tsx index 5582748d..34c4eb97 100644 --- a/react/src/embed-pdf.test.tsx +++ b/react/src/embed-pdf.test.tsx @@ -93,7 +93,7 @@ describe('EmbedPDF (inline)', () => { , ); const iframe = container.querySelector('iframe'); @@ -103,7 +103,7 @@ describe('EmbedPDF (inline)', () => { const src = new URL(iframe.getAttribute('src') ?? ''); expect(src.origin).toBe('https://demo.simplepdf.com'); expect(src.pathname).toBe('/documents/abc'); - expect(src.searchParams.get('prefill')).toBe('p1'); + expect(src.searchParams.get('link')).toBe('p1'); }); it('accepts the deprecated documentURL as an alias for document (deprecated alias)', () => { @@ -172,7 +172,7 @@ describe('EmbedPDF (modal, default)', () => { const user = userEvent.setup(); render( - Open + Open , ); await user.click(screen.getByText('Open')); diff --git a/skills/build-with-simplepdf/SKILL.md b/skills/build-with-simplepdf/SKILL.md index b8edafc7..f86c7a97 100644 --- a/skills/build-with-simplepdf/SKILL.md +++ b/skills/build-with-simplepdf/SKILL.md @@ -292,12 +292,12 @@ Canonical flow (relative to the API base in "REST API" below): 1. `GET /documents/{document_id}/fields` — each field has a stable `id`, a `type`, and (for constrained fields) allowed `options` 2. map known values to stable field ids and allowed options -3. `POST /documents/{document_id}/prefills` — returns the prefill `id`, an `upload` object (where and how to send the values), and an `embed_url` +3. `POST /documents/{document_id}/recipient_links` — creates a recipient link (a unique, revocable URL for one person, agent or integration; you deliver it, and anyone holding it can open it) and returns its `id`, an `upload` object (where and how to send the values), and an `embed_url` 4. upload `{ "fields": [{ "id", "value" }] }` per the `upload` object, straight to the customer storage 5. open/share the returned `embed_url` 6. the human reviews, corrects, signs and submits -Rules: keep sensitive values in the prefill blob, not webhook `context`; unknown field ids are ignored and read-only fields ignore prefill values; a field listing `options` only accepts one of them (checkboxes, dropdowns, radios); otherwise the value is a plain string (text fields) or a base64 data URL (signature and picture fields). When mapping from natural-language data, validate constrained values before writing the blob and never let the model invent a field id when the field schema is available. Prefills are deletable (`DELETE /documents/{document_id}/prefills/{prefill_id}`) — clean up prefills that were superseded or never opened. +Rules: keep sensitive values in the prefill blob, not webhook `context`; unknown field ids are ignored and read-only fields ignore prefill values; a field listing `options` only accepts one of them (checkboxes, dropdowns, radios); otherwise the value is a plain string (text fields) or a base64 data URL (signature and picture fields). When mapping from natural-language data, validate constrained values before writing the blob and never let the model invent a field id when the field schema is available. Recipient links are deletable (`DELETE /documents/{document_id}/recipient_links/{recipient_link_id}`) — clean up recipient links that were superseded or never opened. ## Post-submit @@ -321,7 +321,7 @@ Use `submission.created` for downstream automation. The delivery carries metadat ## Storage -Only when submissions/prefills/persistence make it relevant, and after checking whether the app already has an established storage boundary (`AskUserQuestion`, header `Storage`): +Only when submissions/recipient links/persistence make it relevant, and after checking whether the app already has an established storage boundary (`AskUserQuestion`, header `Storage`): **"Where should submitted PDFs and prefill data live?"** @@ -336,7 +336,7 @@ Supported BYOS families: S3-compatible object storage, Azure Blob Storage, Share ## REST API -Only when the backend needs lifecycle control beyond browser editor actions/webhooks. Server-side resources: documents, field schemas, prefills, submissions. +Only when the backend needs lifecycle control beyond browser editor actions/webhooks. Server-side resources: documents, field schemas, recipient links, submissions. ```text https://{companyIdentifier}.simplepdf.com/api/v1