snow-flow 10.0.1-dev.397 → 10.0.1-dev.399

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
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.1-dev.397",
3
+ "version": "10.0.1-dev.399",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -343,39 +343,58 @@ var FLOW_FACTORY_SCRIPT = [
343
343
  ].join('\n');
344
344
 
345
345
  /**
346
- * Probe the ServiceNow instance to discover the correct URL namespace for
347
- * the Flow Factory Scripted REST API. Sends GET requests to candidate
348
- * namespaces a 405 (Method Not Allowed) or 401/403 confirms the namespace
349
- * exists, while 404 means wrong namespace.
346
+ * Resolve the REST API namespace for a sys_ws_definition record.
347
+ *
348
+ * Collects namespace candidates from multiple sources, then VERIFIES each
349
+ * one via HTTP before accepting it. This prevents returning invalid values
350
+ * like scope sys_ids or numeric identifiers that aren't valid URL namespaces.
351
+ *
352
+ * Verification: GET /api/{candidate}/{api_id}/discover
353
+ * - 200 = correct namespace, /discover endpoint works
354
+ * - 401/403 = correct namespace, auth issue
355
+ * - 405 = correct namespace, wrong HTTP method (endpoint exists)
356
+ * - 400/404 = wrong namespace or API not registered yet
350
357
  */
351
- async function probeFlowFactoryNamespace(
358
+ async function resolveFactoryNamespace(
352
359
  client: any,
353
360
  apiSysId: string,
354
361
  instanceUrl: string
355
362
  ): Promise<string | null> {
356
- // Build candidate list from multiple sources
363
+ // ── Collect namespace candidates (ordered by likelihood) ──
357
364
  var candidates: string[] = [];
358
365
 
359
- // 1. Read back the record's namespace field
366
+ // Most common for PDI / global scope custom APIs
367
+ candidates.push('global');
368
+
369
+ // Dot-walk to sys_scope.scope
370
+ try {
371
+ var dotWalkResp = await client.get('/api/now/table/sys_ws_definition/' + apiSysId, {
372
+ params: {
373
+ sysparm_fields: 'namespace.scope',
374
+ sysparm_display_value: 'false'
375
+ }
376
+ });
377
+ var scopeStr = dotWalkResp.data.result?.['namespace.scope'];
378
+ if (scopeStr && typeof scopeStr === 'string' && scopeStr.length > 1) {
379
+ candidates.push(scopeStr);
380
+ }
381
+ } catch (_) {}
382
+
383
+ // Read namespace display_value (might be scope string itself)
360
384
  try {
361
385
  var nsResp = await client.get('/api/now/table/sys_ws_definition/' + apiSysId, {
362
- params: { sysparm_fields: 'sys_id,api_id,namespace', sysparm_display_value: 'all' }
386
+ params: { sysparm_fields: 'namespace', sysparm_display_value: 'true' }
363
387
  });
364
- var record = nsResp.data.result || {};
365
- var ns = record.namespace;
366
- if (ns) {
367
- if (typeof ns === 'object') {
368
- var dv = ns.display_value || '';
369
- var val = ns.value || '';
370
- if (dv && /^(x_|sn_)/.test(dv)) candidates.push(dv);
371
- if (val && /^(x_|sn_)/.test(val)) candidates.push(val);
372
- } else if (typeof ns === 'string' && ns.length > 0 && ns !== 'Global' && ns !== 'global') {
373
- candidates.push(ns);
374
- }
388
+ var nsDisplay = nsResp.data.result?.namespace;
389
+ if (typeof nsDisplay === 'string' && nsDisplay.length > 1) {
390
+ candidates.push(nsDisplay === 'Global' ? 'global' : nsDisplay);
375
391
  }
376
392
  } catch (_) {}
377
393
 
378
- // 2. Company code from sys_properties
394
+ // OOB namespace
395
+ candidates.push('now');
396
+
397
+ // Company code from sys_properties
379
398
  try {
380
399
  var compResp = await client.get('/api/now/table/sys_properties', {
381
400
  params: {
@@ -388,61 +407,76 @@ async function probeFlowFactoryNamespace(
388
407
  if (companyCode) candidates.push(companyCode);
389
408
  } catch (_) {}
390
409
 
391
- // 3. Instance subdomain (e.g. "dev351277" from "https://dev351277.service-now.com")
410
+ // Instance subdomain (e.g. "dev354059")
392
411
  try {
393
412
  var match = instanceUrl.match(/https?:\/\/([^.]+)\./);
394
413
  if (match && match[1]) candidates.push(match[1]);
395
414
  } catch (_) {}
396
415
 
397
- // 4. Fixed fallbacks
398
- candidates.push('now', 'global');
399
-
400
- // Deduplicate
416
+ // ── Deduplicate ──
401
417
  var seen: Record<string, boolean> = {};
402
418
  var unique: string[] = [];
403
419
  for (var i = 0; i < candidates.length; i++) {
404
- if (!seen[candidates[i]]) {
405
- seen[candidates[i]] = true;
406
- unique.push(candidates[i]);
420
+ var lower = candidates[i].toLowerCase();
421
+ if (!seen[lower]) {
422
+ seen[lower] = true;
423
+ unique.push(lower);
407
424
  }
408
425
  }
409
426
 
410
- // 5. Probe each candidate GET the /discover endpoint (a real GET handler)
411
- // If /discover doesn't exist yet, fall back to /create (expects 405)
427
+ // ── Verify each candidate via HTTP ──
412
428
  for (var j = 0; j < unique.length; j++) {
413
- // Try /discover first (GET endpoint, returns 200 when namespace is correct)
429
+ var ns = unique[j];
430
+ // Check 1: GET /discover (v5 endpoint, returns 200 with discovery data)
414
431
  try {
415
- var discoverResp = await client.get('/api/' + unique[j] + '/' + FLOW_FACTORY_API_ID + '/discover');
416
- if (discoverResp.status === 200 || discoverResp.data) {
417
- return unique[j]; // Namespace confirmed via /discover
418
- }
432
+ var discoverResp = await client.get('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/discover');
433
+ if (discoverResp.status === 200 || discoverResp.data) return ns;
419
434
  } catch (discoverErr: any) {
420
435
  var ds = discoverErr.response?.status;
421
- if (ds === 401 || ds === 403) {
422
- return unique[j]; // Namespace correct but auth issue
423
- }
424
- // 404 = wrong namespace OR /discover not deployed yet, try /create
436
+ if (ds === 401 || ds === 403 || ds === 405) return ns;
437
+ // 400/404 = wrong namespace or not registered yet
425
438
  }
426
- // Fallback: try /create (POST-only, expect 405 for correct namespace)
439
+ // Check 2: GET /create (POST-only, expect 405 for correct namespace)
427
440
  try {
428
- await client.get('/api/' + unique[j] + '/' + FLOW_FACTORY_API_ID + '/create');
429
- return unique[j]; // 200 = unexpected but valid
441
+ await client.get('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/create');
442
+ return ns; // 200 = unexpected but valid
430
443
  } catch (createErr: any) {
431
444
  var cs = createErr.response?.status;
432
- if (cs === 405 || cs === 401 || cs === 403) {
433
- return unique[j]; // Namespace correct method or auth rejected
434
- }
435
- // 404 = wrong namespace, try next candidate
445
+ if (cs === 405 || cs === 401 || cs === 403) return ns;
446
+ // 400/404 = wrong namespace (ServiceNow may return 400 instead of 405)
447
+ }
448
+ // Check 3: POST /create with empty body — distinguishes "wrong namespace" from
449
+ // "correct namespace but script validation error". Wrong namespace returns
450
+ // 400 with "Requested URI does not represent any resource". Our script returns
451
+ // a different error body (e.g. {success:false, error:"..."}).
452
+ try {
453
+ await client.post('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/create', {});
454
+ return ns; // Unexpected success, namespace confirmed
455
+ } catch (postErr: any) {
456
+ var pe = postErr.response;
457
+ if (!pe) continue;
458
+ if (pe.status === 401 || pe.status === 403) return ns;
459
+ // Check error body — "Requested URI" = wrong namespace, anything else = our script
460
+ var errStr = JSON.stringify(pe.data || '');
461
+ if (!errStr.includes('Requested URI')) return ns;
436
462
  }
437
463
  }
438
464
 
439
- return null; // No namespace matchedfactory unreachable
465
+ return null; // No candidate verifiedAPI may not be registered yet
440
466
  }
441
467
 
442
468
  /**
443
469
  * Ensure the Flow Factory Scripted REST API exists on the ServiceNow instance.
444
470
  * Idempotent — checks cache first, then instance, deploys only if missing.
445
- * Uses namespace probing (HTTP GET) instead of record field parsing.
471
+ *
472
+ * Namespace resolution strategy:
473
+ * 1. Dot-walk to sys_scope.scope from the API record (deterministic, no HTTP probing)
474
+ * 2. Read namespace field with display_value=all (fallback)
475
+ * 3. Company code from sys_properties (fallback)
476
+ * 4. HTTP probing to /discover and /create endpoints (last resort)
477
+ *
478
+ * Stale API detection: if the API exists but has no /discover endpoint
479
+ * (created by an older tool version), it is deleted and redeployed.
446
480
  */
447
481
  async function ensureFlowFactoryAPI(
448
482
  client: any,
@@ -460,7 +494,7 @@ async function ensureFlowFactoryAPI(
460
494
 
461
495
  _bootstrapPromise = (async () => {
462
496
  try {
463
- // 3. Check if API already exists on instance (query by api_id, more reliable than name)
497
+ // 3. Check if API already exists on instance
464
498
  var checkResp = await client.get('/api/now/table/sys_ws_definition', {
465
499
  params: {
466
500
  sysparm_query: 'api_id=' + FLOW_FACTORY_API_ID,
@@ -471,21 +505,36 @@ async function ensureFlowFactoryAPI(
471
505
 
472
506
  if (checkResp.data.result && checkResp.data.result.length > 0) {
473
507
  var existing = checkResp.data.result[0];
474
- var ns = await probeFlowFactoryNamespace(client, existing.sys_id, instanceUrl);
475
- if (!ns) {
476
- // Namespace can't be resolved — the API is stale (e.g. created by an older version
477
- // without the /discover endpoint, or ServiceNow REST framework hasn't registered it).
478
- // Delete and redeploy with current v5 scripts.
508
+ var ns = await resolveFactoryNamespace(client, existing.sys_id, instanceUrl);
509
+
510
+ if (ns) {
511
+ // Verify the API has v5 endpoints (check /discover exists)
512
+ var hasDiscover = false;
479
513
  try {
480
- await client.delete('/api/now/table/sys_ws_definition/' + existing.sys_id);
481
- } catch (_) {
482
- // If delete fails, try to continue anyway — deployment step will error if API ID conflicts
514
+ var verifyResp = await client.get('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/discover');
515
+ hasDiscover = verifyResp.status === 200 || !!verifyResp.data;
516
+ } catch (verifyErr: any) {
517
+ var vs = verifyErr.response?.status;
518
+ // 401/403 = endpoint exists but auth issue; 405 = exists but wrong method
519
+ hasDiscover = vs === 401 || vs === 403 || vs === 405;
483
520
  }
484
- // Fall through to step 4 (deploy fresh)
485
- } else {
486
- _flowFactoryCache = { apiSysId: existing.sys_id, namespace: ns, timestamp: Date.now() };
487
- return { namespace: ns, apiSysId: existing.sys_id };
521
+
522
+ if (hasDiscover) {
523
+ _flowFactoryCache = { apiSysId: existing.sys_id, namespace: ns, timestamp: Date.now() };
524
+ return { namespace: ns, apiSysId: existing.sys_id };
525
+ }
526
+ // /discover missing → stale API from older version, fall through to delete
488
527
  }
528
+
529
+ // Delete stale API and redeploy with current scripts
530
+ invalidateFlowFactoryCache();
531
+ try {
532
+ await client.delete('/api/now/table/sys_ws_definition/' + existing.sys_id);
533
+ } catch (_) {
534
+ // If delete fails, try deployment anyway — will error on duplicate api_id
535
+ }
536
+ // Brief pause to let ServiceNow finalize the delete
537
+ await new Promise(resolve => setTimeout(resolve, 1000));
489
538
  }
490
539
 
491
540
  // 4. Deploy the Scripted REST API (do NOT set namespace — let ServiceNow assign it)
@@ -504,7 +553,7 @@ async function ensureFlowFactoryAPI(
504
553
  throw new Error('Failed to create Scripted REST API definition — no sys_id returned');
505
554
  }
506
555
 
507
- // 5. Deploy the POST /create resource
556
+ // 5a. Deploy the POST /create resource
508
557
  try {
509
558
  await client.post('/api/now/table/sys_ws_operation', {
510
559
  web_service_definition: apiSysId,
@@ -540,18 +589,19 @@ async function ensureFlowFactoryAPI(
540
589
  // Non-fatal: create endpoint is more important than discover
541
590
  }
542
591
 
543
- // 6. Wait for ServiceNow REST framework to register the new endpoints
544
- await new Promise(resolve => setTimeout(resolve, 3000));
592
+ // 6. Wait for ServiceNow REST framework to register endpoints
593
+ await new Promise(resolve => setTimeout(resolve, 4000));
594
+
595
+ // 7. Resolve namespace via HTTP-verified probing
596
+ var resolvedNs = await resolveFactoryNamespace(client, apiSysId, instanceUrl);
545
597
 
546
- // 7. Probe to discover the namespace ServiceNow assigned
547
- var resolvedNs = await probeFlowFactoryNamespace(client, apiSysId, instanceUrl);
598
+ // 8. If resolution failed, wait longer and retry (some instances are slow)
548
599
  if (!resolvedNs) {
549
- // Retry once after extra delay — some instances are slow to register
550
- await new Promise(resolve => setTimeout(resolve, 3000));
551
- resolvedNs = await probeFlowFactoryNamespace(client, apiSysId, instanceUrl);
600
+ await new Promise(resolve => setTimeout(resolve, 5000));
601
+ resolvedNs = await resolveFactoryNamespace(client, apiSysId, instanceUrl);
552
602
  }
553
603
  if (!resolvedNs) {
554
- throw new Error('Flow Factory API created (sys_id=' + apiSysId + ') but namespace could not be resolved via HTTP probing after 6s delay');
604
+ throw new Error('Flow Factory deployed (sys_id=' + apiSysId + ') but no namespace candidate could be verified via HTTP after 9s. Candidates tried: global, dot-walk scope, display_value, now, company code, subdomain.');
555
605
  }
556
606
 
557
607
  _flowFactoryCache = { apiSysId: apiSysId, namespace: resolvedNs, timestamp: Date.now() };