@3dn-builder/builder 0.640.0 → 0.641.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/chunks/chunk-XQRNTDNA.js +2 -0
- package/chunks/chunk-XQRNTDNA.js.map +7 -0
- package/events.d.ts +30 -6
- package/events.js +1 -1
- package/index.d.ts +20 -4
- package/index.js +1 -1
- package/index.js.map +3 -3
- package/package.json +1 -1
- package/testing.d.ts +11 -4
- package/testing.js +1 -1
- package/testing.js.map +3 -3
- package/chunks/chunk-F4FUPJ4Y.js +0 -2
- package/chunks/chunk-F4FUPJ4Y.js.map +0 -7
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ const builder = createBuilder({
|
|
|
25
25
|
onRequired: (action) => showLogin(action), // 'logged-in' | 'redirected' | 'cancelled'
|
|
26
26
|
},
|
|
27
27
|
onSaved: (saved) => showSavedDesign(saved.productId),
|
|
28
|
+
onImagesSaved: (saved) => refreshGallery(saved.productId),
|
|
28
29
|
onLicenseError: (error) => reportSetupError(error.code),
|
|
29
30
|
onCloseRequested: ({ hasUnsavedChanges }) => closeModal({ confirm: hasUnsavedChanges }),
|
|
30
31
|
});
|
|
@@ -41,7 +42,7 @@ instance.destroy();
|
|
|
41
42
|
| `load()` | Optional preload, once per page. |
|
|
42
43
|
| `mount(options)` | `{ target, hostProductId, mode?: 'designer' \| 'viewer', designId?, externalExport?, observabilityContext? }` → instance. |
|
|
43
44
|
| `open()` | Shows the builder UI after a hidden mount. |
|
|
44
|
-
| `save()` | Resolves `{ productId, version }` after the save, any clone record
|
|
45
|
+
| `save()` | Resolves `{ productId, version }` after the save, any clone record, `onSaved` and the images refresh. 45 s timeout. |
|
|
45
46
|
| `refreshSession()` | Calls `session.get()` again and applies the result. |
|
|
46
47
|
| `refreshLicense()` | One shared license refresh for every mounted builder. |
|
|
47
48
|
| `logout()` | Removes the shopper tokens, keeps the license token. Late login results are dropped. |
|
|
@@ -86,6 +87,7 @@ Login by redirect: send the shopper to `loginReturnPath()` after login (check it
|
|
|
86
87
|
|
|
87
88
|
```ts
|
|
88
89
|
onSaved?(saved: { hostProductId: string; productId: string; version: number }): void;
|
|
90
|
+
onImagesSaved?(saved: { hostProductId: string; productId: string; version: number }): void;
|
|
89
91
|
|
|
90
92
|
customization?: {
|
|
91
93
|
record(link: {
|
|
@@ -99,6 +101,8 @@ customization?: {
|
|
|
99
101
|
|
|
100
102
|
`onSaved` fires once for every successful save, from `save()` or from the builder's own Save button. A first save makes the shopper's copy: `productId` is then the copy, and `onSaved` fires only after the copy exists and `customization.record()` resolved. It never fires for a failed save or a failed record; the next save records again.
|
|
101
103
|
|
|
104
|
+
`onSaved` comes before the product's images (its thumbnail and camera-angle screenshots) are refreshed. Read the images on `onImagesSaved`: it fires once they show the design at `version`, after that save's `onSaved`, and also when the builder's Close refreshed images that autosave had left behind. It does not fire when the images could not be updated; the product keeps its previous ones. `save()` resolves only after the images are refreshed or have failed, so a host that destroys the builder after `await save()` never cuts the upload short. This needs a builder that sends `tdn-images-saved`, the one released with this SDK or later; with an older pinned `version`, `save()` times out.
|
|
105
|
+
|
|
102
106
|
`asCopy` says what the copy means. `true`: the shopper asked for one ("Save as copy", or "keep both" after a conflict), so store it as a new design and keep the original. `false`: Save had to fork a product the shopper doesn't own, so the copy replaces the open design.
|
|
103
107
|
|
|
104
108
|
## Errors
|
|
@@ -153,9 +157,11 @@ installFakeBuilder(); // before mount(): no real builder is loaded
|
|
|
153
157
|
const instance = await builder.mount({ target, hostProductId: 'mug' });
|
|
154
158
|
const saving = instance.save();
|
|
155
159
|
await findFakeBuilders()[0].emitSavedAsClone('clone-1');
|
|
156
|
-
await saving; // customization.record() and
|
|
160
|
+
await saving; // customization.record(), onSaved and onImagesSaved have run
|
|
157
161
|
```
|
|
158
162
|
|
|
163
|
+
`emitSaved()` and `emitSavedAsClone()` send the images outcome as the builder does; `emitImagesSaved()` and `emitImagesSaveFailed()` send one on its own.
|
|
164
|
+
|
|
159
165
|
## Developing this package
|
|
160
166
|
|
|
161
167
|
```sh
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var l="tdn-open-builder";function r(e){e.dispatchEvent(new CustomEvent(l,{bubbles:!0,composed:!0}))}var d="tdn-status-change";var c="exportRequested",i="tdn-export-complete";function g(e,a){e.dispatchEvent(new CustomEvent(i,{bubbles:!1,composed:!1,detail:a}))}var E="tdn-product-cloned";var b="tdn-login-required";var t="tdn-login-cancelled";function f(e,a={}){e.dispatchEvent(new CustomEvent(t,{bubbles:!1,composed:!1,detail:a}))}var p="tdn-license-error";var u="tdn-design-saved";var F="tdn-images-saved";var m="tdn-images-save-failed";var D="tdn-design-save-failed";var A="tdn-license-token-required";var n="tdn-save-request";function C(e){e.dispatchEvent(new CustomEvent(n,{bubbles:!1,composed:!1}))}var T="tdn-close-requested";var s=[{name:"Grays",colors:["#FFFFFF","#F8F9FA","#E9ECEF","#DEE2E6","#CED4DA","#ADB5BD","#6C757D","#495057","#343A40","#212529","#1A1A1A","#000000"]},{name:"Reds",colors:["#FFE6E6","#FFCCCC","#FF9999","#FF6666","#FF3333","#FF0000","#E60000","#CC0000","#990000","#660000","#330000","#1A0000"]},{name:"Oranges",colors:["#FFF4E6","#FFE6CC","#FFCC99","#FFB366","#FF9933","#FF8000","#E66A00","#CC5200","#993D00","#662900","#331400","#1A0A00"]},{name:"Yellows",colors:["#FFFEF0","#FFFDE6","#FFFACC","#FFF799","#FFF266","#FFED33","#E6D100","#CCBA00","#998C00","#665D00","#332E00","#1A1700"]},{name:"Greens",colors:["#F0FFF0","#E6FFE6","#CCFFCC","#99FF99","#66FF66","#33FF33","#00E600","#00CC00","#009900","#006600","#003300","#001A00"]},{name:"Blues",colors:["#F0F8FF","#E6F3FF","#CCE7FF","#99CFFF","#66B7FF","#339FFF","#0080E6","#0066CC","#004D99","#003366","#001A33","#000D1A"]},{name:"Purples",colors:["#F8F0FF","#F0E6FF","#E0CCFF","#C199FF","#A266FF","#8333FF","#6600E6","#5200CC","#3D0099","#290066","#140033","#0A001A"]},{name:"Pinks",colors:["#FFE6F0","#FFCCE0","#FF99C1","#FF66A2","#FF3383","#FF0064","#E60057","#CC0051","#99003E","#66002A","#330015","#1A000A"]},{name:"Teals",colors:["#F0FFFF","#E0FFFF","#B2FFFF","#66FFFF","#1AFFFF","#00CCCC","#00B3B3","#009999","#006666","#003333","#001A1A","#000D0D"]},{name:"Cyans",colors:["#E0F7FA","#B2EBF2","#80DEEA","#4DD0E1","#26C6DA","#00BCD4","#00ACC1","#0097A7","#00796B","#004D40","#00251A","#00120D"]},{name:"Indigos",colors:["#E8EAF6","#C5CAE9","#9FA8DA","#7986CB","#5C6BC0","#3F51B5","#303F9F","#283593","#1A237E","#0D1442","#060A21","#030510"]},{name:"Violets",colors:["#F3E5F5","#E1BEE7","#CE93D8","#BA68C8","#AB47BC","#9C27B0","#8E24AA","#7B1FA2","#6A1B9A","#4A148C","#2E1065","#1A0D3D"]},{name:"Magentas",colors:["#FCE4EC","#F8BBD9","#F48FB1","#F06292","#EC407A","#E91E63","#D81B60","#C2185B","#AD1457","#880E4F","#4A0A2A","#2D0519"]},{name:"Limes",colors:["#F9FBE7","#F0F4C3","#E6EE9C","#DCE775","#D4E157","#CDDC39","#C0CA33","#AFB42B","#9E9D24","#827717","#4A4514","#2D2A0C"]},{name:"Ambers",colors:["#FFF8E1","#FFECB3","#FFE082","#FFD54F","#FFCA28","#FFC107","#FFB300","#FFA000","#FF8F00","#FF6F00","#994200","#5A2700"]},{name:"Deep Oranges",colors:["#FBE9E7","#FFCCBC","#FFAB91","#FF8A65","#FF7043","#FF5722","#F4511E","#E64A19","#D84315","#BF360C","#7A2408","#4A1605"]},{name:"Light Blues",colors:["#E1F5FE","#B3E5FC","#81D4FA","#4FC3F7","#29B6F6","#03A9F4","#039BE5","#0288D1","#0277BD","#01579B","#003A66","#00223D"]},{name:"Light Greens",colors:["#E8F5E8","#C8E6C9","#A5D6A7","#81C784","#66BB6A","#4CAF50","#43A047","#388E3C","#2E7D32","#1B5E20","#0F3A14","#09230C"]},{name:"Browns",colors:["#F5F5DC","#F0E68C","#D2B48C","#BC8F8F","#A0522D","#8B4513","#654321","#4A2C2A","#2F1B14","#1A0E0A","#0F0705","#090503"]},{name:"Grey-Blues",colors:["#ECEFF1","#CFD8DC","#B0BEC5","#90A4AE","#78909C","#607D8B","#546E7A","#455A64","#37474F","#263238","#1A2126","#0F1316"]}],_=s.flatMap(e=>e.colors);var L=1024*1024;var x=600*1024;var o=["active","suspended","revoked"];var H=[...o,"expired"];var K=1440*60*1e3;export{l as a,r as b,d as c,c as d,i as e,g as f,E as g,b as h,t as i,f as j,p as k,u as l,F as m,m as n,D as o,A as p,n as q,C as r,T as s};
|
|
2
|
+
//# sourceMappingURL=chunk-XQRNTDNA.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../libs/constants/src/lib/builder-webcomponent.ts", "../../../../libs/constants/src/lib/colors.ts", "../../../../libs/constants/src/lib/constants.ts", "../../../../libs/constants/src/lib/license.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Programmatic open for `<threed-builder>` (avoids synthetic `click()` / ghost clicks).\n * Dispatched on the custom element host with composed path so the inner app can subscribe.\n */\nexport const THREED_BUILDER_OPEN_EVENT = 'tdn-open-builder' as const;\n\nexport function dispatchOpenThreedBuilder(host: HTMLElement): void {\n host.dispatchEvent(\n new CustomEvent(THREED_BUILDER_OPEN_EVENT, {\n bubbles: true,\n composed: true,\n })\n );\n}\n\n/**\n * Builder lifecycle statuses emitted via `tdn-status-change` CustomEvent.\n */\nexport type BuilderStatus = 'idle' | 'opening' | 'loading' | 'ready' | 'error';\n\nexport const THREED_BUILDER_STATUS_EVENT = 'tdn-status-change' as const;\n\nexport interface BuilderStatusDetail {\n status: BuilderStatus;\n step?: string;\n message?: string;\n error?: string;\n}\n\nexport function dispatchBuilderStatus(\n host: HTMLElement,\n detail: BuilderStatusDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<BuilderStatusDetail>(THREED_BUILDER_STATUS_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * External export bridge.\n *\n * When `<threed-builder external-export=\"true\">` is set, the webcomponent\n * skips its built-in admin POST and instead emits `exportRequested` so the\n * embedding host can run the export through its own backend (e.g. the\n * store app proxy). The host then hands the resulting file URLs back via\n * `dispatchExportComplete`.\n *\n * NOT FOR A DIELINE PRODUCT (task 3.2, ruling P3-6). A dieline's print files\n * come from ONE pure exporter run over the design's own immutable snapshot\n * (Document 1 sections 12 and 13); a host backend cannot produce that file,\n * and a file it produces instead is a second drawing with nothing holding it\n * to what the customer approved on screen. The webcomponent therefore routes\n * a DIELINE export to the admin API whatever this attribute says, and emits\n * `exportRequested` for GLB products only. A host that needs its own pipeline\n * for dielines needs a different contract, not this one.\n */\nexport const THREED_BUILDER_EXPORT_REQUESTED_EVENT = 'exportRequested' as const;\n\nexport interface ExportRequestedDetail {\n productId: string;\n formats: string[];\n surfaces: string[];\n /**\n * The export mode, `'proof'` or `'production-artwork'` (Document 1 section\n * 12). ADDITIVE in task 1.5b: a host that ignores it keeps working exactly\n * as before. Since task 3.2 only GLB products reach this event, and a GLB\n * export is mode-independent \u2014 so the field is carried for the host's own\n * bookkeeping, not to choose a drawing.\n */\n mode?: string;\n}\n\nexport const THREED_BUILDER_EXPORT_COMPLETE_EVENT =\n 'tdn-export-complete' as const;\n\nexport interface ExportFile {\n url: string;\n format: string;\n surface: string;\n}\n\nexport interface ExportCompleteDetail {\n files: ExportFile[];\n}\n\nexport function dispatchExportComplete(\n host: HTMLElement,\n detail: ExportCompleteDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<ExportCompleteDetail>(\n THREED_BUILDER_EXPORT_COMPLETE_EVENT,\n {\n bubbles: false,\n composed: false,\n detail,\n }\n )\n );\n}\n\n/**\n * Fired after a save that created a new product: \"Save as copy\", or a host\n * shopper's Save of a product they don't own (their first save). The builder\n * has switched to editing the new product. The host should persist\n * `newProductId` so subsequent loads of the builder open it.\n */\nexport const THREED_BUILDER_PRODUCT_CLONED_EVENT =\n 'tdn-product-cloned' as const;\n\nexport interface ProductClonedDetail {\n originalProductId: string;\n newProductId: string;\n name: string;\n /**\n * The user asked for a copy (\"Save as copy\", \"Save as my copy\", \"keep both\"\n * after a conflict) and expects to keep the original too. `false` when Save\n * had to fork because the user doesn't own the product: the copy takes the\n * original's place.\n */\n asCopy: boolean;\n}\n\nexport function dispatchProductCloned(\n host: HTMLElement,\n detail: ProductClonedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<ProductClonedDetail>(THREED_BUILDER_PRODUCT_CLONED_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * Fired when a protected action needs a logged-in user (save / screenshot /\n * export / upload / share), or \u2014 with `action: 'session'` \u2014 when the builder's\n * session could not be recovered (refresh failed). The host shows its own\n * login if its user is not logged in, has its BACKEND mint a token pair from\n * its own session, and sets `bearer-token` / `refresh-token` on the element;\n * the builder then resumes the pending action. If the user dismisses the login\n * or the mint fails, the host dispatches `tdn-login-cancelled` instead.\n * One event per login round-trip: further gated clicks while a login is\n * pending do not fire it again.\n */\nexport const THREED_BUILDER_LOGIN_REQUIRED_EVENT =\n 'tdn-login-required' as const;\n\nexport type LoginRequiredAction =\n | 'save'\n | 'screenshot'\n | 'export'\n | 'upload'\n | 'share'\n | 'session';\n\nexport interface LoginRequiredDetail {\n action: LoginRequiredAction;\n}\n\nexport function dispatchLoginRequired(\n host: HTMLElement,\n detail: LoginRequiredDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<LoginRequiredDetail>(THREED_BUILDER_LOGIN_REQUIRED_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * Host -> builder: the login the builder asked for (`tdn-login-required`) will\n * not happen \u2014 the user closed the host's login, or the host's mint failed.\n * The builder drops the pending action, so a stale Save never runs after a\n * later, unrelated login. A pending Save reports `tdn-design-save-failed`\n * with reason `login-cancelled`.\n */\nexport const THREED_BUILDER_LOGIN_CANCELLED_EVENT =\n 'tdn-login-cancelled' as const;\n\nexport interface LoginCancelledDetail {\n action?: LoginRequiredAction;\n}\n\nexport function dispatchLoginCancelled(\n host: HTMLElement,\n detail: LoginCancelledDetail = {}\n): void {\n host.dispatchEvent(\n new CustomEvent<LoginCancelledDetail>(\n THREED_BUILDER_LOGIN_CANCELLED_EVENT,\n { bubbles: false, composed: false, detail }\n )\n );\n}\n\n/**\n * Builder -> host: the builder cannot work on this page because of the\n * license, not because of the user. Fired once.\n * - `ORIGIN_NOT_ALLOWED`: this page's hostname is not the license's domain or\n * an alias. Do not answer it with a new token: a fresh mint for the same\n * page is refused the same way. Fix the license (add the hostname as an\n * alias) instead.\n * - `LICENSE_TOKEN_INVALID`: the `license-token` was rejected, the builder\n * asked for a fresh one (`tdn-license-token-required`), and the fresh one\n * was rejected too (or never came) \u2014 the license is suspended, revoked or\n * expired, or the host is minting against the wrong installation.\n */\nexport const THREED_BUILDER_LICENSE_ERROR_EVENT = 'tdn-license-error' as const;\n\nexport type LicenseErrorCode = 'ORIGIN_NOT_ALLOWED' | 'LICENSE_TOKEN_INVALID';\n\nexport interface LicenseErrorDetail {\n code: LicenseErrorCode;\n}\n\nexport function dispatchLicenseError(\n host: HTMLElement,\n detail: LicenseErrorDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<LicenseErrorDetail>(THREED_BUILDER_LICENSE_ERROR_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * The design was saved to 3dn. Fired as soon as the save is stored (before\n * the product's images are refreshed; `tdn-images-saved` or\n * `tdn-images-save-failed` follows), and after \"Save as copy\" too (then\n * followed by `tdn-product-cloned`). This \u2014 not `tdn-status-change` `ready`,\n * which also fires after every load step \u2014 is the signal a host should\n * continue on (add to cart, show a confirmation).\n */\nexport const THREED_BUILDER_DESIGN_SAVED_EVENT = 'tdn-design-saved' as const;\n\nexport interface DesignSavedDetail {\n productId: string;\n /** The stored serialized version after this save. */\n version: number;\n}\n\nexport function dispatchDesignSaved(\n host: HTMLElement,\n detail: DesignSavedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<DesignSavedDetail>(THREED_BUILDER_DESIGN_SAVED_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * The product's gallery images (its thumbnail and camera-angle screenshots)\n * now show the design at `version`, and reading the product returns them.\n * Fired after every `tdn-design-saved`, once the screenshots are uploaded and\n * stored on the product \u2014 before `tdn-product-cloned` when the save made a\n * copy. Also fired by the builder's Close when autosave had stored the design\n * without screenshots: the images are refreshed before `tdn-close-requested`.\n * This \u2014 not `tdn-design-saved` or `tdn-status-change` \u2014 is when a host may\n * read the product's images.\n */\nexport const THREED_BUILDER_IMAGES_SAVED_EVENT = 'tdn-images-saved' as const;\n\nexport interface ImagesSavedDetail {\n productId: string;\n /** The stored serialized version the images show. */\n version: number;\n}\n\nexport function dispatchImagesSaved(\n host: HTMLElement,\n detail: ImagesSavedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<ImagesSavedDetail>(THREED_BUILDER_IMAGES_SAVED_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * The design was saved but its images could not be refreshed (capture or\n * upload failed); the product keeps its previous images. Fired in place of\n * `tdn-images-saved`. The builder has already warned the user; the design\n * itself is safe.\n */\nexport const THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT =\n 'tdn-images-save-failed' as const;\n\nexport function dispatchImagesSaveFailed(\n host: HTMLElement,\n detail: ImagesSavedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<ImagesSavedDetail>(\n THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT,\n { bubbles: true, composed: true, detail }\n )\n );\n}\n\n/**\n * Why a save did not happen:\n * - `conflict`: someone saved first; the builder shows its conflict dialog.\n * - `forbidden`: this user may not write to the product; the builder offers\n * request-access or save-as-copy.\n * - `unauthorized`: login recovery ran and the save was still refused.\n * - `login-cancelled`: the host sent `tdn-login-cancelled` for a pending save.\n * - `installation-inactive`: the host installation is not active (setup\n * problem on the host side, not a user error).\n * - `license`: refused with `ORIGIN_NOT_ALLOWED` \u2014 this page isn't licensed,\n * not a permissions or session problem; see `tdn-license-error`.\n * - `error`: network or server failure.\n */\nexport type DesignSaveFailureReason =\n | 'conflict'\n | 'forbidden'\n | 'unauthorized'\n | 'login-cancelled'\n | 'installation-inactive'\n | 'license'\n | 'error';\n\n/**\n * A save the user asked for did not happen, after any retry and login\n * recovery ran out. Never fired while a login the builder asked for is still\n * pending. The builder has already told the user; the host may add its own\n * handling and, when `retryable`, let the user try again.\n */\nexport const THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT =\n 'tdn-design-save-failed' as const;\n\nexport interface DesignSaveFailedDetail {\n productId?: string;\n reason: DesignSaveFailureReason;\n retryable: boolean;\n}\n\nexport function dispatchDesignSaveFailed(\n host: HTMLElement,\n detail: DesignSaveFailedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<DesignSaveFailedDetail>(\n THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,\n { bubbles: true, composed: true, detail }\n )\n );\n}\n\n/**\n * Builder -> host: the `license-token` (the no-user token from the host\n * backend's `POST /plugin/license-token`) was rejected \u2014 it expired, or the\n * installation's key was rotated. The host fetches a fresh license token from\n * its backend and sets `license-token` again; the builder holds the failed\n * request and retries it once with the new token. This is NEVER a login\n * prompt: answer it without involving the user. A second rejection ends in\n * `tdn-license-error` `LICENSE_TOKEN_INVALID`.\n */\nexport const THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT =\n 'tdn-license-token-required' as const;\n\nexport function dispatchLicenseTokenRequired(host: HTMLElement): void {\n host.dispatchEvent(\n new CustomEvent(THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT, {\n bubbles: true,\n composed: true,\n })\n );\n}\n\n/**\n * Host -> builder: save the design now, exactly as if the user pressed Save\n * (login gate, save-as-copy for a product the user does not own, and the\n * usual `tdn-design-saved` / `tdn-design-save-failed` outcome). A host sends\n * it before Add to cart when the design has not been saved yet, then waits\n * for one of those two events.\n */\nexport const THREED_BUILDER_SAVE_REQUEST_EVENT = 'tdn-save-request' as const;\n\nexport function dispatchSaveRequest(host: HTMLElement): void {\n host.dispatchEvent(\n new CustomEvent(THREED_BUILDER_SAVE_REQUEST_EVENT, {\n bubbles: false,\n composed: false,\n })\n );\n}\n\n/**\n * Builder -> host: the user pressed the builder's close button. The builder\n * does nothing else \u2014 it stays open and keeps its state. The host decides\n * what closing means on its page (hide an overlay, navigate back, ask first\n * when `hasUnsavedChanges`) and removes or hides the element itself.\n */\nexport const THREED_BUILDER_CLOSE_REQUESTED_EVENT =\n 'tdn-close-requested' as const;\n\nexport interface CloseRequestedDetail {\n productId?: string;\n /**\n * What is on screen differs from what was last saved (manual save or\n * autosave), in 2D or 3D: elements, the dieline/board, finish styles, or a\n * switched design. Opening and closing the box (fold) does not count, and\n * an edit undone by hand is no change.\n */\n hasUnsavedChanges: boolean;\n}\n\nexport function dispatchCloseRequested(\n host: HTMLElement,\n detail: CloseRequestedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<CloseRequestedDetail>(\n THREED_BUILDER_CLOSE_REQUESTED_EVENT,\n { bubbles: true, composed: true, detail }\n )\n );\n}\n\n/**\n * Fired when the user picks \"AI Video\" inside the builder. The embedding host\n * navigates to its AI-video editor for `productId`, which then drives this same\n * product (loaded headlessly) to render the template's reference views.\n */\nexport const THREED_BUILDER_OPEN_AI_VIDEO_EVENT = 'tdn-open-ai-video' as const;\n\nexport interface OpenAiVideoDetail {\n productId: string;\n}\n\nexport function dispatchOpenAiVideo(\n host: HTMLElement,\n detail: OpenAiVideoDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<OpenAiVideoDetail>(THREED_BUILDER_OPEN_AI_VIDEO_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n", "// Color groups with 12 variants each\nexport const colorGroups = [\n {\n name: 'Grays',\n colors: [\n '#FFFFFF',\n '#F8F9FA',\n '#E9ECEF',\n '#DEE2E6',\n '#CED4DA',\n '#ADB5BD',\n '#6C757D',\n '#495057',\n '#343A40',\n '#212529',\n '#1A1A1A',\n '#000000',\n ],\n },\n {\n name: 'Reds',\n colors: [\n '#FFE6E6',\n '#FFCCCC',\n '#FF9999',\n '#FF6666',\n '#FF3333',\n '#FF0000',\n '#E60000',\n '#CC0000',\n '#990000',\n '#660000',\n '#330000',\n '#1A0000',\n ],\n },\n {\n name: 'Oranges',\n colors: [\n '#FFF4E6',\n '#FFE6CC',\n '#FFCC99',\n '#FFB366',\n '#FF9933',\n '#FF8000',\n '#E66A00',\n '#CC5200',\n '#993D00',\n '#662900',\n '#331400',\n '#1A0A00',\n ],\n },\n {\n name: 'Yellows',\n colors: [\n '#FFFEF0',\n '#FFFDE6',\n '#FFFACC',\n '#FFF799',\n '#FFF266',\n '#FFED33',\n '#E6D100',\n '#CCBA00',\n '#998C00',\n '#665D00',\n '#332E00',\n '#1A1700',\n ],\n },\n {\n name: 'Greens',\n colors: [\n '#F0FFF0',\n '#E6FFE6',\n '#CCFFCC',\n '#99FF99',\n '#66FF66',\n '#33FF33',\n '#00E600',\n '#00CC00',\n '#009900',\n '#006600',\n '#003300',\n '#001A00',\n ],\n },\n {\n name: 'Blues',\n colors: [\n '#F0F8FF',\n '#E6F3FF',\n '#CCE7FF',\n '#99CFFF',\n '#66B7FF',\n '#339FFF',\n '#0080E6',\n '#0066CC',\n '#004D99',\n '#003366',\n '#001A33',\n '#000D1A',\n ],\n },\n {\n name: 'Purples',\n colors: [\n '#F8F0FF',\n '#F0E6FF',\n '#E0CCFF',\n '#C199FF',\n '#A266FF',\n '#8333FF',\n '#6600E6',\n '#5200CC',\n '#3D0099',\n '#290066',\n '#140033',\n '#0A001A',\n ],\n },\n {\n name: 'Pinks',\n colors: [\n '#FFE6F0',\n '#FFCCE0',\n '#FF99C1',\n '#FF66A2',\n '#FF3383',\n '#FF0064',\n '#E60057',\n '#CC0051',\n '#99003E',\n '#66002A',\n '#330015',\n '#1A000A',\n ],\n },\n {\n name: 'Teals',\n colors: [\n '#F0FFFF',\n '#E0FFFF',\n '#B2FFFF',\n '#66FFFF',\n '#1AFFFF',\n '#00CCCC',\n '#00B3B3',\n '#009999',\n '#006666',\n '#003333',\n '#001A1A',\n '#000D0D',\n ],\n },\n {\n name: 'Cyans',\n colors: [\n '#E0F7FA',\n '#B2EBF2',\n '#80DEEA',\n '#4DD0E1',\n '#26C6DA',\n '#00BCD4',\n '#00ACC1',\n '#0097A7',\n '#00796B',\n '#004D40',\n '#00251A',\n '#00120D',\n ],\n },\n {\n name: 'Indigos',\n colors: [\n '#E8EAF6',\n '#C5CAE9',\n '#9FA8DA',\n '#7986CB',\n '#5C6BC0',\n '#3F51B5',\n '#303F9F',\n '#283593',\n '#1A237E',\n '#0D1442',\n '#060A21',\n '#030510',\n ],\n },\n {\n name: 'Violets',\n colors: [\n '#F3E5F5',\n '#E1BEE7',\n '#CE93D8',\n '#BA68C8',\n '#AB47BC',\n '#9C27B0',\n '#8E24AA',\n '#7B1FA2',\n '#6A1B9A',\n '#4A148C',\n '#2E1065',\n '#1A0D3D',\n ],\n },\n {\n name: 'Magentas',\n colors: [\n '#FCE4EC',\n '#F8BBD9',\n '#F48FB1',\n '#F06292',\n '#EC407A',\n '#E91E63',\n '#D81B60',\n '#C2185B',\n '#AD1457',\n '#880E4F',\n '#4A0A2A',\n '#2D0519',\n ],\n },\n {\n name: 'Limes',\n colors: [\n '#F9FBE7',\n '#F0F4C3',\n '#E6EE9C',\n '#DCE775',\n '#D4E157',\n '#CDDC39',\n '#C0CA33',\n '#AFB42B',\n '#9E9D24',\n '#827717',\n '#4A4514',\n '#2D2A0C',\n ],\n },\n {\n name: 'Ambers',\n colors: [\n '#FFF8E1',\n '#FFECB3',\n '#FFE082',\n '#FFD54F',\n '#FFCA28',\n '#FFC107',\n '#FFB300',\n '#FFA000',\n '#FF8F00',\n '#FF6F00',\n '#994200',\n '#5A2700',\n ],\n },\n {\n name: 'Deep Oranges',\n colors: [\n '#FBE9E7',\n '#FFCCBC',\n '#FFAB91',\n '#FF8A65',\n '#FF7043',\n '#FF5722',\n '#F4511E',\n '#E64A19',\n '#D84315',\n '#BF360C',\n '#7A2408',\n '#4A1605',\n ],\n },\n {\n name: 'Light Blues',\n colors: [\n '#E1F5FE',\n '#B3E5FC',\n '#81D4FA',\n '#4FC3F7',\n '#29B6F6',\n '#03A9F4',\n '#039BE5',\n '#0288D1',\n '#0277BD',\n '#01579B',\n '#003A66',\n '#00223D',\n ],\n },\n {\n name: 'Light Greens',\n colors: [\n '#E8F5E8',\n '#C8E6C9',\n '#A5D6A7',\n '#81C784',\n '#66BB6A',\n '#4CAF50',\n '#43A047',\n '#388E3C',\n '#2E7D32',\n '#1B5E20',\n '#0F3A14',\n '#09230C',\n ],\n },\n {\n name: 'Browns',\n colors: [\n '#F5F5DC',\n '#F0E68C',\n '#D2B48C',\n '#BC8F8F',\n '#A0522D',\n '#8B4513',\n '#654321',\n '#4A2C2A',\n '#2F1B14',\n '#1A0E0A',\n '#0F0705',\n '#090503',\n ],\n },\n {\n name: 'Grey-Blues',\n colors: [\n '#ECEFF1',\n '#CFD8DC',\n '#B0BEC5',\n '#90A4AE',\n '#78909C',\n '#607D8B',\n '#546E7A',\n '#455A64',\n '#37474F',\n '#263238',\n '#1A2126',\n '#0F1316',\n ],\n },\n];\n\n// Flattened array for backward compatibility\nexport const colors: string[] = colorGroups.flatMap((group) => group.colors);\n", "export const USER_ROLES = ['ADMIN', 'USER', 'CUSTOMER'] as const;\nexport const AUTH_PROVIDERS = ['REGULAR', 'GOOGLE'] as const;\nexport const TIER_TYPES = ['PAID', 'TRAIL', 'FREE', 'EXPIRED'] as const;\nexport const GLB_MATERIALS = ['PBR', 'STANDARD', 'UNKNOWN'] as const;\nexport const VISIBILITY_STATUS = [\n 'private',\n 'public',\n 'store',\n 'unlisted',\n] as const;\n\nexport const HDRI_CATEGORIES = [\n 'outdoor',\n 'indoor',\n 'studio',\n 'sunset',\n 'night',\n 'abstract',\n] as const;\nexport type HdriCategory = (typeof HDRI_CATEGORIES)[number];\n\nexport const HDRI_CUBE_SIZES = [64, 128, 256, 512] as const;\nexport type HdriCubeSize = (typeof HDRI_CUBE_SIZES)[number];\n\n/** Maximum byte length of a published HDRI's .env file. Enforced by API and UI. */\nexport const HDRI_PUBLISH_MAX_ENV_BYTES = 1024 * 1024; // 1 MB\n\n/** Default pixel size of an imported GLB's dynamic texture. Admins can set any\n * size from 1 to MAX_DYNAMIC_TEXTURE_SIZE per axis. */\nexport const DEFAULT_DYNAMIC_TEXTURE_RESOLUTION = {\n width: 2048,\n height: 2048,\n} as const;\n\n/** Upper bound on dynamic texture dimensions. Beyond this, quality gains are\n * negligible on most GPUs but memory cost scales quadratically. Enforced by\n * API and UI; the builder clamps to it too. */\nexport const MAX_DYNAMIC_TEXTURE_SIZE = 4096;\n\n/** Above this, a design layer's combined path data may slow canvas rendering. */\nexport const MAX_LAYER_PATH_BYTES = 600 * 1024; // 600 KB\n\nexport const MAX_FILE_SIZES = {\n MODEL: 10,\n IMAGE: 20,\n THUMBNAIL: 5,\n DEBUG: 0.5,\n ENV: 10,\n FONT: 20,\n HDRI_SOURCE: 25,\n} as const;\n\n/**\n * Default max upload size (in MB) applied to upload endpoints when no\n * explicit override is provided. Shared between the API middleware and the\n * frontend so that client-side validation matches the server limit.\n */\nexport const DEFAULT_MAX_UPLOAD_SIZE_MB = 10;\n\n/**\n * Centralized upload file-type definitions.\n *\n * Each entry describes a named group of files that the API accepts at a\n * single upload endpoint. The API's upload middleware uses `mimeTypes` /\n * `extensions` for validation, and the Angular frontend uses `accept`\n * directly on `<input type=\"file\">` elements so both layers stay in sync.\n *\n * - `IMAGE` \u2013 any raster or vector image (generic image uploads).\n * - `RASTER_IMAGE` \u2013 raster only (design images the AI separates into layers).\n * - `SVG` \u2013 vector only (design layer uploads, pattern SVG uploads).\n * - `MODEL` \u2013 3D model bundles (glb, gltf + referenced textures).\n * - `FONT` \u2013 font files.\n * - `ENV` \u2013 environment maps for 3D scenes.\n */\nexport type UploadFileTypeKey =\n | 'IMAGE'\n | 'RASTER_IMAGE'\n | 'SVG'\n | 'MODEL'\n | 'FONT'\n | 'ENV';\n\nexport interface UploadFileTypeConfig {\n /** Human readable label used in error messages and UI copy. */\n readonly label: string;\n /** Allowed MIME types (lowercase). */\n readonly mimeTypes: readonly string[];\n /** Allowed file extensions including the leading dot (lowercase). */\n readonly extensions: readonly string[];\n /** Value for the HTML `<input accept>` attribute. */\n readonly accept: string;\n}\n\nexport const UPLOAD_FILE_TYPES: Readonly<\n Record<UploadFileTypeKey, UploadFileTypeConfig>\n> = {\n IMAGE: {\n label: 'PNG, JPG, JPEG, WEBP or SVG',\n mimeTypes: [\n 'image/png',\n 'image/jpeg',\n 'image/jpg',\n 'image/webp',\n 'image/svg+xml',\n ],\n extensions: ['.png', '.jpg', '.jpeg', '.webp', '.svg'],\n accept: 'image/png,image/jpeg,image/jpg,image/webp,image/svg+xml',\n },\n RASTER_IMAGE: {\n label: 'PNG, JPG, JPEG or WEBP',\n mimeTypes: ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'],\n extensions: ['.png', '.jpg', '.jpeg', '.webp'],\n accept: 'image/png,image/jpeg,image/jpg,image/webp',\n },\n SVG: {\n label: 'SVG',\n mimeTypes: ['image/svg+xml'],\n extensions: ['.svg'],\n accept: 'image/svg+xml,.svg',\n },\n MODEL: {\n label: 'GLB, GLTF, BIN, PNG, JPG, JPEG or WEBP',\n mimeTypes: [\n 'model/gltf-binary',\n 'model/gltf+json',\n 'application/octet-stream',\n 'image/png',\n 'image/jpeg',\n 'image/jpg',\n 'image/webp',\n ],\n extensions: ['.glb', '.gltf', '.bin', '.png', '.jpg', '.jpeg', '.webp'],\n accept: '.glb,.gltf,.bin,.png,.jpg,.jpeg,.webp',\n },\n FONT: {\n label: 'OTF, TTF or TXT',\n mimeTypes: [\n 'font/ttf',\n 'font/otf',\n 'application/octet-stream',\n 'text/plain',\n ],\n extensions: ['.ttf', '.otf', '.txt'],\n accept: '.ttf,.otf,.txt',\n },\n ENV: {\n label: '.env or .hdr',\n mimeTypes: ['application/octet-stream'],\n extensions: ['.env', '.hdr'],\n accept: '.env,.hdr',\n },\n};\n\n/**\n * Stable string codes the upload middleware emits on 4xx responses. The\n * frontend matches against these to render specific user-facing messages\n * (oversize, wrong type, etc.) without relying on the backend message text.\n */\nexport const UPLOAD_ERROR_CODES = {\n FILE_TOO_LARGE: 'FILE_TOO_LARGE',\n INVALID_FILE_TYPE: 'INVALID_FILE_TYPE',\n FILE_REQUIRED: 'FILE_REQUIRED',\n TOO_MANY_FILES: 'TOO_MANY_FILES',\n UNEXPECTED_FILE_FIELD: 'UNEXPECTED_FILE_FIELD',\n UPLOAD_FAILED: 'UPLOAD_FAILED',\n} as const;\n\nexport type UploadErrorCode =\n (typeof UPLOAD_ERROR_CODES)[keyof typeof UPLOAD_ERROR_CODES];\n\nexport interface UploadErrorDetails {\n fileType?: UploadFileTypeKey;\n maxSizeMB?: number;\n allowedTypes?: readonly string[];\n filename?: string;\n mimeType?: string;\n field?: string;\n [key: string]: unknown;\n}\n\nexport const ROLES = {\n ADMIN: 'ADMIN',\n USER: 'USER',\n} as const;\n\nexport const ACTIVITY_EVENT_TYPES = [\n 'product.created',\n 'product.updated',\n 'product.published',\n 'product.deleted',\n 'product.design_added',\n 'product.design_removed',\n 'product.serialized_saved',\n 'product.serialized_restored',\n 'product.dieline_resnapshotted',\n 'product.screenshot_captured',\n 'product.shared',\n 'product.unshared',\n 'product.access_requested',\n 'product.access_approved',\n 'product.access_denied',\n 'design.created',\n 'design.updated',\n 'design.published',\n 'design.deleted',\n 'design.layers_updated',\n 'design.thumbnail_generated',\n 'glb.uploaded',\n 'glb.updated',\n 'glb.deleted',\n 'user.login',\n 'user.logout',\n // Installation key lifecycle. Rotation and revocation can lock a host's\n // users out, so they need a durable trail, not just a log line.\n 'installation.key_issued',\n 'installation.key_rotated',\n 'installation.disabled',\n 'installation.enabled',\n 'installation.revoked',\n // License terms (Phase 2).\n 'installation.terms_changed',\n 'installation.ips_changed',\n 'installation.seat_limit_reached',\n] as const;\n\nexport const ACTIVITY_ENTITY_TYPES = [\n 'product',\n 'design',\n 'glb',\n 'user',\n 'print_area',\n 'installation',\n] as const;\n// Installation & Access Grant constants\nexport const INSTALLATION_PLATFORMS = [\n 'wordpress',\n 'shopify',\n 'web-app',\n] as const;\nexport const INSTALLATION_STATUS = ['active', 'suspended', 'revoked'] as const;\n", "import { INSTALLATION_STATUS } from './constants';\n\n/**\n * Whether a license works is decided here and only here. `expired` is never\n * stored: it is computed from `expiresAt` at the moment of the check, so an\n * expiry takes effect without any job.\n */\nexport const LICENSE_STATES = [...INSTALLATION_STATUS, 'expired'] as const;\nexport type LicenseState = (typeof LICENSE_STATES)[number];\n\n/** Owners and admins are warned this many days before a license expires. */\nexport const LICENSE_EXPIRY_WARNING_DAYS = 14;\n/** Owners and admins are warned once this share of the seat cap is used. */\nexport const LICENSE_SEAT_WARNING_RATIO = 0.9;\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport interface LicenseTerms {\n status: (typeof INSTALLATION_STATUS)[number];\n expiresAt?: Date | string | null;\n}\n\n/** Status wins over expiry: a revoked license reports `revoked`, not `expired`. */\nexport function licenseState(\n license: LicenseTerms,\n now: Date = new Date()\n): LicenseState {\n if (license.status !== 'active') return license.status;\n if (\n license.expiresAt != null &&\n new Date(license.expiresAt).getTime() <= now.getTime()\n ) {\n return 'expired';\n }\n return 'active';\n}\n\nexport type SeatLevel = 'unlimited' | 'ok' | 'warning' | 'full';\n\nexport function seatLevel(\n endUserCount: number,\n seatLimit: number | null | undefined\n): SeatLevel {\n if (seatLimit == null) return 'unlimited';\n if (endUserCount >= seatLimit) return 'full';\n if (endUserCount >= seatLimit * LICENSE_SEAT_WARNING_RATIO) return 'warning';\n return 'ok';\n}\n\nexport type ExpiryLevel = 'none' | 'ok' | 'soon' | 'expired';\n\nexport function expiryLevel(\n expiresAt: Date | string | null | undefined,\n now: Date = new Date()\n): ExpiryLevel {\n if (expiresAt == null) return 'none';\n const remaining = new Date(expiresAt).getTime() - now.getTime();\n if (remaining <= 0) return 'expired';\n if (remaining <= LICENSE_EXPIRY_WARNING_DAYS * DAY_MS) return 'soon';\n return 'ok';\n}\n\n/**\n * The 401 error code admin-api answers when a LICENSE token (the no-user\n * bearer a host sets as `license-token`) is rejected \u2014 expired, its license\n * inactive, or its key rotated. The builder answers it by asking the host for\n * a fresh license token (`tdn-license-token-required`), never with a login\n * prompt; a route's own \"needs a user\" 401 keeps the generic code.\n */\nexport const LICENSE_TOKEN_INVALID_CODE = 'LICENSE_TOKEN_INVALID';\n"],
|
|
5
|
+
"mappings": "AAIO,IAAMA,EAA4B,mBAElC,SAASC,EAA0BC,EAAyB,CACjEA,EAAK,cACH,IAAI,YAAYF,EAA2B,CACzC,QAAS,GACT,SAAU,EACZ,CAAC,CACH,CACF,CAOO,IAAMG,EAA8B,oBAwCpC,IAAMC,EAAwC,kBAgBxCC,EACX,sBAYK,SAASC,EACdC,EACAC,EACM,CACND,EAAK,cACH,IAAI,YACFF,EACA,CACE,QAAS,GACT,SAAU,GACV,OAAAG,CACF,CACF,CACF,CACF,CAQO,IAAMC,EACX,qBAuCK,IAAMC,EACX,qBAkCK,IAAMC,EACX,sBAMK,SAASC,EACdC,EACAC,EAA+B,CAAC,EAC1B,CACND,EAAK,cACH,IAAI,YACFF,EACA,CAAE,QAAS,GAAO,SAAU,GAAO,OAAAG,CAAO,CAC5C,CACF,CACF,CAcO,IAAMC,EAAqC,oBA6B3C,IAAMC,EAAoC,mBA+B1C,IAAMC,EAAoC,mBA2B1C,IAAMC,EACX,yBA0CK,IAAMC,EACX,yBA6BK,IAAMC,EACX,6BAkBK,IAAMC,EAAoC,mBAE1C,SAASC,EAAoBC,EAAyB,CAC3DA,EAAK,cACH,IAAI,YAAYF,EAAmC,CACjD,QAAS,GACT,SAAU,EACZ,CAAC,CACH,CACF,CAQO,IAAMG,EACX,sBC7ZK,IAAMC,EAAc,CACzB,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,OACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,SACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,WACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,SACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,eACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,cACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,eACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,SACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,aACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,CACF,EAGaC,EAAmBD,EAAY,QAASE,GAAUA,EAAM,MAAM,EChUpE,IAAMC,EAA6B,KAAO,KAe1C,IAAMC,EAAuB,IAAM,KAuMnC,IAAMC,EAAsB,CAAC,SAAU,YAAa,SAAS,ECxO7D,IAAMC,EAAiB,CAAC,GAAGC,EAAqB,SAAS,EAQhE,IAAMC,EAAS,KAAU,GAAK",
|
|
6
|
+
"names": ["THREED_BUILDER_OPEN_EVENT", "dispatchOpenThreedBuilder", "host", "THREED_BUILDER_STATUS_EVENT", "THREED_BUILDER_EXPORT_REQUESTED_EVENT", "THREED_BUILDER_EXPORT_COMPLETE_EVENT", "dispatchExportComplete", "host", "detail", "THREED_BUILDER_PRODUCT_CLONED_EVENT", "THREED_BUILDER_LOGIN_REQUIRED_EVENT", "THREED_BUILDER_LOGIN_CANCELLED_EVENT", "dispatchLoginCancelled", "host", "detail", "THREED_BUILDER_LICENSE_ERROR_EVENT", "THREED_BUILDER_DESIGN_SAVED_EVENT", "THREED_BUILDER_IMAGES_SAVED_EVENT", "THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT", "THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT", "THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT", "THREED_BUILDER_SAVE_REQUEST_EVENT", "dispatchSaveRequest", "host", "THREED_BUILDER_CLOSE_REQUESTED_EVENT", "colorGroups", "colors", "group", "HDRI_PUBLISH_MAX_ENV_BYTES", "MAX_LAYER_PATH_BYTES", "INSTALLATION_STATUS", "LICENSE_STATES", "INSTALLATION_STATUS", "DAY_MS"]
|
|
7
|
+
}
|
package/events.d.ts
CHANGED
|
@@ -120,10 +120,11 @@ interface LicenseErrorDetail {
|
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
122
|
* The design was saved to 3dn. Fired as soon as the save is stored (before
|
|
123
|
-
* the
|
|
124
|
-
* `tdn-
|
|
125
|
-
*
|
|
126
|
-
*
|
|
123
|
+
* the product's images are refreshed; `tdn-images-saved` or
|
|
124
|
+
* `tdn-images-save-failed` follows), and after "Save as copy" too (then
|
|
125
|
+
* followed by `tdn-product-cloned`). This — not `tdn-status-change` `ready`,
|
|
126
|
+
* which also fires after every load step — is the signal a host should
|
|
127
|
+
* continue on (add to cart, show a confirmation).
|
|
127
128
|
*/
|
|
128
129
|
declare const THREED_BUILDER_DESIGN_SAVED_EVENT: "tdn-design-saved";
|
|
129
130
|
interface DesignSavedDetail {
|
|
@@ -131,6 +132,29 @@ interface DesignSavedDetail {
|
|
|
131
132
|
/** The stored serialized version after this save. */
|
|
132
133
|
version: number;
|
|
133
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The product's gallery images (its thumbnail and camera-angle screenshots)
|
|
137
|
+
* now show the design at `version`, and reading the product returns them.
|
|
138
|
+
* Fired after every `tdn-design-saved`, once the screenshots are uploaded and
|
|
139
|
+
* stored on the product — before `tdn-product-cloned` when the save made a
|
|
140
|
+
* copy. Also fired by the builder's Close when autosave had stored the design
|
|
141
|
+
* without screenshots: the images are refreshed before `tdn-close-requested`.
|
|
142
|
+
* This — not `tdn-design-saved` or `tdn-status-change` — is when a host may
|
|
143
|
+
* read the product's images.
|
|
144
|
+
*/
|
|
145
|
+
declare const THREED_BUILDER_IMAGES_SAVED_EVENT: "tdn-images-saved";
|
|
146
|
+
interface ImagesSavedDetail {
|
|
147
|
+
productId: string;
|
|
148
|
+
/** The stored serialized version the images show. */
|
|
149
|
+
version: number;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The design was saved but its images could not be refreshed (capture or
|
|
153
|
+
* upload failed); the product keeps its previous images. Fired in place of
|
|
154
|
+
* `tdn-images-saved`. The builder has already warned the user; the design
|
|
155
|
+
* itself is safe.
|
|
156
|
+
*/
|
|
157
|
+
declare const THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT: "tdn-images-save-failed";
|
|
134
158
|
/**
|
|
135
159
|
* Why a save did not happen:
|
|
136
160
|
* - `conflict`: someone saved first; the builder shows its conflict dialog.
|
|
@@ -193,5 +217,5 @@ interface CloseRequestedDetail {
|
|
|
193
217
|
hasUnsavedChanges: boolean;
|
|
194
218
|
}
|
|
195
219
|
|
|
196
|
-
export { THREED_BUILDER_CLOSE_REQUESTED_EVENT, THREED_BUILDER_DESIGN_SAVED_EVENT, THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT, THREED_BUILDER_EXPORT_COMPLETE_EVENT, THREED_BUILDER_EXPORT_REQUESTED_EVENT, THREED_BUILDER_LICENSE_ERROR_EVENT, THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT, THREED_BUILDER_LOGIN_CANCELLED_EVENT, THREED_BUILDER_LOGIN_REQUIRED_EVENT, THREED_BUILDER_OPEN_EVENT, THREED_BUILDER_PRODUCT_CLONED_EVENT, THREED_BUILDER_SAVE_REQUEST_EVENT, THREED_BUILDER_STATUS_EVENT };
|
|
197
|
-
export type { BuilderStatus, BuilderStatusDetail, CloseRequestedDetail, DesignSaveFailedDetail, DesignSaveFailureReason, DesignSavedDetail, ExportCompleteDetail, ExportFile, ExportRequestedDetail, LicenseErrorCode, LicenseErrorDetail, LoginCancelledDetail, LoginRequiredAction, LoginRequiredDetail, ProductClonedDetail };
|
|
220
|
+
export { THREED_BUILDER_CLOSE_REQUESTED_EVENT, THREED_BUILDER_DESIGN_SAVED_EVENT, THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT, THREED_BUILDER_EXPORT_COMPLETE_EVENT, THREED_BUILDER_EXPORT_REQUESTED_EVENT, THREED_BUILDER_IMAGES_SAVED_EVENT, THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT, THREED_BUILDER_LICENSE_ERROR_EVENT, THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT, THREED_BUILDER_LOGIN_CANCELLED_EVENT, THREED_BUILDER_LOGIN_REQUIRED_EVENT, THREED_BUILDER_OPEN_EVENT, THREED_BUILDER_PRODUCT_CLONED_EVENT, THREED_BUILDER_SAVE_REQUEST_EVENT, THREED_BUILDER_STATUS_EVENT };
|
|
221
|
+
export type { BuilderStatus, BuilderStatusDetail, CloseRequestedDetail, DesignSaveFailedDetail, DesignSaveFailureReason, DesignSavedDetail, ExportCompleteDetail, ExportFile, ExportRequestedDetail, ImagesSavedDetail, LicenseErrorCode, LicenseErrorDetail, LoginCancelledDetail, LoginRequiredAction, LoginRequiredDetail, ProductClonedDetail };
|
package/events.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as E,c as _,d as D,e,g as R,h as T,i as
|
|
1
|
+
import{a as E,c as _,d as D,e,g as R,h as T,i as L,k as t,l as I,m as i,n as N,o as a,p as S,q as U,s as l}from"./chunks/chunk-XQRNTDNA.js";export{l as THREED_BUILDER_CLOSE_REQUESTED_EVENT,I as THREED_BUILDER_DESIGN_SAVED_EVENT,a as THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,e as THREED_BUILDER_EXPORT_COMPLETE_EVENT,D as THREED_BUILDER_EXPORT_REQUESTED_EVENT,i as THREED_BUILDER_IMAGES_SAVED_EVENT,N as THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT,t as THREED_BUILDER_LICENSE_ERROR_EVENT,S as THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,L as THREED_BUILDER_LOGIN_CANCELLED_EVENT,T as THREED_BUILDER_LOGIN_REQUIRED_EVENT,E as THREED_BUILDER_OPEN_EVENT,R as THREED_BUILDER_PRODUCT_CLONED_EVENT,U as THREED_BUILDER_SAVE_REQUEST_EVENT,_ as THREED_BUILDER_STATUS_EVENT};
|
|
2
2
|
//# sourceMappingURL=events.js.map
|
package/index.d.ts
CHANGED
|
@@ -142,9 +142,17 @@ interface BuilderHostConfig {
|
|
|
142
142
|
/**
|
|
143
143
|
* Every successful save, including ones the shopper starts from the builder's
|
|
144
144
|
* own Save button. Fires after any clone mapping has been recorded; never
|
|
145
|
-
* when recording failed.
|
|
145
|
+
* when recording failed. The product's images may still show the previous
|
|
146
|
+
* save: read them on `onImagesSaved`.
|
|
146
147
|
*/
|
|
147
148
|
onSaved?(saved: SavedDesignEvent): void;
|
|
149
|
+
/**
|
|
150
|
+
* The saved product's images now show the design at `version`: read them
|
|
151
|
+
* now. Fires after the `onSaved` of the save that took them, and when the
|
|
152
|
+
* builder's Close refreshed images that autosave left behind. Never fires
|
|
153
|
+
* when the images could not be updated (the product keeps its old ones).
|
|
154
|
+
*/
|
|
155
|
+
onImagesSaved?(saved: SavedDesignEvent): void;
|
|
148
156
|
onLicenseError?(error: BuilderError): void;
|
|
149
157
|
onCloseRequested?(detail: CloseRequestedDetail): void;
|
|
150
158
|
onStatusChange?(detail: BuilderStatusDetail): void;
|
|
@@ -210,6 +218,11 @@ declare class BuilderInstance {
|
|
|
210
218
|
private unrecordedLink?;
|
|
211
219
|
/** Set between a save that changed the product id and its tdn-product-cloned. */
|
|
212
220
|
private cloneWait?;
|
|
221
|
+
/**
|
|
222
|
+
* Set between a tdn-design-saved and that save's images outcome: true on
|
|
223
|
+
* tdn-images-saved, false on tdn-images-save-failed.
|
|
224
|
+
*/
|
|
225
|
+
private imagesWait?;
|
|
213
226
|
private readonly exports;
|
|
214
227
|
private readonly listeners;
|
|
215
228
|
private constructor();
|
|
@@ -223,8 +236,9 @@ declare class BuilderInstance {
|
|
|
223
236
|
/** Shows the builder UI, for a host that mounted it before showing it. */
|
|
224
237
|
open(): void;
|
|
225
238
|
/**
|
|
226
|
-
* Saves the design. Resolves once it is saved
|
|
227
|
-
* required has been recorded
|
|
239
|
+
* Saves the design. Resolves once it is saved, any clone mapping it
|
|
240
|
+
* required has been recorded, and the product's images were refreshed (or
|
|
241
|
+
* could not be: the save still resolves); concurrent calls share one save.
|
|
228
242
|
*/
|
|
229
243
|
save(): Promise<SavedDesign>;
|
|
230
244
|
/** Calls session.get() again and applies what it returns. */
|
|
@@ -244,7 +258,9 @@ declare class BuilderInstance {
|
|
|
244
258
|
private setLoginPending;
|
|
245
259
|
private onLicenseError;
|
|
246
260
|
private onDesignSaved;
|
|
247
|
-
private
|
|
261
|
+
private onImagesSettled;
|
|
262
|
+
/** A host callback that throws never breaks the save. */
|
|
263
|
+
private notify;
|
|
248
264
|
private onProductCloned;
|
|
249
265
|
private recordLink;
|
|
250
266
|
private finishSave;
|
package/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{b as R,c as k,d as P,e as _,f as B,g as O,h as C,j as m,k as A,l as N,m as M,n as F,p as U,q as V}from"./chunks/chunk-F4FUPJ4Y.js";var s=class t extends Error{constructor(e,n,r={}){var o,i;super(n),this.name="BuilderError",this.code=e,this.retryable=(o=r.retryable)!=null?o:!1,this.fatal=(i=r.fatal)!=null?i:!1,this.reason=r.reason,this.cause=r.cause,Object.setPrototypeOf(this,t.prototype)}},H={conflict:"Someone else saved this design first.",forbidden:"This shopper may not save this design.",unauthorized:"The shopper could not be signed in to save.","login-cancelled":"The design was not saved because the shopper did not log in.","installation-inactive":"This site's 3DN installation is not active.",license:"This page is not licensed to use the 3DN builder.",error:"The design could not be saved. Check the connection and try again."};function q(t){var n;let e=(n=H[t.reason])!=null?n:H.error;return t.reason==="login-cancelled"?new s("LOGIN_CANCELLED",e,{retryable:!0,reason:t.reason,cause:t}):new s("SAVE_FAILED",e,{retryable:t.retryable,reason:t.reason,cause:t})}function $(t){let e=t==="ORIGIN_NOT_ALLOWED"?"This website is not on the 3DN license. Add its hostname to the license.":"The 3DN license token was rejected. The license may be suspended, revoked or expired.";return new s("LICENSE_ERROR",e,{fatal:!0,reason:t,cause:{code:t}})}function g(){return new s("DESTROYED","The builder was destroyed.")}function a(t){return new s("CONFIG_INVALID",t,{fatal:!0})}var oe=45e3,j=12e4,ie=new Set(["environment","version","saveTimeoutMs","session","login","customization","onSaved","onLicenseError","onCloseRequested","onStatusChange","debug"]),se=new Set(["target","hostProductId","mode","designId","externalExport","observabilityContext"]),ae=/^\d+\.\d+\.\d+$/,de=/^tdn_inst_/;function b(t){return ae.test(t)}function u(t){return typeof t=="object"&&t!==null}function f(t){return typeof t=="function"}function z(t){return typeof t=="number"&&Number.isFinite(t)&&t>0}function G(t,e=0){return typeof t=="string"?de.test(t):!u(t)||e>3?!1:Object.values(t).some(n=>G(n,e+1))}function X(t,e,n){for(let r of Object.keys(t))if(!e.has(r))throw/url|src|host|origin/i.test(r)?a(`${n}: "${r}" is not allowed. The SDK decides where the builder loads from; choose an environment and a version instead.`):a(`${n}: unknown option "${r}".`)}function K(t){var h,l,c;if(!u(t))throw a("createBuilder() needs a configuration object.");if(G(t))throw a("createBuilder(): an installation key was passed to the browser. Installation keys stay on your server; pass a session callback instead.");X(t,ie,"createBuilder()");let e=(h=t.environment)!=null?h:"production";if(e!=="production"&&e!=="staging")throw a(`createBuilder(): environment must be "production" or "staging", got ${JSON.stringify(e)}.`);let n=(l=t.version)!=null?l:"latest";if(typeof n!="string"||n!=="latest"&&!b(n))throw a(`createBuilder(): version must be "latest" or an exact release such as "1.2.3", got ${JSON.stringify(n)}.`);let r=(c=t.saveTimeoutMs)!=null?c:oe;if(!z(r))throw a("createBuilder(): saveTimeoutMs must be a positive number of milliseconds.");let o=t.session;if(!u(o)||!f(o.get)||!f(o.refreshLicense))throw a("createBuilder(): session.get() and session.refreshLicense() are required. They call your session endpoint.");let i=t.login;if(i!==void 0){if(!u(i)||!f(i.getTokens)||!f(i.onRequired))throw a("createBuilder(): login needs getTokens() (your login-token endpoint) and onRequired() (your login UI).");if(i.onMintFailed!==void 0&&!f(i.onMintFailed))throw a("createBuilder(): login.onMintFailed must be a function.")}let d=t.customization;if(d!==void 0&&(!u(d)||!f(d.record)))throw a("createBuilder(): customization.record() is required when customization is set. It calls your customization endpoint.");for(let T of["onSaved","onLicenseError","onCloseRequested","onStatusChange"])if(t[T]!==void 0&&!f(t[T]))throw a(`createBuilder(): ${T} must be a function.`);return{...t,environment:e,version:n,saveTimeoutMs:r}}function W(t){var d;if(!u(t))throw a("mount() needs an options object.");X(t,se,"mount()");let e=t.target;if(typeof HTMLElement=="undefined"||!(e instanceof HTMLElement))throw a("mount(): target must be an HTMLElement on the page.");let n=t.hostProductId;if(typeof n!="string"||n.trim()==="")throw a("mount(): hostProductId must be your own, non-empty product reference.");let r=(d=t.mode)!=null?d:"designer";if(r!=="designer"&&r!=="viewer")throw a(`mount(): mode must be "designer" or "viewer", got ${JSON.stringify(r)}.`);let o=t.designId;if(o!==void 0&&(typeof o!="string"||o===""))throw a("mount(): designId must be a non-empty string.");t.observabilityContext!==void 0&&w(t.observabilityContext,"mount()");let i=t.externalExport;if(i!==void 0){if(!u(i)||!f(i.run))throw a("mount(): externalExport.run() is required when externalExport is set.");if(i.timeoutMs!==void 0&&!z(i.timeoutMs))throw a("mount(): externalExport.timeoutMs must be a positive number of milliseconds.")}return t}function w(t,e){let n;try{n=typeof t=="string"?JSON.parse(t):void 0}catch{n=void 0}if(!u(n)||Array.isArray(n))throw a(`${e}: observabilityContext must be a JSON object string.`);return t}function S(t){let e=o=>new s("SESSION_FAILED",`session.get() returned an invalid session: ${o}.`,{retryable:!1});if(!u(t))throw e("expected an object");if(typeof t.licenseToken!="string"||t.licenseToken==="")throw e("licenseToken is missing");if(typeof t.productId!="string"||t.productId==="")throw e("productId is missing");if(t.viewToken!=null&&typeof t.viewToken!="string")throw e("viewToken must be a string");let n=t.token!=null,r=t.refreshToken!=null;if(n!==r)throw e("token and refreshToken must be returned together");if(n&&(typeof t.token!="string"||typeof t.refreshToken!="string"))throw e("token and refreshToken must be strings");return t}function Y(t){if(t===null)return null;if(!u(t)||typeof t.token!="string"||t.token===""||typeof t.refreshToken!="string"||t.refreshToken==="")throw new s("SESSION_FAILED","login.getTokens() must resolve { token, refreshToken } or null.");return t}var ue=/token|secret|key|authorization/i;function D(t,e=0){if(t===null||typeof t!="object"||e>4)return t;if(Array.isArray(t))return t.map(r=>D(r,e+1));let n={};for(let[r,o]of Object.entries(t))n[r]=ue.test(r)&&o!=null?"[redacted]":D(o,e+1);return n}function J(t){return t?(e,n)=>{n===void 0?console.debug(`[3DN] ${e}`):console.debug(`[3DN] ${e}`,D(n))}:()=>{}}var v="threed-builder",ee={production:"https://3d-builder.3dn.app",staging:void 0},le=3e4,Q=Symbol.for("@3dn-builder/builder/loader");function ce(){let t=globalThis,e=t[Q];return e||(e={},t[Q]=e),e}function he(){return typeof window!="undefined"&&typeof document!="undefined"&&typeof customElements!="undefined"}function te(t){if(!ee[t])throw a(`createBuilder(): the "${t}" environment is not available in this SDK release.`)}function p(t,e){return new s("LOAD_FAILED",t,{retryable:!0,cause:e})}async function fe(t,e){if(e!=="latest")return e;let n;try{n=await fetch(`${t}/version-info.json`,{cache:"no-store",credentials:"omit"})}catch(i){throw p("The 3DN builder could not be reached.",i)}if(!n.ok)throw p(`The 3DN builder release could not be resolved (HTTP ${n.status}).`);let r=await n.json().catch(()=>null),o=r==null?void 0:r.version;if(typeof o!="string"||!b(o))throw p("The 3DN builder release information was not valid.");return o}function pe(t){return new Promise((e,n)=>{let r=document.createElement("script");r.type="module",r.src=t,r.crossOrigin="anonymous",r.onload=()=>e(r),r.onerror=()=>{r.remove(),n(p("The 3DN builder failed to load."))},document.head.appendChild(r)})}function ge(){return new Promise((t,e)=>{let n=setTimeout(()=>e(p("The 3DN builder loaded but did not start.")),le);customElements.whenDefined(v).then(()=>{clearTimeout(n),t()},e)})}function Z(t,e,n){if(t.environment&&t.environment!==e)throw new s("VERSION_CONFLICT",`The ${t.environment} builder already runs on this page; ${e} cannot load beside it.`,{fatal:!0});if(n!=="latest"&&t.version&&t.version!==n)throw new s("VERSION_CONFLICT",`Builder ${t.version} already runs on this page; ${n} cannot load beside it.`,{fatal:!0})}async function ne(t,e){if(!he())throw new s("NOT_IN_BROWSER","The 3DN builder can only load in a browser.");let n=ce();if(n.loading){await n.loading,Z(n,t,e);return}if(customElements.get(v))return;let r=ee[t];if(!r)throw a(`The "${t}" environment is not available in this SDK release.`);n.environment=t,n.loading=(async()=>{let o=await fe(r,e);n.version=o;let i=await pe(`${r}/builder-${o}.js`);try{await ge()}catch(d){throw i.remove(),d}})();try{await n.loading}catch(o){throw n.loading=void 0,n.environment=void 0,n.version=void 0,o instanceof s?o:p("The 3DN builder failed to load.",o)}Z(n,t,e)}function re(){let t,e,n=new Promise((r,o)=>{t=r,e=o});return n.catch(()=>{}),{promise:n,resolve:t,reject:e}}var L=class{constructor(e,n){this.onExpire=n;this.startedAt=0;this.remaining=e}start(){this.handle===void 0&&(this.startedAt=Date.now(),this.handle=setTimeout(this.onExpire,this.remaining))}pause(){this.handle!==void 0&&(clearTimeout(this.handle),this.handle=void 0,this.remaining=Math.max(0,this.remaining-(Date.now()-this.startedAt)))}stop(){this.handle!==void 0&&clearTimeout(this.handle),this.handle=void 0}},E=class t{constructor(e,n,r){this.ctx=e;this.options=n;this.epoch=0;this.destroyed=!1;this.loginPending=!1;this.exports=new Set;this.listeners=[];this.currentProductId=r.productId,this.element=this.createElement(r)}static async mount(e,n){let r;try{r=await e.config.session.get({hostProductId:n.hostProductId})}catch(d){throw new s("SESSION_FAILED","The builder session could not be loaded.",{retryable:!0,cause:d})}let o=S(r),i=new t(e,n,o);return n.target.appendChild(i.element),e.log("mounted",{hostProductId:n.hostProductId,productId:o.productId}),i}get productId(){return this.currentProductId}get hostProductId(){return this.options.hostProductId}createElement(e){let n=document.createElement(v);return this.attachListeners(n),n.setAttribute("product",e.productId),n.setAttribute("license-token",e.licenseToken),e.viewToken&&n.setAttribute("view-token",e.viewToken),e.token&&e.refreshToken&&I(n,{token:e.token,refreshToken:e.refreshToken}),this.options.designId&&n.setAttribute("design-id",this.options.designId),this.options.mode==="viewer"&&n.setAttribute("display-only","true"),this.options.externalExport&&n.setAttribute("external-export","true"),this.options.observabilityContext&&n.setAttribute("observability-context",this.options.observabilityContext),n.style.display="block",n.style.width="100%",n.style.height="100%",n}attachListeners(e){let n=(r,o)=>{let i=d=>{d.target!==e||this.destroyed||(this.ctx.log(r,d.detail),o(d.detail))};e.addEventListener(r,i),this.listeners.push([r,i])};n(C,r=>{var o;this.answerLogin((o=r==null?void 0:r.action)!=null?o:"save")}),n(F,()=>{this.ctx.refreshLicense()}),n(A,r=>this.onLicenseError(r)),n(N,r=>{this.onDesignSaved(r)}),n(M,r=>this.finishSave(q(r))),n(O,r=>{this.onProductCloned(r)}),n(V,r=>{var o,i;return(i=(o=this.ctx.config).onCloseRequested)==null?void 0:i.call(o,r)}),n(k,r=>{var o,i;return(i=(o=this.ctx.config).onStatusChange)==null?void 0:i.call(o,r)}),this.options.externalExport&&n(P,r=>{this.answerExport(r)})}open(){this.assertUsable(),R(this.element)}save(){if(this.destroyed)return Promise.reject(g());if(this.fatal)return Promise.reject(this.fatal);if(this.saveRun)return this.saveRun.result.promise;let e={result:re(),timer:new L(this.ctx.config.saveTimeoutMs,()=>this.finishSave(new s("SAVE_TIMEOUT","Saving the design took too long. Try again.",{retryable:!0})))};return this.saveRun=e,this.loginPending||e.timer.start(),U(this.element),e.result.promise}async refreshSession(){this.assertUsable();let e=this.epoch,n;try{n=await this.ctx.config.session.get({hostProductId:this.options.hostProductId})}catch(o){throw new s("SESSION_FAILED","The builder session could not be refreshed.",{retryable:!0,cause:o})}let r=S(n);e!==this.epoch||this.destroyed||(this.applyLicense(r.licenseToken),r.viewToken&&this.element.setAttribute("view-token",r.viewToken),r.token&&r.refreshToken&&I(this.element,{token:r.token,refreshToken:r.refreshToken}),this.fatal=void 0)}logout(){this.destroyed||(this.epoch++,this.element.removeAttribute("bearer-token"),this.element.removeAttribute("refresh-token"),this.loginPending&&(this.loginPending=!1,this.finishSave(new s("LOGIN_CANCELLED","The shopper logged out before the design was saved.",{retryable:!0}))))}destroy(){var e;if(!this.destroyed){this.epoch++,this.finishSave(g()),(e=this.cloneWait)==null||e.reject(g()),this.cloneWait=void 0;for(let n of this.exports)n.abort();this.destroyed=!0;for(let[n,r]of this.listeners)this.element.removeEventListener(n,r);this.listeners.length=0,this.element.remove(),this.ctx.onDestroyed(this),this.ctx.log("destroyed")}}setObservabilityContext(e){this.assertUsable(),this.element.setAttribute("observability-context",w(e,"setObservabilityContext()"))}applyLicense(e){this.destroyed||this.element.setAttribute("license-token",e)}get isDestroyed(){return this.destroyed}async answerLogin(e){if(this.loginPending)return;let n=this.ctx.config.login;if(!n){m(this.element,{action:e});return}let r=this.epoch,o=()=>r!==this.epoch||this.destroyed;this.setLoginPending(!0);let i=!1;try{for(;;){let d;try{d=Y(await n.getTokens())}catch(l){if(o())return;this.ctx.log("login.getTokens() failed",{cause:String(l)});let c=n.onMintFailed?await n.onMintFailed():"cancel";if(o())return;if(c==="retry")continue;m(this.element,{action:e});return}if(o())return;if(d){I(this.element,d);return}let h=await n.onRequired(e);if(o())return;if(h!=="logged-in"){if(h==="redirected"){i=!0;return}m(this.element,{action:e});return}}}finally{!i&&r===this.epoch&&this.setLoginPending(!1)}}setLoginPending(e){this.loginPending=e,this.saveRun&&(e?this.saveRun.timer.pause():this.saveRun.timer.start())}onLicenseError(e){var r,o;let n=$(e.code);this.fatal=n,this.finishSave(n),(o=(r=this.ctx.config).onLicenseError)==null||o.call(r,n)}async onDesignSaved(e){e.productId!==this.currentProductId&&!this.cloneWait&&(this.cloneWait=re());try{this.cloneWait&&await this.cloneWait.promise,this.unrecordedLink&&await this.recordLink(this.unrecordedLink)}catch(r){this.finishSave(r instanceof s?r:new s("SAVE_FAILED","The design was saved but could not be linked to this shopper.",{retryable:!0,reason:"customization",cause:r}));return}if(this.destroyed)return;let n={productId:e.productId,version:e.version};this.notifySaved(n),this.finishSave(void 0,n)}notifySaved(e){var n,r;try{(r=(n=this.ctx.config).onSaved)==null||r.call(n,{hostProductId:this.options.hostProductId,...e})}catch(o){this.ctx.log("onSaved threw",{cause:String(o)})}}async onProductCloned(e){let n={hostProductId:this.options.hostProductId,sourceProductId:e.originalProductId,savedProductId:e.newProductId,asCopy:e.asCopy===!0};this.currentProductId=e.newProductId;let r=this.cloneWait;this.cloneWait=void 0;try{await this.recordLink(n),r==null||r.resolve()}catch(o){r==null||r.reject(o)}}async recordLink(e){let n=this.ctx.config.customization;if(n)try{await n.record(e),this.unrecordedLink===e&&(this.unrecordedLink=void 0)}catch(r){throw this.unrecordedLink=e,new s("SAVE_FAILED","The design was saved but could not be linked to this shopper. Try saving again.",{retryable:!0,reason:"customization",cause:r})}}finishSave(e,n){let r=this.saveRun;r&&(this.saveRun=void 0,r.timer.stop(),e?r.result.reject(e):n&&r.result.resolve(n))}async answerExport(e){var h;let n=this.options.externalExport;if(!n)return;let r=new AbortController;this.exports.add(r);let o=!1,i=setTimeout(()=>{o=!0,r.abort()},(h=n.timeoutMs)!=null?h:j),d=new Promise((l,c)=>r.signal.addEventListener("abort",()=>c(new Error("aborted"))));try{let l=await Promise.race([n.run(e,r.signal),d]);B(this.element,{files:l})}catch(l){let c=o?"EXPORT_TIMEOUT":"EXPORT_FAILED";this.ctx.log("export failed",{code:c,cause:String(l)}),this.replyExportFailure(c)}finally{clearTimeout(i),this.exports.delete(r)}}replyExportFailure(e){let n={files:[],error:{code:e}};this.element.dispatchEvent(new CustomEvent(_,{bubbles:!1,composed:!1,detail:n}))}assertUsable(){if(this.destroyed)throw g()}};function I(t,e){t.setAttribute("refresh-token",e.refreshToken),t.setAttribute("bearer-token",e.token)}var x=class{constructor(e){this.config=e;this.instances=[];this.log=J(e.debug),this.context={config:e,log:this.log,refreshLicense:()=>this.refreshLicense().catch(()=>{}),onDestroyed:n=>{let r=this.instances.indexOf(n);r>=0&&this.instances.splice(r,1)}}}get current(){return this.instances[this.instances.length-1]}load(){return ne(this.config.environment,this.config.version)}async mount(e){let n=W(e);await this.load();let r=await E.mount(this.context,n);return this.instances.push(r),r}open(){this.requireCurrent().open()}save(){let e=this.current;return e?e.save():Promise.reject(this.notMounted())}refreshSession(){let e=this.current;return e?e.refreshSession():Promise.reject(this.notMounted())}refreshLicense(){return this.licenseRefresh||(this.licenseRefresh=(async()=>{try{let e=await this.config.session.refreshLicense(),n=e==null?void 0:e.licenseToken;if(typeof n!="string"||n==="")throw new s("SESSION_FAILED","session.refreshLicense() must resolve { licenseToken }.");for(let r of this.instances)r.applyLicense(n);this.log("license refreshed")}catch(e){throw this.log("license refresh failed",{cause:String(e)}),e instanceof s?e:new s("SESSION_FAILED","A fresh license token could not be fetched.",{retryable:!0,cause:e})}finally{this.licenseRefresh=void 0}})()),this.licenseRefresh}logout(){for(let e of this.instances)e.logout();this.log("logged out")}destroy(){for(let e of[...this.instances])e.destroy()}requireCurrent(){let e=this.current;if(!e)throw this.notMounted();return e}notMounted(){return a("No builder is mounted. Call mount() first.")}};function me(t){let e=K(t);return te(e.environment),new x(e)}var y="tdn_resume",ve="tdn-draft-index";function Ee(){let t=new URL(window.location.href);return t.searchParams.set(y,"1"),t.pathname+t.search+t.hash}function ye(t){return/^\/(?![/\\])/.test(t)}function Te(){if(typeof window=="undefined")return!1;let t=new URL(window.location.href);return t.searchParams.get(y)!=="1"?!1:(t.searchParams.delete(y),window.history.replaceState(window.history.state,"",t.pathname+t.search+t.hash),!0)}function be(t){var e;if(typeof localStorage=="undefined")return!1;try{let n=JSON.parse((e=localStorage.getItem(ve))!=null?e:"{}");return Object.values(n).some(r=>(r==null?void 0:r.productId)===t)}catch{return!1}}export{s as BuilderError,y as RESUME_PARAM,Te as consumeResumeMarker,me as createBuilder,be as hasDraft,ye as isSafeReturnPath,Ee as loginReturnPath};
|
|
1
|
+
import{b as k,c as P,d as _,e as B,f as O,g as C,h as A,j as m,k as N,l as M,m as F,n as U,o as V,p as H,r as $,s as q}from"./chunks/chunk-XQRNTDNA.js";var s=class t extends Error{constructor(e,n,r={}){var i,o;super(n),this.name="BuilderError",this.code=e,this.retryable=(i=r.retryable)!=null?i:!1,this.fatal=(o=r.fatal)!=null?o:!1,this.reason=r.reason,this.cause=r.cause,Object.setPrototypeOf(this,t.prototype)}},j={conflict:"Someone else saved this design first.",forbidden:"This shopper may not save this design.",unauthorized:"The shopper could not be signed in to save.","login-cancelled":"The design was not saved because the shopper did not log in.","installation-inactive":"This site's 3DN installation is not active.",license:"This page is not licensed to use the 3DN builder.",error:"The design could not be saved. Check the connection and try again."};function z(t){var n;let e=(n=j[t.reason])!=null?n:j.error;return t.reason==="login-cancelled"?new s("LOGIN_CANCELLED",e,{retryable:!0,reason:t.reason,cause:t}):new s("SAVE_FAILED",e,{retryable:t.retryable,reason:t.reason,cause:t})}function W(t){let e=t==="ORIGIN_NOT_ALLOWED"?"This website is not on the 3DN license. Add its hostname to the license.":"The 3DN license token was rejected. The license may be suspended, revoked or expired.";return new s("LICENSE_ERROR",e,{fatal:!0,reason:t,cause:{code:t}})}function g(){return new s("DESTROYED","The builder was destroyed.")}function a(t){return new s("CONFIG_INVALID",t,{fatal:!0})}var se=45e3,G=12e4,ae=new Set(["environment","version","saveTimeoutMs","session","login","customization","onSaved","onImagesSaved","onLicenseError","onCloseRequested","onStatusChange","debug"]),de=new Set(["target","hostProductId","mode","designId","externalExport","observabilityContext"]),ue=/^\d+\.\d+\.\d+$/,le=/^tdn_inst_/;function b(t){return ue.test(t)}function u(t){return typeof t=="object"&&t!==null}function f(t){return typeof t=="function"}function X(t){return typeof t=="number"&&Number.isFinite(t)&&t>0}function K(t,e=0){return typeof t=="string"?le.test(t):!u(t)||e>3?!1:Object.values(t).some(n=>K(n,e+1))}function Y(t,e,n){for(let r of Object.keys(t))if(!e.has(r))throw/url|src|host|origin/i.test(r)?a(`${n}: "${r}" is not allowed. The SDK decides where the builder loads from; choose an environment and a version instead.`):a(`${n}: unknown option "${r}".`)}function J(t){var h,l,c;if(!u(t))throw a("createBuilder() needs a configuration object.");if(K(t))throw a("createBuilder(): an installation key was passed to the browser. Installation keys stay on your server; pass a session callback instead.");Y(t,ae,"createBuilder()");let e=(h=t.environment)!=null?h:"production";if(e!=="production"&&e!=="staging")throw a(`createBuilder(): environment must be "production" or "staging", got ${JSON.stringify(e)}.`);let n=(l=t.version)!=null?l:"latest";if(typeof n!="string"||n!=="latest"&&!b(n))throw a(`createBuilder(): version must be "latest" or an exact release such as "1.2.3", got ${JSON.stringify(n)}.`);let r=(c=t.saveTimeoutMs)!=null?c:se;if(!X(r))throw a("createBuilder(): saveTimeoutMs must be a positive number of milliseconds.");let i=t.session;if(!u(i)||!f(i.get)||!f(i.refreshLicense))throw a("createBuilder(): session.get() and session.refreshLicense() are required. They call your session endpoint.");let o=t.login;if(o!==void 0){if(!u(o)||!f(o.getTokens)||!f(o.onRequired))throw a("createBuilder(): login needs getTokens() (your login-token endpoint) and onRequired() (your login UI).");if(o.onMintFailed!==void 0&&!f(o.onMintFailed))throw a("createBuilder(): login.onMintFailed must be a function.")}let d=t.customization;if(d!==void 0&&(!u(d)||!f(d.record)))throw a("createBuilder(): customization.record() is required when customization is set. It calls your customization endpoint.");for(let T of["onSaved","onImagesSaved","onLicenseError","onCloseRequested","onStatusChange"])if(t[T]!==void 0&&!f(t[T]))throw a(`createBuilder(): ${T} must be a function.`);return{...t,environment:e,version:n,saveTimeoutMs:r}}function Q(t){var d;if(!u(t))throw a("mount() needs an options object.");Y(t,de,"mount()");let e=t.target;if(typeof HTMLElement=="undefined"||!(e instanceof HTMLElement))throw a("mount(): target must be an HTMLElement on the page.");let n=t.hostProductId;if(typeof n!="string"||n.trim()==="")throw a("mount(): hostProductId must be your own, non-empty product reference.");let r=(d=t.mode)!=null?d:"designer";if(r!=="designer"&&r!=="viewer")throw a(`mount(): mode must be "designer" or "viewer", got ${JSON.stringify(r)}.`);let i=t.designId;if(i!==void 0&&(typeof i!="string"||i===""))throw a("mount(): designId must be a non-empty string.");t.observabilityContext!==void 0&&S(t.observabilityContext,"mount()");let o=t.externalExport;if(o!==void 0){if(!u(o)||!f(o.run))throw a("mount(): externalExport.run() is required when externalExport is set.");if(o.timeoutMs!==void 0&&!X(o.timeoutMs))throw a("mount(): externalExport.timeoutMs must be a positive number of milliseconds.")}return t}function S(t,e){let n;try{n=typeof t=="string"?JSON.parse(t):void 0}catch{n=void 0}if(!u(n)||Array.isArray(n))throw a(`${e}: observabilityContext must be a JSON object string.`);return t}function w(t){let e=i=>new s("SESSION_FAILED",`session.get() returned an invalid session: ${i}.`,{retryable:!1});if(!u(t))throw e("expected an object");if(typeof t.licenseToken!="string"||t.licenseToken==="")throw e("licenseToken is missing");if(typeof t.productId!="string"||t.productId==="")throw e("productId is missing");if(t.viewToken!=null&&typeof t.viewToken!="string")throw e("viewToken must be a string");let n=t.token!=null,r=t.refreshToken!=null;if(n!==r)throw e("token and refreshToken must be returned together");if(n&&(typeof t.token!="string"||typeof t.refreshToken!="string"))throw e("token and refreshToken must be strings");return t}function Z(t){if(t===null)return null;if(!u(t)||typeof t.token!="string"||t.token===""||typeof t.refreshToken!="string"||t.refreshToken==="")throw new s("SESSION_FAILED","login.getTokens() must resolve { token, refreshToken } or null.");return t}var ce=/token|secret|key|authorization/i;function I(t,e=0){if(t===null||typeof t!="object"||e>4)return t;if(Array.isArray(t))return t.map(r=>I(r,e+1));let n={};for(let[r,i]of Object.entries(t))n[r]=ce.test(r)&&i!=null?"[redacted]":I(i,e+1);return n}function ee(t){return t?(e,n)=>{n===void 0?console.debug(`[3DN] ${e}`):console.debug(`[3DN] ${e}`,I(n))}:()=>{}}var v="threed-builder",re={production:"https://3d-builder.3dn.app",staging:void 0},he=3e4,te=Symbol.for("@3dn-builder/builder/loader");function fe(){let t=globalThis,e=t[te];return e||(e={},t[te]=e),e}function pe(){return typeof window!="undefined"&&typeof document!="undefined"&&typeof customElements!="undefined"}function ie(t){if(!re[t])throw a(`createBuilder(): the "${t}" environment is not available in this SDK release.`)}function p(t,e){return new s("LOAD_FAILED",t,{retryable:!0,cause:e})}async function ge(t,e){if(e!=="latest")return e;let n;try{n=await fetch(`${t}/version-info.json`,{cache:"no-store",credentials:"omit"})}catch(o){throw p("The 3DN builder could not be reached.",o)}if(!n.ok)throw p(`The 3DN builder release could not be resolved (HTTP ${n.status}).`);let r=await n.json().catch(()=>null),i=r==null?void 0:r.version;if(typeof i!="string"||!b(i))throw p("The 3DN builder release information was not valid.");return i}function me(t){return new Promise((e,n)=>{let r=document.createElement("script");r.type="module",r.src=t,r.crossOrigin="anonymous",r.onload=()=>e(r),r.onerror=()=>{r.remove(),n(p("The 3DN builder failed to load."))},document.head.appendChild(r)})}function ve(){return new Promise((t,e)=>{let n=setTimeout(()=>e(p("The 3DN builder loaded but did not start.")),he);customElements.whenDefined(v).then(()=>{clearTimeout(n),t()},e)})}function ne(t,e,n){if(t.environment&&t.environment!==e)throw new s("VERSION_CONFLICT",`The ${t.environment} builder already runs on this page; ${e} cannot load beside it.`,{fatal:!0});if(n!=="latest"&&t.version&&t.version!==n)throw new s("VERSION_CONFLICT",`Builder ${t.version} already runs on this page; ${n} cannot load beside it.`,{fatal:!0})}async function oe(t,e){if(!pe())throw new s("NOT_IN_BROWSER","The 3DN builder can only load in a browser.");let n=fe();if(n.loading){await n.loading,ne(n,t,e);return}if(customElements.get(v))return;let r=re[t];if(!r)throw a(`The "${t}" environment is not available in this SDK release.`);n.environment=t,n.loading=(async()=>{let i=await ge(r,e);n.version=i;let o=await me(`${r}/builder-${i}.js`);try{await ve()}catch(d){throw o.remove(),d}})();try{await n.loading}catch(i){throw n.loading=void 0,n.environment=void 0,n.version=void 0,i instanceof s?i:p("The 3DN builder failed to load.",i)}ne(n,t,e)}function D(){let t,e,n=new Promise((r,i)=>{t=r,e=i});return n.catch(()=>{}),{promise:n,resolve:t,reject:e}}var R=class{constructor(e,n){this.onExpire=n;this.startedAt=0;this.remaining=e}start(){this.handle===void 0&&(this.startedAt=Date.now(),this.handle=setTimeout(this.onExpire,this.remaining))}pause(){this.handle!==void 0&&(clearTimeout(this.handle),this.handle=void 0,this.remaining=Math.max(0,this.remaining-(Date.now()-this.startedAt)))}stop(){this.handle!==void 0&&clearTimeout(this.handle),this.handle=void 0}},E=class t{constructor(e,n,r){this.ctx=e;this.options=n;this.epoch=0;this.destroyed=!1;this.loginPending=!1;this.exports=new Set;this.listeners=[];this.currentProductId=r.productId,this.element=this.createElement(r)}static async mount(e,n){let r;try{r=await e.config.session.get({hostProductId:n.hostProductId})}catch(d){throw new s("SESSION_FAILED","The builder session could not be loaded.",{retryable:!0,cause:d})}let i=w(r),o=new t(e,n,i);return n.target.appendChild(o.element),e.log("mounted",{hostProductId:n.hostProductId,productId:i.productId}),o}get productId(){return this.currentProductId}get hostProductId(){return this.options.hostProductId}createElement(e){let n=document.createElement(v);return this.attachListeners(n),n.setAttribute("product",e.productId),n.setAttribute("license-token",e.licenseToken),e.viewToken&&n.setAttribute("view-token",e.viewToken),e.token&&e.refreshToken&&L(n,{token:e.token,refreshToken:e.refreshToken}),this.options.designId&&n.setAttribute("design-id",this.options.designId),this.options.mode==="viewer"&&n.setAttribute("display-only","true"),this.options.externalExport&&n.setAttribute("external-export","true"),this.options.observabilityContext&&n.setAttribute("observability-context",this.options.observabilityContext),n.style.display="block",n.style.width="100%",n.style.height="100%",n}attachListeners(e){let n=(r,i)=>{let o=d=>{d.target!==e||this.destroyed||(this.ctx.log(r,d.detail),i(d.detail))};e.addEventListener(r,o),this.listeners.push([r,o])};n(A,r=>{var i;this.answerLogin((i=r==null?void 0:r.action)!=null?i:"save")}),n(H,()=>{this.ctx.refreshLicense()}),n(N,r=>this.onLicenseError(r)),n(M,r=>{this.onDesignSaved(r)}),n(V,r=>this.finishSave(z(r))),n(F,r=>this.onImagesSettled(r,!0)),n(U,r=>this.onImagesSettled(r,!1)),n(C,r=>{this.onProductCloned(r)}),n(q,r=>{var i,o;return(o=(i=this.ctx.config).onCloseRequested)==null?void 0:o.call(i,r)}),n(P,r=>{var i,o;return(o=(i=this.ctx.config).onStatusChange)==null?void 0:o.call(i,r)}),this.options.externalExport&&n(_,r=>{this.answerExport(r)})}open(){this.assertUsable(),k(this.element)}save(){if(this.destroyed)return Promise.reject(g());if(this.fatal)return Promise.reject(this.fatal);if(this.saveRun)return this.saveRun.result.promise;let e={result:D(),timer:new R(this.ctx.config.saveTimeoutMs,()=>this.finishSave(new s("SAVE_TIMEOUT","Saving the design took too long. Try again.",{retryable:!0})))};return this.saveRun=e,this.loginPending||e.timer.start(),$(this.element),e.result.promise}async refreshSession(){this.assertUsable();let e=this.epoch,n;try{n=await this.ctx.config.session.get({hostProductId:this.options.hostProductId})}catch(i){throw new s("SESSION_FAILED","The builder session could not be refreshed.",{retryable:!0,cause:i})}let r=w(n);e!==this.epoch||this.destroyed||(this.applyLicense(r.licenseToken),r.viewToken&&this.element.setAttribute("view-token",r.viewToken),r.token&&r.refreshToken&&L(this.element,{token:r.token,refreshToken:r.refreshToken}),this.fatal=void 0)}logout(){this.destroyed||(this.epoch++,this.element.removeAttribute("bearer-token"),this.element.removeAttribute("refresh-token"),this.loginPending&&(this.loginPending=!1,this.finishSave(new s("LOGIN_CANCELLED","The shopper logged out before the design was saved.",{retryable:!0}))))}destroy(){var e,n;if(!this.destroyed){this.epoch++,this.finishSave(g()),(e=this.cloneWait)==null||e.reject(g()),this.cloneWait=void 0,(n=this.imagesWait)==null||n.resolve(!1),this.imagesWait=void 0;for(let r of this.exports)r.abort();this.destroyed=!0;for(let[r,i]of this.listeners)this.element.removeEventListener(r,i);this.listeners.length=0,this.element.remove(),this.ctx.onDestroyed(this),this.ctx.log("destroyed")}}setObservabilityContext(e){this.assertUsable(),this.element.setAttribute("observability-context",S(e,"setObservabilityContext()"))}applyLicense(e){this.destroyed||this.element.setAttribute("license-token",e)}get isDestroyed(){return this.destroyed}async answerLogin(e){if(this.loginPending)return;let n=this.ctx.config.login;if(!n){m(this.element,{action:e});return}let r=this.epoch,i=()=>r!==this.epoch||this.destroyed;this.setLoginPending(!0);let o=!1;try{for(;;){let d;try{d=Z(await n.getTokens())}catch(l){if(i())return;this.ctx.log("login.getTokens() failed",{cause:String(l)});let c=n.onMintFailed?await n.onMintFailed():"cancel";if(i())return;if(c==="retry")continue;m(this.element,{action:e});return}if(i())return;if(d){L(this.element,d);return}let h=await n.onRequired(e);if(i())return;if(h!=="logged-in"){if(h==="redirected"){o=!0;return}m(this.element,{action:e});return}}}finally{!o&&r===this.epoch&&this.setLoginPending(!1)}}setLoginPending(e){this.loginPending=e,this.saveRun&&(e?this.saveRun.timer.pause():this.saveRun.timer.start())}onLicenseError(e){var r,i;let n=W(e.code);this.fatal=n,this.finishSave(n),(i=(r=this.ctx.config).onLicenseError)==null||i.call(r,n)}async onDesignSaved(e){var o;e.productId!==this.currentProductId&&!this.cloneWait&&(this.cloneWait=D()),(o=this.imagesWait)==null||o.resolve(!1);let n=this.imagesWait=D();try{this.cloneWait&&await this.cloneWait.promise,this.unrecordedLink&&await this.recordLink(this.unrecordedLink)}catch(d){this.finishSave(d instanceof s?d:new s("SAVE_FAILED","The design was saved but could not be linked to this shopper.",{retryable:!0,reason:"customization",cause:d}));return}if(this.destroyed)return;let r={productId:e.productId,version:e.version};this.notify("onSaved",r);let i=await n.promise;this.destroyed||(i&&this.notify("onImagesSaved",r),this.finishSave(void 0,r))}onImagesSettled(e,n){let r=this.imagesWait;if(r){this.imagesWait=void 0,r.resolve(n);return}n&&this.notify("onImagesSaved",{productId:e.productId,version:e.version})}notify(e,n){var r,i;try{(i=(r=this.ctx.config)[e])==null||i.call(r,{hostProductId:this.options.hostProductId,...n})}catch(o){this.ctx.log(`${e} threw`,{cause:String(o)})}}async onProductCloned(e){let n={hostProductId:this.options.hostProductId,sourceProductId:e.originalProductId,savedProductId:e.newProductId,asCopy:e.asCopy===!0};this.currentProductId=e.newProductId;let r=this.cloneWait;this.cloneWait=void 0;try{await this.recordLink(n),r==null||r.resolve()}catch(i){r==null||r.reject(i)}}async recordLink(e){let n=this.ctx.config.customization;if(n)try{await n.record(e),this.unrecordedLink===e&&(this.unrecordedLink=void 0)}catch(r){throw this.unrecordedLink=e,new s("SAVE_FAILED","The design was saved but could not be linked to this shopper. Try saving again.",{retryable:!0,reason:"customization",cause:r})}}finishSave(e,n){let r=this.saveRun;r&&(this.saveRun=void 0,r.timer.stop(),e?r.result.reject(e):n&&r.result.resolve(n))}async answerExport(e){var h;let n=this.options.externalExport;if(!n)return;let r=new AbortController;this.exports.add(r);let i=!1,o=setTimeout(()=>{i=!0,r.abort()},(h=n.timeoutMs)!=null?h:G),d=new Promise((l,c)=>r.signal.addEventListener("abort",()=>c(new Error("aborted"))));try{let l=await Promise.race([n.run(e,r.signal),d]);O(this.element,{files:l})}catch(l){let c=i?"EXPORT_TIMEOUT":"EXPORT_FAILED";this.ctx.log("export failed",{code:c,cause:String(l)}),this.replyExportFailure(c)}finally{clearTimeout(o),this.exports.delete(r)}}replyExportFailure(e){let n={files:[],error:{code:e}};this.element.dispatchEvent(new CustomEvent(B,{bubbles:!1,composed:!1,detail:n}))}assertUsable(){if(this.destroyed)throw g()}};function L(t,e){t.setAttribute("refresh-token",e.refreshToken),t.setAttribute("bearer-token",e.token)}var x=class{constructor(e){this.config=e;this.instances=[];this.log=ee(e.debug),this.context={config:e,log:this.log,refreshLicense:()=>this.refreshLicense().catch(()=>{}),onDestroyed:n=>{let r=this.instances.indexOf(n);r>=0&&this.instances.splice(r,1)}}}get current(){return this.instances[this.instances.length-1]}load(){return oe(this.config.environment,this.config.version)}async mount(e){let n=Q(e);await this.load();let r=await E.mount(this.context,n);return this.instances.push(r),r}open(){this.requireCurrent().open()}save(){let e=this.current;return e?e.save():Promise.reject(this.notMounted())}refreshSession(){let e=this.current;return e?e.refreshSession():Promise.reject(this.notMounted())}refreshLicense(){return this.licenseRefresh||(this.licenseRefresh=(async()=>{try{let e=await this.config.session.refreshLicense(),n=e==null?void 0:e.licenseToken;if(typeof n!="string"||n==="")throw new s("SESSION_FAILED","session.refreshLicense() must resolve { licenseToken }.");for(let r of this.instances)r.applyLicense(n);this.log("license refreshed")}catch(e){throw this.log("license refresh failed",{cause:String(e)}),e instanceof s?e:new s("SESSION_FAILED","A fresh license token could not be fetched.",{retryable:!0,cause:e})}finally{this.licenseRefresh=void 0}})()),this.licenseRefresh}logout(){for(let e of this.instances)e.logout();this.log("logged out")}destroy(){for(let e of[...this.instances])e.destroy()}requireCurrent(){let e=this.current;if(!e)throw this.notMounted();return e}notMounted(){return a("No builder is mounted. Call mount() first.")}};function Ee(t){let e=J(t);return ie(e.environment),new x(e)}var y="tdn_resume",ye="tdn-draft-index";function Te(){let t=new URL(window.location.href);return t.searchParams.set(y,"1"),t.pathname+t.search+t.hash}function be(t){return/^\/(?![/\\])/.test(t)}function Se(){if(typeof window=="undefined")return!1;let t=new URL(window.location.href);return t.searchParams.get(y)!=="1"?!1:(t.searchParams.delete(y),window.history.replaceState(window.history.state,"",t.pathname+t.search+t.hash),!0)}function we(t){var e;if(typeof localStorage=="undefined")return!1;try{let n=JSON.parse((e=localStorage.getItem(ye))!=null?e:"{}");return Object.values(n).some(r=>(r==null?void 0:r.productId)===t)}catch{return!1}}export{s as BuilderError,y as RESUME_PARAM,Se as consumeResumeMarker,Ee as createBuilder,we as hasDraft,be as isSafeReturnPath,Te as loginReturnPath};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../libs/builder-host/src/lib/errors.ts", "../../../libs/builder-host/src/lib/config.ts", "../../../libs/builder-host/src/lib/debug.ts", "../../../libs/builder-host/src/lib/loader.ts", "../../../libs/builder-host/src/lib/instance.ts", "../../../libs/builder-host/src/lib/builder.ts", "../../../libs/builder-host/src/lib/resume.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n DesignSaveFailedDetail,\n DesignSaveFailureReason,\n LicenseErrorCode,\n} from '@three-dn/constants';\n\nexport type BuilderErrorCode =\n | 'CONFIG_INVALID'\n | 'NOT_IN_BROWSER'\n | 'LOAD_FAILED'\n | 'VERSION_CONFLICT'\n | 'SESSION_FAILED'\n | 'LICENSE_ERROR'\n | 'LOGIN_CANCELLED'\n | 'SAVE_FAILED'\n | 'SAVE_TIMEOUT'\n | 'EXPORT_FAILED'\n | 'EXPORT_TIMEOUT'\n | 'DESTROYED';\n\n/** The builder's own reason, plus `customization` when only the record failed. */\nexport type BuilderErrorReason =\n | DesignSaveFailureReason\n | LicenseErrorCode\n | 'customization';\n\nexport interface BuilderErrorOptions {\n retryable?: boolean;\n fatal?: boolean;\n reason?: BuilderErrorReason;\n cause?: unknown;\n}\n\n/**\n * Every failure the SDK reports. Hosts switch on `code`; `message` is safe to\n * show or log and never contains a token.\n */\nexport class BuilderError extends Error {\n readonly code: BuilderErrorCode;\n readonly retryable: boolean;\n /** The mount cannot continue until the host refreshes or recreates it. */\n readonly fatal: boolean;\n readonly reason?: BuilderErrorReason;\n readonly cause?: unknown;\n\n constructor(\n code: BuilderErrorCode,\n message: string,\n options: BuilderErrorOptions = {}\n ) {\n super(message);\n this.name = 'BuilderError';\n this.code = code;\n this.retryable = options.retryable ?? false;\n this.fatal = options.fatal ?? false;\n this.reason = options.reason;\n this.cause = options.cause;\n Object.setPrototypeOf(this, BuilderError.prototype);\n }\n}\n\nconst SAVE_FAILURE_MESSAGES: Record<DesignSaveFailureReason, string> = {\n conflict: 'Someone else saved this design first.',\n forbidden: 'This shopper may not save this design.',\n unauthorized: 'The shopper could not be signed in to save.',\n 'login-cancelled':\n 'The design was not saved because the shopper did not log in.',\n 'installation-inactive': \"This site's 3DN installation is not active.\",\n license: 'This page is not licensed to use the 3DN builder.',\n error: 'The design could not be saved. Check the connection and try again.',\n};\n\nexport function saveFailedError(detail: DesignSaveFailedDetail): BuilderError {\n const message =\n SAVE_FAILURE_MESSAGES[detail.reason] ?? SAVE_FAILURE_MESSAGES.error;\n if (detail.reason === 'login-cancelled') {\n return new BuilderError('LOGIN_CANCELLED', message, {\n retryable: true,\n reason: detail.reason,\n cause: detail,\n });\n }\n return new BuilderError('SAVE_FAILED', message, {\n retryable: detail.retryable,\n reason: detail.reason,\n cause: detail,\n });\n}\n\nexport function licenseError(code: LicenseErrorCode): BuilderError {\n const message =\n code === 'ORIGIN_NOT_ALLOWED'\n ? 'This website is not on the 3DN license. Add its hostname to the license.'\n : 'The 3DN license token was rejected. The license may be suspended, revoked or expired.';\n return new BuilderError('LICENSE_ERROR', message, {\n fatal: true,\n reason: code,\n cause: { code },\n });\n}\n\nexport function destroyedError(): BuilderError {\n return new BuilderError('DESTROYED', 'The builder was destroyed.');\n}\n\nexport function configError(message: string): BuilderError {\n return new BuilderError('CONFIG_INVALID', message, { fatal: true });\n}\n", "import type {\n BuilderStatusDetail,\n CloseRequestedDetail,\n ExportFile,\n ExportRequestedDetail,\n LoginRequiredAction,\n} from '@three-dn/constants';\nimport { BuilderError, configError } from './errors';\n\nexport type BuilderEnvironment = 'production' | 'staging';\nexport type BuilderVersion = 'latest' | `${number}.${number}.${number}`;\nexport type BuilderMode = 'designer' | 'viewer';\n\n/** What the host's session endpoint returns: site access plus the product to open. */\nexport interface BuilderSession {\n licenseToken: string;\n /** The 3DN product to open: the shopper's clone, else the global product. */\n productId: string;\n /** Required when the product to open is private. */\n viewToken?: string;\n /** Optional fast path: shopper tokens for a shopper the host backend verified. */\n token?: string;\n refreshToken?: string;\n}\n\n/** Shopper identity, from the host's login-token endpoint. */\nexport interface ShopperTokens {\n token: string;\n refreshToken: string;\n}\n\nexport type LoginOutcome = 'logged-in' | 'redirected' | 'cancelled';\n\n/** What the host stores when a save created the shopper's clone. */\nexport interface CustomizationLink {\n hostProductId: string;\n sourceProductId: string;\n savedProductId: string;\n /**\n * The shopper asked for a copy (\"Save as copy\", \"keep both\") and keeps the\n * original too. `false` when Save had to fork a product they don't own: the\n * copy replaces the original.\n */\n asCopy: boolean;\n}\n\n/** One successful save, however it started: `save()` or the builder's own Save button. */\nexport interface SavedDesignEvent {\n hostProductId: string;\n /** The saved 3DN product: the shopper's clone after a first save. */\n productId: string;\n version: number;\n}\n\nexport interface BuilderHostConfig {\n /** Default `'production'`. */\n environment?: BuilderEnvironment;\n /** `'latest'` or an exact builder release. Default `'latest'`. */\n version?: BuilderVersion;\n /** Default 45 000 ms. */\n saveTimeoutMs?: number;\n session: {\n get(request: { hostProductId: string }): Promise<BuilderSession>;\n refreshLicense(): Promise<Pick<BuilderSession, 'licenseToken'>>;\n };\n login?: {\n /** Answers `tdn-login-required`. Resolve `null` when the endpoint answers 401. */\n getTokens(): Promise<ShopperTokens | null>;\n /** The host's login UX for the action the builder gated. */\n onRequired(action: LoginRequiredAction): Promise<LoginOutcome>;\n /** `getTokens()` failed (5xx, network). Default: `'cancel'`. */\n onMintFailed?(): Promise<'retry' | 'cancel'>;\n };\n customization?: {\n record(link: CustomizationLink): Promise<void>;\n };\n /**\n * Every successful save, including ones the shopper starts from the builder's\n * own Save button. Fires after any clone mapping has been recorded; never\n * when recording failed.\n */\n onSaved?(saved: SavedDesignEvent): void;\n onLicenseError?(error: BuilderError): void;\n onCloseRequested?(detail: CloseRequestedDetail): void;\n onStatusChange?(detail: BuilderStatusDetail): void;\n /** Log lifecycle steps and events to the console. Tokens are always redacted. */\n debug?: boolean;\n}\n\nexport interface ExternalExportOptions {\n run(\n request: ExportRequestedDetail,\n signal: AbortSignal\n ): Promise<ExportFile[]>;\n /** Default 120 000 ms. */\n timeoutMs?: number;\n}\n\nexport interface MountOptions {\n target: HTMLElement;\n /** The host's own product reference, passed to `session.get()`. Never a 3DN id. */\n hostProductId: string;\n /** Default `'designer'`. `'viewer'` is the display-only mode. */\n mode?: BuilderMode;\n designId?: string;\n /** Opt-in, GLB products only. Off unless passed. */\n externalExport?: ExternalExportOptions;\n /**\n * The host page's observability context, a JSON object string the builder\n * reads as `observability-context`: which host app, tenant, site and user\n * its own Sentry events belong to, and the page's trace to join. Set before\n * the element connects; update it with `setObservabilityContext()`. The\n * builder drops keys it does not allow and ignores an invalid value.\n */\n observabilityContext?: string;\n}\n\nexport interface ResolvedConfig extends BuilderHostConfig {\n environment: BuilderEnvironment;\n version: BuilderVersion;\n saveTimeoutMs: number;\n}\n\nexport const DEFAULT_SAVE_TIMEOUT_MS = 45_000;\nexport const DEFAULT_EXPORT_TIMEOUT_MS = 120_000;\n\nconst CONFIG_KEYS = new Set([\n 'environment',\n 'version',\n 'saveTimeoutMs',\n 'session',\n 'login',\n 'customization',\n 'onSaved',\n 'onLicenseError',\n 'onCloseRequested',\n 'onStatusChange',\n 'debug',\n]);\n\nconst MOUNT_KEYS = new Set([\n 'target',\n 'hostProductId',\n 'mode',\n 'designId',\n 'externalExport',\n 'observabilityContext',\n]);\n\nconst VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\nconst INSTALLATION_KEY_PATTERN = /^tdn_inst_/;\n\nexport function isExactVersion(value: string): boolean {\n return VERSION_PATTERN.test(value);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isFunction(value: unknown): value is (...args: never[]) => unknown {\n return typeof value === 'function';\n}\n\nfunction isPositiveNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n/** Walks the config looking for anything that looks like an installation key. */\nfunction containsSecret(value: unknown, depth = 0): boolean {\n if (typeof value === 'string') return INSTALLATION_KEY_PATTERN.test(value);\n if (!isRecord(value) || depth > 3) return false;\n return Object.values(value).some((v) => containsSecret(v, depth + 1));\n}\n\nfunction rejectUnknownKeys(\n value: Record<string, unknown>,\n allowed: Set<string>,\n where: string\n): void {\n for (const key of Object.keys(value)) {\n if (allowed.has(key)) continue;\n if (/url|src|host|origin/i.test(key)) {\n throw configError(\n `${where}: \"${key}\" is not allowed. The SDK decides where the builder loads from; choose an environment and a version instead.`\n );\n }\n throw configError(`${where}: unknown option \"${key}\".`);\n }\n}\n\nexport function resolveConfig(config: unknown): ResolvedConfig {\n if (!isRecord(config)) {\n throw configError('createBuilder() needs a configuration object.');\n }\n if (containsSecret(config)) {\n throw configError(\n 'createBuilder(): an installation key was passed to the browser. Installation keys stay on your server; pass a session callback instead.'\n );\n }\n rejectUnknownKeys(config, CONFIG_KEYS, 'createBuilder()');\n\n const environment = config['environment'] ?? 'production';\n if (environment !== 'production' && environment !== 'staging') {\n throw configError(\n `createBuilder(): environment must be \"production\" or \"staging\", got ${JSON.stringify(\n environment\n )}.`\n );\n }\n\n const version = config['version'] ?? 'latest';\n if (\n typeof version !== 'string' ||\n (version !== 'latest' && !isExactVersion(version))\n ) {\n throw configError(\n `createBuilder(): version must be \"latest\" or an exact release such as \"1.2.3\", got ${JSON.stringify(\n version\n )}.`\n );\n }\n\n const saveTimeoutMs = config['saveTimeoutMs'] ?? DEFAULT_SAVE_TIMEOUT_MS;\n if (!isPositiveNumber(saveTimeoutMs)) {\n throw configError(\n 'createBuilder(): saveTimeoutMs must be a positive number of milliseconds.'\n );\n }\n\n const session = config['session'];\n if (\n !isRecord(session) ||\n !isFunction(session['get']) ||\n !isFunction(session['refreshLicense'])\n ) {\n throw configError(\n 'createBuilder(): session.get() and session.refreshLicense() are required. They call your session endpoint.'\n );\n }\n\n const login = config['login'];\n if (login !== undefined) {\n if (\n !isRecord(login) ||\n !isFunction(login['getTokens']) ||\n !isFunction(login['onRequired'])\n ) {\n throw configError(\n 'createBuilder(): login needs getTokens() (your login-token endpoint) and onRequired() (your login UI).'\n );\n }\n if (\n login['onMintFailed'] !== undefined &&\n !isFunction(login['onMintFailed'])\n ) {\n throw configError(\n 'createBuilder(): login.onMintFailed must be a function.'\n );\n }\n }\n\n const customization = config['customization'];\n if (\n customization !== undefined &&\n (!isRecord(customization) || !isFunction(customization['record']))\n ) {\n throw configError(\n 'createBuilder(): customization.record() is required when customization is set. It calls your customization endpoint.'\n );\n }\n\n for (const key of [\n 'onSaved',\n 'onLicenseError',\n 'onCloseRequested',\n 'onStatusChange',\n ] as const) {\n if (config[key] !== undefined && !isFunction(config[key])) {\n throw configError(`createBuilder(): ${key} must be a function.`);\n }\n }\n\n return {\n ...(config as unknown as BuilderHostConfig),\n environment,\n version: version as BuilderVersion,\n saveTimeoutMs,\n };\n}\n\nexport function validateMountOptions(options: unknown): MountOptions {\n if (!isRecord(options)) {\n throw configError('mount() needs an options object.');\n }\n rejectUnknownKeys(options, MOUNT_KEYS, 'mount()');\n\n const target = options['target'];\n if (typeof HTMLElement === 'undefined' || !(target instanceof HTMLElement)) {\n throw configError('mount(): target must be an HTMLElement on the page.');\n }\n const hostProductId = options['hostProductId'];\n if (typeof hostProductId !== 'string' || hostProductId.trim() === '') {\n throw configError(\n 'mount(): hostProductId must be your own, non-empty product reference.'\n );\n }\n const mode = options['mode'] ?? 'designer';\n if (mode !== 'designer' && mode !== 'viewer') {\n throw configError(\n `mount(): mode must be \"designer\" or \"viewer\", got ${JSON.stringify(\n mode\n )}.`\n );\n }\n const designId = options['designId'];\n if (\n designId !== undefined &&\n (typeof designId !== 'string' || designId === '')\n ) {\n throw configError('mount(): designId must be a non-empty string.');\n }\n if (options['observabilityContext'] !== undefined) {\n validateObservabilityContext(options['observabilityContext'], 'mount()');\n }\n const externalExport = options['externalExport'];\n if (externalExport !== undefined) {\n if (!isRecord(externalExport) || !isFunction(externalExport['run'])) {\n throw configError(\n 'mount(): externalExport.run() is required when externalExport is set.'\n );\n }\n if (\n externalExport['timeoutMs'] !== undefined &&\n !isPositiveNumber(externalExport['timeoutMs'])\n ) {\n throw configError(\n 'mount(): externalExport.timeoutMs must be a positive number of milliseconds.'\n );\n }\n }\n return options as unknown as MountOptions;\n}\n\n/** A JSON object string: what the builder's `observability-context` accepts. */\nexport function validateObservabilityContext(\n value: unknown,\n where: string\n): string {\n let parsed: unknown;\n try {\n parsed = typeof value === 'string' ? JSON.parse(value) : undefined;\n } catch {\n parsed = undefined;\n }\n if (!isRecord(parsed) || Array.isArray(parsed)) {\n throw configError(\n `${where}: observabilityContext must be a JSON object string.`\n );\n }\n return value as string;\n}\n\n/** Checks the shape of a session response before anything reaches the element. */\nexport function validateSession(session: unknown): BuilderSession {\n const fail = (what: string) =>\n new BuilderError(\n 'SESSION_FAILED',\n `session.get() returned an invalid session: ${what}.`,\n {\n retryable: false,\n }\n );\n if (!isRecord(session)) throw fail('expected an object');\n if (\n typeof session['licenseToken'] !== 'string' ||\n session['licenseToken'] === ''\n ) {\n throw fail('licenseToken is missing');\n }\n if (typeof session['productId'] !== 'string' || session['productId'] === '') {\n throw fail('productId is missing');\n }\n if (\n session['viewToken'] != null &&\n typeof session['viewToken'] !== 'string'\n ) {\n throw fail('viewToken must be a string');\n }\n const hasToken = session['token'] != null;\n const hasRefresh = session['refreshToken'] != null;\n if (hasToken !== hasRefresh) {\n throw fail('token and refreshToken must be returned together');\n }\n if (\n hasToken &&\n (typeof session['token'] !== 'string' ||\n typeof session['refreshToken'] !== 'string')\n ) {\n throw fail('token and refreshToken must be strings');\n }\n return session as unknown as BuilderSession;\n}\n\nexport function validateShopperTokens(tokens: unknown): ShopperTokens | null {\n if (tokens === null) return null;\n if (\n !isRecord(tokens) ||\n typeof tokens['token'] !== 'string' ||\n tokens['token'] === '' ||\n typeof tokens['refreshToken'] !== 'string' ||\n tokens['refreshToken'] === ''\n ) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'login.getTokens() must resolve { token, refreshToken } or null.'\n );\n }\n return tokens as unknown as ShopperTokens;\n}\n", "const TOKEN_KEY = /token|secret|key|authorization/i;\n\n/** Copies a value for logging with every token-like field replaced. */\nexport function redact(value: unknown, depth = 0): unknown {\n if (value === null || typeof value !== 'object' || depth > 4) return value;\n if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value)) {\n out[key] =\n TOKEN_KEY.test(key) && v != null ? '[redacted]' : redact(v, depth + 1);\n }\n return out;\n}\n\nexport type DebugLog = (message: string, detail?: unknown) => void;\n\nexport function createDebugLog(enabled: boolean | undefined): DebugLog {\n if (!enabled) return () => undefined;\n return (message, detail) => {\n if (detail === undefined) console.debug(`[3DN] ${message}`);\n else console.debug(`[3DN] ${message}`, redact(detail));\n };\n}\n", "import type { BuilderEnvironment, BuilderVersion } from './config';\nimport { isExactVersion } from './config';\nimport { BuilderError, configError } from './errors';\n\nexport const BUILDER_TAG = 'threed-builder';\n\n/**\n * Where each environment's builder is served from. SDK-owned: hosts choose an\n * environment, never a location. Staging stays unset until its location is\n * confirmed; createBuilder() refuses `environment: 'staging'` until then.\n */\nconst BUNDLE_HOSTS: Record<BuilderEnvironment, string | undefined> = {\n production: 'https://3d-builder.3dn.app',\n staging: undefined,\n};\n\nconst DEFINE_TIMEOUT_MS = 30_000;\n\n/** Shared by every copy of the SDK on the page: one custom element registry. */\ninterface PageLoaderState {\n environment?: BuilderEnvironment;\n /** The exact version loaded, once known. */\n version?: string;\n loading?: Promise<void>;\n}\n\nconst STATE_KEY = Symbol.for('@3dn-builder/builder/loader');\n\nfunction pageState(): PageLoaderState {\n const holder = globalThis as unknown as Record<\n symbol,\n PageLoaderState | undefined\n >;\n let state = holder[STATE_KEY];\n if (!state) {\n state = {};\n holder[STATE_KEY] = state;\n }\n return state;\n}\n\nexport function isBrowser(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof document !== 'undefined' &&\n typeof customElements !== 'undefined'\n );\n}\n\nexport function assertEnvironmentAvailable(\n environment: BuilderEnvironment\n): void {\n if (!BUNDLE_HOSTS[environment]) {\n throw configError(\n `createBuilder(): the \"${environment}\" environment is not available in this SDK release.`\n );\n }\n}\n\nfunction loadFailed(message: string, cause?: unknown): BuilderError {\n return new BuilderError('LOAD_FAILED', message, { retryable: true, cause });\n}\n\nasync function resolveVersion(\n host: string,\n requested: BuilderVersion\n): Promise<string> {\n if (requested !== 'latest') return requested;\n let response: Response;\n try {\n response = await fetch(`${host}/version-info.json`, {\n cache: 'no-store',\n credentials: 'omit',\n });\n } catch (cause) {\n throw loadFailed('The 3DN builder could not be reached.', cause);\n }\n if (!response.ok) {\n throw loadFailed(\n `The 3DN builder release could not be resolved (HTTP ${response.status}).`\n );\n }\n const info = (await response.json().catch(() => null)) as {\n version?: unknown;\n } | null;\n const version = info?.version;\n if (typeof version !== 'string' || !isExactVersion(version)) {\n throw loadFailed('The 3DN builder release information was not valid.');\n }\n return version;\n}\n\nfunction injectScript(src: string): Promise<HTMLScriptElement> {\n return new Promise((resolve, reject) => {\n const script = document.createElement('script');\n script.type = 'module';\n script.src = src;\n script.crossOrigin = 'anonymous';\n script.onload = () => resolve(script);\n script.onerror = () => {\n script.remove();\n reject(loadFailed('The 3DN builder failed to load.'));\n };\n document.head.appendChild(script);\n });\n}\n\nfunction whenDefined(): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(\n () => reject(loadFailed('The 3DN builder loaded but did not start.')),\n DEFINE_TIMEOUT_MS\n );\n customElements.whenDefined(BUILDER_TAG).then(() => {\n clearTimeout(timer);\n resolve();\n }, reject);\n });\n}\n\nfunction assertCompatible(\n state: PageLoaderState,\n environment: BuilderEnvironment,\n requested: BuilderVersion\n): void {\n if (state.environment && state.environment !== environment) {\n throw new BuilderError(\n 'VERSION_CONFLICT',\n `The ${state.environment} builder already runs on this page; ${environment} cannot load beside it.`,\n { fatal: true }\n );\n }\n if (requested !== 'latest' && state.version && state.version !== requested) {\n throw new BuilderError(\n 'VERSION_CONFLICT',\n `Builder ${state.version} already runs on this page; ${requested} cannot load beside it.`,\n { fatal: true }\n );\n }\n}\n\n/**\n * Loads the builder once per page. A failed load leaves nothing behind, so the\n * next call tries again.\n */\nexport async function loadBuilder(\n environment: BuilderEnvironment,\n requested: BuilderVersion\n): Promise<void> {\n if (!isBrowser()) {\n throw new BuilderError(\n 'NOT_IN_BROWSER',\n 'The 3DN builder can only load in a browser.'\n );\n }\n const state = pageState();\n\n if (state.loading) {\n await state.loading;\n assertCompatible(state, environment, requested);\n return;\n }\n // Defined by something other than this SDK (an older embed, or the testing\n // fake): its version is unknown, so use it as it is.\n if (customElements.get(BUILDER_TAG)) return;\n\n const host = BUNDLE_HOSTS[environment];\n if (!host) {\n throw configError(\n `The \"${environment}\" environment is not available in this SDK release.`\n );\n }\n\n state.environment = environment;\n state.loading = (async () => {\n const version = await resolveVersion(host, requested);\n state.version = version;\n const script = await injectScript(`${host}/builder-${version}.js`);\n try {\n await whenDefined();\n } catch (error) {\n script.remove();\n throw error;\n }\n })();\n\n try {\n await state.loading;\n } catch (error) {\n state.loading = undefined;\n state.environment = undefined;\n state.version = undefined;\n throw error instanceof BuilderError\n ? error\n : loadFailed('The 3DN builder failed to load.', error);\n }\n assertCompatible(state, environment, requested);\n}\n", "import {\n THREED_BUILDER_CLOSE_REQUESTED_EVENT,\n THREED_BUILDER_DESIGN_SAVED_EVENT,\n THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,\n THREED_BUILDER_EXPORT_COMPLETE_EVENT,\n THREED_BUILDER_EXPORT_REQUESTED_EVENT,\n THREED_BUILDER_LICENSE_ERROR_EVENT,\n THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,\n THREED_BUILDER_LOGIN_REQUIRED_EVENT,\n THREED_BUILDER_PRODUCT_CLONED_EVENT,\n THREED_BUILDER_STATUS_EVENT,\n dispatchExportComplete,\n dispatchLoginCancelled,\n dispatchOpenThreedBuilder,\n dispatchSaveRequest,\n type BuilderStatusDetail,\n type CloseRequestedDetail,\n type DesignSaveFailedDetail,\n type DesignSavedDetail,\n type ExportRequestedDetail,\n type LicenseErrorDetail,\n type LoginRequiredAction,\n type LoginRequiredDetail,\n type ProductClonedDetail,\n} from '@three-dn/constants';\nimport {\n DEFAULT_EXPORT_TIMEOUT_MS,\n validateObservabilityContext,\n validateSession,\n validateShopperTokens,\n type BuilderSession,\n type CustomizationLink,\n type MountOptions,\n type ResolvedConfig,\n type ShopperTokens,\n} from './config';\nimport type { DebugLog } from './debug';\nimport {\n BuilderError,\n destroyedError,\n licenseError,\n saveFailedError,\n} from './errors';\nimport { BUILDER_TAG } from './loader';\n\n/** What `save()` resolves with. `productId` is always the one to put in the cart. */\nexport interface SavedDesign {\n productId: string;\n version: number;\n}\n\nexport interface InstanceContext {\n config: ResolvedConfig;\n log: DebugLog;\n /** Single-flight license refresh shared by every instance of a controller. */\n refreshLicense(): Promise<void>;\n onDestroyed(instance: BuilderInstance): void;\n}\n\ninterface Deferred<T> {\n promise: Promise<T>;\n resolve(value: T): void;\n reject(error: unknown): void;\n}\n\nfunction deferred<T>(): Deferred<T> {\n let resolve!: (value: T) => void;\n let reject!: (error: unknown) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n // A rejection nobody awaits yet must not surface as unhandled.\n promise.catch(() => undefined);\n return { promise, resolve, reject };\n}\n\n/** A timer that stops counting while a login round-trip is pending. */\nclass PausableTimer {\n private handle?: ReturnType<typeof setTimeout>;\n private startedAt = 0;\n private remaining: number;\n\n constructor(ms: number, private readonly onExpire: () => void) {\n this.remaining = ms;\n }\n\n start(): void {\n if (this.handle !== undefined) return;\n this.startedAt = Date.now();\n this.handle = setTimeout(this.onExpire, this.remaining);\n }\n\n pause(): void {\n if (this.handle === undefined) return;\n clearTimeout(this.handle);\n this.handle = undefined;\n this.remaining = Math.max(\n 0,\n this.remaining - (Date.now() - this.startedAt)\n );\n }\n\n stop(): void {\n if (this.handle !== undefined) clearTimeout(this.handle);\n this.handle = undefined;\n }\n}\n\ninterface SaveRun {\n result: Deferred<SavedDesign>;\n timer: PausableTimer;\n}\n\ninterface ExportReply {\n files: { url: string; format: string; surface: string }[];\n error?: { code: 'EXPORT_FAILED' | 'EXPORT_TIMEOUT' };\n}\n\n/** One mounted `<threed-builder>`. */\nexport class BuilderInstance {\n readonly element: HTMLElement;\n private currentProductId: string;\n /** Bumped by logout() and destroy(): async results from an older epoch are dropped. */\n private epoch = 0;\n private destroyed = false;\n private fatal?: BuilderError;\n private loginPending = false;\n private saveRun?: SaveRun;\n /** A clone mapping whose record() failed; retried before the next save resolves. */\n private unrecordedLink?: CustomizationLink;\n /** Set between a save that changed the product id and its tdn-product-cloned. */\n private cloneWait?: Deferred<void>;\n private readonly exports = new Set<AbortController>();\n private readonly listeners: [string, EventListener][] = [];\n\n private constructor(\n private readonly ctx: InstanceContext,\n private readonly options: MountOptions,\n session: BuilderSession\n ) {\n this.currentProductId = session.productId;\n this.element = this.createElement(session);\n }\n\n /** Builds the element in the documented order and inserts it last. */\n static async mount(\n ctx: InstanceContext,\n options: MountOptions\n ): Promise<BuilderInstance> {\n let raw: unknown;\n try {\n raw = await ctx.config.session.get({\n hostProductId: options.hostProductId,\n });\n } catch (cause) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'The builder session could not be loaded.',\n {\n retryable: true,\n cause,\n }\n );\n }\n const session = validateSession(raw);\n const instance = new BuilderInstance(ctx, options, session);\n options.target.appendChild(instance.element);\n ctx.log('mounted', {\n hostProductId: options.hostProductId,\n productId: session.productId,\n });\n return instance;\n }\n\n /** The 3DN product the builder is editing now: the clone, after a first save. */\n get productId(): string {\n return this.currentProductId;\n }\n\n get hostProductId(): string {\n return this.options.hostProductId;\n }\n\n private createElement(session: BuilderSession): HTMLElement {\n const el = document.createElement(BUILDER_TAG);\n this.attachListeners(el);\n el.setAttribute('product', session.productId);\n el.setAttribute('license-token', session.licenseToken);\n if (session.viewToken) el.setAttribute('view-token', session.viewToken);\n if (session.token && session.refreshToken) {\n setShopperTokens(el, {\n token: session.token,\n refreshToken: session.refreshToken,\n });\n }\n if (this.options.designId)\n el.setAttribute('design-id', this.options.designId);\n if (this.options.mode === 'viewer') el.setAttribute('display-only', 'true');\n if (this.options.externalExport) el.setAttribute('external-export', 'true');\n if (this.options.observabilityContext) {\n el.setAttribute(\n 'observability-context',\n this.options.observabilityContext\n );\n }\n el.style.display = 'block';\n el.style.width = '100%';\n el.style.height = '100%';\n return el;\n }\n\n private attachListeners(el: HTMLElement): void {\n const on = <T>(name: string, handler: (detail: T) => void) => {\n const listener: EventListener = (event) => {\n // Only this builder's own events: never a neighbour's, never a bubbled copy.\n if (event.target !== el || this.destroyed) return;\n this.ctx.log(name, (event as CustomEvent<T>).detail);\n handler((event as CustomEvent<T>).detail);\n };\n el.addEventListener(name, listener);\n this.listeners.push([name, listener]);\n };\n\n on<LoginRequiredDetail>(\n THREED_BUILDER_LOGIN_REQUIRED_EVENT,\n (d) => void this.answerLogin(d?.action ?? 'save')\n );\n on(\n THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,\n () => void this.ctx.refreshLicense()\n );\n on<LicenseErrorDetail>(THREED_BUILDER_LICENSE_ERROR_EVENT, (d) =>\n this.onLicenseError(d)\n );\n on<DesignSavedDetail>(\n THREED_BUILDER_DESIGN_SAVED_EVENT,\n (d) => void this.onDesignSaved(d)\n );\n on<DesignSaveFailedDetail>(THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT, (d) =>\n this.finishSave(saveFailedError(d))\n );\n on<ProductClonedDetail>(\n THREED_BUILDER_PRODUCT_CLONED_EVENT,\n (d) => void this.onProductCloned(d)\n );\n on<CloseRequestedDetail>(THREED_BUILDER_CLOSE_REQUESTED_EVENT, (d) =>\n this.ctx.config.onCloseRequested?.(d)\n );\n on<BuilderStatusDetail>(THREED_BUILDER_STATUS_EVENT, (d) =>\n this.ctx.config.onStatusChange?.(d)\n );\n if (this.options.externalExport) {\n on<ExportRequestedDetail>(\n THREED_BUILDER_EXPORT_REQUESTED_EVENT,\n (d) => void this.answerExport(d)\n );\n }\n }\n\n // \u2500\u2500 Host-facing methods \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Shows the builder UI, for a host that mounted it before showing it. */\n open(): void {\n this.assertUsable();\n dispatchOpenThreedBuilder(this.element);\n }\n\n /**\n * Saves the design. Resolves once it is saved and any clone mapping it\n * required has been recorded; concurrent calls share one save.\n */\n save(): Promise<SavedDesign> {\n if (this.destroyed) return Promise.reject(destroyedError());\n if (this.fatal) return Promise.reject(this.fatal);\n if (this.saveRun) return this.saveRun.result.promise;\n\n const run: SaveRun = {\n result: deferred<SavedDesign>(),\n timer: new PausableTimer(this.ctx.config.saveTimeoutMs, () =>\n this.finishSave(\n new BuilderError(\n 'SAVE_TIMEOUT',\n 'Saving the design took too long. Try again.',\n {\n retryable: true,\n }\n )\n )\n ),\n };\n this.saveRun = run;\n if (!this.loginPending) run.timer.start();\n dispatchSaveRequest(this.element);\n return run.result.promise;\n }\n\n /** Calls session.get() again and applies what it returns. */\n async refreshSession(): Promise<void> {\n this.assertUsable();\n const epoch = this.epoch;\n let raw: unknown;\n try {\n raw = await this.ctx.config.session.get({\n hostProductId: this.options.hostProductId,\n });\n } catch (cause) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'The builder session could not be refreshed.',\n {\n retryable: true,\n cause,\n }\n );\n }\n const session = validateSession(raw);\n if (epoch !== this.epoch || this.destroyed) return;\n this.applyLicense(session.licenseToken);\n if (session.viewToken)\n this.element.setAttribute('view-token', session.viewToken);\n if (session.token && session.refreshToken) {\n setShopperTokens(this.element, {\n token: session.token,\n refreshToken: session.refreshToken,\n });\n }\n this.fatal = undefined;\n }\n\n /** Removes the shopper layer. The site layer stays and the builder revokes the session. */\n logout(): void {\n if (this.destroyed) return;\n this.epoch++;\n this.element.removeAttribute('bearer-token');\n this.element.removeAttribute('refresh-token');\n if (this.loginPending) {\n this.loginPending = false;\n this.finishSave(\n new BuilderError(\n 'LOGIN_CANCELLED',\n 'The shopper logged out before the design was saved.',\n {\n retryable: true,\n }\n )\n );\n }\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.epoch++;\n this.finishSave(destroyedError());\n this.cloneWait?.reject(destroyedError());\n this.cloneWait = undefined;\n for (const controller of this.exports) controller.abort();\n this.destroyed = true;\n for (const [name, listener] of this.listeners) {\n this.element.removeEventListener(name, listener);\n }\n this.listeners.length = 0;\n this.element.remove();\n this.ctx.onDestroyed(this);\n this.ctx.log('destroyed');\n }\n\n /**\n * Replaces the host page's observability context, for example once the\n * page's own Sentry has started and a trace exists to join.\n */\n setObservabilityContext(context: string): void {\n this.assertUsable();\n this.element.setAttribute(\n 'observability-context',\n validateObservabilityContext(context, 'setObservabilityContext()')\n );\n }\n\n /** Used by the controller to hand every instance the shared fresh license token. */\n applyLicense(licenseToken: string): void {\n if (this.destroyed) return;\n this.element.setAttribute('license-token', licenseToken);\n }\n\n get isDestroyed(): boolean {\n return this.destroyed;\n }\n\n // \u2500\u2500 Login round-trip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async answerLogin(action: LoginRequiredAction): Promise<void> {\n if (this.loginPending) return;\n const login = this.ctx.config.login;\n if (!login) {\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n const epoch = this.epoch;\n const stale = () => epoch !== this.epoch || this.destroyed;\n this.setLoginPending(true);\n let redirected = false;\n try {\n for (;;) {\n let tokens: ShopperTokens | null;\n try {\n tokens = validateShopperTokens(await login.getTokens());\n } catch (cause) {\n if (stale()) return;\n this.ctx.log('login.getTokens() failed', { cause: String(cause) });\n const choice = login.onMintFailed\n ? await login.onMintFailed()\n : 'cancel';\n if (stale()) return;\n if (choice === 'retry') continue;\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n if (stale()) return;\n if (tokens) {\n setShopperTokens(this.element, tokens); // resumes the pending action\n return;\n }\n const outcome = await login.onRequired(action);\n if (stale()) return;\n if (outcome === 'logged-in') continue;\n if (outcome === 'redirected') {\n // The page is navigating to the host's login. Never cancel: the\n // builder resumes the save after the shopper comes back.\n redirected = true;\n return;\n }\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n } finally {\n if (!redirected && epoch === this.epoch) this.setLoginPending(false);\n }\n }\n\n private setLoginPending(pending: boolean): void {\n this.loginPending = pending;\n if (!this.saveRun) return;\n if (pending) this.saveRun.timer.pause();\n else this.saveRun.timer.start();\n }\n\n // \u2500\u2500 License \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private onLicenseError(detail: LicenseErrorDetail): void {\n const error = licenseError(detail.code);\n this.fatal = error;\n this.finishSave(error);\n this.ctx.config.onLicenseError?.(error);\n }\n\n // \u2500\u2500 Save and clone recording \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async onDesignSaved(detail: DesignSavedDetail): Promise<void> {\n if (detail.productId !== this.currentProductId && !this.cloneWait) {\n // The save created a clone; tdn-product-cloned follows the thumbnail upload.\n this.cloneWait = deferred<void>();\n }\n // Saves from the builder's own Save button come through here too, with no\n // save() pending: they wait for the clone and its record all the same, so\n // onSaved only ever reports a save the host can act on.\n try {\n if (this.cloneWait) await this.cloneWait.promise;\n if (this.unrecordedLink) await this.recordLink(this.unrecordedLink);\n } catch (error) {\n this.finishSave(\n error instanceof BuilderError\n ? error\n : new BuilderError(\n 'SAVE_FAILED',\n 'The design was saved but could not be linked to this shopper.',\n {\n retryable: true,\n reason: 'customization',\n cause: error,\n }\n )\n );\n return;\n }\n if (this.destroyed) return;\n const saved: SavedDesign = {\n productId: detail.productId,\n version: detail.version,\n };\n this.notifySaved(saved);\n this.finishSave(undefined, saved);\n }\n\n private notifySaved(saved: SavedDesign): void {\n try {\n this.ctx.config.onSaved?.({\n hostProductId: this.options.hostProductId,\n ...saved,\n });\n } catch (cause) {\n this.ctx.log('onSaved threw', { cause: String(cause) });\n }\n }\n\n private async onProductCloned(detail: ProductClonedDetail): Promise<void> {\n const link: CustomizationLink = {\n hostProductId: this.options.hostProductId,\n sourceProductId: detail.originalProductId,\n savedProductId: detail.newProductId,\n asCopy: detail.asCopy === true,\n };\n this.currentProductId = detail.newProductId; // later saves update the clone\n const wait = this.cloneWait;\n this.cloneWait = undefined;\n try {\n await this.recordLink(link);\n wait?.resolve();\n } catch (error) {\n wait?.reject(error);\n }\n }\n\n private async recordLink(link: CustomizationLink): Promise<void> {\n const customization = this.ctx.config.customization;\n if (!customization) return;\n try {\n await customization.record(link);\n if (this.unrecordedLink === link) this.unrecordedLink = undefined;\n } catch (cause) {\n this.unrecordedLink = link;\n throw new BuilderError(\n 'SAVE_FAILED',\n 'The design was saved but could not be linked to this shopper. Try saving again.',\n { retryable: true, reason: 'customization', cause }\n );\n }\n }\n\n private finishSave(error?: BuilderError, saved?: SavedDesign): void {\n const run = this.saveRun;\n if (!run) return;\n this.saveRun = undefined;\n run.timer.stop();\n if (error) run.result.reject(error);\n else if (saved) run.result.resolve(saved);\n }\n\n // \u2500\u2500 External export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async answerExport(request: ExportRequestedDetail): Promise<void> {\n const external = this.options.externalExport;\n if (!external) return;\n const controller = new AbortController();\n this.exports.add(controller);\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, external.timeoutMs ?? DEFAULT_EXPORT_TIMEOUT_MS);\n\n const abort = new Promise<never>((_, reject) =>\n controller.signal.addEventListener('abort', () =>\n reject(new Error('aborted'))\n )\n );\n try {\n const files = await Promise.race([\n external.run(request, controller.signal),\n abort,\n ]);\n dispatchExportComplete(this.element, { files });\n } catch (cause) {\n const code = timedOut ? 'EXPORT_TIMEOUT' : 'EXPORT_FAILED';\n this.ctx.log('export failed', { code, cause: String(cause) });\n this.replyExportFailure(code);\n } finally {\n clearTimeout(timer);\n this.exports.delete(controller);\n }\n }\n\n /**\n * The builder has no error field on tdn-export-complete yet: it ignores\n * `error`, and the empty file list ends its wait.\n */\n private replyExportFailure(code: 'EXPORT_FAILED' | 'EXPORT_TIMEOUT'): void {\n const detail: ExportReply = { files: [], error: { code } };\n this.element.dispatchEvent(\n new CustomEvent<ExportReply>(THREED_BUILDER_EXPORT_COMPLETE_EVENT, {\n bubbles: false,\n composed: false,\n detail,\n })\n );\n }\n\n private assertUsable(): void {\n if (this.destroyed) throw destroyedError();\n }\n}\n\n/** Refresh first, then bearer: setting the bearer token resumes a pending action. */\nfunction setShopperTokens(el: HTMLElement, tokens: ShopperTokens): void {\n el.setAttribute('refresh-token', tokens.refreshToken);\n el.setAttribute('bearer-token', tokens.token);\n}\n", "import {\n resolveConfig,\n validateMountOptions,\n type ResolvedConfig,\n} from './config';\nimport { createDebugLog, type DebugLog } from './debug';\nimport { BuilderError, configError } from './errors';\nimport {\n BuilderInstance,\n type InstanceContext,\n type SavedDesign,\n} from './instance';\nimport { assertEnvironmentAvailable, loadBuilder } from './loader';\nimport type { BuilderHostConfig, MountOptions } from './config';\n\nexport interface Builder {\n /** Optional preload: loads the builder code once per page. */\n load(): Promise<void>;\n /** Creates and inserts a builder. Later calls add more instances. */\n mount(options: MountOptions): Promise<BuilderInstance>;\n /** The most recently mounted builder that is still alive. */\n readonly current: BuilderInstance | undefined;\n /** Shows the current builder's UI. */\n open(): void;\n /** Saves the current builder's design; see BuilderInstance.save(). */\n save(): Promise<SavedDesign>;\n /** Calls session.get() again for the current builder. */\n refreshSession(): Promise<void>;\n /** Fetches a fresh license token once and applies it to every builder. */\n refreshLicense(): Promise<void>;\n /** Removes the shopper layer from every builder. */\n logout(): void;\n /** Destroys every builder this controller mounted. */\n destroy(): void;\n}\n\nclass BuilderController implements Builder {\n private readonly instances: BuilderInstance[] = [];\n private licenseRefresh?: Promise<void>;\n private readonly log: DebugLog;\n private readonly context: InstanceContext;\n\n constructor(private readonly config: ResolvedConfig) {\n this.log = createDebugLog(config.debug);\n this.context = {\n config,\n log: this.log,\n refreshLicense: () => this.refreshLicense().catch(() => undefined),\n onDestroyed: (instance) => {\n const index = this.instances.indexOf(instance);\n if (index >= 0) this.instances.splice(index, 1);\n },\n };\n }\n\n get current(): BuilderInstance | undefined {\n return this.instances[this.instances.length - 1];\n }\n\n load(): Promise<void> {\n return loadBuilder(this.config.environment, this.config.version);\n }\n\n async mount(options: MountOptions): Promise<BuilderInstance> {\n const valid = validateMountOptions(options);\n await this.load();\n const instance = await BuilderInstance.mount(this.context, valid);\n this.instances.push(instance);\n return instance;\n }\n\n open(): void {\n this.requireCurrent().open();\n }\n\n save(): Promise<SavedDesign> {\n const current = this.current;\n return current ? current.save() : Promise.reject(this.notMounted());\n }\n\n refreshSession(): Promise<void> {\n const current = this.current;\n return current\n ? current.refreshSession()\n : Promise.reject(this.notMounted());\n }\n\n refreshLicense(): Promise<void> {\n if (!this.licenseRefresh) {\n this.licenseRefresh = (async () => {\n try {\n const result = await this.config.session.refreshLicense();\n const token = result?.licenseToken;\n if (typeof token !== 'string' || token === '') {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'session.refreshLicense() must resolve { licenseToken }.'\n );\n }\n for (const instance of this.instances) instance.applyLicense(token);\n this.log('license refreshed');\n } catch (cause) {\n this.log('license refresh failed', { cause: String(cause) });\n throw cause instanceof BuilderError\n ? cause\n : new BuilderError(\n 'SESSION_FAILED',\n 'A fresh license token could not be fetched.',\n {\n retryable: true,\n cause,\n }\n );\n } finally {\n this.licenseRefresh = undefined;\n }\n })();\n }\n return this.licenseRefresh;\n }\n\n logout(): void {\n for (const instance of this.instances) instance.logout();\n this.log('logged out');\n }\n\n destroy(): void {\n for (const instance of [...this.instances]) instance.destroy();\n }\n\n private requireCurrent(): BuilderInstance {\n const current = this.current;\n if (!current) throw this.notMounted();\n return current;\n }\n\n private notMounted(): BuilderError {\n return configError('No builder is mounted. Call mount() first.');\n }\n}\n\n/**\n * Records the host's configuration and callbacks. Touches nothing on the page\n * until load() or mount(), so it is safe to call during server-side rendering.\n */\nexport function createBuilder(config: BuilderHostConfig): Builder {\n const resolved = resolveConfig(config);\n assertEnvironmentAvailable(resolved.environment);\n return new BuilderController(resolved);\n}\n", "/** Query parameter that marks \"back from the login page, resume the save\". */\nexport const RESUME_PARAM = 'tdn_resume';\n\n/** localStorage key where the builder indexes the drafts it kept on this device. */\nconst DRAFT_INDEX_KEY = 'tdn-draft-index';\n\n/**\n * A relative return path for the host's login redirect: the current page with\n * the resume marker. Only same-site paths, so the redirect can never become an\n * open redirect.\n */\nexport function loginReturnPath(): string {\n const url = new URL(window.location.href);\n url.searchParams.set(RESUME_PARAM, '1');\n return url.pathname + url.search + url.hash;\n}\n\n/** True for a path that stays on this site: starts with \"/\" but not \"//\" or \"/\\\". */\nexport function isSafeReturnPath(path: string): boolean {\n return /^\\/(?![/\\\\])/.test(path);\n}\n\n/**\n * True once, when the page was opened by a login redirect carrying the resume\n * marker. Strips the marker from the address bar.\n */\nexport function consumeResumeMarker(): boolean {\n if (typeof window === 'undefined') return false;\n const url = new URL(window.location.href);\n if (url.searchParams.get(RESUME_PARAM) !== '1') return false;\n url.searchParams.delete(RESUME_PARAM);\n window.history.replaceState(\n window.history.state,\n '',\n url.pathname + url.search + url.hash\n );\n return true;\n}\n\n/** True when the builder kept an unsaved draft of this 3DN product on this device. */\nexport function hasDraft(productId: string): boolean {\n if (typeof localStorage === 'undefined') return false;\n try {\n const index = JSON.parse(\n localStorage.getItem(DRAFT_INDEX_KEY) ?? '{}'\n ) as Record<string, { productId?: string } | undefined>;\n return Object.values(index).some((draft) => draft?.productId === productId);\n } catch {\n return false;\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["BuilderError", "_BuilderError", "code", "message", "options", "_a", "_b", "SAVE_FAILURE_MESSAGES", "saveFailedError", "detail", "licenseError", "destroyedError", "configError", "DEFAULT_SAVE_TIMEOUT_MS", "DEFAULT_EXPORT_TIMEOUT_MS", "CONFIG_KEYS", "MOUNT_KEYS", "VERSION_PATTERN", "INSTALLATION_KEY_PATTERN", "isExactVersion", "value", "isRecord", "isFunction", "isPositiveNumber", "containsSecret", "depth", "v", "rejectUnknownKeys", "allowed", "where", "key", "configError", "resolveConfig", "config", "_a", "_b", "_c", "environment", "version", "saveTimeoutMs", "session", "login", "customization", "validateMountOptions", "options", "target", "hostProductId", "mode", "designId", "validateObservabilityContext", "externalExport", "parsed", "validateSession", "fail", "what", "BuilderError", "hasToken", "hasRefresh", "validateShopperTokens", "tokens", "TOKEN_KEY", "redact", "value", "depth", "v", "out", "key", "createDebugLog", "enabled", "message", "detail", "BUILDER_TAG", "BUNDLE_HOSTS", "DEFINE_TIMEOUT_MS", "STATE_KEY", "pageState", "holder", "state", "isBrowser", "assertEnvironmentAvailable", "environment", "configError", "loadFailed", "message", "cause", "BuilderError", "resolveVersion", "host", "requested", "response", "info", "version", "isExactVersion", "injectScript", "src", "resolve", "reject", "script", "whenDefined", "timer", "assertCompatible", "loadBuilder", "error", "deferred", "resolve", "reject", "promise", "res", "rej", "PausableTimer", "ms", "onExpire", "BuilderInstance", "_BuilderInstance", "ctx", "options", "session", "raw", "cause", "BuilderError", "validateSession", "instance", "el", "BUILDER_TAG", "setShopperTokens", "on", "name", "handler", "listener", "event", "THREED_BUILDER_LOGIN_REQUIRED_EVENT", "d", "_a", "THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT", "THREED_BUILDER_LICENSE_ERROR_EVENT", "THREED_BUILDER_DESIGN_SAVED_EVENT", "THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT", "saveFailedError", "THREED_BUILDER_PRODUCT_CLONED_EVENT", "THREED_BUILDER_CLOSE_REQUESTED_EVENT", "_b", "THREED_BUILDER_STATUS_EVENT", "THREED_BUILDER_EXPORT_REQUESTED_EVENT", "dispatchOpenThreedBuilder", "destroyedError", "run", "dispatchSaveRequest", "epoch", "controller", "context", "validateObservabilityContext", "licenseToken", "action", "login", "dispatchLoginCancelled", "stale", "redirected", "tokens", "validateShopperTokens", "choice", "outcome", "pending", "detail", "error", "licenseError", "saved", "
|
|
4
|
+
"sourcesContent": ["import type {\n DesignSaveFailedDetail,\n DesignSaveFailureReason,\n LicenseErrorCode,\n} from '@three-dn/constants';\n\nexport type BuilderErrorCode =\n | 'CONFIG_INVALID'\n | 'NOT_IN_BROWSER'\n | 'LOAD_FAILED'\n | 'VERSION_CONFLICT'\n | 'SESSION_FAILED'\n | 'LICENSE_ERROR'\n | 'LOGIN_CANCELLED'\n | 'SAVE_FAILED'\n | 'SAVE_TIMEOUT'\n | 'EXPORT_FAILED'\n | 'EXPORT_TIMEOUT'\n | 'DESTROYED';\n\n/** The builder's own reason, plus `customization` when only the record failed. */\nexport type BuilderErrorReason =\n | DesignSaveFailureReason\n | LicenseErrorCode\n | 'customization';\n\nexport interface BuilderErrorOptions {\n retryable?: boolean;\n fatal?: boolean;\n reason?: BuilderErrorReason;\n cause?: unknown;\n}\n\n/**\n * Every failure the SDK reports. Hosts switch on `code`; `message` is safe to\n * show or log and never contains a token.\n */\nexport class BuilderError extends Error {\n readonly code: BuilderErrorCode;\n readonly retryable: boolean;\n /** The mount cannot continue until the host refreshes or recreates it. */\n readonly fatal: boolean;\n readonly reason?: BuilderErrorReason;\n readonly cause?: unknown;\n\n constructor(\n code: BuilderErrorCode,\n message: string,\n options: BuilderErrorOptions = {}\n ) {\n super(message);\n this.name = 'BuilderError';\n this.code = code;\n this.retryable = options.retryable ?? false;\n this.fatal = options.fatal ?? false;\n this.reason = options.reason;\n this.cause = options.cause;\n Object.setPrototypeOf(this, BuilderError.prototype);\n }\n}\n\nconst SAVE_FAILURE_MESSAGES: Record<DesignSaveFailureReason, string> = {\n conflict: 'Someone else saved this design first.',\n forbidden: 'This shopper may not save this design.',\n unauthorized: 'The shopper could not be signed in to save.',\n 'login-cancelled':\n 'The design was not saved because the shopper did not log in.',\n 'installation-inactive': \"This site's 3DN installation is not active.\",\n license: 'This page is not licensed to use the 3DN builder.',\n error: 'The design could not be saved. Check the connection and try again.',\n};\n\nexport function saveFailedError(detail: DesignSaveFailedDetail): BuilderError {\n const message =\n SAVE_FAILURE_MESSAGES[detail.reason] ?? SAVE_FAILURE_MESSAGES.error;\n if (detail.reason === 'login-cancelled') {\n return new BuilderError('LOGIN_CANCELLED', message, {\n retryable: true,\n reason: detail.reason,\n cause: detail,\n });\n }\n return new BuilderError('SAVE_FAILED', message, {\n retryable: detail.retryable,\n reason: detail.reason,\n cause: detail,\n });\n}\n\nexport function licenseError(code: LicenseErrorCode): BuilderError {\n const message =\n code === 'ORIGIN_NOT_ALLOWED'\n ? 'This website is not on the 3DN license. Add its hostname to the license.'\n : 'The 3DN license token was rejected. The license may be suspended, revoked or expired.';\n return new BuilderError('LICENSE_ERROR', message, {\n fatal: true,\n reason: code,\n cause: { code },\n });\n}\n\nexport function destroyedError(): BuilderError {\n return new BuilderError('DESTROYED', 'The builder was destroyed.');\n}\n\nexport function configError(message: string): BuilderError {\n return new BuilderError('CONFIG_INVALID', message, { fatal: true });\n}\n", "import type {\n BuilderStatusDetail,\n CloseRequestedDetail,\n ExportFile,\n ExportRequestedDetail,\n LoginRequiredAction,\n} from '@three-dn/constants';\nimport { BuilderError, configError } from './errors';\n\nexport type BuilderEnvironment = 'production' | 'staging';\nexport type BuilderVersion = 'latest' | `${number}.${number}.${number}`;\nexport type BuilderMode = 'designer' | 'viewer';\n\n/** What the host's session endpoint returns: site access plus the product to open. */\nexport interface BuilderSession {\n licenseToken: string;\n /** The 3DN product to open: the shopper's clone, else the global product. */\n productId: string;\n /** Required when the product to open is private. */\n viewToken?: string;\n /** Optional fast path: shopper tokens for a shopper the host backend verified. */\n token?: string;\n refreshToken?: string;\n}\n\n/** Shopper identity, from the host's login-token endpoint. */\nexport interface ShopperTokens {\n token: string;\n refreshToken: string;\n}\n\nexport type LoginOutcome = 'logged-in' | 'redirected' | 'cancelled';\n\n/** What the host stores when a save created the shopper's clone. */\nexport interface CustomizationLink {\n hostProductId: string;\n sourceProductId: string;\n savedProductId: string;\n /**\n * The shopper asked for a copy (\"Save as copy\", \"keep both\") and keeps the\n * original too. `false` when Save had to fork a product they don't own: the\n * copy replaces the original.\n */\n asCopy: boolean;\n}\n\n/** One successful save, however it started: `save()` or the builder's own Save button. */\nexport interface SavedDesignEvent {\n hostProductId: string;\n /** The saved 3DN product: the shopper's clone after a first save. */\n productId: string;\n version: number;\n}\n\nexport interface BuilderHostConfig {\n /** Default `'production'`. */\n environment?: BuilderEnvironment;\n /** `'latest'` or an exact builder release. Default `'latest'`. */\n version?: BuilderVersion;\n /** Default 45 000 ms. */\n saveTimeoutMs?: number;\n session: {\n get(request: { hostProductId: string }): Promise<BuilderSession>;\n refreshLicense(): Promise<Pick<BuilderSession, 'licenseToken'>>;\n };\n login?: {\n /** Answers `tdn-login-required`. Resolve `null` when the endpoint answers 401. */\n getTokens(): Promise<ShopperTokens | null>;\n /** The host's login UX for the action the builder gated. */\n onRequired(action: LoginRequiredAction): Promise<LoginOutcome>;\n /** `getTokens()` failed (5xx, network). Default: `'cancel'`. */\n onMintFailed?(): Promise<'retry' | 'cancel'>;\n };\n customization?: {\n record(link: CustomizationLink): Promise<void>;\n };\n /**\n * Every successful save, including ones the shopper starts from the builder's\n * own Save button. Fires after any clone mapping has been recorded; never\n * when recording failed. The product's images may still show the previous\n * save: read them on `onImagesSaved`.\n */\n onSaved?(saved: SavedDesignEvent): void;\n /**\n * The saved product's images now show the design at `version`: read them\n * now. Fires after the `onSaved` of the save that took them, and when the\n * builder's Close refreshed images that autosave left behind. Never fires\n * when the images could not be updated (the product keeps its old ones).\n */\n onImagesSaved?(saved: SavedDesignEvent): void;\n onLicenseError?(error: BuilderError): void;\n onCloseRequested?(detail: CloseRequestedDetail): void;\n onStatusChange?(detail: BuilderStatusDetail): void;\n /** Log lifecycle steps and events to the console. Tokens are always redacted. */\n debug?: boolean;\n}\n\nexport interface ExternalExportOptions {\n run(\n request: ExportRequestedDetail,\n signal: AbortSignal\n ): Promise<ExportFile[]>;\n /** Default 120 000 ms. */\n timeoutMs?: number;\n}\n\nexport interface MountOptions {\n target: HTMLElement;\n /** The host's own product reference, passed to `session.get()`. Never a 3DN id. */\n hostProductId: string;\n /** Default `'designer'`. `'viewer'` is the display-only mode. */\n mode?: BuilderMode;\n designId?: string;\n /** Opt-in, GLB products only. Off unless passed. */\n externalExport?: ExternalExportOptions;\n /**\n * The host page's observability context, a JSON object string the builder\n * reads as `observability-context`: which host app, tenant, site and user\n * its own Sentry events belong to, and the page's trace to join. Set before\n * the element connects; update it with `setObservabilityContext()`. The\n * builder drops keys it does not allow and ignores an invalid value.\n */\n observabilityContext?: string;\n}\n\nexport interface ResolvedConfig extends BuilderHostConfig {\n environment: BuilderEnvironment;\n version: BuilderVersion;\n saveTimeoutMs: number;\n}\n\nexport const DEFAULT_SAVE_TIMEOUT_MS = 45_000;\nexport const DEFAULT_EXPORT_TIMEOUT_MS = 120_000;\n\nconst CONFIG_KEYS = new Set([\n 'environment',\n 'version',\n 'saveTimeoutMs',\n 'session',\n 'login',\n 'customization',\n 'onSaved',\n 'onImagesSaved',\n 'onLicenseError',\n 'onCloseRequested',\n 'onStatusChange',\n 'debug',\n]);\n\nconst MOUNT_KEYS = new Set([\n 'target',\n 'hostProductId',\n 'mode',\n 'designId',\n 'externalExport',\n 'observabilityContext',\n]);\n\nconst VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\nconst INSTALLATION_KEY_PATTERN = /^tdn_inst_/;\n\nexport function isExactVersion(value: string): boolean {\n return VERSION_PATTERN.test(value);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isFunction(value: unknown): value is (...args: never[]) => unknown {\n return typeof value === 'function';\n}\n\nfunction isPositiveNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n/** Walks the config looking for anything that looks like an installation key. */\nfunction containsSecret(value: unknown, depth = 0): boolean {\n if (typeof value === 'string') return INSTALLATION_KEY_PATTERN.test(value);\n if (!isRecord(value) || depth > 3) return false;\n return Object.values(value).some((v) => containsSecret(v, depth + 1));\n}\n\nfunction rejectUnknownKeys(\n value: Record<string, unknown>,\n allowed: Set<string>,\n where: string\n): void {\n for (const key of Object.keys(value)) {\n if (allowed.has(key)) continue;\n if (/url|src|host|origin/i.test(key)) {\n throw configError(\n `${where}: \"${key}\" is not allowed. The SDK decides where the builder loads from; choose an environment and a version instead.`\n );\n }\n throw configError(`${where}: unknown option \"${key}\".`);\n }\n}\n\nexport function resolveConfig(config: unknown): ResolvedConfig {\n if (!isRecord(config)) {\n throw configError('createBuilder() needs a configuration object.');\n }\n if (containsSecret(config)) {\n throw configError(\n 'createBuilder(): an installation key was passed to the browser. Installation keys stay on your server; pass a session callback instead.'\n );\n }\n rejectUnknownKeys(config, CONFIG_KEYS, 'createBuilder()');\n\n const environment = config['environment'] ?? 'production';\n if (environment !== 'production' && environment !== 'staging') {\n throw configError(\n `createBuilder(): environment must be \"production\" or \"staging\", got ${JSON.stringify(\n environment\n )}.`\n );\n }\n\n const version = config['version'] ?? 'latest';\n if (\n typeof version !== 'string' ||\n (version !== 'latest' && !isExactVersion(version))\n ) {\n throw configError(\n `createBuilder(): version must be \"latest\" or an exact release such as \"1.2.3\", got ${JSON.stringify(\n version\n )}.`\n );\n }\n\n const saveTimeoutMs = config['saveTimeoutMs'] ?? DEFAULT_SAVE_TIMEOUT_MS;\n if (!isPositiveNumber(saveTimeoutMs)) {\n throw configError(\n 'createBuilder(): saveTimeoutMs must be a positive number of milliseconds.'\n );\n }\n\n const session = config['session'];\n if (\n !isRecord(session) ||\n !isFunction(session['get']) ||\n !isFunction(session['refreshLicense'])\n ) {\n throw configError(\n 'createBuilder(): session.get() and session.refreshLicense() are required. They call your session endpoint.'\n );\n }\n\n const login = config['login'];\n if (login !== undefined) {\n if (\n !isRecord(login) ||\n !isFunction(login['getTokens']) ||\n !isFunction(login['onRequired'])\n ) {\n throw configError(\n 'createBuilder(): login needs getTokens() (your login-token endpoint) and onRequired() (your login UI).'\n );\n }\n if (\n login['onMintFailed'] !== undefined &&\n !isFunction(login['onMintFailed'])\n ) {\n throw configError(\n 'createBuilder(): login.onMintFailed must be a function.'\n );\n }\n }\n\n const customization = config['customization'];\n if (\n customization !== undefined &&\n (!isRecord(customization) || !isFunction(customization['record']))\n ) {\n throw configError(\n 'createBuilder(): customization.record() is required when customization is set. It calls your customization endpoint.'\n );\n }\n\n for (const key of [\n 'onSaved',\n 'onImagesSaved',\n 'onLicenseError',\n 'onCloseRequested',\n 'onStatusChange',\n ] as const) {\n if (config[key] !== undefined && !isFunction(config[key])) {\n throw configError(`createBuilder(): ${key} must be a function.`);\n }\n }\n\n return {\n ...(config as unknown as BuilderHostConfig),\n environment,\n version: version as BuilderVersion,\n saveTimeoutMs,\n };\n}\n\nexport function validateMountOptions(options: unknown): MountOptions {\n if (!isRecord(options)) {\n throw configError('mount() needs an options object.');\n }\n rejectUnknownKeys(options, MOUNT_KEYS, 'mount()');\n\n const target = options['target'];\n if (typeof HTMLElement === 'undefined' || !(target instanceof HTMLElement)) {\n throw configError('mount(): target must be an HTMLElement on the page.');\n }\n const hostProductId = options['hostProductId'];\n if (typeof hostProductId !== 'string' || hostProductId.trim() === '') {\n throw configError(\n 'mount(): hostProductId must be your own, non-empty product reference.'\n );\n }\n const mode = options['mode'] ?? 'designer';\n if (mode !== 'designer' && mode !== 'viewer') {\n throw configError(\n `mount(): mode must be \"designer\" or \"viewer\", got ${JSON.stringify(\n mode\n )}.`\n );\n }\n const designId = options['designId'];\n if (\n designId !== undefined &&\n (typeof designId !== 'string' || designId === '')\n ) {\n throw configError('mount(): designId must be a non-empty string.');\n }\n if (options['observabilityContext'] !== undefined) {\n validateObservabilityContext(options['observabilityContext'], 'mount()');\n }\n const externalExport = options['externalExport'];\n if (externalExport !== undefined) {\n if (!isRecord(externalExport) || !isFunction(externalExport['run'])) {\n throw configError(\n 'mount(): externalExport.run() is required when externalExport is set.'\n );\n }\n if (\n externalExport['timeoutMs'] !== undefined &&\n !isPositiveNumber(externalExport['timeoutMs'])\n ) {\n throw configError(\n 'mount(): externalExport.timeoutMs must be a positive number of milliseconds.'\n );\n }\n }\n return options as unknown as MountOptions;\n}\n\n/** A JSON object string: what the builder's `observability-context` accepts. */\nexport function validateObservabilityContext(\n value: unknown,\n where: string\n): string {\n let parsed: unknown;\n try {\n parsed = typeof value === 'string' ? JSON.parse(value) : undefined;\n } catch {\n parsed = undefined;\n }\n if (!isRecord(parsed) || Array.isArray(parsed)) {\n throw configError(\n `${where}: observabilityContext must be a JSON object string.`\n );\n }\n return value as string;\n}\n\n/** Checks the shape of a session response before anything reaches the element. */\nexport function validateSession(session: unknown): BuilderSession {\n const fail = (what: string) =>\n new BuilderError(\n 'SESSION_FAILED',\n `session.get() returned an invalid session: ${what}.`,\n {\n retryable: false,\n }\n );\n if (!isRecord(session)) throw fail('expected an object');\n if (\n typeof session['licenseToken'] !== 'string' ||\n session['licenseToken'] === ''\n ) {\n throw fail('licenseToken is missing');\n }\n if (typeof session['productId'] !== 'string' || session['productId'] === '') {\n throw fail('productId is missing');\n }\n if (\n session['viewToken'] != null &&\n typeof session['viewToken'] !== 'string'\n ) {\n throw fail('viewToken must be a string');\n }\n const hasToken = session['token'] != null;\n const hasRefresh = session['refreshToken'] != null;\n if (hasToken !== hasRefresh) {\n throw fail('token and refreshToken must be returned together');\n }\n if (\n hasToken &&\n (typeof session['token'] !== 'string' ||\n typeof session['refreshToken'] !== 'string')\n ) {\n throw fail('token and refreshToken must be strings');\n }\n return session as unknown as BuilderSession;\n}\n\nexport function validateShopperTokens(tokens: unknown): ShopperTokens | null {\n if (tokens === null) return null;\n if (\n !isRecord(tokens) ||\n typeof tokens['token'] !== 'string' ||\n tokens['token'] === '' ||\n typeof tokens['refreshToken'] !== 'string' ||\n tokens['refreshToken'] === ''\n ) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'login.getTokens() must resolve { token, refreshToken } or null.'\n );\n }\n return tokens as unknown as ShopperTokens;\n}\n", "const TOKEN_KEY = /token|secret|key|authorization/i;\n\n/** Copies a value for logging with every token-like field replaced. */\nexport function redact(value: unknown, depth = 0): unknown {\n if (value === null || typeof value !== 'object' || depth > 4) return value;\n if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value)) {\n out[key] =\n TOKEN_KEY.test(key) && v != null ? '[redacted]' : redact(v, depth + 1);\n }\n return out;\n}\n\nexport type DebugLog = (message: string, detail?: unknown) => void;\n\nexport function createDebugLog(enabled: boolean | undefined): DebugLog {\n if (!enabled) return () => undefined;\n return (message, detail) => {\n if (detail === undefined) console.debug(`[3DN] ${message}`);\n else console.debug(`[3DN] ${message}`, redact(detail));\n };\n}\n", "import type { BuilderEnvironment, BuilderVersion } from './config';\nimport { isExactVersion } from './config';\nimport { BuilderError, configError } from './errors';\n\nexport const BUILDER_TAG = 'threed-builder';\n\n/**\n * Where each environment's builder is served from. SDK-owned: hosts choose an\n * environment, never a location. Staging stays unset until its location is\n * confirmed; createBuilder() refuses `environment: 'staging'` until then.\n */\nconst BUNDLE_HOSTS: Record<BuilderEnvironment, string | undefined> = {\n production: 'https://3d-builder.3dn.app',\n staging: undefined,\n};\n\nconst DEFINE_TIMEOUT_MS = 30_000;\n\n/** Shared by every copy of the SDK on the page: one custom element registry. */\ninterface PageLoaderState {\n environment?: BuilderEnvironment;\n /** The exact version loaded, once known. */\n version?: string;\n loading?: Promise<void>;\n}\n\nconst STATE_KEY = Symbol.for('@3dn-builder/builder/loader');\n\nfunction pageState(): PageLoaderState {\n const holder = globalThis as unknown as Record<\n symbol,\n PageLoaderState | undefined\n >;\n let state = holder[STATE_KEY];\n if (!state) {\n state = {};\n holder[STATE_KEY] = state;\n }\n return state;\n}\n\nexport function isBrowser(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof document !== 'undefined' &&\n typeof customElements !== 'undefined'\n );\n}\n\nexport function assertEnvironmentAvailable(\n environment: BuilderEnvironment\n): void {\n if (!BUNDLE_HOSTS[environment]) {\n throw configError(\n `createBuilder(): the \"${environment}\" environment is not available in this SDK release.`\n );\n }\n}\n\nfunction loadFailed(message: string, cause?: unknown): BuilderError {\n return new BuilderError('LOAD_FAILED', message, { retryable: true, cause });\n}\n\nasync function resolveVersion(\n host: string,\n requested: BuilderVersion\n): Promise<string> {\n if (requested !== 'latest') return requested;\n let response: Response;\n try {\n response = await fetch(`${host}/version-info.json`, {\n cache: 'no-store',\n credentials: 'omit',\n });\n } catch (cause) {\n throw loadFailed('The 3DN builder could not be reached.', cause);\n }\n if (!response.ok) {\n throw loadFailed(\n `The 3DN builder release could not be resolved (HTTP ${response.status}).`\n );\n }\n const info = (await response.json().catch(() => null)) as {\n version?: unknown;\n } | null;\n const version = info?.version;\n if (typeof version !== 'string' || !isExactVersion(version)) {\n throw loadFailed('The 3DN builder release information was not valid.');\n }\n return version;\n}\n\nfunction injectScript(src: string): Promise<HTMLScriptElement> {\n return new Promise((resolve, reject) => {\n const script = document.createElement('script');\n script.type = 'module';\n script.src = src;\n script.crossOrigin = 'anonymous';\n script.onload = () => resolve(script);\n script.onerror = () => {\n script.remove();\n reject(loadFailed('The 3DN builder failed to load.'));\n };\n document.head.appendChild(script);\n });\n}\n\nfunction whenDefined(): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(\n () => reject(loadFailed('The 3DN builder loaded but did not start.')),\n DEFINE_TIMEOUT_MS\n );\n customElements.whenDefined(BUILDER_TAG).then(() => {\n clearTimeout(timer);\n resolve();\n }, reject);\n });\n}\n\nfunction assertCompatible(\n state: PageLoaderState,\n environment: BuilderEnvironment,\n requested: BuilderVersion\n): void {\n if (state.environment && state.environment !== environment) {\n throw new BuilderError(\n 'VERSION_CONFLICT',\n `The ${state.environment} builder already runs on this page; ${environment} cannot load beside it.`,\n { fatal: true }\n );\n }\n if (requested !== 'latest' && state.version && state.version !== requested) {\n throw new BuilderError(\n 'VERSION_CONFLICT',\n `Builder ${state.version} already runs on this page; ${requested} cannot load beside it.`,\n { fatal: true }\n );\n }\n}\n\n/**\n * Loads the builder once per page. A failed load leaves nothing behind, so the\n * next call tries again.\n */\nexport async function loadBuilder(\n environment: BuilderEnvironment,\n requested: BuilderVersion\n): Promise<void> {\n if (!isBrowser()) {\n throw new BuilderError(\n 'NOT_IN_BROWSER',\n 'The 3DN builder can only load in a browser.'\n );\n }\n const state = pageState();\n\n if (state.loading) {\n await state.loading;\n assertCompatible(state, environment, requested);\n return;\n }\n // Defined by something other than this SDK (an older embed, or the testing\n // fake): its version is unknown, so use it as it is.\n if (customElements.get(BUILDER_TAG)) return;\n\n const host = BUNDLE_HOSTS[environment];\n if (!host) {\n throw configError(\n `The \"${environment}\" environment is not available in this SDK release.`\n );\n }\n\n state.environment = environment;\n state.loading = (async () => {\n const version = await resolveVersion(host, requested);\n state.version = version;\n const script = await injectScript(`${host}/builder-${version}.js`);\n try {\n await whenDefined();\n } catch (error) {\n script.remove();\n throw error;\n }\n })();\n\n try {\n await state.loading;\n } catch (error) {\n state.loading = undefined;\n state.environment = undefined;\n state.version = undefined;\n throw error instanceof BuilderError\n ? error\n : loadFailed('The 3DN builder failed to load.', error);\n }\n assertCompatible(state, environment, requested);\n}\n", "import {\n THREED_BUILDER_CLOSE_REQUESTED_EVENT,\n THREED_BUILDER_DESIGN_SAVED_EVENT,\n THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,\n THREED_BUILDER_EXPORT_COMPLETE_EVENT,\n THREED_BUILDER_EXPORT_REQUESTED_EVENT,\n THREED_BUILDER_IMAGES_SAVED_EVENT,\n THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT,\n THREED_BUILDER_LICENSE_ERROR_EVENT,\n THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,\n THREED_BUILDER_LOGIN_REQUIRED_EVENT,\n THREED_BUILDER_PRODUCT_CLONED_EVENT,\n THREED_BUILDER_STATUS_EVENT,\n dispatchExportComplete,\n dispatchLoginCancelled,\n dispatchOpenThreedBuilder,\n dispatchSaveRequest,\n type BuilderStatusDetail,\n type CloseRequestedDetail,\n type DesignSaveFailedDetail,\n type DesignSavedDetail,\n type ExportRequestedDetail,\n type ImagesSavedDetail,\n type LicenseErrorDetail,\n type LoginRequiredAction,\n type LoginRequiredDetail,\n type ProductClonedDetail,\n} from '@three-dn/constants';\nimport {\n DEFAULT_EXPORT_TIMEOUT_MS,\n validateObservabilityContext,\n validateSession,\n validateShopperTokens,\n type BuilderSession,\n type CustomizationLink,\n type MountOptions,\n type ResolvedConfig,\n type ShopperTokens,\n} from './config';\nimport type { DebugLog } from './debug';\nimport {\n BuilderError,\n destroyedError,\n licenseError,\n saveFailedError,\n} from './errors';\nimport { BUILDER_TAG } from './loader';\n\n/** What `save()` resolves with. `productId` is always the one to put in the cart. */\nexport interface SavedDesign {\n productId: string;\n version: number;\n}\n\nexport interface InstanceContext {\n config: ResolvedConfig;\n log: DebugLog;\n /** Single-flight license refresh shared by every instance of a controller. */\n refreshLicense(): Promise<void>;\n onDestroyed(instance: BuilderInstance): void;\n}\n\ninterface Deferred<T> {\n promise: Promise<T>;\n resolve(value: T): void;\n reject(error: unknown): void;\n}\n\nfunction deferred<T>(): Deferred<T> {\n let resolve!: (value: T) => void;\n let reject!: (error: unknown) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n // A rejection nobody awaits yet must not surface as unhandled.\n promise.catch(() => undefined);\n return { promise, resolve, reject };\n}\n\n/** A timer that stops counting while a login round-trip is pending. */\nclass PausableTimer {\n private handle?: ReturnType<typeof setTimeout>;\n private startedAt = 0;\n private remaining: number;\n\n constructor(ms: number, private readonly onExpire: () => void) {\n this.remaining = ms;\n }\n\n start(): void {\n if (this.handle !== undefined) return;\n this.startedAt = Date.now();\n this.handle = setTimeout(this.onExpire, this.remaining);\n }\n\n pause(): void {\n if (this.handle === undefined) return;\n clearTimeout(this.handle);\n this.handle = undefined;\n this.remaining = Math.max(\n 0,\n this.remaining - (Date.now() - this.startedAt)\n );\n }\n\n stop(): void {\n if (this.handle !== undefined) clearTimeout(this.handle);\n this.handle = undefined;\n }\n}\n\ninterface SaveRun {\n result: Deferred<SavedDesign>;\n timer: PausableTimer;\n}\n\ninterface ExportReply {\n files: { url: string; format: string; surface: string }[];\n error?: { code: 'EXPORT_FAILED' | 'EXPORT_TIMEOUT' };\n}\n\n/** One mounted `<threed-builder>`. */\nexport class BuilderInstance {\n readonly element: HTMLElement;\n private currentProductId: string;\n /** Bumped by logout() and destroy(): async results from an older epoch are dropped. */\n private epoch = 0;\n private destroyed = false;\n private fatal?: BuilderError;\n private loginPending = false;\n private saveRun?: SaveRun;\n /** A clone mapping whose record() failed; retried before the next save resolves. */\n private unrecordedLink?: CustomizationLink;\n /** Set between a save that changed the product id and its tdn-product-cloned. */\n private cloneWait?: Deferred<void>;\n /**\n * Set between a tdn-design-saved and that save's images outcome: true on\n * tdn-images-saved, false on tdn-images-save-failed.\n */\n private imagesWait?: Deferred<boolean>;\n private readonly exports = new Set<AbortController>();\n private readonly listeners: [string, EventListener][] = [];\n\n private constructor(\n private readonly ctx: InstanceContext,\n private readonly options: MountOptions,\n session: BuilderSession\n ) {\n this.currentProductId = session.productId;\n this.element = this.createElement(session);\n }\n\n /** Builds the element in the documented order and inserts it last. */\n static async mount(\n ctx: InstanceContext,\n options: MountOptions\n ): Promise<BuilderInstance> {\n let raw: unknown;\n try {\n raw = await ctx.config.session.get({\n hostProductId: options.hostProductId,\n });\n } catch (cause) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'The builder session could not be loaded.',\n {\n retryable: true,\n cause,\n }\n );\n }\n const session = validateSession(raw);\n const instance = new BuilderInstance(ctx, options, session);\n options.target.appendChild(instance.element);\n ctx.log('mounted', {\n hostProductId: options.hostProductId,\n productId: session.productId,\n });\n return instance;\n }\n\n /** The 3DN product the builder is editing now: the clone, after a first save. */\n get productId(): string {\n return this.currentProductId;\n }\n\n get hostProductId(): string {\n return this.options.hostProductId;\n }\n\n private createElement(session: BuilderSession): HTMLElement {\n const el = document.createElement(BUILDER_TAG);\n this.attachListeners(el);\n el.setAttribute('product', session.productId);\n el.setAttribute('license-token', session.licenseToken);\n if (session.viewToken) el.setAttribute('view-token', session.viewToken);\n if (session.token && session.refreshToken) {\n setShopperTokens(el, {\n token: session.token,\n refreshToken: session.refreshToken,\n });\n }\n if (this.options.designId)\n el.setAttribute('design-id', this.options.designId);\n if (this.options.mode === 'viewer') el.setAttribute('display-only', 'true');\n if (this.options.externalExport) el.setAttribute('external-export', 'true');\n if (this.options.observabilityContext) {\n el.setAttribute(\n 'observability-context',\n this.options.observabilityContext\n );\n }\n el.style.display = 'block';\n el.style.width = '100%';\n el.style.height = '100%';\n return el;\n }\n\n private attachListeners(el: HTMLElement): void {\n const on = <T>(name: string, handler: (detail: T) => void) => {\n const listener: EventListener = (event) => {\n // Only this builder's own events: never a neighbour's, never a bubbled copy.\n if (event.target !== el || this.destroyed) return;\n this.ctx.log(name, (event as CustomEvent<T>).detail);\n handler((event as CustomEvent<T>).detail);\n };\n el.addEventListener(name, listener);\n this.listeners.push([name, listener]);\n };\n\n on<LoginRequiredDetail>(\n THREED_BUILDER_LOGIN_REQUIRED_EVENT,\n (d) => void this.answerLogin(d?.action ?? 'save')\n );\n on(\n THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,\n () => void this.ctx.refreshLicense()\n );\n on<LicenseErrorDetail>(THREED_BUILDER_LICENSE_ERROR_EVENT, (d) =>\n this.onLicenseError(d)\n );\n on<DesignSavedDetail>(\n THREED_BUILDER_DESIGN_SAVED_EVENT,\n (d) => void this.onDesignSaved(d)\n );\n on<DesignSaveFailedDetail>(THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT, (d) =>\n this.finishSave(saveFailedError(d))\n );\n on<ImagesSavedDetail>(THREED_BUILDER_IMAGES_SAVED_EVENT, (d) =>\n this.onImagesSettled(d, true)\n );\n on<ImagesSavedDetail>(THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT, (d) =>\n this.onImagesSettled(d, false)\n );\n on<ProductClonedDetail>(\n THREED_BUILDER_PRODUCT_CLONED_EVENT,\n (d) => void this.onProductCloned(d)\n );\n on<CloseRequestedDetail>(THREED_BUILDER_CLOSE_REQUESTED_EVENT, (d) =>\n this.ctx.config.onCloseRequested?.(d)\n );\n on<BuilderStatusDetail>(THREED_BUILDER_STATUS_EVENT, (d) =>\n this.ctx.config.onStatusChange?.(d)\n );\n if (this.options.externalExport) {\n on<ExportRequestedDetail>(\n THREED_BUILDER_EXPORT_REQUESTED_EVENT,\n (d) => void this.answerExport(d)\n );\n }\n }\n\n // \u2500\u2500 Host-facing methods \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Shows the builder UI, for a host that mounted it before showing it. */\n open(): void {\n this.assertUsable();\n dispatchOpenThreedBuilder(this.element);\n }\n\n /**\n * Saves the design. Resolves once it is saved, any clone mapping it\n * required has been recorded, and the product's images were refreshed (or\n * could not be: the save still resolves); concurrent calls share one save.\n */\n save(): Promise<SavedDesign> {\n if (this.destroyed) return Promise.reject(destroyedError());\n if (this.fatal) return Promise.reject(this.fatal);\n if (this.saveRun) return this.saveRun.result.promise;\n\n const run: SaveRun = {\n result: deferred<SavedDesign>(),\n timer: new PausableTimer(this.ctx.config.saveTimeoutMs, () =>\n this.finishSave(\n new BuilderError(\n 'SAVE_TIMEOUT',\n 'Saving the design took too long. Try again.',\n {\n retryable: true,\n }\n )\n )\n ),\n };\n this.saveRun = run;\n if (!this.loginPending) run.timer.start();\n dispatchSaveRequest(this.element);\n return run.result.promise;\n }\n\n /** Calls session.get() again and applies what it returns. */\n async refreshSession(): Promise<void> {\n this.assertUsable();\n const epoch = this.epoch;\n let raw: unknown;\n try {\n raw = await this.ctx.config.session.get({\n hostProductId: this.options.hostProductId,\n });\n } catch (cause) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'The builder session could not be refreshed.',\n {\n retryable: true,\n cause,\n }\n );\n }\n const session = validateSession(raw);\n if (epoch !== this.epoch || this.destroyed) return;\n this.applyLicense(session.licenseToken);\n if (session.viewToken)\n this.element.setAttribute('view-token', session.viewToken);\n if (session.token && session.refreshToken) {\n setShopperTokens(this.element, {\n token: session.token,\n refreshToken: session.refreshToken,\n });\n }\n this.fatal = undefined;\n }\n\n /** Removes the shopper layer. The site layer stays and the builder revokes the session. */\n logout(): void {\n if (this.destroyed) return;\n this.epoch++;\n this.element.removeAttribute('bearer-token');\n this.element.removeAttribute('refresh-token');\n if (this.loginPending) {\n this.loginPending = false;\n this.finishSave(\n new BuilderError(\n 'LOGIN_CANCELLED',\n 'The shopper logged out before the design was saved.',\n {\n retryable: true,\n }\n )\n );\n }\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.epoch++;\n this.finishSave(destroyedError());\n this.cloneWait?.reject(destroyedError());\n this.cloneWait = undefined;\n this.imagesWait?.resolve(false);\n this.imagesWait = undefined;\n for (const controller of this.exports) controller.abort();\n this.destroyed = true;\n for (const [name, listener] of this.listeners) {\n this.element.removeEventListener(name, listener);\n }\n this.listeners.length = 0;\n this.element.remove();\n this.ctx.onDestroyed(this);\n this.ctx.log('destroyed');\n }\n\n /**\n * Replaces the host page's observability context, for example once the\n * page's own Sentry has started and a trace exists to join.\n */\n setObservabilityContext(context: string): void {\n this.assertUsable();\n this.element.setAttribute(\n 'observability-context',\n validateObservabilityContext(context, 'setObservabilityContext()')\n );\n }\n\n /** Used by the controller to hand every instance the shared fresh license token. */\n applyLicense(licenseToken: string): void {\n if (this.destroyed) return;\n this.element.setAttribute('license-token', licenseToken);\n }\n\n get isDestroyed(): boolean {\n return this.destroyed;\n }\n\n // \u2500\u2500 Login round-trip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async answerLogin(action: LoginRequiredAction): Promise<void> {\n if (this.loginPending) return;\n const login = this.ctx.config.login;\n if (!login) {\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n const epoch = this.epoch;\n const stale = () => epoch !== this.epoch || this.destroyed;\n this.setLoginPending(true);\n let redirected = false;\n try {\n for (;;) {\n let tokens: ShopperTokens | null;\n try {\n tokens = validateShopperTokens(await login.getTokens());\n } catch (cause) {\n if (stale()) return;\n this.ctx.log('login.getTokens() failed', { cause: String(cause) });\n const choice = login.onMintFailed\n ? await login.onMintFailed()\n : 'cancel';\n if (stale()) return;\n if (choice === 'retry') continue;\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n if (stale()) return;\n if (tokens) {\n setShopperTokens(this.element, tokens); // resumes the pending action\n return;\n }\n const outcome = await login.onRequired(action);\n if (stale()) return;\n if (outcome === 'logged-in') continue;\n if (outcome === 'redirected') {\n // The page is navigating to the host's login. Never cancel: the\n // builder resumes the save after the shopper comes back.\n redirected = true;\n return;\n }\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n } finally {\n if (!redirected && epoch === this.epoch) this.setLoginPending(false);\n }\n }\n\n private setLoginPending(pending: boolean): void {\n this.loginPending = pending;\n if (!this.saveRun) return;\n if (pending) this.saveRun.timer.pause();\n else this.saveRun.timer.start();\n }\n\n // \u2500\u2500 License \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private onLicenseError(detail: LicenseErrorDetail): void {\n const error = licenseError(detail.code);\n this.fatal = error;\n this.finishSave(error);\n this.ctx.config.onLicenseError?.(error);\n }\n\n // \u2500\u2500 Save and clone recording \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async onDesignSaved(detail: DesignSavedDetail): Promise<void> {\n if (detail.productId !== this.currentProductId && !this.cloneWait) {\n // The save created a clone; tdn-product-cloned follows the thumbnail upload.\n this.cloneWait = deferred<void>();\n }\n // Every save reports its images next, before any tdn-product-cloned. A\n // save still waiting on its images when another lands reports none.\n this.imagesWait?.resolve(false);\n const images = (this.imagesWait = deferred<boolean>());\n // Saves from the builder's own Save button come through here too, with no\n // save() pending: they wait for the clone and its record all the same, so\n // onSaved only ever reports a save the host can act on.\n try {\n if (this.cloneWait) await this.cloneWait.promise;\n if (this.unrecordedLink) await this.recordLink(this.unrecordedLink);\n } catch (error) {\n this.finishSave(\n error instanceof BuilderError\n ? error\n : new BuilderError(\n 'SAVE_FAILED',\n 'The design was saved but could not be linked to this shopper.',\n {\n retryable: true,\n reason: 'customization',\n cause: error,\n }\n )\n );\n return;\n }\n if (this.destroyed) return;\n const saved: SavedDesign = {\n productId: detail.productId,\n version: detail.version,\n };\n this.notify('onSaved', saved);\n const updated = await images.promise;\n if (this.destroyed) return;\n if (updated) this.notify('onImagesSaved', saved);\n this.finishSave(undefined, saved);\n }\n\n private onImagesSettled(detail: ImagesSavedDetail, updated: boolean): void {\n const wait = this.imagesWait;\n if (wait) {\n // A save's images: onDesignSaved reports them after its onSaved.\n this.imagesWait = undefined;\n wait.resolve(updated);\n return;\n }\n // No save waiting: the builder's Close refreshed images autosave left behind.\n if (updated) {\n this.notify('onImagesSaved', {\n productId: detail.productId,\n version: detail.version,\n });\n }\n }\n\n /** A host callback that throws never breaks the save. */\n private notify(\n callback: 'onSaved' | 'onImagesSaved',\n saved: SavedDesign\n ): void {\n try {\n this.ctx.config[callback]?.({\n hostProductId: this.options.hostProductId,\n ...saved,\n });\n } catch (cause) {\n this.ctx.log(`${callback} threw`, { cause: String(cause) });\n }\n }\n\n private async onProductCloned(detail: ProductClonedDetail): Promise<void> {\n const link: CustomizationLink = {\n hostProductId: this.options.hostProductId,\n sourceProductId: detail.originalProductId,\n savedProductId: detail.newProductId,\n asCopy: detail.asCopy === true,\n };\n this.currentProductId = detail.newProductId; // later saves update the clone\n const wait = this.cloneWait;\n this.cloneWait = undefined;\n try {\n await this.recordLink(link);\n wait?.resolve();\n } catch (error) {\n wait?.reject(error);\n }\n }\n\n private async recordLink(link: CustomizationLink): Promise<void> {\n const customization = this.ctx.config.customization;\n if (!customization) return;\n try {\n await customization.record(link);\n if (this.unrecordedLink === link) this.unrecordedLink = undefined;\n } catch (cause) {\n this.unrecordedLink = link;\n throw new BuilderError(\n 'SAVE_FAILED',\n 'The design was saved but could not be linked to this shopper. Try saving again.',\n { retryable: true, reason: 'customization', cause }\n );\n }\n }\n\n private finishSave(error?: BuilderError, saved?: SavedDesign): void {\n const run = this.saveRun;\n if (!run) return;\n this.saveRun = undefined;\n run.timer.stop();\n if (error) run.result.reject(error);\n else if (saved) run.result.resolve(saved);\n }\n\n // \u2500\u2500 External export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async answerExport(request: ExportRequestedDetail): Promise<void> {\n const external = this.options.externalExport;\n if (!external) return;\n const controller = new AbortController();\n this.exports.add(controller);\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, external.timeoutMs ?? DEFAULT_EXPORT_TIMEOUT_MS);\n\n const abort = new Promise<never>((_, reject) =>\n controller.signal.addEventListener('abort', () =>\n reject(new Error('aborted'))\n )\n );\n try {\n const files = await Promise.race([\n external.run(request, controller.signal),\n abort,\n ]);\n dispatchExportComplete(this.element, { files });\n } catch (cause) {\n const code = timedOut ? 'EXPORT_TIMEOUT' : 'EXPORT_FAILED';\n this.ctx.log('export failed', { code, cause: String(cause) });\n this.replyExportFailure(code);\n } finally {\n clearTimeout(timer);\n this.exports.delete(controller);\n }\n }\n\n /**\n * The builder has no error field on tdn-export-complete yet: it ignores\n * `error`, and the empty file list ends its wait.\n */\n private replyExportFailure(code: 'EXPORT_FAILED' | 'EXPORT_TIMEOUT'): void {\n const detail: ExportReply = { files: [], error: { code } };\n this.element.dispatchEvent(\n new CustomEvent<ExportReply>(THREED_BUILDER_EXPORT_COMPLETE_EVENT, {\n bubbles: false,\n composed: false,\n detail,\n })\n );\n }\n\n private assertUsable(): void {\n if (this.destroyed) throw destroyedError();\n }\n}\n\n/** Refresh first, then bearer: setting the bearer token resumes a pending action. */\nfunction setShopperTokens(el: HTMLElement, tokens: ShopperTokens): void {\n el.setAttribute('refresh-token', tokens.refreshToken);\n el.setAttribute('bearer-token', tokens.token);\n}\n", "import {\n resolveConfig,\n validateMountOptions,\n type ResolvedConfig,\n} from './config';\nimport { createDebugLog, type DebugLog } from './debug';\nimport { BuilderError, configError } from './errors';\nimport {\n BuilderInstance,\n type InstanceContext,\n type SavedDesign,\n} from './instance';\nimport { assertEnvironmentAvailable, loadBuilder } from './loader';\nimport type { BuilderHostConfig, MountOptions } from './config';\n\nexport interface Builder {\n /** Optional preload: loads the builder code once per page. */\n load(): Promise<void>;\n /** Creates and inserts a builder. Later calls add more instances. */\n mount(options: MountOptions): Promise<BuilderInstance>;\n /** The most recently mounted builder that is still alive. */\n readonly current: BuilderInstance | undefined;\n /** Shows the current builder's UI. */\n open(): void;\n /** Saves the current builder's design; see BuilderInstance.save(). */\n save(): Promise<SavedDesign>;\n /** Calls session.get() again for the current builder. */\n refreshSession(): Promise<void>;\n /** Fetches a fresh license token once and applies it to every builder. */\n refreshLicense(): Promise<void>;\n /** Removes the shopper layer from every builder. */\n logout(): void;\n /** Destroys every builder this controller mounted. */\n destroy(): void;\n}\n\nclass BuilderController implements Builder {\n private readonly instances: BuilderInstance[] = [];\n private licenseRefresh?: Promise<void>;\n private readonly log: DebugLog;\n private readonly context: InstanceContext;\n\n constructor(private readonly config: ResolvedConfig) {\n this.log = createDebugLog(config.debug);\n this.context = {\n config,\n log: this.log,\n refreshLicense: () => this.refreshLicense().catch(() => undefined),\n onDestroyed: (instance) => {\n const index = this.instances.indexOf(instance);\n if (index >= 0) this.instances.splice(index, 1);\n },\n };\n }\n\n get current(): BuilderInstance | undefined {\n return this.instances[this.instances.length - 1];\n }\n\n load(): Promise<void> {\n return loadBuilder(this.config.environment, this.config.version);\n }\n\n async mount(options: MountOptions): Promise<BuilderInstance> {\n const valid = validateMountOptions(options);\n await this.load();\n const instance = await BuilderInstance.mount(this.context, valid);\n this.instances.push(instance);\n return instance;\n }\n\n open(): void {\n this.requireCurrent().open();\n }\n\n save(): Promise<SavedDesign> {\n const current = this.current;\n return current ? current.save() : Promise.reject(this.notMounted());\n }\n\n refreshSession(): Promise<void> {\n const current = this.current;\n return current\n ? current.refreshSession()\n : Promise.reject(this.notMounted());\n }\n\n refreshLicense(): Promise<void> {\n if (!this.licenseRefresh) {\n this.licenseRefresh = (async () => {\n try {\n const result = await this.config.session.refreshLicense();\n const token = result?.licenseToken;\n if (typeof token !== 'string' || token === '') {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'session.refreshLicense() must resolve { licenseToken }.'\n );\n }\n for (const instance of this.instances) instance.applyLicense(token);\n this.log('license refreshed');\n } catch (cause) {\n this.log('license refresh failed', { cause: String(cause) });\n throw cause instanceof BuilderError\n ? cause\n : new BuilderError(\n 'SESSION_FAILED',\n 'A fresh license token could not be fetched.',\n {\n retryable: true,\n cause,\n }\n );\n } finally {\n this.licenseRefresh = undefined;\n }\n })();\n }\n return this.licenseRefresh;\n }\n\n logout(): void {\n for (const instance of this.instances) instance.logout();\n this.log('logged out');\n }\n\n destroy(): void {\n for (const instance of [...this.instances]) instance.destroy();\n }\n\n private requireCurrent(): BuilderInstance {\n const current = this.current;\n if (!current) throw this.notMounted();\n return current;\n }\n\n private notMounted(): BuilderError {\n return configError('No builder is mounted. Call mount() first.');\n }\n}\n\n/**\n * Records the host's configuration and callbacks. Touches nothing on the page\n * until load() or mount(), so it is safe to call during server-side rendering.\n */\nexport function createBuilder(config: BuilderHostConfig): Builder {\n const resolved = resolveConfig(config);\n assertEnvironmentAvailable(resolved.environment);\n return new BuilderController(resolved);\n}\n", "/** Query parameter that marks \"back from the login page, resume the save\". */\nexport const RESUME_PARAM = 'tdn_resume';\n\n/** localStorage key where the builder indexes the drafts it kept on this device. */\nconst DRAFT_INDEX_KEY = 'tdn-draft-index';\n\n/**\n * A relative return path for the host's login redirect: the current page with\n * the resume marker. Only same-site paths, so the redirect can never become an\n * open redirect.\n */\nexport function loginReturnPath(): string {\n const url = new URL(window.location.href);\n url.searchParams.set(RESUME_PARAM, '1');\n return url.pathname + url.search + url.hash;\n}\n\n/** True for a path that stays on this site: starts with \"/\" but not \"//\" or \"/\\\". */\nexport function isSafeReturnPath(path: string): boolean {\n return /^\\/(?![/\\\\])/.test(path);\n}\n\n/**\n * True once, when the page was opened by a login redirect carrying the resume\n * marker. Strips the marker from the address bar.\n */\nexport function consumeResumeMarker(): boolean {\n if (typeof window === 'undefined') return false;\n const url = new URL(window.location.href);\n if (url.searchParams.get(RESUME_PARAM) !== '1') return false;\n url.searchParams.delete(RESUME_PARAM);\n window.history.replaceState(\n window.history.state,\n '',\n url.pathname + url.search + url.hash\n );\n return true;\n}\n\n/** True when the builder kept an unsaved draft of this 3DN product on this device. */\nexport function hasDraft(productId: string): boolean {\n if (typeof localStorage === 'undefined') return false;\n try {\n const index = JSON.parse(\n localStorage.getItem(DRAFT_INDEX_KEY) ?? '{}'\n ) as Record<string, { productId?: string } | undefined>;\n return Object.values(index).some((draft) => draft?.productId === productId);\n } catch {\n return false;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["BuilderError", "_BuilderError", "code", "message", "options", "_a", "_b", "SAVE_FAILURE_MESSAGES", "saveFailedError", "detail", "licenseError", "destroyedError", "configError", "DEFAULT_SAVE_TIMEOUT_MS", "DEFAULT_EXPORT_TIMEOUT_MS", "CONFIG_KEYS", "MOUNT_KEYS", "VERSION_PATTERN", "INSTALLATION_KEY_PATTERN", "isExactVersion", "value", "isRecord", "isFunction", "isPositiveNumber", "containsSecret", "depth", "v", "rejectUnknownKeys", "allowed", "where", "key", "configError", "resolveConfig", "config", "_a", "_b", "_c", "environment", "version", "saveTimeoutMs", "session", "login", "customization", "validateMountOptions", "options", "target", "hostProductId", "mode", "designId", "validateObservabilityContext", "externalExport", "parsed", "validateSession", "fail", "what", "BuilderError", "hasToken", "hasRefresh", "validateShopperTokens", "tokens", "TOKEN_KEY", "redact", "value", "depth", "v", "out", "key", "createDebugLog", "enabled", "message", "detail", "BUILDER_TAG", "BUNDLE_HOSTS", "DEFINE_TIMEOUT_MS", "STATE_KEY", "pageState", "holder", "state", "isBrowser", "assertEnvironmentAvailable", "environment", "configError", "loadFailed", "message", "cause", "BuilderError", "resolveVersion", "host", "requested", "response", "info", "version", "isExactVersion", "injectScript", "src", "resolve", "reject", "script", "whenDefined", "timer", "assertCompatible", "loadBuilder", "error", "deferred", "resolve", "reject", "promise", "res", "rej", "PausableTimer", "ms", "onExpire", "BuilderInstance", "_BuilderInstance", "ctx", "options", "session", "raw", "cause", "BuilderError", "validateSession", "instance", "el", "BUILDER_TAG", "setShopperTokens", "on", "name", "handler", "listener", "event", "THREED_BUILDER_LOGIN_REQUIRED_EVENT", "d", "_a", "THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT", "THREED_BUILDER_LICENSE_ERROR_EVENT", "THREED_BUILDER_DESIGN_SAVED_EVENT", "THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT", "saveFailedError", "THREED_BUILDER_IMAGES_SAVED_EVENT", "THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT", "THREED_BUILDER_PRODUCT_CLONED_EVENT", "THREED_BUILDER_CLOSE_REQUESTED_EVENT", "_b", "THREED_BUILDER_STATUS_EVENT", "THREED_BUILDER_EXPORT_REQUESTED_EVENT", "dispatchOpenThreedBuilder", "destroyedError", "run", "dispatchSaveRequest", "epoch", "controller", "context", "validateObservabilityContext", "licenseToken", "action", "login", "dispatchLoginCancelled", "stale", "redirected", "tokens", "validateShopperTokens", "choice", "outcome", "pending", "detail", "error", "licenseError", "images", "saved", "updated", "wait", "callback", "link", "customization", "request", "external", "timedOut", "timer", "DEFAULT_EXPORT_TIMEOUT_MS", "abort", "_", "files", "dispatchExportComplete", "code", "THREED_BUILDER_EXPORT_COMPLETE_EVENT", "BuilderController", "config", "createDebugLog", "instance", "index", "loadBuilder", "options", "valid", "validateMountOptions", "BuilderInstance", "current", "result", "token", "BuilderError", "cause", "configError", "createBuilder", "resolved", "resolveConfig", "assertEnvironmentAvailable", "RESUME_PARAM", "DRAFT_INDEX_KEY", "loginReturnPath", "url", "isSafeReturnPath", "path", "consumeResumeMarker", "hasDraft", "productId", "_a", "index", "draft"]
|
|
7
7
|
}
|
package/package.json
CHANGED
package/testing.d.ts
CHANGED
|
@@ -45,15 +45,22 @@ interface FakeBuilderElement extends HTMLElement {
|
|
|
45
45
|
readonly attributesAtConnect: Record<string, string>;
|
|
46
46
|
/** Emits any builder → host event, with the builder's own flags. */
|
|
47
47
|
emit(type: string, detail?: unknown): void;
|
|
48
|
-
/** A successful save of the current product. */
|
|
48
|
+
/** A successful save of the current product, then its images. */
|
|
49
49
|
emitSaved(version?: number): void;
|
|
50
50
|
/**
|
|
51
|
-
* A first save that cloned the product: tdn-design-saved
|
|
52
|
-
*
|
|
53
|
-
* thumbnail upload.
|
|
51
|
+
* A first save that cloned the product: tdn-design-saved and
|
|
52
|
+
* tdn-images-saved with the clone id, then tdn-product-cloned on the next
|
|
53
|
+
* task, as the builder does after its thumbnail upload.
|
|
54
54
|
*/
|
|
55
55
|
emitSavedAsClone(newProductId: string, version?: number, asCopy?: boolean): Promise<void>;
|
|
56
56
|
emitSaveFailed(reason: DesignSaveFailureReason, retryable?: boolean): void;
|
|
57
|
+
/**
|
|
58
|
+
* The product's images now show `version` (a save's thumbnail upload, or
|
|
59
|
+
* Close after autosave). `productId` defaults to the current product.
|
|
60
|
+
*/
|
|
61
|
+
emitImagesSaved(version?: number, productId?: string): void;
|
|
62
|
+
/** The product's images could not be refreshed after a save. */
|
|
63
|
+
emitImagesSaveFailed(version?: number, productId?: string): void;
|
|
57
64
|
emitLoginRequired(action?: LoginRequiredAction): void;
|
|
58
65
|
emitLicenseTokenRequired(): void;
|
|
59
66
|
emitLicenseError(code: LicenseErrorCode): void;
|
package/testing.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a,c as
|
|
1
|
+
import{a,c as m,d as E,e as u,g as R,h as c,i as l,k as _,l as r,m as v,n as D,o as T,p as L,q as I,s as p}from"./chunks/chunk-XQRNTDNA.js";var s="threed-builder",S=[a,I,l,u],h=new Set([E]);function A(){class o extends HTMLElement{constructor(){super();this.received=[];this.attributesAtConnect={};for(let e of S)this.addEventListener(e,t=>{t instanceof CustomEvent&&t.fromFake||this.received.push({type:e,detail:t.detail})})}connectedCallback(){this.attributesAtConnect=Object.fromEntries(Array.from(this.attributes,e=>[e.name,e.value])),this.emit(m,{status:"idle"})}emit(e,t){let i=!h.has(e),n=new CustomEvent(e,{bubbles:i,composed:i,detail:t});n.fromFake=!0,this.dispatchEvent(n)}emitSaved(e=1){var t;this.emit(r,{productId:(t=this.getAttribute("product"))!=null?t:"",version:e}),this.emitImagesSaved(e)}async emitSavedAsClone(e,t=1,i=!1){var d;let n=(d=this.getAttribute("product"))!=null?d:"";this.emit(r,{productId:e,version:t}),this.emitImagesSaved(t,e),await new Promise(g=>setTimeout(g,0)),this.emit(R,{originalProductId:n,newProductId:e,name:"Customized",asCopy:i})}emitSaveFailed(e,t=!1){var i;this.emit(T,{productId:(i=this.getAttribute("product"))!=null?i:void 0,reason:e,retryable:t})}emitImagesSaved(e=1,t=(i=>(i=this.getAttribute("product"))!=null?i:"")()){this.emit(v,{productId:t,version:e})}emitImagesSaveFailed(e=1,t=(i=>(i=this.getAttribute("product"))!=null?i:"")()){this.emit(D,{productId:t,version:e})}emitLoginRequired(e="save"){this.emit(c,{action:e})}emitLicenseTokenRequired(){this.emit(L)}emitLicenseError(e){this.emit(_,{code:e})}emitCloseRequested(e){var t;this.emit(p,{productId:(t=this.getAttribute("product"))!=null?t:void 0,hasUnsavedChanges:e})}emitExportRequested(e){this.emit(E,e)}}customElements.define(s,o)}function f(){let o=customElements.get(s);if(o){if("prototype"in o&&"emitSaved"in o.prototype)return;throw new Error("A real <threed-builder> is already defined on this page.")}A()}function B(o=document){return Array.from(o.querySelectorAll(s))}export{B as findFakeBuilders,f as installFakeBuilder};
|
|
2
2
|
//# sourceMappingURL=testing.js.map
|
package/testing.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../libs/builder-host/src/testing.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n THREED_BUILDER_CLOSE_REQUESTED_EVENT,\n THREED_BUILDER_DESIGN_SAVED_EVENT,\n THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,\n THREED_BUILDER_EXPORT_COMPLETE_EVENT,\n THREED_BUILDER_EXPORT_REQUESTED_EVENT,\n THREED_BUILDER_LICENSE_ERROR_EVENT,\n THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,\n THREED_BUILDER_LOGIN_CANCELLED_EVENT,\n THREED_BUILDER_LOGIN_REQUIRED_EVENT,\n THREED_BUILDER_OPEN_EVENT,\n THREED_BUILDER_PRODUCT_CLONED_EVENT,\n THREED_BUILDER_SAVE_REQUEST_EVENT,\n THREED_BUILDER_STATUS_EVENT,\n type DesignSaveFailureReason,\n type ExportRequestedDetail,\n type LicenseErrorCode,\n type LoginRequiredAction,\n} from '@three-dn/constants';\n\nconst TAG = 'threed-builder';\n\n/** A host \u2192 builder event the fake received. */\nexport interface ReceivedCommand {\n type: string;\n detail?: unknown;\n}\n\n/**\n * A stand-in for `<threed-builder>` that speaks the builder's event contract\n * without loading the real builder. Install it before mount() and drive it\n * from tests: it records what the host sends and emits what the builder would.\n */\nexport interface FakeBuilderElement extends HTMLElement {\n /** Host \u2192 builder events, in order. */\n readonly received: ReceivedCommand[];\n /** Attributes as they were when the element was inserted into the page. */\n readonly attributesAtConnect: Record<string, string>;\n /** Emits any builder \u2192 host event, with the builder's own flags. */\n emit(type: string, detail?: unknown): void;\n /** A successful save of the current product. */\n emitSaved(version?: number): void;\n /**\n * A first save that cloned the product: tdn-design-saved with the clone id
|
|
5
|
-
"mappings": "
|
|
6
|
-
"names": ["TAG", "HOST_COMMANDS", "THREED_BUILDER_OPEN_EVENT", "THREED_BUILDER_SAVE_REQUEST_EVENT", "THREED_BUILDER_LOGIN_CANCELLED_EVENT", "THREED_BUILDER_EXPORT_COMPLETE_EVENT", "NON_BUBBLING", "THREED_BUILDER_EXPORT_REQUESTED_EVENT", "defineFakeElement", "FakeThreedBuilder", "type", "event", "a", "THREED_BUILDER_STATUS_EVENT", "detail", "bubbles", "version", "_a", "THREED_BUILDER_DESIGN_SAVED_EVENT", "newProductId", "asCopy", "originalProductId", "resolve", "THREED_BUILDER_PRODUCT_CLONED_EVENT", "reason", "retryable", "THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT", "action", "THREED_BUILDER_LOGIN_REQUIRED_EVENT", "THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT", "code", "THREED_BUILDER_LICENSE_ERROR_EVENT", "hasUnsavedChanges", "THREED_BUILDER_CLOSE_REQUESTED_EVENT", "installFakeBuilder", "existing", "findFakeBuilders", "root"]
|
|
4
|
+
"sourcesContent": ["import {\n THREED_BUILDER_CLOSE_REQUESTED_EVENT,\n THREED_BUILDER_DESIGN_SAVED_EVENT,\n THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,\n THREED_BUILDER_EXPORT_COMPLETE_EVENT,\n THREED_BUILDER_EXPORT_REQUESTED_EVENT,\n THREED_BUILDER_IMAGES_SAVED_EVENT,\n THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT,\n THREED_BUILDER_LICENSE_ERROR_EVENT,\n THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,\n THREED_BUILDER_LOGIN_CANCELLED_EVENT,\n THREED_BUILDER_LOGIN_REQUIRED_EVENT,\n THREED_BUILDER_OPEN_EVENT,\n THREED_BUILDER_PRODUCT_CLONED_EVENT,\n THREED_BUILDER_SAVE_REQUEST_EVENT,\n THREED_BUILDER_STATUS_EVENT,\n type DesignSaveFailureReason,\n type ExportRequestedDetail,\n type LicenseErrorCode,\n type LoginRequiredAction,\n} from '@three-dn/constants';\n\nconst TAG = 'threed-builder';\n\n/** A host \u2192 builder event the fake received. */\nexport interface ReceivedCommand {\n type: string;\n detail?: unknown;\n}\n\n/**\n * A stand-in for `<threed-builder>` that speaks the builder's event contract\n * without loading the real builder. Install it before mount() and drive it\n * from tests: it records what the host sends and emits what the builder would.\n */\nexport interface FakeBuilderElement extends HTMLElement {\n /** Host \u2192 builder events, in order. */\n readonly received: ReceivedCommand[];\n /** Attributes as they were when the element was inserted into the page. */\n readonly attributesAtConnect: Record<string, string>;\n /** Emits any builder \u2192 host event, with the builder's own flags. */\n emit(type: string, detail?: unknown): void;\n /** A successful save of the current product, then its images. */\n emitSaved(version?: number): void;\n /**\n * A first save that cloned the product: tdn-design-saved and\n * tdn-images-saved with the clone id, then tdn-product-cloned on the next\n * task, as the builder does after its thumbnail upload.\n */\n emitSavedAsClone(\n newProductId: string,\n version?: number,\n asCopy?: boolean\n ): Promise<void>;\n emitSaveFailed(reason: DesignSaveFailureReason, retryable?: boolean): void;\n /**\n * The product's images now show `version` (a save's thumbnail upload, or\n * Close after autosave). `productId` defaults to the current product.\n */\n emitImagesSaved(version?: number, productId?: string): void;\n /** The product's images could not be refreshed after a save. */\n emitImagesSaveFailed(version?: number, productId?: string): void;\n emitLoginRequired(action?: LoginRequiredAction): void;\n emitLicenseTokenRequired(): void;\n emitLicenseError(code: LicenseErrorCode): void;\n emitCloseRequested(hasUnsavedChanges: boolean): void;\n emitExportRequested(detail: ExportRequestedDetail): void;\n}\n\nconst HOST_COMMANDS = [\n THREED_BUILDER_OPEN_EVENT,\n THREED_BUILDER_SAVE_REQUEST_EVENT,\n THREED_BUILDER_LOGIN_CANCELLED_EVENT,\n THREED_BUILDER_EXPORT_COMPLETE_EVENT,\n];\n\n/** Builder \u2192 host events that do not bubble (an Angular output). */\nconst NON_BUBBLING = new Set<string>([THREED_BUILDER_EXPORT_REQUESTED_EVENT]);\n\nfunction defineFakeElement(): void {\n class FakeThreedBuilder extends HTMLElement implements FakeBuilderElement {\n readonly received: ReceivedCommand[] = [];\n attributesAtConnect: Record<string, string> = {};\n\n constructor() {\n super();\n for (const type of HOST_COMMANDS) {\n this.addEventListener(type, (event) => {\n // Ignore the fake's own emits; only commands the host dispatched.\n if (event instanceof CustomEvent && (event as FakeEvent).fromFake)\n return;\n this.received.push({ type, detail: (event as CustomEvent).detail });\n });\n }\n }\n\n connectedCallback(): void {\n this.attributesAtConnect = Object.fromEntries(\n Array.from(this.attributes, (a) => [a.name, a.value])\n );\n // The real builder reports `idle` as soon as it starts.\n this.emit(THREED_BUILDER_STATUS_EVENT, { status: 'idle' });\n }\n\n emit(type: string, detail?: unknown): void {\n const bubbles = !NON_BUBBLING.has(type);\n const event = new CustomEvent(type, {\n bubbles,\n composed: bubbles,\n detail,\n }) as FakeEvent;\n event.fromFake = true;\n this.dispatchEvent(event);\n }\n\n emitSaved(version = 1): void {\n this.emit(THREED_BUILDER_DESIGN_SAVED_EVENT, {\n productId: this.getAttribute('product') ?? '',\n version,\n });\n this.emitImagesSaved(version);\n }\n\n async emitSavedAsClone(\n newProductId: string,\n version = 1,\n asCopy = false\n ): Promise<void> {\n const originalProductId = this.getAttribute('product') ?? '';\n this.emit(THREED_BUILDER_DESIGN_SAVED_EVENT, {\n productId: newProductId,\n version,\n });\n this.emitImagesSaved(version, newProductId);\n await new Promise((resolve) => setTimeout(resolve, 0));\n this.emit(THREED_BUILDER_PRODUCT_CLONED_EVENT, {\n originalProductId,\n newProductId,\n name: 'Customized',\n asCopy,\n });\n }\n\n emitSaveFailed(reason: DesignSaveFailureReason, retryable = false): void {\n this.emit(THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT, {\n productId: this.getAttribute('product') ?? undefined,\n reason,\n retryable,\n });\n }\n\n emitImagesSaved(\n version = 1,\n productId = this.getAttribute('product') ?? ''\n ): void {\n this.emit(THREED_BUILDER_IMAGES_SAVED_EVENT, { productId, version });\n }\n\n emitImagesSaveFailed(\n version = 1,\n productId = this.getAttribute('product') ?? ''\n ): void {\n this.emit(THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT, {\n productId,\n version,\n });\n }\n\n emitLoginRequired(action: LoginRequiredAction = 'save'): void {\n this.emit(THREED_BUILDER_LOGIN_REQUIRED_EVENT, { action });\n }\n\n emitLicenseTokenRequired(): void {\n this.emit(THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT);\n }\n\n emitLicenseError(code: LicenseErrorCode): void {\n this.emit(THREED_BUILDER_LICENSE_ERROR_EVENT, { code });\n }\n\n emitCloseRequested(hasUnsavedChanges: boolean): void {\n this.emit(THREED_BUILDER_CLOSE_REQUESTED_EVENT, {\n productId: this.getAttribute('product') ?? undefined,\n hasUnsavedChanges,\n });\n }\n\n emitExportRequested(detail: ExportRequestedDetail): void {\n this.emit(THREED_BUILDER_EXPORT_REQUESTED_EVENT, detail);\n }\n }\n customElements.define(TAG, FakeThreedBuilder);\n}\n\ninterface FakeEvent extends CustomEvent {\n fromFake?: boolean;\n}\n\n/**\n * Registers the fake as `<threed-builder>`. mount() then uses it instead of\n * loading the real builder. Call once per test environment.\n */\nexport function installFakeBuilder(): void {\n const existing = customElements.get(TAG);\n if (existing) {\n if ('prototype' in existing && 'emitSaved' in existing.prototype) return;\n throw new Error('A real <threed-builder> is already defined on this page.');\n }\n defineFakeElement();\n}\n\n/** Every fake builder currently in the document, in document order. */\nexport function findFakeBuilders(\n root: ParentNode = document\n): FakeBuilderElement[] {\n return Array.from(root.querySelectorAll(TAG)) as FakeBuilderElement[];\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["TAG", "HOST_COMMANDS", "THREED_BUILDER_OPEN_EVENT", "THREED_BUILDER_SAVE_REQUEST_EVENT", "THREED_BUILDER_LOGIN_CANCELLED_EVENT", "THREED_BUILDER_EXPORT_COMPLETE_EVENT", "NON_BUBBLING", "THREED_BUILDER_EXPORT_REQUESTED_EVENT", "defineFakeElement", "FakeThreedBuilder", "type", "event", "a", "THREED_BUILDER_STATUS_EVENT", "detail", "bubbles", "version", "_a", "THREED_BUILDER_DESIGN_SAVED_EVENT", "newProductId", "asCopy", "originalProductId", "resolve", "THREED_BUILDER_PRODUCT_CLONED_EVENT", "reason", "retryable", "THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT", "productId", "THREED_BUILDER_IMAGES_SAVED_EVENT", "_b", "THREED_BUILDER_IMAGES_SAVE_FAILED_EVENT", "action", "THREED_BUILDER_LOGIN_REQUIRED_EVENT", "THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT", "code", "THREED_BUILDER_LICENSE_ERROR_EVENT", "hasUnsavedChanges", "THREED_BUILDER_CLOSE_REQUESTED_EVENT", "installFakeBuilder", "existing", "findFakeBuilders", "root"]
|
|
7
7
|
}
|
package/chunks/chunk-F4FUPJ4Y.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
var l="tdn-open-builder";function r(e){e.dispatchEvent(new CustomEvent(l,{bubbles:!0,composed:!0}))}var d="tdn-status-change";var c="exportRequested",i="tdn-export-complete";function g(e,a){e.dispatchEvent(new CustomEvent(i,{bubbles:!1,composed:!1,detail:a}))}var E="tdn-product-cloned";var b="tdn-login-required";var t="tdn-login-cancelled";function f(e,a={}){e.dispatchEvent(new CustomEvent(t,{bubbles:!1,composed:!1,detail:a}))}var u="tdn-license-error";var p="tdn-design-saved";var F="tdn-design-save-failed";var m="tdn-license-token-required";var n="tdn-save-request";function C(e){e.dispatchEvent(new CustomEvent(n,{bubbles:!1,composed:!1}))}var A="tdn-close-requested";var s=[{name:"Grays",colors:["#FFFFFF","#F8F9FA","#E9ECEF","#DEE2E6","#CED4DA","#ADB5BD","#6C757D","#495057","#343A40","#212529","#1A1A1A","#000000"]},{name:"Reds",colors:["#FFE6E6","#FFCCCC","#FF9999","#FF6666","#FF3333","#FF0000","#E60000","#CC0000","#990000","#660000","#330000","#1A0000"]},{name:"Oranges",colors:["#FFF4E6","#FFE6CC","#FFCC99","#FFB366","#FF9933","#FF8000","#E66A00","#CC5200","#993D00","#662900","#331400","#1A0A00"]},{name:"Yellows",colors:["#FFFEF0","#FFFDE6","#FFFACC","#FFF799","#FFF266","#FFED33","#E6D100","#CCBA00","#998C00","#665D00","#332E00","#1A1700"]},{name:"Greens",colors:["#F0FFF0","#E6FFE6","#CCFFCC","#99FF99","#66FF66","#33FF33","#00E600","#00CC00","#009900","#006600","#003300","#001A00"]},{name:"Blues",colors:["#F0F8FF","#E6F3FF","#CCE7FF","#99CFFF","#66B7FF","#339FFF","#0080E6","#0066CC","#004D99","#003366","#001A33","#000D1A"]},{name:"Purples",colors:["#F8F0FF","#F0E6FF","#E0CCFF","#C199FF","#A266FF","#8333FF","#6600E6","#5200CC","#3D0099","#290066","#140033","#0A001A"]},{name:"Pinks",colors:["#FFE6F0","#FFCCE0","#FF99C1","#FF66A2","#FF3383","#FF0064","#E60057","#CC0051","#99003E","#66002A","#330015","#1A000A"]},{name:"Teals",colors:["#F0FFFF","#E0FFFF","#B2FFFF","#66FFFF","#1AFFFF","#00CCCC","#00B3B3","#009999","#006666","#003333","#001A1A","#000D0D"]},{name:"Cyans",colors:["#E0F7FA","#B2EBF2","#80DEEA","#4DD0E1","#26C6DA","#00BCD4","#00ACC1","#0097A7","#00796B","#004D40","#00251A","#00120D"]},{name:"Indigos",colors:["#E8EAF6","#C5CAE9","#9FA8DA","#7986CB","#5C6BC0","#3F51B5","#303F9F","#283593","#1A237E","#0D1442","#060A21","#030510"]},{name:"Violets",colors:["#F3E5F5","#E1BEE7","#CE93D8","#BA68C8","#AB47BC","#9C27B0","#8E24AA","#7B1FA2","#6A1B9A","#4A148C","#2E1065","#1A0D3D"]},{name:"Magentas",colors:["#FCE4EC","#F8BBD9","#F48FB1","#F06292","#EC407A","#E91E63","#D81B60","#C2185B","#AD1457","#880E4F","#4A0A2A","#2D0519"]},{name:"Limes",colors:["#F9FBE7","#F0F4C3","#E6EE9C","#DCE775","#D4E157","#CDDC39","#C0CA33","#AFB42B","#9E9D24","#827717","#4A4514","#2D2A0C"]},{name:"Ambers",colors:["#FFF8E1","#FFECB3","#FFE082","#FFD54F","#FFCA28","#FFC107","#FFB300","#FFA000","#FF8F00","#FF6F00","#994200","#5A2700"]},{name:"Deep Oranges",colors:["#FBE9E7","#FFCCBC","#FFAB91","#FF8A65","#FF7043","#FF5722","#F4511E","#E64A19","#D84315","#BF360C","#7A2408","#4A1605"]},{name:"Light Blues",colors:["#E1F5FE","#B3E5FC","#81D4FA","#4FC3F7","#29B6F6","#03A9F4","#039BE5","#0288D1","#0277BD","#01579B","#003A66","#00223D"]},{name:"Light Greens",colors:["#E8F5E8","#C8E6C9","#A5D6A7","#81C784","#66BB6A","#4CAF50","#43A047","#388E3C","#2E7D32","#1B5E20","#0F3A14","#09230C"]},{name:"Browns",colors:["#F5F5DC","#F0E68C","#D2B48C","#BC8F8F","#A0522D","#8B4513","#654321","#4A2C2A","#2F1B14","#1A0E0A","#0F0705","#090503"]},{name:"Grey-Blues",colors:["#ECEFF1","#CFD8DC","#B0BEC5","#90A4AE","#78909C","#607D8B","#546E7A","#455A64","#37474F","#263238","#1A2126","#0F1316"]}],T=s.flatMap(e=>e.colors);var _=1024*1024;var L=600*1024;var o=["active","suspended","revoked"];var w=[...o,"expired"];var H=1440*60*1e3;export{l as a,r as b,d as c,c as d,i as e,g as f,E as g,b as h,t as i,f as j,u as k,p as l,F as m,m as n,n as o,C as p,A as q};
|
|
2
|
-
//# sourceMappingURL=chunk-F4FUPJ4Y.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../../libs/constants/src/lib/builder-webcomponent.ts", "../../../../libs/constants/src/lib/colors.ts", "../../../../libs/constants/src/lib/constants.ts", "../../../../libs/constants/src/lib/license.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Programmatic open for `<threed-builder>` (avoids synthetic `click()` / ghost clicks).\n * Dispatched on the custom element host with composed path so the inner app can subscribe.\n */\nexport const THREED_BUILDER_OPEN_EVENT = 'tdn-open-builder' as const;\n\nexport function dispatchOpenThreedBuilder(host: HTMLElement): void {\n host.dispatchEvent(\n new CustomEvent(THREED_BUILDER_OPEN_EVENT, {\n bubbles: true,\n composed: true,\n })\n );\n}\n\n/**\n * Builder lifecycle statuses emitted via `tdn-status-change` CustomEvent.\n */\nexport type BuilderStatus = 'idle' | 'opening' | 'loading' | 'ready' | 'error';\n\nexport const THREED_BUILDER_STATUS_EVENT = 'tdn-status-change' as const;\n\nexport interface BuilderStatusDetail {\n status: BuilderStatus;\n step?: string;\n message?: string;\n error?: string;\n}\n\nexport function dispatchBuilderStatus(\n host: HTMLElement,\n detail: BuilderStatusDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<BuilderStatusDetail>(THREED_BUILDER_STATUS_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * External export bridge.\n *\n * When `<threed-builder external-export=\"true\">` is set, the webcomponent\n * skips its built-in admin POST and instead emits `exportRequested` so the\n * embedding host can run the export through its own backend (e.g. the\n * store app proxy). The host then hands the resulting file URLs back via\n * `dispatchExportComplete`.\n *\n * NOT FOR A DIELINE PRODUCT (task 3.2, ruling P3-6). A dieline's print files\n * come from ONE pure exporter run over the design's own immutable snapshot\n * (Document 1 sections 12 and 13); a host backend cannot produce that file,\n * and a file it produces instead is a second drawing with nothing holding it\n * to what the customer approved on screen. The webcomponent therefore routes\n * a DIELINE export to the admin API whatever this attribute says, and emits\n * `exportRequested` for GLB products only. A host that needs its own pipeline\n * for dielines needs a different contract, not this one.\n */\nexport const THREED_BUILDER_EXPORT_REQUESTED_EVENT = 'exportRequested' as const;\n\nexport interface ExportRequestedDetail {\n productId: string;\n formats: string[];\n surfaces: string[];\n /**\n * The export mode, `'proof'` or `'production-artwork'` (Document 1 section\n * 12). ADDITIVE in task 1.5b: a host that ignores it keeps working exactly\n * as before. Since task 3.2 only GLB products reach this event, and a GLB\n * export is mode-independent \u2014 so the field is carried for the host's own\n * bookkeeping, not to choose a drawing.\n */\n mode?: string;\n}\n\nexport const THREED_BUILDER_EXPORT_COMPLETE_EVENT =\n 'tdn-export-complete' as const;\n\nexport interface ExportFile {\n url: string;\n format: string;\n surface: string;\n}\n\nexport interface ExportCompleteDetail {\n files: ExportFile[];\n}\n\nexport function dispatchExportComplete(\n host: HTMLElement,\n detail: ExportCompleteDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<ExportCompleteDetail>(\n THREED_BUILDER_EXPORT_COMPLETE_EVENT,\n {\n bubbles: false,\n composed: false,\n detail,\n }\n )\n );\n}\n\n/**\n * Fired after a save that created a new product: \"Save as copy\", or a host\n * shopper's Save of a product they don't own (their first save). The builder\n * has switched to editing the new product. The host should persist\n * `newProductId` so subsequent loads of the builder open it.\n */\nexport const THREED_BUILDER_PRODUCT_CLONED_EVENT =\n 'tdn-product-cloned' as const;\n\nexport interface ProductClonedDetail {\n originalProductId: string;\n newProductId: string;\n name: string;\n /**\n * The user asked for a copy (\"Save as copy\", \"Save as my copy\", \"keep both\"\n * after a conflict) and expects to keep the original too. `false` when Save\n * had to fork because the user doesn't own the product: the copy takes the\n * original's place.\n */\n asCopy: boolean;\n}\n\nexport function dispatchProductCloned(\n host: HTMLElement,\n detail: ProductClonedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<ProductClonedDetail>(THREED_BUILDER_PRODUCT_CLONED_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * Fired when a protected action needs a logged-in user (save / screenshot /\n * export / upload / share), or \u2014 with `action: 'session'` \u2014 when the builder's\n * session could not be recovered (refresh failed). The host shows its own\n * login if its user is not logged in, has its BACKEND mint a token pair from\n * its own session, and sets `bearer-token` / `refresh-token` on the element;\n * the builder then resumes the pending action. If the user dismisses the login\n * or the mint fails, the host dispatches `tdn-login-cancelled` instead.\n * One event per login round-trip: further gated clicks while a login is\n * pending do not fire it again.\n */\nexport const THREED_BUILDER_LOGIN_REQUIRED_EVENT =\n 'tdn-login-required' as const;\n\nexport type LoginRequiredAction =\n | 'save'\n | 'screenshot'\n | 'export'\n | 'upload'\n | 'share'\n | 'session';\n\nexport interface LoginRequiredDetail {\n action: LoginRequiredAction;\n}\n\nexport function dispatchLoginRequired(\n host: HTMLElement,\n detail: LoginRequiredDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<LoginRequiredDetail>(THREED_BUILDER_LOGIN_REQUIRED_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * Host -> builder: the login the builder asked for (`tdn-login-required`) will\n * not happen \u2014 the user closed the host's login, or the host's mint failed.\n * The builder drops the pending action, so a stale Save never runs after a\n * later, unrelated login. A pending Save reports `tdn-design-save-failed`\n * with reason `login-cancelled`.\n */\nexport const THREED_BUILDER_LOGIN_CANCELLED_EVENT =\n 'tdn-login-cancelled' as const;\n\nexport interface LoginCancelledDetail {\n action?: LoginRequiredAction;\n}\n\nexport function dispatchLoginCancelled(\n host: HTMLElement,\n detail: LoginCancelledDetail = {}\n): void {\n host.dispatchEvent(\n new CustomEvent<LoginCancelledDetail>(\n THREED_BUILDER_LOGIN_CANCELLED_EVENT,\n { bubbles: false, composed: false, detail }\n )\n );\n}\n\n/**\n * Builder -> host: the builder cannot work on this page because of the\n * license, not because of the user. Fired once.\n * - `ORIGIN_NOT_ALLOWED`: this page's hostname is not the license's domain or\n * an alias. Do not answer it with a new token: a fresh mint for the same\n * page is refused the same way. Fix the license (add the hostname as an\n * alias) instead.\n * - `LICENSE_TOKEN_INVALID`: the `license-token` was rejected, the builder\n * asked for a fresh one (`tdn-license-token-required`), and the fresh one\n * was rejected too (or never came) \u2014 the license is suspended, revoked or\n * expired, or the host is minting against the wrong installation.\n */\nexport const THREED_BUILDER_LICENSE_ERROR_EVENT = 'tdn-license-error' as const;\n\nexport type LicenseErrorCode = 'ORIGIN_NOT_ALLOWED' | 'LICENSE_TOKEN_INVALID';\n\nexport interface LicenseErrorDetail {\n code: LicenseErrorCode;\n}\n\nexport function dispatchLicenseError(\n host: HTMLElement,\n detail: LicenseErrorDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<LicenseErrorDetail>(THREED_BUILDER_LICENSE_ERROR_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * The design was saved to 3dn. Fired as soon as the save is stored (before\n * the thumbnail refresh), and after \"Save as copy\" too (then followed by\n * `tdn-product-cloned`). This \u2014 not `tdn-status-change` `ready`, which also\n * fires after every load step \u2014 is the signal a host should continue on\n * (add to cart, show a confirmation).\n */\nexport const THREED_BUILDER_DESIGN_SAVED_EVENT = 'tdn-design-saved' as const;\n\nexport interface DesignSavedDetail {\n productId: string;\n /** The stored serialized version after this save. */\n version: number;\n}\n\nexport function dispatchDesignSaved(\n host: HTMLElement,\n detail: DesignSavedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<DesignSavedDetail>(THREED_BUILDER_DESIGN_SAVED_EVENT, {\n bubbles: true,\n composed: true,\n detail,\n })\n );\n}\n\n/**\n * Why a save did not happen:\n * - `conflict`: someone saved first; the builder shows its conflict dialog.\n * - `forbidden`: this user may not write to the product; the builder offers\n * request-access or save-as-copy.\n * - `unauthorized`: login recovery ran and the save was still refused.\n * - `login-cancelled`: the host sent `tdn-login-cancelled` for a pending save.\n * - `installation-inactive`: the host installation is not active (setup\n * problem on the host side, not a user error).\n * - `license`: refused with `ORIGIN_NOT_ALLOWED` \u2014 this page isn't licensed,\n * not a permissions or session problem; see `tdn-license-error`.\n * - `error`: network or server failure.\n */\nexport type DesignSaveFailureReason =\n | 'conflict'\n | 'forbidden'\n | 'unauthorized'\n | 'login-cancelled'\n | 'installation-inactive'\n | 'license'\n | 'error';\n\n/**\n * A save the user asked for did not happen, after any retry and login\n * recovery ran out. Never fired while a login the builder asked for is still\n * pending. The builder has already told the user; the host may add its own\n * handling and, when `retryable`, let the user try again.\n */\nexport const THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT =\n 'tdn-design-save-failed' as const;\n\nexport interface DesignSaveFailedDetail {\n productId?: string;\n reason: DesignSaveFailureReason;\n retryable: boolean;\n}\n\nexport function dispatchDesignSaveFailed(\n host: HTMLElement,\n detail: DesignSaveFailedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<DesignSaveFailedDetail>(\n THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,\n { bubbles: true, composed: true, detail }\n )\n );\n}\n\n/**\n * Builder -> host: the `license-token` (the no-user token from the host\n * backend's `POST /plugin/license-token`) was rejected \u2014 it expired, or the\n * installation's key was rotated. The host fetches a fresh license token from\n * its backend and sets `license-token` again; the builder holds the failed\n * request and retries it once with the new token. This is NEVER a login\n * prompt: answer it without involving the user. A second rejection ends in\n * `tdn-license-error` `LICENSE_TOKEN_INVALID`.\n */\nexport const THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT =\n 'tdn-license-token-required' as const;\n\nexport function dispatchLicenseTokenRequired(host: HTMLElement): void {\n host.dispatchEvent(\n new CustomEvent(THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT, {\n bubbles: true,\n composed: true,\n })\n );\n}\n\n/**\n * Host -> builder: save the design now, exactly as if the user pressed Save\n * (login gate, save-as-copy for a product the user does not own, and the\n * usual `tdn-design-saved` / `tdn-design-save-failed` outcome). A host sends\n * it before Add to cart when the design has not been saved yet, then waits\n * for one of those two events.\n */\nexport const THREED_BUILDER_SAVE_REQUEST_EVENT = 'tdn-save-request' as const;\n\nexport function dispatchSaveRequest(host: HTMLElement): void {\n host.dispatchEvent(\n new CustomEvent(THREED_BUILDER_SAVE_REQUEST_EVENT, {\n bubbles: false,\n composed: false,\n })\n );\n}\n\n/**\n * Builder -> host: the user pressed the builder's close button. The builder\n * does nothing else \u2014 it stays open and keeps its state. The host decides\n * what closing means on its page (hide an overlay, navigate back, ask first\n * when `hasUnsavedChanges`) and removes or hides the element itself.\n */\nexport const THREED_BUILDER_CLOSE_REQUESTED_EVENT =\n 'tdn-close-requested' as const;\n\nexport interface CloseRequestedDetail {\n productId?: string;\n /**\n * What is on screen differs from what was last saved (manual save or\n * autosave), in 2D or 3D: elements, the dieline/board, finish styles, or a\n * switched design. Opening and closing the box (fold) does not count, and\n * an edit undone by hand is no change.\n */\n hasUnsavedChanges: boolean;\n}\n\nexport function dispatchCloseRequested(\n host: HTMLElement,\n detail: CloseRequestedDetail\n): void {\n host.dispatchEvent(\n new CustomEvent<CloseRequestedDetail>(\n THREED_BUILDER_CLOSE_REQUESTED_EVENT,\n { bubbles: true, composed: true, detail }\n )\n );\n}\n", "// Color groups with 12 variants each\nexport const colorGroups = [\n {\n name: 'Grays',\n colors: [\n '#FFFFFF',\n '#F8F9FA',\n '#E9ECEF',\n '#DEE2E6',\n '#CED4DA',\n '#ADB5BD',\n '#6C757D',\n '#495057',\n '#343A40',\n '#212529',\n '#1A1A1A',\n '#000000',\n ],\n },\n {\n name: 'Reds',\n colors: [\n '#FFE6E6',\n '#FFCCCC',\n '#FF9999',\n '#FF6666',\n '#FF3333',\n '#FF0000',\n '#E60000',\n '#CC0000',\n '#990000',\n '#660000',\n '#330000',\n '#1A0000',\n ],\n },\n {\n name: 'Oranges',\n colors: [\n '#FFF4E6',\n '#FFE6CC',\n '#FFCC99',\n '#FFB366',\n '#FF9933',\n '#FF8000',\n '#E66A00',\n '#CC5200',\n '#993D00',\n '#662900',\n '#331400',\n '#1A0A00',\n ],\n },\n {\n name: 'Yellows',\n colors: [\n '#FFFEF0',\n '#FFFDE6',\n '#FFFACC',\n '#FFF799',\n '#FFF266',\n '#FFED33',\n '#E6D100',\n '#CCBA00',\n '#998C00',\n '#665D00',\n '#332E00',\n '#1A1700',\n ],\n },\n {\n name: 'Greens',\n colors: [\n '#F0FFF0',\n '#E6FFE6',\n '#CCFFCC',\n '#99FF99',\n '#66FF66',\n '#33FF33',\n '#00E600',\n '#00CC00',\n '#009900',\n '#006600',\n '#003300',\n '#001A00',\n ],\n },\n {\n name: 'Blues',\n colors: [\n '#F0F8FF',\n '#E6F3FF',\n '#CCE7FF',\n '#99CFFF',\n '#66B7FF',\n '#339FFF',\n '#0080E6',\n '#0066CC',\n '#004D99',\n '#003366',\n '#001A33',\n '#000D1A',\n ],\n },\n {\n name: 'Purples',\n colors: [\n '#F8F0FF',\n '#F0E6FF',\n '#E0CCFF',\n '#C199FF',\n '#A266FF',\n '#8333FF',\n '#6600E6',\n '#5200CC',\n '#3D0099',\n '#290066',\n '#140033',\n '#0A001A',\n ],\n },\n {\n name: 'Pinks',\n colors: [\n '#FFE6F0',\n '#FFCCE0',\n '#FF99C1',\n '#FF66A2',\n '#FF3383',\n '#FF0064',\n '#E60057',\n '#CC0051',\n '#99003E',\n '#66002A',\n '#330015',\n '#1A000A',\n ],\n },\n {\n name: 'Teals',\n colors: [\n '#F0FFFF',\n '#E0FFFF',\n '#B2FFFF',\n '#66FFFF',\n '#1AFFFF',\n '#00CCCC',\n '#00B3B3',\n '#009999',\n '#006666',\n '#003333',\n '#001A1A',\n '#000D0D',\n ],\n },\n {\n name: 'Cyans',\n colors: [\n '#E0F7FA',\n '#B2EBF2',\n '#80DEEA',\n '#4DD0E1',\n '#26C6DA',\n '#00BCD4',\n '#00ACC1',\n '#0097A7',\n '#00796B',\n '#004D40',\n '#00251A',\n '#00120D',\n ],\n },\n {\n name: 'Indigos',\n colors: [\n '#E8EAF6',\n '#C5CAE9',\n '#9FA8DA',\n '#7986CB',\n '#5C6BC0',\n '#3F51B5',\n '#303F9F',\n '#283593',\n '#1A237E',\n '#0D1442',\n '#060A21',\n '#030510',\n ],\n },\n {\n name: 'Violets',\n colors: [\n '#F3E5F5',\n '#E1BEE7',\n '#CE93D8',\n '#BA68C8',\n '#AB47BC',\n '#9C27B0',\n '#8E24AA',\n '#7B1FA2',\n '#6A1B9A',\n '#4A148C',\n '#2E1065',\n '#1A0D3D',\n ],\n },\n {\n name: 'Magentas',\n colors: [\n '#FCE4EC',\n '#F8BBD9',\n '#F48FB1',\n '#F06292',\n '#EC407A',\n '#E91E63',\n '#D81B60',\n '#C2185B',\n '#AD1457',\n '#880E4F',\n '#4A0A2A',\n '#2D0519',\n ],\n },\n {\n name: 'Limes',\n colors: [\n '#F9FBE7',\n '#F0F4C3',\n '#E6EE9C',\n '#DCE775',\n '#D4E157',\n '#CDDC39',\n '#C0CA33',\n '#AFB42B',\n '#9E9D24',\n '#827717',\n '#4A4514',\n '#2D2A0C',\n ],\n },\n {\n name: 'Ambers',\n colors: [\n '#FFF8E1',\n '#FFECB3',\n '#FFE082',\n '#FFD54F',\n '#FFCA28',\n '#FFC107',\n '#FFB300',\n '#FFA000',\n '#FF8F00',\n '#FF6F00',\n '#994200',\n '#5A2700',\n ],\n },\n {\n name: 'Deep Oranges',\n colors: [\n '#FBE9E7',\n '#FFCCBC',\n '#FFAB91',\n '#FF8A65',\n '#FF7043',\n '#FF5722',\n '#F4511E',\n '#E64A19',\n '#D84315',\n '#BF360C',\n '#7A2408',\n '#4A1605',\n ],\n },\n {\n name: 'Light Blues',\n colors: [\n '#E1F5FE',\n '#B3E5FC',\n '#81D4FA',\n '#4FC3F7',\n '#29B6F6',\n '#03A9F4',\n '#039BE5',\n '#0288D1',\n '#0277BD',\n '#01579B',\n '#003A66',\n '#00223D',\n ],\n },\n {\n name: 'Light Greens',\n colors: [\n '#E8F5E8',\n '#C8E6C9',\n '#A5D6A7',\n '#81C784',\n '#66BB6A',\n '#4CAF50',\n '#43A047',\n '#388E3C',\n '#2E7D32',\n '#1B5E20',\n '#0F3A14',\n '#09230C',\n ],\n },\n {\n name: 'Browns',\n colors: [\n '#F5F5DC',\n '#F0E68C',\n '#D2B48C',\n '#BC8F8F',\n '#A0522D',\n '#8B4513',\n '#654321',\n '#4A2C2A',\n '#2F1B14',\n '#1A0E0A',\n '#0F0705',\n '#090503',\n ],\n },\n {\n name: 'Grey-Blues',\n colors: [\n '#ECEFF1',\n '#CFD8DC',\n '#B0BEC5',\n '#90A4AE',\n '#78909C',\n '#607D8B',\n '#546E7A',\n '#455A64',\n '#37474F',\n '#263238',\n '#1A2126',\n '#0F1316',\n ],\n },\n];\n\n// Flattened array for backward compatibility\nexport const colors: string[] = colorGroups.flatMap((group) => group.colors);\n", "export const USER_ROLES = ['ADMIN', 'USER', 'CUSTOMER'] as const;\nexport const AUTH_PROVIDERS = ['REGULAR', 'GOOGLE'] as const;\nexport const TIER_TYPES = ['PAID', 'TRAIL', 'FREE', 'EXPIRED'] as const;\nexport const GLB_MATERIALS = ['PBR', 'STANDARD', 'UNKNOWN'] as const;\nexport const VISIBILITY_STATUS = [\n 'private',\n 'public',\n 'store',\n 'unlisted',\n] as const;\n\nexport const HDRI_CATEGORIES = [\n 'outdoor',\n 'indoor',\n 'studio',\n 'sunset',\n 'night',\n 'abstract',\n] as const;\nexport type HdriCategory = (typeof HDRI_CATEGORIES)[number];\n\nexport const HDRI_CUBE_SIZES = [64, 128, 256, 512] as const;\nexport type HdriCubeSize = (typeof HDRI_CUBE_SIZES)[number];\n\n/** Maximum byte length of a published HDRI's .env file. Enforced by API and UI. */\nexport const HDRI_PUBLISH_MAX_ENV_BYTES = 1024 * 1024; // 1 MB\n\n/** Default pixel size of an imported GLB's dynamic texture. Admins can set any\n * size from 1 to MAX_DYNAMIC_TEXTURE_SIZE per axis. */\nexport const DEFAULT_DYNAMIC_TEXTURE_RESOLUTION = {\n width: 2048,\n height: 2048,\n} as const;\n\n/** Upper bound on dynamic texture dimensions. Beyond this, quality gains are\n * negligible on most GPUs but memory cost scales quadratically. Enforced by\n * API and UI; the builder clamps to it too. */\nexport const MAX_DYNAMIC_TEXTURE_SIZE = 4096;\n\n/** Above this, a design layer's combined path data may slow canvas rendering. */\nexport const MAX_LAYER_PATH_BYTES = 600 * 1024; // 600 KB\n\nexport const MAX_FILE_SIZES = {\n MODEL: 10,\n IMAGE: 20,\n THUMBNAIL: 5,\n DEBUG: 0.5,\n ENV: 10,\n FONT: 20,\n HDRI_SOURCE: 25,\n} as const;\n\n/**\n * Default max upload size (in MB) applied to upload endpoints when no\n * explicit override is provided. Shared between the API middleware and the\n * frontend so that client-side validation matches the server limit.\n */\nexport const DEFAULT_MAX_UPLOAD_SIZE_MB = 10;\n\n/**\n * Centralized upload file-type definitions.\n *\n * Each entry describes a named group of files that the API accepts at a\n * single upload endpoint. The API's upload middleware uses `mimeTypes` /\n * `extensions` for validation, and the Angular frontend uses `accept`\n * directly on `<input type=\"file\">` elements so both layers stay in sync.\n *\n * - `IMAGE` \u2013 any raster or vector image (generic image uploads).\n * - `RASTER_IMAGE` \u2013 raster only (design images the AI separates into layers).\n * - `SVG` \u2013 vector only (design layer uploads, pattern SVG uploads).\n * - `MODEL` \u2013 3D model bundles (glb, gltf + referenced textures).\n * - `FONT` \u2013 font files.\n * - `ENV` \u2013 environment maps for 3D scenes.\n */\nexport type UploadFileTypeKey =\n | 'IMAGE'\n | 'RASTER_IMAGE'\n | 'SVG'\n | 'MODEL'\n | 'FONT'\n | 'ENV';\n\nexport interface UploadFileTypeConfig {\n /** Human readable label used in error messages and UI copy. */\n readonly label: string;\n /** Allowed MIME types (lowercase). */\n readonly mimeTypes: readonly string[];\n /** Allowed file extensions including the leading dot (lowercase). */\n readonly extensions: readonly string[];\n /** Value for the HTML `<input accept>` attribute. */\n readonly accept: string;\n}\n\nexport const UPLOAD_FILE_TYPES: Readonly<\n Record<UploadFileTypeKey, UploadFileTypeConfig>\n> = {\n IMAGE: {\n label: 'PNG, JPG, JPEG, WEBP or SVG',\n mimeTypes: [\n 'image/png',\n 'image/jpeg',\n 'image/jpg',\n 'image/webp',\n 'image/svg+xml',\n ],\n extensions: ['.png', '.jpg', '.jpeg', '.webp', '.svg'],\n accept: 'image/png,image/jpeg,image/jpg,image/webp,image/svg+xml',\n },\n RASTER_IMAGE: {\n label: 'PNG, JPG, JPEG or WEBP',\n mimeTypes: ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'],\n extensions: ['.png', '.jpg', '.jpeg', '.webp'],\n accept: 'image/png,image/jpeg,image/jpg,image/webp',\n },\n SVG: {\n label: 'SVG',\n mimeTypes: ['image/svg+xml'],\n extensions: ['.svg'],\n accept: 'image/svg+xml,.svg',\n },\n MODEL: {\n label: 'GLB, GLTF, BIN, PNG, JPG, JPEG or WEBP',\n mimeTypes: [\n 'model/gltf-binary',\n 'model/gltf+json',\n 'application/octet-stream',\n 'image/png',\n 'image/jpeg',\n 'image/jpg',\n 'image/webp',\n ],\n extensions: ['.glb', '.gltf', '.bin', '.png', '.jpg', '.jpeg', '.webp'],\n accept: '.glb,.gltf,.bin,.png,.jpg,.jpeg,.webp',\n },\n FONT: {\n label: 'OTF, TTF or TXT',\n mimeTypes: [\n 'font/ttf',\n 'font/otf',\n 'application/octet-stream',\n 'text/plain',\n ],\n extensions: ['.ttf', '.otf', '.txt'],\n accept: '.ttf,.otf,.txt',\n },\n ENV: {\n label: '.env or .hdr',\n mimeTypes: ['application/octet-stream'],\n extensions: ['.env', '.hdr'],\n accept: '.env,.hdr',\n },\n};\n\n/**\n * Stable string codes the upload middleware emits on 4xx responses. The\n * frontend matches against these to render specific user-facing messages\n * (oversize, wrong type, etc.) without relying on the backend message text.\n */\nexport const UPLOAD_ERROR_CODES = {\n FILE_TOO_LARGE: 'FILE_TOO_LARGE',\n INVALID_FILE_TYPE: 'INVALID_FILE_TYPE',\n FILE_REQUIRED: 'FILE_REQUIRED',\n TOO_MANY_FILES: 'TOO_MANY_FILES',\n UNEXPECTED_FILE_FIELD: 'UNEXPECTED_FILE_FIELD',\n UPLOAD_FAILED: 'UPLOAD_FAILED',\n} as const;\n\nexport type UploadErrorCode =\n (typeof UPLOAD_ERROR_CODES)[keyof typeof UPLOAD_ERROR_CODES];\n\nexport interface UploadErrorDetails {\n fileType?: UploadFileTypeKey;\n maxSizeMB?: number;\n allowedTypes?: readonly string[];\n filename?: string;\n mimeType?: string;\n field?: string;\n [key: string]: unknown;\n}\n\nexport const ROLES = {\n ADMIN: 'ADMIN',\n USER: 'USER',\n} as const;\n\nexport const ACTIVITY_EVENT_TYPES = [\n 'product.created',\n 'product.updated',\n 'product.published',\n 'product.deleted',\n 'product.design_added',\n 'product.design_removed',\n 'product.serialized_saved',\n 'product.serialized_restored',\n 'product.dieline_resnapshotted',\n 'product.screenshot_captured',\n 'product.shared',\n 'product.unshared',\n 'product.access_requested',\n 'product.access_approved',\n 'product.access_denied',\n 'design.created',\n 'design.updated',\n 'design.published',\n 'design.deleted',\n 'design.layers_updated',\n 'design.thumbnail_generated',\n 'glb.uploaded',\n 'glb.updated',\n 'glb.deleted',\n 'user.login',\n 'user.logout',\n // Installation key lifecycle. Rotation and revocation can lock a host's\n // users out, so they need a durable trail, not just a log line.\n 'installation.key_issued',\n 'installation.key_rotated',\n 'installation.disabled',\n 'installation.enabled',\n 'installation.revoked',\n // License terms (Phase 2).\n 'installation.terms_changed',\n 'installation.ips_changed',\n 'installation.seat_limit_reached',\n] as const;\n\nexport const ACTIVITY_ENTITY_TYPES = [\n 'product',\n 'design',\n 'glb',\n 'user',\n 'print_area',\n 'installation',\n] as const;\n// Installation & Access Grant constants\nexport const INSTALLATION_PLATFORMS = [\n 'wordpress',\n 'shopify',\n 'web-app',\n] as const;\nexport const INSTALLATION_STATUS = ['active', 'suspended', 'revoked'] as const;\n", "import { INSTALLATION_STATUS } from './constants';\n\n/**\n * Whether a license works is decided here and only here. `expired` is never\n * stored: it is computed from `expiresAt` at the moment of the check, so an\n * expiry takes effect without any job.\n */\nexport const LICENSE_STATES = [...INSTALLATION_STATUS, 'expired'] as const;\nexport type LicenseState = (typeof LICENSE_STATES)[number];\n\n/** Owners and admins are warned this many days before a license expires. */\nexport const LICENSE_EXPIRY_WARNING_DAYS = 14;\n/** Owners and admins are warned once this share of the seat cap is used. */\nexport const LICENSE_SEAT_WARNING_RATIO = 0.9;\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport interface LicenseTerms {\n status: (typeof INSTALLATION_STATUS)[number];\n expiresAt?: Date | string | null;\n}\n\n/** Status wins over expiry: a revoked license reports `revoked`, not `expired`. */\nexport function licenseState(\n license: LicenseTerms,\n now: Date = new Date()\n): LicenseState {\n if (license.status !== 'active') return license.status;\n if (\n license.expiresAt != null &&\n new Date(license.expiresAt).getTime() <= now.getTime()\n ) {\n return 'expired';\n }\n return 'active';\n}\n\nexport type SeatLevel = 'unlimited' | 'ok' | 'warning' | 'full';\n\nexport function seatLevel(\n endUserCount: number,\n seatLimit: number | null | undefined\n): SeatLevel {\n if (seatLimit == null) return 'unlimited';\n if (endUserCount >= seatLimit) return 'full';\n if (endUserCount >= seatLimit * LICENSE_SEAT_WARNING_RATIO) return 'warning';\n return 'ok';\n}\n\nexport type ExpiryLevel = 'none' | 'ok' | 'soon' | 'expired';\n\nexport function expiryLevel(\n expiresAt: Date | string | null | undefined,\n now: Date = new Date()\n): ExpiryLevel {\n if (expiresAt == null) return 'none';\n const remaining = new Date(expiresAt).getTime() - now.getTime();\n if (remaining <= 0) return 'expired';\n if (remaining <= LICENSE_EXPIRY_WARNING_DAYS * DAY_MS) return 'soon';\n return 'ok';\n}\n\n/**\n * The 401 error code admin-api answers when a LICENSE token (the no-user\n * bearer a host sets as `license-token`) is rejected \u2014 expired, its license\n * inactive, or its key rotated. The builder answers it by asking the host for\n * a fresh license token (`tdn-license-token-required`), never with a login\n * prompt; a route's own \"needs a user\" 401 keeps the generic code.\n */\nexport const LICENSE_TOKEN_INVALID_CODE = 'LICENSE_TOKEN_INVALID';\n"],
|
|
5
|
-
"mappings": "AAIO,IAAMA,EAA4B,mBAElC,SAASC,EAA0BC,EAAyB,CACjEA,EAAK,cACH,IAAI,YAAYF,EAA2B,CACzC,QAAS,GACT,SAAU,EACZ,CAAC,CACH,CACF,CAOO,IAAMG,EAA8B,oBAwCpC,IAAMC,EAAwC,kBAgBxCC,EACX,sBAYK,SAASC,EACdC,EACAC,EACM,CACND,EAAK,cACH,IAAI,YACFF,EACA,CACE,QAAS,GACT,SAAU,GACV,OAAAG,CACF,CACF,CACF,CACF,CAQO,IAAMC,EACX,qBAuCK,IAAMC,EACX,qBAkCK,IAAMC,EACX,sBAMK,SAASC,EACdC,EACAC,EAA+B,CAAC,EAC1B,CACND,EAAK,cACH,IAAI,YACFF,EACA,CAAE,QAAS,GAAO,SAAU,GAAO,OAAAG,CAAO,CAC5C,CACF,CACF,CAcO,IAAMC,EAAqC,oBA4B3C,IAAMC,EAAoC,mBAiD1C,IAAMC,EACX,yBA6BK,IAAMC,EACX,6BAkBK,IAAMC,EAAoC,mBAE1C,SAASC,EAAoBC,EAAyB,CAC3DA,EAAK,cACH,IAAI,YAAYF,EAAmC,CACjD,QAAS,GACT,SAAU,EACZ,CAAC,CACH,CACF,CAQO,IAAMG,EACX,sBCxWK,IAAMC,EAAc,CACzB,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,OACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,SACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,UACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,WACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,QACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,SACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,eACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,cACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,eACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,SACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,EACA,CACE,KAAM,aACN,OAAQ,CACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,CACF,CACF,EAGaC,EAAmBD,EAAY,QAASE,GAAUA,EAAM,MAAM,EChUpE,IAAMC,EAA6B,KAAO,KAe1C,IAAMC,EAAuB,IAAM,KAuMnC,IAAMC,EAAsB,CAAC,SAAU,YAAa,SAAS,ECxO7D,IAAMC,EAAiB,CAAC,GAAGC,EAAqB,SAAS,EAQhE,IAAMC,EAAS,KAAU,GAAK",
|
|
6
|
-
"names": ["THREED_BUILDER_OPEN_EVENT", "dispatchOpenThreedBuilder", "host", "THREED_BUILDER_STATUS_EVENT", "THREED_BUILDER_EXPORT_REQUESTED_EVENT", "THREED_BUILDER_EXPORT_COMPLETE_EVENT", "dispatchExportComplete", "host", "detail", "THREED_BUILDER_PRODUCT_CLONED_EVENT", "THREED_BUILDER_LOGIN_REQUIRED_EVENT", "THREED_BUILDER_LOGIN_CANCELLED_EVENT", "dispatchLoginCancelled", "host", "detail", "THREED_BUILDER_LICENSE_ERROR_EVENT", "THREED_BUILDER_DESIGN_SAVED_EVENT", "THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT", "THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT", "THREED_BUILDER_SAVE_REQUEST_EVENT", "dispatchSaveRequest", "host", "THREED_BUILDER_CLOSE_REQUESTED_EVENT", "colorGroups", "colors", "group", "HDRI_PUBLISH_MAX_ENV_BYTES", "MAX_LAYER_PATH_BYTES", "INSTALLATION_STATUS", "LICENSE_STATES", "INSTALLATION_STATUS", "DAY_MS"]
|
|
7
|
-
}
|