snow-flow 10.0.1-dev.396 → 10.0.1-dev.398

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.396",
3
+ "version": "10.0.1-dev.398",
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,53 @@ 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
+ * Uses dot-walking to read the scope string from the related sys_scope record,
348
+ * which is deterministic and doesn't depend on endpoint registration timing.
349
+ * Falls back to HTTP probing if dot-walking fails.
350
350
  */
351
- async function probeFlowFactoryNamespace(
351
+ async function resolveFactoryNamespace(
352
352
  client: any,
353
353
  apiSysId: string,
354
354
  instanceUrl: string
355
355
  ): Promise<string | null> {
356
- // Build candidate list from multiple sources
357
- var candidates: string[] = [];
356
+ // ── Strategy 1: Dot-walk to sys_scope.scope (deterministic) ──
357
+ try {
358
+ var dotWalkResp = await client.get('/api/now/table/sys_ws_definition/' + apiSysId, {
359
+ params: {
360
+ sysparm_fields: 'namespace,namespace.scope',
361
+ sysparm_display_value: 'false'
362
+ }
363
+ });
364
+ var scopeStr = dotWalkResp.data.result?.['namespace.scope'];
365
+ if (scopeStr && typeof scopeStr === 'string' && scopeStr.length > 0) {
366
+ return scopeStr;
367
+ }
368
+ } catch (_) {}
358
369
 
359
- // 1. Read back the record's namespace field
370
+ // ── Strategy 2: Read namespace field with display_value=all ──
360
371
  try {
361
372
  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' }
373
+ params: { sysparm_fields: 'namespace', sysparm_display_value: 'all' }
363
374
  });
364
- var record = nsResp.data.result || {};
365
- var ns = record.namespace;
375
+ var ns = nsResp.data.result?.namespace;
366
376
  if (ns) {
377
+ // If it's a reference object, extract useful values
367
378
  if (typeof ns === 'object') {
368
379
  var dv = ns.display_value || '';
369
380
  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);
381
+ // display_value might be the scope string itself (e.g. "global", "x_snflw")
382
+ if (dv && dv !== 'Global') return dv;
383
+ // If display_value is "Global", the scope string is typically "global"
384
+ if (dv === 'Global') return 'global';
385
+ if (val && val.length > 0) return val;
386
+ } else if (typeof ns === 'string' && ns.length > 0) {
387
+ return ns === 'Global' ? 'global' : ns;
374
388
  }
375
389
  }
376
390
  } catch (_) {}
377
391
 
378
- // 2. Company code from sys_properties
392
+ // ── Strategy 3: Company code from sys_properties ──
379
393
  try {
380
394
  var compResp = await client.get('/api/now/table/sys_properties', {
381
395
  params: {
@@ -385,64 +399,41 @@ async function probeFlowFactoryNamespace(
385
399
  }
386
400
  });
387
401
  var companyCode = compResp.data.result?.[0]?.value;
388
- if (companyCode) candidates.push(companyCode);
402
+ if (companyCode) return companyCode;
389
403
  } catch (_) {}
390
404
 
391
- // 3. Instance subdomain (e.g. "dev351277" from "https://dev351277.service-now.com")
405
+ // ── Strategy 4: HTTP probing as last resort ──
406
+ var probeCandidates = ['global', 'now'];
392
407
  try {
393
408
  var match = instanceUrl.match(/https?:\/\/([^.]+)\./);
394
- if (match && match[1]) candidates.push(match[1]);
409
+ if (match && match[1]) probeCandidates.push(match[1]);
395
410
  } catch (_) {}
396
411
 
397
- // 4. Fixed fallbacks
398
- candidates.push('now', 'global');
399
-
400
- // Deduplicate
401
- var seen: Record<string, boolean> = {};
402
- var unique: string[] = [];
403
- for (var i = 0; i < candidates.length; i++) {
404
- if (!seen[candidates[i]]) {
405
- seen[candidates[i]] = true;
406
- unique.push(candidates[i]);
407
- }
408
- }
409
-
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)
412
- for (var j = 0; j < unique.length; j++) {
413
- // Try /discover first (GET endpoint, returns 200 when namespace is correct)
414
- 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
- }
419
- } catch (discoverErr: any) {
420
- 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
425
- }
426
- // Fallback: try /create (POST-only, expect 405 for correct namespace)
412
+ for (var j = 0; j < probeCandidates.length; j++) {
427
413
  try {
428
- await client.get('/api/' + unique[j] + '/' + FLOW_FACTORY_API_ID + '/create');
429
- return unique[j]; // 200 = unexpected but valid
430
- } catch (createErr: any) {
431
- 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
414
+ var probeResp = await client.get('/api/' + probeCandidates[j] + '/' + FLOW_FACTORY_API_ID + '/discover');
415
+ if (probeResp.status === 200 || probeResp.data) return probeCandidates[j];
416
+ } catch (probeErr: any) {
417
+ var ps = probeErr.response?.status;
418
+ if (ps === 401 || ps === 403 || ps === 405) return probeCandidates[j];
436
419
  }
437
420
  }
438
421
 
439
- return null; // No namespace matched — factory unreachable
422
+ return null;
440
423
  }
441
424
 
442
425
  /**
443
426
  * Ensure the Flow Factory Scripted REST API exists on the ServiceNow instance.
444
427
  * Idempotent — checks cache first, then instance, deploys only if missing.
445
- * Uses namespace probing (HTTP GET) instead of record field parsing.
428
+ *
429
+ * Namespace resolution strategy:
430
+ * 1. Dot-walk to sys_scope.scope from the API record (deterministic, no HTTP probing)
431
+ * 2. Read namespace field with display_value=all (fallback)
432
+ * 3. Company code from sys_properties (fallback)
433
+ * 4. HTTP probing to /discover and /create endpoints (last resort)
434
+ *
435
+ * Stale API detection: if the API exists but has no /discover endpoint
436
+ * (created by an older tool version), it is deleted and redeployed.
446
437
  */
447
438
  async function ensureFlowFactoryAPI(
448
439
  client: any,
@@ -460,7 +451,7 @@ async function ensureFlowFactoryAPI(
460
451
 
461
452
  _bootstrapPromise = (async () => {
462
453
  try {
463
- // 3. Check if API already exists on instance (query by api_id, more reliable than name)
454
+ // 3. Check if API already exists on instance
464
455
  var checkResp = await client.get('/api/now/table/sys_ws_definition', {
465
456
  params: {
466
457
  sysparm_query: 'api_id=' + FLOW_FACTORY_API_ID,
@@ -471,12 +462,36 @@ async function ensureFlowFactoryAPI(
471
462
 
472
463
  if (checkResp.data.result && checkResp.data.result.length > 0) {
473
464
  var existing = checkResp.data.result[0];
474
- var ns = await probeFlowFactoryNamespace(client, existing.sys_id, instanceUrl);
475
- if (!ns) {
476
- throw new Error('Flow Factory API exists (sys_id=' + existing.sys_id + ') but namespace could not be resolved via HTTP probing');
465
+ var ns = await resolveFactoryNamespace(client, existing.sys_id, instanceUrl);
466
+
467
+ if (ns) {
468
+ // Verify the API has v5 endpoints (check /discover exists)
469
+ var hasDiscover = false;
470
+ try {
471
+ var verifyResp = await client.get('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/discover');
472
+ hasDiscover = verifyResp.status === 200 || !!verifyResp.data;
473
+ } catch (verifyErr: any) {
474
+ var vs = verifyErr.response?.status;
475
+ // 401/403 = endpoint exists but auth issue; 405 = exists but wrong method
476
+ hasDiscover = vs === 401 || vs === 403 || vs === 405;
477
+ }
478
+
479
+ if (hasDiscover) {
480
+ _flowFactoryCache = { apiSysId: existing.sys_id, namespace: ns, timestamp: Date.now() };
481
+ return { namespace: ns, apiSysId: existing.sys_id };
482
+ }
483
+ // /discover missing → stale API from older version, fall through to delete
484
+ }
485
+
486
+ // Delete stale API and redeploy with current scripts
487
+ invalidateFlowFactoryCache();
488
+ try {
489
+ await client.delete('/api/now/table/sys_ws_definition/' + existing.sys_id);
490
+ } catch (_) {
491
+ // If delete fails, try deployment anyway — will error on duplicate api_id
477
492
  }
478
- _flowFactoryCache = { apiSysId: existing.sys_id, namespace: ns, timestamp: Date.now() };
479
- return { namespace: ns, apiSysId: existing.sys_id };
493
+ // Brief pause to let ServiceNow finalize the delete
494
+ await new Promise(resolve => setTimeout(resolve, 1000));
480
495
  }
481
496
 
482
497
  // 4. Deploy the Scripted REST API (do NOT set namespace — let ServiceNow assign it)
@@ -495,7 +510,7 @@ async function ensureFlowFactoryAPI(
495
510
  throw new Error('Failed to create Scripted REST API definition — no sys_id returned');
496
511
  }
497
512
 
498
- // 5. Deploy the POST /create resource
513
+ // 5a. Deploy the POST /create resource
499
514
  try {
500
515
  await client.post('/api/now/table/sys_ws_operation', {
501
516
  web_service_definition: apiSysId,
@@ -531,10 +546,25 @@ async function ensureFlowFactoryAPI(
531
546
  // Non-fatal: create endpoint is more important than discover
532
547
  }
533
548
 
534
- // 6. Probe to discover the namespace ServiceNow assigned
535
- var resolvedNs = await probeFlowFactoryNamespace(client, apiSysId, instanceUrl);
549
+ // 6. Resolve namespace read directly from the record (deterministic)
550
+ var resolvedNs = await resolveFactoryNamespace(client, apiSysId, instanceUrl);
551
+
552
+ // 7. If direct resolution failed, wait for REST framework and retry
553
+ if (!resolvedNs) {
554
+ await new Promise(resolve => setTimeout(resolve, 3000));
555
+ resolvedNs = await resolveFactoryNamespace(client, apiSysId, instanceUrl);
556
+ }
557
+ if (!resolvedNs) {
558
+ // Last resort: try 'global' (most common for PDI instances)
559
+ try {
560
+ var globalTest = await client.get('/api/global/' + FLOW_FACTORY_API_ID + '/discover');
561
+ if (globalTest.status === 200 || globalTest.data) resolvedNs = 'global';
562
+ } catch (gtErr: any) {
563
+ if (gtErr.response?.status === 401 || gtErr.response?.status === 403) resolvedNs = 'global';
564
+ }
565
+ }
536
566
  if (!resolvedNs) {
537
- throw new Error('Flow Factory API created (sys_id=' + apiSysId + ') but namespace could not be resolved via HTTP probing');
567
+ throw new Error('Flow Factory deployed (sys_id=' + apiSysId + ') but namespace could not be resolved. This may indicate a ServiceNow scope/permissions issue.');
538
568
  }
539
569
 
540
570
  _flowFactoryCache = { apiSysId: apiSysId, namespace: resolvedNs, timestamp: Date.now() };