snow-flow 10.0.1-dev.397 → 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.397",
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)
412
+ for (var j = 0; j < probeCandidates.length; j++) {
414
413
  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)
427
- 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,21 +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
- // 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.
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;
479
470
  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
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;
483
477
  }
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 };
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
488
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
492
+ }
493
+ // Brief pause to let ServiceNow finalize the delete
494
+ await new Promise(resolve => setTimeout(resolve, 1000));
489
495
  }
490
496
 
491
497
  // 4. Deploy the Scripted REST API (do NOT set namespace — let ServiceNow assign it)
@@ -504,7 +510,7 @@ async function ensureFlowFactoryAPI(
504
510
  throw new Error('Failed to create Scripted REST API definition — no sys_id returned');
505
511
  }
506
512
 
507
- // 5. Deploy the POST /create resource
513
+ // 5a. Deploy the POST /create resource
508
514
  try {
509
515
  await client.post('/api/now/table/sys_ws_operation', {
510
516
  web_service_definition: apiSysId,
@@ -540,18 +546,25 @@ async function ensureFlowFactoryAPI(
540
546
  // Non-fatal: create endpoint is more important than discover
541
547
  }
542
548
 
543
- // 6. Wait for ServiceNow REST framework to register the new endpoints
544
- await new Promise(resolve => setTimeout(resolve, 3000));
549
+ // 6. Resolve namespace read directly from the record (deterministic)
550
+ var resolvedNs = await resolveFactoryNamespace(client, apiSysId, instanceUrl);
545
551
 
546
- // 7. Probe to discover the namespace ServiceNow assigned
547
- var resolvedNs = await probeFlowFactoryNamespace(client, apiSysId, instanceUrl);
552
+ // 7. If direct resolution failed, wait for REST framework and retry
548
553
  if (!resolvedNs) {
549
- // Retry once after extra delay — some instances are slow to register
550
554
  await new Promise(resolve => setTimeout(resolve, 3000));
551
- resolvedNs = await probeFlowFactoryNamespace(client, apiSysId, instanceUrl);
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
+ }
552
565
  }
553
566
  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');
567
+ throw new Error('Flow Factory deployed (sys_id=' + apiSysId + ') but namespace could not be resolved. This may indicate a ServiceNow scope/permissions issue.');
555
568
  }
556
569
 
557
570
  _flowFactoryCache = { apiSysId: apiSysId, namespace: resolvedNs, timestamp: Date.now() };