@orbital.vision/prophecy
v1.2.0
Published
Embeddable AI room visualisation SDK from Orbital Vision
Readme
Prophecy
Prophecy is Orbital Vision's embeddable AI room-visualisation SDK. It owns the browser experience, account-history lifecycle and integration hooks; OV25 remains the authenticated API and generation control plane.
The default modal always includes the branded attribution Powered by OV25 from Orbital Vision and the Orbital Vision mark in its footer.
The same release supports:
- a single declarative
<script>tag; - programmatic browser installation;
- npm/ES module consumers;
- anonymous or retailer-authenticated sessions;
- account-owned room photos and generated-result history.
Product image sources
Prophecy supports two mutually exclusive product sources:
- OV25 configurator: when
configuratoris omitted, Prophecy finds the first HTTPS iframe whose host is exactlyconfigurator.orbital.vision, reads its public organisation/product identity and requests 1–4 rendered product views over its existingpostMessagecontract. - External catalogue: pass
productReferenceplusproductImages. Images may be JPEG, PNG or WebP blobs, or CORS-readable HTTP(S) URLs. Supply 1–4 views of the exact same product; each image may be at most 12 MB.
External products require a same-origin sessionEndpoint or sessionProvider. The retailer backend exchanges its private Orbital API key for the short-lived browser token; OV25 derives organizationId from that key and returns it in the public session. A private key and organisation ID must never be passed to Prophecy for partner authentication.
Script-tag installation
Publish Prophecy to npm and load its browser bundle through jsDelivr. npm is the package registry; jsDelivr is the globally cached CDN layer designed to serve the script to site visitors.
The documented URL carries the major version only. Fixes and new features reach every integration without anyone editing a script tag, and a breaking change is a deliberate move to the next major. A retailer who needs to freeze a specific build can pin the exact version instead, and takes on updating it themselves.
<iframe
id="product-configurator"
src="https://configurator.orbital.vision/89-public-key/7218"
></iframe>
<script src="https://cdn.jsdelivr.net/npm/@orbital.vision/prophecy@1/dist/prophecy.js"></script>That is the entire OV25 integration. The tag needs no attributes and the site needs no backend: Prophecy discovers the configurator.orbital.vision iframe, reads the organisation, public key and product out of its URL, follows its current configuration and mounts a safe floating launcher. The created instance is exposed as window.prophecy.
Prophecy waits for the configurator rather than requiring it at parse time, so an iframe mounted after hydration, or on a later route of a single-page site, still activates. Include the tag once; a second copy will not mount a second launcher.
To place the launcher yourself, mark the button in your own markup:
<button type="button" data-prophecy-launcher>See it in your room</button>Prophecy attaches to the first marked element, follows one that a single-page site mounts and unmounts per route, and stops offering its own floating launcher once a page has marked one.
Attributes only override discovery: data-prophecy-configurator for a specific iframe, data-prophecy-button for a fixed trigger, data-prophecy-endpoint for a non-production API host, data-prophecy-auto="false" to install programmatically instead. data-prophecy-session-endpoint="/api/orbital/prophecy/session" switches to the authenticated exchange below.
An external product can also be installed declaratively:
<button id="see-in-room" type="button">See it in your room</button>
<script
src="https://cdn.jsdelivr.net/npm/@orbital.vision/prophecy@1/dist/prophecy.js"
data-prophecy-button="#see-in-room"
data-prophecy-product-reference="shopify:variant:SKU-42"
data-prophecy-product-images='["https://shop.example/images/sku-42-front.webp","https://shop.example/images/sku-42-side.webp"]'
data-prophecy-product-url="https://shop.example/products/sku-42"
data-prophecy-session-endpoint="/api/orbital/prophecy/session"
></script>For hooks, account history or custom controls, load the same script and initialise it programmatically:
<script src="https://cdn.jsdelivr.net/npm/@orbital.vision/prophecy@1/dist/prophecy.js"></script>
<script>
const prophecy = Prophecy.create({
configurator: '#product-configurator',
endpoint: 'https://demo.orbital.vision',
accountKey: signedInAccountVersion,
sessionEndpoint: '/api/orbital/prophecy/session',
history: accountHistory,
onResult(result) {
analytics.track('prophecy_generation_complete', result);
}
});
prophecy.mount('#prophecy-launcher');
</script>accountKey is a stable, non-sensitive browser value used only to notice authentication changes. It is never sent to Orbital. On login, logout or account switching, call:
await prophecy.setAccountContext(nextAccountKey);This immediately invalidates cached browser sessions, aborts stale work, removes the previous account's photos/results from memory and reloads the new account's history. Do not use an email address or access token as the key.
npm installation
npm install @orbital.vision/prophecyimport Prophecy from '@orbital.vision/prophecy';
const prophecy = Prophecy.create({
endpoint: 'https://demo.orbital.vision',
sessionEndpoint: '/api/orbital/prophecy/session'
});For any non-OV25 product:
const prophecy = Prophecy.create({
endpoint: 'https://demo.orbital.vision',
productReference: 'shopify:variant:SKU-42',
productImages: [frontImageUrl, sideImageUrl, detailImageBlob],
productUrl: location.href,
sessionEndpoint: '/api/orbital/prophecy/session'
});
// SPA navigation: atomically replace identity, images and result CTA URL.
prophecy.setProduct({
reference: 'shopify:variant:SKU-43',
images: [nextFrontUrl, nextSideUrl],
url: nextProductUrl
});The package ships an ES module, an IIFE browser bundle, source maps and TypeScript declarations. window.OrbitalRoomAI remains as a deprecated migration alias for existing OV25 embeds.
The browser bundle is also available from unpkg at https://unpkg.com/@orbital.vision/prophecy@1/dist/prophecy.js. jsDelivr is the documented default; npm itself should not be used as the browser asset server.
Availability preflight
In production, Prophecy asks whether the site may offer Room AI before showing or enabling its launcher. For an anonymous OV25 install that is GET /api/configurator/room-ai/availability, which checks the public key against the organisation, that the origin is authorised, and that Room AI is enabled. It opens no session, spends none of the shopper's generation allowance and does not depend on the product, so one cached answer serves a whole catalogue and the result is fetched once per page. A session is created when the shopper actually opens the preview. Installs that use sessionEndpoint or sessionProvider still preflight by requesting the partner session, because only the retailer's backend can say whether one is obtainable.
If the preflight fails, Prophecy keeps the site-owned button hidden, emits availability-change and error, and logs the reason to the console instead of opening a broken dialog.
Use await prophecy.checkAvailability() when a custom interface needs the result directly. Localhost is development mode by default: the launcher stays available and session errors render inside the dialog for inspection. Set development explicitly only when a non-local development hostname needs the same behaviour.
Access modes
Each organisation picks one on Dashboard → Prophecy:
| Mode | Anonymous sessions | Retailer needs |
| --- | --- | --- |
| open (default) | allowed | The script tag |
| free-trial | allowed until the browser's daily free allowance is spent | The script tag, plus a session endpoint |
| signed-in | refused | A session endpoint |
free-trial counts against a marker Prophecy stores in the browser and sends with each session request. The server hashes it with the organisation ID before it reaches a counter. Whoever holds the browser can clear it, so it paces shoppers towards signing in rather than capping spend; the organisation's daily ceilings and token balance do that.
When the allowance is spent the generation request answers 403 { code: 'SIGN_IN_REQUIRED' }, Prophecy emits sign-in-required instead of a generation error, and the page starts its own login. getState() reports access and freeGenerations.
Prophecy.create({
sessionEndpoint: '/api/orbital/prophecy/session',
onSignInRequired({ freeGenerations }) {
showSignInPrompt(`You have used your ${freeGenerations} free previews.`);
}
});Authenticated session exchange
The retailer backend derives its customer from its own signed-in session and exchanges a Prophecy server key for a short-lived browser token. That key type only buys previews: unlike the general private API key it cannot read the organisation's products, swatches or swatch rules.
const account = await requireSignedInAccount(request);
const { productId, productReference, productSource } = await request.json();
const response = await fetch(
'https://demo.orbital.vision/api/configurator/room-ai/server/session',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.ORBITAL_PROPHECY_SERVER_KEY}`
},
body: JSON.stringify({
productId,
productReference,
productSource,
origin: new URL(request.url).origin,
customerId: account.id
})
}
);The browser calls a same-origin retailer endpoint through sessionEndpoint. That endpoint must require the retailer's normal authenticated session, derive customerId server-side, enforce CSRF/origin protections and validate the requested productId or productReference against the product currently being viewed. OV25 products use productSource: 'ov25-configurator'; supplied-image products use productSource: 'external-images' and may have a null productId.
Account history
History belongs in the retailer's database and private object storage - not browser local storage. Each route must derive the account from the retailer session rather than accepting an account ID from the SDK.
const accountHistory = {
async load({ signal }) {
const response = await fetch('/api/orbital/prophecy/history', { signal });
if (!response.ok) throw new Error('History could not be loaded');
return response.json();
},
async saveRoom(photo) {
const body = new FormData();
body.append('file', photo.blob, 'room.jpg');
body.append('width', String(photo.width));
body.append('height', String(photo.height));
body.append('aspectRatio', photo.aspectRatio);
const response = await fetch('/api/orbital/prophecy/rooms', {
method: 'POST',
body,
signal: photo.signal
});
if (!response.ok) throw new Error('Room photo could not be saved');
return response.json(); // { id }
},
async saveResult(result) {
const response = await fetch('/api/orbital/prophecy/results', {
method: 'POST',
keepalive: true,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result),
signal: result.signal
});
if (!response.ok) throw new Error('Result could not be saved');
return response.json();
},
async clear({ signal }) {
const response = await fetch('/api/orbital/prophecy/history', {
method: 'DELETE',
signal
});
if (!response.ok) throw new Error('History could not be cleared');
}
};Prophecy serialises writes and deletion so a pending save cannot recreate data after clearHistory(). Adapters should honour the supplied AbortSignal. Returned room-photo URLs must be same-origin or CORS-enabled; Prophecy streams them sequentially with 12 MB per-image and 48 MB total restoration limits.
Before saving a generated result, verify the job from the retailer backend:
const verified = await fetch(
`https://demo.orbital.vision/api/configurator/room-ai/server/jobs/${jobId}`,
{
headers: {
Authorization: `Bearer ${process.env.ORBITAL_PROPHECY_SERVER_KEY}`,
'X-Orbital-Customer-Id': account.id
}
}
).then(response => response.json());
if (verified.status !== 'COMPLETED' || !verified.resultUrl) {
throw new Error('Unverified Prophecy result');
}
// Persist these verified server fields, not the browser-submitted equivalents.
await saveResult({
orbitalJobId: verified.jobId,
productId: verified.productId,
productReference: verified.productReference,
productSource: verified.productSource,
selectionString: verified.selectionString,
sourceUrl: verified.resultUrl
});Instance API
| Method | Purpose |
| --- | --- |
| checkAvailability() | Preflight setup, origin access and generation credits |
| open() / close() / isOpen() | Control the dialog |
| mount(target) | Mount Prophecy's default launcher |
| mountOverlay(target?) | Put the widget over the configurator, or another element |
| useLauncher(target) / releaseLauncher() | Move the launcher to another element, or detach it |
| setProductId(id) | Synchronise an SPA product change |
| setProduct({ reference, images, productId?, url? }) | Atomically switch an external product and its 1–4 images |
| setAccountContext(key) | Fence a login, logout or account switch |
| getState() | Read current public state |
| refreshHistory() | Reload account history |
| clearHistory() | Delete device history and invoke the account adapter |
| on(name, handler) / off(name, handler) | Manage event subscriptions |
| destroy() | Abort work and remove UI/listeners/object URLs |
Events and hooks
Subscribe through onEvent, a named option callback, prophecy.on(name, handler), prophecy.on('*', handler), or the bubbling prophecy DOM CustomEvent emitted from the launcher.
| Event | Option callback |
| --- | --- |
| ready | onReady |
| availability-change | onAvailabilityChange |
| sign-in-required | onSignInRequired |
| order | onOrder |
| open / close | onOpen / onClose |
| view-change | onViewChange |
| product-change | onProductChange |
| account-change | onAccountChange |
| selection-change | onSelectionChange |
| photo-selected / photo-cleared | onPhotoSelected / onPhotoCleared |
| phone-handoff-start / phone-photo-received | onPhoneHandoffStart / onPhonePhotoReceived |
| generation-start / generation-progress | onGenerationStart / onGenerationProgress |
| generation-complete | onResult |
| generation-error | onGenerationError |
| history-load / history-save / history-error | onHistoryLoad / onHistorySave / onHistoryError |
| error | onError |
| destroyed | onDestroy |
The legacy orbital-room-ai DOM event is emitted during the migration period.
Order this product
Every preview remembers the configurator's SKU for that exact configuration and the page it was generated from, query string and all. When a shopper presses Order this product, Prophecy tells the page first. Call preventDefault() to keep them on the page and take the order yourself; otherwise they go to that page.
Prophecy.create({
onOrder(order) {
order.preventDefault();
addToBasket(order.sku); // e.g. "BLOOMSBURY-MED-TWEED-BOULDER"
// order.pageUrl is the page the preview was made from, query string and all
}
});With the script tag, set the hook before the script loads:
<script>
window.prophecyOptions = {
onOrder(order) {
order.preventDefault();
addToBasket(order.sku);
}
};
</script>
<script src="https://cdn.jsdelivr.net/npm/@orbital.vision/prophecy@1/dist/prophecy.js"></script>sku and pageUrl are also on every generation-complete result and every saved preview.
Local development
npm install
npm run dev
npm run checkCI runs the complete check and a package dry-run on Node 20 and Node 24 for every pull request and push to main.
The Vite playground exercises the production Shadow DOM UI. To build and copy generated artifacts into an OV25 checkout:
During OV25 development, point the host app directly at Vite so SDK edits only need a page refresh:
# Prophecy
npm run dev
# Optional OV25 override; this is the development default.
NEXT_PUBLIC_PROPHECY_SDK_SRC=http://127.0.0.1:5173/src/runtime.jsnpm run sync:ov25 -- /absolute/path/to/OV25Prophecy source lives here. The corresponding OV25 files are generated distribution artifacts and should not be edited by hand.
Publishing releases
Releases publish @orbital.vision/prophecy to the public npm registry from .github/workflows/release.yml. The workflow uses npm trusted publishing through GitHub OIDC; it does not require a long-lived npm write token.
The package is public under the @orbital.vision scope. Use the release workflow for normal releases; manual publishing is reserved for recovery:
npm login
npm run check
npm publish --access publicAfter 0.2.0 exists, configure its npm Trusted Publisher with these exact values:
- GitHub organization:
orbitalvision - Repository:
Prophecy - Workflow filename:
release.yml - Environment:
npm - Allowed action:
npm publish
Create the matching protected npm environment in GitHub, ideally with required maintainer approval. Once an OIDC release succeeds, set npm publishing access to require 2FA and disallow tokens, then revoke any bootstrap token.
For each subsequent release, npm version updates the manifest, lockfile and both runtime version constants, then creates the release commit and tag. Push both:
npm version patch # or minor / major
git push origin main --follow-tagsThe v* tag starts the release workflow. It rejects a tag that does not exactly match package.json, rebuilds and tests the SDK, verifies the mandatory attribution and package contents, then publishes. Because this GitHub repository is private, npm cannot currently attach a public provenance attestation even though authentication still uses short-lived OIDC credentials.
