Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/web-embed-on-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@simplepdf/web-embed-pdf': minor
---

**Your pages are now ready for AI agents.** While the editor is open, an agent in your visitor's browser (ChatGPT's browser, Chrome with WebMCP) can read and fill the form, move between pages and submit it, with the same permissions as any other integration. It is on by default: add `webmcp="false"` to the script tag to turn it off.

- **More PDF links open in SimplePDF**: `Report.PDF`, `form.pdf?v=2` and `guide.pdf#page=3` used to open in the browser.
- **Japanese and Dutch**: pages in either language open the editor in that language.
- **Stored documents open directly**: a link to a SimplePDF document (`/documents/<id>`) opens it in the editor.
- **Clear errors for invalid company identifiers**: a value that cannot form an editor address (a URL, spaces, dots, underscores) logs an error and opens nothing. An identifier without an account still opens the editor, which explains the problem.
- **Same script, same API**: now built on `@simplepdf/embed`, the engine behind the React component. `window.simplePDF`, the script-tag attributes and the modal are unchanged.
95 changes: 92 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 32 additions & 1 deletion web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ See [Data Privacy & companyIdentifier](../README.md#data-privacy--companyidentif

## How does it work?

**Anchor links (`a`) with an href pointing to a PDF file (`.pdf`) or [SimplePDF forms](https://simplepdf.com/portal) are automatically opened in [SimplePDF](https://simplepdf.com)**
**Anchor links (`a`) with an href pointing to a PDF file (`.pdf`, in any case, with or without a query string or fragment) or [SimplePDF forms](https://simplepdf.com/portal) are automatically opened in [SimplePDF](https://simplepdf.com)**

### I don't want every PDF document to be opened in SimplePDF

Expand Down Expand Up @@ -89,6 +89,8 @@ SimplePDF automatically detects the language of the page (using the `lang` attri
- Spanish (`es`)
- French (`fr`)
- Italian (`it`)
- Japanese (`ja`)
- Dutch (`nl`)
- Portuguese (`pt`)

**If you wish to override the automatic detection, you can specify the `locale` attribute on the script tag as follows**:
Expand All @@ -104,6 +106,35 @@ SimplePDF automatically detects the language of the page (using the `lang` attri
```
<!-- prettier-ignore-end -->

### In-browser agents (WebMCP)

While the editor is open, an agent running in the visitor's browser (ChatGPT's browser, Chrome with WebMCP) finds the editor's operations as tools on your page: read and fill fields, move between pages, submit. Every call goes through the same permissions as any other integration. Nothing happens in browsers without an agent.

To turn it off, add `webmcp="false"` to the script tag (any value other than `true` or `false` turns it off and logs an error):

<!-- prettier-ignore-start -->
```html
<script
src="https://unpkg.com/@simplepdf/web-embed-pdf"
companyIdentifier="yourcompany"
webmcp="false"
defer
></script>
```
<!-- prettier-ignore-end -->

Or from JavaScript:

```javascript
// Turn it off
window.simplePDF.setConfig({ webMCP: { enabled: false } });

// Keep it on, but withhold the operations a person must do themselves
window.simplePDF.setConfig({ webMCP: { enabled: true, exclude: ['submit'] } });
```

The option applies to the next editor opened: an editor already open keeps the tools it registered until it closes.

### Opening the editor programmatically

#### Open the editor with a specific PDF
Expand Down
6 changes: 5 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"start": "rollup -c -w"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^0.4.4",
"@types/jsdom": "^21.1.7",
"jsdom": "^26.0.0",
Expand All @@ -50,5 +51,8 @@
"typescript",
"npm",
"pdf"
]
],
"dependencies": {
"@simplepdf/embed": "0.7.0"
}
}
4 changes: 3 additions & 1 deletion web/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import typescript from 'rollup-plugin-typescript2';
import terser from '@rollup/plugin-terser';
import { nodeResolve } from '@rollup/plugin-node-resolve';

import pkg from './package.json' with { type: 'json' };

function createOutputConfig({ file, minify }) {
return {
file,
format: 'umd',
inlineDynamicImports: true,
name: 'simplePDF',
strict: true,
plugins: minify ? [terser({ format: { comments: false } })] : [],
Expand All @@ -22,6 +24,6 @@ export default {
minify: true,
}),
],
plugins: [typescript()],
plugins: [nodeResolve({ browser: true }), typescript()],
external: [],
};
33 changes: 33 additions & 0 deletions web/src/__tests__/fixtures/pdf-link-cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[
{ "href": "https://pdfobject.com/pdf/sample-3pp.pdf", "classes": [], "opens_in_simplepdf": true },
{ "href": "https://example.com/files/Consent.PDF", "classes": [], "opens_in_simplepdf": true },
{ "href": "https://example.com/uploads/intake-form.pdf?ver=2", "classes": [], "opens_in_simplepdf": true },
{ "href": "https://example.com/files/guide.pdf#page=3", "classes": [], "opens_in_simplepdf": true },
{ "href": "https://example.com/download.php?file=Form.PDF", "classes": [], "opens_in_simplepdf": true },
{ "href": "https://example.com/some-pdf-without-extension", "classes": ["simplepdf"], "opens_in_simplepdf": true },
{
"href": "https://yourcompany.simplepdf.com/form/d8d57ec7-f3e9-4fc9-8cc5-4a92c02d30d0",
"classes": [],
"opens_in_simplepdf": true
},
{
"href": "https://yourcompany.simplepdf.com/documents/d8d57ec7-f3e9-4fc9-8cc5-4a92c02d30d0",
"classes": [],
"opens_in_simplepdf": true
},
{ "href": "https://pdfobject.com/pdf/sample-3pp.pdf", "classes": ["exclude-simplepdf"], "opens_in_simplepdf": false },
{
"href": "https://yourcompany.simplepdf.com/form/d8d57ec7-f3e9-4fc9-8cc5-4a92c02d30d0",
"classes": ["exclude-simplepdf"],
"opens_in_simplepdf": false
},
{ "href": "https://www.pdfsomething.com/anything", "classes": [], "opens_in_simplepdf": false },
{ "href": "https://www.website.com/some-pdf", "classes": [], "opens_in_simplepdf": false },
{ "href": "https://www.website.com/some-other.pdf.png", "classes": [], "opens_in_simplepdf": false },
{
"href": "https://www.simplepdf.app/s/article/How-to-Manage-PDF-Settings",
"classes": [],
"opens_in_simplepdf": false
},
{ "href": "https://www.app.pdf/some-url", "classes": [], "opens_in_simplepdf": false }
]
91 changes: 91 additions & 0 deletions web/src/__tests__/open-editor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// @vitest-environment jsdom
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';

// The script tag passes untyped values: whatever a page puts in the attributes or in setConfig
// must either open the editor or refuse with a console error, never throw on the page.
describe('openEditor with untyped configuration', () => {
beforeAll(async () => {
await import('../index');
});

afterEach(() => {
window.simplePDF?.closeEditor();
window.simplePDF?.setConfig({ companyIdentifier: 'embed' });
vi.restoreAllMocks();
});

const openAndSettle = async (): Promise<void> => {
window.simplePDF?.openEditor({ href: null });
await new Promise((resolve) => setTimeout(resolve, 0));
};

const readEditorOrigin = (): string | null => {
const source = document.getElementById('simplePDF_iframe')?.getAttribute('src');
return source === undefined || source === null ? null : new URL(source).origin;
};

it('treats a null baseDomain as absent and opens the production editor', async () => {
Object.assign(window.simplePDF?.config ?? {}, { baseDomain: null });

await expect(openAndSettle()).resolves.toBeUndefined();

expect(readEditorOrigin()).toBe('https://embed.simplepdf.com');
Object.assign(window.simplePDF?.config ?? {}, { baseDomain: undefined });
});

it.each([
['simplepdf.com:00443', 'https://embed.simplepdf.com'],
['SIMPLEPDF.COM:08443', 'https://embed.simplepdf.com:8443'],
])('accepts the editor messages when baseDomain is %s', async (baseDomain, editorOrigin) => {
const registeredTools: string[] = [];
Object.defineProperty(document, 'modelContext', {
configurable: true,
value: {
registerTool: (tool: { name: string }) => {
registeredTools.push(tool.name);
},
},
});
window.simplePDF?.setConfig({ baseDomain });

await openAndSettle();
const iframe = document.getElementById('simplePDF_iframe');
if (!(iframe instanceof HTMLIFrameElement)) {
throw new Error('The editor iframe was not created');
}
window.dispatchEvent(
new MessageEvent('message', {
data: JSON.stringify({ type: 'EDITOR_READY', data: {} }),
origin: editorOrigin,
source: iframe.contentWindow,
}),
);

await vi.waitFor(() => expect(registeredTools).toContain('simplepdf_embed_get_fields'));
Object.assign(window.simplePDF?.config ?? {}, { baseDomain: undefined });
});

it.each(['simplepdf.com@evil.example', 'evil.example/x', '-.com', 'simplepdf', 42])(
'refuses the baseDomain %s with a console error and opens nothing',
async (baseDomain) => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
Object.assign(window.simplePDF?.config ?? {}, { baseDomain });

await openAndSettle();

expect(document.getElementById('simplePDF_modal')).toBeNull();
expect(consoleError).toHaveBeenCalledWith(expect.stringContaining('is not a domain name'));
Object.assign(window.simplePDF?.config ?? {}, { baseDomain: undefined });
},
);

it('reports a companyIdentifier that is not a string and closes the modal instead of throwing', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
Object.assign(window.simplePDF?.config ?? {}, { companyIdentifier: 123 });

await expect(openAndSettle()).resolves.toBeUndefined();

expect(document.getElementById('simplePDF_modal')).toBeNull();
expect(consoleError).toHaveBeenCalledWith('@simplepdf/web-embed-pdf: the editor could not open', expect.anything());
});
});
Loading
Loading