@3dn-builder/builder 0.636.0 → 0.638.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # @3dn-builder/builder
2
2
 
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.
3
+ Embed the 3DN product builder in any storefront. The SDK loads the builder, runs the login round-trip, refreshes the license, saves and reports every save. Your site supplies a session endpoint, a login-token endpoint and its own login UI.
6
4
 
7
5
  ## Install
8
6
 
@@ -10,74 +8,134 @@ Embed the 3DN product builder in any storefront. The package handles the host si
10
8
  npm install @3dn-builder/builder
11
9
  ```
12
10
 
13
- ## Quick start
11
+ ESM, zero runtime dependencies. Entry points: `.`, `./events` (types and constants only), `./testing` (a fake `<threed-builder>`).
12
+
13
+ ## Create, mount, destroy
14
14
 
15
15
  ```ts
16
- import { createBuilder, BuilderError } from '@3dn-builder/builder';
16
+ import { createBuilder } from '@3dn-builder/builder';
17
17
 
18
18
  const builder = createBuilder({
19
19
  session: {
20
- // Your session endpoint: license token + the 3DN product to open for this shopper.
21
20
  get: ({ hostProductId }) => api.getBuilderSession(hostProductId),
22
21
  refreshLicense: () => api.refreshBuilderLicense(),
23
22
  },
24
23
  login: {
25
- // Your login-token endpoint. Resolve null when it answers 401.
26
- getTokens: () => api.mintBuilderTokens(),
24
+ getTokens: () => api.mintBuilderTokens(), // null on 401
27
25
  onRequired: (action) => showLogin(action), // 'logged-in' | 'redirected' | 'cancelled'
28
26
  },
29
- customization: {
30
- // Called when a save created the shopper's clone.
31
- record: (link) => api.recordCustomization(link),
32
- },
27
+ onSaved: (saved) => showSavedDesign(saved.productId),
33
28
  onLicenseError: (error) => reportSetupError(error.code),
34
29
  onCloseRequested: ({ hasUnsavedChanges }) => closeModal({ confirm: hasUnsavedChanges }),
35
30
  });
36
31
 
37
- await builder.mount({ target: container, hostProductId: shopProduct.id });
32
+ const instance = await builder.mount({ target: container, hostProductId: 'alpine-mug' });
33
+ const design = await instance.save(); // design.productId goes on the cart line
34
+ instance.destroy();
35
+ ```
36
+
37
+ `createBuilder()` touches nothing on the page, so it is safe during server-side rendering; the builder loads on `load()` or `mount()`. The target needs a real height.
38
+
39
+ | Method | Behavior |
40
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------- |
41
+ | `load()` | Optional preload, once per page. |
42
+ | `mount(options)` | `{ target, hostProductId, mode?: 'designer' \| 'viewer', designId?, externalExport?, observabilityContext? }` → instance. |
43
+ | `open()` | Shows the builder UI after a hidden mount. |
44
+ | `save()` | Resolves `{ productId, version }` after the save, any clone record and `onSaved`. 45 s timeout. |
45
+ | `refreshSession()` | Calls `session.get()` again and applies the result. |
46
+ | `refreshLicense()` | One shared license refresh for every mounted builder. |
47
+ | `logout()` | Removes the shopper tokens, keeps the license token. Late login results are dropped. |
48
+ | `destroy()` | Removes the builder and its listeners; pending calls reject with `DESTROYED`. |
49
+
50
+ `observabilityContext` is the builder's `observability-context`: a JSON object string naming your app, tenant, site, pseudonymous user and request, plus your page's `sentry-trace` and `baggage`, so the builder's own error reports say where they happened. It is set before the element connects; `instance.setObservabilityContext(json)` replaces it later, for example once your page's Sentry has a trace. The keys and their rules are in the integration guide, section 6.3.
38
51
 
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);
52
+ The controller's methods act on the most recently mounted builder. With several builders on a page, call the instance `mount()` returned.
53
+
54
+ ## Session
55
+
56
+ ```ts
57
+ session: {
58
+ get(request: { hostProductId: string }): Promise<BuilderSession>;
59
+ refreshLicense(): Promise<{ licenseToken: string }>;
60
+ }
61
+
62
+ interface BuilderSession {
63
+ licenseToken: string;
64
+ productId: string; // the shopper's saved copy, else the global product
65
+ viewToken?: string; // required for a private product
66
+ token?: string; // optional shopper tokens, always together
67
+ refreshToken?: string;
45
68
  }
46
69
  ```
47
70
 
48
- `createBuilder()` touches nothing on the page, so it is safe during server-side rendering. The builder loads on `load()` or `mount()`.
71
+ `hostProductId` is your own reference; your backend decides which 3DN product it opens. Installation keys stay on your server: `createBuilder()` rejects anything that looks like one, and any option that names where the builder loads from.
49
72
 
50
- ## Your backend
73
+ ## Login
51
74
 
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 |
75
+ ```ts
76
+ login: {
77
+ getTokens(): Promise<{ token: string; refreshToken: string } | null>; // null on 401
78
+ onRequired(action: LoginRequiredAction): Promise<'logged-in' | 'redirected' | 'cancelled'>;
79
+ onMintFailed?(): Promise<'retry' | 'cancel'>; // getTokens() threw; default 'cancel'
80
+ }
81
+ ```
57
82
 
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.
83
+ Login by redirect: send the shopper to `loginReturnPath()` after login (check it with `isSafeReturnPath()`), return `'redirected'`, and reopen the builder when `consumeResumeMarker()` is true; the builder finishes the pending save. `hasDraft(productId)` says whether a draft is kept on this device.
59
84
 
60
- ## API
85
+ ## Saving
61
86
 
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. |
87
+ ```ts
88
+ onSaved?(saved: { hostProductId: string; productId: string; version: number }): void;
72
89
 
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.
90
+ customization?: {
91
+ record(link: { hostProductId: string; sourceProductId: string; savedProductId: string }): Promise<void>;
92
+ };
93
+ ```
74
94
 
75
- Helpers for login by redirect: `loginReturnPath()`, `isSafeReturnPath()`, `consumeResumeMarker()` and `hasDraft(productId)`.
95
+ `onSaved` fires once for every successful save, from `save()` or from the builder's own Save button. A first save makes the shopper's copy: `productId` is then the copy, and `onSaved` fires only after the copy exists and `customization.record()` resolved. It never fires for a failed save or a failed record; the next save records again.
76
96
 
77
97
  ## Errors
78
98
 
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`.
99
+ Every failure is a `BuilderError`: `code`, a `message` safe to show, `retryable`, `fatal`, and the builder's `reason`.
100
+
101
+ | Code | When |
102
+ | --------------------------------- | -------------------------------------------------------------------------------- |
103
+ | `CONFIG_INVALID` | Bad option, or a secret or script URL passed in. |
104
+ | `NOT_IN_BROWSER` | `load()` or `mount()` during SSR. |
105
+ | `LOAD_FAILED` | The builder did not load; the next call retries. |
106
+ | `VERSION_CONFLICT` | Another builder version already runs on the page. |
107
+ | `SESSION_FAILED` | `session.get()` failed or returned an invalid session. |
108
+ | `LICENSE_ERROR` | The license was refused. Fatal for the mount; `onLicenseError` is called. |
109
+ | `LOGIN_CANCELLED` | The shopper did not log in. |
110
+ | `SAVE_FAILED` | The save failed, or `customization.record()` failed (`reason: 'customization'`). |
111
+ | `SAVE_TIMEOUT` | No outcome within `saveTimeoutMs`. |
112
+ | `EXPORT_FAILED`, `EXPORT_TIMEOUT` | `externalExport.run()` failed or timed out. |
113
+ | `DESTROYED` | `destroy()` ran while the call was pending. |
114
+
115
+ Other callbacks: `onCloseRequested({ productId?, hasUnsavedChanges })`, `onStatusChange({ status, step?, message?, error? })` (load progress, never a save).
116
+
117
+ ## Angular
118
+
119
+ ```ts
120
+ @Component({ selector: 'app-designer', template: '<div #target style="height: 100%"></div>' })
121
+ export class DesignerComponent implements OnDestroy {
122
+ @Input({ required: true }) productRef!: string;
123
+ @ViewChild('target', { static: true }) target!: ElementRef<HTMLElement>;
124
+ private instance?: BuilderInstance;
125
+
126
+ constructor() {
127
+ afterNextRender(async () => {
128
+ const { createBuilder } = await import('@3dn-builder/builder'); // browser only
129
+ const builder = createBuilder({ session, login, onSaved: (s) => this.saved(s) });
130
+ this.instance = await builder.mount({ target: this.target.nativeElement, hostProductId: this.productRef });
131
+ });
132
+ }
133
+
134
+ ngOnDestroy() {
135
+ this.instance?.destroy();
136
+ }
137
+ }
138
+ ```
81
139
 
82
140
  ## Testing your integration
83
141
 
@@ -85,11 +143,10 @@ Every failure is a `BuilderError` with a stable `code`, a `message` that is safe
85
143
  import { installFakeBuilder, findFakeBuilders } from '@3dn-builder/builder/testing';
86
144
 
87
145
  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
146
+ const instance = await builder.mount({ target, hostProductId: 'mug' });
147
+ const saving = instance.save();
148
+ await findFakeBuilders()[0].emitSavedAsClone('clone-1');
149
+ await saving; // customization.record() and onSaved have run
93
150
  ```
94
151
 
95
152
  ## Developing this package
@@ -100,6 +157,4 @@ npx nx lint builder-host
100
157
  npx nx build builder-host # dist/libs/builder-host
101
158
  ```
102
159
 
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.
160
+ Event names and payload types come from `@three-dn/constants` and are bundled into the published files. The build stamps the version from the root `package.json`; the `publish-sdk` job in `.github/workflows/deploy.yml` publishes each production release.
package/index.d.ts CHANGED
@@ -102,6 +102,13 @@ interface CustomizationLink {
102
102
  sourceProductId: string;
103
103
  savedProductId: string;
104
104
  }
105
+ /** One successful save, however it started: `save()` or the builder's own Save button. */
106
+ interface SavedDesignEvent {
107
+ hostProductId: string;
108
+ /** The saved 3DN product: the shopper's clone after a first save. */
109
+ productId: string;
110
+ version: number;
111
+ }
105
112
  interface BuilderHostConfig {
106
113
  /** Default `'production'`. */
107
114
  environment?: BuilderEnvironment;
@@ -126,6 +133,12 @@ interface BuilderHostConfig {
126
133
  customization?: {
127
134
  record(link: CustomizationLink): Promise<void>;
128
135
  };
136
+ /**
137
+ * Every successful save, including ones the shopper starts from the builder's
138
+ * own Save button. Fires after any clone mapping has been recorded; never
139
+ * when recording failed.
140
+ */
141
+ onSaved?(saved: SavedDesignEvent): void;
129
142
  onLicenseError?(error: BuilderError): void;
130
143
  onCloseRequested?(detail: CloseRequestedDetail): void;
131
144
  onStatusChange?(detail: BuilderStatusDetail): void;
@@ -146,6 +159,14 @@ interface MountOptions {
146
159
  designId?: string;
147
160
  /** Opt-in, GLB products only. Off unless passed. */
148
161
  externalExport?: ExternalExportOptions;
162
+ /**
163
+ * The host page's observability context, a JSON object string the builder
164
+ * reads as `observability-context`: which host app, tenant, site and user
165
+ * its own Sentry events belong to, and the page's trace to join. Set before
166
+ * the element connects; update it with `setObservabilityContext()`. The
167
+ * builder drops keys it does not allow and ignores an invalid value.
168
+ */
169
+ observabilityContext?: string;
149
170
  }
150
171
  interface ResolvedConfig extends BuilderHostConfig {
151
172
  environment: BuilderEnvironment;
@@ -205,6 +226,11 @@ declare class BuilderInstance {
205
226
  /** Removes the shopper layer. The site layer stays and the builder revokes the session. */
206
227
  logout(): void;
207
228
  destroy(): void;
229
+ /**
230
+ * Replaces the host page's observability context, for example once the
231
+ * page's own Sentry has started and a trace exists to join.
232
+ */
233
+ setObservabilityContext(context: string): void;
208
234
  /** Used by the controller to hand every instance the shared fresh license token. */
209
235
  applyLicense(licenseToken: string): void;
210
236
  get isDestroyed(): boolean;
@@ -212,6 +238,7 @@ declare class BuilderInstance {
212
238
  private setLoginPending;
213
239
  private onLicenseError;
214
240
  private onDesignSaved;
241
+ private notifySaved;
215
242
  private onProductCloned;
216
243
  private recordLink;
217
244
  private finishSave;
@@ -269,4 +296,4 @@ declare function consumeResumeMarker(): boolean;
269
296
  declare function hasDraft(productId: string): boolean;
270
297
 
271
298
  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 };
299
+ export type { Builder, BuilderEnvironment, BuilderErrorCode, BuilderErrorReason, BuilderHostConfig, BuilderMode, BuilderSession, BuilderVersion, CustomizationLink, ExternalExportOptions, LoginOutcome, MountOptions, SavedDesign, SavedDesignEvent, ShopperTokens };
package/index.js CHANGED
@@ -1,2 +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};
1
+ import{b as R,c as k,d as P,e as _,f as B,g as O,h as C,j as m,k as A,l as N,m as M,n as F,p as U,q as V}from"./chunks/chunk-F4FUPJ4Y.js";var s=class t extends Error{constructor(e,n,r={}){var o,i;super(n),this.name="BuilderError",this.code=e,this.retryable=(o=r.retryable)!=null?o:!1,this.fatal=(i=r.fatal)!=null?i:!1,this.reason=r.reason,this.cause=r.cause,Object.setPrototypeOf(this,t.prototype)}},H={conflict:"Someone else saved this design first.",forbidden:"This shopper may not save this design.",unauthorized:"The shopper could not be signed in to save.","login-cancelled":"The design was not saved because the shopper did not log in.","installation-inactive":"This site's 3DN installation is not active.",license:"This page is not licensed to use the 3DN builder.",error:"The design could not be saved. Check the connection and try again."};function q(t){var n;let e=(n=H[t.reason])!=null?n:H.error;return t.reason==="login-cancelled"?new s("LOGIN_CANCELLED",e,{retryable:!0,reason:t.reason,cause:t}):new s("SAVE_FAILED",e,{retryable:t.retryable,reason:t.reason,cause:t})}function $(t){let e=t==="ORIGIN_NOT_ALLOWED"?"This website is not on the 3DN license. Add its hostname to the license.":"The 3DN license token was rejected. The license may be suspended, revoked or expired.";return new s("LICENSE_ERROR",e,{fatal:!0,reason:t,cause:{code:t}})}function g(){return new s("DESTROYED","The builder was destroyed.")}function a(t){return new s("CONFIG_INVALID",t,{fatal:!0})}var oe=45e3,j=12e4,ie=new Set(["environment","version","saveTimeoutMs","session","login","customization","onSaved","onLicenseError","onCloseRequested","onStatusChange","debug"]),se=new Set(["target","hostProductId","mode","designId","externalExport","observabilityContext"]),ae=/^\d+\.\d+\.\d+$/,de=/^tdn_inst_/;function b(t){return ae.test(t)}function u(t){return typeof t=="object"&&t!==null}function f(t){return typeof t=="function"}function z(t){return typeof t=="number"&&Number.isFinite(t)&&t>0}function G(t,e=0){return typeof t=="string"?de.test(t):!u(t)||e>3?!1:Object.values(t).some(n=>G(n,e+1))}function X(t,e,n){for(let r of Object.keys(t))if(!e.has(r))throw/url|src|host|origin/i.test(r)?a(`${n}: "${r}" is not allowed. The SDK decides where the builder loads from; choose an environment and a version instead.`):a(`${n}: unknown option "${r}".`)}function K(t){var h,l,c;if(!u(t))throw a("createBuilder() needs a configuration object.");if(G(t))throw a("createBuilder(): an installation key was passed to the browser. Installation keys stay on your server; pass a session callback instead.");X(t,ie,"createBuilder()");let e=(h=t.environment)!=null?h:"production";if(e!=="production"&&e!=="staging")throw a(`createBuilder(): environment must be "production" or "staging", got ${JSON.stringify(e)}.`);let n=(l=t.version)!=null?l:"latest";if(typeof n!="string"||n!=="latest"&&!b(n))throw a(`createBuilder(): version must be "latest" or an exact release such as "1.2.3", got ${JSON.stringify(n)}.`);let r=(c=t.saveTimeoutMs)!=null?c:oe;if(!z(r))throw a("createBuilder(): saveTimeoutMs must be a positive number of milliseconds.");let o=t.session;if(!u(o)||!f(o.get)||!f(o.refreshLicense))throw a("createBuilder(): session.get() and session.refreshLicense() are required. They call your session endpoint.");let i=t.login;if(i!==void 0){if(!u(i)||!f(i.getTokens)||!f(i.onRequired))throw a("createBuilder(): login needs getTokens() (your login-token endpoint) and onRequired() (your login UI).");if(i.onMintFailed!==void 0&&!f(i.onMintFailed))throw a("createBuilder(): login.onMintFailed must be a function.")}let d=t.customization;if(d!==void 0&&(!u(d)||!f(d.record)))throw a("createBuilder(): customization.record() is required when customization is set. It calls your customization endpoint.");for(let T of["onSaved","onLicenseError","onCloseRequested","onStatusChange"])if(t[T]!==void 0&&!f(t[T]))throw a(`createBuilder(): ${T} must be a function.`);return{...t,environment:e,version:n,saveTimeoutMs:r}}function W(t){var d;if(!u(t))throw a("mount() needs an options object.");X(t,se,"mount()");let e=t.target;if(typeof HTMLElement=="undefined"||!(e instanceof HTMLElement))throw a("mount(): target must be an HTMLElement on the page.");let n=t.hostProductId;if(typeof n!="string"||n.trim()==="")throw a("mount(): hostProductId must be your own, non-empty product reference.");let r=(d=t.mode)!=null?d:"designer";if(r!=="designer"&&r!=="viewer")throw a(`mount(): mode must be "designer" or "viewer", got ${JSON.stringify(r)}.`);let o=t.designId;if(o!==void 0&&(typeof o!="string"||o===""))throw a("mount(): designId must be a non-empty string.");t.observabilityContext!==void 0&&w(t.observabilityContext,"mount()");let i=t.externalExport;if(i!==void 0){if(!u(i)||!f(i.run))throw a("mount(): externalExport.run() is required when externalExport is set.");if(i.timeoutMs!==void 0&&!z(i.timeoutMs))throw a("mount(): externalExport.timeoutMs must be a positive number of milliseconds.")}return t}function w(t,e){let n;try{n=typeof t=="string"?JSON.parse(t):void 0}catch{n=void 0}if(!u(n)||Array.isArray(n))throw a(`${e}: observabilityContext must be a JSON object string.`);return t}function S(t){let e=o=>new s("SESSION_FAILED",`session.get() returned an invalid session: ${o}.`,{retryable:!1});if(!u(t))throw e("expected an object");if(typeof t.licenseToken!="string"||t.licenseToken==="")throw e("licenseToken is missing");if(typeof t.productId!="string"||t.productId==="")throw e("productId is missing");if(t.viewToken!=null&&typeof t.viewToken!="string")throw e("viewToken must be a string");let n=t.token!=null,r=t.refreshToken!=null;if(n!==r)throw e("token and refreshToken must be returned together");if(n&&(typeof t.token!="string"||typeof t.refreshToken!="string"))throw e("token and refreshToken must be strings");return t}function Y(t){if(t===null)return null;if(!u(t)||typeof t.token!="string"||t.token===""||typeof t.refreshToken!="string"||t.refreshToken==="")throw new s("SESSION_FAILED","login.getTokens() must resolve { token, refreshToken } or null.");return t}var ue=/token|secret|key|authorization/i;function D(t,e=0){if(t===null||typeof t!="object"||e>4)return t;if(Array.isArray(t))return t.map(r=>D(r,e+1));let n={};for(let[r,o]of Object.entries(t))n[r]=ue.test(r)&&o!=null?"[redacted]":D(o,e+1);return n}function J(t){return t?(e,n)=>{n===void 0?console.debug(`[3DN] ${e}`):console.debug(`[3DN] ${e}`,D(n))}:()=>{}}var v="threed-builder",ee={production:"https://3d-builder.3dn.app",staging:void 0},le=3e4,Q=Symbol.for("@3dn-builder/builder/loader");function ce(){let t=globalThis,e=t[Q];return e||(e={},t[Q]=e),e}function he(){return typeof window!="undefined"&&typeof document!="undefined"&&typeof customElements!="undefined"}function te(t){if(!ee[t])throw a(`createBuilder(): the "${t}" environment is not available in this SDK release.`)}function p(t,e){return new s("LOAD_FAILED",t,{retryable:!0,cause:e})}async function fe(t,e){if(e!=="latest")return e;let n;try{n=await fetch(`${t}/version-info.json`,{cache:"no-store",credentials:"omit"})}catch(i){throw p("The 3DN builder could not be reached.",i)}if(!n.ok)throw p(`The 3DN builder release could not be resolved (HTTP ${n.status}).`);let r=await n.json().catch(()=>null),o=r==null?void 0:r.version;if(typeof o!="string"||!b(o))throw p("The 3DN builder release information was not valid.");return o}function pe(t){return new Promise((e,n)=>{let r=document.createElement("script");r.type="module",r.src=t,r.crossOrigin="anonymous",r.onload=()=>e(r),r.onerror=()=>{r.remove(),n(p("The 3DN builder failed to load."))},document.head.appendChild(r)})}function ge(){return new Promise((t,e)=>{let n=setTimeout(()=>e(p("The 3DN builder loaded but did not start.")),le);customElements.whenDefined(v).then(()=>{clearTimeout(n),t()},e)})}function Z(t,e,n){if(t.environment&&t.environment!==e)throw new s("VERSION_CONFLICT",`The ${t.environment} builder already runs on this page; ${e} cannot load beside it.`,{fatal:!0});if(n!=="latest"&&t.version&&t.version!==n)throw new s("VERSION_CONFLICT",`Builder ${t.version} already runs on this page; ${n} cannot load beside it.`,{fatal:!0})}async function ne(t,e){if(!he())throw new s("NOT_IN_BROWSER","The 3DN builder can only load in a browser.");let n=ce();if(n.loading){await n.loading,Z(n,t,e);return}if(customElements.get(v))return;let r=ee[t];if(!r)throw a(`The "${t}" environment is not available in this SDK release.`);n.environment=t,n.loading=(async()=>{let o=await fe(r,e);n.version=o;let i=await pe(`${r}/builder-${o}.js`);try{await ge()}catch(d){throw i.remove(),d}})();try{await n.loading}catch(o){throw n.loading=void 0,n.environment=void 0,n.version=void 0,o instanceof s?o:p("The 3DN builder failed to load.",o)}Z(n,t,e)}function re(){let t,e,n=new Promise((r,o)=>{t=r,e=o});return n.catch(()=>{}),{promise:n,resolve:t,reject:e}}var L=class{constructor(e,n){this.onExpire=n;this.startedAt=0;this.remaining=e}start(){this.handle===void 0&&(this.startedAt=Date.now(),this.handle=setTimeout(this.onExpire,this.remaining))}pause(){this.handle!==void 0&&(clearTimeout(this.handle),this.handle=void 0,this.remaining=Math.max(0,this.remaining-(Date.now()-this.startedAt)))}stop(){this.handle!==void 0&&clearTimeout(this.handle),this.handle=void 0}},E=class t{constructor(e,n,r){this.ctx=e;this.options=n;this.epoch=0;this.destroyed=!1;this.loginPending=!1;this.exports=new Set;this.listeners=[];this.currentProductId=r.productId,this.element=this.createElement(r)}static async mount(e,n){let r;try{r=await e.config.session.get({hostProductId:n.hostProductId})}catch(d){throw new s("SESSION_FAILED","The builder session could not be loaded.",{retryable:!0,cause:d})}let o=S(r),i=new t(e,n,o);return n.target.appendChild(i.element),e.log("mounted",{hostProductId:n.hostProductId,productId:o.productId}),i}get productId(){return this.currentProductId}get hostProductId(){return this.options.hostProductId}createElement(e){let n=document.createElement(v);return this.attachListeners(n),n.setAttribute("product",e.productId),n.setAttribute("license-token",e.licenseToken),e.viewToken&&n.setAttribute("view-token",e.viewToken),e.token&&e.refreshToken&&I(n,{token:e.token,refreshToken:e.refreshToken}),this.options.designId&&n.setAttribute("design-id",this.options.designId),this.options.mode==="viewer"&&n.setAttribute("display-only","true"),this.options.externalExport&&n.setAttribute("external-export","true"),this.options.observabilityContext&&n.setAttribute("observability-context",this.options.observabilityContext),n.style.display="block",n.style.width="100%",n.style.height="100%",n}attachListeners(e){let n=(r,o)=>{let i=d=>{d.target!==e||this.destroyed||(this.ctx.log(r,d.detail),o(d.detail))};e.addEventListener(r,i),this.listeners.push([r,i])};n(C,r=>{var o;this.answerLogin((o=r==null?void 0:r.action)!=null?o:"save")}),n(F,()=>{this.ctx.refreshLicense()}),n(A,r=>this.onLicenseError(r)),n(N,r=>{this.onDesignSaved(r)}),n(M,r=>this.finishSave(q(r))),n(O,r=>{this.onProductCloned(r)}),n(V,r=>{var o,i;return(i=(o=this.ctx.config).onCloseRequested)==null?void 0:i.call(o,r)}),n(k,r=>{var o,i;return(i=(o=this.ctx.config).onStatusChange)==null?void 0:i.call(o,r)}),this.options.externalExport&&n(P,r=>{this.answerExport(r)})}open(){this.assertUsable(),R(this.element)}save(){if(this.destroyed)return Promise.reject(g());if(this.fatal)return Promise.reject(this.fatal);if(this.saveRun)return this.saveRun.result.promise;let e={result:re(),timer:new L(this.ctx.config.saveTimeoutMs,()=>this.finishSave(new s("SAVE_TIMEOUT","Saving the design took too long. Try again.",{retryable:!0})))};return this.saveRun=e,this.loginPending||e.timer.start(),U(this.element),e.result.promise}async refreshSession(){this.assertUsable();let e=this.epoch,n;try{n=await this.ctx.config.session.get({hostProductId:this.options.hostProductId})}catch(o){throw new s("SESSION_FAILED","The builder session could not be refreshed.",{retryable:!0,cause:o})}let r=S(n);e!==this.epoch||this.destroyed||(this.applyLicense(r.licenseToken),r.viewToken&&this.element.setAttribute("view-token",r.viewToken),r.token&&r.refreshToken&&I(this.element,{token:r.token,refreshToken:r.refreshToken}),this.fatal=void 0)}logout(){this.destroyed||(this.epoch++,this.element.removeAttribute("bearer-token"),this.element.removeAttribute("refresh-token"),this.loginPending&&(this.loginPending=!1,this.finishSave(new s("LOGIN_CANCELLED","The shopper logged out before the design was saved.",{retryable:!0}))))}destroy(){var e;if(!this.destroyed){this.epoch++,this.finishSave(g()),(e=this.cloneWait)==null||e.reject(g()),this.cloneWait=void 0;for(let n of this.exports)n.abort();this.destroyed=!0;for(let[n,r]of this.listeners)this.element.removeEventListener(n,r);this.listeners.length=0,this.element.remove(),this.ctx.onDestroyed(this),this.ctx.log("destroyed")}}setObservabilityContext(e){this.assertUsable(),this.element.setAttribute("observability-context",w(e,"setObservabilityContext()"))}applyLicense(e){this.destroyed||this.element.setAttribute("license-token",e)}get isDestroyed(){return this.destroyed}async answerLogin(e){if(this.loginPending)return;let n=this.ctx.config.login;if(!n){m(this.element,{action:e});return}let r=this.epoch,o=()=>r!==this.epoch||this.destroyed;this.setLoginPending(!0);let i=!1;try{for(;;){let d;try{d=Y(await n.getTokens())}catch(l){if(o())return;this.ctx.log("login.getTokens() failed",{cause:String(l)});let c=n.onMintFailed?await n.onMintFailed():"cancel";if(o())return;if(c==="retry")continue;m(this.element,{action:e});return}if(o())return;if(d){I(this.element,d);return}let h=await n.onRequired(e);if(o())return;if(h!=="logged-in"){if(h==="redirected"){i=!0;return}m(this.element,{action:e});return}}}finally{!i&&r===this.epoch&&this.setLoginPending(!1)}}setLoginPending(e){this.loginPending=e,this.saveRun&&(e?this.saveRun.timer.pause():this.saveRun.timer.start())}onLicenseError(e){var r,o;let n=$(e.code);this.fatal=n,this.finishSave(n),(o=(r=this.ctx.config).onLicenseError)==null||o.call(r,n)}async onDesignSaved(e){e.productId!==this.currentProductId&&!this.cloneWait&&(this.cloneWait=re());try{this.cloneWait&&await this.cloneWait.promise,this.unrecordedLink&&await this.recordLink(this.unrecordedLink)}catch(r){this.finishSave(r instanceof s?r:new s("SAVE_FAILED","The design was saved but could not be linked to this shopper.",{retryable:!0,reason:"customization",cause:r}));return}if(this.destroyed)return;let n={productId:e.productId,version:e.version};this.notifySaved(n),this.finishSave(void 0,n)}notifySaved(e){var n,r;try{(r=(n=this.ctx.config).onSaved)==null||r.call(n,{hostProductId:this.options.hostProductId,...e})}catch(o){this.ctx.log("onSaved threw",{cause:String(o)})}}async onProductCloned(e){let n={hostProductId:this.options.hostProductId,sourceProductId:e.originalProductId,savedProductId:e.newProductId};this.currentProductId=e.newProductId;let r=this.cloneWait;this.cloneWait=void 0;try{await this.recordLink(n),r==null||r.resolve()}catch(o){r==null||r.reject(o)}}async recordLink(e){let n=this.ctx.config.customization;if(n)try{await n.record(e),this.unrecordedLink===e&&(this.unrecordedLink=void 0)}catch(r){throw this.unrecordedLink=e,new s("SAVE_FAILED","The design was saved but could not be linked to this shopper. Try saving again.",{retryable:!0,reason:"customization",cause:r})}}finishSave(e,n){let r=this.saveRun;r&&(this.saveRun=void 0,r.timer.stop(),e?r.result.reject(e):n&&r.result.resolve(n))}async answerExport(e){var h;let n=this.options.externalExport;if(!n)return;let r=new AbortController;this.exports.add(r);let o=!1,i=setTimeout(()=>{o=!0,r.abort()},(h=n.timeoutMs)!=null?h:j),d=new Promise((l,c)=>r.signal.addEventListener("abort",()=>c(new Error("aborted"))));try{let l=await Promise.race([n.run(e,r.signal),d]);B(this.element,{files:l})}catch(l){let c=o?"EXPORT_TIMEOUT":"EXPORT_FAILED";this.ctx.log("export failed",{code:c,cause:String(l)}),this.replyExportFailure(c)}finally{clearTimeout(i),this.exports.delete(r)}}replyExportFailure(e){let n={files:[],error:{code:e}};this.element.dispatchEvent(new CustomEvent(_,{bubbles:!1,composed:!1,detail:n}))}assertUsable(){if(this.destroyed)throw g()}};function I(t,e){t.setAttribute("refresh-token",e.refreshToken),t.setAttribute("bearer-token",e.token)}var x=class{constructor(e){this.config=e;this.instances=[];this.log=J(e.debug),this.context={config:e,log:this.log,refreshLicense:()=>this.refreshLicense().catch(()=>{}),onDestroyed:n=>{let r=this.instances.indexOf(n);r>=0&&this.instances.splice(r,1)}}}get current(){return this.instances[this.instances.length-1]}load(){return ne(this.config.environment,this.config.version)}async mount(e){let n=W(e);await this.load();let r=await E.mount(this.context,n);return this.instances.push(r),r}open(){this.requireCurrent().open()}save(){let e=this.current;return e?e.save():Promise.reject(this.notMounted())}refreshSession(){let e=this.current;return e?e.refreshSession():Promise.reject(this.notMounted())}refreshLicense(){return this.licenseRefresh||(this.licenseRefresh=(async()=>{try{let e=await this.config.session.refreshLicense(),n=e==null?void 0:e.licenseToken;if(typeof n!="string"||n==="")throw new s("SESSION_FAILED","session.refreshLicense() must resolve { licenseToken }.");for(let r of this.instances)r.applyLicense(n);this.log("license refreshed")}catch(e){throw this.log("license refresh failed",{cause:String(e)}),e instanceof s?e:new s("SESSION_FAILED","A fresh license token could not be fetched.",{retryable:!0,cause:e})}finally{this.licenseRefresh=void 0}})()),this.licenseRefresh}logout(){for(let e of this.instances)e.logout();this.log("logged out")}destroy(){for(let e of[...this.instances])e.destroy()}requireCurrent(){let e=this.current;if(!e)throw this.notMounted();return e}notMounted(){return a("No builder is mounted. Call mount() first.")}};function me(t){let e=K(t);return te(e.environment),new x(e)}var y="tdn_resume",ve="tdn-draft-index";function Ee(){let t=new URL(window.location.href);return t.searchParams.set(y,"1"),t.pathname+t.search+t.hash}function ye(t){return/^\/(?![/\\])/.test(t)}function Te(){if(typeof window=="undefined")return!1;let t=new URL(window.location.href);return t.searchParams.get(y)!=="1"?!1:(t.searchParams.delete(y),window.history.replaceState(window.history.state,"",t.pathname+t.search+t.hash),!0)}function be(t){var e;if(typeof localStorage=="undefined")return!1;try{let n=JSON.parse((e=localStorage.getItem(ve))!=null?e:"{}");return Object.values(n).some(r=>(r==null?void 0:r.productId)===t)}catch{return!1}}export{s as BuilderError,y as RESUME_PARAM,Te as consumeResumeMarker,me as createBuilder,be as hasDraft,ye as isSafeReturnPath,Ee as loginReturnPath};
2
2
  //# sourceMappingURL=index.js.map
package/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../libs/builder-host/src/lib/errors.ts", "../../../libs/builder-host/src/lib/config.ts", "../../../libs/builder-host/src/lib/debug.ts", "../../../libs/builder-host/src/lib/loader.ts", "../../../libs/builder-host/src/lib/instance.ts", "../../../libs/builder-host/src/lib/builder.ts", "../../../libs/builder-host/src/lib/resume.ts"],
4
- "sourcesContent": ["import type {\n DesignSaveFailedDetail,\n DesignSaveFailureReason,\n LicenseErrorCode,\n} from '@three-dn/constants';\n\nexport type BuilderErrorCode =\n | 'CONFIG_INVALID'\n | 'NOT_IN_BROWSER'\n | 'LOAD_FAILED'\n | 'VERSION_CONFLICT'\n | 'SESSION_FAILED'\n | 'LICENSE_ERROR'\n | 'LOGIN_CANCELLED'\n | 'SAVE_FAILED'\n | 'SAVE_TIMEOUT'\n | 'EXPORT_FAILED'\n | 'EXPORT_TIMEOUT'\n | 'DESTROYED';\n\n/** The builder's own reason, plus `customization` when only the record failed. */\nexport type BuilderErrorReason =\n | DesignSaveFailureReason\n | LicenseErrorCode\n | 'customization';\n\nexport interface BuilderErrorOptions {\n retryable?: boolean;\n fatal?: boolean;\n reason?: BuilderErrorReason;\n cause?: unknown;\n}\n\n/**\n * Every failure the SDK reports. Hosts switch on `code`; `message` is safe to\n * show or log and never contains a token.\n */\nexport class BuilderError extends Error {\n readonly code: BuilderErrorCode;\n readonly retryable: boolean;\n /** The mount cannot continue until the host refreshes or recreates it. */\n readonly fatal: boolean;\n readonly reason?: BuilderErrorReason;\n readonly cause?: unknown;\n\n constructor(\n code: BuilderErrorCode,\n message: string,\n options: BuilderErrorOptions = {}\n ) {\n super(message);\n this.name = 'BuilderError';\n this.code = code;\n this.retryable = options.retryable ?? false;\n this.fatal = options.fatal ?? false;\n this.reason = options.reason;\n this.cause = options.cause;\n Object.setPrototypeOf(this, BuilderError.prototype);\n }\n}\n\nconst SAVE_FAILURE_MESSAGES: Record<DesignSaveFailureReason, string> = {\n conflict: 'Someone else saved this design first.',\n forbidden: 'This shopper may not save this design.',\n unauthorized: 'The shopper could not be signed in to save.',\n 'login-cancelled':\n 'The design was not saved because the shopper did not log in.',\n 'installation-inactive': \"This site's 3DN installation is not active.\",\n license: 'This page is not licensed to use the 3DN builder.',\n error: 'The design could not be saved. Check the connection and try again.',\n};\n\nexport function saveFailedError(detail: DesignSaveFailedDetail): BuilderError {\n const message =\n SAVE_FAILURE_MESSAGES[detail.reason] ?? SAVE_FAILURE_MESSAGES.error;\n if (detail.reason === 'login-cancelled') {\n return new BuilderError('LOGIN_CANCELLED', message, {\n retryable: true,\n reason: detail.reason,\n cause: detail,\n });\n }\n return new BuilderError('SAVE_FAILED', message, {\n retryable: detail.retryable,\n reason: detail.reason,\n cause: detail,\n });\n}\n\nexport function licenseError(code: LicenseErrorCode): BuilderError {\n const message =\n code === 'ORIGIN_NOT_ALLOWED'\n ? 'This website is not on the 3DN license. Add its hostname to the license.'\n : 'The 3DN license token was rejected. The license may be suspended, revoked or expired.';\n return new BuilderError('LICENSE_ERROR', message, {\n fatal: true,\n reason: code,\n cause: { code },\n });\n}\n\nexport function destroyedError(): BuilderError {\n return new BuilderError('DESTROYED', 'The builder was destroyed.');\n}\n\nexport function configError(message: string): BuilderError {\n return new BuilderError('CONFIG_INVALID', message, { fatal: true });\n}\n", "import type {\n BuilderStatusDetail,\n CloseRequestedDetail,\n ExportFile,\n ExportRequestedDetail,\n LoginRequiredAction,\n} from '@three-dn/constants';\nimport { BuilderError, configError } from './errors';\n\nexport type BuilderEnvironment = 'production' | 'staging';\nexport type BuilderVersion = 'latest' | `${number}.${number}.${number}`;\nexport type BuilderMode = 'designer' | 'viewer';\n\n/** What the host's session endpoint returns: site access plus the product to open. */\nexport interface BuilderSession {\n licenseToken: string;\n /** The 3DN product to open: the shopper's clone, else the global product. */\n productId: string;\n /** Required when the product to open is private. */\n viewToken?: string;\n /** Optional fast path: shopper tokens for a shopper the host backend verified. */\n token?: string;\n refreshToken?: string;\n}\n\n/** Shopper identity, from the host's login-token endpoint. */\nexport interface ShopperTokens {\n token: string;\n refreshToken: string;\n}\n\nexport type LoginOutcome = 'logged-in' | 'redirected' | 'cancelled';\n\n/** What the host stores when a save created the shopper's clone. */\nexport interface CustomizationLink {\n hostProductId: string;\n sourceProductId: string;\n savedProductId: string;\n}\n\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"]
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\n/** One successful save, however it started: `save()` or the builder's own Save button. */\nexport interface SavedDesignEvent {\n hostProductId: string;\n /** The saved 3DN product: the shopper's clone after a first save. */\n productId: string;\n version: number;\n}\n\nexport interface BuilderHostConfig {\n /** Default `'production'`. */\n environment?: BuilderEnvironment;\n /** `'latest'` or an exact builder release. Default `'latest'`. */\n version?: BuilderVersion;\n /** Default 45 000 ms. */\n saveTimeoutMs?: number;\n session: {\n get(request: { hostProductId: string }): Promise<BuilderSession>;\n refreshLicense(): Promise<Pick<BuilderSession, 'licenseToken'>>;\n };\n login?: {\n /** Answers `tdn-login-required`. Resolve `null` when the endpoint answers 401. */\n getTokens(): Promise<ShopperTokens | null>;\n /** The host's login UX for the action the builder gated. */\n onRequired(action: LoginRequiredAction): Promise<LoginOutcome>;\n /** `getTokens()` failed (5xx, network). Default: `'cancel'`. */\n onMintFailed?(): Promise<'retry' | 'cancel'>;\n };\n customization?: {\n record(link: CustomizationLink): Promise<void>;\n };\n /**\n * Every successful save, including ones the shopper starts from the builder's\n * own Save button. Fires after any clone mapping has been recorded; never\n * when recording failed.\n */\n onSaved?(saved: SavedDesignEvent): void;\n onLicenseError?(error: BuilderError): void;\n onCloseRequested?(detail: CloseRequestedDetail): void;\n onStatusChange?(detail: BuilderStatusDetail): void;\n /** Log lifecycle steps and events to the console. Tokens are always redacted. */\n debug?: boolean;\n}\n\nexport interface ExternalExportOptions {\n run(\n request: ExportRequestedDetail,\n signal: AbortSignal\n ): Promise<ExportFile[]>;\n /** Default 120 000 ms. */\n timeoutMs?: number;\n}\n\nexport interface MountOptions {\n target: HTMLElement;\n /** The host's own product reference, passed to `session.get()`. Never a 3DN id. */\n hostProductId: string;\n /** Default `'designer'`. `'viewer'` is the display-only mode. */\n mode?: BuilderMode;\n designId?: string;\n /** Opt-in, GLB products only. Off unless passed. */\n externalExport?: ExternalExportOptions;\n /**\n * The host page's observability context, a JSON object string the builder\n * reads as `observability-context`: which host app, tenant, site and user\n * its own Sentry events belong to, and the page's trace to join. Set before\n * the element connects; update it with `setObservabilityContext()`. The\n * builder drops keys it does not allow and ignores an invalid value.\n */\n observabilityContext?: string;\n}\n\nexport interface ResolvedConfig extends BuilderHostConfig {\n environment: BuilderEnvironment;\n version: BuilderVersion;\n saveTimeoutMs: number;\n}\n\nexport const DEFAULT_SAVE_TIMEOUT_MS = 45_000;\nexport const DEFAULT_EXPORT_TIMEOUT_MS = 120_000;\n\nconst CONFIG_KEYS = new Set([\n 'environment',\n 'version',\n 'saveTimeoutMs',\n 'session',\n 'login',\n 'customization',\n 'onSaved',\n 'onLicenseError',\n 'onCloseRequested',\n 'onStatusChange',\n 'debug',\n]);\n\nconst MOUNT_KEYS = new Set([\n 'target',\n 'hostProductId',\n 'mode',\n 'designId',\n 'externalExport',\n 'observabilityContext',\n]);\n\nconst VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\nconst INSTALLATION_KEY_PATTERN = /^tdn_inst_/;\n\nexport function isExactVersion(value: string): boolean {\n return VERSION_PATTERN.test(value);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isFunction(value: unknown): value is (...args: never[]) => unknown {\n return typeof value === 'function';\n}\n\nfunction isPositiveNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n/** Walks the config looking for anything that looks like an installation key. */\nfunction containsSecret(value: unknown, depth = 0): boolean {\n if (typeof value === 'string') return INSTALLATION_KEY_PATTERN.test(value);\n if (!isRecord(value) || depth > 3) return false;\n return Object.values(value).some((v) => containsSecret(v, depth + 1));\n}\n\nfunction rejectUnknownKeys(\n value: Record<string, unknown>,\n allowed: Set<string>,\n where: string\n): void {\n for (const key of Object.keys(value)) {\n if (allowed.has(key)) continue;\n if (/url|src|host|origin/i.test(key)) {\n throw configError(\n `${where}: \"${key}\" is not allowed. The SDK decides where the builder loads from; choose an environment and a version instead.`\n );\n }\n throw configError(`${where}: unknown option \"${key}\".`);\n }\n}\n\nexport function resolveConfig(config: unknown): ResolvedConfig {\n if (!isRecord(config)) {\n throw configError('createBuilder() needs a configuration object.');\n }\n if (containsSecret(config)) {\n throw configError(\n 'createBuilder(): an installation key was passed to the browser. Installation keys stay on your server; pass a session callback instead.'\n );\n }\n rejectUnknownKeys(config, CONFIG_KEYS, 'createBuilder()');\n\n const environment = config['environment'] ?? 'production';\n if (environment !== 'production' && environment !== 'staging') {\n throw configError(\n `createBuilder(): environment must be \"production\" or \"staging\", got ${JSON.stringify(\n environment\n )}.`\n );\n }\n\n const version = config['version'] ?? 'latest';\n if (\n typeof version !== 'string' ||\n (version !== 'latest' && !isExactVersion(version))\n ) {\n throw configError(\n `createBuilder(): version must be \"latest\" or an exact release such as \"1.2.3\", got ${JSON.stringify(\n version\n )}.`\n );\n }\n\n const saveTimeoutMs = config['saveTimeoutMs'] ?? DEFAULT_SAVE_TIMEOUT_MS;\n if (!isPositiveNumber(saveTimeoutMs)) {\n throw configError(\n 'createBuilder(): saveTimeoutMs must be a positive number of milliseconds.'\n );\n }\n\n const session = config['session'];\n if (\n !isRecord(session) ||\n !isFunction(session['get']) ||\n !isFunction(session['refreshLicense'])\n ) {\n throw configError(\n 'createBuilder(): session.get() and session.refreshLicense() are required. They call your session endpoint.'\n );\n }\n\n const login = config['login'];\n if (login !== undefined) {\n if (\n !isRecord(login) ||\n !isFunction(login['getTokens']) ||\n !isFunction(login['onRequired'])\n ) {\n throw configError(\n 'createBuilder(): login needs getTokens() (your login-token endpoint) and onRequired() (your login UI).'\n );\n }\n if (\n login['onMintFailed'] !== undefined &&\n !isFunction(login['onMintFailed'])\n ) {\n throw configError(\n 'createBuilder(): login.onMintFailed must be a function.'\n );\n }\n }\n\n const customization = config['customization'];\n if (\n customization !== undefined &&\n (!isRecord(customization) || !isFunction(customization['record']))\n ) {\n throw configError(\n 'createBuilder(): customization.record() is required when customization is set. It calls your customization endpoint.'\n );\n }\n\n for (const key of [\n 'onSaved',\n 'onLicenseError',\n 'onCloseRequested',\n 'onStatusChange',\n ] as const) {\n if (config[key] !== undefined && !isFunction(config[key])) {\n throw configError(`createBuilder(): ${key} must be a function.`);\n }\n }\n\n return {\n ...(config as unknown as BuilderHostConfig),\n environment,\n version: version as BuilderVersion,\n saveTimeoutMs,\n };\n}\n\nexport function validateMountOptions(options: unknown): MountOptions {\n if (!isRecord(options)) {\n throw configError('mount() needs an options object.');\n }\n rejectUnknownKeys(options, MOUNT_KEYS, 'mount()');\n\n const target = options['target'];\n if (typeof HTMLElement === 'undefined' || !(target instanceof HTMLElement)) {\n throw configError('mount(): target must be an HTMLElement on the page.');\n }\n const hostProductId = options['hostProductId'];\n if (typeof hostProductId !== 'string' || hostProductId.trim() === '') {\n throw configError(\n 'mount(): hostProductId must be your own, non-empty product reference.'\n );\n }\n const mode = options['mode'] ?? 'designer';\n if (mode !== 'designer' && mode !== 'viewer') {\n throw configError(\n `mount(): mode must be \"designer\" or \"viewer\", got ${JSON.stringify(\n mode\n )}.`\n );\n }\n const designId = options['designId'];\n if (\n designId !== undefined &&\n (typeof designId !== 'string' || designId === '')\n ) {\n throw configError('mount(): designId must be a non-empty string.');\n }\n if (options['observabilityContext'] !== undefined) {\n validateObservabilityContext(options['observabilityContext'], 'mount()');\n }\n const externalExport = options['externalExport'];\n if (externalExport !== undefined) {\n if (!isRecord(externalExport) || !isFunction(externalExport['run'])) {\n throw configError(\n 'mount(): externalExport.run() is required when externalExport is set.'\n );\n }\n if (\n externalExport['timeoutMs'] !== undefined &&\n !isPositiveNumber(externalExport['timeoutMs'])\n ) {\n throw configError(\n 'mount(): externalExport.timeoutMs must be a positive number of milliseconds.'\n );\n }\n }\n return options as unknown as MountOptions;\n}\n\n/** A JSON object string: what the builder's `observability-context` accepts. */\nexport function validateObservabilityContext(\n value: unknown,\n where: string\n): string {\n let parsed: unknown;\n try {\n parsed = typeof value === 'string' ? JSON.parse(value) : undefined;\n } catch {\n parsed = undefined;\n }\n if (!isRecord(parsed) || Array.isArray(parsed)) {\n throw configError(\n `${where}: observabilityContext must be a JSON object string.`\n );\n }\n return value as string;\n}\n\n/** Checks the shape of a session response before anything reaches the element. */\nexport function validateSession(session: unknown): BuilderSession {\n const fail = (what: string) =>\n new BuilderError(\n 'SESSION_FAILED',\n `session.get() returned an invalid session: ${what}.`,\n {\n retryable: false,\n }\n );\n if (!isRecord(session)) throw fail('expected an object');\n if (\n typeof session['licenseToken'] !== 'string' ||\n session['licenseToken'] === ''\n ) {\n throw fail('licenseToken is missing');\n }\n if (typeof session['productId'] !== 'string' || session['productId'] === '') {\n throw fail('productId is missing');\n }\n if (\n session['viewToken'] != null &&\n typeof session['viewToken'] !== 'string'\n ) {\n throw fail('viewToken must be a string');\n }\n const hasToken = session['token'] != null;\n const hasRefresh = session['refreshToken'] != null;\n if (hasToken !== hasRefresh) {\n throw fail('token and refreshToken must be returned together');\n }\n if (\n hasToken &&\n (typeof session['token'] !== 'string' ||\n typeof session['refreshToken'] !== 'string')\n ) {\n throw fail('token and refreshToken must be strings');\n }\n return session as unknown as BuilderSession;\n}\n\nexport function validateShopperTokens(tokens: unknown): ShopperTokens | null {\n if (tokens === null) return null;\n if (\n !isRecord(tokens) ||\n typeof tokens['token'] !== 'string' ||\n tokens['token'] === '' ||\n typeof tokens['refreshToken'] !== 'string' ||\n tokens['refreshToken'] === ''\n ) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'login.getTokens() must resolve { token, refreshToken } or null.'\n );\n }\n return tokens as unknown as ShopperTokens;\n}\n", "const TOKEN_KEY = /token|secret|key|authorization/i;\n\n/** Copies a value for logging with every token-like field replaced. */\nexport function redact(value: unknown, depth = 0): unknown {\n if (value === null || typeof value !== 'object' || depth > 4) return value;\n if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value)) {\n out[key] =\n TOKEN_KEY.test(key) && v != null ? '[redacted]' : redact(v, depth + 1);\n }\n return out;\n}\n\nexport type DebugLog = (message: string, detail?: unknown) => void;\n\nexport function createDebugLog(enabled: boolean | undefined): DebugLog {\n if (!enabled) return () => undefined;\n return (message, detail) => {\n if (detail === undefined) console.debug(`[3DN] ${message}`);\n else console.debug(`[3DN] ${message}`, redact(detail));\n };\n}\n", "import type { BuilderEnvironment, BuilderVersion } from './config';\nimport { isExactVersion } from './config';\nimport { BuilderError, configError } from './errors';\n\nexport const BUILDER_TAG = 'threed-builder';\n\n/**\n * Where each environment's builder is served from. SDK-owned: hosts choose an\n * environment, never a location. Staging stays unset until its location is\n * confirmed; createBuilder() refuses `environment: 'staging'` until then.\n */\nconst BUNDLE_HOSTS: Record<BuilderEnvironment, string | undefined> = {\n production: 'https://3d-builder.3dn.app',\n staging: undefined,\n};\n\nconst DEFINE_TIMEOUT_MS = 30_000;\n\n/** Shared by every copy of the SDK on the page: one custom element registry. */\ninterface PageLoaderState {\n environment?: BuilderEnvironment;\n /** The exact version loaded, once known. */\n version?: string;\n loading?: Promise<void>;\n}\n\nconst STATE_KEY = Symbol.for('@3dn-builder/builder/loader');\n\nfunction pageState(): PageLoaderState {\n const holder = globalThis as unknown as Record<\n symbol,\n PageLoaderState | undefined\n >;\n let state = holder[STATE_KEY];\n if (!state) {\n state = {};\n holder[STATE_KEY] = state;\n }\n return state;\n}\n\nexport function isBrowser(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof document !== 'undefined' &&\n typeof customElements !== 'undefined'\n );\n}\n\nexport function assertEnvironmentAvailable(\n environment: BuilderEnvironment\n): void {\n if (!BUNDLE_HOSTS[environment]) {\n throw configError(\n `createBuilder(): the \"${environment}\" environment is not available in this SDK release.`\n );\n }\n}\n\nfunction loadFailed(message: string, cause?: unknown): BuilderError {\n return new BuilderError('LOAD_FAILED', message, { retryable: true, cause });\n}\n\nasync function resolveVersion(\n host: string,\n requested: BuilderVersion\n): Promise<string> {\n if (requested !== 'latest') return requested;\n let response: Response;\n try {\n response = await fetch(`${host}/version-info.json`, {\n cache: 'no-store',\n credentials: 'omit',\n });\n } catch (cause) {\n throw loadFailed('The 3DN builder could not be reached.', cause);\n }\n if (!response.ok) {\n throw loadFailed(\n `The 3DN builder release could not be resolved (HTTP ${response.status}).`\n );\n }\n const info = (await response.json().catch(() => null)) as {\n version?: unknown;\n } | null;\n const version = info?.version;\n if (typeof version !== 'string' || !isExactVersion(version)) {\n throw loadFailed('The 3DN builder release information was not valid.');\n }\n return version;\n}\n\nfunction injectScript(src: string): Promise<HTMLScriptElement> {\n return new Promise((resolve, reject) => {\n const script = document.createElement('script');\n script.type = 'module';\n script.src = src;\n script.crossOrigin = 'anonymous';\n script.onload = () => resolve(script);\n script.onerror = () => {\n script.remove();\n reject(loadFailed('The 3DN builder failed to load.'));\n };\n document.head.appendChild(script);\n });\n}\n\nfunction whenDefined(): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(\n () => reject(loadFailed('The 3DN builder loaded but did not start.')),\n DEFINE_TIMEOUT_MS\n );\n customElements.whenDefined(BUILDER_TAG).then(() => {\n clearTimeout(timer);\n resolve();\n }, reject);\n });\n}\n\nfunction assertCompatible(\n state: PageLoaderState,\n environment: BuilderEnvironment,\n requested: BuilderVersion\n): void {\n if (state.environment && state.environment !== environment) {\n throw new BuilderError(\n 'VERSION_CONFLICT',\n `The ${state.environment} builder already runs on this page; ${environment} cannot load beside it.`,\n { fatal: true }\n );\n }\n if (requested !== 'latest' && state.version && state.version !== requested) {\n throw new BuilderError(\n 'VERSION_CONFLICT',\n `Builder ${state.version} already runs on this page; ${requested} cannot load beside it.`,\n { fatal: true }\n );\n }\n}\n\n/**\n * Loads the builder once per page. A failed load leaves nothing behind, so the\n * next call tries again.\n */\nexport async function loadBuilder(\n environment: BuilderEnvironment,\n requested: BuilderVersion\n): Promise<void> {\n if (!isBrowser()) {\n throw new BuilderError(\n 'NOT_IN_BROWSER',\n 'The 3DN builder can only load in a browser.'\n );\n }\n const state = pageState();\n\n if (state.loading) {\n await state.loading;\n assertCompatible(state, environment, requested);\n return;\n }\n // Defined by something other than this SDK (an older embed, or the testing\n // fake): its version is unknown, so use it as it is.\n if (customElements.get(BUILDER_TAG)) return;\n\n const host = BUNDLE_HOSTS[environment];\n if (!host) {\n throw configError(\n `The \"${environment}\" environment is not available in this SDK release.`\n );\n }\n\n state.environment = environment;\n state.loading = (async () => {\n const version = await resolveVersion(host, requested);\n state.version = version;\n const script = await injectScript(`${host}/builder-${version}.js`);\n try {\n await whenDefined();\n } catch (error) {\n script.remove();\n throw error;\n }\n })();\n\n try {\n await state.loading;\n } catch (error) {\n state.loading = undefined;\n state.environment = undefined;\n state.version = undefined;\n throw error instanceof BuilderError\n ? error\n : loadFailed('The 3DN builder failed to load.', error);\n }\n assertCompatible(state, environment, requested);\n}\n", "import {\n THREED_BUILDER_CLOSE_REQUESTED_EVENT,\n THREED_BUILDER_DESIGN_SAVED_EVENT,\n THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT,\n THREED_BUILDER_EXPORT_COMPLETE_EVENT,\n THREED_BUILDER_EXPORT_REQUESTED_EVENT,\n THREED_BUILDER_LICENSE_ERROR_EVENT,\n THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,\n THREED_BUILDER_LOGIN_REQUIRED_EVENT,\n THREED_BUILDER_PRODUCT_CLONED_EVENT,\n THREED_BUILDER_STATUS_EVENT,\n dispatchExportComplete,\n dispatchLoginCancelled,\n dispatchOpenThreedBuilder,\n dispatchSaveRequest,\n type BuilderStatusDetail,\n type CloseRequestedDetail,\n type DesignSaveFailedDetail,\n type DesignSavedDetail,\n type ExportRequestedDetail,\n type LicenseErrorDetail,\n type LoginRequiredAction,\n type LoginRequiredDetail,\n type ProductClonedDetail,\n} from '@three-dn/constants';\nimport {\n DEFAULT_EXPORT_TIMEOUT_MS,\n validateObservabilityContext,\n validateSession,\n validateShopperTokens,\n type BuilderSession,\n type CustomizationLink,\n type MountOptions,\n type ResolvedConfig,\n type ShopperTokens,\n} from './config';\nimport type { DebugLog } from './debug';\nimport {\n BuilderError,\n destroyedError,\n licenseError,\n saveFailedError,\n} from './errors';\nimport { BUILDER_TAG } from './loader';\n\n/** What `save()` resolves with. `productId` is always the one to put in the cart. */\nexport interface SavedDesign {\n productId: string;\n version: number;\n}\n\nexport interface InstanceContext {\n config: ResolvedConfig;\n log: DebugLog;\n /** Single-flight license refresh shared by every instance of a controller. */\n refreshLicense(): Promise<void>;\n onDestroyed(instance: BuilderInstance): void;\n}\n\ninterface Deferred<T> {\n promise: Promise<T>;\n resolve(value: T): void;\n reject(error: unknown): void;\n}\n\nfunction deferred<T>(): Deferred<T> {\n let resolve!: (value: T) => void;\n let reject!: (error: unknown) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n // A rejection nobody awaits yet must not surface as unhandled.\n promise.catch(() => undefined);\n return { promise, resolve, reject };\n}\n\n/** A timer that stops counting while a login round-trip is pending. */\nclass PausableTimer {\n private handle?: ReturnType<typeof setTimeout>;\n private startedAt = 0;\n private remaining: number;\n\n constructor(ms: number, private readonly onExpire: () => void) {\n this.remaining = ms;\n }\n\n start(): void {\n if (this.handle !== undefined) return;\n this.startedAt = Date.now();\n this.handle = setTimeout(this.onExpire, this.remaining);\n }\n\n pause(): void {\n if (this.handle === undefined) return;\n clearTimeout(this.handle);\n this.handle = undefined;\n this.remaining = Math.max(\n 0,\n this.remaining - (Date.now() - this.startedAt)\n );\n }\n\n stop(): void {\n if (this.handle !== undefined) clearTimeout(this.handle);\n this.handle = undefined;\n }\n}\n\ninterface SaveRun {\n result: Deferred<SavedDesign>;\n timer: PausableTimer;\n}\n\ninterface ExportReply {\n files: { url: string; format: string; surface: string }[];\n error?: { code: 'EXPORT_FAILED' | 'EXPORT_TIMEOUT' };\n}\n\n/** One mounted `<threed-builder>`. */\nexport class BuilderInstance {\n readonly element: HTMLElement;\n private currentProductId: string;\n /** Bumped by logout() and destroy(): async results from an older epoch are dropped. */\n private epoch = 0;\n private destroyed = false;\n private fatal?: BuilderError;\n private loginPending = false;\n private saveRun?: SaveRun;\n /** A clone mapping whose record() failed; retried before the next save resolves. */\n private unrecordedLink?: CustomizationLink;\n /** Set between a save that changed the product id and its tdn-product-cloned. */\n private cloneWait?: Deferred<void>;\n private readonly exports = new Set<AbortController>();\n private readonly listeners: [string, EventListener][] = [];\n\n private constructor(\n private readonly ctx: InstanceContext,\n private readonly options: MountOptions,\n session: BuilderSession\n ) {\n this.currentProductId = session.productId;\n this.element = this.createElement(session);\n }\n\n /** Builds the element in the documented order and inserts it last. */\n static async mount(\n ctx: InstanceContext,\n options: MountOptions\n ): Promise<BuilderInstance> {\n let raw: unknown;\n try {\n raw = await ctx.config.session.get({\n hostProductId: options.hostProductId,\n });\n } catch (cause) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'The builder session could not be loaded.',\n {\n retryable: true,\n cause,\n }\n );\n }\n const session = validateSession(raw);\n const instance = new BuilderInstance(ctx, options, session);\n options.target.appendChild(instance.element);\n ctx.log('mounted', {\n hostProductId: options.hostProductId,\n productId: session.productId,\n });\n return instance;\n }\n\n /** The 3DN product the builder is editing now: the clone, after a first save. */\n get productId(): string {\n return this.currentProductId;\n }\n\n get hostProductId(): string {\n return this.options.hostProductId;\n }\n\n private createElement(session: BuilderSession): HTMLElement {\n const el = document.createElement(BUILDER_TAG);\n this.attachListeners(el);\n el.setAttribute('product', session.productId);\n el.setAttribute('license-token', session.licenseToken);\n if (session.viewToken) el.setAttribute('view-token', session.viewToken);\n if (session.token && session.refreshToken) {\n setShopperTokens(el, {\n token: session.token,\n refreshToken: session.refreshToken,\n });\n }\n if (this.options.designId)\n el.setAttribute('design-id', this.options.designId);\n if (this.options.mode === 'viewer') el.setAttribute('display-only', 'true');\n if (this.options.externalExport) el.setAttribute('external-export', 'true');\n if (this.options.observabilityContext) {\n el.setAttribute(\n 'observability-context',\n this.options.observabilityContext\n );\n }\n el.style.display = 'block';\n el.style.width = '100%';\n el.style.height = '100%';\n return el;\n }\n\n private attachListeners(el: HTMLElement): void {\n const on = <T>(name: string, handler: (detail: T) => void) => {\n const listener: EventListener = (event) => {\n // Only this builder's own events: never a neighbour's, never a bubbled copy.\n if (event.target !== el || this.destroyed) return;\n this.ctx.log(name, (event as CustomEvent<T>).detail);\n handler((event as CustomEvent<T>).detail);\n };\n el.addEventListener(name, listener);\n this.listeners.push([name, listener]);\n };\n\n on<LoginRequiredDetail>(\n THREED_BUILDER_LOGIN_REQUIRED_EVENT,\n (d) => void this.answerLogin(d?.action ?? 'save')\n );\n on(\n THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT,\n () => void this.ctx.refreshLicense()\n );\n on<LicenseErrorDetail>(THREED_BUILDER_LICENSE_ERROR_EVENT, (d) =>\n this.onLicenseError(d)\n );\n on<DesignSavedDetail>(\n THREED_BUILDER_DESIGN_SAVED_EVENT,\n (d) => void this.onDesignSaved(d)\n );\n on<DesignSaveFailedDetail>(THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT, (d) =>\n this.finishSave(saveFailedError(d))\n );\n on<ProductClonedDetail>(\n THREED_BUILDER_PRODUCT_CLONED_EVENT,\n (d) => void this.onProductCloned(d)\n );\n on<CloseRequestedDetail>(THREED_BUILDER_CLOSE_REQUESTED_EVENT, (d) =>\n this.ctx.config.onCloseRequested?.(d)\n );\n on<BuilderStatusDetail>(THREED_BUILDER_STATUS_EVENT, (d) =>\n this.ctx.config.onStatusChange?.(d)\n );\n if (this.options.externalExport) {\n on<ExportRequestedDetail>(\n THREED_BUILDER_EXPORT_REQUESTED_EVENT,\n (d) => void this.answerExport(d)\n );\n }\n }\n\n // \u2500\u2500 Host-facing methods \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Shows the builder UI, for a host that mounted it before showing it. */\n open(): void {\n this.assertUsable();\n dispatchOpenThreedBuilder(this.element);\n }\n\n /**\n * Saves the design. Resolves once it is saved and any clone mapping it\n * required has been recorded; concurrent calls share one save.\n */\n save(): Promise<SavedDesign> {\n if (this.destroyed) return Promise.reject(destroyedError());\n if (this.fatal) return Promise.reject(this.fatal);\n if (this.saveRun) return this.saveRun.result.promise;\n\n const run: SaveRun = {\n result: deferred<SavedDesign>(),\n timer: new PausableTimer(this.ctx.config.saveTimeoutMs, () =>\n this.finishSave(\n new BuilderError(\n 'SAVE_TIMEOUT',\n 'Saving the design took too long. Try again.',\n {\n retryable: true,\n }\n )\n )\n ),\n };\n this.saveRun = run;\n if (!this.loginPending) run.timer.start();\n dispatchSaveRequest(this.element);\n return run.result.promise;\n }\n\n /** Calls session.get() again and applies what it returns. */\n async refreshSession(): Promise<void> {\n this.assertUsable();\n const epoch = this.epoch;\n let raw: unknown;\n try {\n raw = await this.ctx.config.session.get({\n hostProductId: this.options.hostProductId,\n });\n } catch (cause) {\n throw new BuilderError(\n 'SESSION_FAILED',\n 'The builder session could not be refreshed.',\n {\n retryable: true,\n cause,\n }\n );\n }\n const session = validateSession(raw);\n if (epoch !== this.epoch || this.destroyed) return;\n this.applyLicense(session.licenseToken);\n if (session.viewToken)\n this.element.setAttribute('view-token', session.viewToken);\n if (session.token && session.refreshToken) {\n setShopperTokens(this.element, {\n token: session.token,\n refreshToken: session.refreshToken,\n });\n }\n this.fatal = undefined;\n }\n\n /** Removes the shopper layer. The site layer stays and the builder revokes the session. */\n logout(): void {\n if (this.destroyed) return;\n this.epoch++;\n this.element.removeAttribute('bearer-token');\n this.element.removeAttribute('refresh-token');\n if (this.loginPending) {\n this.loginPending = false;\n this.finishSave(\n new BuilderError(\n 'LOGIN_CANCELLED',\n 'The shopper logged out before the design was saved.',\n {\n retryable: true,\n }\n )\n );\n }\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.epoch++;\n this.finishSave(destroyedError());\n this.cloneWait?.reject(destroyedError());\n this.cloneWait = undefined;\n for (const controller of this.exports) controller.abort();\n this.destroyed = true;\n for (const [name, listener] of this.listeners) {\n this.element.removeEventListener(name, listener);\n }\n this.listeners.length = 0;\n this.element.remove();\n this.ctx.onDestroyed(this);\n this.ctx.log('destroyed');\n }\n\n /**\n * Replaces the host page's observability context, for example once the\n * page's own Sentry has started and a trace exists to join.\n */\n setObservabilityContext(context: string): void {\n this.assertUsable();\n this.element.setAttribute(\n 'observability-context',\n validateObservabilityContext(context, 'setObservabilityContext()')\n );\n }\n\n /** Used by the controller to hand every instance the shared fresh license token. */\n applyLicense(licenseToken: string): void {\n if (this.destroyed) return;\n this.element.setAttribute('license-token', licenseToken);\n }\n\n get isDestroyed(): boolean {\n return this.destroyed;\n }\n\n // \u2500\u2500 Login round-trip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async answerLogin(action: LoginRequiredAction): Promise<void> {\n if (this.loginPending) return;\n const login = this.ctx.config.login;\n if (!login) {\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n const epoch = this.epoch;\n const stale = () => epoch !== this.epoch || this.destroyed;\n this.setLoginPending(true);\n let redirected = false;\n try {\n for (;;) {\n let tokens: ShopperTokens | null;\n try {\n tokens = validateShopperTokens(await login.getTokens());\n } catch (cause) {\n if (stale()) return;\n this.ctx.log('login.getTokens() failed', { cause: String(cause) });\n const choice = login.onMintFailed\n ? await login.onMintFailed()\n : 'cancel';\n if (stale()) return;\n if (choice === 'retry') continue;\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n if (stale()) return;\n if (tokens) {\n setShopperTokens(this.element, tokens); // resumes the pending action\n return;\n }\n const outcome = await login.onRequired(action);\n if (stale()) return;\n if (outcome === 'logged-in') continue;\n if (outcome === 'redirected') {\n // The page is navigating to the host's login. Never cancel: the\n // builder resumes the save after the shopper comes back.\n redirected = true;\n return;\n }\n dispatchLoginCancelled(this.element, { action });\n return;\n }\n } finally {\n if (!redirected && epoch === this.epoch) this.setLoginPending(false);\n }\n }\n\n private setLoginPending(pending: boolean): void {\n this.loginPending = pending;\n if (!this.saveRun) return;\n if (pending) this.saveRun.timer.pause();\n else this.saveRun.timer.start();\n }\n\n // \u2500\u2500 License \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private onLicenseError(detail: LicenseErrorDetail): void {\n const error = licenseError(detail.code);\n this.fatal = error;\n this.finishSave(error);\n this.ctx.config.onLicenseError?.(error);\n }\n\n // \u2500\u2500 Save and clone recording \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async onDesignSaved(detail: DesignSavedDetail): Promise<void> {\n if (detail.productId !== this.currentProductId && !this.cloneWait) {\n // The save created a clone; tdn-product-cloned follows the thumbnail upload.\n this.cloneWait = deferred<void>();\n }\n // Saves from the builder's own Save button come through here too, with no\n // save() pending: they wait for the clone and its record all the same, so\n // onSaved only ever reports a save the host can act on.\n try {\n if (this.cloneWait) await this.cloneWait.promise;\n if (this.unrecordedLink) await this.recordLink(this.unrecordedLink);\n } catch (error) {\n this.finishSave(\n error instanceof BuilderError\n ? error\n : new BuilderError(\n 'SAVE_FAILED',\n 'The design was saved but could not be linked to this shopper.',\n {\n retryable: true,\n reason: 'customization',\n cause: error,\n }\n )\n );\n return;\n }\n if (this.destroyed) return;\n const saved: SavedDesign = {\n productId: detail.productId,\n version: detail.version,\n };\n this.notifySaved(saved);\n this.finishSave(undefined, saved);\n }\n\n private notifySaved(saved: SavedDesign): void {\n try {\n this.ctx.config.onSaved?.({\n hostProductId: this.options.hostProductId,\n ...saved,\n });\n } catch (cause) {\n this.ctx.log('onSaved threw', { cause: String(cause) });\n }\n }\n\n private async onProductCloned(detail: ProductClonedDetail): Promise<void> {\n const link: CustomizationLink = {\n hostProductId: this.options.hostProductId,\n sourceProductId: detail.originalProductId,\n savedProductId: detail.newProductId,\n };\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": "0IAqCO,IAAMA,EAAN,MAAMC,UAAqB,KAAM,CAQtC,YACEC,EACAC,EACAC,EAA+B,CAAC,EAChC,CAjDJ,IAAAC,EAAAC,EAkDI,MAAMH,CAAO,EACb,KAAK,KAAO,eACZ,KAAK,KAAOD,EACZ,KAAK,WAAYG,EAAAD,EAAQ,YAAR,KAAAC,EAAqB,GACtC,KAAK,OAAQC,EAAAF,EAAQ,QAAR,KAAAE,EAAiB,GAC9B,KAAK,OAASF,EAAQ,OACtB,KAAK,MAAQA,EAAQ,MACrB,OAAO,eAAe,KAAMH,EAAa,SAAS,CACpD,CACF,EAEMM,EAAiE,CACrE,SAAU,wCACV,UAAW,yCACX,aAAc,8CACd,kBACE,+DACF,wBAAyB,8CACzB,QAAS,oDACT,MAAO,oEACT,EAEO,SAASC,EAAgBC,EAA8C,CAxE9E,IAAAJ,EAyEE,IAAMF,GACJE,EAAAE,EAAsBE,EAAO,MAAM,IAAnC,KAAAJ,EAAwCE,EAAsB,MAChE,OAAIE,EAAO,SAAW,kBACb,IAAIT,EAAa,kBAAmBG,EAAS,CAClD,UAAW,GACX,OAAQM,EAAO,OACf,MAAOA,CACT,CAAC,EAEI,IAAIT,EAAa,cAAeG,EAAS,CAC9C,UAAWM,EAAO,UAClB,OAAQA,EAAO,OACf,MAAOA,CACT,CAAC,CACH,CAEO,SAASC,EAAaR,EAAsC,CACjE,IAAMC,EACJD,IAAS,qBACL,2EACA,wFACN,OAAO,IAAIF,EAAa,gBAAiBG,EAAS,CAChD,MAAO,GACP,OAAQD,EACR,MAAO,CAAE,KAAAA,CAAK,CAChB,CAAC,CACH,CAEO,SAASS,GAA+B,CAC7C,OAAO,IAAIX,EAAa,YAAa,4BAA4B,CACnE,CAEO,SAASY,EAAYT,EAA+B,CACzD,OAAO,IAAIH,EAAa,iBAAkBG,EAAS,CAAE,MAAO,EAAK,CAAC,CACpE,CCUO,IAAMU,GAA0B,KAC1BC,EAA4B,KAEnCC,GAAc,IAAI,IAAI,CAC1B,cACA,UACA,gBACA,UACA,QACA,gBACA,UACA,iBACA,mBACA,iBACA,OACF,CAAC,EAEKC,GAAa,IAAI,IAAI,CACzB,SACA,gBACA,OACA,WACA,iBACA,sBACF,CAAC,EAEKC,GAAkB,kBAClBC,GAA2B,aAE1B,SAASC,EAAeC,EAAwB,CACrD,OAAOH,GAAgB,KAAKG,CAAK,CACnC,CAEA,SAASC,EAASD,EAAkD,CAClE,OAAO,OAAOA,GAAU,UAAYA,IAAU,IAChD,CAEA,SAASE,EAAWF,EAAwD,CAC1E,OAAO,OAAOA,GAAU,UAC1B,CAEA,SAASG,EAAiBH,EAAiC,CACzD,OAAO,OAAOA,GAAU,UAAY,OAAO,SAASA,CAAK,GAAKA,EAAQ,CACxE,CAGA,SAASI,EAAeJ,EAAgBK,EAAQ,EAAY,CAC1D,OAAI,OAAOL,GAAU,SAAiBF,GAAyB,KAAKE,CAAK,EACrE,CAACC,EAASD,CAAK,GAAKK,EAAQ,EAAU,GACnC,OAAO,OAAOL,CAAK,EAAE,KAAMM,GAAMF,EAAeE,EAAGD,EAAQ,CAAC,CAAC,CACtE,CAEA,SAASE,EACPP,EACAQ,EACAC,EACM,CACN,QAAWC,KAAO,OAAO,KAAKV,CAAK,EACjC,GAAI,CAAAQ,EAAQ,IAAIE,CAAG,EACnB,KAAI,uBAAuB,KAAKA,CAAG,EAC3BC,EACJ,GAAGF,CAAK,MAAMC,CAAG,8GACnB,EAEIC,EAAY,GAAGF,CAAK,qBAAqBC,CAAG,IAAI,CAE1D,CAEO,SAASE,EAAcC,EAAiC,CAzL/D,IAAAC,EAAAC,EAAAC,EA0LE,GAAI,CAACf,EAASY,CAAM,EAClB,MAAMF,EAAY,+CAA+C,EAEnE,GAAIP,EAAeS,CAAM,EACvB,MAAMF,EACJ,yIACF,EAEFJ,EAAkBM,EAAQlB,GAAa,iBAAiB,EAExD,IAAMsB,GAAcH,EAAAD,EAAO,cAAP,KAAAC,EAAyB,aAC7C,GAAIG,IAAgB,cAAgBA,IAAgB,UAClD,MAAMN,EACJ,uEAAuE,KAAK,UAC1EM,CACF,CAAC,GACH,EAGF,IAAMC,GAAUH,EAAAF,EAAO,UAAP,KAAAE,EAAqB,SACrC,GACE,OAAOG,GAAY,UAClBA,IAAY,UAAY,CAACnB,EAAemB,CAAO,EAEhD,MAAMP,EACJ,sFAAsF,KAAK,UACzFO,CACF,CAAC,GACH,EAGF,IAAMC,GAAgBH,EAAAH,EAAO,gBAAP,KAAAG,EAA2BvB,GACjD,GAAI,CAACU,EAAiBgB,CAAa,EACjC,MAAMR,EACJ,2EACF,EAGF,IAAMS,EAAUP,EAAO,QACvB,GACE,CAACZ,EAASmB,CAAO,GACjB,CAAClB,EAAWkB,EAAQ,GAAM,GAC1B,CAAClB,EAAWkB,EAAQ,cAAiB,EAErC,MAAMT,EACJ,4GACF,EAGF,IAAMU,EAAQR,EAAO,MACrB,GAAIQ,IAAU,OAAW,CACvB,GACE,CAACpB,EAASoB,CAAK,GACf,CAACnB,EAAWmB,EAAM,SAAY,GAC9B,CAACnB,EAAWmB,EAAM,UAAa,EAE/B,MAAMV,EACJ,wGACF,EAEF,GACEU,EAAM,eAAoB,QAC1B,CAACnB,EAAWmB,EAAM,YAAe,EAEjC,MAAMV,EACJ,yDACF,CAEJ,CAEA,IAAMW,EAAgBT,EAAO,cAC7B,GACES,IAAkB,SACjB,CAACrB,EAASqB,CAAa,GAAK,CAACpB,EAAWoB,EAAc,MAAS,GAEhE,MAAMX,EACJ,sHACF,EAGF,QAAWD,IAAO,CAChB,UACA,iBACA,mBACA,gBACF,EACE,GAAIG,EAAOH,CAAG,IAAM,QAAa,CAACR,EAAWW,EAAOH,CAAG,CAAC,EACtD,MAAMC,EAAY,oBAAoBD,CAAG,sBAAsB,EAInE,MAAO,CACL,GAAIG,EACJ,YAAAI,EACA,QAASC,EACT,cAAAC,CACF,CACF,CAEO,SAASI,EAAqBC,EAAgC,CA7RrE,IAAAV,EA8RE,GAAI,CAACb,EAASuB,CAAO,EACnB,MAAMb,EAAY,kCAAkC,EAEtDJ,EAAkBiB,EAAS5B,GAAY,SAAS,EAEhD,IAAM6B,EAASD,EAAQ,OACvB,GAAI,OAAO,aAAgB,aAAe,EAAEC,aAAkB,aAC5D,MAAMd,EAAY,qDAAqD,EAEzE,IAAMe,EAAgBF,EAAQ,cAC9B,GAAI,OAAOE,GAAkB,UAAYA,EAAc,KAAK,IAAM,GAChE,MAAMf,EACJ,uEACF,EAEF,IAAMgB,GAAOb,EAAAU,EAAQ,OAAR,KAAAV,EAAmB,WAChC,GAAIa,IAAS,YAAcA,IAAS,SAClC,MAAMhB,EACJ,qDAAqD,KAAK,UACxDgB,CACF,CAAC,GACH,EAEF,IAAMC,EAAWJ,EAAQ,SACzB,GACEI,IAAa,SACZ,OAAOA,GAAa,UAAYA,IAAa,IAE9C,MAAMjB,EAAY,+CAA+C,EAE/Da,EAAQ,uBAA4B,QACtCK,EAA6BL,EAAQ,qBAAyB,SAAS,EAEzE,IAAMM,EAAiBN,EAAQ,eAC/B,GAAIM,IAAmB,OAAW,CAChC,GAAI,CAAC7B,EAAS6B,CAAc,GAAK,CAAC5B,EAAW4B,EAAe,GAAM,EAChE,MAAMnB,EACJ,uEACF,EAEF,GACEmB,EAAe,YAAiB,QAChC,CAAC3B,EAAiB2B,EAAe,SAAY,EAE7C,MAAMnB,EACJ,8EACF,CAEJ,CACA,OAAOa,CACT,CAGO,SAASK,EACd7B,EACAS,EACQ,CACR,IAAIsB,EACJ,GAAI,CACFA,EAAS,OAAO/B,GAAU,SAAW,KAAK,MAAMA,CAAK,EAAI,MAC3D,MAAQ,CACN+B,EAAS,MACX,CACA,GAAI,CAAC9B,EAAS8B,CAAM,GAAK,MAAM,QAAQA,CAAM,EAC3C,MAAMpB,EACJ,GAAGF,CAAK,sDACV,EAEF,OAAOT,CACT,CAGO,SAASgC,EAAgBZ,EAAkC,CAChE,IAAMa,EAAQC,GACZ,IAAIC,EACF,iBACA,8CAA8CD,CAAI,IAClD,CACE,UAAW,EACb,CACF,EACF,GAAI,CAACjC,EAASmB,CAAO,EAAG,MAAMa,EAAK,oBAAoB,EACvD,GACE,OAAOb,EAAQ,cAAoB,UACnCA,EAAQ,eAAoB,GAE5B,MAAMa,EAAK,yBAAyB,EAEtC,GAAI,OAAOb,EAAQ,WAAiB,UAAYA,EAAQ,YAAiB,GACvE,MAAMa,EAAK,sBAAsB,EAEnC,GACEb,EAAQ,WAAgB,MACxB,OAAOA,EAAQ,WAAiB,SAEhC,MAAMa,EAAK,4BAA4B,EAEzC,IAAMG,EAAWhB,EAAQ,OAAY,KAC/BiB,EAAajB,EAAQ,cAAmB,KAC9C,GAAIgB,IAAaC,EACf,MAAMJ,EAAK,kDAAkD,EAE/D,GACEG,IACC,OAAOhB,EAAQ,OAAa,UAC3B,OAAOA,EAAQ,cAAoB,UAErC,MAAMa,EAAK,wCAAwC,EAErD,OAAOb,CACT,CAEO,SAASkB,EAAsBC,EAAuC,CAC3E,GAAIA,IAAW,KAAM,OAAO,KAC5B,GACE,CAACtC,EAASsC,CAAM,GAChB,OAAOA,EAAO,OAAa,UAC3BA,EAAO,QAAa,IACpB,OAAOA,EAAO,cAAoB,UAClCA,EAAO,eAAoB,GAE3B,MAAM,IAAIJ,EACR,iBACA,iEACF,EAEF,OAAOI,CACT,CC7ZA,IAAMC,GAAY,kCAGX,SAASC,EAAOC,EAAgBC,EAAQ,EAAY,CACzD,GAAID,IAAU,MAAQ,OAAOA,GAAU,UAAYC,EAAQ,EAAG,OAAOD,EACrE,GAAI,MAAM,QAAQA,CAAK,EAAG,OAAOA,EAAM,IAAKE,GAAMH,EAAOG,EAAGD,EAAQ,CAAC,CAAC,EACtE,IAAME,EAA+B,CAAC,EACtC,OAAW,CAACC,EAAKF,CAAC,IAAK,OAAO,QAAQF,CAAK,EACzCG,EAAIC,CAAG,EACLN,GAAU,KAAKM,CAAG,GAAKF,GAAK,KAAO,aAAeH,EAAOG,EAAGD,EAAQ,CAAC,EAEzE,OAAOE,CACT,CAIO,SAASE,EAAeC,EAAwC,CACrE,OAAKA,EACE,CAACC,EAASC,IAAW,CACtBA,IAAW,OAAW,QAAQ,MAAM,SAASD,CAAO,EAAE,EACrD,QAAQ,MAAM,SAASA,CAAO,GAAIR,EAAOS,CAAM,CAAC,CACvD,EAJqB,IAAG,EAK1B,CClBO,IAAMC,EAAc,iBAOrBC,GAA+D,CACnE,WAAY,6BACZ,QAAS,MACX,EAEMC,GAAoB,IAUpBC,EAAY,OAAO,IAAI,6BAA6B,EAE1D,SAASC,IAA6B,CACpC,IAAMC,EAAS,WAIXC,EAAQD,EAAOF,CAAS,EAC5B,OAAKG,IACHA,EAAQ,CAAC,EACTD,EAAOF,CAAS,EAAIG,GAEfA,CACT,CAEO,SAASC,IAAqB,CACnC,OACE,OAAO,QAAW,aAClB,OAAO,UAAa,aACpB,OAAO,gBAAmB,WAE9B,CAEO,SAASC,GACdC,EACM,CACN,GAAI,CAACR,GAAaQ,CAAW,EAC3B,MAAMC,EACJ,yBAAyBD,CAAW,qDACtC,CAEJ,CAEA,SAASE,EAAWC,EAAiBC,EAA+B,CAClE,OAAO,IAAIC,EAAa,cAAeF,EAAS,CAAE,UAAW,GAAM,MAAAC,CAAM,CAAC,CAC5E,CAEA,eAAeE,GACbC,EACAC,EACiB,CACjB,GAAIA,IAAc,SAAU,OAAOA,EACnC,IAAIC,EACJ,GAAI,CACFA,EAAW,MAAM,MAAM,GAAGF,CAAI,qBAAsB,CAClD,MAAO,WACP,YAAa,MACf,CAAC,CACH,OAASH,EAAO,CACd,MAAMF,EAAW,wCAAyCE,CAAK,CACjE,CACA,GAAI,CAACK,EAAS,GACZ,MAAMP,EACJ,uDAAuDO,EAAS,MAAM,IACxE,EAEF,IAAMC,EAAQ,MAAMD,EAAS,KAAK,EAAE,MAAM,IAAM,IAAI,EAG9CE,EAAUD,GAAA,YAAAA,EAAM,QACtB,GAAI,OAAOC,GAAY,UAAY,CAACC,EAAeD,CAAO,EACxD,MAAMT,EAAW,oDAAoD,EAEvE,OAAOS,CACT,CAEA,SAASE,GAAaC,EAAyC,CAC7D,OAAO,IAAI,QAAQ,CAACC,EAASC,IAAW,CACtC,IAAMC,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,KAAO,SACdA,EAAO,IAAMH,EACbG,EAAO,YAAc,YACrBA,EAAO,OAAS,IAAMF,EAAQE,CAAM,EACpCA,EAAO,QAAU,IAAM,CACrBA,EAAO,OAAO,EACdD,EAAOd,EAAW,iCAAiC,CAAC,CACtD,EACA,SAAS,KAAK,YAAYe,CAAM,CAClC,CAAC,CACH,CAEA,SAASC,IAA6B,CACpC,OAAO,IAAI,QAAQ,CAACH,EAASC,IAAW,CACtC,IAAMG,EAAQ,WACZ,IAAMH,EAAOd,EAAW,2CAA2C,CAAC,EACpET,EACF,EACA,eAAe,YAAYF,CAAW,EAAE,KAAK,IAAM,CACjD,aAAa4B,CAAK,EAClBJ,EAAQ,CACV,EAAGC,CAAM,CACX,CAAC,CACH,CAEA,SAASI,EACPvB,EACAG,EACAQ,EACM,CACN,GAAIX,EAAM,aAAeA,EAAM,cAAgBG,EAC7C,MAAM,IAAIK,EACR,mBACA,OAAOR,EAAM,WAAW,uCAAuCG,CAAW,0BAC1E,CAAE,MAAO,EAAK,CAChB,EAEF,GAAIQ,IAAc,UAAYX,EAAM,SAAWA,EAAM,UAAYW,EAC/D,MAAM,IAAIH,EACR,mBACA,WAAWR,EAAM,OAAO,+BAA+BW,CAAS,0BAChE,CAAE,MAAO,EAAK,CAChB,CAEJ,CAMA,eAAsBa,GACpBrB,EACAQ,EACe,CACf,GAAI,CAACV,GAAU,EACb,MAAM,IAAIO,EACR,iBACA,6CACF,EAEF,IAAMR,EAAQF,GAAU,EAExB,GAAIE,EAAM,QAAS,CACjB,MAAMA,EAAM,QACZuB,EAAiBvB,EAAOG,EAAaQ,CAAS,EAC9C,MACF,CAGA,GAAI,eAAe,IAAIjB,CAAW,EAAG,OAErC,IAAMgB,EAAOf,GAAaQ,CAAW,EACrC,GAAI,CAACO,EACH,MAAMN,EACJ,QAAQD,CAAW,qDACrB,EAGFH,EAAM,YAAcG,EACpBH,EAAM,SAAW,SAAY,CAC3B,IAAMc,EAAU,MAAML,GAAeC,EAAMC,CAAS,EACpDX,EAAM,QAAUc,EAChB,IAAMM,EAAS,MAAMJ,GAAa,GAAGN,CAAI,YAAYI,CAAO,KAAK,EACjE,GAAI,CACF,MAAMO,GAAY,CACpB,OAASI,EAAO,CACd,MAAAL,EAAO,OAAO,EACRK,CACR,CACF,GAAG,EAEH,GAAI,CACF,MAAMzB,EAAM,OACd,OAASyB,EAAO,CACd,MAAAzB,EAAM,QAAU,OAChBA,EAAM,YAAc,OACpBA,EAAM,QAAU,OACVyB,aAAiBjB,EACnBiB,EACApB,EAAW,kCAAmCoB,CAAK,CACzD,CACAF,EAAiBvB,EAAOG,EAAaQ,CAAS,CAChD,CCpIA,SAASe,IAA2B,CAClC,IAAIC,EACAC,EACEC,EAAU,IAAI,QAAW,CAACC,EAAKC,IAAQ,CAC3CJ,EAAUG,EACVF,EAASG,CACX,CAAC,EAED,OAAAF,EAAQ,MAAM,IAAG,EAAY,EACtB,CAAE,QAAAA,EAAS,QAAAF,EAAS,OAAAC,CAAO,CACpC,CAGA,IAAMI,EAAN,KAAoB,CAKlB,YAAYC,EAA6BC,EAAsB,CAAtB,cAAAA,EAHzC,KAAQ,UAAY,EAIlB,KAAK,UAAYD,CACnB,CAEA,OAAc,CACR,KAAK,SAAW,SACpB,KAAK,UAAY,KAAK,IAAI,EAC1B,KAAK,OAAS,WAAW,KAAK,SAAU,KAAK,SAAS,EACxD,CAEA,OAAc,CACR,KAAK,SAAW,SACpB,aAAa,KAAK,MAAM,EACxB,KAAK,OAAS,OACd,KAAK,UAAY,KAAK,IACpB,EACA,KAAK,WAAa,KAAK,IAAI,EAAI,KAAK,UACtC,EACF,CAEA,MAAa,CACP,KAAK,SAAW,QAAW,aAAa,KAAK,MAAM,EACvD,KAAK,OAAS,MAChB,CACF,EAaaE,EAAN,MAAMC,CAAgB,CAgBnB,YACWC,EACAC,EACjBC,EACA,CAHiB,SAAAF,EACA,aAAAC,EAdnB,KAAQ,MAAQ,EAChB,KAAQ,UAAY,GAEpB,KAAQ,aAAe,GAMvB,KAAiB,QAAU,IAAI,IAC/B,KAAiB,UAAuC,CAAC,EAOvD,KAAK,iBAAmBC,EAAQ,UAChC,KAAK,QAAU,KAAK,cAAcA,CAAO,CAC3C,CAGA,aAAa,MACXF,EACAC,EAC0B,CAC1B,IAAIE,EACJ,GAAI,CACFA,EAAM,MAAMH,EAAI,OAAO,QAAQ,IAAI,CACjC,cAAeC,EAAQ,aACzB,CAAC,CACH,OAASG,EAAO,CACd,MAAM,IAAIC,EACR,iBACA,2CACA,CACE,UAAW,GACX,MAAAD,CACF,CACF,CACF,CACA,IAAMF,EAAUI,EAAgBH,CAAG,EAC7BI,EAAW,IAAIR,EAAgBC,EAAKC,EAASC,CAAO,EAC1D,OAAAD,EAAQ,OAAO,YAAYM,EAAS,OAAO,EAC3CP,EAAI,IAAI,UAAW,CACjB,cAAeC,EAAQ,cACvB,UAAWC,EAAQ,SACrB,CAAC,EACMK,CACT,CAGA,IAAI,WAAoB,CACtB,OAAO,KAAK,gBACd,CAEA,IAAI,eAAwB,CAC1B,OAAO,KAAK,QAAQ,aACtB,CAEQ,cAAcL,EAAsC,CAC1D,IAAMM,EAAK,SAAS,cAAcC,CAAW,EAC7C,YAAK,gBAAgBD,CAAE,EACvBA,EAAG,aAAa,UAAWN,EAAQ,SAAS,EAC5CM,EAAG,aAAa,gBAAiBN,EAAQ,YAAY,EACjDA,EAAQ,WAAWM,EAAG,aAAa,aAAcN,EAAQ,SAAS,EAClEA,EAAQ,OAASA,EAAQ,cAC3BQ,EAAiBF,EAAI,CACnB,MAAON,EAAQ,MACf,aAAcA,EAAQ,YACxB,CAAC,EAEC,KAAK,QAAQ,UACfM,EAAG,aAAa,YAAa,KAAK,QAAQ,QAAQ,EAChD,KAAK,QAAQ,OAAS,UAAUA,EAAG,aAAa,eAAgB,MAAM,EACtE,KAAK,QAAQ,gBAAgBA,EAAG,aAAa,kBAAmB,MAAM,EACtE,KAAK,QAAQ,sBACfA,EAAG,aACD,wBACA,KAAK,QAAQ,oBACf,EAEFA,EAAG,MAAM,QAAU,QACnBA,EAAG,MAAM,MAAQ,OACjBA,EAAG,MAAM,OAAS,OACXA,CACT,CAEQ,gBAAgBA,EAAuB,CAC7C,IAAMG,EAAK,CAAIC,EAAcC,IAAiC,CAC5D,IAAMC,EAA2BC,GAAU,CAErCA,EAAM,SAAWP,GAAM,KAAK,YAChC,KAAK,IAAI,IAAII,EAAOG,EAAyB,MAAM,EACnDF,EAASE,EAAyB,MAAM,EAC1C,EACAP,EAAG,iBAAiBI,EAAME,CAAQ,EAClC,KAAK,UAAU,KAAK,CAACF,EAAME,CAAQ,CAAC,CACtC,EAEAH,EACEK,EACCC,GAAG,CAlOV,IAAAC,EAkOkB,KAAK,aAAYA,EAAAD,GAAA,YAAAA,EAAG,SAAH,KAAAC,EAAa,MAAM,EAClD,EACAP,EACEQ,EACA,IAAG,CAAQ,KAAK,IAAI,eAAe,EACrC,EACAR,EAAuBS,EAAqCH,GAC1D,KAAK,eAAeA,CAAC,CACvB,EACAN,EACEU,EACCJ,GAAG,CAAQ,KAAK,cAAcA,CAAC,EAClC,EACAN,EAA2BW,EAA0CL,GACnE,KAAK,WAAWM,EAAgBN,CAAC,CAAC,CACpC,EACAN,EACEa,EACCP,GAAG,CAAQ,KAAK,gBAAgBA,CAAC,EACpC,EACAN,EAAyBc,EAAuCR,GAAG,CAtPvE,IAAAC,EAAAQ,EAuPM,OAAAA,GAAAR,EAAA,KAAK,IAAI,QAAO,mBAAhB,YAAAQ,EAAA,KAAAR,EAAmCD,GACrC,EACAN,EAAwBgB,EAA8BV,GAAG,CAzP7D,IAAAC,EAAAQ,EA0PM,OAAAA,GAAAR,EAAA,KAAK,IAAI,QAAO,iBAAhB,YAAAQ,EAAA,KAAAR,EAAiCD,GACnC,EACI,KAAK,QAAQ,gBACfN,EACEiB,EACCX,GAAG,CAAQ,KAAK,aAAaA,CAAC,EACjC,CAEJ,CAKA,MAAa,CACX,KAAK,aAAa,EAClBY,EAA0B,KAAK,OAAO,CACxC,CAMA,MAA6B,CAC3B,GAAI,KAAK,UAAW,OAAO,QAAQ,OAAOC,EAAe,CAAC,EAC1D,GAAI,KAAK,MAAO,OAAO,QAAQ,OAAO,KAAK,KAAK,EAChD,GAAI,KAAK,QAAS,OAAO,KAAK,QAAQ,OAAO,QAE7C,IAAMC,EAAe,CACnB,OAAQ1C,GAAsB,EAC9B,MAAO,IAAIM,EAAc,KAAK,IAAI,OAAO,cAAe,IACtD,KAAK,WACH,IAAIU,EACF,eACA,8CACA,CACE,UAAW,EACb,CACF,CACF,CACF,CACF,EACA,YAAK,QAAU0B,EACV,KAAK,cAAcA,EAAI,MAAM,MAAM,EACxCC,EAAoB,KAAK,OAAO,EACzBD,EAAI,OAAO,OACpB,CAGA,MAAM,gBAAgC,CACpC,KAAK,aAAa,EAClB,IAAME,EAAQ,KAAK,MACf9B,EACJ,GAAI,CACFA,EAAM,MAAM,KAAK,IAAI,OAAO,QAAQ,IAAI,CACtC,cAAe,KAAK,QAAQ,aAC9B,CAAC,CACH,OAASC,EAAO,CACd,MAAM,IAAIC,EACR,iBACA,8CACA,CACE,UAAW,GACX,MAAAD,CACF,CACF,CACF,CACA,IAAMF,EAAUI,EAAgBH,CAAG,EAC/B8B,IAAU,KAAK,OAAS,KAAK,YACjC,KAAK,aAAa/B,EAAQ,YAAY,EAClCA,EAAQ,WACV,KAAK,QAAQ,aAAa,aAAcA,EAAQ,SAAS,EACvDA,EAAQ,OAASA,EAAQ,cAC3BQ,EAAiB,KAAK,QAAS,CAC7B,MAAOR,EAAQ,MACf,aAAcA,EAAQ,YACxB,CAAC,EAEH,KAAK,MAAQ,OACf,CAGA,QAAe,CACT,KAAK,YACT,KAAK,QACL,KAAK,QAAQ,gBAAgB,cAAc,EAC3C,KAAK,QAAQ,gBAAgB,eAAe,EACxC,KAAK,eACP,KAAK,aAAe,GACpB,KAAK,WACH,IAAIG,EACF,kBACA,sDACA,CACE,UAAW,EACb,CACF,CACF,GAEJ,CAEA,SAAgB,CA9VlB,IAAAa,EA+VI,GAAI,MAAK,UACT,MAAK,QACL,KAAK,WAAWY,EAAe,CAAC,GAChCZ,EAAA,KAAK,YAAL,MAAAA,EAAgB,OAAOY,EAAe,GACtC,KAAK,UAAY,OACjB,QAAWI,KAAc,KAAK,QAASA,EAAW,MAAM,EACxD,KAAK,UAAY,GACjB,OAAW,CAACtB,EAAME,CAAQ,IAAK,KAAK,UAClC,KAAK,QAAQ,oBAAoBF,EAAME,CAAQ,EAEjD,KAAK,UAAU,OAAS,EACxB,KAAK,QAAQ,OAAO,EACpB,KAAK,IAAI,YAAY,IAAI,EACzB,KAAK,IAAI,IAAI,WAAW,EAC1B,CAMA,wBAAwBqB,EAAuB,CAC7C,KAAK,aAAa,EAClB,KAAK,QAAQ,aACX,wBACAC,EAA6BD,EAAS,2BAA2B,CACnE,CACF,CAGA,aAAaE,EAA4B,CACnC,KAAK,WACT,KAAK,QAAQ,aAAa,gBAAiBA,CAAY,CACzD,CAEA,IAAI,aAAuB,CACzB,OAAO,KAAK,SACd,CAIA,MAAc,YAAYC,EAA4C,CACpE,GAAI,KAAK,aAAc,OACvB,IAAMC,EAAQ,KAAK,IAAI,OAAO,MAC9B,GAAI,CAACA,EAAO,CACVC,EAAuB,KAAK,QAAS,CAAE,OAAAF,CAAO,CAAC,EAC/C,MACF,CACA,IAAML,EAAQ,KAAK,MACbQ,EAAQ,IAAMR,IAAU,KAAK,OAAS,KAAK,UACjD,KAAK,gBAAgB,EAAI,EACzB,IAAIS,EAAa,GACjB,GAAI,CACF,OAAS,CACP,IAAIC,EACJ,GAAI,CACFA,EAASC,EAAsB,MAAML,EAAM,UAAU,CAAC,CACxD,OAASnC,EAAO,CACd,GAAIqC,EAAM,EAAG,OACb,KAAK,IAAI,IAAI,2BAA4B,CAAE,MAAO,OAAOrC,CAAK,CAAE,CAAC,EACjE,IAAMyC,EAASN,EAAM,aACjB,MAAMA,EAAM,aAAa,EACzB,SACJ,GAAIE,EAAM,EAAG,OACb,GAAII,IAAW,QAAS,SACxBL,EAAuB,KAAK,QAAS,CAAE,OAAAF,CAAO,CAAC,EAC/C,MACF,CACA,GAAIG,EAAM,EAAG,OACb,GAAIE,EAAQ,CACVjC,EAAiB,KAAK,QAASiC,CAAM,EACrC,MACF,CACA,IAAMG,EAAU,MAAMP,EAAM,WAAWD,CAAM,EAC7C,GAAIG,EAAM,EAAG,OACb,GAAIK,IAAY,YAChB,IAAIA,IAAY,aAAc,CAG5BJ,EAAa,GACb,MACF,CACAF,EAAuB,KAAK,QAAS,CAAE,OAAAF,CAAO,CAAC,EAC/C,OACF,CACF,QAAE,CACI,CAACI,GAAcT,IAAU,KAAK,OAAO,KAAK,gBAAgB,EAAK,CACrE,CACF,CAEQ,gBAAgBc,EAAwB,CAC9C,KAAK,aAAeA,EACf,KAAK,UACNA,EAAS,KAAK,QAAQ,MAAM,MAAM,EACjC,KAAK,QAAQ,MAAM,MAAM,EAChC,CAIQ,eAAeC,EAAkC,CAjc3D,IAAA9B,EAAAQ,EAkcI,IAAMuB,EAAQC,EAAaF,EAAO,IAAI,EACtC,KAAK,MAAQC,EACb,KAAK,WAAWA,CAAK,GACrBvB,GAAAR,EAAA,KAAK,IAAI,QAAO,iBAAhB,MAAAQ,EAAA,KAAAR,EAAiC+B,EACnC,CAIA,MAAc,cAAcD,EAA0C,CAChEA,EAAO,YAAc,KAAK,kBAAoB,CAAC,KAAK,YAEtD,KAAK,UAAY3D,GAAe,GAKlC,GAAI,CACE,KAAK,WAAW,MAAM,KAAK,UAAU,QACrC,KAAK,gBAAgB,MAAM,KAAK,WAAW,KAAK,cAAc,CACpE,OAAS4D,EAAO,CACd,KAAK,WACHA,aAAiB5C,EACb4C,EACA,IAAI5C,EACF,cACA,gEACA,CACE,UAAW,GACX,OAAQ,gBACR,MAAO4C,CACT,CACF,CACN,EACA,MACF,CACA,GAAI,KAAK,UAAW,OACpB,IAAME,EAAqB,CACzB,UAAWH,EAAO,UAClB,QAASA,EAAO,OAClB,EACA,KAAK,YAAYG,CAAK,EACtB,KAAK,WAAW,OAAWA,CAAK,CAClC,CAEQ,YAAYA,EAA0B,CA9ehD,IAAAjC,EAAAQ,EA+eI,GAAI,EACFA,GAAAR,EAAA,KAAK,IAAI,QAAO,UAAhB,MAAAQ,EAAA,KAAAR,EAA0B,CACxB,cAAe,KAAK,QAAQ,cAC5B,GAAGiC,CACL,EACF,OAAS/C,EAAO,CACd,KAAK,IAAI,IAAI,gBAAiB,CAAE,MAAO,OAAOA,CAAK,CAAE,CAAC,CACxD,CACF,CAEA,MAAc,gBAAgB4C,EAA4C,CACxE,IAAMI,EAA0B,CAC9B,cAAe,KAAK,QAAQ,cAC5B,gBAAiBJ,EAAO,kBACxB,eAAgBA,EAAO,YACzB,EACA,KAAK,iBAAmBA,EAAO,aAC/B,IAAMK,EAAO,KAAK,UAClB,KAAK,UAAY,OACjB,GAAI,CACF,MAAM,KAAK,WAAWD,CAAI,EAC1BC,GAAA,MAAAA,EAAM,SACR,OAASJ,EAAO,CACdI,GAAA,MAAAA,EAAM,OAAOJ,EACf,CACF,CAEA,MAAc,WAAWG,EAAwC,CAC/D,IAAME,EAAgB,KAAK,IAAI,OAAO,cACtC,GAAKA,EACL,GAAI,CACF,MAAMA,EAAc,OAAOF,CAAI,EAC3B,KAAK,iBAAmBA,IAAM,KAAK,eAAiB,OAC1D,OAAShD,EAAO,CACd,WAAK,eAAiBgD,EAChB,IAAI/C,EACR,cACA,kFACA,CAAE,UAAW,GAAM,OAAQ,gBAAiB,MAAAD,CAAM,CACpD,CACF,CACF,CAEQ,WAAW6C,EAAsBE,EAA2B,CAClE,IAAMpB,EAAM,KAAK,QACZA,IACL,KAAK,QAAU,OACfA,EAAI,MAAM,KAAK,EACXkB,EAAOlB,EAAI,OAAO,OAAOkB,CAAK,EACzBE,GAAOpB,EAAI,OAAO,QAAQoB,CAAK,EAC1C,CAIA,MAAc,aAAaI,EAA+C,CAriB5E,IAAArC,EAsiBI,IAAMsC,EAAW,KAAK,QAAQ,eAC9B,GAAI,CAACA,EAAU,OACf,IAAMtB,EAAa,IAAI,gBACvB,KAAK,QAAQ,IAAIA,CAAU,EAC3B,IAAIuB,EAAW,GACTC,EAAQ,WAAW,IAAM,CAC7BD,EAAW,GACXvB,EAAW,MAAM,CACnB,GAAGhB,EAAAsC,EAAS,YAAT,KAAAtC,EAAsByC,CAAyB,EAE5CC,EAAQ,IAAI,QAAe,CAACC,EAAGtE,IACnC2C,EAAW,OAAO,iBAAiB,QAAS,IAC1C3C,EAAO,IAAI,MAAM,SAAS,CAAC,CAC7B,CACF,EACA,GAAI,CACF,IAAMuE,EAAQ,MAAM,QAAQ,KAAK,CAC/BN,EAAS,IAAID,EAASrB,EAAW,MAAM,EACvC0B,CACF,CAAC,EACDG,EAAuB,KAAK,QAAS,CAAE,MAAAD,CAAM,CAAC,CAChD,OAAS1D,EAAO,CACd,IAAM4D,EAAOP,EAAW,iBAAmB,gBAC3C,KAAK,IAAI,IAAI,gBAAiB,CAAE,KAAAO,EAAM,MAAO,OAAO5D,CAAK,CAAE,CAAC,EAC5D,KAAK,mBAAmB4D,CAAI,CAC9B,QAAE,CACA,aAAaN,CAAK,EAClB,KAAK,QAAQ,OAAOxB,CAAU,CAChC,CACF,CAMQ,mBAAmB8B,EAAgD,CACzE,IAAMhB,EAAsB,CAAE,MAAO,CAAC,EAAG,MAAO,CAAE,KAAAgB,CAAK,CAAE,EACzD,KAAK,QAAQ,cACX,IAAI,YAAyBC,EAAsC,CACjE,QAAS,GACT,SAAU,GACV,OAAAjB,CACF,CAAC,CACH,CACF,CAEQ,cAAqB,CAC3B,GAAI,KAAK,UAAW,MAAMlB,EAAe,CAC3C,CACF,EAGA,SAASpB,EAAiBF,EAAiBmC,EAA6B,CACtEnC,EAAG,aAAa,gBAAiBmC,EAAO,YAAY,EACpDnC,EAAG,aAAa,eAAgBmC,EAAO,KAAK,CAC9C,CCzjBA,IAAMuB,EAAN,KAA2C,CAMzC,YAA6BC,EAAwB,CAAxB,YAAAA,EAL7B,KAAiB,UAA+B,CAAC,EAM/C,KAAK,IAAMC,EAAeD,EAAO,KAAK,EACtC,KAAK,QAAU,CACb,OAAAA,EACA,IAAK,KAAK,IACV,eAAgB,IAAM,KAAK,eAAe,EAAE,MAAM,IAAG,EAAY,EACjE,YAAcE,GAAa,CACzB,IAAMC,EAAQ,KAAK,UAAU,QAAQD,CAAQ,EACzCC,GAAS,GAAG,KAAK,UAAU,OAAOA,EAAO,CAAC,CAChD,CACF,CACF,CAEA,IAAI,SAAuC,CACzC,OAAO,KAAK,UAAU,KAAK,UAAU,OAAS,CAAC,CACjD,CAEA,MAAsB,CACpB,OAAOC,GAAY,KAAK,OAAO,YAAa,KAAK,OAAO,OAAO,CACjE,CAEA,MAAM,MAAMC,EAAiD,CAC3D,IAAMC,EAAQC,EAAqBF,CAAO,EAC1C,MAAM,KAAK,KAAK,EAChB,IAAMH,EAAW,MAAMM,EAAgB,MAAM,KAAK,QAASF,CAAK,EAChE,YAAK,UAAU,KAAKJ,CAAQ,EACrBA,CACT,CAEA,MAAa,CACX,KAAK,eAAe,EAAE,KAAK,CAC7B,CAEA,MAA6B,CAC3B,IAAMO,EAAU,KAAK,QACrB,OAAOA,EAAUA,EAAQ,KAAK,EAAI,QAAQ,OAAO,KAAK,WAAW,CAAC,CACpE,CAEA,gBAAgC,CAC9B,IAAMA,EAAU,KAAK,QACrB,OAAOA,EACHA,EAAQ,eAAe,EACvB,QAAQ,OAAO,KAAK,WAAW,CAAC,CACtC,CAEA,gBAAgC,CAC9B,OAAK,KAAK,iBACR,KAAK,gBAAkB,SAAY,CACjC,GAAI,CACF,IAAMC,EAAS,MAAM,KAAK,OAAO,QAAQ,eAAe,EAClDC,EAAQD,GAAA,YAAAA,EAAQ,aACtB,GAAI,OAAOC,GAAU,UAAYA,IAAU,GACzC,MAAM,IAAIC,EACR,iBACA,yDACF,EAEF,QAAWV,KAAY,KAAK,UAAWA,EAAS,aAAaS,CAAK,EAClE,KAAK,IAAI,mBAAmB,CAC9B,OAASE,EAAO,CACd,WAAK,IAAI,yBAA0B,CAAE,MAAO,OAAOA,CAAK,CAAE,CAAC,EACrDA,aAAiBD,EACnBC,EACA,IAAID,EACF,iBACA,8CACA,CACE,UAAW,GACX,MAAAC,CACF,CACF,CACN,QAAE,CACA,KAAK,eAAiB,MACxB,CACF,GAAG,GAEE,KAAK,cACd,CAEA,QAAe,CACb,QAAWX,KAAY,KAAK,UAAWA,EAAS,OAAO,EACvD,KAAK,IAAI,YAAY,CACvB,CAEA,SAAgB,CACd,QAAWA,IAAY,CAAC,GAAG,KAAK,SAAS,EAAGA,EAAS,QAAQ,CAC/D,CAEQ,gBAAkC,CACxC,IAAMO,EAAU,KAAK,QACrB,GAAI,CAACA,EAAS,MAAM,KAAK,WAAW,EACpC,OAAOA,CACT,CAEQ,YAA2B,CACjC,OAAOK,EAAY,4CAA4C,CACjE,CACF,EAMO,SAASC,GAAcf,EAAoC,CAChE,IAAMgB,EAAWC,EAAcjB,CAAM,EACrC,OAAAkB,GAA2BF,EAAS,WAAW,EACxC,IAAIjB,EAAkBiB,CAAQ,CACvC,CCpJO,IAAMG,EAAe,aAGtBC,GAAkB,kBAOjB,SAASC,IAA0B,CACxC,IAAMC,EAAM,IAAI,IAAI,OAAO,SAAS,IAAI,EACxC,OAAAA,EAAI,aAAa,IAAIH,EAAc,GAAG,EAC/BG,EAAI,SAAWA,EAAI,OAASA,EAAI,IACzC,CAGO,SAASC,GAAiBC,EAAuB,CACtD,MAAO,eAAe,KAAKA,CAAI,CACjC,CAMO,SAASC,IAA+B,CAC7C,GAAI,OAAO,QAAW,YAAa,MAAO,GAC1C,IAAMH,EAAM,IAAI,IAAI,OAAO,SAAS,IAAI,EACxC,OAAIA,EAAI,aAAa,IAAIH,CAAY,IAAM,IAAY,IACvDG,EAAI,aAAa,OAAOH,CAAY,EACpC,OAAO,QAAQ,aACb,OAAO,QAAQ,MACf,GACAG,EAAI,SAAWA,EAAI,OAASA,EAAI,IAClC,EACO,GACT,CAGO,SAASI,GAASC,EAA4B,CAxCrD,IAAAC,EAyCE,GAAI,OAAO,cAAiB,YAAa,MAAO,GAChD,GAAI,CACF,IAAMC,EAAQ,KAAK,OACjBD,EAAA,aAAa,QAAQR,EAAe,IAApC,KAAAQ,EAAyC,IAC3C,EACA,OAAO,OAAO,OAAOC,CAAK,EAAE,KAAMC,IAAUA,GAAA,YAAAA,EAAO,aAAcH,CAAS,CAC5E,MAAQ,CACN,MAAO,EACT,CACF",
6
+ "names": ["BuilderError", "_BuilderError", "code", "message", "options", "_a", "_b", "SAVE_FAILURE_MESSAGES", "saveFailedError", "detail", "licenseError", "destroyedError", "configError", "DEFAULT_SAVE_TIMEOUT_MS", "DEFAULT_EXPORT_TIMEOUT_MS", "CONFIG_KEYS", "MOUNT_KEYS", "VERSION_PATTERN", "INSTALLATION_KEY_PATTERN", "isExactVersion", "value", "isRecord", "isFunction", "isPositiveNumber", "containsSecret", "depth", "v", "rejectUnknownKeys", "allowed", "where", "key", "configError", "resolveConfig", "config", "_a", "_b", "_c", "environment", "version", "saveTimeoutMs", "session", "login", "customization", "validateMountOptions", "options", "target", "hostProductId", "mode", "designId", "validateObservabilityContext", "externalExport", "parsed", "validateSession", "fail", "what", "BuilderError", "hasToken", "hasRefresh", "validateShopperTokens", "tokens", "TOKEN_KEY", "redact", "value", "depth", "v", "out", "key", "createDebugLog", "enabled", "message", "detail", "BUILDER_TAG", "BUNDLE_HOSTS", "DEFINE_TIMEOUT_MS", "STATE_KEY", "pageState", "holder", "state", "isBrowser", "assertEnvironmentAvailable", "environment", "configError", "loadFailed", "message", "cause", "BuilderError", "resolveVersion", "host", "requested", "response", "info", "version", "isExactVersion", "injectScript", "src", "resolve", "reject", "script", "whenDefined", "timer", "assertCompatible", "loadBuilder", "error", "deferred", "resolve", "reject", "promise", "res", "rej", "PausableTimer", "ms", "onExpire", "BuilderInstance", "_BuilderInstance", "ctx", "options", "session", "raw", "cause", "BuilderError", "validateSession", "instance", "el", "BUILDER_TAG", "setShopperTokens", "on", "name", "handler", "listener", "event", "THREED_BUILDER_LOGIN_REQUIRED_EVENT", "d", "_a", "THREED_BUILDER_LICENSE_TOKEN_REQUIRED_EVENT", "THREED_BUILDER_LICENSE_ERROR_EVENT", "THREED_BUILDER_DESIGN_SAVED_EVENT", "THREED_BUILDER_DESIGN_SAVE_FAILED_EVENT", "saveFailedError", "THREED_BUILDER_PRODUCT_CLONED_EVENT", "THREED_BUILDER_CLOSE_REQUESTED_EVENT", "_b", "THREED_BUILDER_STATUS_EVENT", "THREED_BUILDER_EXPORT_REQUESTED_EVENT", "dispatchOpenThreedBuilder", "destroyedError", "run", "dispatchSaveRequest", "epoch", "controller", "context", "validateObservabilityContext", "licenseToken", "action", "login", "dispatchLoginCancelled", "stale", "redirected", "tokens", "validateShopperTokens", "choice", "outcome", "pending", "detail", "error", "licenseError", "saved", "link", "wait", "customization", "request", "external", "timedOut", "timer", "DEFAULT_EXPORT_TIMEOUT_MS", "abort", "_", "files", "dispatchExportComplete", "code", "THREED_BUILDER_EXPORT_COMPLETE_EVENT", "BuilderController", "config", "createDebugLog", "instance", "index", "loadBuilder", "options", "valid", "validateMountOptions", "BuilderInstance", "current", "result", "token", "BuilderError", "cause", "configError", "createBuilder", "resolved", "resolveConfig", "assertEnvironmentAvailable", "RESUME_PARAM", "DRAFT_INDEX_KEY", "loginReturnPath", "url", "isSafeReturnPath", "path", "consumeResumeMarker", "hasDraft", "productId", "_a", "index", "draft"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3dn-builder/builder",
3
- "version": "0.636.0",
3
+ "version": "0.638.0",
4
4
  "description": "Embed the 3DN product builder in any storefront: loading, login, license refresh, save and customization records.",
5
5
  "license": "MIT",
6
6
  "repository": {