Skip to content

Commit 559d805

Browse files
authored
build: Release (#3226)
2 parents 7f2c183 + 75b0ea4 commit 559d805

25 files changed

Lines changed: 2164 additions & 252 deletions

.github/workflows/ci-automated-check-environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Setup Node
1818
uses: actions/setup-node@v4
1919
with:
20-
node-version: 18
20+
node-version: 20
2121
cache: npm
2222
- name: Install dependencies
2323
run: npm ci

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,6 @@ jobs:
102102
strategy:
103103
matrix:
104104
include:
105-
- name: Node 18
106-
NODE_VERSION: 18.20.4
107105
- name: Node 20
108106
NODE_VERSION: 20.18.0
109107
- name: Node 22

Parse-Dashboard/CLI/mfa.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
const crypto = require('crypto');
2-
let inquirer = require('inquirer');
3-
if (inquirer.default) {
4-
inquirer = inquirer.default;
5-
}
2+
let inquirer;
3+
const loadInquirer = async () => {
4+
if (!inquirer) {
5+
const mod = await import('inquirer');
6+
inquirer = mod.default || mod;
7+
}
8+
};
69
const OTPAuth = require('otpauth');
710
const { copy } = require('./utils.js');
811
const phrases = {
@@ -11,9 +14,10 @@ const phrases = {
1114
enterAppName: 'Enter the app name:',
1215
}
1316
const getAlgorithm = async () => {
17+
await loadInquirer();
1418
let { algorithm } = await inquirer.prompt([
1519
{
16-
type: 'list',
20+
type: 'select',
1721
name: 'algorithm',
1822
message: 'Which hashing algorithm do you want to use?',
1923
default: 'SHA1',
@@ -141,6 +145,7 @@ const showInstructions = ({ app, username, passwordCopied, encrypt, config }) =>
141145

142146
module.exports = {
143147
async createUser() {
148+
await loadInquirer();
144149
const data = {};
145150

146151
console.log('');
@@ -210,6 +215,7 @@ module.exports = {
210215
showInstructions({ app: data.app, username, passwordCopied: true, encrypt, config });
211216
},
212217
async createMFA() {
218+
await loadInquirer();
213219
console.log('');
214220
const { username, app } = await inquirer.prompt([
215221
{

Parse-Dashboard/app.js

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ module.exports = function(config, options) {
158158
}
159159

160160
if (typeof app.masterKey === 'function') {
161-
app.masterKey = await ConfigKeyCache.get(app.appId, 'masterKey', app.masterKeyTtl, app.masterKey);
161+
const cacheKey = matchingAccess.readOnly ? 'readOnlyMasterKey' : 'masterKey';
162+
app.masterKey = await ConfigKeyCache.get(app.appId, cacheKey, app.masterKeyTtl, app.masterKey);
162163
}
163164

164165
return app;
@@ -197,9 +198,11 @@ module.exports = function(config, options) {
197198
// In-memory conversation storage (consider using Redis in future)
198199
const conversations = new Map();
199200

200-
// Agent API endpoint for handling AI requests - scoped to specific app
201-
app.post('/apps/:appId/agent', async (req, res) => {
201+
// Agent API endpoint handler
202+
async function agentHandler(req, res) {
202203
try {
204+
const authentication = req.user;
205+
203206
const { message, modelName, conversationId, permissions } = req.body || {};
204207
const { appId } = req.params;
205208

@@ -221,11 +224,40 @@ module.exports = function(config, options) {
221224
}
222225

223226
// Find the app in the configuration
224-
const app = config.apps.find(app => (app.appNameForURL || app.appName) === appId);
225-
if (!app) {
227+
const appConfig = config.apps.find(a => (a.appNameForURL || a.appName) === appId);
228+
if (!appConfig) {
226229
return res.status(404).json({ error: `App "${appId}" not found` });
227230
}
228231

232+
// Cross-app access control — restrict to apps the authenticated user has access to
233+
const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;
234+
let isPerAppReadOnly = false;
235+
if (appsUserHasAccess) {
236+
const matchingAccess = appsUserHasAccess.find(access => access.appId === appConfig.appId);
237+
if (!matchingAccess) {
238+
return res.status(403).json({ error: 'Forbidden: you do not have access to this app' });
239+
}
240+
isPerAppReadOnly = !!matchingAccess.readOnly;
241+
}
242+
243+
// Determine if the user is read-only (globally or per-app)
244+
const isReadOnly = (authentication && authentication.isReadOnly) || isPerAppReadOnly;
245+
246+
// Build the app context — always shallow copy to avoid mutating the shared config
247+
const appContext = { ...appConfig };
248+
if (isReadOnly) {
249+
if (!appConfig.readOnlyMasterKey) {
250+
return res.status(400).json({ error: 'You need to provide a readOnlyMasterKey to use read-only features.' });
251+
}
252+
appContext.masterKey = appConfig.readOnlyMasterKey;
253+
}
254+
255+
// Resolve function-typed masterKey (supports dynamic key rotation via ConfigKeyCache)
256+
if (typeof appContext.masterKey === 'function') {
257+
const cacheKey = isReadOnly ? 'readOnlyMasterKey' : 'masterKey';
258+
appContext.masterKey = await ConfigKeyCache.get(appContext.appId, cacheKey, appContext.masterKeyTtl, appContext.masterKey);
259+
}
260+
229261
// Find the requested model
230262
const modelConfig = config.agent.models.find(model => model.name === modelName);
231263
if (!modelConfig) {
@@ -258,8 +290,12 @@ module.exports = function(config, options) {
258290
// Array to track database operations for this request
259291
const operationLog = [];
260292

293+
// Read-only users: override client permissions to deny all write operations,
294+
// preventing privilege escalation via self-authorized permissions in the request body
295+
const effectivePermissions = isReadOnly ? {} : (permissions || {});
296+
261297
// Make request to OpenAI API with app context and conversation history
262-
const response = await makeOpenAIRequest(message, model, apiKey, app, conversationHistory, operationLog, permissions);
298+
const response = await makeOpenAIRequest(message, model, apiKey, appContext, conversationHistory, operationLog, effectivePermissions);
263299

264300
// Update conversation history with user message and AI response
265301
conversationHistory.push(
@@ -280,7 +316,7 @@ module.exports = function(config, options) {
280316
conversationId: finalConversationId,
281317
debug: {
282318
timestamp: new Date().toISOString(),
283-
appId: app.appId,
319+
appId: appContext.appId,
284320
modelUsed: model,
285321
operations: operationLog
286322
}
@@ -291,7 +327,19 @@ module.exports = function(config, options) {
291327
const errorMessage = error.message || 'Provider error';
292328
res.status(500).json({ error: `Error: ${errorMessage}` });
293329
}
294-
});
330+
}
331+
332+
// Agent API endpoint — middleware chain: auth check (401) → CSRF validation (403) → handler
333+
app.post('/apps/:appId/agent',
334+
(req, res, next) => {
335+
if (users && (!req.user || !req.user.isAuthenticated)) {
336+
return res.status(401).json({ error: 'Unauthorized' });
337+
}
338+
next();
339+
},
340+
Authentication.csrfProtection,
341+
agentHandler
342+
);
295343

296344
/**
297345
* Database function tools for the AI agent
@@ -1115,7 +1163,7 @@ You have direct access to the Parse database through function calls, so you can
11151163
});
11161164

11171165
// For every other request, go to index.html. Let client-side handle the rest.
1118-
app.get('{*splat}', function(req, res) {
1166+
app.get('{*splat}', Authentication.csrfProtection, function(req, res) {
11191167
if (users && (!req.user || !req.user.isAuthenticated)) {
11201168
const redirect = req.url.replace('/login', '');
11211169
if (redirect.length > 1) {
@@ -1139,6 +1187,7 @@ You have direct access to the Parse database through function calls, so you can
11391187
</head>
11401188
<body>
11411189
<div id="browser_mount"></div>
1190+
<script id="csrf" type="application/json">"${req.csrfToken()}"</script>
11421191
<script src="${mountPath}bundles/dashboard.bundle.js"></script>
11431192
</body>
11441193
</html>

README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![Build Status](https://github.com/parse-community/parse-dashboard/workflows/ci/badge.svg?branch=release)](https://github.com/parse-community/parse-dashboard/actions?query=workflow%3Aci+branch%3Arelease)
77
[![Snyk Badge](https://snyk.io/test/github/parse-community/parse-dashboard/badge.svg)](https://snyk.io/test/github/parse-community/parse-dashboard)
88

9-
[![Node Version](https://img.shields.io/badge/nodejs-18,_20,_22,_24-green.svg?logo=node.js&style=flat)](https://nodejs.org/)
9+
[![Node Version](https://img.shields.io/badge/nodejs-20,_22,_24-green.svg?logo=node.js&style=flat)](https://nodejs.org/)
1010
[![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/parse-dashboard/releases)
1111

1212
[![npm latest version](https://img.shields.io/npm/v/parse-dashboard/latest.svg)](https://www.npmjs.com/package/parse-dashboard)
@@ -164,7 +164,6 @@ Parse Dashboard is continuously tested with the most recent releases of Node.js
164164

165165
| Version | Minimum version | End-of-Life | Compatible |
166166
|------------|----------------|-------------|------------|
167-
| Node.js 18 | 18.20.4 | May 2025 | ✅ Yes |
168167
| Node.js 20 | 20.18.0 | April 2026 | ✅ Yes |
169168
| Node.js 22 | 22.9.0 | April 2027 | ✅ Yes |
170169
| Node.js 24 | 24.0.0 | April 2028 | ✅ Yes |
@@ -1560,11 +1559,52 @@ A drop-down to select a single item from a list.
15601559
| Parameter | Value | Optional | Description |
15611560
|-----------|--------|----------|----------------------------------|
15621561
| `element` | `String` | No | Must be `"dropDown"`. |
1562+
| `name` | `String` | No | The key used in `formData`. |
15631563
| `label` | `String` | No | The display label shown next to the dropdown. |
1564+
| `description` | `String` | Yes | Secondary text below the label. |
15641565
| `items` | `Array` | No | The selectable options. |
15651566
| `items[].title` | `String` | No | The display text of the option. |
15661567
| `items[].value` | `String` | No | The value of the option. |
15671568
1569+
###### Checkbox
1570+
1571+
A checkbox for boolean input.
1572+
1573+
| Parameter | Value | Optional | Default | Description |
1574+
|-----------|--------|----------|---------|----------------------------------|
1575+
| `element` | `String` | No | - | Must be `"checkbox"`. |
1576+
| `name` | `String` | No | - | The key used in `formData`. |
1577+
| `label` | `String` | No | - | The display label. |
1578+
| `description` | `String` | Yes | - | Secondary text below the label. |
1579+
| `default` | `Boolean`| Yes | `false` | The initial checked state. |
1580+
1581+
###### Toggle
1582+
1583+
A toggle switch for boolean input.
1584+
1585+
| Parameter | Value | Optional | Default | Description |
1586+
|-------------|----------|----------|---------|----------------------------------|
1587+
| `element` | `String` | No | - | Must be `"toggle"`. |
1588+
| `name` | `String` | No | - | The key used in `formData`. |
1589+
| `label` | `String` | No | - | The display label. |
1590+
| `description` | `String` | Yes | - | Secondary text below the label. |
1591+
| `default` | `Boolean`| Yes | `false` | The initial toggle state. |
1592+
| `labelTrue` | `String` | Yes | `"Yes"` | Label for the `true` side. |
1593+
| `labelFalse` | `String` | Yes | `"No"` | Label for the `false` side. |
1594+
1595+
###### Text Input
1596+
1597+
A single-line text input.
1598+
1599+
| Parameter | Value | Optional | Default | Description |
1600+
|-------------|----------|----------|---------|----------------------------------|
1601+
| `element` | `String` | No | - | Must be `"textInput"`. |
1602+
| `name` | `String` | No | - | The key used in `formData`. |
1603+
| `label` | `String` | No | - | The display label. |
1604+
| `description` | `String` | Yes | - | Secondary text below the label. |
1605+
| `placeholder` | `String` | Yes | `""` | Placeholder text. |
1606+
| `default` | `String` | Yes | `""` | The initial value. |
1607+
15681608
### Graph
15691609
15701610
▶️ *Core > Browser > Graph*

changelogs/CHANGELOG_alpha.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,64 @@
1+
# [9.0.0-alpha.8](https://github.com/parse-community/parse-dashboard/compare/9.0.0-alpha.7...9.0.0-alpha.8) (2026-02-19)
2+
3+
4+
### Bug Fixes
5+
6+
* Incomplete authentication on AI Agent endpoint ([GHSA-qwc3-h9mg-4582](https://github.com/parse-community/parse-dashboard/security/advisories/GHSA-qwc3-h9mg-4582)) ([#3224](https://github.com/parse-community/parse-dashboard/issues/3224)) ([f92a9ef](https://github.com/parse-community/parse-dashboard/commit/f92a9ef5246d57e51696bd881a15f3b133b2bb50))
7+
8+
# [9.0.0-alpha.7](https://github.com/parse-community/parse-dashboard/compare/9.0.0-alpha.6...9.0.0-alpha.7) (2026-02-19)
9+
10+
11+
### Features
12+
13+
* Add option to block saving Cloud Config parameter if validation fails ([#3225](https://github.com/parse-community/parse-dashboard/issues/3225)) ([41691aa](https://github.com/parse-community/parse-dashboard/commit/41691aafbd516a3e63cf30a1739e30985e7ca4ea))
14+
15+
# [9.0.0-alpha.6](https://github.com/parse-community/parse-dashboard/compare/9.0.0-alpha.5...9.0.0-alpha.6) (2026-02-17)
16+
17+
18+
### Features
19+
20+
* Add Regex string validation when editing Cloud Config parameter ([#3222](https://github.com/parse-community/parse-dashboard/issues/3222)) ([067b9d1](https://github.com/parse-community/parse-dashboard/commit/067b9d114aa0f6de24940fa6d4a2b3e667c8b3b5))
21+
22+
# [9.0.0-alpha.5](https://github.com/parse-community/parse-dashboard/compare/9.0.0-alpha.4...9.0.0-alpha.5) (2026-02-14)
23+
24+
25+
### Bug Fixes
26+
27+
* Cloud Config parameter modal overlays non-printable character info box ([#3221](https://github.com/parse-community/parse-dashboard/issues/3221)) ([983253e](https://github.com/parse-community/parse-dashboard/commit/983253e85e5fe7aefeb34051bebee1ddd2fa4274))
28+
29+
# [9.0.0-alpha.4](https://github.com/parse-community/parse-dashboard/compare/9.0.0-alpha.3...9.0.0-alpha.4) (2026-02-14)
30+
31+
32+
### Features
33+
34+
* Add support for reversing info panel auto-scroll direction while holding Command+Option keys ([#3220](https://github.com/parse-community/parse-dashboard/issues/3220)) ([7ebd121](https://github.com/parse-community/parse-dashboard/commit/7ebd1210b47f483182da80b3f2fb31b1c1b4bf16))
35+
36+
# [9.0.0-alpha.3](https://github.com/parse-community/parse-dashboard/compare/9.0.0-alpha.2...9.0.0-alpha.3) (2026-02-13)
37+
38+
39+
### Features
40+
41+
* Add support for checkbox, toggle, text input elements to script form ([#3219](https://github.com/parse-community/parse-dashboard/issues/3219)) ([b9366bc](https://github.com/parse-community/parse-dashboard/commit/b9366bc9cf4b24650f991b0883c1ba1f2897969d))
42+
43+
# [9.0.0-alpha.2](https://github.com/parse-community/parse-dashboard/compare/9.0.0-alpha.1...9.0.0-alpha.2) (2026-02-13)
44+
45+
46+
### Bug Fixes
47+
48+
* Non-printable character box missing when editing Cloud Config parameter ([#3218](https://github.com/parse-community/parse-dashboard/issues/3218)) ([719ac09](https://github.com/parse-community/parse-dashboard/commit/719ac0948344c8956ccc9bbf8a0242131fc01962))
49+
50+
# [9.0.0-alpha.1](https://github.com/parse-community/parse-dashboard/compare/8.5.0...9.0.0-alpha.1) (2026-02-12)
51+
52+
53+
### Features
54+
55+
* Remove Node 18 support ([#3212](https://github.com/parse-community/parse-dashboard/issues/3212)) ([a5c1bb2](https://github.com/parse-community/parse-dashboard/commit/a5c1bb2d597df1e8670aa5f61640fa7961812530))
56+
57+
58+
### BREAKING CHANGES
59+
60+
* Removes support for Node 18. ([a5c1bb2](a5c1bb2))
61+
162
# [8.5.0-alpha.7](https://github.com/parse-community/parse-dashboard/compare/8.5.0-alpha.6...8.5.0-alpha.7) (2026-02-10)
263

364

0 commit comments

Comments
 (0)