@indigoai-us/hq-cli 5.345.4 → 5.345.6
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/CHANGELOG.md +4 -0
- package/dist/commands/daemon.js +91 -7
- package/dist/commands/login.js +2 -2
- package/dist/commands/mesh.js +17 -17
- package/dist/lib/daemon/hq-cloud.d.ts +3 -0
- package/dist/lib/daemon/hq-cloud.js +7 -3
- package/dist/lib/daemon/sync-runner-bundle.d.ts +4 -0
- package/dist/lib/daemon/sync-runner-bundle.js +10 -1
- package/dist/lib/daemon/sync-runner.d.ts +2 -0
- package/dist/lib/daemon/sync-runner.js +8 -0
- package/dist/lib/work-context/config.d.ts +13 -12
- package/dist/lib/work-context/config.js +18 -18
- package/dist/lib/work-context/person-identity.d.ts +11 -7
- package/dist/lib/work-context/person-identity.js +24 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.6] — 2026-10-05
|
|
6
|
+
- `hq mesh context default set <slug> --count-usage` now works for people signed in with a normal HQ account. It previously refused with "the signed-in person could not be determined". An opt-in saved by 5.344.0 is ignored; run the command again to opt in.
|
|
7
|
+
- `hq daemon sync status` now identifies the executing runner and derives its hq-cloud version from the owner status or runner package when needed.
|
|
8
|
+
|
|
5
9
|
## [5.345.4] — 2026-10-05
|
|
6
10
|
- Run a pinned or newer managed hq-cloud sync runner ahead of the staged sync bundle, so daemon sync no longer stays on the hq-cloud version bundled with the CLI after an update or pin (hq-cli#1547).
|
|
7
11
|
- Avoid dispatching the global hook when the working directory is inside an HQ root.
|
package/dist/commands/daemon.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
-
import { major as semverMajor, valid as validSemver } from "semver";
|
|
19
|
+
import { compare as semverCompare, major as semverMajor, valid as validSemver } from "semver";
|
|
20
20
|
import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
|
|
21
21
|
import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
22
22
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
@@ -24,13 +24,14 @@ import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/d
|
|
|
24
24
|
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
25
25
|
import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
|
|
26
26
|
import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
27
|
-
import { bundledHqCloud } from "../lib/daemon/hq-cloud.js";
|
|
27
|
+
import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy } from "../lib/daemon/hq-cloud.js";
|
|
28
28
|
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
29
29
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
30
30
|
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
31
31
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
32
32
|
import { runDaemonCall } from "../lib/daemon/command-call.js";
|
|
33
|
-
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
33
|
+
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, syncRunnerPathFromCommandLine, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
34
|
+
import { stagedSyncRunnerVersion, syncRunnerBundleStageRoot } from "../lib/daemon/sync-runner-bundle.js";
|
|
34
35
|
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
35
36
|
import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
|
|
36
37
|
import { hqdRequest, hqdSocketPath, HqdClientError } from "../lib/daemon/protocol.js";
|
|
@@ -440,6 +441,38 @@ function daemonSyncModeClient(d) {
|
|
|
440
441
|
return new VaultClient(buildVaultConfig(token));
|
|
441
442
|
})();
|
|
442
443
|
}
|
|
444
|
+
function pathIsWithin(candidate, root, platform) {
|
|
445
|
+
const pathApi = platform === "win32" ? path.win32 : path;
|
|
446
|
+
const relative = pathApi.relative(pathApi.resolve(root), pathApi.resolve(candidate));
|
|
447
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(".." + pathApi.sep) && !pathApi.isAbsolute(relative));
|
|
448
|
+
}
|
|
449
|
+
function syncRunnerSource(runnerPath, daemonRoot, platform) {
|
|
450
|
+
if (!runnerPath)
|
|
451
|
+
return "unknown";
|
|
452
|
+
if (pathIsWithin(runnerPath, syncRunnerBundleStageRoot(daemonRoot), platform))
|
|
453
|
+
return "staged-sync";
|
|
454
|
+
if (pathIsWithin(runnerPath, managedHqCloudRoot(daemonRoot), platform))
|
|
455
|
+
return "managed-full-package";
|
|
456
|
+
const bundled = bundledHqCloud();
|
|
457
|
+
if (bundled && pathIsWithin(runnerPath, bundled.root, platform))
|
|
458
|
+
return "bundled-full-package";
|
|
459
|
+
return "unknown";
|
|
460
|
+
}
|
|
461
|
+
function syncVersionMismatch(status) {
|
|
462
|
+
const running = status.hqCloudVersion;
|
|
463
|
+
if (!running)
|
|
464
|
+
return null;
|
|
465
|
+
if (status.pinnedHqCloudVersion && running !== status.pinnedHqCloudVersion) {
|
|
466
|
+
return "Sync runs hq-cloud " + running + ", while the configured pin expects " + status.pinnedHqCloudVersion + ". Run hq daemon restart sync after updating hq-cli to apply it.";
|
|
467
|
+
}
|
|
468
|
+
if (status.managedHqCloudVersion
|
|
469
|
+
&& validSemver(status.managedHqCloudVersion)
|
|
470
|
+
&& validSemver(running)
|
|
471
|
+
&& semverCompare(status.managedHqCloudVersion, running) > 0) {
|
|
472
|
+
return "Sync runs hq-cloud " + running + ", while managed hq-cloud " + status.managedHqCloudVersion + " is newer. Run hq daemon restart sync after updating hq-cli to apply it.";
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
443
476
|
function daemonSyncStatus(d) {
|
|
444
477
|
const host = currentHost(d);
|
|
445
478
|
const { paths } = host;
|
|
@@ -461,22 +494,64 @@ function daemonSyncStatus(d) {
|
|
|
461
494
|
const ownerCommandLine = ownerStatusCandidate
|
|
462
495
|
? processCommandLine(ownerStatusCandidate.pid, { platform: d.platform, run: d.run })
|
|
463
496
|
: null;
|
|
464
|
-
const
|
|
465
|
-
?
|
|
497
|
+
const ownerRunnerPath = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
|
|
498
|
+
? syncRunnerPathFromCommandLine(ownerCommandLine)
|
|
466
499
|
: null;
|
|
467
|
-
|
|
500
|
+
const ownerStatus = ownerRunnerPath ? ownerStatusCandidate : null;
|
|
501
|
+
const unitCommandLine = ownerRunnerPath || !syncUnit?.pid
|
|
502
|
+
? null
|
|
503
|
+
: processCommandLine(syncUnit.pid, { platform: d.platform, run: d.run });
|
|
504
|
+
const unitRunnerPath = unitCommandLine && isSyncRunnerCommandLine(unitCommandLine)
|
|
505
|
+
? syncRunnerPathFromCommandLine(unitCommandLine)
|
|
506
|
+
: null;
|
|
507
|
+
const runnerPath = ownerRunnerPath ?? unitRunnerPath;
|
|
508
|
+
const runnerSource = syncRunnerSource(runnerPath, paths.root, d.platform);
|
|
509
|
+
const managedCopy = managedHqCloud(paths.root);
|
|
510
|
+
const managedVersion = managedCopy?.version ?? null;
|
|
511
|
+
let hqCloudVersion = ownerStatus?.version ?? null;
|
|
512
|
+
let hqCloudVersionSource = hqCloudVersion ? "owner-status" : "unknown";
|
|
513
|
+
if (!hqCloudVersion && runnerPath) {
|
|
514
|
+
try {
|
|
515
|
+
const copy = runnerSource === "staged-sync"
|
|
516
|
+
? { root: "", version: stagedSyncRunnerVersion(runnerPath) }
|
|
517
|
+
: runnerSource === "managed-full-package"
|
|
518
|
+
? readHqCloudCopy(managedHqCloudRoot(paths.root))
|
|
519
|
+
: runnerSource === "bundled-full-package"
|
|
520
|
+
? bundledHqCloud()
|
|
521
|
+
: null;
|
|
522
|
+
if (copy) {
|
|
523
|
+
hqCloudVersion = copy.version;
|
|
524
|
+
hqCloudVersionSource = "runner-path";
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
d.err("Could not read sync runner version metadata; treating it as unavailable.");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Compare the executing version, not the installed managed copy: the copy can be updated on disk while an older runner still runs.
|
|
532
|
+
const runnerMatchesPin = runnerSource === "managed-full-package" && config.hqCloudVersion && hqCloudVersion
|
|
533
|
+
? hqCloudVersion === config.hqCloudVersion
|
|
534
|
+
: null;
|
|
535
|
+
const status = {
|
|
468
536
|
running: syncUnit?.status === "running",
|
|
469
537
|
paused: config.syncPaused === true,
|
|
470
538
|
syncOwner: effectiveSyncOwner(config, host.detected.type),
|
|
471
539
|
owner: ownerStatus?.owner ?? (syncUnit?.status === "running" ? "hq-daemon" : null),
|
|
472
540
|
runnerPid: ownerStatus?.pid ?? syncUnit?.pid ?? null,
|
|
473
|
-
hqCloudVersion
|
|
541
|
+
hqCloudVersion,
|
|
542
|
+
hqCloudVersionSource,
|
|
543
|
+
runnerMatchesPin,
|
|
544
|
+
runnerPath,
|
|
545
|
+
runnerSource,
|
|
546
|
+
managedHqCloudVersion: managedVersion,
|
|
547
|
+
pinnedHqCloudVersion: config.hqCloudVersion ?? null,
|
|
474
548
|
lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
|
|
475
549
|
lastPassResult: syncUnit?.lastPassResult ?? null,
|
|
476
550
|
unitStatus: syncUnit?.status ?? "not-running",
|
|
477
551
|
reason: syncUnit?.reason ?? null,
|
|
478
552
|
logPath: paths.unitLogPath("sync"),
|
|
479
553
|
};
|
|
554
|
+
return status;
|
|
480
555
|
}
|
|
481
556
|
async function runDaemonSyncStatus(d, opts) {
|
|
482
557
|
const status = daemonSyncStatus(d);
|
|
@@ -487,6 +562,15 @@ async function runDaemonSyncStatus(d, opts) {
|
|
|
487
562
|
d.out(`Sync ${status.running ? "running" : status.unitStatus}${status.paused ? " (paused)" : ""}.`);
|
|
488
563
|
d.out(`Configured sync owner: ${status.syncOwner}.`);
|
|
489
564
|
d.out(`Owner: ${status.owner ?? "none"}; runner pid: ${status.runnerPid ?? "none"}; hq-cloud: ${status.hqCloudVersion ?? "unknown"}.`);
|
|
565
|
+
d.out(`Sync runner: ${status.runnerPath ?? "unknown"} (${status.runnerSource}); hq-cloud ${status.hqCloudVersion ?? "unknown"}.`);
|
|
566
|
+
d.out("Managed hq-cloud: " + (status.managedHqCloudVersion ?? "unknown") + "; pinned hq-cloud: " + (status.pinnedHqCloudVersion ?? "none") + ".");
|
|
567
|
+
if (status.runnerSource === "managed-full-package") {
|
|
568
|
+
const pinMatch = status.runnerMatchesPin === null ? "unknown" : status.runnerMatchesPin ? "yes" : "no";
|
|
569
|
+
d.out("Managed runner matches pin: " + pinMatch + ".");
|
|
570
|
+
}
|
|
571
|
+
const versionMismatch = syncVersionMismatch(status);
|
|
572
|
+
if (versionMismatch)
|
|
573
|
+
d.out(versionMismatch);
|
|
490
574
|
if (status.reason)
|
|
491
575
|
d.out(`Reason: ${status.reason}`);
|
|
492
576
|
d.out(`Log: ${status.logPath}`);
|
package/dist/commands/login.js
CHANGED
|
@@ -9,7 +9,7 @@ import { cognitoConfigForLoginProvider } from '../utils/login-provider.js';
|
|
|
9
9
|
import * as readline from "node:readline/promises";
|
|
10
10
|
import { listActiveMembershipCompanies } from "../lib/mesh/api.js";
|
|
11
11
|
import { retainUsageAttributionForPerson, setDefaultCompany } from "../lib/work-context/config.js";
|
|
12
|
-
import {
|
|
12
|
+
import { cognitoSubFromTokens } from "../lib/work-context/person-identity.js";
|
|
13
13
|
import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
|
|
14
14
|
import { workContextRoot } from "../lib/work-context/paths.js";
|
|
15
15
|
function peekIdToken(idToken) {
|
|
@@ -66,7 +66,7 @@ export function registerLoginCommand(program) {
|
|
|
66
66
|
try {
|
|
67
67
|
// The usage-attribution opt-in belongs to the person who gave it. A
|
|
68
68
|
// different (or unknown) account signing in on this OS user drops it.
|
|
69
|
-
retainUsageAttributionForPerson(
|
|
69
|
+
retainUsageAttributionForPerson(cognitoSubFromTokens(loadCachedTokens()), {
|
|
70
70
|
root: workContextRoot(),
|
|
71
71
|
});
|
|
72
72
|
}
|
package/dist/commands/mesh.js
CHANGED
|
@@ -20,7 +20,7 @@ import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.j
|
|
|
20
20
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
21
21
|
import { clearDefaultCompany, effectiveUsageAttribution, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, setUsageAttribution, } from "../lib/work-context/config.js";
|
|
22
22
|
import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
|
|
23
|
-
import {
|
|
23
|
+
import { cognitoSubFromTokens } from "../lib/work-context/person-identity.js";
|
|
24
24
|
import { DefaultCompanyUnavailableError, WorkContextError, } from "../lib/work-context/errors.js";
|
|
25
25
|
import { isValidSessionId } from "../lib/mesh/live/session-identity.js";
|
|
26
26
|
import { flushSessionEvents } from "../lib/mesh/live/flush.js";
|
|
@@ -1212,17 +1212,17 @@ export async function askYesNo(question, io = {}) {
|
|
|
1212
1212
|
rl.close();
|
|
1213
1213
|
}
|
|
1214
1214
|
}
|
|
1215
|
-
/** The signed-in person's
|
|
1216
|
-
function
|
|
1215
|
+
/** The signed-in person's Cognito sub from the cached session, or null. */
|
|
1216
|
+
function currentCognitoSub() {
|
|
1217
1217
|
try {
|
|
1218
|
-
return
|
|
1218
|
+
return cognitoSubFromTokens(loadCachedTokens());
|
|
1219
1219
|
}
|
|
1220
1220
|
catch {
|
|
1221
1221
|
return null;
|
|
1222
1222
|
}
|
|
1223
1223
|
}
|
|
1224
|
-
function usageAttributionJson(cfg,
|
|
1225
|
-
const effective = effectiveUsageAttribution(cfg,
|
|
1224
|
+
function usageAttributionJson(cfg, cognitoSub) {
|
|
1225
|
+
const effective = effectiveUsageAttribution(cfg, cognitoSub);
|
|
1226
1226
|
return { countUsage: effective !== null, usageAttribution: effective };
|
|
1227
1227
|
}
|
|
1228
1228
|
async function runContextDefaultGet(opts) {
|
|
@@ -1244,7 +1244,7 @@ async function runContextDefaultGet(opts) {
|
|
|
1244
1244
|
needsChoice: !current && !disabled && memberships.length > 1,
|
|
1245
1245
|
memberships,
|
|
1246
1246
|
repairHeldWithDefault: cfg.repairHeldWithDefault === true,
|
|
1247
|
-
...usageAttributionJson(cfg,
|
|
1247
|
+
...usageAttributionJson(cfg, currentCognitoSub()),
|
|
1248
1248
|
}, null, 2));
|
|
1249
1249
|
return;
|
|
1250
1250
|
}
|
|
@@ -1257,7 +1257,7 @@ async function runContextDefaultGet(opts) {
|
|
|
1257
1257
|
return;
|
|
1258
1258
|
}
|
|
1259
1259
|
console.log(`Default company: ${current.slug}${current.uid ? ` (${current.uid})` : ""} enabled=${current.enabled} source=${source}`);
|
|
1260
|
-
console.log(effectiveUsageAttribution(cfg,
|
|
1260
|
+
console.log(effectiveUsageAttribution(cfg, currentCognitoSub())
|
|
1261
1261
|
? usageCountedLine(current.slug)
|
|
1262
1262
|
: usageNotCountedLine(current.slug));
|
|
1263
1263
|
}
|
|
@@ -1269,8 +1269,8 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
|
|
|
1269
1269
|
: opts.countUsage === false
|
|
1270
1270
|
? "opt-out"
|
|
1271
1271
|
: "keep-if-same";
|
|
1272
|
-
const
|
|
1273
|
-
const priorOptIn = effectiveUsageAttribution(readDeviceConfig({ root }),
|
|
1272
|
+
const cognitoSub = currentCognitoSub();
|
|
1273
|
+
const priorOptIn = effectiveUsageAttribution(readDeviceConfig({ root }), cognitoSub);
|
|
1274
1274
|
let token;
|
|
1275
1275
|
try {
|
|
1276
1276
|
token = await requireToken();
|
|
@@ -1311,7 +1311,7 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
|
|
|
1311
1311
|
root,
|
|
1312
1312
|
migrationCapability,
|
|
1313
1313
|
usageAttribution: usageChoice,
|
|
1314
|
-
|
|
1314
|
+
cognitoSub,
|
|
1315
1315
|
validateMembership: async (candidate) => {
|
|
1316
1316
|
if (!token) {
|
|
1317
1317
|
throw new DefaultCompanyUnavailableError(`Cannot verify membership for company "${candidate}" (no Cognito session; offline or not logged in)`);
|
|
@@ -1340,7 +1340,7 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
|
|
|
1340
1340
|
ok: true,
|
|
1341
1341
|
config: configured,
|
|
1342
1342
|
migrationWarning,
|
|
1343
|
-
...usageAttributionJson(configured,
|
|
1343
|
+
...usageAttributionJson(configured, cognitoSub),
|
|
1344
1344
|
}, null, 2));
|
|
1345
1345
|
return;
|
|
1346
1346
|
}
|
|
@@ -1352,8 +1352,8 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
|
|
|
1352
1352
|
(() => isInteractiveTerminal()))();
|
|
1353
1353
|
if (usageChoice === "keep-if-same" &&
|
|
1354
1354
|
interactive &&
|
|
1355
|
-
|
|
1356
|
-
!effectiveUsageAttribution(configured,
|
|
1355
|
+
cognitoSub &&
|
|
1356
|
+
!effectiveUsageAttribution(configured, cognitoSub) &&
|
|
1357
1357
|
stored?.uid?.startsWith("cmp_")) {
|
|
1358
1358
|
console.error(usageOptInExplainer(company));
|
|
1359
1359
|
let yes = false;
|
|
@@ -1364,9 +1364,9 @@ async function runContextDefaultSet(slug, opts, promptDeps = {}) {
|
|
|
1364
1364
|
yes = false;
|
|
1365
1365
|
}
|
|
1366
1366
|
if (yes)
|
|
1367
|
-
configured = setUsageAttribution({ companyUid: stored.uid,
|
|
1367
|
+
configured = setUsageAttribution({ companyUid: stored.uid, cognitoSub }, { root });
|
|
1368
1368
|
}
|
|
1369
|
-
console.log(effectiveUsageAttribution(configured,
|
|
1369
|
+
console.log(effectiveUsageAttribution(configured, cognitoSub)
|
|
1370
1370
|
? usageCountedLine(company)
|
|
1371
1371
|
: usageNotCountedLine(company));
|
|
1372
1372
|
if (migrationWarning)
|
|
@@ -1503,7 +1503,7 @@ async function runContextDefaultClear(opts) {
|
|
|
1503
1503
|
const hadOptIn = readDeviceConfig({ root }).usageAttribution !== undefined;
|
|
1504
1504
|
const cfg = clearDefaultCompany({ root });
|
|
1505
1505
|
if (opts.json) {
|
|
1506
|
-
console.log(JSON.stringify({ ok: true, config: cfg, ...usageAttributionJson(cfg,
|
|
1506
|
+
console.log(JSON.stringify({ ok: true, config: cfg, ...usageAttributionJson(cfg, currentCognitoSub()) }, null, 2));
|
|
1507
1507
|
return;
|
|
1508
1508
|
}
|
|
1509
1509
|
console.log("Default company cleared.");
|
|
@@ -60,6 +60,9 @@ export declare function hqCloudInstallCommand(prefix: string, version: string, p
|
|
|
60
60
|
args: string[];
|
|
61
61
|
shell: boolean;
|
|
62
62
|
};
|
|
63
|
+
export declare function readHqCloudCopy(root: string): HqCloudCopy | null;
|
|
64
|
+
/** The hq-cloud copy installed in the daemon-owned managed package root. */
|
|
65
|
+
export declare function managedHqCloud(daemonRoot: string): HqCloudCopy | null;
|
|
63
66
|
/** The copy bundled with this CLI. */
|
|
64
67
|
export declare function bundledHqCloud(): HqCloudCopy | null;
|
|
65
68
|
/** The hq-cloud the daemon should run right now. */
|
|
@@ -118,7 +118,7 @@ export function hqCloudInstallCommand(prefix, version, platform = process.platfo
|
|
|
118
118
|
const plan = buildSpawnPlan("npm", ["install", "--prefix", prefix, "--no-audit", "--no-fund", "--omit=dev", `${HQ_CLOUD_PACKAGE}@${version}`], platform);
|
|
119
119
|
return { file: plan.cmd, args: plan.args, shell: plan.shell };
|
|
120
120
|
}
|
|
121
|
-
function readHqCloudCopy(root) {
|
|
121
|
+
export function readHqCloudCopy(root) {
|
|
122
122
|
try {
|
|
123
123
|
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
124
124
|
return typeof pkg.version === "string" ? { root, version: pkg.version } : null;
|
|
@@ -127,6 +127,10 @@ function readHqCloudCopy(root) {
|
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
/** The hq-cloud copy installed in the daemon-owned managed package root. */
|
|
131
|
+
export function managedHqCloud(daemonRoot) {
|
|
132
|
+
return readHqCloudCopy(managedHqCloudRoot(daemonRoot));
|
|
133
|
+
}
|
|
130
134
|
/** The copy bundled with this CLI. */
|
|
131
135
|
export function bundledHqCloud() {
|
|
132
136
|
try {
|
|
@@ -140,12 +144,12 @@ export function bundledHqCloud() {
|
|
|
140
144
|
/** The hq-cloud the daemon should run right now. */
|
|
141
145
|
export function currentHqCloud(daemonRoot) {
|
|
142
146
|
const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
|
|
143
|
-
return pickHqCloud(bundledHqCloud(),
|
|
147
|
+
return pickHqCloud(bundledHqCloud(), managedHqCloud(daemonRoot), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
|
|
144
148
|
}
|
|
145
149
|
/** After the CLI package lease is released, only daemon-owned package files are eligible. */
|
|
146
150
|
export function currentHqCloudAfterLeaseRelease(daemonRoot, expectedVersion) {
|
|
147
151
|
const config = readDaemonConfig(path.join(daemonRoot, "config.json"));
|
|
148
|
-
const managed =
|
|
152
|
+
const managed = managedHqCloud(daemonRoot);
|
|
149
153
|
if (!managed || !hqCloudRunners(managed.root).syncRunner)
|
|
150
154
|
return null;
|
|
151
155
|
if (config.hqCloudVersion && config.hqCloudVersion !== managed.version)
|
|
@@ -4,6 +4,8 @@ export declare const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
|
|
|
4
4
|
export declare const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
|
|
5
5
|
export declare const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
|
|
6
6
|
export declare const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
|
|
7
|
+
/** Root directory where validated sync-runner bundles are selected. */
|
|
8
|
+
export declare function syncRunnerBundleStageRoot(daemonRoot: string): string;
|
|
7
9
|
/** package.json contract that the hq-cli build step emits beside the bundled bin directory. */
|
|
8
10
|
export interface SyncRunnerBundleMetadata {
|
|
9
11
|
name: "hq-cli-daemon-sync-runner";
|
|
@@ -32,6 +34,8 @@ export interface StageSyncRunnerBundleOptions {
|
|
|
32
34
|
copyFile?: typeof fs.copyFileSync;
|
|
33
35
|
renameDirectory?: (from: string, to: string) => void;
|
|
34
36
|
}
|
|
37
|
+
/** Read the validated metadata version for a runner inside its staged bundle. */
|
|
38
|
+
export declare function stagedSyncRunnerVersion(runnerPath: string): string;
|
|
35
39
|
/**
|
|
36
40
|
* Stage one validated runner bundle into an immutable daemon-owned directory.
|
|
37
41
|
* Only a completed, fsynced directory is returned; temp siblings are ignored.
|
|
@@ -9,6 +9,10 @@ export const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
|
|
|
9
9
|
export const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
|
|
10
10
|
export const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
|
|
11
11
|
export const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
|
|
12
|
+
/** Root directory where validated sync-runner bundles are selected. */
|
|
13
|
+
export function syncRunnerBundleStageRoot(daemonRoot) {
|
|
14
|
+
return path.join(daemonRoot, "bundles", "sync-runner");
|
|
15
|
+
}
|
|
12
16
|
export class SyncRunnerBundleStageError extends Error {
|
|
13
17
|
code;
|
|
14
18
|
constructor(code, message, options) {
|
|
@@ -55,6 +59,11 @@ function readMetadata(file) {
|
|
|
55
59
|
}
|
|
56
60
|
return metadata;
|
|
57
61
|
}
|
|
62
|
+
/** Read the validated metadata version for a runner inside its staged bundle. */
|
|
63
|
+
export function stagedSyncRunnerVersion(runnerPath) {
|
|
64
|
+
const bundleDirectory = path.dirname(path.dirname(runnerPath));
|
|
65
|
+
return readMetadata(path.join(bundleDirectory, SYNC_RUNNER_BUNDLE_METADATA_FILE)).version;
|
|
66
|
+
}
|
|
58
67
|
function hqCloudRange() {
|
|
59
68
|
const cliPackagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../package.json");
|
|
60
69
|
try {
|
|
@@ -138,7 +147,7 @@ export function stageSyncRunnerBundle(options) {
|
|
|
138
147
|
if (sha256(sourceRunner) !== metadata.bundleSha256) {
|
|
139
148
|
fail("HASH_MISMATCH", "The sync-runner bundle sha256 does not match its sidecar");
|
|
140
149
|
}
|
|
141
|
-
const stageRoot =
|
|
150
|
+
const stageRoot = syncRunnerBundleStageRoot(options.daemonRoot);
|
|
142
151
|
const selectedDirectory = path.join(stageRoot, stagedLeaf(metadata));
|
|
143
152
|
const existing = readStaged(selectedDirectory, metadata, range);
|
|
144
153
|
if (existing)
|
|
@@ -44,6 +44,8 @@ export declare function watchRunnerOwnerStatusPath(hqRoot: string, home?: string
|
|
|
44
44
|
export declare function readWatchRunnerOwnerStatus(statusPath: string): WatchRunnerOwnerStatus | null;
|
|
45
45
|
/** Match a live process command line to the hq-cloud watch runner executable. */
|
|
46
46
|
export declare function isSyncRunnerCommandLine(commandLine: string): boolean;
|
|
47
|
+
/** Return the script path from a command line already verified as a sync runner. */
|
|
48
|
+
export declare function syncRunnerPathFromCommandLine(commandLine: string): string | null;
|
|
47
49
|
/** Return a clear wait reason when an unleased runner already targets this HQ root. */
|
|
48
50
|
export declare function syncRunnerUnleasedBlockReason(hqRoot: string, processes: readonly SyncRunnerProcess[], ownerPid: number | null, platform?: NodeJS.Platform, daemonRunnerPids?: ReadonlySet<number>): string | undefined;
|
|
49
51
|
/** Read complete process command lines so root matching can include trailing arguments. */
|
|
@@ -59,6 +59,14 @@ export function readWatchRunnerOwnerStatus(statusPath) {
|
|
|
59
59
|
export function isSyncRunnerCommandLine(commandLine) {
|
|
60
60
|
return SYNC_RUNNER_COMMAND.test(commandLine);
|
|
61
61
|
}
|
|
62
|
+
/** Return the script path from a command line already verified as a sync runner. */
|
|
63
|
+
export function syncRunnerPathFromCommandLine(commandLine) {
|
|
64
|
+
if (!isSyncRunnerCommandLine(commandLine))
|
|
65
|
+
return null;
|
|
66
|
+
const args = commandLineArgs(commandLine);
|
|
67
|
+
return args.find((arg) => (path.isAbsolute(arg) || path.win32.isAbsolute(arg))
|
|
68
|
+
&& /(?:^|[\\/])(?:sync-runner\.js|hq-sync-runner(?:\.(?:cmd|exe))?)$/i.test(arg)) ?? null;
|
|
69
|
+
}
|
|
62
70
|
function commandLineArgs(commandLine) {
|
|
63
71
|
const args = [];
|
|
64
72
|
let current = "";
|
|
@@ -56,15 +56,16 @@ export interface UsageAttributionOptIn {
|
|
|
56
56
|
mode: "default-company";
|
|
57
57
|
companyUid: string;
|
|
58
58
|
/**
|
|
59
|
-
* The signed-in person who consented: the `
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* The signed-in person who consented: the Cognito `sub` claim of the cached
|
|
60
|
+
* person session. Consent never carries to another account on the same OS
|
|
61
|
+
* user. Older records that carry only `personUid` are treated as not opted
|
|
62
|
+
* in (fail closed).
|
|
62
63
|
*/
|
|
63
|
-
|
|
64
|
+
cognitoSub: string;
|
|
64
65
|
optedInAt: string;
|
|
65
66
|
}
|
|
66
|
-
/** A
|
|
67
|
-
export declare function
|
|
67
|
+
/** A Cognito `sub` claim: any non-empty string (after trimming). */
|
|
68
|
+
export declare function isCognitoSub(value: unknown): value is string;
|
|
68
69
|
/** How `setDefaultCompany` treats the usage-attribution opt-in. */
|
|
69
70
|
export type UsageAttributionChoice =
|
|
70
71
|
/** Write an opt-in for the newly set company (requires its uid). */
|
|
@@ -113,10 +114,10 @@ export interface DeviceConfigDeps {
|
|
|
113
114
|
*/
|
|
114
115
|
usageAttribution?: UsageAttributionChoice;
|
|
115
116
|
/**
|
|
116
|
-
* The signed-in person (
|
|
117
|
+
* The signed-in person (Cognito `sub`). Required to write an opt-in; an existing
|
|
117
118
|
* opt-in is kept only when it was given by this person.
|
|
118
119
|
*/
|
|
119
|
-
|
|
120
|
+
cognitoSub?: string | null;
|
|
120
121
|
}
|
|
121
122
|
/** Acquire an interprocess config lock; exported for direct lock regression tests. */
|
|
122
123
|
export declare function acquireDeviceConfigLock(root: string, options?: {
|
|
@@ -130,7 +131,7 @@ export declare function sanitizeUsageAttribution(raw: unknown): UsageAttribution
|
|
|
130
131
|
* default company, and given by the signed-in person. Mirrors the hq-cloud
|
|
131
132
|
* reader. No signed-in person means no opt-in.
|
|
132
133
|
*/
|
|
133
|
-
export declare function effectiveUsageAttribution(cfg: Pick<WorkContextDeviceConfig, "defaultCompany" | "usageAttribution">,
|
|
134
|
+
export declare function effectiveUsageAttribution(cfg: Pick<WorkContextDeviceConfig, "defaultCompany" | "usageAttribution">, cognitoSub: string | null | undefined): UsageAttributionOptIn | null;
|
|
134
135
|
/** Injection points for assertSafeConfigPath (tests only). */
|
|
135
136
|
export interface SafeConfigPathIo {
|
|
136
137
|
platform?: NodeJS.Platform;
|
|
@@ -162,14 +163,14 @@ export declare function setDefaultCompany(slug: string, deps: DeviceConfigDeps):
|
|
|
162
163
|
*/
|
|
163
164
|
export declare function setUsageAttribution(optIn: {
|
|
164
165
|
companyUid: string;
|
|
165
|
-
|
|
166
|
+
cognitoSub: string;
|
|
166
167
|
} | null, deps: Pick<DeviceConfigDeps, "root" | "now">): WorkContextDeviceConfig;
|
|
167
168
|
/**
|
|
168
|
-
* Drop the usage-attribution opt-in unless it was given by `
|
|
169
|
+
* Drop the usage-attribution opt-in unless it was given by `cognitoSub`. Called
|
|
169
170
|
* on login (identity may have changed) and logout (null: always drop). A
|
|
170
171
|
* config with no opt-in, or one already matching, is left untouched.
|
|
171
172
|
*/
|
|
172
|
-
export declare function retainUsageAttributionForPerson(
|
|
173
|
+
export declare function retainUsageAttributionForPerson(cognitoSub: string | null | undefined, deps: Pick<DeviceConfigDeps, "root" | "now">): boolean;
|
|
173
174
|
export declare function clearDefaultCompany(deps: DeviceConfigDeps): WorkContextDeviceConfig;
|
|
174
175
|
/**
|
|
175
176
|
* Read the persisted company mapping for a repo identity key (gap 4).
|
|
@@ -9,9 +9,9 @@ import { DefaultCompanyUnavailableError, UnsafeConfigPathError, WorkContextError
|
|
|
9
9
|
import { workContextConfigPath } from "./paths.js";
|
|
10
10
|
import { assertWindowsOwnerOnlyAcl } from "./windows-acl.js";
|
|
11
11
|
const DEVICE_CONFIG_SCHEMA_VERSION = 1;
|
|
12
|
-
/** A
|
|
13
|
-
export function
|
|
14
|
-
return typeof value === "string" &&
|
|
12
|
+
/** A Cognito `sub` claim: any non-empty string (after trimming). */
|
|
13
|
+
export function isCognitoSub(value) {
|
|
14
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
15
15
|
}
|
|
16
16
|
const CONFIG_LOCK_TIMEOUT_MS = 5_000;
|
|
17
17
|
const CONFIG_LOCK_RETRY_MS = 10;
|
|
@@ -158,24 +158,24 @@ export function sanitizeUsageAttribution(raw) {
|
|
|
158
158
|
if (v.mode !== "default-company")
|
|
159
159
|
return undefined;
|
|
160
160
|
const companyUid = typeof v.companyUid === "string" ? v.companyUid.trim() : "";
|
|
161
|
-
const
|
|
161
|
+
const cognitoSub = typeof v.cognitoSub === "string" ? v.cognitoSub.trim() : "";
|
|
162
162
|
const optedInAt = typeof v.optedInAt === "string" ? v.optedInAt.trim() : "";
|
|
163
163
|
if (!companyUid.startsWith("cmp_") ||
|
|
164
|
-
!
|
|
164
|
+
!isCognitoSub(cognitoSub) ||
|
|
165
165
|
!optedInAt ||
|
|
166
166
|
!Number.isFinite(Date.parse(optedInAt))) {
|
|
167
167
|
return undefined;
|
|
168
168
|
}
|
|
169
|
-
return { mode: "default-company", companyUid,
|
|
169
|
+
return { mode: "default-company", companyUid, cognitoSub, optedInAt };
|
|
170
170
|
}
|
|
171
171
|
/**
|
|
172
172
|
* The opt-in that is in effect: present, for the uid of the current enabled
|
|
173
173
|
* default company, and given by the signed-in person. Mirrors the hq-cloud
|
|
174
174
|
* reader. No signed-in person means no opt-in.
|
|
175
175
|
*/
|
|
176
|
-
export function effectiveUsageAttribution(cfg,
|
|
176
|
+
export function effectiveUsageAttribution(cfg, cognitoSub) {
|
|
177
177
|
const opt = cfg.usageAttribution;
|
|
178
|
-
if (!opt || !
|
|
178
|
+
if (!opt || !isCognitoSub(cognitoSub) || opt.cognitoSub !== cognitoSub)
|
|
179
179
|
return null;
|
|
180
180
|
const def = cfg.defaultCompany;
|
|
181
181
|
if (!opt || !def || def.enabled !== true || def.source === "disabled")
|
|
@@ -332,27 +332,27 @@ export async function setDefaultCompany(slug, deps) {
|
|
|
332
332
|
defaultCompany.uid = resolvedUid;
|
|
333
333
|
const choice = deps.usageAttribution ?? "keep-if-same";
|
|
334
334
|
let usageAttribution;
|
|
335
|
-
const
|
|
335
|
+
const cognitoSub = isCognitoSub(deps.cognitoSub) ? deps.cognitoSub : undefined;
|
|
336
336
|
if (choice === "opt-in") {
|
|
337
337
|
if (!resolvedUid?.startsWith("cmp_")) {
|
|
338
338
|
throw new WorkContextError("UsageAttributionUnavailable", `Cannot count usage toward "${trimmed}": its company uid is unknown`);
|
|
339
339
|
}
|
|
340
|
-
if (!
|
|
340
|
+
if (!cognitoSub) {
|
|
341
341
|
throw new WorkContextError("UsageAttributionUnavailable", `Cannot count usage toward "${trimmed}": the signed-in person could not be determined`);
|
|
342
342
|
}
|
|
343
343
|
usageAttribution = {
|
|
344
344
|
mode: "default-company",
|
|
345
345
|
companyUid: resolvedUid,
|
|
346
|
-
|
|
346
|
+
cognitoSub,
|
|
347
347
|
optedInAt: now().toISOString(),
|
|
348
348
|
};
|
|
349
349
|
}
|
|
350
350
|
else if (choice === "keep-if-same") {
|
|
351
351
|
usageAttribution =
|
|
352
352
|
resolvedUid &&
|
|
353
|
-
|
|
353
|
+
cognitoSub &&
|
|
354
354
|
prior.usageAttribution?.companyUid === resolvedUid &&
|
|
355
|
-
prior.usageAttribution.
|
|
355
|
+
prior.usageAttribution.cognitoSub === cognitoSub
|
|
356
356
|
? prior.usageAttribution
|
|
357
357
|
: undefined;
|
|
358
358
|
}
|
|
@@ -388,13 +388,13 @@ export function setUsageAttribution(optIn, deps) {
|
|
|
388
388
|
def.source !== "chosen" ||
|
|
389
389
|
!def.uid?.startsWith("cmp_") ||
|
|
390
390
|
def.uid !== optIn.companyUid ||
|
|
391
|
-
!
|
|
391
|
+
!isCognitoSub(optIn.cognitoSub)) {
|
|
392
392
|
throw new WorkContextError("UsageAttributionUnavailable", "Usage can only be counted toward a default company you chose on this device");
|
|
393
393
|
}
|
|
394
394
|
usageAttribution = {
|
|
395
395
|
mode: "default-company",
|
|
396
396
|
companyUid: def.uid,
|
|
397
|
-
|
|
397
|
+
cognitoSub: optIn.cognitoSub,
|
|
398
398
|
optedInAt: now().toISOString(),
|
|
399
399
|
};
|
|
400
400
|
}
|
|
@@ -413,11 +413,11 @@ export function setUsageAttribution(optIn, deps) {
|
|
|
413
413
|
});
|
|
414
414
|
}
|
|
415
415
|
/**
|
|
416
|
-
* Drop the usage-attribution opt-in unless it was given by `
|
|
416
|
+
* Drop the usage-attribution opt-in unless it was given by `cognitoSub`. Called
|
|
417
417
|
* on login (identity may have changed) and logout (null: always drop). A
|
|
418
418
|
* config with no opt-in, or one already matching, is left untouched.
|
|
419
419
|
*/
|
|
420
|
-
export function retainUsageAttributionForPerson(
|
|
420
|
+
export function retainUsageAttributionForPerson(cognitoSub, deps) {
|
|
421
421
|
if (!fs.existsSync(workContextConfigPath(deps.root)))
|
|
422
422
|
return false;
|
|
423
423
|
return withDeviceConfigLock(deps.root, () => {
|
|
@@ -425,7 +425,7 @@ export function retainUsageAttributionForPerson(personUid, deps) {
|
|
|
425
425
|
const opt = prior.usageAttribution;
|
|
426
426
|
if (!opt)
|
|
427
427
|
return false;
|
|
428
|
-
if (
|
|
428
|
+
if (isCognitoSub(cognitoSub) && opt.cognitoSub === cognitoSub)
|
|
429
429
|
return false;
|
|
430
430
|
const now = deps.now ?? (() => new Date());
|
|
431
431
|
const { usageAttribution: _dropped, ...rest } = prior;
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
* Signed-in person identity for the usage-attribution opt-in.
|
|
3
3
|
*
|
|
4
4
|
* Source: the cached Cognito person session (`~/.hq/cognito-tokens.json`,
|
|
5
|
-
* read through `loadCachedTokens`). The
|
|
6
|
-
* claim of the ID token (falling back to the access token)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* read through `loadCachedTokens`). The identity key is the Cognito `sub`
|
|
6
|
+
* claim of the ID token (falling back to the access token); it must be a
|
|
7
|
+
* non-empty string. Human sessions carry no `custom:entityUid` claim (hq-pro
|
|
8
|
+
* sets it only on agent and outpost machine identities), so it is not used.
|
|
9
|
+
* An agent or outpost session has its own Cognito user and therefore a
|
|
10
|
+
* different `sub`, so it could never match a person's opt-in; a token that
|
|
11
|
+
* names a machine identity (`custom:entityType` agent/outpost, or a
|
|
12
|
+
* non-`prs_` `custom:entityUid`) is refused outright. Claims are read
|
|
13
|
+
* without verification; this decides only whether local consent belongs to
|
|
14
|
+
* the account that is signed in, never authorization.
|
|
11
15
|
*/
|
|
12
|
-
export declare function
|
|
16
|
+
export declare function cognitoSubFromTokens(tokens: {
|
|
13
17
|
idToken?: string;
|
|
14
18
|
accessToken?: string;
|
|
15
19
|
} | null | undefined): string | null;
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
* Signed-in person identity for the usage-attribution opt-in.
|
|
3
3
|
*
|
|
4
4
|
* Source: the cached Cognito person session (`~/.hq/cognito-tokens.json`,
|
|
5
|
-
* read through `loadCachedTokens`). The
|
|
6
|
-
* claim of the ID token (falling back to the access token)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* read through `loadCachedTokens`). The identity key is the Cognito `sub`
|
|
6
|
+
* claim of the ID token (falling back to the access token); it must be a
|
|
7
|
+
* non-empty string. Human sessions carry no `custom:entityUid` claim (hq-pro
|
|
8
|
+
* sets it only on agent and outpost machine identities), so it is not used.
|
|
9
|
+
* An agent or outpost session has its own Cognito user and therefore a
|
|
10
|
+
* different `sub`, so it could never match a person's opt-in; a token that
|
|
11
|
+
* names a machine identity (`custom:entityType` agent/outpost, or a
|
|
12
|
+
* non-`prs_` `custom:entityUid`) is refused outright. Claims are read
|
|
13
|
+
* without verification; this decides only whether local consent belongs to
|
|
14
|
+
* the account that is signed in, never authorization.
|
|
11
15
|
*/
|
|
12
|
-
import {
|
|
16
|
+
import { isCognitoSub } from "./config.js";
|
|
13
17
|
function claimsOf(token) {
|
|
14
18
|
if (typeof token !== "string")
|
|
15
19
|
return null;
|
|
@@ -26,13 +30,22 @@ function claimsOf(token) {
|
|
|
26
30
|
return null;
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
|
-
export function
|
|
33
|
+
export function cognitoSubFromTokens(tokens) {
|
|
30
34
|
if (!tokens)
|
|
31
35
|
return null;
|
|
32
36
|
for (const token of [tokens.idToken, tokens.accessToken]) {
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
|
|
37
|
+
const claims = claimsOf(token);
|
|
38
|
+
if (!claims)
|
|
39
|
+
continue;
|
|
40
|
+
const entityType = claims["custom:entityType"];
|
|
41
|
+
const entityUid = claims["custom:entityUid"];
|
|
42
|
+
if (entityType === "agent" || entityType === "outpost")
|
|
43
|
+
return null;
|
|
44
|
+
if (typeof entityUid === "string" && !entityUid.startsWith("prs_"))
|
|
45
|
+
return null;
|
|
46
|
+
const sub = claims.sub;
|
|
47
|
+
if (isCognitoSub(sub))
|
|
48
|
+
return sub.trim();
|
|
36
49
|
}
|
|
37
50
|
return null;
|
|
38
51
|
}
|