@3dn-builder/builder 0.0.0-stage → 0.636.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/LICENSE +21 -0
- package/README.md +104 -2
- package/chunks/chunk-F4FUPJ4Y.js +2 -0
- package/chunks/chunk-F4FUPJ4Y.js.map +7 -0
- package/events.d.ts +197 -0
- package/events.js +2 -0
- package/events.js.map +7 -0
- package/index.d.ts +272 -0
- package/index.js +2 -0
- package/index.js.map +7 -0
- package/package.json +31 -4
- package/testing.d.ts +72 -0
- package/testing.js +2 -0
- package/testing.js.map +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 3DN
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @3dn-builder/builder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Embed the 3DN product builder in any storefront. The package handles the host side of the builder contract: loading, the login round-trip, license refresh, save before cart, clone mapping, export replies, close, logout and teardown. Your site supplies three backend endpoints and its own login UI.
|
|
4
|
+
|
|
5
|
+
> Published with every 3DN production release, at that release's version. `environment: 'staging'` is not available yet.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @3dn-builder/builder
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createBuilder, BuilderError } from '@3dn-builder/builder';
|
|
17
|
+
|
|
18
|
+
const builder = createBuilder({
|
|
19
|
+
session: {
|
|
20
|
+
// Your session endpoint: license token + the 3DN product to open for this shopper.
|
|
21
|
+
get: ({ hostProductId }) => api.getBuilderSession(hostProductId),
|
|
22
|
+
refreshLicense: () => api.refreshBuilderLicense(),
|
|
23
|
+
},
|
|
24
|
+
login: {
|
|
25
|
+
// Your login-token endpoint. Resolve null when it answers 401.
|
|
26
|
+
getTokens: () => api.mintBuilderTokens(),
|
|
27
|
+
onRequired: (action) => showLogin(action), // 'logged-in' | 'redirected' | 'cancelled'
|
|
28
|
+
},
|
|
29
|
+
customization: {
|
|
30
|
+
// Called when a save created the shopper's clone.
|
|
31
|
+
record: (link) => api.recordCustomization(link),
|
|
32
|
+
},
|
|
33
|
+
onLicenseError: (error) => reportSetupError(error.code),
|
|
34
|
+
onCloseRequested: ({ hasUnsavedChanges }) => closeModal({ confirm: hasUnsavedChanges }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await builder.mount({ target: container, hostProductId: shopProduct.id });
|
|
38
|
+
|
|
39
|
+
// When save() resolves, the design is saved and any clone mapping is recorded.
|
|
40
|
+
try {
|
|
41
|
+
const design = await builder.save();
|
|
42
|
+
await cart.add({ variantId, properties: { threednProductId: design.productId } });
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e instanceof BuilderError && e.code !== 'LOGIN_CANCELLED') showMessage(e.message);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`createBuilder()` touches nothing on the page, so it is safe during server-side rendering. The builder loads on `load()` or `mount()`.
|
|
49
|
+
|
|
50
|
+
## Your backend
|
|
51
|
+
|
|
52
|
+
| Endpoint | Callback | Returns |
|
|
53
|
+
| ------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
54
|
+
| session | `session.get()`, `session.refreshLicense()` | `{ licenseToken, productId, viewToken?, token?, refreshToken? }`: the shopper's clone as `productId` when they have one, else the global product |
|
|
55
|
+
| login-token | `login.getTokens()` | `{ token, refreshToken }` for the logged-in shopper, or 401 |
|
|
56
|
+
| customization | `customization.record()` | Upserts `{ hostProductId, sourceProductId, savedProductId }` for the shopper from **your** server session |
|
|
57
|
+
|
|
58
|
+
Installation keys stay on your server. `createBuilder()` rejects anything that looks like one, and any option that would choose where the builder loads from.
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
| Method | What it does |
|
|
63
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| `load()` | Optional preload, once per page. |
|
|
65
|
+
| `mount(options)` | `{ target, hostProductId, mode?: 'designer' \| 'viewer', designId?, externalExport? }`. Resolves to the instance. |
|
|
66
|
+
| `open()` | Shows the builder UI after a preload or a hidden mount. |
|
|
67
|
+
| `save()` | Resolves `{ productId, version }` once saved and linked. 45 s timeout (`saveTimeoutMs`), paused during login. |
|
|
68
|
+
| `refreshSession()` | Calls `session.get()` again and applies the tokens it returns. |
|
|
69
|
+
| `refreshLicense()` | One shared license refresh for every mounted builder. |
|
|
70
|
+
| `logout()` | Removes the shopper tokens and keeps the license token. |
|
|
71
|
+
| `destroy()` | Removes every builder this controller mounted. |
|
|
72
|
+
|
|
73
|
+
The controller's `open()`, `save()` and `refreshSession()` act on the most recently mounted builder. Use the instance returned by `mount()` to target a specific one.
|
|
74
|
+
|
|
75
|
+
Helpers for login by redirect: `loginReturnPath()`, `isSafeReturnPath()`, `consumeResumeMarker()` and `hasDraft(productId)`.
|
|
76
|
+
|
|
77
|
+
## Errors
|
|
78
|
+
|
|
79
|
+
Every failure is a `BuilderError` with a stable `code`, a `message` that is safe to show, `retryable`, `fatal`, and the builder's original `reason`:
|
|
80
|
+
`CONFIG_INVALID`, `NOT_IN_BROWSER`, `LOAD_FAILED`, `VERSION_CONFLICT`, `SESSION_FAILED`, `LICENSE_ERROR`, `LOGIN_CANCELLED`, `SAVE_FAILED`, `SAVE_TIMEOUT`, `EXPORT_FAILED`, `EXPORT_TIMEOUT`, `DESTROYED`.
|
|
81
|
+
|
|
82
|
+
## Testing your integration
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { installFakeBuilder, findFakeBuilders } from '@3dn-builder/builder/testing';
|
|
86
|
+
|
|
87
|
+
installFakeBuilder(); // before mount(): no real builder is loaded
|
|
88
|
+
await builder.mount({ target, hostProductId: 'mug' });
|
|
89
|
+
const fake = findFakeBuilders()[0];
|
|
90
|
+
const saving = builder.save();
|
|
91
|
+
await fake.emitSavedAsClone('clone-1');
|
|
92
|
+
await saving; // your customization.record() has run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Developing this package
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
npx nx test builder-host
|
|
99
|
+
npx nx lint builder-host
|
|
100
|
+
npx nx build builder-host # dist/libs/builder-host
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The event names and payload types come from `@three-dn/constants` and are bundled into the published files.
|
|
104
|
+
|
|
105
|
+
The package has no version of its own: the build stamps the app version from the root `package.json`, and the `publish-sdk` job in `.github/workflows/deploy.yml` publishes it to npm after each production release.
|
|
@@ -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 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
|
|
@@ -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 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
|
+
}
|
package/events.d.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic open for `<threed-builder>` (avoids synthetic `click()` / ghost clicks).
|
|
3
|
+
* Dispatched on the custom element host with composed path so the inner app can subscribe.
|
|
4
|
+
*/
|
|
5
|
+
declare const THREED_BUILDER_OPEN_EVENT: "tdn-open-builder";
|
|
6
|
+
/**
|
|
7
|
+
* Builder lifecycle statuses emitted via `tdn-status-change` CustomEvent.
|
|
8
|
+
*/
|
|
9
|
+
type BuilderStatus = 'idle' | 'opening' | 'loading' | 'ready' | 'error';
|
|
10
|
+
declare const THREED_BUILDER_STATUS_EVENT: "tdn-status-change";
|
|
11
|
+
interface BuilderStatusDetail {
|
|
12
|
+
status: BuilderStatus;
|
|
13
|
+
step?: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* External export bridge.
|
|
19
|
+
*
|
|
20
|
+
* When `<threed-builder external-export="true">` is set, the webcomponent
|
|
21
|
+
* skips its built-in admin POST and instead emits `exportRequested` so the
|
|
22
|
+
* embedding host can run the export through its own backend (e.g. the
|
|
23
|
+
* store app proxy). The host then hands the resulting file URLs back via
|
|
24
|
+
* `dispatchExportComplete`.
|
|
25
|
+
*
|
|
26
|
+
* NOT FOR A DIELINE PRODUCT (task 3.2, ruling P3-6). A dieline's print files
|
|
27
|
+
* come from ONE pure exporter run over the design's own immutable snapshot
|
|
28
|
+
* (Document 1 sections 12 and 13); a host backend cannot produce that file,
|
|
29
|
+
* and a file it produces instead is a second drawing with nothing holding it
|
|
30
|
+
* to what the customer approved on screen. The webcomponent therefore routes
|
|
31
|
+
* a DIELINE export to the admin API whatever this attribute says, and emits
|
|
32
|
+
* `exportRequested` for GLB products only. A host that needs its own pipeline
|
|
33
|
+
* for dielines needs a different contract, not this one.
|
|
34
|
+
*/
|
|
35
|
+
declare const THREED_BUILDER_EXPORT_REQUESTED_EVENT: "exportRequested";
|
|
36
|
+
interface ExportRequestedDetail {
|
|
37
|
+
productId: string;
|
|
38
|
+
formats: string[];
|
|
39
|
+
surfaces: string[];
|
|
40
|
+
/**
|
|
41
|
+
* The export mode, `'proof'` or `'production-artwork'` (Document 1 section
|
|
42
|
+
* 12). ADDITIVE in task 1.5b: a host that ignores it keeps working exactly
|
|
43
|
+
* as before. Since task 3.2 only GLB products reach this event, and a GLB
|
|
44
|
+
* export is mode-independent — so the field is carried for the host's own
|
|
45
|
+
* bookkeeping, not to choose a drawing.
|
|
46
|
+
*/
|
|
47
|
+
mode?: string;
|
|
48
|
+
}
|
|
49
|
+
declare const THREED_BUILDER_EXPORT_COMPLETE_EVENT: "tdn-export-complete";
|
|
50
|
+
interface ExportFile {
|
|
51
|
+
url: string;
|
|
52
|
+
format: string;
|
|
53
|
+
surface: string;
|
|
54
|
+
}
|
|
55
|
+
interface ExportCompleteDetail {
|
|
56
|
+
files: ExportFile[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Fired after a save that created a new product: "Save as copy", or a host
|
|
60
|
+
* shopper's Save of a product they don't own (their first save). The builder
|
|
61
|
+
* has switched to editing the new product. The host should persist
|
|
62
|
+
* `newProductId` so subsequent loads of the builder open it.
|
|
63
|
+
*/
|
|
64
|
+
declare const THREED_BUILDER_PRODUCT_CLONED_EVENT: "tdn-product-cloned";
|
|
65
|
+
interface ProductClonedDetail {
|
|
66
|
+
originalProductId: string;
|
|
67
|
+
newProductId: string;
|
|
68
|
+
name: string;
|
|
69
|
+
/**
|
|
70
|
+
* The user asked for a copy ("Save as copy", "Save as my copy", "keep both"
|
|
71
|
+
* after a conflict) and expects to keep the original too. `false` when Save
|
|
72
|
+
* had to fork because the user doesn't own the product: the copy takes the
|
|
73
|
+
* original's place.
|
|
74
|
+
*/
|
|
75
|
+
asCopy: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fired when a protected action needs a logged-in user (save / screenshot /
|
|
79
|
+
* export / upload / share), or — with `action: 'session'` — when the builder's
|
|
80
|
+
* session could not be recovered (refresh failed). The host shows its own
|
|
81
|
+
* login if its user is not logged in, has its BACKEND mint a token pair from
|
|
82
|
+
* its own session, and sets `bearer-token` / `refresh-token` on the element;
|
|
83
|
+
* the builder then resumes the pending action. If the user dismisses the login
|
|
84
|
+
* or the mint fails, the host dispatches `tdn-login-cancelled` instead.
|
|
85
|
+
* One event per login round-trip: further gated clicks while a login is
|
|
86
|
+
* pending do not fire it again.
|
|
87
|
+
*/
|
|
88
|
+
declare const THREED_BUILDER_LOGIN_REQUIRED_EVENT: "tdn-login-required";
|
|
89
|
+
type LoginRequiredAction = 'save' | 'screenshot' | 'export' | 'upload' | 'share' | 'session';
|
|
90
|
+
interface LoginRequiredDetail {
|
|
91
|
+
action: LoginRequiredAction;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Host -> builder: the login the builder asked for (`tdn-login-required`) will
|
|
95
|
+
* not happen — the user closed the host's login, or the host's mint failed.
|
|
96
|
+
* The builder drops the pending action, so a stale Save never runs after a
|
|
97
|
+
* later, unrelated login. A pending Save reports `tdn-design-save-failed`
|
|
98
|
+
* with reason `login-cancelled`.
|
|
99
|
+
*/
|
|
100
|
+
declare const THREED_BUILDER_LOGIN_CANCELLED_EVENT: "tdn-login-cancelled";
|
|
101
|
+
interface LoginCancelledDetail {
|
|
102
|
+
action?: LoginRequiredAction;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Builder -> host: the builder cannot work on this page because of the
|
|
106
|
+
* license, not because of the user. Fired once.
|
|
107
|
+
* - `ORIGIN_NOT_ALLOWED`: this page's hostname is not the license's domain or
|
|
108
|
+
* an alias. Do not answer it with a new token: a fresh mint for the same
|
|
109
|
+
* page is refused the same way. Fix the license (add the hostname as an
|
|
110
|
+
* alias) instead.
|
|
111
|
+
* - `LICENSE_TOKEN_INVALID`: the `license-token` was rejected, the builder
|
|
112
|
+
* asked for a fresh one (`tdn-license-token-required`), and the fresh one
|
|
113
|
+
* was rejected too (or never came) — the license is suspended, revoked or
|
|
114
|
+
* expired, or the host is minting against the wrong installation.
|
|
115
|
+
*/
|
|
116
|
+
declare const THREED_BUILDER_LICENSE_ERROR_EVENT: "tdn-license-error";
|
|
117
|
+
type LicenseErrorCode = 'ORIGIN_NOT_ALLOWED' | 'LICENSE_TOKEN_INVALID';
|
|
118
|
+
interface LicenseErrorDetail {
|
|
119
|
+
code: LicenseErrorCode;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The design was saved to 3dn. Fired as soon as the save is stored (before
|
|
123
|
+
* the thumbnail refresh), and after "Save as copy" too (then followed by
|
|
124
|
+
* `tdn-product-cloned`). This — not `tdn-status-change` `ready`, which also
|
|
125
|
+
* fires after every load step — is the signal a host should continue on
|
|
126
|
+
* (add to cart, show a confirmation).
|
|
127
|
+
*/
|
|
128
|
+
declare const THREED_BUILDER_DESIGN_SAVED_EVENT: "tdn-design-saved";
|
|
129
|
+
interface DesignSavedDetail {
|
|
130
|
+
productId: string;
|
|
131
|
+
/** The stored serialized version after this save. */
|
|
132
|
+
version: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Why a save did not happen:
|
|
136
|
+
* - `conflict`: someone saved first; the builder shows its conflict dialog.
|
|
137
|
+
* - `forbidden`: this user may not write to the product; the builder offers
|
|
138
|
+
* request-access or save-as-copy.
|
|
139
|
+
* - `unauthorized`: login recovery ran and the save was still refused.
|
|
140
|
+
* - `login-cancelled`: the host sent `tdn-login-cancelled` for a pending save.
|
|
141
|
+
* - `installation-inactive`: the host installation is not active (setup
|
|
142
|
+
* problem on the host side, not a user error).
|
|
143
|
+
* - `license`: refused with `ORIGIN_NOT_ALLOWED` — this page isn't licensed,
|
|
144
|
+
* not a permissions or session problem; see `tdn-license-error`.
|
|
145
|
+
* - `error`: network or server failure.
|
|
146
|
+
*/
|
|
147
|
+
type DesignSaveFailureReason = 'conflict' | 'forbidden' | 'unauthorized' | 'login-cancelled' | 'installation-inactive' | 'license' | 'error';
|
|
148
|
+
/**
|
|
149
|
+
* A save the user asked for did not happen, after any retry and login
|
|
150
|
+
* recovery ran out. Never fired while a login the builder asked for is still
|
|
151
|
+
* pending. The builder has already told the user; the host may add its own
|
|
152
|
+
* handling and, when `retryable`, let the user try again.
|
|
153
|
+
*/
|
|
154
|
+
declare const THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT: "tdn-design-save-failed";
|
|
155
|
+
interface DesignSaveFailedDetail {
|
|
156
|
+
productId?: string;
|
|
157
|
+
reason: DesignSaveFailureReason;
|
|
158
|
+
retryable: boolean;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Builder -> host: the `license-token` (the no-user token from the host
|
|
162
|
+
* backend's `POST /plugin/license-token`) was rejected — it expired, or the
|
|
163
|
+
* installation's key was rotated. The host fetches a fresh license token from
|
|
164
|
+
* its backend and sets `license-token` again; the builder holds the failed
|
|
165
|
+
* request and retries it once with the new token. This is NEVER a login
|
|
166
|
+
* prompt: answer it without involving the user. A second rejection ends in
|
|
167
|
+
* `tdn-license-error` `LICENSE_TOKEN_INVALID`.
|
|
168
|
+
*/
|
|
169
|
+
declare const THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT: "tdn-license-token-required";
|
|
170
|
+
/**
|
|
171
|
+
* Host -> builder: save the design now, exactly as if the user pressed Save
|
|
172
|
+
* (login gate, save-as-copy for a product the user does not own, and the
|
|
173
|
+
* usual `tdn-design-saved` / `tdn-design-save-failed` outcome). A host sends
|
|
174
|
+
* it before Add to cart when the design has not been saved yet, then waits
|
|
175
|
+
* for one of those two events.
|
|
176
|
+
*/
|
|
177
|
+
declare const THREED_BUILDER_SAVE_REQUEST_EVENT: "tdn-save-request";
|
|
178
|
+
/**
|
|
179
|
+
* Builder -> host: the user pressed the builder's close button. The builder
|
|
180
|
+
* does nothing else — it stays open and keeps its state. The host decides
|
|
181
|
+
* what closing means on its page (hide an overlay, navigate back, ask first
|
|
182
|
+
* when `hasUnsavedChanges`) and removes or hides the element itself.
|
|
183
|
+
*/
|
|
184
|
+
declare const THREED_BUILDER_CLOSE_REQUESTED_EVENT: "tdn-close-requested";
|
|
185
|
+
interface CloseRequestedDetail {
|
|
186
|
+
productId?: string;
|
|
187
|
+
/**
|
|
188
|
+
* What is on screen differs from what was last saved (manual save or
|
|
189
|
+
* autosave), in 2D or 3D: elements, the dieline/board, finish styles, or a
|
|
190
|
+
* switched design. Opening and closing the box (fold) does not count, and
|
|
191
|
+
* an edit undone by hand is no change.
|
|
192
|
+
*/
|
|
193
|
+
hasUnsavedChanges: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
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 };
|
package/events.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as E,c as _,d as D,e,g as R,h as T,i as t,k as L,l as i,m as N,n as I,o as a,q as l}from"./chunks/chunk-F4FUPJ4Y.js";export{l as THREED_BUILDER_CLOSE_REQUESTED_EVENT,i as THREED_BUILDER_DESIGN_SAVED_EVENT,N as THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,e as THREED_BUILDER_EXPORT_COMPLETE_EVENT,D as THREED_BUILDER_EXPORT_REQUESTED_EVENT,L as THREED_BUILDER_LICENSE_ERROR_EVENT,I as THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,t 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,a as THREED_BUILDER_SAVE_REQUEST_EVENT,_ as THREED_BUILDER_STATUS_EVENT};
|
|
2
|
+
//# sourceMappingURL=events.js.map
|
package/events.js.map
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder lifecycle statuses emitted via `tdn-status-change` CustomEvent.
|
|
3
|
+
*/
|
|
4
|
+
type BuilderStatus = 'idle' | 'opening' | 'loading' | 'ready' | 'error';
|
|
5
|
+
interface BuilderStatusDetail {
|
|
6
|
+
status: BuilderStatus;
|
|
7
|
+
step?: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ExportRequestedDetail {
|
|
12
|
+
productId: string;
|
|
13
|
+
formats: string[];
|
|
14
|
+
surfaces: string[];
|
|
15
|
+
/**
|
|
16
|
+
* The export mode, `'proof'` or `'production-artwork'` (Document 1 section
|
|
17
|
+
* 12). ADDITIVE in task 1.5b: a host that ignores it keeps working exactly
|
|
18
|
+
* as before. Since task 3.2 only GLB products reach this event, and a GLB
|
|
19
|
+
* export is mode-independent — so the field is carried for the host's own
|
|
20
|
+
* bookkeeping, not to choose a drawing.
|
|
21
|
+
*/
|
|
22
|
+
mode?: string;
|
|
23
|
+
}
|
|
24
|
+
interface ExportFile {
|
|
25
|
+
url: string;
|
|
26
|
+
format: string;
|
|
27
|
+
surface: string;
|
|
28
|
+
}
|
|
29
|
+
type LoginRequiredAction = 'save' | 'screenshot' | 'export' | 'upload' | 'share' | 'session';
|
|
30
|
+
type LicenseErrorCode = 'ORIGIN_NOT_ALLOWED' | 'LICENSE_TOKEN_INVALID';
|
|
31
|
+
/**
|
|
32
|
+
* Why a save did not happen:
|
|
33
|
+
* - `conflict`: someone saved first; the builder shows its conflict dialog.
|
|
34
|
+
* - `forbidden`: this user may not write to the product; the builder offers
|
|
35
|
+
* request-access or save-as-copy.
|
|
36
|
+
* - `unauthorized`: login recovery ran and the save was still refused.
|
|
37
|
+
* - `login-cancelled`: the host sent `tdn-login-cancelled` for a pending save.
|
|
38
|
+
* - `installation-inactive`: the host installation is not active (setup
|
|
39
|
+
* problem on the host side, not a user error).
|
|
40
|
+
* - `license`: refused with `ORIGIN_NOT_ALLOWED` — this page isn't licensed,
|
|
41
|
+
* not a permissions or session problem; see `tdn-license-error`.
|
|
42
|
+
* - `error`: network or server failure.
|
|
43
|
+
*/
|
|
44
|
+
type DesignSaveFailureReason = 'conflict' | 'forbidden' | 'unauthorized' | 'login-cancelled' | 'installation-inactive' | 'license' | 'error';
|
|
45
|
+
interface CloseRequestedDetail {
|
|
46
|
+
productId?: string;
|
|
47
|
+
/**
|
|
48
|
+
* What is on screen differs from what was last saved (manual save or
|
|
49
|
+
* autosave), in 2D or 3D: elements, the dieline/board, finish styles, or a
|
|
50
|
+
* switched design. Opening and closing the box (fold) does not count, and
|
|
51
|
+
* an edit undone by hand is no change.
|
|
52
|
+
*/
|
|
53
|
+
hasUnsavedChanges: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type BuilderErrorCode = 'CONFIG_INVALID' | 'NOT_IN_BROWSER' | 'LOAD_FAILED' | 'VERSION_CONFLICT' | 'SESSION_FAILED' | 'LICENSE_ERROR' | 'LOGIN_CANCELLED' | 'SAVE_FAILED' | 'SAVE_TIMEOUT' | 'EXPORT_FAILED' | 'EXPORT_TIMEOUT' | 'DESTROYED';
|
|
57
|
+
/** The builder's own reason, plus `customization` when only the record failed. */
|
|
58
|
+
type BuilderErrorReason = DesignSaveFailureReason | LicenseErrorCode | 'customization';
|
|
59
|
+
interface BuilderErrorOptions {
|
|
60
|
+
retryable?: boolean;
|
|
61
|
+
fatal?: boolean;
|
|
62
|
+
reason?: BuilderErrorReason;
|
|
63
|
+
cause?: unknown;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Every failure the SDK reports. Hosts switch on `code`; `message` is safe to
|
|
67
|
+
* show or log and never contains a token.
|
|
68
|
+
*/
|
|
69
|
+
declare class BuilderError extends Error {
|
|
70
|
+
readonly code: BuilderErrorCode;
|
|
71
|
+
readonly retryable: boolean;
|
|
72
|
+
/** The mount cannot continue until the host refreshes or recreates it. */
|
|
73
|
+
readonly fatal: boolean;
|
|
74
|
+
readonly reason?: BuilderErrorReason;
|
|
75
|
+
readonly cause?: unknown;
|
|
76
|
+
constructor(code: BuilderErrorCode, message: string, options?: BuilderErrorOptions);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type BuilderEnvironment = 'production' | 'staging';
|
|
80
|
+
type BuilderVersion = 'latest' | `${number}.${number}.${number}`;
|
|
81
|
+
type BuilderMode = 'designer' | 'viewer';
|
|
82
|
+
/** What the host's session endpoint returns: site access plus the product to open. */
|
|
83
|
+
interface BuilderSession {
|
|
84
|
+
licenseToken: string;
|
|
85
|
+
/** The 3DN product to open: the shopper's clone, else the global product. */
|
|
86
|
+
productId: string;
|
|
87
|
+
/** Required when the product to open is private. */
|
|
88
|
+
viewToken?: string;
|
|
89
|
+
/** Optional fast path: shopper tokens for a shopper the host backend verified. */
|
|
90
|
+
token?: string;
|
|
91
|
+
refreshToken?: string;
|
|
92
|
+
}
|
|
93
|
+
/** Shopper identity, from the host's login-token endpoint. */
|
|
94
|
+
interface ShopperTokens {
|
|
95
|
+
token: string;
|
|
96
|
+
refreshToken: string;
|
|
97
|
+
}
|
|
98
|
+
type LoginOutcome = 'logged-in' | 'redirected' | 'cancelled';
|
|
99
|
+
/** What the host stores when a save created the shopper's clone. */
|
|
100
|
+
interface CustomizationLink {
|
|
101
|
+
hostProductId: string;
|
|
102
|
+
sourceProductId: string;
|
|
103
|
+
savedProductId: string;
|
|
104
|
+
}
|
|
105
|
+
interface BuilderHostConfig {
|
|
106
|
+
/** Default `'production'`. */
|
|
107
|
+
environment?: BuilderEnvironment;
|
|
108
|
+
/** `'latest'` or an exact builder release. Default `'latest'`. */
|
|
109
|
+
version?: BuilderVersion;
|
|
110
|
+
/** Default 45 000 ms. */
|
|
111
|
+
saveTimeoutMs?: number;
|
|
112
|
+
session: {
|
|
113
|
+
get(request: {
|
|
114
|
+
hostProductId: string;
|
|
115
|
+
}): Promise<BuilderSession>;
|
|
116
|
+
refreshLicense(): Promise<Pick<BuilderSession, 'licenseToken'>>;
|
|
117
|
+
};
|
|
118
|
+
login?: {
|
|
119
|
+
/** Answers `tdn-login-required`. Resolve `null` when the endpoint answers 401. */
|
|
120
|
+
getTokens(): Promise<ShopperTokens | null>;
|
|
121
|
+
/** The host's login UX for the action the builder gated. */
|
|
122
|
+
onRequired(action: LoginRequiredAction): Promise<LoginOutcome>;
|
|
123
|
+
/** `getTokens()` failed (5xx, network). Default: `'cancel'`. */
|
|
124
|
+
onMintFailed?(): Promise<'retry' | 'cancel'>;
|
|
125
|
+
};
|
|
126
|
+
customization?: {
|
|
127
|
+
record(link: CustomizationLink): Promise<void>;
|
|
128
|
+
};
|
|
129
|
+
onLicenseError?(error: BuilderError): void;
|
|
130
|
+
onCloseRequested?(detail: CloseRequestedDetail): void;
|
|
131
|
+
onStatusChange?(detail: BuilderStatusDetail): void;
|
|
132
|
+
/** Log lifecycle steps and events to the console. Tokens are always redacted. */
|
|
133
|
+
debug?: boolean;
|
|
134
|
+
}
|
|
135
|
+
interface ExternalExportOptions {
|
|
136
|
+
run(request: ExportRequestedDetail, signal: AbortSignal): Promise<ExportFile[]>;
|
|
137
|
+
/** Default 120 000 ms. */
|
|
138
|
+
timeoutMs?: number;
|
|
139
|
+
}
|
|
140
|
+
interface MountOptions {
|
|
141
|
+
target: HTMLElement;
|
|
142
|
+
/** The host's own product reference, passed to `session.get()`. Never a 3DN id. */
|
|
143
|
+
hostProductId: string;
|
|
144
|
+
/** Default `'designer'`. `'viewer'` is the display-only mode. */
|
|
145
|
+
mode?: BuilderMode;
|
|
146
|
+
designId?: string;
|
|
147
|
+
/** Opt-in, GLB products only. Off unless passed. */
|
|
148
|
+
externalExport?: ExternalExportOptions;
|
|
149
|
+
}
|
|
150
|
+
interface ResolvedConfig extends BuilderHostConfig {
|
|
151
|
+
environment: BuilderEnvironment;
|
|
152
|
+
version: BuilderVersion;
|
|
153
|
+
saveTimeoutMs: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
type DebugLog = (message: string, detail?: unknown) => void;
|
|
157
|
+
|
|
158
|
+
/** What `save()` resolves with. `productId` is always the one to put in the cart. */
|
|
159
|
+
interface SavedDesign {
|
|
160
|
+
productId: string;
|
|
161
|
+
version: number;
|
|
162
|
+
}
|
|
163
|
+
interface InstanceContext {
|
|
164
|
+
config: ResolvedConfig;
|
|
165
|
+
log: DebugLog;
|
|
166
|
+
/** Single-flight license refresh shared by every instance of a controller. */
|
|
167
|
+
refreshLicense(): Promise<void>;
|
|
168
|
+
onDestroyed(instance: BuilderInstance): void;
|
|
169
|
+
}
|
|
170
|
+
/** One mounted `<threed-builder>`. */
|
|
171
|
+
declare class BuilderInstance {
|
|
172
|
+
private readonly ctx;
|
|
173
|
+
private readonly options;
|
|
174
|
+
readonly element: HTMLElement;
|
|
175
|
+
private currentProductId;
|
|
176
|
+
/** Bumped by logout() and destroy(): async results from an older epoch are dropped. */
|
|
177
|
+
private epoch;
|
|
178
|
+
private destroyed;
|
|
179
|
+
private fatal?;
|
|
180
|
+
private loginPending;
|
|
181
|
+
private saveRun?;
|
|
182
|
+
/** A clone mapping whose record() failed; retried before the next save resolves. */
|
|
183
|
+
private unrecordedLink?;
|
|
184
|
+
/** Set between a save that changed the product id and its tdn-product-cloned. */
|
|
185
|
+
private cloneWait?;
|
|
186
|
+
private readonly exports;
|
|
187
|
+
private readonly listeners;
|
|
188
|
+
private constructor();
|
|
189
|
+
/** Builds the element in the documented order and inserts it last. */
|
|
190
|
+
static mount(ctx: InstanceContext, options: MountOptions): Promise<BuilderInstance>;
|
|
191
|
+
/** The 3DN product the builder is editing now: the clone, after a first save. */
|
|
192
|
+
get productId(): string;
|
|
193
|
+
get hostProductId(): string;
|
|
194
|
+
private createElement;
|
|
195
|
+
private attachListeners;
|
|
196
|
+
/** Shows the builder UI, for a host that mounted it before showing it. */
|
|
197
|
+
open(): void;
|
|
198
|
+
/**
|
|
199
|
+
* Saves the design. Resolves once it is saved and any clone mapping it
|
|
200
|
+
* required has been recorded; concurrent calls share one save.
|
|
201
|
+
*/
|
|
202
|
+
save(): Promise<SavedDesign>;
|
|
203
|
+
/** Calls session.get() again and applies what it returns. */
|
|
204
|
+
refreshSession(): Promise<void>;
|
|
205
|
+
/** Removes the shopper layer. The site layer stays and the builder revokes the session. */
|
|
206
|
+
logout(): void;
|
|
207
|
+
destroy(): void;
|
|
208
|
+
/** Used by the controller to hand every instance the shared fresh license token. */
|
|
209
|
+
applyLicense(licenseToken: string): void;
|
|
210
|
+
get isDestroyed(): boolean;
|
|
211
|
+
private answerLogin;
|
|
212
|
+
private setLoginPending;
|
|
213
|
+
private onLicenseError;
|
|
214
|
+
private onDesignSaved;
|
|
215
|
+
private onProductCloned;
|
|
216
|
+
private recordLink;
|
|
217
|
+
private finishSave;
|
|
218
|
+
private answerExport;
|
|
219
|
+
/**
|
|
220
|
+
* The builder has no error field on tdn-export-complete yet: it ignores
|
|
221
|
+
* `error`, and the empty file list ends its wait.
|
|
222
|
+
*/
|
|
223
|
+
private replyExportFailure;
|
|
224
|
+
private assertUsable;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface Builder {
|
|
228
|
+
/** Optional preload: loads the builder code once per page. */
|
|
229
|
+
load(): Promise<void>;
|
|
230
|
+
/** Creates and inserts a builder. Later calls add more instances. */
|
|
231
|
+
mount(options: MountOptions): Promise<BuilderInstance>;
|
|
232
|
+
/** The most recently mounted builder that is still alive. */
|
|
233
|
+
readonly current: BuilderInstance | undefined;
|
|
234
|
+
/** Shows the current builder's UI. */
|
|
235
|
+
open(): void;
|
|
236
|
+
/** Saves the current builder's design; see BuilderInstance.save(). */
|
|
237
|
+
save(): Promise<SavedDesign>;
|
|
238
|
+
/** Calls session.get() again for the current builder. */
|
|
239
|
+
refreshSession(): Promise<void>;
|
|
240
|
+
/** Fetches a fresh license token once and applies it to every builder. */
|
|
241
|
+
refreshLicense(): Promise<void>;
|
|
242
|
+
/** Removes the shopper layer from every builder. */
|
|
243
|
+
logout(): void;
|
|
244
|
+
/** Destroys every builder this controller mounted. */
|
|
245
|
+
destroy(): void;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Records the host's configuration and callbacks. Touches nothing on the page
|
|
249
|
+
* until load() or mount(), so it is safe to call during server-side rendering.
|
|
250
|
+
*/
|
|
251
|
+
declare function createBuilder(config: BuilderHostConfig): Builder;
|
|
252
|
+
|
|
253
|
+
/** Query parameter that marks "back from the login page, resume the save". */
|
|
254
|
+
declare const RESUME_PARAM = "tdn_resume";
|
|
255
|
+
/**
|
|
256
|
+
* A relative return path for the host's login redirect: the current page with
|
|
257
|
+
* the resume marker. Only same-site paths, so the redirect can never become an
|
|
258
|
+
* open redirect.
|
|
259
|
+
*/
|
|
260
|
+
declare function loginReturnPath(): string;
|
|
261
|
+
/** True for a path that stays on this site: starts with "/" but not "//" or "/\". */
|
|
262
|
+
declare function isSafeReturnPath(path: string): boolean;
|
|
263
|
+
/**
|
|
264
|
+
* True once, when the page was opened by a login redirect carrying the resume
|
|
265
|
+
* marker. Strips the marker from the address bar.
|
|
266
|
+
*/
|
|
267
|
+
declare function consumeResumeMarker(): boolean;
|
|
268
|
+
/** True when the builder kept an unsaved draft of this 3DN product on this device. */
|
|
269
|
+
declare function hasDraft(productId: string): boolean;
|
|
270
|
+
|
|
271
|
+
export { BuilderError, BuilderInstance, RESUME_PARAM, consumeResumeMarker, createBuilder, hasDraft, isSafeReturnPath, loginReturnPath };
|
|
272
|
+
export type { Builder, BuilderEnvironment, BuilderErrorCode, BuilderErrorReason, BuilderHostConfig, BuilderMode, BuilderSession, BuilderVersion, CustomizationLink, ExternalExportOptions, LoginOutcome, MountOptions, SavedDesign, ShopperTokens };
|
package/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{b as R,c as k,d as x,e as P,f as _,g as B,h as O,j as m,k as C,l as A,m as N,n as M,p as F,q as U}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)}},V={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 H(t){var n;let e=(n=V[t.reason])!=null?n:V.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 q(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 re=45e3,$=12e4,oe=new Set(["environment","version","saveTimeoutMs","session","login","customization","onLicenseError","onCloseRequested","onStatusChange","debug"]),ie=new Set(["target","hostProductId","mode","designId","externalExport"]),se=/^\d+\.\d+\.\d+$/,ae=/^tdn_inst_/;function w(t){return se.test(t)}function c(t){return typeof t=="object"&&t!==null}function f(t){return typeof t=="function"}function j(t){return typeof t=="number"&&Number.isFinite(t)&&t>0}function z(t,e=0){return typeof t=="string"?ae.test(t):!c(t)||e>3?!1:Object.values(t).some(n=>z(n,e+1))}function G(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 X(t){var h,u,l;if(!c(t))throw a("createBuilder() needs a configuration object.");if(z(t))throw a("createBuilder(): an installation key was passed to the browser. Installation keys stay on your server; pass a session callback instead.");G(t,oe,"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=(u=t.version)!=null?u:"latest";if(typeof n!="string"||n!=="latest"&&!w(n))throw a(`createBuilder(): version must be "latest" or an exact release such as "1.2.3", got ${JSON.stringify(n)}.`);let r=(l=t.saveTimeoutMs)!=null?l:re;if(!j(r))throw a("createBuilder(): saveTimeoutMs must be a positive number of milliseconds.");let o=t.session;if(!c(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(!c(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&&(!c(d)||!f(d.record)))throw a("createBuilder(): customization.record() is required when customization is set. It calls your customization endpoint.");for(let y of["onLicenseError","onCloseRequested","onStatusChange"])if(t[y]!==void 0&&!f(t[y]))throw a(`createBuilder(): ${y} must be a function.`);return{...t,environment:e,version:n,saveTimeoutMs:r}}function K(t){var d;if(!c(t))throw a("mount() needs an options object.");G(t,ie,"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.");let i=t.externalExport;if(i!==void 0){if(!c(i)||!f(i.run))throw a("mount(): externalExport.run() is required when externalExport is set.");if(i.timeoutMs!==void 0&&!j(i.timeoutMs))throw a("mount(): externalExport.timeoutMs must be a positive number of milliseconds.")}return t}function S(t){let e=o=>new s("SESSION_FAILED",`session.get() returned an invalid session: ${o}.`,{retryable:!1});if(!c(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 W(t){if(t===null)return null;if(!c(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 de=/token|secret|key|authorization/i;function b(t,e=0){if(t===null||typeof t!="object"||e>4)return t;if(Array.isArray(t))return t.map(r=>b(r,e+1));let n={};for(let[r,o]of Object.entries(t))n[r]=de.test(r)&&o!=null?"[redacted]":b(o,e+1);return n}function Y(t){return t?(e,n)=>{n===void 0?console.debug(`[3DN] ${e}`):console.debug(`[3DN] ${e}`,b(n))}:()=>{}}var E="threed-builder",Z={production:"https://3d-builder.3dn.app",staging:void 0},ue=3e4,J=Symbol.for("@3dn-builder/builder/loader");function le(){let t=globalThis,e=t[J];return e||(e={},t[J]=e),e}function ce(){return typeof window!="undefined"&&typeof document!="undefined"&&typeof customElements!="undefined"}function ee(t){if(!Z[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 he(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"||!w(o))throw p("The 3DN builder release information was not valid.");return o}function fe(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 pe(){return new Promise((t,e)=>{let n=setTimeout(()=>e(p("The 3DN builder loaded but did not start.")),ue);customElements.whenDefined(E).then(()=>{clearTimeout(n),t()},e)})}function Q(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 te(t,e){if(!ce())throw new s("NOT_IN_BROWSER","The 3DN builder can only load in a browser.");let n=le();if(n.loading){await n.loading,Q(n,t,e);return}if(customElements.get(E))return;let r=Z[t];if(!r)throw a(`The "${t}" environment is not available in this SDK release.`);n.environment=t,n.loading=(async()=>{let o=await he(r,e);n.version=o;let i=await fe(`${r}/builder-${o}.js`);try{await pe()}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)}Q(n,t,e)}function ne(){let t,e,n=new Promise((r,o)=>{t=r,e=o});return n.catch(()=>{}),{promise:n,resolve:t,reject:e}}var I=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}},v=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(E);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&&D(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"),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(O,r=>{var o;this.answerLogin((o=r==null?void 0:r.action)!=null?o:"save")}),n(M,()=>{this.ctx.refreshLicense()}),n(C,r=>this.onLicenseError(r)),n(A,r=>{this.onDesignSaved(r)}),n(N,r=>this.finishSave(H(r))),n(B,r=>{this.onProductCloned(r)}),n(U,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(x,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:ne(),timer:new I(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(),F(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&&D(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")}}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=W(await n.getTokens())}catch(u){if(o())return;this.ctx.log("login.getTokens() failed",{cause:String(u)});let l=n.onMintFailed?await n.onMintFailed():"cancel";if(o())return;if(l==="retry")continue;m(this.element,{action:e});return}if(o())return;if(d){D(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=q(e.code);this.fatal=n,this.finishSave(n),(o=(r=this.ctx.config).onLicenseError)==null||o.call(r,n)}async onDesignSaved(e){if(e.productId!==this.currentProductId&&!this.cloneWait&&(this.cloneWait=ne()),!!this.saveRun){try{this.cloneWait&&await this.cloneWait.promise,this.unrecordedLink&&await this.recordLink(this.unrecordedLink)}catch(n){this.finishSave(n instanceof s?n:new s("SAVE_FAILED","The design was saved but could not be linked to this shopper.",{retryable:!0,reason:"customization",cause:n}));return}this.finishSave(void 0,{productId:e.productId,version:e.version})}}async onProductCloned(e){let n={hostProductId:this.options.hostProductId,sourceProductId:e.originalProductId,savedProductId:e.newProductId};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:$),d=new Promise((u,l)=>r.signal.addEventListener("abort",()=>l(new Error("aborted"))));try{let u=await Promise.race([n.run(e,r.signal),d]);_(this.element,{files:u})}catch(u){let l=o?"EXPORT_TIMEOUT":"EXPORT_FAILED";this.ctx.log("export failed",{code:l,cause:String(u)}),this.replyExportFailure(l)}finally{clearTimeout(i),this.exports.delete(r)}}replyExportFailure(e){let n={files:[],error:{code:e}};this.element.dispatchEvent(new CustomEvent(P,{bubbles:!1,composed:!1,detail:n}))}assertUsable(){if(this.destroyed)throw g()}};function D(t,e){t.setAttribute("refresh-token",e.refreshToken),t.setAttribute("bearer-token",e.token)}var L=class{constructor(e){this.config=e;this.instances=[];this.log=Y(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 te(this.config.environment,this.config.version)}async mount(e){let n=K(e);await this.load();let r=await v.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 ge(t){let e=X(t);return ee(e.environment),new L(e)}var T="tdn_resume",me="tdn-draft-index";function Ee(){let t=new URL(window.location.href);return t.searchParams.set(T,"1"),t.pathname+t.search+t.hash}function ve(t){return/^\/(?![/\\])/.test(t)}function Te(){if(typeof window=="undefined")return!1;let t=new URL(window.location.href);return t.searchParams.get(T)!=="1"?!1:(t.searchParams.delete(T),window.history.replaceState(window.history.state,"",t.pathname+t.search+t.hash),!0)}function ye(t){var e;if(typeof localStorage=="undefined")return!1;try{let n=JSON.parse((e=localStorage.getItem(me))!=null?e:"{}");return Object.values(n).some(r=>(r==null?void 0:r.productId)===t)}catch{return!1}}export{s as BuilderError,T as RESUME_PARAM,Te as consumeResumeMarker,ge as createBuilder,ye as hasDraft,ve as isSafeReturnPath,Ee as loginReturnPath};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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\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 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\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 '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]);\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 '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 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/** 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 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 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 /** 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 if (!this.saveRun) return;\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 this.finishSave(undefined, {\n productId: detail.productId,\n version: detail.version,\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 };\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", "externalExport", "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", "licenseToken", "action", "login", "dispatchLoginCancelled", "stale", "redirected", "tokens", "validateShopperTokens", "choice", "outcome", "pending", "detail", "error", "licenseError", "link", "wait", "customization", "saved", "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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@3dn-builder/builder",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.636.0",
|
|
4
|
+
"description": "Embed the 3DN product builder in any storefront: loading, login, license refresh, save and customization records.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/harikrishna266/three-dn.git",
|
|
9
|
+
"directory": "libs/builder-host"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/harikrishna266/three-dn/tree/develop/libs/builder-host#readme",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"import": "./index.js"
|
|
18
|
+
},
|
|
19
|
+
"./events": {
|
|
20
|
+
"types": "./events.d.ts",
|
|
21
|
+
"import": "./events.js"
|
|
22
|
+
},
|
|
23
|
+
"./testing": {
|
|
24
|
+
"types": "./testing.d.ts",
|
|
25
|
+
"import": "./testing.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/testing.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
interface ExportRequestedDetail {
|
|
2
|
+
productId: string;
|
|
3
|
+
formats: string[];
|
|
4
|
+
surfaces: string[];
|
|
5
|
+
/**
|
|
6
|
+
* The export mode, `'proof'` or `'production-artwork'` (Document 1 section
|
|
7
|
+
* 12). ADDITIVE in task 1.5b: a host that ignores it keeps working exactly
|
|
8
|
+
* as before. Since task 3.2 only GLB products reach this event, and a GLB
|
|
9
|
+
* export is mode-independent — so the field is carried for the host's own
|
|
10
|
+
* bookkeeping, not to choose a drawing.
|
|
11
|
+
*/
|
|
12
|
+
mode?: string;
|
|
13
|
+
}
|
|
14
|
+
type LoginRequiredAction = 'save' | 'screenshot' | 'export' | 'upload' | 'share' | 'session';
|
|
15
|
+
type LicenseErrorCode = 'ORIGIN_NOT_ALLOWED' | 'LICENSE_TOKEN_INVALID';
|
|
16
|
+
/**
|
|
17
|
+
* Why a save did not happen:
|
|
18
|
+
* - `conflict`: someone saved first; the builder shows its conflict dialog.
|
|
19
|
+
* - `forbidden`: this user may not write to the product; the builder offers
|
|
20
|
+
* request-access or save-as-copy.
|
|
21
|
+
* - `unauthorized`: login recovery ran and the save was still refused.
|
|
22
|
+
* - `login-cancelled`: the host sent `tdn-login-cancelled` for a pending save.
|
|
23
|
+
* - `installation-inactive`: the host installation is not active (setup
|
|
24
|
+
* problem on the host side, not a user error).
|
|
25
|
+
* - `license`: refused with `ORIGIN_NOT_ALLOWED` — this page isn't licensed,
|
|
26
|
+
* not a permissions or session problem; see `tdn-license-error`.
|
|
27
|
+
* - `error`: network or server failure.
|
|
28
|
+
*/
|
|
29
|
+
type DesignSaveFailureReason = 'conflict' | 'forbidden' | 'unauthorized' | 'login-cancelled' | 'installation-inactive' | 'license' | 'error';
|
|
30
|
+
|
|
31
|
+
/** A host → builder event the fake received. */
|
|
32
|
+
interface ReceivedCommand {
|
|
33
|
+
type: string;
|
|
34
|
+
detail?: unknown;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A stand-in for `<threed-builder>` that speaks the builder's event contract
|
|
38
|
+
* without loading the real builder. Install it before mount() and drive it
|
|
39
|
+
* from tests: it records what the host sends and emits what the builder would.
|
|
40
|
+
*/
|
|
41
|
+
interface FakeBuilderElement extends HTMLElement {
|
|
42
|
+
/** Host → builder events, in order. */
|
|
43
|
+
readonly received: ReceivedCommand[];
|
|
44
|
+
/** Attributes as they were when the element was inserted into the page. */
|
|
45
|
+
readonly attributesAtConnect: Record<string, string>;
|
|
46
|
+
/** Emits any builder → host event, with the builder's own flags. */
|
|
47
|
+
emit(type: string, detail?: unknown): void;
|
|
48
|
+
/** A successful save of the current product. */
|
|
49
|
+
emitSaved(version?: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* A first save that cloned the product: tdn-design-saved with the clone id,
|
|
52
|
+
* then tdn-product-cloned on the next task, as the builder does after its
|
|
53
|
+
* thumbnail upload.
|
|
54
|
+
*/
|
|
55
|
+
emitSavedAsClone(newProductId: string, version?: number): Promise<void>;
|
|
56
|
+
emitSaveFailed(reason: DesignSaveFailureReason, retryable?: boolean): void;
|
|
57
|
+
emitLoginRequired(action?: LoginRequiredAction): void;
|
|
58
|
+
emitLicenseTokenRequired(): void;
|
|
59
|
+
emitLicenseError(code: LicenseErrorCode): void;
|
|
60
|
+
emitCloseRequested(hasUnsavedChanges: boolean): void;
|
|
61
|
+
emitExportRequested(detail: ExportRequestedDetail): void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Registers the fake as `<threed-builder>`. mount() then uses it instead of
|
|
65
|
+
* loading the real builder. Call once per test environment.
|
|
66
|
+
*/
|
|
67
|
+
declare function installFakeBuilder(): void;
|
|
68
|
+
/** Every fake builder currently in the document, in document order. */
|
|
69
|
+
declare function findFakeBuilders(root?: ParentNode): FakeBuilderElement[];
|
|
70
|
+
|
|
71
|
+
export { findFakeBuilders, installFakeBuilder };
|
|
72
|
+
export type { FakeBuilderElement, ReceivedCommand };
|
package/testing.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as d,c as a,d as r,e as u,g as m,h as c,i as R,k as l,l as E,m as _,n as v,o as D,q as T}from"./chunks/chunk-F4FUPJ4Y.js";var s="threed-builder",p=[d,D,R,u],C=new Set([r]);function h(){class i extends HTMLElement{constructor(){super();this.received=[];this.attributesAtConnect={};for(let e of p)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(a,{status:"idle"})}emit(e,t){let o=!C.has(e),n=new CustomEvent(e,{bubbles:o,composed:o,detail:t});n.fromFake=!0,this.dispatchEvent(n)}emitSaved(e=1){var t;this.emit(E,{productId:(t=this.getAttribute("product"))!=null?t:"",version:e})}async emitSavedAsClone(e,t=1){var n;let o=(n=this.getAttribute("product"))!=null?n:"";this.emit(E,{productId:e,version:t}),await new Promise(L=>setTimeout(L,0)),this.emit(m,{originalProductId:o,newProductId:e,name:"Customized",asCopy:!1})}emitSaveFailed(e,t=!1){var o;this.emit(_,{productId:(o=this.getAttribute("product"))!=null?o:void 0,reason:e,retryable:t})}emitLoginRequired(e="save"){this.emit(c,{action:e})}emitLicenseTokenRequired(){this.emit(v)}emitLicenseError(e){this.emit(l,{code:e})}emitCloseRequested(e){var t;this.emit(T,{productId:(t=this.getAttribute("product"))!=null?t:void 0,hasUnsavedChanges:e})}emitExportRequested(e){this.emit(r,e)}}customElements.define(s,i)}function g(){let i=customElements.get(s);if(i){if("prototype"in i&&"emitSaved"in i.prototype)return;throw new Error("A real <threed-builder> is already defined on this page.")}h()}function S(i=document){return Array.from(i.querySelectorAll(s))}export{S as findFakeBuilders,g as installFakeBuilder};
|
|
2
|
+
//# sourceMappingURL=testing.js.map
|
package/testing.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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,\n * then tdn-product-cloned on the next task, as the builder does after its\n * thumbnail upload.\n */\n emitSavedAsClone(newProductId: string, version?: number): Promise<void>;\n emitSaveFailed(reason: DesignSaveFailureReason, retryable?: boolean): 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 }\n\n async emitSavedAsClone(newProductId: string, version = 1): Promise<void> {\n const originalProductId = this.getAttribute('product') ?? '';\n this.emit(THREED_BUILDER_DESIGN_SAVED_EVENT, {\n productId: newProductId,\n version,\n });\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: false,\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 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": "mIAoBA,IAAMA,EAAM,iBAoCNC,EAAgB,CACpBC,EACAC,EACAC,EACAC,CACF,EAGMC,EAAe,IAAI,IAAY,CAACC,CAAqC,CAAC,EAE5E,SAASC,GAA0B,CACjC,MAAMC,UAA0B,WAA0C,CAIxE,aAAc,CACZ,MAAM,EAJR,KAAS,SAA8B,CAAC,EACxC,yBAA8C,CAAC,EAI7C,QAAWC,KAAQT,EACjB,KAAK,iBAAiBS,EAAOC,GAAU,CAEjCA,aAAiB,aAAgBA,EAAoB,UAEzD,KAAK,SAAS,KAAK,CAAE,KAAAD,EAAM,OAASC,EAAsB,MAAO,CAAC,CACpE,CAAC,CAEL,CAEA,mBAA0B,CACxB,KAAK,oBAAsB,OAAO,YAChC,MAAM,KAAK,KAAK,WAAaC,GAAM,CAACA,EAAE,KAAMA,EAAE,KAAK,CAAC,CACtD,EAEA,KAAK,KAAKC,EAA6B,CAAE,OAAQ,MAAO,CAAC,CAC3D,CAEA,KAAKH,EAAcI,EAAwB,CACzC,IAAMC,EAAU,CAACT,EAAa,IAAII,CAAI,EAChCC,EAAQ,IAAI,YAAYD,EAAM,CAClC,QAAAK,EACA,SAAUA,EACV,OAAAD,CACF,CAAC,EACDH,EAAM,SAAW,GACjB,KAAK,cAAcA,CAAK,CAC1B,CAEA,UAAUK,EAAU,EAAS,CAtGjC,IAAAC,EAuGM,KAAK,KAAKC,EAAmC,CAC3C,WAAWD,EAAA,KAAK,aAAa,SAAS,IAA3B,KAAAA,EAAgC,GAC3C,QAAAD,CACF,CAAC,CACH,CAEA,MAAM,iBAAiBG,EAAsBH,EAAU,EAAkB,CA7G7E,IAAAC,EA8GM,IAAMG,GAAoBH,EAAA,KAAK,aAAa,SAAS,IAA3B,KAAAA,EAAgC,GAC1D,KAAK,KAAKC,EAAmC,CAC3C,UAAWC,EACX,QAAAH,CACF,CAAC,EACD,MAAM,IAAI,QAASK,GAAY,WAAWA,EAAS,CAAC,CAAC,EACrD,KAAK,KAAKC,EAAqC,CAC7C,kBAAAF,EACA,aAAAD,EACA,KAAM,aACN,OAAQ,EACV,CAAC,CACH,CAEA,eAAeI,EAAiCC,EAAY,GAAa,CA5H7E,IAAAP,EA6HM,KAAK,KAAKQ,EAAyC,CACjD,WAAWR,EAAA,KAAK,aAAa,SAAS,IAA3B,KAAAA,EAAgC,OAC3C,OAAAM,EACA,UAAAC,CACF,CAAC,CACH,CAEA,kBAAkBE,EAA8B,OAAc,CAC5D,KAAK,KAAKC,EAAqC,CAAE,OAAAD,CAAO,CAAC,CAC3D,CAEA,0BAAiC,CAC/B,KAAK,KAAKE,CAA2C,CACvD,CAEA,iBAAiBC,EAA8B,CAC7C,KAAK,KAAKC,EAAoC,CAAE,KAAAD,CAAK,CAAC,CACxD,CAEA,mBAAmBE,EAAkC,CAhJzD,IAAAd,EAiJM,KAAK,KAAKe,EAAsC,CAC9C,WAAWf,EAAA,KAAK,aAAa,SAAS,IAA3B,KAAAA,EAAgC,OAC3C,kBAAAc,CACF,CAAC,CACH,CAEA,oBAAoBjB,EAAqC,CACvD,KAAK,KAAKP,EAAuCO,CAAM,CACzD,CACF,CACA,eAAe,OAAOd,EAAKS,CAAiB,CAC9C,CAUO,SAASwB,GAA2B,CACzC,IAAMC,EAAW,eAAe,IAAIlC,CAAG,EACvC,GAAIkC,EAAU,CACZ,GAAI,cAAeA,GAAY,cAAeA,EAAS,UAAW,OAClE,MAAM,IAAI,MAAM,0DAA0D,CAC5E,CACA1B,EAAkB,CACpB,CAGO,SAAS2B,EACdC,EAAmB,SACG,CACtB,OAAO,MAAM,KAAKA,EAAK,iBAAiBpC,CAAG,CAAC,CAC9C",
|
|
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", "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"]
|
|
7
|
+
}
|