@pie-players/pie-calculator-desmos 0.3.68 → 0.3.70

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,245 +1,167 @@
1
1
  # @pie-players/pie-calculator-desmos
2
2
 
3
- Desmos calculator provider for PIE Players - Premium graphing, scientific, and basic calculators.
3
+ PIE's provider adapter for the Desmos Graphing, Scientific, and Four Function
4
+ Calculator APIs.
4
5
 
5
- ## Features
6
-
7
- - Beautiful, intuitive graphing calculators
8
- - Interactive expression lists
9
- - ✅ Scientific and basic calculator modes
10
- - ✅ State persistence and export
11
- - ⚠️ Requires Desmos API key from [desmos.com/api](https://www.desmos.com/api)
12
- - ⚠️ Requires internet connection
6
+ This package contains only PIE-authored adapter code. It does not bundle,
7
+ redistribute, cache, or self-host Desmos's `calculator.js` or other Desmos
8
+ assets. Unless the host has already loaded a build, the provider loads the
9
+ stable v1.12 script directly from `www.desmos.com`.
13
10
 
14
11
  ## Installation
15
12
 
16
13
  ```bash
17
- npm install @pie-players/pie-calculator-desmos
14
+ bun add @pie-players/pie-calculator-desmos
18
15
  ```
19
16
 
20
- ## How Desmos API Keys Work
21
-
22
- According to Desmos documentation, the API key should be included when loading the Desmos calculator library:
23
-
24
- ```html
25
- <script src="https://www.desmos.com/api/v1.11/calculator.js?apiKey=YOUR_KEY"></script>
26
- ```
27
-
28
- However, **this approach exposes your API key in client-side HTML**, which is a security risk for production applications.
29
-
30
- ## Security Best Practices
31
-
32
- ### ⚠️ Important Security Considerations
17
+ ## Desmos license and API key
33
18
 
34
- While Desmos's documentation shows the API key embedded in the script URL (client-side), this is **NOT SECURE for production** because:
19
+ Desmos is separately licensed and is not covered by this package's MIT license.
20
+ Obtain a key for the application at [Desmos My API](https://www.desmos.com/my-api)
21
+ and follow the [Desmos API Terms](https://www.desmos.com/api-terms):
35
22
 
36
- 1. Anyone can view your HTML source and see the API key
37
- 2. The key can be copied and used by others
38
- 3. You cannot rotate keys without redeploying your entire application
39
- 4. You have no control over who uses your key
23
+ - the free Trial Tier is limited to personal non-commercial use or a 90-day
24
+ internal evaluation for prospective commercial use;
25
+ - production use by end users and internal business use require the Commercial
26
+ Tier unless a separate written agreement applies;
27
+ - the key identifies the licensed application and must not be committed to the
28
+ repository or shared between unrelated applications; and
29
+ - self-hosting is a Desmos partner option, not a general substitute for loading
30
+ the official CDN script.
40
31
 
41
- ### Our Recommended Patterns
32
+ Desmos's documented browser integration requires the key in the
33
+ `calculator.js` URL. A browser user can therefore observe it in the network
34
+ request. Fetching the key at runtime keeps it out of source and static bundles,
35
+ but does not make it a server-only secret.
42
36
 
43
- This package supports three configuration patterns:
37
+ For backwards compatibility, calling `initialize()` with no key still loads the
38
+ historical unkeyed v1.12 URL when `window.Desmos` is absent. Existing clients
39
+ therefore continue to work without new configuration. That technical fallback
40
+ does not grant or imply a Desmos license; the deploying host remains responsible
41
+ for obtaining the rights required for its application.
44
42
 
45
- #### 1. Development Mode (Direct API Key) - Testing Only
43
+ ## Provider loading
46
44
 
47
- Use the demo key or your own key for local development:
48
-
49
- ```html
50
- <!-- Load Desmos with demo key -->
51
- <script src="https://www.desmos.com/api/v1.11/calculator.js?apiKey=REDACTED_API_KEY"></script>
52
- ```
45
+ Pass the application key when the licensed deployment should load Desmos from
46
+ its official CDN:
53
47
 
54
48
  ```typescript
55
- import { DesmosCalculatorProvider } from '@pie-players/pie-calculator-desmos';
56
-
57
- const provider = new DesmosCalculatorProvider();
58
- await provider.initialize(); // Uses the globally loaded Desmos
59
- ```
60
-
61
- ⚠️ Only use your real API key like this during development!
62
-
63
- #### 2. Production Mode (Server-Side Proxy) - ✅ RECOMMENDED
49
+ import { DesmosCalculatorProvider } from "@pie-players/pie-calculator-desmos";
64
50
 
65
- For production, use a server-side proxy to keep your API key secure:
66
-
67
- ```typescript
68
51
  const provider = new DesmosCalculatorProvider();
69
52
  await provider.initialize({
70
- proxyEndpoint: '/api/desmos/script-url' // Server returns the script URL with key
53
+ apiKey: runtimeConfig.desmosApiKey,
71
54
  });
72
55
  ```
73
56
 
74
- Your server endpoint returns a signed or time-limited URL.
75
-
76
- #### 3. Pre-loaded Library (Client-Side)
77
-
78
- If you must use client-side loading in production (not recommended), at least load the library yourself and don't pass the key through our package:
57
+ The provider loads:
79
58
 
80
- ```html
81
- <script src="https://www.desmos.com/api/v1.11/calculator.js?apiKey=YOUR_KEY"></script>
82
- ```
83
-
84
- ```typescript
85
- const provider = new DesmosCalculatorProvider();
86
- await provider.initialize(); // No key needed, uses window.Desmos
59
+ ```text
60
+ https://www.desmos.com/api/v1.12/calculator.js?apiKey=<application-key>
87
61
  ```
88
62
 
89
- **Note**: This still exposes your key in HTML, but at least it's not in your JavaScript bundle.
90
-
91
- ## Server-Side Proxy Implementation
92
-
93
- ### Understanding the Challenge
94
-
95
- Desmos requires the API key in the script URL when loading the library. To keep your key secure while still working within Desmos's architecture, you need a server-side proxy.
96
-
97
- ### Option A: Proxy the Desmos Script (Most Secure)
98
-
99
- Create a server endpoint that proxies the Desmos calculator script with your API key:
100
-
101
- #### Express.js Example
63
+ A host may fetch the key from an authenticated, same-origin endpoint before
64
+ calling `initialize()`. That endpoint should be limited to authorized users,
65
+ rate-limited as appropriate, and returned with `Cache-Control: private,
66
+ no-store`. The key still reaches those users' browsers as required by the
67
+ Desmos API.
102
68
 
103
- ```javascript
104
- // server.js
105
- app.get('/api/desmos/calculator.js', requireAuth, async (req, res) => {
106
- if (!req.user) {
107
- return res.status(401).json({ error: 'Unauthorized' });
108
- }
69
+ If a Desmos agreement permits the host to preload or self-host the API, load
70
+ that build first and initialize without a key:
109
71
 
110
- // Fetch Desmos script with your API key server-side
111
- const desmosUrl = `https://www.desmos.com/api/v1.11/calculator.js?apiKey=${process.env.DESMOS_API_KEY}`;
112
- const response = await fetch(desmosUrl);
113
- const script = await response.text();
114
-
115
- res.setHeader('Content-Type', 'application/javascript');
116
- res.send(script);
117
- });
118
- ```
119
-
120
- Then load from your proxy in the client:
72
+ ```typescript
73
+ if (!window.Desmos) throw new Error("Authorized Desmos API build was not loaded");
121
74
 
122
- ```html
123
- <script src="/api/desmos/calculator.js"></script>
75
+ const provider = new DesmosCalculatorProvider();
76
+ await provider.initialize();
124
77
  ```
125
78
 
126
- ### Option B: Return Signed/Time-Limited URL
79
+ This package deliberately has no script-proxy or self-hosting option. Do not
80
+ copy or proxy `calculator.js` unless the application's Desmos agreement grants
81
+ that right.
127
82
 
128
- Create temporary, authenticated URLs that expire:
83
+ ## Usage
129
84
 
130
- #### Next.js API Route Example
85
+ Import the provider and its owner-defined per-instance configuration from this package:
131
86
 
132
87
  ```typescript
133
- // pages/api/desmos/script-url.ts
134
- import { getServerSession } from 'next-auth';
135
- import { sign } from 'jsonwebtoken';
136
-
137
- export default async function handler(req, res) {
138
- const session = await getServerSession(req, res);
139
-
140
- if (!session) {
141
- return res.status(401).json({ error: 'Unauthorized' });
142
- }
143
-
144
- // Create a time-limited token
145
- const token = sign(
146
- { userId: session.user.id },
147
- process.env.JWT_SECRET,
148
- { expiresIn: '1h' }
149
- );
150
-
151
- // Return URL with your API key (only valid for this session)
152
- res.json({
153
- scriptUrl: `https://www.desmos.com/api/v1.11/calculator.js?apiKey=${process.env.DESMOS_API_KEY}`,
154
- expiresAt: Date.now() + 3600000
155
- });
156
- }
88
+ import {
89
+ DesmosCalculatorProvider,
90
+ type DesmosCalculatorSettings,
91
+ type DesmosCalculatorProviderConfig,
92
+ } from '@pie-players/pie-calculator-desmos';
157
93
  ```
158
94
 
159
- ### Advantages of Server-Side Proxy
95
+ `initialize()` takes the shared `CalculatorProviderInit` whole: Desmos is the one
96
+ adapter needing a credential, so it declares no narrower init type of its own.
97
+ `createCalculator()` takes `DesmosCalculatorProviderConfig`, which is the
98
+ provider-neutral configuration with `settings` typed as
99
+ `DesmosCalculatorSettings`. That is what the packaged toolkit composition
100
+ passes.
160
101
 
161
- 1. **Security**: API key never exposed to client in retrievable form
162
- 2. **Authentication**: Control who can access Desmos
163
- 3. **Rate Limiting**: Implement usage limits server-side
164
- 4. **Usage Tracking**: Monitor calculator usage for billing
165
- 5. **Key Rotation**: Change keys without redeploying client code
166
- 6. **Compliance**: Meets security requirements for sensitive data
167
-
168
- ### Reality Check
169
-
170
- **Important**: Even with a proxy, if the client loads the Desmos script, a determined user could still inspect network traffic and find the API key embedded in the script content. For true security:
171
-
172
- 1. Consider if Desmos calculators are necessary for your use case
173
- 2. Use provider-driven fallback strategies when a licensed graphing calculator is not required
174
- 3. Contact Desmos at <partnerships@desmos.com> to discuss enterprise security options
175
- 4. Implement rate limiting and usage monitoring to detect key misuse
176
-
177
- ## Usage
178
-
179
- ### Basic Calculator
102
+ ### Basic calculator
180
103
 
181
104
  ```typescript
182
- const provider = new DesmosCalculatorProvider();
183
- await provider.initialize({ proxyEndpoint: '/api/desmos/token' });
184
-
185
105
  const calculator = await provider.createCalculator(
186
- 'basic',
187
- document.getElementById('calculator-container')
106
+ "basic",
107
+ document.getElementById("calculator-container")!,
188
108
  );
189
109
  ```
190
110
 
191
- ### Scientific Calculator
111
+ ### Scientific calculator
192
112
 
193
113
  ```typescript
194
114
  const calculator = await provider.createCalculator(
195
- 'scientific',
196
- document.getElementById('calculator-container'),
115
+ "scientific",
116
+ document.getElementById("calculator-container")!,
197
117
  {
198
- desmos: {
118
+ settings: {
199
119
  degreeMode: true,
200
- functionDefinition: true
201
- }
202
- }
120
+ functionDefinition: true,
121
+ },
122
+ },
203
123
  );
204
124
  ```
205
125
 
206
- ### Graphing Calculator
126
+ ### Graphing calculator
207
127
 
208
128
  ```typescript
209
129
  const calculator = await provider.createCalculator(
210
- 'graphing',
211
- document.getElementById('calculator-container'),
130
+ "graphing",
131
+ document.getElementById("calculator-container")!,
212
132
  {
213
- desmos: {
133
+ settings: {
214
134
  expressions: true,
215
135
  settingsMenu: true,
216
136
  zoomButtons: true,
217
- plotInequalities: true
218
- }
219
- }
137
+ plotInequalities: true,
138
+ },
139
+ },
220
140
  );
221
141
  ```
222
142
 
223
- ### Restricted/Test Mode
224
-
225
- For assessments, you can restrict calculator features:
143
+ ### Restricted/test mode
226
144
 
227
145
  ```typescript
228
- const calculator = await provider.createCalculator(
229
- 'graphing',
230
- container,
231
- {
232
- restrictedMode: true, // Disables settings, zoom, expressions topbar
233
- desmos: {
234
- restrictedFunctions: true // Additional Desmos restrictions
235
- }
236
- }
237
- );
146
+ const calculator = await provider.createCalculator("graphing", container, {
147
+ restrictedMode: true,
148
+ settings: {
149
+ restrictedFunctions: true,
150
+ },
151
+ });
238
152
  ```
239
153
 
240
- ## Configuration Options
154
+ `restrictedMode` is monotonic: it suppresses the expression topbar, the settings
155
+ menu, the zoom buttons and links, it lands after `settings`, and a host cannot
156
+ relax it. It deliberately leaves the expression list alone — that list is a
157
+ `GraphingCalculator`'s only input, and hiding it left graph paper with nothing to
158
+ plot on. A host that wants it gone passes `settings: { expressions: false }` and
159
+ sets the rest itself.
160
+
161
+ Assessment restrictions and the Desmos API tier are separate concerns. A
162
+ restricted calculator still requires a key licensed for the application.
241
163
 
242
- See the `DesmosCalculatorConfig` interface in `@pie-players/pie-calculator` for all available options.
164
+ See the `DesmosCalculatorSettings` interface exported by `@pie-players/pie-calculator-desmos` for all available options.
243
165
 
244
166
  Common options:
245
167
 
@@ -250,36 +172,21 @@ Common options:
250
172
  - `border`: Show calculator border
251
173
  - `links`: Enable links to Desmos.com
252
174
 
253
- ## State Management
175
+ ## State management
254
176
 
255
177
  Save and restore calculator state:
256
178
 
257
179
  ```typescript
258
- // Export state
259
180
  const state = calculator.exportState();
260
- localStorage.setItem('calculator-state', JSON.stringify(state));
181
+ localStorage.setItem("calculator-state", JSON.stringify(state));
261
182
 
262
- // Import state
263
- const savedState = JSON.parse(localStorage.getItem('calculator-state'));
183
+ const savedState = JSON.parse(localStorage.getItem("calculator-state")!);
264
184
  calculator.importState(savedState);
265
185
  ```
266
186
 
267
- ## Loading Desmos API
268
-
269
- The Desmos API must be loaded before initializing the provider. You can load it from CDN:
270
-
271
- ```html
272
- <script src="https://www.desmos.com/api/v1.10/calculator.js?apiKey=REDACTED_API_KEY"></script>
273
- ```
274
-
275
- Or load it dynamically in your application.
276
-
277
- ## License
278
-
279
- This package is MIT licensed. The Desmos API requires a separate API key from Desmos.
280
-
281
187
  ## Links
282
188
 
283
- - [Desmos API Documentation](https://www.desmos.com/api)
284
- - [Get Desmos API Key](https://www.desmos.com/api)
189
+ - [Desmos API v1.12 documentation](https://www.desmos.com/api/v1.12/docs/index.html)
190
+ - [Desmos API Terms](https://www.desmos.com/api-terms)
191
+ - [Desmos My API](https://www.desmos.com/my-api)
285
192
  - [PIE Calculator Base Package](https://www.npmjs.com/package/@pie-players/pie-calculator)
@@ -4,20 +4,54 @@
4
4
  *
5
5
  * Supports: Basic, Scientific, and Graphing calculators
6
6
  * Based on Desmos API v1.12+
7
- * Requires: Desmos API key (obtain from https://www.desmos.com/api)
7
+ * Requires: A Desmos API key when this provider loads the API from desmos.com
8
8
  *
9
- * SECURITY BEST PRACTICE:
10
- * - Development: Pass apiKey directly for local testing
11
- * - Production: Use proxyEndpoint to keep API key server-side
9
+ * Desmos's documented browser integration places the key in the calculator.js
10
+ * URL. A runtime credential endpoint can keep the key out of source and static
11
+ * bundles, but cannot keep it secret from a browser that loads the API.
12
+ */
13
+ import type { Calculator, CalculatorProvider, CalculatorProviderCapabilities, CalculatorProviderConfig, CalculatorProviderInit, CalculatorType } from "@pie-players/pie-calculator";
14
+ /** Desmos API options accepted through `CalculatorProviderConfig.settings`. */
15
+ export interface DesmosCalculatorSettings extends Record<string, unknown> {
16
+ border?: boolean;
17
+ degreeMode?: boolean | "degree" | "radian";
18
+ decimalToFraction?: boolean;
19
+ links?: boolean;
20
+ settingsMenu?: boolean;
21
+ expressions?: boolean;
22
+ zoomButtons?: boolean;
23
+ expressionsTopbar?: boolean;
24
+ notes?: boolean;
25
+ folders?: boolean;
26
+ images?: boolean;
27
+ qwertyKeyboard?: boolean;
28
+ restrictedFunctions?: boolean;
29
+ plotSingleVariableImplicitEquations?: boolean;
30
+ distributions?: boolean;
31
+ plotImplicits?: boolean;
32
+ plotInequalities?: boolean;
33
+ geometryComputationFunctions?: boolean;
34
+ sliders?: boolean;
35
+ tables?: boolean;
36
+ expressionsCollapsed?: boolean;
37
+ administerSecretFolders?: boolean;
38
+ lockViewport?: boolean;
39
+ functionDefinition?: boolean;
40
+ brailleExpressionDownload?: boolean;
41
+ keypad?: boolean;
42
+ graphpaper?: boolean;
43
+ additionalFunctions?: string | string[];
44
+ }
45
+ /**
46
+ * Per-instance configuration accepted by the Desmos calculator provider.
12
47
  *
13
- * Example server-side proxy (Express.js):
14
- * ```
15
- * app.get('/api/desmos/token', requireAuth, (req, res) => {
16
- * res.json({ apiKey: process.env.DESMOS_API_KEY });
17
- * });
18
- * ```
48
+ * Identical to the provider-neutral shape apart from naming what `settings`
49
+ * holds, which the neutral seam deliberately leaves as `Record<string,
50
+ * unknown>`.
19
51
  */
20
- import type { Calculator, CalculatorProvider, CalculatorProviderCapabilities, CalculatorProviderConfig, CalculatorType } from "@pie-players/pie-calculator";
52
+ export interface DesmosCalculatorProviderConfig extends Omit<CalculatorProviderConfig, "settings"> {
53
+ settings?: DesmosCalculatorSettings;
54
+ }
21
55
  declare global {
22
56
  interface Window {
23
57
  Desmos?: any;
@@ -33,33 +67,20 @@ export declare class DesmosCalculatorProvider implements CalculatorProvider {
33
67
  readonly version = "1.12";
34
68
  private initialized;
35
69
  private apiKey?;
36
- private proxyEndpoint?;
37
- private isDevelopment;
70
+ private readonly instances;
38
71
  private onTelemetry;
39
72
  private emitTelemetry;
40
- /**
41
- * Get the configured API key
42
- * @internal Used internally by calculator instances
43
- */
44
- getApiKey(): string | undefined;
45
73
  /**
46
74
  * Dynamically load the Desmos calculator library
47
75
  * @private
48
76
  */
49
77
  private loadDesmosScript;
50
- /**
51
- * Initialize Desmos library
52
- * @param config Configuration with API key (development) or proxy endpoint (production)
53
- */
54
- initialize(config?: {
55
- apiKey?: string;
56
- proxyEndpoint?: string;
57
- onTelemetry?: (eventName: string, payload?: Record<string, unknown>) => void | Promise<void>;
58
- }): Promise<void>;
78
+ /** Initialize Desmos with provider-level credentials and instrumentation. */
79
+ initialize(config?: CalculatorProviderInit): Promise<void>;
59
80
  /**
60
81
  * Create a calculator instance
61
82
  */
62
- createCalculator(type: CalculatorType, container: HTMLElement, config?: CalculatorProviderConfig): Promise<Calculator>;
83
+ createCalculator(type: CalculatorType, container: HTMLElement, config?: DesmosCalculatorProviderConfig): Promise<Calculator>;
63
84
  /**
64
85
  * Check if type is supported
65
86
  */
@@ -4,18 +4,11 @@
4
4
  *
5
5
  * Supports: Basic, Scientific, and Graphing calculators
6
6
  * Based on Desmos API v1.12+
7
- * Requires: Desmos API key (obtain from https://www.desmos.com/api)
7
+ * Requires: A Desmos API key when this provider loads the API from desmos.com
8
8
  *
9
- * SECURITY BEST PRACTICE:
10
- * - Development: Pass apiKey directly for local testing
11
- * - Production: Use proxyEndpoint to keep API key server-side
12
- *
13
- * Example server-side proxy (Express.js):
14
- * ```
15
- * app.get('/api/desmos/token', requireAuth, (req, res) => {
16
- * res.json({ apiKey: process.env.DESMOS_API_KEY });
17
- * });
18
- * ```
9
+ * Desmos's documented browser integration places the key in the calculator.js
10
+ * URL. A runtime credential endpoint can keep the key out of source and static
11
+ * bundles, but cannot keep it secret from a browser that loads the API.
19
12
  */
20
13
  /**
21
14
  * Desmos Calculator Provider Implementation
@@ -31,8 +24,16 @@ export class DesmosCalculatorProvider {
31
24
  version = "1.12";
32
25
  initialized = false;
33
26
  apiKey;
34
- proxyEndpoint;
35
- isDevelopment = false;
27
+ /*
28
+ * Every calculator this provider handed out and that has not destroyed itself.
29
+ *
30
+ * `destroy()` is a host's one call to release the provider, and without this it
31
+ * released only the provider's own fields: every calculator it created stayed
32
+ * mounted, with its vendor instance alive and its container populated. A host
33
+ * that swaps providers, or tears a section down without walking its calculators
34
+ * first, leaked all of them.
35
+ */
36
+ instances = new Set();
36
37
  onTelemetry;
37
38
  async emitTelemetry(eventName, payload) {
38
39
  try {
@@ -42,29 +43,20 @@ export class DesmosCalculatorProvider {
42
43
  console.warn("[DesmosProvider] telemetry callback failed:", error);
43
44
  }
44
45
  }
45
- /**
46
- * Get the configured API key
47
- * @internal Used internally by calculator instances
48
- */
49
- getApiKey() {
50
- return this.apiKey;
51
- }
52
46
  /**
53
47
  * Dynamically load the Desmos calculator library
54
48
  * @private
55
49
  */
56
- async loadDesmosScript() {
50
+ async loadDesmosScript(apiKey) {
57
51
  return new Promise((resolve, reject) => {
58
52
  const script = document.createElement("script");
59
- // Include API key in script URL if available
60
- const scriptUrl = this.apiKey
61
- ? `https://www.desmos.com/api/v1.12/calculator.js?apiKey=${this.apiKey}`
62
- : "https://www.desmos.com/api/v1.12/calculator.js";
63
- script.src = scriptUrl;
53
+ const scriptUrl = new URL("https://www.desmos.com/api/v1.12/calculator.js");
54
+ if (apiKey)
55
+ scriptUrl.searchParams.set("apiKey", apiKey);
56
+ script.src = scriptUrl.toString();
64
57
  script.async = true;
65
58
  script.onload = () => {
66
59
  if (window.Desmos) {
67
- console.log("[DesmosProvider] Desmos API loaded successfully");
68
60
  resolve();
69
61
  }
70
62
  else {
@@ -77,11 +69,8 @@ export class DesmosCalculatorProvider {
77
69
  document.head.appendChild(script);
78
70
  });
79
71
  }
80
- /**
81
- * Initialize Desmos library
82
- * @param config Configuration with API key (development) or proxy endpoint (production)
83
- */
84
- async initialize(config) {
72
+ /** Initialize Desmos with provider-level credentials and instrumentation. */
73
+ async initialize(config = {}) {
85
74
  if (this.initialized)
86
75
  return;
87
76
  this.onTelemetry = config?.onTelemetry;
@@ -89,15 +78,9 @@ export class DesmosCalculatorProvider {
89
78
  if (typeof window === "undefined") {
90
79
  throw new Error("Desmos calculators can only be initialized in the browser");
91
80
  }
92
- // Determine if we're in development mode
93
- this.isDevelopment =
94
- process.env.NODE_ENV === "development" ||
95
- typeof process === "undefined" ||
96
- !process.env.NODE_ENV;
97
- // Configure API access pattern
98
- if (config?.proxyEndpoint) {
99
- // Production pattern: server-side proxy
100
- this.proxyEndpoint = config.proxyEndpoint;
81
+ this.apiKey = config.apiKey?.trim() || undefined;
82
+ const proxyEndpoint = config.proxyEndpoint?.trim();
83
+ if (proxyEndpoint) {
101
84
  const authStartedAt = Date.now();
102
85
  await this.emitTelemetry("pie-tool-backend-call-start", {
103
86
  toolId: "calculator",
@@ -105,19 +88,21 @@ export class DesmosCalculatorProvider {
105
88
  operation: "proxy-auth-fetch",
106
89
  });
107
90
  try {
108
- const response = await fetch(config.proxyEndpoint);
91
+ const response = await fetch(proxyEndpoint);
109
92
  if (!response.ok) {
110
- throw new Error(`Proxy endpoint returned ${response.status}`);
93
+ throw new Error(`Runtime endpoint returned ${response.status}`);
111
94
  }
112
- const data = await response.json();
113
- this.apiKey = data.apiKey;
95
+ const body = (await response.json());
96
+ if (typeof body.apiKey !== "string" || !body.apiKey.trim()) {
97
+ throw new Error("Runtime endpoint did not return a non-empty apiKey");
98
+ }
99
+ this.apiKey = body.apiKey.trim();
114
100
  await this.emitTelemetry("pie-tool-backend-call-success", {
115
101
  toolId: "calculator",
116
102
  backend: "desmos",
117
103
  operation: "proxy-auth-fetch",
118
104
  duration: Date.now() - authStartedAt,
119
105
  });
120
- console.log("[DesmosProvider] Initialized with server-side proxy (SECURE)");
121
106
  }
122
107
  catch (error) {
123
108
  await this.emitTelemetry("pie-tool-backend-call-error", {
@@ -128,31 +113,14 @@ export class DesmosCalculatorProvider {
128
113
  errorType: "CalculatorProxyAuthError",
129
114
  message: error instanceof Error ? error.message : String(error),
130
115
  });
131
- throw new Error(`[DesmosProvider] Failed to fetch API key from proxy: ${error}`);
132
- }
133
- }
134
- else if (config?.apiKey) {
135
- // Development pattern: direct API key
136
- this.apiKey = config.apiKey;
137
- // Security warning in production
138
- if (!this.isDevelopment) {
139
- console.error("⚠️ [DesmosProvider] SECURITY WARNING: API key exposed in client-side code!\n" +
140
- "This is insecure for production. Use proxyEndpoint instead.\n" +
141
- "See: https://pie-players.dev/docs/calculator-desmos#security");
142
- }
143
- else {
144
- console.log("[DesmosProvider] Initialized with direct API key (DEVELOPMENT MODE)");
116
+ throw new Error(`[DesmosProvider] Failed to fetch API key from runtime endpoint: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
145
117
  }
146
118
  }
147
- else {
148
- // No API key provided
149
- console.warn("[DesmosProvider] No API key or proxy endpoint provided.\n" +
150
- "Production usage requires authentication. Obtain API key from https://www.desmos.com/api\n" +
151
- "Recommended: Use proxyEndpoint for production, apiKey for development only.");
152
- }
153
119
  // Load Desmos API if not already loaded
154
120
  if (!window.Desmos) {
155
- console.log("[DesmosProvider] Loading Desmos API library...");
121
+ if (!this.apiKey) {
122
+ console.warn("[DesmosProvider] Loading the legacy unkeyed Desmos URL for compatibility. Configure an application API key for licensed deployments.");
123
+ }
156
124
  const libraryLoadStartedAt = Date.now();
157
125
  await this.emitTelemetry("pie-tool-library-load-start", {
158
126
  toolId: "calculator",
@@ -160,7 +128,7 @@ export class DesmosCalculatorProvider {
160
128
  operation: "desmos-script-load",
161
129
  });
162
130
  try {
163
- await this.loadDesmosScript();
131
+ await this.loadDesmosScript(this.apiKey);
164
132
  await this.emitTelemetry("pie-tool-library-load-success", {
165
133
  toolId: "calculator",
166
134
  backend: "desmos",
@@ -192,7 +160,9 @@ export class DesmosCalculatorProvider {
192
160
  if (!this.supportsType(type)) {
193
161
  throw new Error(`Desmos does not support calculator type: ${type}`);
194
162
  }
195
- return new DesmosCalculator(this, type, container, config, this.apiKey);
163
+ const calculator = new DesmosCalculator(this, type, container, config, () => this.instances.delete(calculator));
164
+ this.instances.add(calculator);
165
+ return calculator;
196
166
  }
197
167
  /**
198
168
  * Check if type is supported
@@ -204,7 +174,12 @@ export class DesmosCalculatorProvider {
204
174
  * Cleanup
205
175
  */
206
176
  destroy() {
177
+ // A copy: each `destroy()` calls back to remove itself from the set.
178
+ for (const instance of [...this.instances])
179
+ instance.destroy();
180
+ this.instances.clear();
207
181
  this.initialized = false;
182
+ this.apiKey = undefined;
208
183
  this.onTelemetry = undefined;
209
184
  }
210
185
  /**
@@ -221,16 +196,32 @@ export class DesmosCalculatorProvider {
221
196
  };
222
197
  }
223
198
  }
199
+ /**
200
+ * Credentials are provider-level (`initialize()`), never per-instance options.
201
+ *
202
+ * `DesmosCalculatorSettings` carries an index signature, so `settings` still
203
+ * *accepts* both names from a caller that has not moved them yet. Desmos treats
204
+ * an unknown option as an error, and a key there reaches nothing that would use
205
+ * it, so both are dropped before `settings` reaches the vendor constructor.
206
+ */
207
+ const CREDENTIAL_KEYS = ["apiKey", "proxyEndpoint"];
208
+ function stripCredentialKeys(config) {
209
+ for (const key of CREDENTIAL_KEYS)
210
+ delete config[key];
211
+ }
224
212
  /**
225
213
  * Desmos Calculator Instance
226
214
  */
227
215
  class DesmosCalculator {
216
+ onDestroy;
228
217
  provider;
229
218
  type;
230
219
  Desmos;
231
220
  calculator;
232
221
  container;
233
- constructor(provider, type, container, config, apiKey) {
222
+ destroyed = false;
223
+ constructor(provider, type, container, config, onDestroy) {
224
+ this.onDestroy = onDestroy;
234
225
  this.provider = provider;
235
226
  this.type = type;
236
227
  this.container = container;
@@ -238,21 +229,41 @@ class DesmosCalculator {
238
229
  if (!this.Desmos) {
239
230
  throw new Error("Desmos API not available");
240
231
  }
241
- this._initializeCalculator(config, apiKey);
232
+ this._initializeCalculator(config);
242
233
  }
243
- _initializeCalculator(config, apiKey) {
244
- // Merge Desmos-specific config with defaults
234
+ _initializeCalculator(config) {
235
+ const isGraphing = this.type === "graphing";
245
236
  const desmosConfig = {
246
- ...(config?.desmos || {}),
247
- apiKey: apiKey || config?.desmos?.apiKey,
237
+ degreeMode: true,
238
+ settingsMenu: isGraphing,
239
+ qwertyKeyboard: false,
240
+ notes: isGraphing,
241
+ folders: isGraphing,
242
+ sliders: isGraphing,
243
+ tables: isGraphing,
244
+ ...(config?.settings || {}),
248
245
  };
249
- // Apply restricted mode if specified
246
+ stripCredentialKeys(desmosConfig);
247
+ /*
248
+ * Restricted mode is monotonic: it lands after the host's own `settings` and a
249
+ * host cannot relax it, which is the same contract the Cortex adapter states
250
+ * for its own flag. It suppresses chrome a learner has no use for mid-item and
251
+ * routes out of the tool.
252
+ *
253
+ * `expressions: false` is deliberately not among them. It removes the whole
254
+ * expression list, which on a `GraphingCalculator` is the only way to enter a
255
+ * function, so a restricted graphing calculator was graph paper with nothing to
256
+ * plot on it — the call this package's own README documents. Basic and
257
+ * scientific never noticed: `expressions` is a graphing option their
258
+ * constructors ignore. A host that does want the list gone passes
259
+ * `settings: { expressions: false }` and sets the flags below itself, since
260
+ * those are honoured whenever `restrictedMode` is not what overrides them.
261
+ */
250
262
  if (config?.restrictedMode) {
251
263
  Object.assign(desmosConfig, {
252
264
  expressionsTopbar: false,
253
265
  settingsMenu: false,
254
266
  zoomButtons: false,
255
- expressions: false,
256
267
  links: false,
257
268
  });
258
269
  }
@@ -270,7 +281,6 @@ class DesmosCalculator {
270
281
  default:
271
282
  throw new Error(`Unsupported calculator type: ${this.type}`);
272
283
  }
273
- console.log(`[DesmosCalculator] Created ${this.type} calculator`);
274
284
  }
275
285
  getValue() {
276
286
  // For graphing calculator, get the state
@@ -361,10 +371,13 @@ class DesmosCalculator {
361
371
  }
362
372
  }
363
373
  destroy() {
374
+ if (this.destroyed)
375
+ return;
376
+ this.destroyed = true;
364
377
  if (this.calculator && this.calculator.destroy) {
365
378
  this.calculator.destroy();
366
379
  }
367
380
  this.container.replaceChildren();
368
- console.log("[DesmosCalculator] destroyed");
381
+ this.onDestroy();
369
382
  }
370
383
  }
package/dist/index.d.ts CHANGED
@@ -4,3 +4,4 @@
4
4
  * Desmos calculator provider - High-quality graphing calculators.
5
5
  */
6
6
  export { DesmosCalculatorProvider } from "./desmos-provider.js";
7
+ export type { DesmosCalculatorProviderConfig, DesmosCalculatorSettings, } from "./desmos-provider.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-calculator-desmos",
3
- "version": "0.3.68",
3
+ "version": "0.3.70",
4
4
  "description": "Desmos calculator provider for PIE Assessment Toolkit - High-quality graphing calculators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,6 +17,7 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "tsc",
20
+ "test": "bun test tests",
20
21
  "typecheck": "tsc --noEmit"
21
22
  },
22
23
  "keywords": [
@@ -38,7 +39,7 @@
38
39
  "access": "public"
39
40
  },
40
41
  "dependencies": {
41
- "@pie-players/pie-calculator": "0.3.68"
42
+ "@pie-players/pie-calculator": "0.3.70"
42
43
  },
43
44
  "devDependencies": {
44
45
  "typescript": "^5.9.3"