-
Notifications
You must be signed in to change notification settings - Fork 444
[Client] Add addRemoteOrigin() runtime API for extra iframe origins #3561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
apeatling
wants to merge
6
commits into
WordPress:trunk
Choose a base branch
from
apeatling:add-runtime-remote-origins
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
950a70a
[Client] Add addRemoteOrigin() runtime API for extra iframe origins
apeatling 1ce071c
Merge branch 'trunk' into add-runtime-remote-origins
apeatling 85c11d9
Merge branch 'trunk' into add-runtime-remote-origins
apeatling 1892d99
Merge branch 'trunk' into add-runtime-remote-origins
apeatling cd30f4e
Merge branch 'trunk' into add-runtime-remote-origins
apeatling 2b28b4d
Merge branch 'trunk' into add-runtime-remote-origins
apeatling File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
163 changes: 163 additions & 0 deletions
163
packages/playground/client/src/runtime-remote-origins.spec.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| describe('addRemoteOrigin', () => { | ||
| let addRemoteOrigin: (origin: string) => void; | ||
| let getRuntimeRemoteOrigins: () => readonly string[]; | ||
|
|
||
| beforeEach(async () => { | ||
| // Reset the module so each test starts with an empty registry. | ||
| vi.resetModules(); | ||
| ({ addRemoteOrigin, getRuntimeRemoteOrigins } = | ||
| await import('./runtime-remote-origins')); | ||
| }); | ||
|
|
||
| describe('valid origins', () => { | ||
| it('should accept a basic https origin', () => { | ||
| addRemoteOrigin('https://example.com'); | ||
| expect(getRuntimeRemoteOrigins()).toEqual(['https://example.com']); | ||
| }); | ||
|
|
||
| it('should accept an http origin', () => { | ||
| addRemoteOrigin('http://example.com'); | ||
| expect(getRuntimeRemoteOrigins()).toEqual(['http://example.com']); | ||
| }); | ||
|
|
||
| it('should accept an origin with a non-default port', () => { | ||
| addRemoteOrigin('http://localhost:5401'); | ||
| expect(getRuntimeRemoteOrigins()).toEqual([ | ||
| 'http://localhost:5401', | ||
| ]); | ||
| }); | ||
|
|
||
| it('should accept a bracketed IPv6 origin', () => { | ||
| addRemoteOrigin('http://[::1]:8080'); | ||
| expect(getRuntimeRemoteOrigins()).toEqual(['http://[::1]:8080']); | ||
| }); | ||
|
|
||
| it('should accept a punycode host', () => { | ||
| addRemoteOrigin('https://xn--exmple-cua.com'); | ||
| expect(getRuntimeRemoteOrigins()).toEqual([ | ||
| 'https://xn--exmple-cua.com', | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('invalid origins', () => { | ||
| it('should reject an empty string', () => { | ||
| expect(() => addRemoteOrigin('')).toThrow("Invalid origin: ''"); | ||
| }); | ||
|
|
||
| it('should reject an unparseable string', () => { | ||
| expect(() => addRemoteOrigin('not a url')).toThrow( | ||
| "Invalid origin: 'not a url'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject an origin with a path', () => { | ||
| expect(() => addRemoteOrigin('https://example.com/path')).toThrow( | ||
| "Invalid origin: 'https://example.com/path'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject an origin with a trailing slash', () => { | ||
| expect(() => addRemoteOrigin('https://example.com/')).toThrow( | ||
| "Invalid origin: 'https://example.com/'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject an origin with a query string', () => { | ||
| expect(() => addRemoteOrigin('https://example.com?x=1')).toThrow( | ||
| "Invalid origin: 'https://example.com?x=1'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject an origin with a fragment', () => { | ||
| expect(() => addRemoteOrigin('https://example.com#frag')).toThrow( | ||
| "Invalid origin: 'https://example.com#frag'" | ||
| ); | ||
| }); | ||
|
|
||
| it("should reject an origin with the scheme's default port specified", () => { | ||
| // `new URL('https://example.com:443').href === 'https://example.com/'`, | ||
| // so the round-trip strict-equality check rejects the input. | ||
| expect(() => addRemoteOrigin('https://example.com:443')).toThrow( | ||
| "Invalid origin: 'https://example.com:443'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject an origin with an uppercase scheme', () => { | ||
| // The URL parser normalizes the scheme to lowercase, so the | ||
| // round-trip strict-equality check rejects upper-case input. | ||
| expect(() => addRemoteOrigin('HTTPS://example.com')).toThrow( | ||
| "Invalid origin: 'HTTPS://example.com'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject an origin with a non-ASCII host', () => { | ||
| // IDN hosts must be supplied in punycode form; otherwise the | ||
| // URL parser normalizes the host and the round-trip check fails. | ||
| expect(() => addRemoteOrigin('https://exämple.com')).toThrow( | ||
| "Invalid origin: 'https://exämple.com'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject an origin with userinfo', () => { | ||
| // `URL.origin` strips userinfo, so an origin with userinfo can | ||
| // never match the iframe's `url.origin` at validation time. | ||
| expect(() => | ||
| addRemoteOrigin('https://user:pass@example.com') | ||
| ).toThrow("Invalid origin: 'https://user:pass@example.com'"); | ||
| }); | ||
|
|
||
| it('should reject a `ws://` origin', () => { | ||
| expect(() => addRemoteOrigin('ws://example.com')).toThrow( | ||
| "Invalid origin: 'ws://example.com'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject a `file://` origin', () => { | ||
| expect(() => addRemoteOrigin('file:///tmp/x')).toThrow( | ||
| "Invalid origin: 'file:///tmp/x'" | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject a `data:` URL', () => { | ||
| expect(() => addRemoteOrigin('data:text/plain,hi')).toThrow( | ||
| "Invalid origin: 'data:text/plain,hi'" | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('accumulation', () => { | ||
| it('should accumulate across calls in insertion order', () => { | ||
| addRemoteOrigin('https://a.example.com'); | ||
| addRemoteOrigin('https://b.example.com'); | ||
| addRemoteOrigin('https://c.example.com'); | ||
| expect(getRuntimeRemoteOrigins()).toEqual([ | ||
| 'https://a.example.com', | ||
| 'https://b.example.com', | ||
| 'https://c.example.com', | ||
| ]); | ||
| }); | ||
|
|
||
| it('should not deduplicate when the same origin is added twice', () => { | ||
| addRemoteOrigin('https://example.com'); | ||
| addRemoteOrigin('https://example.com'); | ||
| expect(getRuntimeRemoteOrigins()).toEqual([ | ||
| 'https://example.com', | ||
| 'https://example.com', | ||
| ]); | ||
| }); | ||
|
|
||
| it('should not register an origin when validation throws', () => { | ||
| expect(() => addRemoteOrigin('not a url')).toThrow(); | ||
| expect(getRuntimeRemoteOrigins()).toEqual([]); | ||
| }); | ||
|
|
||
| it('should preserve previously registered origins when a later call is rejected', () => { | ||
| addRemoteOrigin('https://first.example.com'); | ||
| expect(() => addRemoteOrigin('not a url')).toThrow(); | ||
| expect(getRuntimeRemoteOrigins()).toEqual([ | ||
| 'https://first.example.com', | ||
| ]); | ||
| }); | ||
| }); | ||
| }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { validateOrigin } from './validate-origin'; | ||
|
|
||
| // Origins added at runtime via `addRemoteOrigin`. Kept separate from the | ||
| // build-time `additionalRemoteOrigins` array (see `additional-remote-origins.ts`) | ||
| // because the vite plugin in this package's `vite.config.ts` rewrites the | ||
| // source of that file when `ADDITIONAL_REMOTE_ORIGINS` is set, and would | ||
| // otherwise clobber any runtime registrations colocated with it. | ||
| const runtimeRemoteOrigins: string[] = []; | ||
|
|
||
| /** | ||
| * Returns the origins registered at runtime via {@link addRemoteOrigin}. | ||
| * | ||
| * @internal — exposed for use by the assertion in `index.ts` and by tests. | ||
| * Not part of the public package API. | ||
| */ | ||
| export function getRuntimeRemoteOrigins(): readonly string[] { | ||
| return runtimeRemoteOrigins; | ||
| } | ||
|
|
||
| /** | ||
| * Register an additional origin that is a valid host for Playground's | ||
| * `remote.html` iframe. | ||
| * | ||
| * Use this when embedding Playground from an origin that isn't part of | ||
| * the built-in allowlist (`playground.wordpress.net`, `wasm.wordpress.net`, | ||
| * the embedding page's own origin, or `localhost`/`127.0.0.1` on the | ||
| * default dev port). Call this before the `startPlaygroundWeb()` | ||
| * invocation that should see the new origin. Multiple calls accumulate: | ||
| * each call appends an entry, and duplicate origins are preserved (they | ||
| * are not deduplicated or treated as a no-op). | ||
| * | ||
| * The origin must be in canonical form: an `http://` or `https://` scheme, | ||
| * a lowercase ASCII (or punycode) host, an optional non-default port, and | ||
| * nothing else. No userinfo, path, query, fragment, or trailing slash. | ||
| * Invalid input throws and the registration is rejected. | ||
| * | ||
| * @example | ||
| * import { addRemoteOrigin, startPlaygroundWeb } from '@wp-playground/client'; | ||
| * | ||
| * addRemoteOrigin('https://playground.example.com'); | ||
| * await startPlaygroundWeb({ ... }); | ||
| */ | ||
| export function addRemoteOrigin(origin: string): void { | ||
| validateOrigin(origin); | ||
| runtimeRemoteOrigins.push(origin); | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /** | ||
| * Assert that a string is a canonical origin: an `http://` or `https://` | ||
| * scheme followed by a host (with an optional non-default port) and | ||
| * nothing else — no userinfo, no path, no query, no fragment. | ||
| * | ||
| * Used by both the build-time `ADDITIONAL_REMOTE_ORIGINS` env-var seam | ||
| * (see `vite.config.ts`) and the runtime `addRemoteOrigin` API | ||
| * (see `runtime-remote-origins.ts`) so the two paths stay in lockstep. | ||
| * | ||
| * @throws if the input is not a canonical origin. The thrown message | ||
| * includes a short reason so build-time misconfigurations are easy to | ||
| * diagnose from a CI log. | ||
| * | ||
| * @internal | ||
| */ | ||
| export function validateOrigin(origin: string): void { | ||
| let url: URL; | ||
| try { | ||
| url = new URL(origin); | ||
| } catch { | ||
| throw invalidOriginError(origin, 'not a valid URL'); | ||
| } | ||
| if (url.protocol !== 'http:' && url.protocol !== 'https:') { | ||
| throw invalidOriginError(origin, 'must use http:// or https://'); | ||
| } | ||
| if (url.username !== '' || url.password !== '') { | ||
| throw invalidOriginError( | ||
| origin, | ||
| 'must not include username or password' | ||
| ); | ||
| } | ||
| // Round-trip check rejects any input that the URL parser had to | ||
| // normalize: paths, queries, fragments, trailing slashes, uppercase | ||
| // schemes, and ports that match the scheme's default. | ||
| if (url.href !== `${origin}/`) { | ||
| throw invalidOriginError( | ||
| origin, | ||
| 'must be a canonical http(s) origin with no path, query, fragment, trailing slash, or default port' | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function invalidOriginError(origin: string, reason: string): Error { | ||
| return new Error(`Invalid origin: '${origin}' (${reason})`); | ||
| } | ||
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason to limit this?