@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 +103 -196
- package/dist/desmos-provider.d.ts +49 -28
- package/dist/desmos-provider.js +92 -79
- package/dist/index.d.ts +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,245 +1,167 @@
|
|
|
1
1
|
# @pie-players/pie-calculator-desmos
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PIE's provider adapter for the Desmos Graphing, Scientific, and Four Function
|
|
4
|
+
Calculator APIs.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
14
|
+
bun add @pie-players/pie-calculator-desmos
|
|
18
15
|
```
|
|
19
16
|
|
|
20
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
## Provider loading
|
|
46
44
|
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
53
|
+
apiKey: runtimeConfig.desmosApiKey,
|
|
71
54
|
});
|
|
72
55
|
```
|
|
73
56
|
|
|
74
|
-
|
|
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
|
-
```
|
|
81
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
123
|
-
|
|
75
|
+
const provider = new DesmosCalculatorProvider();
|
|
76
|
+
await provider.initialize();
|
|
124
77
|
```
|
|
125
78
|
|
|
126
|
-
|
|
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
|
-
|
|
83
|
+
## Usage
|
|
129
84
|
|
|
130
|
-
|
|
85
|
+
Import the provider and its owner-defined per-instance configuration from this package:
|
|
131
86
|
|
|
132
87
|
```typescript
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
document.getElementById(
|
|
106
|
+
"basic",
|
|
107
|
+
document.getElementById("calculator-container")!,
|
|
188
108
|
);
|
|
189
109
|
```
|
|
190
110
|
|
|
191
|
-
### Scientific
|
|
111
|
+
### Scientific calculator
|
|
192
112
|
|
|
193
113
|
```typescript
|
|
194
114
|
const calculator = await provider.createCalculator(
|
|
195
|
-
|
|
196
|
-
document.getElementById(
|
|
115
|
+
"scientific",
|
|
116
|
+
document.getElementById("calculator-container")!,
|
|
197
117
|
{
|
|
198
|
-
|
|
118
|
+
settings: {
|
|
199
119
|
degreeMode: true,
|
|
200
|
-
functionDefinition: true
|
|
201
|
-
}
|
|
202
|
-
}
|
|
120
|
+
functionDefinition: true,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
203
123
|
);
|
|
204
124
|
```
|
|
205
125
|
|
|
206
|
-
### Graphing
|
|
126
|
+
### Graphing calculator
|
|
207
127
|
|
|
208
128
|
```typescript
|
|
209
129
|
const calculator = await provider.createCalculator(
|
|
210
|
-
|
|
211
|
-
document.getElementById(
|
|
130
|
+
"graphing",
|
|
131
|
+
document.getElementById("calculator-container")!,
|
|
212
132
|
{
|
|
213
|
-
|
|
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/
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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(
|
|
181
|
+
localStorage.setItem("calculator-state", JSON.stringify(state));
|
|
261
182
|
|
|
262
|
-
|
|
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
|
|
284
|
-
- [
|
|
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
|
|
7
|
+
* Requires: A Desmos API key when this provider loads the API from desmos.com
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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?:
|
|
83
|
+
createCalculator(type: CalculatorType, container: HTMLElement, config?: DesmosCalculatorProviderConfig): Promise<Calculator>;
|
|
63
84
|
/**
|
|
64
85
|
* Check if type is supported
|
|
65
86
|
*/
|
package/dist/desmos-provider.js
CHANGED
|
@@ -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
|
|
7
|
+
* Requires: A Desmos API key when this provider loads the API from desmos.com
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
91
|
+
const response = await fetch(proxyEndpoint);
|
|
109
92
|
if (!response.ok) {
|
|
110
|
-
throw new Error(`
|
|
93
|
+
throw new Error(`Runtime endpoint returned ${response.status}`);
|
|
111
94
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
232
|
+
this._initializeCalculator(config);
|
|
242
233
|
}
|
|
243
|
-
_initializeCalculator(config
|
|
244
|
-
|
|
234
|
+
_initializeCalculator(config) {
|
|
235
|
+
const isGraphing = this.type === "graphing";
|
|
245
236
|
const desmosConfig = {
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
381
|
+
this.onDestroy();
|
|
369
382
|
}
|
|
370
383
|
}
|
package/dist/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-calculator-desmos",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
42
|
+
"@pie-players/pie-calculator": "0.3.70"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"typescript": "^5.9.3"
|