json-object-editor 0.10.625 → 0.10.632

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-object-editor",
3
- "version": "0.10.625",
3
+ "version": "0.10.632",
4
4
  "description": "JOE the Json Object Editor | Platform Edition",
5
5
  "main": "app.js",
6
6
  "scripts": {
package/readme.md CHANGED
@@ -660,4 +660,11 @@ To help you develop and debug the widget + plugin in your instance, JOE exposes
660
660
  <a href="/ai-widget-test.html" target="ai_widget_test_win" rel="noopener">AI Widget</a>
661
661
  ```
662
662
 
663
- - This appears on MCP test/export/prompt pages and on the AI widget test page itself.
663
+ - This appears on MCP test/export/prompt pages and on the AI widget test page itself.
664
+
665
+ ### AI / Widget Changelog (current work – `0.10.632`)
666
+
667
+ - Added a Responses‑based tool runner for `<joe-ai-widget>` that wires `ai_assistant.tools` into MCP functions via `chatgpt.runWithTools`.
668
+ - Enhanced widget UX: assistant/user bubble theming (using `assistant_color` and user `color`), inline “tools used this turn” meta messages, and markdown rendering for assistant replies.
669
+ - Expanded the AI widget test page with an assistant picker, live tool JSON viewer, a clickable conversation history list (resume existing `ai_widget_conversation` threads), and safer user handling (widget conversations now store user id/name/color explicitly and OAuth token‑exchange errors from Google are surfaced clearly during login).
670
+ - Added field-level AI autofill support: schemas can declare `ai` config on a field (e.g. `{ name:'ai_summary', type:'rendering', ai:{ prompt:'Summarize the project in a few sentences.' } }`), which renders an inline “AI” button that calls `_joe.Ai.populateField('ai_summary')` and posts to `/API/plugin/chatgpt/autofill` to compute a JSON `patch` and update the UI (with confirmation before overwriting non-empty values).
@@ -0,0 +1,97 @@
1
+ var App = function () {
2
+
3
+ this.title = 'AIHub';
4
+ this.description = 'Central hub for managing AI assistants, prompts, tools, conversations, and widget chats in JOE.';
5
+
6
+ // Core AI-related schemas in JOE
7
+ this.collections = [
8
+ 'ai_assistant',
9
+ 'ai_prompt',
10
+ 'ai_tool',
11
+ 'ai_response',
12
+ 'ai_conversation',
13
+ 'ai_widget_conversation'
14
+ ].concat(JOE.webconfig.default_schemas);
15
+
16
+ this.dashboard = [
17
+ // Standard app home: shows AIHub schemas plus default schemas
18
+ JOE.Apps.Cards.appHome({ cssclass: 'w2 h3' }),
19
+
20
+ // Small MCP test portal card
21
+ {
22
+ type: 'Card',
23
+ config: {
24
+ title: 'MCP Test Portal',
25
+ left: 0,
26
+ top: 3,
27
+ cssclass: 'w2 h1',
28
+ content: function () {
29
+ return '' +
30
+ '<div class="spaced">' +
31
+ '<div><b>MCP Tools & Tests</b></div>' +
32
+ '<ul style="margin:6px 0 0 16px; padding:0; list-style:disc;">' +
33
+ '<li><a href="/mcp-test.html" target="mcp_test_win" rel="noopener">MCP Test</a></li>' +
34
+ '<li><a href="/mcp-export.html" target="mcp_export_win" rel="noopener">MCP Export</a></li>' +
35
+ '<li><a href="/mcp-schemas.html" target="mcp_schemas_win" rel="noopener">Schemas</a></li>' +
36
+ '<li><a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a></li>' +
37
+ '<li><a href="/ai-widget-test.html" target="ai_widget_test_win">AI Widget</a></li>'+
38
+ '</ul>' +
39
+ '</div>';
40
+ }
41
+ }
42
+ },
43
+
44
+ // Cap card with an embedded joe-ai-widget that fills the panel
45
+ {
46
+ type: 'Card',
47
+ config: {
48
+ title: 'AIHub Chat',
49
+ left: 2,
50
+ top: 0,
51
+ cssclass: 'w4 h4',
52
+ content: function () {
53
+ return '' +
54
+ '<div style="width:100%;height:100%;display:flex;flex-direction:column;">' +
55
+ '<joe-ai-assistant-picker for_widget="aihub_widget"></joe-ai-assistant-picker>' +
56
+ '<joe-ai-widget ' +
57
+ 'id="aihub_widget" ' +
58
+ 'title="AIHub Assistant" ' +
59
+ 'source="aihub_card" ' +
60
+ 'user_id="' + ((_joe && _joe.User && _joe.User._id) || '') + '" ' +
61
+ 'style="flex:1 1 auto;width:100%;height:100%;">' +
62
+ '</joe-ai-widget>' +
63
+ '</div>';
64
+ }
65
+ }
66
+ },
67
+
68
+ // Widget conversations list (replaces Recently Updated AI Items card for now)
69
+ {
70
+ type: 'Card',
71
+ config: {
72
+ title: 'Widget Conversations',
73
+ left: 6,
74
+ top: 1,
75
+ cssclass: 'w2 h3',
76
+ content: function () {
77
+ return '' +
78
+ '<div style="width:100%;height:100%;display:flex;flex-direction:column;">' +
79
+ '<joe-ai-conversation-list ' +
80
+ 'for_widget="aihub_widget" ' +
81
+ 'source="aihub_card">' +
82
+ '</joe-ai-conversation-list>' +
83
+ '</div>';
84
+ }
85
+ }
86
+ },
87
+
88
+ // Platform/system stats in the context of AIHub
89
+ JOE.Apps.Cards.systemStats({ top: 0, left: 6 })
90
+ ];
91
+
92
+ return this;
93
+ };
94
+
95
+ module.exports = new App();
96
+
97
+
@@ -546,7 +546,10 @@ var fields = {
546
546
  return `_joe.Ai.spawnChatHelper('${object._id}');`;
547
547
  },
548
548
 
549
- }
549
+ },
550
+ listConversations:{display:'Ai Conversations', type:"content",reloadable:true,run:function(obj){
551
+ return _joe.schemas.ai_conversation.methods.listConversations(obj,true);
552
+ }},
550
553
 
551
554
  };
552
555
 
@@ -7,12 +7,21 @@
7
7
  */
8
8
 
9
9
  const MCP = {};
10
- const { Storage, Schemas } = global.JOE; // Adjust as needed based on how your modules are wired
11
10
 
12
11
  // Internal helpers
12
+ function getStorage() {
13
+ return (global.JOE && global.JOE.Storage) || null;
14
+ }
15
+
16
+ function getSchemas() {
17
+ return (global.JOE && global.JOE.Schemas) || null;
18
+ }
19
+
13
20
  function loadFromStorage(collection, query) {
14
21
  return new Promise((resolve, reject) => {
15
22
  try {
23
+ const Storage = getStorage();
24
+ if (!Storage) return reject(new Error('Storage module not initialized'));
16
25
  Storage.load(collection, query || {}, function(err, results){
17
26
  if (err) return reject(err);
18
27
  resolve(results || []);
@@ -38,6 +47,60 @@ function sanitizeItems(items) {
38
47
  }
39
48
  }
40
49
 
50
+ // Resolve simple dotted paths against an object, including arrays.
51
+ // Example: getPathValues(recipe, "ingredients.id") → ["ing1","ing2",...]
52
+ function getPathValues(root, path) {
53
+ if (!root || !path) return [];
54
+ const parts = String(path).split('.');
55
+ let current = [root];
56
+ for (let i = 0; i < parts.length; i++) {
57
+ const key = parts[i];
58
+ const next = [];
59
+ for (let j = 0; j < current.length; j++) {
60
+ const val = current[j];
61
+ if (val == null) continue;
62
+ if (Array.isArray(val)) {
63
+ val.forEach(function (item) {
64
+ if (item && Object.prototype.hasOwnProperty.call(item, key)) {
65
+ next.push(item[key]);
66
+ }
67
+ });
68
+ } else if (Object.prototype.hasOwnProperty.call(val, key)) {
69
+ next.push(val[key]);
70
+ }
71
+ }
72
+ current = next;
73
+ if (!current.length) break;
74
+ }
75
+ // Flatten one level in case the last hop produced arrays
76
+ const out = [];
77
+ current.forEach(function (v) {
78
+ if (Array.isArray(v)) {
79
+ v.forEach(function (x) { if (x != null) out.push(x); });
80
+ } else if (v != null) {
81
+ out.push(v);
82
+ }
83
+ });
84
+ return out;
85
+ }
86
+
87
+ // Best-effort helper to get a normalized schema summary for a given name.
88
+ // Prefers the precomputed `Schemas.summary[name]` map, but falls back to
89
+ // `Schemas.schema[name].summary` when the summary map has not been generated
90
+ // or that particular schema has not yet been merged in.
91
+ function getSchemaSummary(name) {
92
+ if (!name) return null;
93
+ const Schemas = getSchemas();
94
+ if (!Schemas) return null;
95
+ if (Schemas.summary && Schemas.summary[name]) {
96
+ return Schemas.summary[name];
97
+ }
98
+ if (Schemas.schema && Schemas.schema[name] && Schemas.schema[name].summary) {
99
+ return Schemas.schema[name].summary;
100
+ }
101
+ return null;
102
+ }
103
+
41
104
  function getComparable(val){
42
105
  if (val == null) return null;
43
106
  // Date-like
@@ -84,6 +147,7 @@ MCP.tools = {
84
147
 
85
148
  // List all schema names in the system
86
149
  listSchemas: async (_params, _ctx) => {
150
+ var Schemas = getSchemas();
87
151
  const list = (Schemas && (
88
152
  (Schemas.schemaList && Schemas.schemaList.length && Schemas.schemaList) ||
89
153
  (Schemas.schema && Object.keys(Schemas.schema))
@@ -146,6 +210,156 @@ MCP.tools = {
146
210
  return sanitizeItems(obj)[0];
147
211
  },
148
212
 
213
+ /**
214
+ * understandObject
215
+ *
216
+ * High-level helper for agents: given an _id (and optional itemtype),
217
+ * returns a rich payload combining:
218
+ * - object: the raw object (ids intact)
219
+ * - flattened: the same object flattened to a limited depth
220
+ * - schemas: a map of schema summaries for the main itemtype and any
221
+ * referenced itemtypes (keyed by schema name)
222
+ * - related: an array of referenced objects discovered via outbound
223
+ * relationships in the schema summary.
224
+ *
225
+ * When `slim` is false (default), each related entry includes both `object`
226
+ * and `flattened`. When `slim` is true, only the main object is flattened
227
+ * and related entries are reduced to slim references:
228
+ * { field, _id, itemtype, object: { _id, itemtype, name, info } }
229
+ *
230
+ * Agents should prefer this tool when they need to understand or work with
231
+ * an object by id, instead of issuing many individual getObject / getSchema
232
+ * calls. The original object always keeps its reference ids; expanded views
233
+ * live under `flattened` and `related[*]`.
234
+ */
235
+ understandObject: async ({ _id, itemtype, schema, depth = 2, slim = false } = {}, _ctx) => {
236
+ if (!_id) throw new Error("Missing required param '_id'");
237
+ itemtype = itemtype || schema;
238
+
239
+ // Base object (sanitized) without flattening
240
+ const base = await MCP.tools.getObject({ _id, itemtype, flatten: false }, _ctx);
241
+ const mainType = base.itemtype || itemtype || null;
242
+
243
+ const result = {
244
+ _id: base._id,
245
+ itemtype: mainType,
246
+ object: base,
247
+ flattened: null,
248
+ schemas: {},
249
+ related: [],
250
+ // Deduped lookups for global reference types
251
+ tags: {},
252
+ statuses: {},
253
+ slim: !!slim
254
+ };
255
+
256
+ // Main schema summary
257
+ const mainSummary = getSchemaSummary(mainType);
258
+ if (mainType && mainSummary) {
259
+ result.schemas[mainType] = mainSummary;
260
+ }
261
+
262
+ // Flattened view of the main object (depth-limited)
263
+ if (JOE && JOE.Utils && JOE.Utils.flattenObject) {
264
+ try {
265
+ const flat = JOE.Utils.flattenObject(base._id, { recursive: true, depth });
266
+ result.flattened = sanitizeItems(flat)[0];
267
+ } catch (_e) {
268
+ result.flattened = null;
269
+ }
270
+ }
271
+
272
+ const seenSchemas = new Set(Object.keys(result.schemas || {}));
273
+ function addSchemaIfPresent(name) {
274
+ if (!name || seenSchemas.has(name)) return;
275
+ const sum = getSchemaSummary(name);
276
+ if (sum) {
277
+ result.schemas[name] = sum;
278
+ seenSchemas.add(name);
279
+ }
280
+ }
281
+
282
+ // Discover outbound relationships from schema summary
283
+ const schemaSummary = mainType && getSchemaSummary(mainType);
284
+ const outbound = (schemaSummary &&
285
+ schemaSummary.relationships &&
286
+ Array.isArray(schemaSummary.relationships.outbound))
287
+ ? schemaSummary.relationships.outbound
288
+ : [];
289
+
290
+ for (let i = 0; i < outbound.length; i++) {
291
+ const rel = outbound[i] || {};
292
+ const field = rel.field;
293
+ const targetSchema = rel.targetSchema;
294
+ if (!field || !targetSchema) continue;
295
+
296
+ // Support nested paths like "ingredients.id" coming from objectList fields.
297
+ const vals = getPathValues(base, field);
298
+ if (!vals || !vals.length) continue;
299
+
300
+ const ids = Array.isArray(vals) ? vals : [vals];
301
+ for (let j = 0; j < ids.length; j++) {
302
+ const rid = ids[j];
303
+ if (!rid) continue;
304
+ let robj = null;
305
+ try {
306
+ robj = await MCP.tools.getObject({ _id: rid, itemtype: targetSchema, flatten: false }, _ctx);
307
+ } catch (_e) {
308
+ continue;
309
+ }
310
+ if (!robj) continue;
311
+
312
+ const rType = robj.itemtype || targetSchema;
313
+ // Global reference types (tag/status) go into top-level lookup maps
314
+ if (rType === 'tag' || rType === 'status') {
315
+ const mapName = (rType === 'tag') ? 'tags' : 'statuses';
316
+ if (!result[mapName][robj._id]) {
317
+ result[mapName][robj._id] = {
318
+ _id: robj._id,
319
+ itemtype: rType,
320
+ name: robj.name || robj.label || robj.info || ''
321
+ };
322
+ }
323
+ } else {
324
+ if (slim) {
325
+ const slimObj = toSlim(robj);
326
+ result.related.push({
327
+ field,
328
+ _id: slimObj._id,
329
+ itemtype: slimObj.itemtype || rType,
330
+ object: {
331
+ _id: slimObj._id,
332
+ itemtype: slimObj.itemtype || rType,
333
+ name: slimObj.name,
334
+ info: slimObj.info
335
+ }
336
+ });
337
+ } else {
338
+ let rflat = null;
339
+ if (JOE && JOE.Utils && JOE.Utils.flattenObject) {
340
+ try {
341
+ const f = JOE.Utils.flattenObject(robj._id, { recursive: true, depth: Math.max(1, depth - 1) });
342
+ rflat = sanitizeItems(f)[0];
343
+ } catch (_e) {
344
+ rflat = null;
345
+ }
346
+ }
347
+ result.related.push({
348
+ field,
349
+ _id: robj._id,
350
+ itemtype: rType,
351
+ object: robj,
352
+ flattened: rflat
353
+ });
354
+ }
355
+ }
356
+ addSchemaIfPresent(rType || targetSchema);
357
+ }
358
+ }
359
+
360
+ return result;
361
+ },
362
+
149
363
  /* Deprecated: use unified 'search' instead
150
364
  getObjectsByIds: async () => { throw new Error('Use search with ids instead'); },
151
365
  queryObjects: async () => { throw new Error('Use search instead'); },
@@ -249,6 +463,8 @@ MCP.tools = {
249
463
 
250
464
  // Save an object via Storage (respects events/history)
251
465
  saveObject: async ({ object }, ctx = {}) => {
466
+ const Storage = getStorage();
467
+ if (!Storage) throw new Error('Storage module not initialized');
252
468
  if (!object || !object.itemtype) throw new Error("'object' with 'itemtype' is required");
253
469
  const user = (ctx.req && ctx.req.User) ? ctx.req.User : { name: 'anonymous' };
254
470
  // Ensure server-side update timestamp parity with /API/save
@@ -270,6 +486,8 @@ MCP.tools = {
270
486
 
271
487
  // Batch save with bounded concurrency; preserves per-item history/events
272
488
  saveObjects: async ({ objects, stopOnError = false, concurrency = 5 } = {}, ctx = {}) => {
489
+ const Storage = getStorage();
490
+ if (!Storage) throw new Error('Storage module not initialized');
273
491
  if (!Array.isArray(objects) || objects.length === 0) {
274
492
  throw new Error("'objects' (non-empty array) is required");
275
493
  }
@@ -325,6 +543,7 @@ MCP.tools = {
325
543
 
326
544
  // Hydration: surface core fields (from file), all schemas, statuses, and tags (no params)
327
545
  hydrate: async (_params, _ctx) => {
546
+ var Schemas = getSchemas();
328
547
  let coreDef = (JOE && JOE.Fields && JOE.Fields['core']) || null;
329
548
  if (!coreDef) {
330
549
  try { coreDef = require(__dirname + '/../fields/core.js'); } catch (_e) { coreDef = {}; }
@@ -387,7 +606,8 @@ MCP.descriptions = {
387
606
  saveObject: "Create/update an object; triggers events/history.",
388
607
  saveObjects: "Batch save objects with bounded concurrency; per-item history/events preserved.",
389
608
  hydrate: "Loads and merges the full JOE context, including core and organization-specific schemas, relationships, universal fields (tags and statuses), and datasets. Returns a single unified object describing the active environment for use by agents, UIs, and plugins.",
390
- listApps: "List app definitions (title, description, collections, plugins)."
609
+ listApps: "List app definitions (title, description, collections, plugins).",
610
+ understandObject: "High-level helper: given an _id (and optional itemtype), returns { object, flattened, schemas, related[] } combining the main object, its schema summary, and referenced objects plus their schemas. Prefer this when you need to understand or reason about an object by id instead of issuing many separate getObject/getSchema calls."
391
611
  };
392
612
 
393
613
  MCP.params = {
@@ -440,6 +660,17 @@ MCP.params = {
440
660
  },
441
661
  required: []
442
662
  },
663
+ understandObject: {
664
+ type: "object",
665
+ properties: {
666
+ _id: { type: "string" },
667
+ itemtype: { type: "string" },
668
+ schema: { type: "string" },
669
+ depth: { type: "integer" },
670
+ slim: { type: "boolean" }
671
+ },
672
+ required: ["_id"]
673
+ },
443
674
  fuzzySearch: {
444
675
  type: "object",
445
676
  properties: {
@@ -171,52 +171,7 @@ server.get(JOE.webconfig.joepath+'_www/mcp-schemas.html',auth,function(req,res){
171
171
 
172
172
  // AI Widget test page (Responses + assistants) – auth protected
173
173
  server.get(['/ai-widget-test.html', JOE.webconfig.joepath + 'ai-widget-test.html'], auth, function(req,res){
174
- var assistantId = (req.query.assistant_id || req.query.assistant || '').trim();
175
- if (!assistantId && JOE && JOE.Utils && JOE.Utils.Settings) {
176
- try {
177
- var settingObj = JOE.Utils.Settings('DEFAULT_AI_ASSISTANT', { object: true });
178
- assistantId = (settingObj && settingObj.value) || '';
179
- } catch(e) {
180
- console.log('[ai-widget-test] error reading DEFAULT_AI_ASSISTANT:', e && e.message);
181
- }
182
- }
183
- var joePath = (JOE && JOE.webconfig && JOE.webconfig.joepath) || '/JsonObjectEditor/';
184
- var assistantAttr = assistantId ? ' ai_assistant_id="'+assistantId.replace(/"/g,'&quot;')+'"' : '';
185
-
186
- res.send(`<!doctype html>
187
- <html>
188
- <head>
189
- <meta charset="utf-8">
190
- <title>JOE AI Widget Test</title>
191
- <meta name="viewport" content="width=device-width, initial-scale=1">
192
- <style>
193
- body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px;background:#f3f4f6;}
194
- h1{margin-top:0;}
195
- .small{font-size:13px;color:#6b7280;margin-bottom:16px;}
196
- .container{max-width:480px;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 4px 14px rgba(0,0,0,0.06);padding:16px;}
197
- .meta{font-size:12px;color:#6b7280;margin-bottom:8px;}
198
- code{background:#e5e7eb;border-radius:4px;padding:2px 4px;font-size:12px;}
199
- </style>
200
- </head>
201
- <body>
202
- <div id="mcp-nav"></div>
203
- <script src="${joePath}_www/mcp-nav.js"></script>
204
-
205
- <div class="container">
206
- <h1>AI Widget Test</h1>
207
- <div class="small">
208
- This page mounts <code>&lt;joe-ai-widget&gt;</code> and sends messages through
209
- <code>/API/plugin/chatgpt/widget*</code> using the OpenAI Responses API.
210
- ${assistantId ? `Using <code>DEFAULT_AI_ASSISTANT</code> (ai_assistant_id=${assistantId}).` : 'No DEFAULT_AI_ASSISTANT is set; widget will use model defaults.'}
211
- You can override the assistant via <code>?assistant_id=&lt;ai_assistant _id&gt;</code>.
212
- </div>
213
-
214
- <joe-ai-widget id="widget" title="JOE AI Assistant"${assistantAttr}></joe-ai-widget>
215
- </div>
216
-
217
- <script src="${joePath}js/joe-ai.js"></script>
218
- </body>
219
- </html>`);
174
+ res.sendFile(path.join(JOE.joedir,'_www','ai-widget-test.html'));
220
175
  });
221
176
 
222
177
  server.use(JOE.webconfig.joepath,express.static(JOE.joedir));
@@ -22,50 +22,46 @@ function Auth(){
22
22
  };
23
23
 
24
24
  this.login = async function(data,req,res){
25
- const got = await import('got');
26
- //make post request
25
+ // Use got to exchange the Google authorization_code for tokens.
26
+ // We POST form-encoded params to the current Google OAuth token endpoint.
27
+ const gotMod = await import('got');
28
+ const got = gotMod.default || gotMod;
29
+
27
30
  var originalUrl = data.state||'';
28
31
  console.log(originalUrl);
29
32
 
30
- var options = {
33
+ const tokenUrl = 'https://oauth2.googleapis.com/token';
34
+ const options = {
31
35
  method: 'POST',
32
- url: 'https://www.googleapis.com/oauth2/v4/token',
33
- searchParams: {
36
+ // Google expects application/x-www-form-urlencoded body, not querystring.
37
+ form: {
34
38
  grant_type: 'authorization_code',
35
39
  code: data.code,
36
40
  redirect_uri: `${JOE.webconfig.authorization.host}/API/plugin/auth/login`,
37
- 'Content-Type': 'application/x-www-form-urlencoded',
38
- 'client_id':JOE.webconfig.authorization.client_id,
39
- 'client_secret':JOE.webconfig.authorization.client_secret
41
+ client_id: JOE.webconfig.authorization.client_id,
42
+ client_secret: JOE.webconfig.authorization.client_secret
40
43
  },
41
- headers:
42
- {
44
+ headers: {
43
45
  'cache-control': 'no-cache',
44
- Authorization: JOE.webconfig.authorization.header,
45
- Accept: 'application/json',
46
-
46
+ Accept: 'application/json'
47
47
  },
48
48
  responseType: 'json',
49
49
  https: {
50
50
  rejectUnauthorized: false
51
51
  }
52
- // rejectUnauthorized:false,
53
- // json: true
54
52
  };
55
53
 
56
- // request(options, function (error, response, body) {
57
- got.default(options)
58
- .catch(error => {
59
- res.send(error);
60
- })
54
+ got(tokenUrl, options)
61
55
  .then(response => {
62
- const body = response.body;
56
+ const body = response.body || {};
63
57
  // if (error){
64
58
  // res.send(error);
65
59
  // return;
66
60
  // }
67
61
  if (body.error){
68
- res.send(body.error);
62
+ // Bubble up Google's error payload so it's easier to diagnose
63
+ console.error('[auth.login] Google token error payload:', body);
64
+ res.status(400).send(body);
69
65
  return;
70
66
  }
71
67
  //res.send(body);
@@ -91,17 +87,25 @@ function Auth(){
91
87
  res.redirect(finalUrl);
92
88
  return;
93
89
  }
94
- //redirect to home or gotoUrl
90
+ //redirect to home or gotoUrl
95
91
  res.redirect(originalUrl || `/JOE/${User.apps[0]}`);
96
- //res.send(body);
97
-
98
-
99
-
92
+ })
93
+ .catch(error => {
94
+ // Log structured info so we can see status + body from Google
95
+ const status = error.response && error.response.statusCode;
96
+ const body = error.response && error.response.body;
97
+ console.error('[auth.login] HTTPError from Google token endpoint:', {
98
+ status,
99
+ body,
100
+ message: error.message
101
+ });
102
+ res.status(status || 500).send(body || {
103
+ error: 'oauth_token_error',
104
+ message: error.message
105
+ });
100
106
  });
101
107
 
102
-
103
- //return(data);
104
- return({use_callback:true})
108
+ return({use_callback:true});
105
109
  }
106
110
  this.html = function(data,req,res){
107
111